| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { randomUUID } from "crypto"; |
|
|
| |
|
|
| export type TaskState = "submitted" | "working" | "completed" | "failed" | "cancelled"; |
|
|
| export interface TaskInput { |
| skill: string; |
| messages: Array<{ role: string; content: string }>; |
| metadata?: Record<string, unknown>; |
| } |
|
|
| export interface TaskArtifact { |
| type: "text" | "json" | "error"; |
| content: string; |
| } |
|
|
| export interface TaskEvent { |
| timestamp: string; |
| state: TaskState; |
| message?: string; |
| } |
|
|
| export interface A2ATask { |
| id: string; |
| skill: string; |
| state: TaskState; |
| input: TaskInput; |
| artifacts: TaskArtifact[]; |
| events: TaskEvent[]; |
| metadata: Record<string, unknown>; |
| createdAt: string; |
| updatedAt: string; |
| expiresAt: string; |
| } |
|
|
| export interface TaskListFilter { |
| state?: TaskState; |
| skill?: string; |
| limit?: number; |
| offset?: number; |
| } |
|
|
| export interface A2ATaskStats { |
| counts: Record<TaskState, number>; |
| total: number; |
| activeStreams: number; |
| lastTaskAt: string | null; |
| } |
|
|
| |
|
|
| const VALID_TRANSITIONS: Record<TaskState, TaskState[]> = { |
| submitted: ["working", "failed", "cancelled"], |
| working: ["completed", "failed", "cancelled"], |
| completed: [], |
| failed: [], |
| cancelled: [], |
| }; |
|
|
| |
|
|
| export class A2ATaskManager { |
| private tasks = new Map<string, A2ATask>(); |
| private readonly ttlMs: number; |
| private cleanupInterval: ReturnType<typeof setInterval>; |
| private activeStreams = 0; |
|
|
| constructor(ttlMinutes: number = 5) { |
| this.ttlMs = ttlMinutes * 60 * 1000; |
| this.cleanupInterval = setInterval(() => this.cleanupExpired(), 60_000); |
| if ( |
| this.cleanupInterval && |
| typeof this.cleanupInterval === "object" && |
| "unref" in this.cleanupInterval |
| ) { |
| (this.cleanupInterval as { unref?: () => void }).unref?.(); |
| } |
| } |
|
|
| createTask(input: TaskInput): A2ATask { |
| const now = new Date(); |
| const task: A2ATask = { |
| id: randomUUID(), |
| skill: input.skill, |
| state: "submitted", |
| input, |
| artifacts: [], |
| events: [{ timestamp: now.toISOString(), state: "submitted" }], |
| metadata: input.metadata || {}, |
| createdAt: now.toISOString(), |
| updatedAt: now.toISOString(), |
| expiresAt: new Date(now.getTime() + this.ttlMs).toISOString(), |
| }; |
| this.tasks.set(task.id, task); |
| return task; |
| } |
|
|
| getTask(taskId: string): A2ATask | undefined { |
| const task = this.tasks.get(taskId); |
| if (task && new Date(task.expiresAt) < new Date()) { |
| if (task.state === "submitted" || task.state === "working") { |
| this.updateTask(taskId, "failed", undefined, "Task expired"); |
| } |
| } |
| return this.tasks.get(taskId); |
| } |
|
|
| updateTask( |
| taskId: string, |
| state: TaskState, |
| artifacts?: TaskArtifact[], |
| message?: string |
| ): A2ATask { |
| const task = this.tasks.get(taskId); |
| if (!task) throw new Error(`Task ${taskId} not found`); |
|
|
| const valid = VALID_TRANSITIONS[task.state]; |
| if (!valid.includes(state)) { |
| throw new Error(`Invalid transition: ${task.state} → ${state}`); |
| } |
|
|
| const now = new Date().toISOString(); |
| task.state = state; |
| task.updatedAt = now; |
| task.events.push({ timestamp: now, state, message }); |
| if (artifacts) task.artifacts.push(...artifacts); |
|
|
| return task; |
| } |
|
|
| cancelTask(taskId: string): A2ATask { |
| return this.updateTask(taskId, "cancelled", undefined, "Cancelled by client"); |
| } |
|
|
| countTasks(filter?: Pick<TaskListFilter, "state" | "skill">): number { |
| let tasks = [...this.tasks.values()]; |
| if (filter?.state) tasks = tasks.filter((t) => t.state === filter.state); |
| if (filter?.skill) tasks = tasks.filter((t) => t.skill === filter.skill); |
| return tasks.length; |
| } |
|
|
| listTasks(filter?: TaskListFilter): A2ATask[] { |
| let tasks = [...this.tasks.values()]; |
| if (filter?.state) tasks = tasks.filter((t) => t.state === filter.state); |
| if (filter?.skill) tasks = tasks.filter((t) => t.skill === filter.skill); |
| tasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); |
| const offset = Math.max(0, filter?.offset || 0); |
| const limit = |
| typeof filter?.limit === "number" && Number.isFinite(filter.limit) |
| ? Math.max(1, Math.floor(filter.limit)) |
| : 50; |
| return tasks.slice(offset, offset + limit); |
| } |
|
|
| beginStream() { |
| this.activeStreams += 1; |
| } |
|
|
| endStream() { |
| this.activeStreams = Math.max(0, this.activeStreams - 1); |
| } |
|
|
| getStats(): A2ATaskStats { |
| const counts: Record<TaskState, number> = { |
| submitted: 0, |
| working: 0, |
| completed: 0, |
| failed: 0, |
| cancelled: 0, |
| }; |
|
|
| let lastTaskAt: string | null = null; |
| for (const task of this.tasks.values()) { |
| counts[task.state] += 1; |
| const updatedAt = new Date(task.updatedAt).getTime(); |
| if (!Number.isFinite(updatedAt)) continue; |
| if (!lastTaskAt || updatedAt > new Date(lastTaskAt).getTime()) { |
| lastTaskAt = task.updatedAt; |
| } |
| } |
|
|
| return { |
| counts, |
| total: this.tasks.size, |
| activeStreams: this.activeStreams, |
| lastTaskAt, |
| }; |
| } |
|
|
| private cleanupExpired() { |
| const now = new Date(); |
| for (const [id, task] of this.tasks) { |
| if ( |
| new Date(task.expiresAt) < now && |
| task.state !== "completed" && |
| task.state !== "failed" && |
| task.state !== "cancelled" |
| ) { |
| task.state = "failed"; |
| task.updatedAt = now.toISOString(); |
| task.events.push({ timestamp: now.toISOString(), state: "failed", message: "TTL expired" }); |
| } |
| |
| if ( |
| ["completed", "failed", "cancelled"].includes(task.state) && |
| now.getTime() - new Date(task.updatedAt).getTime() > this.ttlMs * 2 |
| ) { |
| this.tasks.delete(id); |
| } |
| } |
| } |
|
|
| destroy() { |
| clearInterval(this.cleanupInterval); |
| } |
| } |
|
|
| |
| const globalForA2A = globalThis as unknown as { _a2aTaskManager?: A2ATaskManager }; |
|
|
| export function getTaskManager(): A2ATaskManager { |
| if (!globalForA2A._a2aTaskManager) { |
| globalForA2A._a2aTaskManager = new A2ATaskManager(); |
| } |
| return globalForA2A._a2aTaskManager; |
| } |
|
|