-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmanager.ts
More file actions
131 lines (114 loc) · 4.19 KB
/
Copy pathmanager.ts
File metadata and controls
131 lines (114 loc) · 4.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Background tasks — the agent spawns a long-running sub-agent that runs
// concurrently with the main turn; TaskList/Get/Output/Stop/Monitor inspect
// and control it. Spec: docs/DEVELOPMENT_PLAN.md §3.15.3 (TaskCreate family).
//
// The manager is runner-agnostic: TaskCreate hands it a `runner` (the agent
// loop wires one backed by runSubAgent) which it invokes WITHOUT blocking the
// caller, piping streamed output into the task buffer and flipping status when
// it settles.
export type TaskStatus = 'running' | 'completed' | 'failed' | 'stopped';
export interface Task {
id: string;
description: string;
status: TaskStatus;
/** Accumulated output (streamed chunks for a live runner, else final text). */
output: string;
createdAt: string;
finishedAt?: string;
}
export interface TaskRunHandle {
/** Resolves with the task's final text when the run completes. */
done: Promise<string>;
/** Abort the run (best-effort). */
abort: () => void;
/** Register a streamed-output sink (optional — runners may not stream). */
onChunk?: (cb: (chunk: string) => void) => void;
}
export interface CreateTaskSpec {
description: string;
prompt: string;
agentType?: string;
}
/** Runner the host supplies: starts the work and returns a handle. */
export type TaskRunner = (spec: CreateTaskSpec) => TaskRunHandle;
export class TaskManager {
private readonly tasks = new Map<string, Task>();
private readonly handles = new Map<string, TaskRunHandle>();
private seq = 0;
constructor(private runner: TaskRunner) {}
/**
* Replace the runner used for subsequent `create()` calls. Lets a host own a
* long-lived (e.g. REPL session-scoped) manager while the agent loop attaches
* its run-local sub-agent runner each turn. Tasks already started are
* unaffected — their handle is captured at `create()` time.
*/
setRunner(runner: TaskRunner): void {
this.runner = runner;
}
private newId(): string {
return `task-${(this.seq++).toString(36)}`;
}
/** Start a background task; returns the task record immediately. */
create(spec: CreateTaskSpec): Task {
const id = this.newId();
const task: Task = {
id,
description: spec.description,
status: 'running',
output: '',
createdAt: new Date().toISOString(),
};
this.tasks.set(id, task);
const handle = this.runner(spec);
this.handles.set(id, handle);
handle.onChunk?.((chunk) => {
const t = this.tasks.get(id);
if (t && t.status === 'running') t.output += chunk;
});
handle.done.then(
(text) => this.settle(id, 'completed', text),
(err) => this.settle(id, 'failed', `Error: ${(err as Error).message}`),
);
return { ...task };
}
private settle(id: string, status: TaskStatus, finalText: string): void {
const t = this.tasks.get(id);
if (!t || t.status !== 'running') return; // already stopped/settled
// For non-streaming runners output is empty until now → use the final text.
if (!t.output) t.output = finalText;
t.status = status;
t.finishedAt = new Date().toISOString();
}
get(id: string): Task | undefined {
const t = this.tasks.get(id);
return t ? { ...t } : undefined;
}
list(): Task[] {
return [...this.tasks.values()].map((t) => ({ ...t }));
}
output(id: string): string | undefined {
return this.tasks.get(id)?.output;
}
/** Abort a running task. Returns false if unknown or already finished. */
stop(id: string): boolean {
const t = this.tasks.get(id);
if (!t || t.status !== 'running') return false;
this.handles.get(id)?.abort();
t.status = 'stopped';
t.finishedAt = new Date().toISOString();
return true;
}
/** Update mutable task metadata (currently the description). */
update(id: string, patch: { description?: string }): boolean {
const t = this.tasks.get(id);
if (!t) return false;
if (patch.description !== undefined) t.description = patch.description;
return true;
}
/** Await a task's completion (resolves immediately if already settled). */
async wait(id: string): Promise<Task | undefined> {
const handle = this.handles.get(id);
if (handle) await handle.done.catch(() => undefined);
return this.get(id);
}
}