@xl0/pi-lovely-agents 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +19 -23
- package/extensions/lovely-agents/agent.ts +146 -452
- package/extensions/lovely-agents/bash.ts +61 -110
- package/extensions/lovely-agents/child-session.ts +22 -67
- package/extensions/lovely-agents/config.ts +18 -19
- package/extensions/lovely-agents/coordinator.ts +51 -186
- package/extensions/lovely-agents/definitions.ts +31 -23
- package/extensions/lovely-agents/index.ts +75 -155
- package/extensions/lovely-agents/lifecycle.ts +45 -170
- package/extensions/lovely-agents/management.ts +108 -384
- package/extensions/lovely-agents/notifications.ts +5 -47
- package/{scripts/prune-tasks.ts → extensions/lovely-agents/prune.ts} +30 -76
- package/extensions/lovely-agents/rendering.ts +11 -24
- package/extensions/lovely-agents/state.ts +119 -246
- package/extensions/lovely-agents/task-panel.ts +19 -59
- package/extensions/lovely-agents/tools.ts +50 -127
- package/extensions/lovely-agents/utils.ts +27 -0
- package/node_modules/@xl0/pi-lovely-config/CHANGELOG.md +12 -0
- package/node_modules/@xl0/pi-lovely-config/README.md +44 -2
- package/node_modules/@xl0/pi-lovely-config/package.json +3 -2
- package/node_modules/@xl0/pi-lovely-config/src/config.ts +87 -45
- package/node_modules/@xl0/pi-lovely-config/src/ui.ts +48 -21
- package/package.json +10 -10
- package/skills/agent/SKILL.md +8 -4
- package/skills/agent-creator/SKILL.md +1 -1
- package/extensions/lovely-agents/provider-limits.ts +0 -13
- package/extensions/lovely-agents/storage.ts +0 -204
|
@@ -12,18 +12,10 @@ import {
|
|
|
12
12
|
} from "./child-session.js"
|
|
13
13
|
import type { AgentsConfig } from "./config.js"
|
|
14
14
|
import { resolveConfiguredModels } from "./config.js"
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
getAgentCoordinator,
|
|
18
|
-
type ModelTuple,
|
|
19
|
-
type ResidentAgent,
|
|
20
|
-
type ResidentInputOptions,
|
|
21
|
-
type ResidentInputResult
|
|
22
|
-
} from "./coordinator.js"
|
|
23
|
-
import { type AgentDefinition, discoverAgentDefinitions } from "./definitions.js"
|
|
15
|
+
import { getAgentCoordinator, type ResidentAgent, type ResidentInputOptions, type ResidentInputResult } from "./coordinator.js"
|
|
16
|
+
import { type AgentDefinition, discoverAgentDefinitions, projectResourcesTrusted } from "./definitions.js"
|
|
24
17
|
import { discardTask, stopOwnedTaskTree, stopTask } from "./lifecycle.js"
|
|
25
18
|
import { appendTaskNotification, deliverTaskNotifications, prepareTaskNotification } from "./notifications.js"
|
|
26
|
-
import { isProviderLimitError } from "./provider-limits.js"
|
|
27
19
|
import { renderExpandableResult } from "./rendering.js"
|
|
28
20
|
import {
|
|
29
21
|
type AgentTaskMetadata,
|
|
@@ -36,10 +28,10 @@ import {
|
|
|
36
28
|
MAX_AGENT_LABEL_BYTES,
|
|
37
29
|
MAX_QUEUED_FOLLOWUPS,
|
|
38
30
|
mutateTaskMetadata,
|
|
31
|
+
RETAINED_OUTPUT_MAX_WAIT_MS,
|
|
39
32
|
readRetainedOutput,
|
|
40
33
|
readTaskMetadata,
|
|
41
34
|
reserveTaskStorage,
|
|
42
|
-
retainedOutputSnapshot,
|
|
43
35
|
retainedPaths,
|
|
44
36
|
TASK_METADATA_VERSION,
|
|
45
37
|
TASK_REFERENCE_PATTERN,
|
|
@@ -49,7 +41,7 @@ import {
|
|
|
49
41
|
writeTaskMetadata,
|
|
50
42
|
writeTaskProgress
|
|
51
43
|
} from "./state.js"
|
|
52
|
-
import {
|
|
44
|
+
import { deferred } from "./utils.js"
|
|
53
45
|
|
|
54
46
|
const ThinkingLevel = Type.Union([
|
|
55
47
|
Type.Literal("off"),
|
|
@@ -61,24 +53,6 @@ const ThinkingLevel = Type.Union([
|
|
|
61
53
|
Type.Literal("max")
|
|
62
54
|
])
|
|
63
55
|
|
|
64
|
-
type RecoveryMode = "automatic" | "manual" | "stop"
|
|
65
|
-
type SuspendedRuntime = {
|
|
66
|
-
wakeProviderRecovery(mode: RecoveryMode): boolean
|
|
67
|
-
}
|
|
68
|
-
const SUSPENDED_RUNTIMES_SYMBOL = Symbol.for("@xl0/pi-lovely-agents/suspended-runtimes/v1")
|
|
69
|
-
|
|
70
|
-
/** Opens one tuple and wakes every process-resident run suspended on it. */
|
|
71
|
-
export function recoverProviderTuple(tuple: ModelTuple): number {
|
|
72
|
-
getAgentCoordinator().openTuple(tuple)
|
|
73
|
-
const runtimes = suspendedRuntimes().get(modelTupleKey(tuple))
|
|
74
|
-
if (!runtimes) return 0
|
|
75
|
-
let recovered = 0
|
|
76
|
-
for (const runtime of [...runtimes]) {
|
|
77
|
-
if (runtime.wakeProviderRecovery("automatic")) recovered++
|
|
78
|
-
}
|
|
79
|
-
return recovered
|
|
80
|
-
}
|
|
81
|
-
|
|
82
56
|
export type AgentCreationResult = {
|
|
83
57
|
id: string
|
|
84
58
|
run: number
|
|
@@ -93,7 +67,6 @@ export type AgentCreationResult = {
|
|
|
93
67
|
detached: boolean
|
|
94
68
|
queuedFollowUps: number
|
|
95
69
|
output: Awaited<ReturnType<typeof readRetainedOutput>>
|
|
96
|
-
tasks: Awaited<ReturnType<typeof loadTaskList>>["details"]
|
|
97
70
|
}
|
|
98
71
|
|
|
99
72
|
export type TaskInputResult = {
|
|
@@ -106,27 +79,21 @@ export type TaskInputResult = {
|
|
|
106
79
|
state: TaskMetadata["state"]
|
|
107
80
|
latestOutcome: TaskMetadata["latestOutcome"]
|
|
108
81
|
queuedFollowUps: number
|
|
109
|
-
output?: Awaited<ReturnType<typeof readRetainedOutput>>
|
|
110
82
|
}
|
|
111
83
|
|
|
112
84
|
export type AgentToolOptions = {
|
|
113
85
|
getConfig: () => AgentsConfig
|
|
114
86
|
createChild?: typeof createChildSession
|
|
115
87
|
getAgentDir?: () => string
|
|
116
|
-
canDelegate?: () => boolean
|
|
117
|
-
/** Input-schema visibility from allowed producers plus retained owned kinds; not execution gates. */
|
|
118
|
-
agentInputEnabled?: () => boolean
|
|
119
|
-
bashInputEnabled?: () => boolean
|
|
120
88
|
}
|
|
121
89
|
|
|
90
|
+
/** Schemas and descriptions are static; config and depth limits are enforced at execution. */
|
|
122
91
|
export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions): void {
|
|
123
|
-
const background = options.getConfig().backgroundAgents
|
|
124
92
|
pi.registerTool({
|
|
125
93
|
name: "agent",
|
|
126
94
|
label: "Agent",
|
|
127
|
-
description:
|
|
128
|
-
|
|
129
|
-
: "Create a durable Lovely Agent session and wait for its terminal result. Cancellation stops the accepted work.",
|
|
95
|
+
description:
|
|
96
|
+
"Create a durable Lovely Agent session and start its initial run. The call waits up to waitMs for the result, then detaches; a detached run notifies you when it ends. Cancellation before detachment stops the accepted work.",
|
|
130
97
|
promptSnippet: "Create or delegate work to a durable agent",
|
|
131
98
|
promptGuidelines: ["Call agent_roster before creating an agent and use task tools for existing work."],
|
|
132
99
|
parameters: Type.Object(
|
|
@@ -134,14 +101,14 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
134
101
|
definition: Type.String({ minLength: 1, description: "Agent Definition name" }),
|
|
135
102
|
label: Type.String({ minLength: 1, description: "Short task label" }),
|
|
136
103
|
prompt: Type.String({ minLength: 1, description: "Initial task prompt" }),
|
|
137
|
-
|
|
104
|
+
waitMs: Type.Optional(
|
|
105
|
+
Type.Integer({ minimum: 0, maximum: RETAINED_OUTPUT_MAX_WAIT_MS, description: "Wait before detaching; 0 detaches immediately" })
|
|
106
|
+
),
|
|
138
107
|
model: Type.Optional(
|
|
139
108
|
Type.String({ minLength: 1, description: "Configured provider/model ID or an enabled alias from agent_roster" })
|
|
140
109
|
),
|
|
141
110
|
thinking: Type.Optional(ThinkingLevel),
|
|
142
|
-
|
|
143
|
-
? {}
|
|
144
|
-
: { allowAgents: Type.Optional(Type.Boolean({ description: "Allow this child to create descendants" })) })
|
|
111
|
+
allowAgents: Type.Optional(Type.Boolean({ description: "Allow this child to create descendants, depth permitting" }))
|
|
145
112
|
},
|
|
146
113
|
{ additionalProperties: false }
|
|
147
114
|
),
|
|
@@ -181,17 +148,7 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
181
148
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
182
149
|
if (signal?.aborted) throw abortError(signal)
|
|
183
150
|
const config = options.getConfig()
|
|
184
|
-
const background = config.backgroundAgents
|
|
185
|
-
if (!background && "waitMs" in params) throw new Error("waitMs requires backgroundAgents")
|
|
186
|
-
if (
|
|
187
|
-
"waitMs" in params &&
|
|
188
|
-
(typeof params.waitMs !== "number" || !Number.isInteger(params.waitMs) || params.waitMs < 0 || params.waitMs > 600_000)
|
|
189
|
-
) {
|
|
190
|
-
throw new Error("waitMs must be an integer from 0 to 600000")
|
|
191
|
-
}
|
|
192
|
-
if ("allowAgents" in params && typeof params.allowAgents !== "boolean") throw new Error("allowAgents must be boolean")
|
|
193
151
|
const allowAgents = params.allowAgents === true
|
|
194
|
-
if (allowAgents && options.canDelegate?.() === false) throw new Error("This child cannot delegate at the current maximum depth")
|
|
195
152
|
validateInput(params.label, "label", MAX_AGENT_LABEL_BYTES)
|
|
196
153
|
validateInput(params.prompt, "prompt", MAX_AGENT_INPUT_BYTES)
|
|
197
154
|
const parentSessionId = ctx.sessionManager.getSessionId()
|
|
@@ -199,6 +156,7 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
199
156
|
const parentContext = coordinator.getSessionContext(parentSessionId)
|
|
200
157
|
if (parentContext && !parentContext.allowAgents) throw new Error("This parent agent is not allowed to delegate")
|
|
201
158
|
const parentDepth = parentContext?.depth ?? 0
|
|
159
|
+
if (allowAgents && parentDepth + 1 >= config.maxDepth) throw new Error("This child cannot delegate at the current maximum depth")
|
|
202
160
|
resolveChildToolPolicy({
|
|
203
161
|
parentDepth,
|
|
204
162
|
maximumDepth: config.maxDepth,
|
|
@@ -206,7 +164,7 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
206
164
|
})
|
|
207
165
|
const discovered = discoverAgentDefinitions({
|
|
208
166
|
cwd: ctx.cwd,
|
|
209
|
-
projectTrusted: ctx
|
|
167
|
+
projectTrusted: projectResourcesTrusted(ctx),
|
|
210
168
|
toolNames: pi.getAllTools().map(tool => tool.name),
|
|
211
169
|
models: ctx.modelRegistry.getAll(),
|
|
212
170
|
...(options.getAgentDir ? { agentDir: options.getAgentDir() } : {})
|
|
@@ -276,7 +234,6 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
276
234
|
id: runId,
|
|
277
235
|
sequence: 1,
|
|
278
236
|
acceptanceOrder,
|
|
279
|
-
background,
|
|
280
237
|
kind: "initial",
|
|
281
238
|
state: "queued",
|
|
282
239
|
input: params.prompt,
|
|
@@ -288,27 +245,18 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
288
245
|
createdAt: acceptedAt,
|
|
289
246
|
updatedAt: acceptedAt
|
|
290
247
|
}
|
|
291
|
-
await appendHistoryLog(paths, { type: "run-start", sequence: 1, kind: "initial", timestamp: acceptedAt })
|
|
292
|
-
await appendHistoryLog(paths, { type: "input", delivery: "initial", timestamp: acceptedAt, content: params.prompt })
|
|
293
248
|
await writeTaskMetadata(paths, metadata)
|
|
294
249
|
accepted = true
|
|
295
250
|
|
|
296
251
|
const runtime = new AgentRuntime(paths, child, metadata, config.expandPromptTemplates)
|
|
297
252
|
child = undefined
|
|
298
253
|
runtime.start()
|
|
299
|
-
|
|
300
|
-
const completed = await runtime.waitForRun(runId, signal)
|
|
301
|
-
const tasks = (await loadTaskList(ctx.cwd, parentSessionId)).details
|
|
302
|
-
return buildAgentCreationToolResult(completed, false, retainedOutputSnapshot(paths, completed), tasks)
|
|
303
|
-
}
|
|
304
|
-
const waitMs = (params.waitMs as number | undefined) ?? config.waitMs
|
|
254
|
+
const waitMs = params.waitMs ?? config.waitMs
|
|
305
255
|
const wait = () => runtime.wait(waitMs, signal)
|
|
306
256
|
const detached = waitMs === 0 ? await wait() : await coordinator.withLentPermit(wait, signal)
|
|
307
257
|
const loaded = await readTaskMetadata(paths)
|
|
308
258
|
if (loaded.status !== "ok") throw new Error(`Could not read accepted task ${paths.taskRef}`)
|
|
309
|
-
|
|
310
|
-
const tasks = (await loadTaskList(ctx.cwd, parentSessionId)).details
|
|
311
|
-
return buildAgentCreationToolResult(loaded.metadata, detached, output, tasks)
|
|
259
|
+
return buildAgentCreationToolResult(loaded.metadata, detached, await readRetainedOutput(paths, { run: 1 }))
|
|
312
260
|
} finally {
|
|
313
261
|
child?.dispose()
|
|
314
262
|
if (!accepted) await rm(paths.taskDirectory, { recursive: true, force: true })
|
|
@@ -317,44 +265,25 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
317
265
|
})
|
|
318
266
|
}
|
|
319
267
|
|
|
268
|
+
const TaskReference = Type.String({ pattern: TASK_REFERENCE_PATTERN.source, description: "Task Reference" })
|
|
269
|
+
|
|
320
270
|
export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOptions): void {
|
|
321
|
-
const background = options.getConfig().backgroundAgents
|
|
322
|
-
// Foreground agents remain a producer by default; the parent knows allowed producers and owned kinds.
|
|
323
|
-
const agentInput = options.agentInputEnabled?.() ?? true
|
|
324
|
-
const bashInput = options.bashInputEnabled?.() ?? options.getConfig().backgroundBash
|
|
325
271
|
pi.registerTool({
|
|
326
272
|
name: "task_input",
|
|
327
273
|
label: "Task Input",
|
|
328
274
|
description:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
? [
|
|
332
|
-
background
|
|
333
|
-
? "Send a durable Follow-up or live Steer to an owned Lovely Agent task."
|
|
334
|
-
: "Run a Follow-up on an idle owned task and wait for its terminal reply, or send a run-scoped live Steer. Busy tasks reject Follow-ups."
|
|
335
|
-
]
|
|
336
|
-
: []),
|
|
337
|
-
...(bashInput ? ["Write literal stdin to a running owned Bash task; eof closes stdin. Empty content requires eof:true."] : [])
|
|
338
|
-
].join(" ") || "Send input to an owned task.",
|
|
339
|
-
promptSnippet: agentInput
|
|
340
|
-
? `Send Follow-up work or a live Steer${bashInput ? ", or literal Bash stdin" : ""} to a durable task`
|
|
341
|
-
: bashInput
|
|
342
|
-
? "Write literal stdin to a running Bash task"
|
|
343
|
-
: "Send input to an owned task",
|
|
275
|
+
"Send a durable Follow-up or live Steer to an owned Lovely Agent task, or write literal stdin to a running owned Bash task; eof closes stdin. Empty content requires eof:true.",
|
|
276
|
+
promptSnippet: "Send Follow-up work or a live Steer, or literal Bash stdin, to a durable task",
|
|
344
277
|
promptGuidelines: [
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"Use task_input Follow-up for a new run; Steer queues input for a live streaming run. Without a live steering target, Steer becomes a Follow-up (foreground busy tasks reject it). Acceptance does not guarantee observation before stop."
|
|
348
|
-
]
|
|
349
|
-
: []),
|
|
350
|
-
...(bashInput ? ["For Bash, omit delivery and write literal stdin; eof closes stdin without restarting the command."] : [])
|
|
278
|
+
"Use task_input Follow-up for a new run; Steer queues input for a live streaming run. Without a live steering target, Steer becomes a Follow-up. Acceptance does not guarantee observation before stop.",
|
|
279
|
+
"For Bash, omit delivery and write literal stdin; eof closes stdin without restarting the command."
|
|
351
280
|
],
|
|
352
281
|
parameters: Type.Object(
|
|
353
282
|
{
|
|
354
283
|
id: Type.String({ pattern: TASK_REFERENCE_PATTERN.source, description: "Task Reference" }),
|
|
355
|
-
content: Type.String({
|
|
356
|
-
|
|
357
|
-
|
|
284
|
+
content: Type.String({ description: "Input text" }),
|
|
285
|
+
delivery: Type.Optional(Type.Union([Type.Literal("followup"), Type.Literal("steer")], { description: "Agent tasks only" })),
|
|
286
|
+
eof: Type.Optional(Type.Boolean({ description: "Bash only: close stdin after writing content" }))
|
|
358
287
|
},
|
|
359
288
|
{ additionalProperties: false }
|
|
360
289
|
),
|
|
@@ -369,11 +298,6 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
369
298
|
)
|
|
370
299
|
},
|
|
371
300
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
372
|
-
if ("waitMs" in params) throw new Error("task_input does not accept waitMs")
|
|
373
|
-
if (params.delivery !== undefined && params.delivery !== "followup" && params.delivery !== "steer") {
|
|
374
|
-
throw new Error("delivery must be followup or steer")
|
|
375
|
-
}
|
|
376
|
-
if (params.eof !== undefined && typeof params.eof !== "boolean") throw new Error("eof must be boolean")
|
|
377
301
|
const result = await sendTaskInput(
|
|
378
302
|
ctx,
|
|
379
303
|
options,
|
|
@@ -392,9 +316,7 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
392
316
|
? `${result.id} run=${result.run}: stdin delivered${params.eof ? " (EOF)" : ""}`
|
|
393
317
|
: result.effectiveDelivery === "steer"
|
|
394
318
|
? `${result.id} run=${result.run}: steer queued for current run`
|
|
395
|
-
: result.
|
|
396
|
-
? `${result.id} run=${result.run}: ${result.conversionReason ? `requested steer -> followup (${result.conversionReason}); ` : ""}followup ${result.latestOutcome}\n${result.output.text}`
|
|
397
|
-
: `${result.id} run=${result.run}: ${result.conversionReason ? `requested steer -> ` : ""}followup accepted${result.conversionReason ? `; ${result.conversionReason}` : ""} (position ${result.queuePosition}; task=${result.state}; ${result.queuedFollowUps} queued)`
|
|
319
|
+
: `${result.id} run=${result.run}: ${result.conversionReason ? `requested steer -> ` : ""}followup accepted${result.conversionReason ? `; ${result.conversionReason}` : ""} (position ${result.queuePosition}; task=${result.state}; ${result.queuedFollowUps} queued)`
|
|
398
320
|
}
|
|
399
321
|
],
|
|
400
322
|
details: result
|
|
@@ -408,8 +330,8 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
408
330
|
label: action === "stop" ? "Task Stop" : "Task Discard",
|
|
409
331
|
description:
|
|
410
332
|
action === "stop"
|
|
411
|
-
? "Stop active or queued work for
|
|
412
|
-
: "Stop and permanently discard
|
|
333
|
+
? "Stop active or queued work for owned Agent or Bash tasks while preserving their files; id accepts one Task Reference or a list."
|
|
334
|
+
: "Stop and permanently discard owned task subtrees from active work; id accepts one Task Reference or a list. Files stay at their original paths for read-only inspection.",
|
|
413
335
|
promptSnippet: action === "stop" ? "Stop work for a durable task" : "Discard a durable task",
|
|
414
336
|
promptGuidelines:
|
|
415
337
|
action === "discard"
|
|
@@ -418,26 +340,39 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
418
340
|
]
|
|
419
341
|
: [],
|
|
420
342
|
parameters: Type.Object(
|
|
421
|
-
{
|
|
343
|
+
{
|
|
344
|
+
id: Type.Union([TaskReference, Type.Array(TaskReference, { minItems: 1, maxItems: 64, uniqueItems: true })], {
|
|
345
|
+
description: "One Task Reference, or a list to handle in one call"
|
|
346
|
+
})
|
|
347
|
+
},
|
|
422
348
|
{ additionalProperties: false }
|
|
423
349
|
),
|
|
424
350
|
renderCall(args, theme) {
|
|
425
|
-
|
|
351
|
+
const ids = [args.id ?? []].flat().join(" ")
|
|
352
|
+
return new Text(`${theme.fg("toolTitle", theme.bold(`task_${action}`))}${ids ? ` ${theme.fg("muted", ids)}` : ""}`, 0, 0)
|
|
426
353
|
},
|
|
427
354
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
{
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
355
|
+
const describe = (details: Awaited<ReturnType<typeof controlTaskLifecycle>>) =>
|
|
356
|
+
action === "discard"
|
|
357
|
+
? `${details.id}: discarded (files retained at ${details.taskDirectory})`
|
|
358
|
+
: `${details.id}: stop complete (${details.state}; outcome ${details.latestOutcome ?? "none"}; ${details.queuedFollowUps} queued)`
|
|
359
|
+
if (typeof params.id === "string") {
|
|
360
|
+
const details = await controlTaskLifecycle(ctx, params.id, action)
|
|
361
|
+
return { content: [{ type: "text", text: describe(details) }], details }
|
|
362
|
+
}
|
|
363
|
+
// One bad reference must not abandon the rest of a batch; only a total failure is a tool error.
|
|
364
|
+
const tasks: Array<Awaited<ReturnType<typeof controlTaskLifecycle>>> = []
|
|
365
|
+
const errors: Array<{ id: string; message: string }> = []
|
|
366
|
+
for (const id of params.id) {
|
|
367
|
+
try {
|
|
368
|
+
tasks.push(await controlTaskLifecycle(ctx, id, action))
|
|
369
|
+
} catch (error) {
|
|
370
|
+
errors.push({ id, message: error instanceof Error ? error.message : String(error) })
|
|
371
|
+
}
|
|
440
372
|
}
|
|
373
|
+
const text = [...tasks.map(describe), ...errors.map(error => `${error.id}: failed: ${error.message}`)].join("\n")
|
|
374
|
+
if (tasks.length === 0) throw new Error(text)
|
|
375
|
+
return { content: [{ type: "text", text }], details: { tasks, errors } }
|
|
441
376
|
}
|
|
442
377
|
})
|
|
443
378
|
}
|
|
@@ -453,11 +388,6 @@ export async function sendTaskInput(
|
|
|
453
388
|
inputOptions: { eof?: boolean } = {}
|
|
454
389
|
): Promise<TaskInputResult> {
|
|
455
390
|
if (signal?.aborted) throw abortError(signal)
|
|
456
|
-
if (typeof content !== "string") throw new Error("content must be a string")
|
|
457
|
-
if (inputOptions.eof !== undefined && typeof inputOptions.eof !== "boolean") throw new Error("eof must be boolean")
|
|
458
|
-
if (requestedDelivery !== undefined && requestedDelivery !== "followup" && requestedDelivery !== "steer") {
|
|
459
|
-
throw new Error("delivery must be followup or steer")
|
|
460
|
-
}
|
|
461
391
|
if (!id.startsWith("b_")) validateInput(content, "content", MAX_AGENT_INPUT_BYTES)
|
|
462
392
|
const lease = await acquireParentLease(ctx.cwd, ctx.sessionManager.getSessionId())
|
|
463
393
|
const paths = taskStoragePaths(lease.paths, id)
|
|
@@ -494,10 +424,7 @@ export async function sendTaskInput(
|
|
|
494
424
|
if (signal?.aborted) throw abortError(signal)
|
|
495
425
|
const runtime = await controllableRuntime(ctx, paths, options)
|
|
496
426
|
try {
|
|
497
|
-
accepted = await runtime.input(content, delivery, {
|
|
498
|
-
background: options.getConfig().backgroundAgents,
|
|
499
|
-
...(signal ? { signal } : {})
|
|
500
|
-
})
|
|
427
|
+
accepted = await runtime.input(content, delivery, signal ? { signal } : {})
|
|
501
428
|
} catch (error) {
|
|
502
429
|
if (!isRuntimeClosingError(error)) throw error
|
|
503
430
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
@@ -512,10 +439,9 @@ export async function sendTaskInput(
|
|
|
512
439
|
requestedDelivery: delivery,
|
|
513
440
|
effectiveDelivery: accepted.delivery,
|
|
514
441
|
queuePosition: accepted.queuePosition,
|
|
515
|
-
state:
|
|
516
|
-
latestOutcome:
|
|
517
|
-
queuedFollowUps:
|
|
518
|
-
...(accepted.completed ? { output: retainedOutputSnapshot(paths, accepted.completed) } : {})
|
|
442
|
+
state: current.metadata.state,
|
|
443
|
+
latestOutcome: current.metadata.latestOutcome,
|
|
444
|
+
queuedFollowUps: current.metadata.queuedFollowUps.length
|
|
519
445
|
}
|
|
520
446
|
}
|
|
521
447
|
|
|
@@ -559,41 +485,32 @@ class AgentRuntime implements ResidentAgent {
|
|
|
559
485
|
readonly #paths: TaskStoragePaths
|
|
560
486
|
readonly #child: ChildSessionHandle
|
|
561
487
|
readonly #expandPromptTemplates: boolean
|
|
562
|
-
readonly #tuple: ModelTuple
|
|
563
488
|
readonly #scheduleAbort = new AbortController()
|
|
564
489
|
readonly #initialRunId: string | undefined
|
|
565
490
|
readonly #initialCompletion = deferred<void>()
|
|
566
491
|
readonly #runtimeCompletion = deferred<void>()
|
|
567
492
|
readonly #toolArguments = new Map<string, { name: string; arguments: string }>()
|
|
568
|
-
readonly #pendingSteers: Array<{ content: string }> = []
|
|
569
|
-
readonly #reservations = new Map<string, AgentReservation>()
|
|
570
|
-
readonly #completions = new Map<string, ReturnType<typeof deferred<TaskMetadata | undefined>>>()
|
|
571
493
|
#unbindResident: (() => void) | undefined
|
|
572
|
-
#unbindSuspended: (() => void) | undefined
|
|
573
494
|
#unsubscribe: (() => void) | undefined
|
|
574
495
|
#eventWrites: Promise<void> = Promise.resolve()
|
|
575
|
-
#eventWriteFailed = false
|
|
576
496
|
#recordingRunId: string | undefined
|
|
577
497
|
#pendingProgress: ({ runId: string } & Partial<Pick<TaskMetadata, "latestReply" | "lastActivity">>) | undefined
|
|
578
498
|
#lastHeartbeatAt = 0
|
|
579
499
|
#progressWriteQueued = false
|
|
500
|
+
#progressTimer: ReturnType<typeof setTimeout> | undefined
|
|
501
|
+
#lastProgressWriteAt = 0
|
|
580
502
|
#started = false
|
|
581
503
|
#stopRequested = false
|
|
582
504
|
#accepting = true
|
|
583
505
|
#disposed = false
|
|
584
|
-
#
|
|
506
|
+
#sawInput = false
|
|
585
507
|
#lastAssistantOutcome: NonNullable<TaskMetadata["latestOutcome"]> | undefined
|
|
586
|
-
#providerLimitReached = false
|
|
587
|
-
#recoveryWait: (ReturnType<typeof deferred<RecoveryMode>> & { mode: RecoveryMode | undefined }) | undefined
|
|
588
|
-
#recoveryMode: Exclude<RecoveryMode, "stop"> | undefined
|
|
589
508
|
|
|
590
509
|
constructor(paths: TaskStoragePaths, child: ChildSessionHandle, metadata: AgentTaskMetadata, expandPromptTemplates: boolean) {
|
|
591
510
|
this.#paths = paths
|
|
592
511
|
this.#child = child
|
|
593
512
|
this.#initialRunId = metadata.activeRun?.kind === "initial" ? metadata.activeRun.id : undefined
|
|
594
513
|
this.#expandPromptTemplates = expandPromptTemplates
|
|
595
|
-
this.#tuple = { provider: metadata.model.provider, model: metadata.model.id }
|
|
596
|
-
if (metadata.activeRun && !metadata.activeRun.background) this.#completions.set(metadata.activeRun.id, deferred())
|
|
597
514
|
this.#unbindResident = getAgentCoordinator().bindResident(paths.taskDirectory, this)
|
|
598
515
|
this.#unsubscribe = child.session.subscribe(event => this.recordEvent(event))
|
|
599
516
|
}
|
|
@@ -604,37 +521,12 @@ class AgentRuntime implements ResidentAgent {
|
|
|
604
521
|
void this.run().catch(() => {})
|
|
605
522
|
}
|
|
606
523
|
|
|
607
|
-
/** Run-specific synchronous wait; cancellation owns the accepted run and its descendants. */
|
|
608
|
-
async waitForRun(runId: string, signal?: AbortSignal): Promise<TaskMetadata> {
|
|
609
|
-
const completion = this.#completions.get(runId)
|
|
610
|
-
if (!completion) throw new Error(`Missing completion for run ${runId}`)
|
|
611
|
-
return getAgentCoordinator().withLentPermit(async () => {
|
|
612
|
-
const aborted = deferred<void>()
|
|
613
|
-
const onAbort = () => aborted.resolve(undefined)
|
|
614
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
615
|
-
if (signal?.aborted) onAbort()
|
|
616
|
-
try {
|
|
617
|
-
const result = await Promise.race([completion.promise, aborted.promise.then(() => undefined)])
|
|
618
|
-
if (signal?.aborted) {
|
|
619
|
-
await this.stop()
|
|
620
|
-
await stopOwnedTaskTree(this.#paths.workspace, this.#child.session.sessionId)
|
|
621
|
-
throw abortError(signal)
|
|
622
|
-
}
|
|
623
|
-
if (!result) throw new Error(`Agent run ${runId} ended without a retained terminal result`)
|
|
624
|
-
return result
|
|
625
|
-
} finally {
|
|
626
|
-
signal?.removeEventListener("abort", onAbort)
|
|
627
|
-
this.#completions.delete(runId)
|
|
628
|
-
}
|
|
629
|
-
}, signal)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
524
|
async wait(waitMs: number, signal?: AbortSignal): Promise<boolean> {
|
|
633
525
|
if (waitMs === 0) {
|
|
634
526
|
await this.detach()
|
|
635
527
|
return true
|
|
636
528
|
}
|
|
637
|
-
if (signal?.aborted) await this.
|
|
529
|
+
if (signal?.aborted) await this.stopTree()
|
|
638
530
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
639
531
|
let onAbort: (() => void) | undefined
|
|
640
532
|
const timeout = new Promise<"timeout">(resolve => {
|
|
@@ -655,7 +547,7 @@ class AgentRuntime implements ResidentAgent {
|
|
|
655
547
|
if (timer) clearTimeout(timer)
|
|
656
548
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort)
|
|
657
549
|
if (result === "aborted") {
|
|
658
|
-
await this.
|
|
550
|
+
await this.stopTree()
|
|
659
551
|
return false
|
|
660
552
|
}
|
|
661
553
|
if (result === "timeout") {
|
|
@@ -665,11 +557,16 @@ class AgentRuntime implements ResidentAgent {
|
|
|
665
557
|
return false
|
|
666
558
|
}
|
|
667
559
|
|
|
560
|
+
/** Cancellation owns the accepted run and any descendants it already detached. */
|
|
561
|
+
private async stopTree(): Promise<void> {
|
|
562
|
+
await this.stop()
|
|
563
|
+
await stopOwnedTaskTree(this.#paths.workspace, this.#child.session.sessionId)
|
|
564
|
+
}
|
|
565
|
+
|
|
668
566
|
async stop(): Promise<void> {
|
|
669
567
|
if (this.#stopRequested) return this.#runtimeCompletion.promise
|
|
670
568
|
this.#stopRequested = true
|
|
671
569
|
this.#accepting = false
|
|
672
|
-
this.wakeProviderRecovery("stop")
|
|
673
570
|
this.#scheduleAbort.abort(new Error("Agent run stopped"))
|
|
674
571
|
await this.#child.session.abort()
|
|
675
572
|
const loaded = await readTaskMetadata(this.#paths)
|
|
@@ -697,8 +594,6 @@ class AgentRuntime implements ResidentAgent {
|
|
|
697
594
|
this.#unsubscribe = undefined
|
|
698
595
|
this.#unbindResident?.()
|
|
699
596
|
this.#unbindResident = undefined
|
|
700
|
-
this.#unbindSuspended?.()
|
|
701
|
-
this.#unbindSuspended = undefined
|
|
702
597
|
this.#child.dispose()
|
|
703
598
|
}
|
|
704
599
|
|
|
@@ -706,24 +601,15 @@ class AgentRuntime implements ResidentAgent {
|
|
|
706
601
|
if (delivery === "stdin" || options.eof !== undefined) throw new Error("Agent tasks do not accept stdin or eof")
|
|
707
602
|
if (!this.#accepting || this.#disposed) throw new RuntimeClosingError()
|
|
708
603
|
let result: ResidentInputResult | undefined
|
|
709
|
-
let acceptedRun: NonNullable<TaskMetadata["activeRun"]> | undefined
|
|
710
604
|
try {
|
|
711
605
|
await mutateTaskMetadata(this.#paths, async metadata => {
|
|
712
606
|
if (options.signal?.aborted) throw abortError(options.signal)
|
|
713
607
|
if (!this.#accepting || this.#disposed) throw new RuntimeClosingError()
|
|
714
608
|
if (metadata.discardedAt !== null) throw new Error(`Task ${metadata.taskRef} has been discarded`)
|
|
715
609
|
if (delivery === "steer" && metadata.state === "running" && metadata.activeRun && this.#child.session.isStreaming) {
|
|
716
|
-
|
|
717
|
-
this.#
|
|
718
|
-
|
|
719
|
-
// prompt() can yield in input hooks and start a new run after streaming ends.
|
|
720
|
-
if (this.#expandPromptTemplates) await this.#child.session.steer(content)
|
|
721
|
-
else this.#child.session.agent.steer({ role: "user", content, timestamp: Date.now() })
|
|
722
|
-
} catch (error) {
|
|
723
|
-
const index = this.#pendingSteers.indexOf(pending)
|
|
724
|
-
if (index >= 0) this.#pendingSteers.splice(index, 1)
|
|
725
|
-
throw error
|
|
726
|
-
}
|
|
610
|
+
// prompt() can yield in input hooks and start a new run after streaming ends.
|
|
611
|
+
if (this.#expandPromptTemplates) await this.#child.session.steer(content)
|
|
612
|
+
else this.#child.session.agent.steer({ role: "user", content, timestamp: Date.now() })
|
|
727
613
|
result = {
|
|
728
614
|
run: metadata.activeRun.sequence,
|
|
729
615
|
delivery: "steer",
|
|
@@ -732,12 +618,6 @@ class AgentRuntime implements ResidentAgent {
|
|
|
732
618
|
}
|
|
733
619
|
return metadata
|
|
734
620
|
}
|
|
735
|
-
if (
|
|
736
|
-
(!options.background && (metadata.activeRun || metadata.queuedFollowUps.length)) ||
|
|
737
|
-
(metadata.activeRun && !metadata.activeRun.background)
|
|
738
|
-
) {
|
|
739
|
-
throw new Error(`Task ${metadata.taskRef} is busy; foreground Follow-ups require an idle task`)
|
|
740
|
-
}
|
|
741
621
|
if (metadata.queuedFollowUps.length >= MAX_QUEUED_FOLLOWUPS) {
|
|
742
622
|
throw new Error(`Task ${metadata.taskRef} already has ${MAX_QUEUED_FOLLOWUPS} queued Follow-ups`)
|
|
743
623
|
}
|
|
@@ -753,11 +633,10 @@ class AgentRuntime implements ResidentAgent {
|
|
|
753
633
|
queuedFollowUps: metadata.queuedFollowUps.length + 1,
|
|
754
634
|
...(delivery === "steer" ? { conversionReason: `task ${metadata.state}, no live stream to steer` } : {})
|
|
755
635
|
}
|
|
756
|
-
acceptedRun = {
|
|
636
|
+
const acceptedRun: NonNullable<TaskMetadata["activeRun"]> = {
|
|
757
637
|
id: runId,
|
|
758
638
|
sequence,
|
|
759
639
|
acceptanceOrder,
|
|
760
|
-
background: options.background ?? false,
|
|
761
640
|
kind: "followup",
|
|
762
641
|
state: "queued",
|
|
763
642
|
input: content,
|
|
@@ -776,10 +655,7 @@ class AgentRuntime implements ResidentAgent {
|
|
|
776
655
|
return {
|
|
777
656
|
...metadata,
|
|
778
657
|
lastRunSequence: sequence,
|
|
779
|
-
queuedFollowUps: [
|
|
780
|
-
...metadata.queuedFollowUps,
|
|
781
|
-
{ id: runId, sequence, acceptanceOrder, background: options.background ?? false, content, acceptedAt }
|
|
782
|
-
],
|
|
658
|
+
queuedFollowUps: [...metadata.queuedFollowUps, { id: runId, sequence, acceptanceOrder, content, acceptedAt }],
|
|
783
659
|
updatedAt: acceptedAt
|
|
784
660
|
}
|
|
785
661
|
})
|
|
@@ -793,47 +669,19 @@ class AgentRuntime implements ResidentAgent {
|
|
|
793
669
|
throw error
|
|
794
670
|
}
|
|
795
671
|
if (!result) throw new Error("Task input was not accepted")
|
|
796
|
-
if (acceptedRun) {
|
|
797
|
-
if (!acceptedRun.background) this.#completions.set(acceptedRun.id, deferred())
|
|
798
|
-
// Inactive background reservations preserve acceptance order across promotion.
|
|
799
|
-
if (acceptedRun.background) this.reserveRun(acceptedRun)
|
|
800
|
-
}
|
|
801
672
|
this.start()
|
|
802
|
-
if (acceptedRun && !acceptedRun.background) result.completed = await this.waitForRun(acceptedRun.id, options.signal)
|
|
803
673
|
return result
|
|
804
674
|
}
|
|
805
675
|
|
|
806
|
-
recover(): boolean {
|
|
807
|
-
return this.wakeProviderRecovery("manual")
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
wakeProviderRecovery(mode: RecoveryMode): boolean {
|
|
811
|
-
const wait = this.#recoveryWait
|
|
812
|
-
if (!wait || wait.mode || this.#disposed || (this.#stopRequested && mode !== "stop")) return false
|
|
813
|
-
wait.mode = mode
|
|
814
|
-
this.#unbindSuspended?.()
|
|
815
|
-
this.#unbindSuspended = undefined
|
|
816
|
-
wait.resolve(mode)
|
|
817
|
-
return true
|
|
818
|
-
}
|
|
819
|
-
|
|
820
676
|
private async run(): Promise<void> {
|
|
821
677
|
try {
|
|
822
678
|
while (!this.#stopRequested) {
|
|
823
679
|
const activeRun = await this.nextRun()
|
|
824
680
|
if (!activeRun) break
|
|
825
|
-
|
|
826
|
-
const wait = this.#recoveryWait
|
|
827
|
-
if (!wait) throw new Error("Suspended run has no recovery wait")
|
|
828
|
-
const mode = await wait.promise
|
|
829
|
-
if (this.#recoveryWait === wait) this.#recoveryWait = undefined
|
|
830
|
-
if (mode === "stop") break
|
|
831
|
-
this.#recoveryMode = mode
|
|
832
|
-
}
|
|
681
|
+
await this.executeRun(activeRun)
|
|
833
682
|
}
|
|
834
683
|
} finally {
|
|
835
684
|
this.dispose()
|
|
836
|
-
for (const completion of this.#completions.values()) completion.resolve(undefined)
|
|
837
685
|
this.#initialCompletion.resolve(undefined)
|
|
838
686
|
this.#runtimeCompletion.resolve(undefined)
|
|
839
687
|
}
|
|
@@ -855,7 +703,6 @@ class AgentRuntime implements ResidentAgent {
|
|
|
855
703
|
id: next.id,
|
|
856
704
|
sequence: next.sequence,
|
|
857
705
|
...(next.acceptanceOrder ? { acceptanceOrder: next.acceptanceOrder } : {}),
|
|
858
|
-
background: next.background ?? false,
|
|
859
706
|
kind: "followup",
|
|
860
707
|
state: "queued",
|
|
861
708
|
input: next.content,
|
|
@@ -866,19 +713,17 @@ class AgentRuntime implements ResidentAgent {
|
|
|
866
713
|
return selected.run
|
|
867
714
|
}
|
|
868
715
|
|
|
869
|
-
private async executeRun(run: NonNullable<TaskMetadata["activeRun"]>): Promise<
|
|
716
|
+
private async executeRun(run: NonNullable<TaskMetadata["activeRun"]>): Promise<void> {
|
|
870
717
|
this.#recordingRunId = run.id
|
|
871
718
|
this.#lastHeartbeatAt = 0
|
|
872
719
|
let outcome: NonNullable<TaskMetadata["latestOutcome"]> = "failed"
|
|
873
|
-
const recovering = run.state === "suspended"
|
|
874
720
|
this.#lastAssistantOutcome = undefined
|
|
875
|
-
this.#providerLimitReached = false
|
|
876
721
|
let settled = false
|
|
877
|
-
let suspended = false
|
|
878
722
|
try {
|
|
879
|
-
|
|
723
|
+
// The stored acceptance order puts a promoted Follow-up ahead of later-accepted work.
|
|
724
|
+
const schedule = { signal: this.#scheduleAbort.signal, ...(run.acceptanceOrder ? { acceptanceOrder: run.acceptanceOrder } : {}) }
|
|
725
|
+
await getAgentCoordinator().run(schedule, async () => {
|
|
880
726
|
if (this.#stopRequested) return
|
|
881
|
-
let promptCompleted = false
|
|
882
727
|
try {
|
|
883
728
|
let won = false
|
|
884
729
|
const startedAt = Date.now()
|
|
@@ -895,116 +740,33 @@ class AgentRuntime implements ResidentAgent {
|
|
|
895
740
|
}
|
|
896
741
|
})
|
|
897
742
|
if (!won) return
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
kind: "followup",
|
|
904
|
-
timestamp: startedAt
|
|
905
|
-
})
|
|
906
|
-
await appendHistoryLog(this.#paths, {
|
|
907
|
-
type: "input",
|
|
908
|
-
delivery: "followup",
|
|
909
|
-
timestamp: startedAt,
|
|
910
|
-
content: run.input
|
|
911
|
-
})
|
|
912
|
-
}
|
|
913
|
-
this.#awaitingPrimaryInput = true
|
|
914
|
-
await this.#child.prompt(
|
|
915
|
-
run.id,
|
|
916
|
-
recovering ? "Continue." : run.input,
|
|
917
|
-
childPromptOptions(recovering ? false : this.#expandPromptTemplates)
|
|
918
|
-
)
|
|
919
|
-
promptCompleted = true
|
|
743
|
+
// Inputs are logged as the child session observes them: the first is the run's
|
|
744
|
+
// prompt, later ones are delivered Steers.
|
|
745
|
+
this.#sawInput = false
|
|
746
|
+
this.queueEventWrite(() => appendHistoryLog(this.#paths, { type: "run-start", sequence: run.sequence, kind: run.kind }))
|
|
747
|
+
await this.#child.prompt(run.id, run.input, childPromptOptions(this.#expandPromptTemplates))
|
|
920
748
|
await this.#eventWrites
|
|
921
|
-
if (this.#eventWriteFailed) throw new Error("Could not retain one or more child session events")
|
|
922
749
|
outcome = this.#stopRequested ? "stopped" : (this.#lastAssistantOutcome ?? "failed")
|
|
923
750
|
} catch {
|
|
924
|
-
if (!promptCompleted) this.#providerLimitReached = false
|
|
925
751
|
await this.#eventWrites
|
|
926
752
|
outcome = this.#stopRequested ? "stopped" : "failed"
|
|
927
753
|
}
|
|
928
|
-
|
|
929
|
-
suspended = await this.suspend(run)
|
|
930
|
-
if (suspended) return
|
|
931
|
-
}
|
|
932
|
-
const promoted = await this.settle(run, outcome, this.#stopRequested)
|
|
754
|
+
await this.settle(run, outcome, this.#stopRequested)
|
|
933
755
|
settled = true
|
|
934
|
-
if (promoted) this.reserveRun(promoted).activate()
|
|
935
756
|
})
|
|
936
|
-
} catch
|
|
757
|
+
} catch {
|
|
937
758
|
await this.#eventWrites
|
|
938
759
|
outcome = this.#stopRequested ? "stopped" : "failed"
|
|
939
|
-
if (!run.background && !this.#stopRequested) {
|
|
940
|
-
await mutateTaskMetadata(this.#paths, metadata =>
|
|
941
|
-
metadata.activeRun?.id === run.id
|
|
942
|
-
? {
|
|
943
|
-
...metadata,
|
|
944
|
-
latestReply: { text: error instanceof Error ? error.message : String(error), streaming: false },
|
|
945
|
-
updatedAt: Date.now()
|
|
946
|
-
}
|
|
947
|
-
: metadata
|
|
948
|
-
)
|
|
949
|
-
}
|
|
950
760
|
}
|
|
951
761
|
this.#child.session.clearQueue()
|
|
952
|
-
this
|
|
953
|
-
this.#reservations.delete(run.id)
|
|
954
|
-
if (!settled && !suspended) {
|
|
955
|
-
const promoted = await this.settle(run, outcome, this.#stopRequested)
|
|
956
|
-
if (promoted) this.reserveRun(promoted).activate()
|
|
957
|
-
}
|
|
958
|
-
return suspended
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
private async suspend(run: NonNullable<TaskMetadata["activeRun"]>): Promise<boolean> {
|
|
962
|
-
// Provider evidence is tuple-global even if another task mutation wins.
|
|
963
|
-
getAgentCoordinator().closeTuple(this.#tuple)
|
|
964
|
-
if (!run.background) {
|
|
965
|
-
await writeTaskProgress(this.#paths, run.id, {
|
|
966
|
-
latestReply: { text: "Provider limit reached; foreground run failed and will not restart automatically.", streaming: false }
|
|
967
|
-
})
|
|
968
|
-
return false
|
|
969
|
-
}
|
|
970
|
-
const wait = Object.assign(deferred<RecoveryMode>(), { mode: undefined as RecoveryMode | undefined })
|
|
971
|
-
const unbind = bindSuspendedRuntime(this.#tuple, this)
|
|
972
|
-
this.#recoveryWait = wait
|
|
973
|
-
this.#unbindSuspended = unbind
|
|
974
|
-
let won = false
|
|
975
|
-
let notification: TaskMetadata["notifications"][number] | undefined
|
|
976
|
-
const timestamp = Date.now()
|
|
977
|
-
await mutateTaskMetadata(this.#paths, async metadata => {
|
|
978
|
-
const activeRun = metadata.activeRun
|
|
979
|
-
if (!activeRun || activeRun.id !== run.id) return metadata
|
|
980
|
-
won = true
|
|
981
|
-
if (run.kind === "followup" || activeRun.detachedAt !== undefined) {
|
|
982
|
-
notification = await prepareTaskNotification(this.#paths, metadata, activeRun, "suspension")
|
|
983
|
-
}
|
|
984
|
-
return {
|
|
985
|
-
...metadata,
|
|
986
|
-
state: "suspended",
|
|
987
|
-
activeRun: { ...activeRun, state: "suspended" },
|
|
988
|
-
...(notification ? { notifications: appendTaskNotification(metadata.notifications, notification) } : {}),
|
|
989
|
-
updatedAt: timestamp
|
|
990
|
-
}
|
|
991
|
-
})
|
|
992
|
-
if (won) {
|
|
993
|
-
if (run.id === this.#initialRunId) this.#initialCompletion.resolve(undefined)
|
|
994
|
-
if (notification) await deliverTaskNotifications(this.#paths).catch(() => {})
|
|
995
|
-
} else {
|
|
996
|
-
unbind()
|
|
997
|
-
if (this.#recoveryWait === wait) this.#recoveryWait = undefined
|
|
998
|
-
if (this.#unbindSuspended === unbind) this.#unbindSuspended = undefined
|
|
999
|
-
}
|
|
1000
|
-
return won
|
|
762
|
+
if (!settled) await this.settle(run, outcome, this.#stopRequested)
|
|
1001
763
|
}
|
|
1002
764
|
|
|
1003
765
|
private async detach(): Promise<void> {
|
|
1004
766
|
const detachedAt = Date.now()
|
|
1005
767
|
await mutateTaskMetadata(this.#paths, metadata => {
|
|
1006
768
|
const activeRun = metadata.activeRun
|
|
1007
|
-
if (!activeRun
|
|
769
|
+
if (!activeRun || activeRun.id !== this.#initialRunId || activeRun.detachedAt !== undefined) return metadata
|
|
1008
770
|
return { ...metadata, activeRun: { ...activeRun, detachedAt }, updatedAt: detachedAt }
|
|
1009
771
|
})
|
|
1010
772
|
}
|
|
@@ -1013,39 +775,24 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1013
775
|
run: NonNullable<TaskMetadata["activeRun"]>,
|
|
1014
776
|
outcome: NonNullable<TaskMetadata["latestOutcome"]>,
|
|
1015
777
|
clearFollowUps = false
|
|
1016
|
-
): Promise<
|
|
778
|
+
): Promise<void> {
|
|
1017
779
|
let won = false
|
|
1018
|
-
const promoted: { run: NonNullable<TaskMetadata["activeRun"]> | null } = { run: null }
|
|
1019
780
|
let notification: TaskMetadata["notifications"][number] | undefined
|
|
1020
|
-
let completed: TaskMetadata | undefined
|
|
1021
781
|
const timestamp = Date.now()
|
|
1022
782
|
await mutateTaskMetadata(this.#paths, async metadata => {
|
|
1023
783
|
const activeRun = metadata.activeRun
|
|
1024
784
|
if (activeRun?.id !== run.id) return clearFollowUps ? { ...metadata, queuedFollowUps: [], updatedAt: timestamp } : metadata
|
|
1025
785
|
won = true
|
|
1026
|
-
|
|
1027
|
-
...metadata,
|
|
1028
|
-
state: "idle",
|
|
1029
|
-
latestOutcome: outcome,
|
|
1030
|
-
lastSettledRun: run.sequence,
|
|
1031
|
-
activeRun: null,
|
|
1032
|
-
queuedFollowUps: [],
|
|
1033
|
-
updatedAt: timestamp
|
|
1034
|
-
}
|
|
1035
|
-
if (
|
|
1036
|
-
activeRun.background &&
|
|
1037
|
-
(outcome === "succeeded" || outcome === "failed") &&
|
|
1038
|
-
(run.kind === "followup" || activeRun.detachedAt !== undefined)
|
|
1039
|
-
) {
|
|
786
|
+
if ((outcome === "succeeded" || outcome === "failed") && (run.kind === "followup" || activeRun.detachedAt !== undefined)) {
|
|
1040
787
|
notification = await prepareTaskNotification(this.#paths, metadata, activeRun, "completion", outcome)
|
|
1041
788
|
}
|
|
1042
789
|
const [next, ...remaining] = clearFollowUps ? [] : metadata.queuedFollowUps
|
|
1043
790
|
if (next) {
|
|
1044
|
-
|
|
791
|
+
// Promote atomically: an idle gap would let new input jump the queue.
|
|
792
|
+
const promoted: NonNullable<TaskMetadata["activeRun"]> = {
|
|
1045
793
|
id: next.id,
|
|
1046
794
|
sequence: next.sequence,
|
|
1047
795
|
...(next.acceptanceOrder ? { acceptanceOrder: next.acceptanceOrder } : {}),
|
|
1048
|
-
background: next.background ?? false,
|
|
1049
796
|
kind: "followup",
|
|
1050
797
|
state: "queued",
|
|
1051
798
|
input: next.content,
|
|
@@ -1055,7 +802,7 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1055
802
|
...metadata,
|
|
1056
803
|
state: "queued",
|
|
1057
804
|
latestOutcome: outcome,
|
|
1058
|
-
activeRun: promoted
|
|
805
|
+
activeRun: promoted,
|
|
1059
806
|
queuedFollowUps: remaining,
|
|
1060
807
|
...(notification ? { notifications: appendTaskNotification(metadata.notifications, notification) } : {}),
|
|
1061
808
|
updatedAt: timestamp
|
|
@@ -1071,28 +818,10 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1071
818
|
updatedAt: timestamp
|
|
1072
819
|
}
|
|
1073
820
|
})
|
|
1074
|
-
|
|
821
|
+
// Observability only: a failed append must not skip notification delivery below.
|
|
822
|
+
if (won) await appendHistoryLog(this.#paths, { type: "run-end", sequence: run.sequence, outcome }).catch(() => {})
|
|
1075
823
|
if (won && notification) await deliverTaskNotifications(this.#paths).catch(() => {})
|
|
1076
|
-
if (won) this.#completions.get(run.id)?.resolve(completed)
|
|
1077
824
|
if (run.id === this.#initialRunId) this.#initialCompletion.resolve(undefined)
|
|
1078
|
-
return promoted.run
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
private reserveRun(run: NonNullable<TaskMetadata["activeRun"]>): AgentReservation {
|
|
1082
|
-
const existing = this.#reservations.get(run.id)
|
|
1083
|
-
if (existing) return existing
|
|
1084
|
-
const tuple =
|
|
1085
|
-
run.state === "suspended" && this.#recoveryMode === "manual"
|
|
1086
|
-
? { provider: `${this.#tuple.provider}#manual#${this.#paths.taskRef}`, model: this.#tuple.model }
|
|
1087
|
-
: this.#tuple
|
|
1088
|
-
const reservation = getAgentCoordinator().reserve({
|
|
1089
|
-
tuple,
|
|
1090
|
-
signal: this.#scheduleAbort.signal,
|
|
1091
|
-
rejectOnClosedTuple: !run.background,
|
|
1092
|
-
...(run.acceptanceOrder ? { acceptanceOrder: run.acceptanceOrder } : {})
|
|
1093
|
-
})
|
|
1094
|
-
this.#reservations.set(run.id, reservation)
|
|
1095
|
-
return reservation
|
|
1096
825
|
}
|
|
1097
826
|
|
|
1098
827
|
private recordEvent(event: AgentSessionEvent): void {
|
|
@@ -1123,11 +852,7 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1123
852
|
event.message.role === "assistant" &&
|
|
1124
853
|
(event.type !== "message_update" || event.assistantMessageEvent.type === "text_delta")
|
|
1125
854
|
) {
|
|
1126
|
-
|
|
1127
|
-
.filter(part => part.type === "text")
|
|
1128
|
-
.map(part => part.text)
|
|
1129
|
-
.join("")
|
|
1130
|
-
latestReply = { text, streaming: event.type !== "message_end" }
|
|
855
|
+
latestReply = { text: messageText(event.message.content), streaming: event.type !== "message_end" }
|
|
1131
856
|
action = latestReply.streaming ? "responding" : "reply complete"
|
|
1132
857
|
}
|
|
1133
858
|
if (runId && action) {
|
|
@@ -1137,21 +862,7 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1137
862
|
...(latestReply ? { latestReply } : {}),
|
|
1138
863
|
lastActivity: { at, action }
|
|
1139
864
|
}
|
|
1140
|
-
|
|
1141
|
-
this.#progressWriteQueued = true
|
|
1142
|
-
this.queueEventWrite(async () => {
|
|
1143
|
-
try {
|
|
1144
|
-
// Coalesce replies and activity together so bursts cannot reorder their last action.
|
|
1145
|
-
while (this.#pendingProgress) {
|
|
1146
|
-
const { runId, ...progress } = this.#pendingProgress
|
|
1147
|
-
this.#pendingProgress = undefined
|
|
1148
|
-
await writeTaskProgress(this.#paths, runId, progress)
|
|
1149
|
-
}
|
|
1150
|
-
} finally {
|
|
1151
|
-
this.#progressWriteQueued = false
|
|
1152
|
-
}
|
|
1153
|
-
})
|
|
1154
|
-
}
|
|
865
|
+
this.scheduleProgressWrite(latestReply?.streaming === true)
|
|
1155
866
|
}
|
|
1156
867
|
if (event.type === "tool_execution_start") {
|
|
1157
868
|
this.#toolArguments.set(event.toolCallId, { name: event.toolName, arguments: renderUnknown(event.args) })
|
|
@@ -1159,37 +870,14 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1159
870
|
}
|
|
1160
871
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
1161
872
|
this.#lastAssistantOutcome = event.message.stopReason === "error" || event.message.stopReason === "aborted" ? "failed" : "succeeded"
|
|
1162
|
-
|
|
1163
|
-
const content = event.message.content
|
|
1164
|
-
.filter(part => part.type === "text")
|
|
1165
|
-
.map(part => part.text)
|
|
1166
|
-
.join("")
|
|
873
|
+
const content = messageText(event.message.content)
|
|
1167
874
|
if (content) this.queueEventWrite(() => appendHistoryLog(this.#paths, { type: "assistant", content }))
|
|
1168
875
|
return
|
|
1169
876
|
}
|
|
1170
|
-
if (event.type === "message_start" && event.message.role === "user"
|
|
1171
|
-
this.#
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (event.type === "message_start" && event.message.role === "user" && this.#pendingSteers.length > 0) {
|
|
1175
|
-
const delivered = this.#pendingSteers.shift()
|
|
1176
|
-
if (delivered) {
|
|
1177
|
-
const content =
|
|
1178
|
-
typeof event.message.content === "string"
|
|
1179
|
-
? event.message.content
|
|
1180
|
-
: event.message.content
|
|
1181
|
-
.filter(part => part.type === "text")
|
|
1182
|
-
.map(part => part.text)
|
|
1183
|
-
.join("")
|
|
1184
|
-
this.queueEventWrite(() =>
|
|
1185
|
-
appendHistoryLog(this.#paths, {
|
|
1186
|
-
type: "input",
|
|
1187
|
-
delivery: "steer",
|
|
1188
|
-
timestamp: Date.now(),
|
|
1189
|
-
content: content || delivered.content
|
|
1190
|
-
})
|
|
1191
|
-
)
|
|
1192
|
-
}
|
|
877
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
878
|
+
const entry = { type: this.#sawInput ? ("steer" as const) : ("user" as const), content: messageText(event.message.content) }
|
|
879
|
+
this.#sawInput = true
|
|
880
|
+
this.queueEventWrite(() => appendHistoryLog(this.#paths, entry))
|
|
1193
881
|
return
|
|
1194
882
|
}
|
|
1195
883
|
if (event.type === "tool_execution_end") {
|
|
@@ -1207,11 +895,45 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1207
895
|
}
|
|
1208
896
|
}
|
|
1209
897
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
898
|
+
/**
|
|
899
|
+
* Each write rewrites and fsyncs the whole snapshot, so streaming deltas are
|
|
900
|
+
* capped at two per second; completed replies and tool activity flush at once.
|
|
901
|
+
*/
|
|
902
|
+
private scheduleProgressWrite(deferrable: boolean): void {
|
|
903
|
+
const wait = deferrable ? this.#lastProgressWriteAt + 500 - Date.now() : 0
|
|
904
|
+
if (wait > 0) {
|
|
905
|
+
this.#progressTimer ??= setTimeout(() => this.scheduleProgressWrite(false), wait)
|
|
906
|
+
this.#progressTimer.unref()
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
clearTimeout(this.#progressTimer)
|
|
910
|
+
this.#progressTimer = undefined
|
|
911
|
+
if (this.#progressWriteQueued) return
|
|
912
|
+
this.#progressWriteQueued = true
|
|
913
|
+
this.queueEventWrite(async () => {
|
|
914
|
+
try {
|
|
915
|
+
// Coalesce replies and activity together so bursts cannot reorder their last action.
|
|
916
|
+
// Drain inside this one chain link: settlement awaits the chain it saw, so a
|
|
917
|
+
// terminal reply arriving mid-write must not be deferred to a later link.
|
|
918
|
+
while (this.#pendingProgress) {
|
|
919
|
+
if (this.#pendingProgress.latestReply?.streaming === true && this.#lastProgressWriteAt + 500 > Date.now()) break
|
|
920
|
+
const { runId, ...progress } = this.#pendingProgress
|
|
921
|
+
this.#pendingProgress = undefined
|
|
922
|
+
this.#lastProgressWriteAt = Date.now()
|
|
923
|
+
await writeTaskProgress(this.#paths, runId, progress)
|
|
924
|
+
}
|
|
925
|
+
} finally {
|
|
926
|
+
this.#progressWriteQueued = false
|
|
927
|
+
// Only a throttled streaming snapshot can remain.
|
|
928
|
+
if (this.#pendingProgress) this.scheduleProgressWrite(true)
|
|
929
|
+
}
|
|
1213
930
|
})
|
|
1214
931
|
}
|
|
932
|
+
|
|
933
|
+
private queueEventWrite(write: () => Promise<void>): void {
|
|
934
|
+
// history.md and progress snapshots are observability; their I/O errors never decide a run's outcome.
|
|
935
|
+
this.#eventWrites = this.#eventWrites.then(write).catch(() => {})
|
|
936
|
+
}
|
|
1215
937
|
}
|
|
1216
938
|
|
|
1217
939
|
class RuntimeClosingError extends Error {
|
|
@@ -1307,8 +1029,7 @@ function coldRuntimeLoads(): Map<string, Promise<AgentRuntime>> {
|
|
|
1307
1029
|
function buildAgentCreationToolResult(
|
|
1308
1030
|
metadata: TaskMetadata,
|
|
1309
1031
|
detached: boolean,
|
|
1310
|
-
output: Awaited<ReturnType<typeof readRetainedOutput
|
|
1311
|
-
tasks: Awaited<ReturnType<typeof loadTaskList>>["details"]
|
|
1032
|
+
output: Awaited<ReturnType<typeof readRetainedOutput>>
|
|
1312
1033
|
): { content: [{ type: "text"; text: string }]; details: AgentCreationResult } {
|
|
1313
1034
|
if (metadata.kind !== "agent") throw new Error("Expected an Agent task")
|
|
1314
1035
|
const result: AgentCreationResult = {
|
|
@@ -1324,10 +1045,9 @@ function buildAgentCreationToolResult(
|
|
|
1324
1045
|
allowAgents: metadata.allowAgents,
|
|
1325
1046
|
detached,
|
|
1326
1047
|
queuedFollowUps: metadata.queuedFollowUps.length,
|
|
1327
|
-
output
|
|
1328
|
-
tasks
|
|
1048
|
+
output
|
|
1329
1049
|
}
|
|
1330
|
-
const pending = detached && (result.state === "queued" || result.state === "running"
|
|
1050
|
+
const pending = detached && (result.state === "queued" || result.state === "running")
|
|
1331
1051
|
return {
|
|
1332
1052
|
content: [
|
|
1333
1053
|
{
|
|
@@ -1360,6 +1080,10 @@ function createRunId(): string {
|
|
|
1360
1080
|
return `r_${randomBytes(8).toString("hex")}`
|
|
1361
1081
|
}
|
|
1362
1082
|
|
|
1083
|
+
function messageText(content: string | ReadonlyArray<{ type: string; text?: string }>): string {
|
|
1084
|
+
return typeof content === "string" ? content : content.map(part => (part.type === "text" ? (part.text ?? "") : "")).join("")
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1363
1087
|
function renderUnknown(value: unknown): string {
|
|
1364
1088
|
if (typeof value === "string") return value
|
|
1365
1089
|
try {
|
|
@@ -1369,36 +1093,6 @@ function renderUnknown(value: unknown): string {
|
|
|
1369
1093
|
}
|
|
1370
1094
|
}
|
|
1371
1095
|
|
|
1372
|
-
function suspendedRuntimes(): Map<string, Set<SuspendedRuntime>> {
|
|
1373
|
-
const global = globalThis as typeof globalThis & { [SUSPENDED_RUNTIMES_SYMBOL]?: Map<string, Set<SuspendedRuntime>> }
|
|
1374
|
-
global[SUSPENDED_RUNTIMES_SYMBOL] ??= new Map()
|
|
1375
|
-
return global[SUSPENDED_RUNTIMES_SYMBOL]
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function bindSuspendedRuntime(tuple: ModelTuple, runtime: SuspendedRuntime): () => void {
|
|
1379
|
-
const registry = suspendedRuntimes()
|
|
1380
|
-
const key = modelTupleKey(tuple)
|
|
1381
|
-
const runtimes = registry.get(key) ?? new Set()
|
|
1382
|
-
runtimes.add(runtime)
|
|
1383
|
-
registry.set(key, runtimes)
|
|
1384
|
-
return () => {
|
|
1385
|
-
runtimes.delete(runtime)
|
|
1386
|
-
if (runtimes.size === 0 && registry.get(key) === runtimes) registry.delete(key)
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
function modelTupleKey(tuple: ModelTuple): string {
|
|
1391
|
-
return `${tuple.provider}\0${tuple.model}`
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
1096
|
function abortError(signal: AbortSignal): Error {
|
|
1395
1097
|
return signal.reason instanceof Error ? signal.reason : new Error("Agent creation aborted")
|
|
1396
1098
|
}
|
|
1397
|
-
|
|
1398
|
-
function deferred<T>(): { promise: Promise<T>; resolve(value: T | PromiseLike<T>): void } {
|
|
1399
|
-
let resolve!: (value: T | PromiseLike<T>) => void
|
|
1400
|
-
const promise = new Promise<T>(resolvePromise => {
|
|
1401
|
-
resolve = resolvePromise
|
|
1402
|
-
})
|
|
1403
|
-
return { promise, resolve }
|
|
1404
|
-
}
|