macroclaw 0.28.0 → 0.29.0
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/package.json +1 -1
- package/src/orchestrator.test.ts +148 -0
- package/src/orchestrator.ts +123 -6
- package/src/prompts.test.ts +14 -0
- package/src/prompts.ts +12 -1
package/package.json
CHANGED
package/src/orchestrator.test.ts
CHANGED
|
@@ -920,6 +920,154 @@ describe("Orchestrator", () => {
|
|
|
920
920
|
});
|
|
921
921
|
});
|
|
922
922
|
|
|
923
|
+
describe("health checks", () => {
|
|
924
|
+
it("runs health check after interval and reports finished agent", async () => {
|
|
925
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
926
|
+
const { query: bgQuery } = pendingQuery("bg-sid");
|
|
927
|
+
|
|
928
|
+
let callCount = 0;
|
|
929
|
+
const claude = mockClaude((info: CallInfo): RunningQuery<unknown> => {
|
|
930
|
+
callCount++;
|
|
931
|
+
if (callCount === 1) return bgQuery; // background agent spawn
|
|
932
|
+
if (info.prompt.includes("health-check")) {
|
|
933
|
+
return resolvedQuery({
|
|
934
|
+
finished: true,
|
|
935
|
+
output: { action: "send", message: "task complete", actionReason: "done" },
|
|
936
|
+
}, "hc-sid");
|
|
937
|
+
}
|
|
938
|
+
// Main session processes the background-agent-result
|
|
939
|
+
return resolvedQuery({ action: "send", message: "relayed", actionReason: "ok" });
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
const { orch } = makeOrchestrator(claude, {
|
|
943
|
+
healthCheckInterval: 50,
|
|
944
|
+
healthCheckTimeout: 5000,
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
orch.handleBackgroundCommand("long task");
|
|
948
|
+
await waitForProcessing(200);
|
|
949
|
+
|
|
950
|
+
// Health check should have fired, detected finished, killed original, and pushed result
|
|
951
|
+
expect(callCount).toBeGreaterThanOrEqual(2);
|
|
952
|
+
const hcCall = claude.calls.find((c: CallInfo) => c.prompt.includes("health-check"));
|
|
953
|
+
expect(hcCall).toBeDefined();
|
|
954
|
+
expect(hcCall!.model).toBe("haiku");
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it("reports progress and schedules next check when not finished", async () => {
|
|
958
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
959
|
+
const { query: bgQuery } = pendingQuery("bg-sid");
|
|
960
|
+
|
|
961
|
+
let hcCount = 0;
|
|
962
|
+
const claude = mockClaude((info: CallInfo): RunningQuery<unknown> => {
|
|
963
|
+
if (info.prompt.includes("health-check")) {
|
|
964
|
+
hcCount++;
|
|
965
|
+
return resolvedQuery({ finished: false, progress: "still working" }, "hc-sid");
|
|
966
|
+
}
|
|
967
|
+
if (info.prompt.includes("background-agent-result")) {
|
|
968
|
+
return resolvedQuery({ action: "silent", message: "ok", actionReason: "progress" });
|
|
969
|
+
}
|
|
970
|
+
return bgQuery; // background agent spawn
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
const { orch } = makeOrchestrator(claude, {
|
|
974
|
+
healthCheckInterval: 50,
|
|
975
|
+
healthCheckTimeout: 5000,
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
orch.handleBackgroundCommand("long task");
|
|
979
|
+
// Wait for two health check cycles
|
|
980
|
+
await waitForProcessing(250);
|
|
981
|
+
|
|
982
|
+
expect(hcCount).toBeGreaterThanOrEqual(2);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it("kills unresponsive agent on health check timeout", async () => {
|
|
986
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
987
|
+
const { query: bgQuery } = pendingQuery("bg-sid");
|
|
988
|
+
|
|
989
|
+
const claude = mockClaude((info: CallInfo): RunningQuery<unknown> => {
|
|
990
|
+
if (info.prompt.includes("health-check")) {
|
|
991
|
+
// Never resolves — simulates unresponsive agent
|
|
992
|
+
return { sessionId: "hc-sid", startedAt: new Date(), result: new Promise(() => {}), kill: mock(async () => {}) };
|
|
993
|
+
}
|
|
994
|
+
return bgQuery;
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const { orch, responses } = makeOrchestrator(claude, {
|
|
998
|
+
healthCheckInterval: 30,
|
|
999
|
+
healthCheckTimeout: 60,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
orch.handleBackgroundCommand("stuck task");
|
|
1003
|
+
await waitForProcessing(200);
|
|
1004
|
+
|
|
1005
|
+
const killMsg = responses.find((r) => r.message.includes("unresponsive"));
|
|
1006
|
+
expect(killMsg).toBeDefined();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it("does not run health checks when interval is 0", async () => {
|
|
1010
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
1011
|
+
const { query: bgQuery } = pendingQuery("bg-sid");
|
|
1012
|
+
|
|
1013
|
+
const claude = mockClaude((): RunningQuery<unknown> => bgQuery);
|
|
1014
|
+
const { orch } = makeOrchestrator(claude, { healthCheckInterval: 0 });
|
|
1015
|
+
|
|
1016
|
+
orch.handleBackgroundCommand("some task");
|
|
1017
|
+
await waitForProcessing(100);
|
|
1018
|
+
|
|
1019
|
+
// Only the spawn call, no health check fork
|
|
1020
|
+
expect(claude.calls).toHaveLength(1);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it("clears health check timer when session is killed", async () => {
|
|
1024
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
1025
|
+
const { query: bgQuery } = pendingQuery("bg-sid");
|
|
1026
|
+
|
|
1027
|
+
let hcCount = 0;
|
|
1028
|
+
const claude = mockClaude((info: CallInfo): RunningQuery<unknown> => {
|
|
1029
|
+
if (info.prompt.includes("health-check")) hcCount++;
|
|
1030
|
+
return bgQuery;
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
const { orch } = makeOrchestrator(claude, { healthCheckInterval: 100 });
|
|
1034
|
+
|
|
1035
|
+
orch.handleBackgroundCommand("killable task");
|
|
1036
|
+
await waitForProcessing();
|
|
1037
|
+
|
|
1038
|
+
// Get the session ID and kill it before health check fires
|
|
1039
|
+
orch.handleKill("bg-sid");
|
|
1040
|
+
await waitForProcessing(200);
|
|
1041
|
+
|
|
1042
|
+
expect(hcCount).toBe(0);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it("stops health check if session completes organically before timer", async () => {
|
|
1046
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
1047
|
+
const { query: bgQuery, resolve: resolveBg } = pendingQuery("bg-sid");
|
|
1048
|
+
|
|
1049
|
+
let hcCount = 0;
|
|
1050
|
+
let callCount = 0;
|
|
1051
|
+
const claude = mockClaude((info: CallInfo): RunningQuery<unknown> => {
|
|
1052
|
+
callCount++;
|
|
1053
|
+
if (info.prompt.includes("health-check")) { hcCount++; return pendingQuery().query; }
|
|
1054
|
+
if (callCount === 1) return bgQuery;
|
|
1055
|
+
return resolvedQuery({ action: "send", message: "processed", actionReason: "ok" });
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
const { orch } = makeOrchestrator(claude, { healthCheckInterval: 200 });
|
|
1059
|
+
|
|
1060
|
+
orch.handleBackgroundCommand("fast task");
|
|
1061
|
+
await waitForProcessing();
|
|
1062
|
+
|
|
1063
|
+
// Complete before health check fires
|
|
1064
|
+
resolveBg(queryResult({ action: "send", message: "done", actionReason: "done" }));
|
|
1065
|
+
await waitForProcessing(350);
|
|
1066
|
+
|
|
1067
|
+
expect(hcCount).toBe(0);
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
|
|
923
1071
|
describe("onResponse error handling", () => {
|
|
924
1072
|
it("logs error and does not throw when onResponse callback fails", async () => {
|
|
925
1073
|
const claude = mockClaude({ action: "send", message: "hello", actionReason: "ok" });
|
package/src/orchestrator.ts
CHANGED
|
@@ -20,6 +20,8 @@ const log = createLogger("orchestrator");
|
|
|
20
20
|
// --- Constants ---
|
|
21
21
|
|
|
22
22
|
const WAIT_THRESHOLD = 60_000;
|
|
23
|
+
const HEALTH_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
|
24
|
+
const HEALTH_CHECK_TIMEOUT_MS = 120 * 1000;
|
|
23
25
|
|
|
24
26
|
// --- Response schema ---
|
|
25
27
|
|
|
@@ -40,7 +42,14 @@ const agentOutputSchema = z.object({
|
|
|
40
42
|
|
|
41
43
|
type AgentOutput = z.infer<typeof agentOutputSchema>;
|
|
42
44
|
|
|
45
|
+
const healthCheckSchema = z.object({
|
|
46
|
+
finished: z.boolean().describe("True if the task is complete, false if still working"),
|
|
47
|
+
output: agentOutputSchema.optional().describe("Full output when finished=true"),
|
|
48
|
+
progress: z.string().optional().describe("One-sentence status when finished=false"),
|
|
49
|
+
});
|
|
50
|
+
|
|
43
51
|
const responseResultType = { type: "object" as const, schema: agentOutputSchema };
|
|
52
|
+
const healthCheckResultType = { type: "object" as const, schema: healthCheckSchema };
|
|
44
53
|
|
|
45
54
|
const textResultType = { type: "text" } as const;
|
|
46
55
|
|
|
@@ -60,6 +69,7 @@ export interface OrchestratorResponse {
|
|
|
60
69
|
type OrchestratorRequest =
|
|
61
70
|
| { type: "user"; message: string; files?: string[] }
|
|
62
71
|
| { type: "background-agent-result"; name: string; response: AgentOutput }
|
|
72
|
+
| { type: "background-agent-progress"; name: string; progress: string }
|
|
63
73
|
| { type: "button"; label: string };
|
|
64
74
|
|
|
65
75
|
function escapeHtml(text: string): string {
|
|
@@ -74,6 +84,7 @@ interface SessionInfo {
|
|
|
74
84
|
model?: string;
|
|
75
85
|
query: RunningQuery<AgentOutput>;
|
|
76
86
|
lastMessageAt: Date;
|
|
87
|
+
healthCheckTimer?: Timer;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
export interface OrchestratorConfig {
|
|
@@ -84,12 +95,18 @@ export interface OrchestratorConfig {
|
|
|
84
95
|
claude?: Claude;
|
|
85
96
|
/** How long to wait for a running main session before demoting it (ms). Default: 60000 */
|
|
86
97
|
waitThreshold?: number;
|
|
98
|
+
/** Interval between background agent health checks (ms). Default: 300000. Set to 0 to disable. */
|
|
99
|
+
healthCheckInterval?: number;
|
|
100
|
+
/** Timeout for health check fork responses (ms). Default: 120000 */
|
|
101
|
+
healthCheckTimeout?: number;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
export class Orchestrator {
|
|
90
105
|
#config: Omit<OrchestratorConfig , 'claude'>;
|
|
91
106
|
#claude: Claude;
|
|
92
107
|
#waitThreshold: number;
|
|
108
|
+
#healthCheckInterval: number;
|
|
109
|
+
#healthCheckTimeout: number;
|
|
93
110
|
|
|
94
111
|
#mainSessionId: string | undefined;
|
|
95
112
|
#runningSessions = new Map<string, SessionInfo>();
|
|
@@ -99,6 +116,8 @@ export class Orchestrator {
|
|
|
99
116
|
this.#config = config;
|
|
100
117
|
this.#claude = config.claude ?? new Claude({ workspace: config.workspace, systemPrompt: SYSTEM_PROMPT });
|
|
101
118
|
this.#waitThreshold = config.waitThreshold ?? WAIT_THRESHOLD;
|
|
119
|
+
this.#healthCheckInterval = config.healthCheckInterval ?? HEALTH_CHECK_INTERVAL_MS;
|
|
120
|
+
this.#healthCheckTimeout = config.healthCheckTimeout ?? HEALTH_CHECK_TIMEOUT_MS;
|
|
102
121
|
this.#queue = new Queue<OrchestratorRequest>();
|
|
103
122
|
this.#queue.setHandler((request) => this.#handleRequest(request));
|
|
104
123
|
|
|
@@ -217,7 +236,7 @@ export class Orchestrator {
|
|
|
217
236
|
return;
|
|
218
237
|
}
|
|
219
238
|
|
|
220
|
-
this.#
|
|
239
|
+
this.#clearSession(sessionId);
|
|
221
240
|
|
|
222
241
|
try {
|
|
223
242
|
await session.query.kill();
|
|
@@ -330,7 +349,7 @@ export class Orchestrator {
|
|
|
330
349
|
await this.#deliverResponse(response);
|
|
331
350
|
return;
|
|
332
351
|
}
|
|
333
|
-
this.#
|
|
352
|
+
this.#clearSession(sid);
|
|
334
353
|
|
|
335
354
|
if (sid === this.#mainSessionId) {
|
|
336
355
|
log.debug({ name, sessionId: sid }, "Main query finished, delivering directly");
|
|
@@ -344,7 +363,7 @@ export class Orchestrator {
|
|
|
344
363
|
if (!this.#runningSessions.has(sid)) {
|
|
345
364
|
log.error({ name, sessionId: sid, err }, "Failed session not in runningSessions — delivering error");
|
|
346
365
|
} else {
|
|
347
|
-
this.#
|
|
366
|
+
this.#clearSession(sid);
|
|
348
367
|
log.error({ name, sessionId: sid, err }, "Main query failed");
|
|
349
368
|
}
|
|
350
369
|
await this.#deliverResponse(this.#errorResponse(err));
|
|
@@ -380,6 +399,17 @@ export class Orchestrator {
|
|
|
380
399
|
instructions: "Forward this result to the user (action=\"send\"). Summarize or add context from the conversation as appropriate.",
|
|
381
400
|
};
|
|
382
401
|
break;
|
|
402
|
+
case "background-agent-progress":
|
|
403
|
+
input = {
|
|
404
|
+
name,
|
|
405
|
+
type: "background-agent-progress",
|
|
406
|
+
session: "main",
|
|
407
|
+
originalEvent: request.name,
|
|
408
|
+
progress: request.progress,
|
|
409
|
+
instructions: "This is an interim progress update, not a final result. Do not report to the user unless it contains exceptionally important information.",
|
|
410
|
+
backgroundedEvent,
|
|
411
|
+
};
|
|
412
|
+
break;
|
|
383
413
|
case "button":
|
|
384
414
|
input = {
|
|
385
415
|
name,
|
|
@@ -400,6 +430,8 @@ export class Orchestrator {
|
|
|
400
430
|
return request.message;
|
|
401
431
|
case "background-agent-result":
|
|
402
432
|
return `bg:${request.name}`;
|
|
433
|
+
case "background-agent-progress":
|
|
434
|
+
return `progress:${request.name}`;
|
|
403
435
|
case "button":
|
|
404
436
|
return `btn:${request.label}`;
|
|
405
437
|
}
|
|
@@ -441,23 +473,108 @@ export class Orchestrator {
|
|
|
441
473
|
|
|
442
474
|
#registerBackground(name: string, prompt: string, model: string | undefined, query: RunningQuery<AgentOutput>) {
|
|
443
475
|
const sid = query.sessionId;
|
|
444
|
-
|
|
476
|
+
const info: SessionInfo = { name, prompt, model, query, lastMessageAt: new Date() };
|
|
477
|
+
this.#runningSessions.set(sid, info);
|
|
445
478
|
|
|
446
479
|
log.debug({ name, sessionId: sid }, "Background session registered");
|
|
447
480
|
|
|
481
|
+
this.#scheduleHealthCheck(sid);
|
|
482
|
+
|
|
448
483
|
query.result.then(
|
|
449
484
|
({ value: response }) => {
|
|
450
485
|
if (!this.#runningSessions.has(sid)) return;
|
|
451
|
-
this.#
|
|
486
|
+
this.#clearSession(sid);
|
|
452
487
|
log.debug({ name, message: response.message }, "Background session finished");
|
|
453
488
|
this.#queue.push({ type: "background-agent-result", name, response });
|
|
454
489
|
},
|
|
455
490
|
(err) => {
|
|
456
491
|
if (!this.#runningSessions.has(sid)) return;
|
|
457
|
-
this.#
|
|
492
|
+
this.#clearSession(sid);
|
|
458
493
|
log.error({ name, err }, "Background session failed");
|
|
459
494
|
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-failed" } });
|
|
460
495
|
},
|
|
461
496
|
);
|
|
462
497
|
}
|
|
498
|
+
|
|
499
|
+
// --- Session cleanup ---
|
|
500
|
+
|
|
501
|
+
#clearSession(sessionId: string) {
|
|
502
|
+
const info = this.#runningSessions.get(sessionId);
|
|
503
|
+
if (info?.healthCheckTimer) clearTimeout(info.healthCheckTimer);
|
|
504
|
+
this.#runningSessions.delete(sessionId);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// --- Health checks ---
|
|
508
|
+
|
|
509
|
+
#scheduleHealthCheck(sessionId: string) {
|
|
510
|
+
if (this.#healthCheckInterval <= 0) return;
|
|
511
|
+
|
|
512
|
+
const info = this.#runningSessions.get(sessionId);
|
|
513
|
+
if (!info) return;
|
|
514
|
+
|
|
515
|
+
info.healthCheckTimer = setTimeout(() => {
|
|
516
|
+
this.#runHealthCheck(sessionId);
|
|
517
|
+
}, this.#healthCheckInterval);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async #runHealthCheck(sessionId: string): Promise<void> {
|
|
521
|
+
const info = this.#runningSessions.get(sessionId);
|
|
522
|
+
if (!info) return;
|
|
523
|
+
|
|
524
|
+
log.debug({ name: info.name, sessionId }, "Running health check");
|
|
525
|
+
|
|
526
|
+
const prompt = buildEvent({
|
|
527
|
+
name: `health-check-${info.name}`,
|
|
528
|
+
type: "health-check",
|
|
529
|
+
session: "background",
|
|
530
|
+
targetEvent: info.name,
|
|
531
|
+
instructions: "Report your current status. If your task is complete, set finished=true and provide the full output. If still working, set finished=false and describe current progress in one sentence.",
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
let query: RunningQuery<z.infer<typeof healthCheckSchema>>;
|
|
535
|
+
try {
|
|
536
|
+
query = this.#claude.forkSession(sessionId, prompt, healthCheckResultType, { model: "haiku" });
|
|
537
|
+
} catch (err) {
|
|
538
|
+
log.error({ name: info.name, sessionId, err }, "Health check fork failed");
|
|
539
|
+
this.#scheduleHealthCheck(sessionId);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const result = await Promise.race([
|
|
544
|
+
query.result.then((r) => r.value),
|
|
545
|
+
new Promise<"timeout">((r) => setTimeout(() => r("timeout"), this.#healthCheckTimeout)),
|
|
546
|
+
]);
|
|
547
|
+
|
|
548
|
+
// Session may have completed/been killed while health check was running
|
|
549
|
+
if (!this.#runningSessions.has(sessionId)) return;
|
|
550
|
+
|
|
551
|
+
if (result === "timeout") {
|
|
552
|
+
log.warn({ name: info.name, sessionId }, "Health check timed out, killing session");
|
|
553
|
+
try { await query.kill(); } catch { /* ignore */ }
|
|
554
|
+
this.#clearSession(sessionId);
|
|
555
|
+
try { await info.query.kill(); } catch { /* ignore */ }
|
|
556
|
+
this.#callOnResponse({ message: `Agent <b>${escapeHtml(info.name)}</b> appears unresponsive, killed it.` });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (result.finished) {
|
|
561
|
+
log.info({ name: info.name, sessionId }, "Health check: agent reports finished");
|
|
562
|
+
this.#clearSession(sessionId);
|
|
563
|
+
try { await info.query.kill(); } catch { /* ignore */ }
|
|
564
|
+
const response = result.output ?? { action: "send" as const, message: "[Agent finished but returned no output]", actionReason: "health-check-finished" };
|
|
565
|
+
this.#queue.push({ type: "background-agent-result", name: info.name, response });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
log.debug({ name: info.name, progress: result.progress }, "Health check: still running");
|
|
570
|
+
if (result.progress) {
|
|
571
|
+
this.#queue.push({
|
|
572
|
+
type: "background-agent-progress",
|
|
573
|
+
name: info.name,
|
|
574
|
+
progress: result.progress,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.#scheduleHealthCheck(sessionId);
|
|
579
|
+
}
|
|
463
580
|
}
|
package/src/prompts.test.ts
CHANGED
|
@@ -231,6 +231,20 @@ describe("buildEvent", () => {
|
|
|
231
231
|
expect(result).not.toContain("<text>");
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
it("builds progress event with progress tag", () => {
|
|
235
|
+
const result = buildEvent({
|
|
236
|
+
name: "progress-research",
|
|
237
|
+
type: "background-agent-progress",
|
|
238
|
+
session: "main",
|
|
239
|
+
originalEvent: "research",
|
|
240
|
+
progress: "indexing 500 documents",
|
|
241
|
+
});
|
|
242
|
+
expect(result).toContain('type="background-agent-progress"');
|
|
243
|
+
expect(result).toContain('<original-event name="research" />');
|
|
244
|
+
expect(result).toContain("<progress>indexing 500 documents</progress>");
|
|
245
|
+
expect(result).not.toContain("<result>");
|
|
246
|
+
});
|
|
247
|
+
|
|
234
248
|
it("includes instructions in event", () => {
|
|
235
249
|
const result = buildEvent({
|
|
236
250
|
name: "bg-research",
|
package/src/prompts.ts
CHANGED
|
@@ -23,7 +23,9 @@ Event format: every incoming message is wrapped in an <event> XML block. Attribu
|
|
|
23
23
|
- schedule-trigger — automated scheduled task. Contains <schedule> with name and optional missed-by/scheduled-at attributes. Prefer action="silent" when nothing noteworthy.
|
|
24
24
|
- background-agent-start — you are a background agent. Complete the task in <text> and return a result.
|
|
25
25
|
- background-agent-result — a background agent has finished. Contains <original-event name="..." /> linking to the agent that produced it, and a <result> block with <text> and optional <files>. Always use action="send" — the user expects to see the outcome. Summarize, relay, or add additional context from the conversation as appropriate.
|
|
26
|
+
- background-agent-progress — interim progress update from a still-running background agent. Contains <original-event name="..." /> and a <progress> element. This is NOT a final result. Do not report to the user unless it contains exceptionally important information (errors, blockers, urgent findings). Keep this context in mind — if the user later asks about progress of a background task, use the latest progress update to answer.
|
|
26
27
|
- peek — status check on a running session. Contains <target-event name="..." /> identifying the event being peeked at. Only consider progress since that event started. Respond with a brief status update (2-3 sentences): what has been done, what's happening now, what's remaining. Return plain text, not structured output.
|
|
28
|
+
- health-check — automated status check on a background agent. Contains <target-event name="..." />. Report whether the task is complete or still in progress.
|
|
27
29
|
- session — "main" (primary conversation) or "background" (background agent).
|
|
28
30
|
|
|
29
31
|
Backgrounded events: when a new message arrives while a previous task is still running, \
|
|
@@ -40,6 +42,7 @@ Inner elements:
|
|
|
40
42
|
- <backgrounded-event name="..." /> — a previously running task moved to background (see above).
|
|
41
43
|
- <original-event name="..." /> — in background-agent-result, links to the agent that produced the result.
|
|
42
44
|
- <target-event name="..." /> — in peek, identifies the event being checked on.
|
|
45
|
+
- <progress> — interim status from a still-running background agent.
|
|
43
46
|
- <result> — wraps the output from a completed background agent. Contains <text> and optional <files>.
|
|
44
47
|
- <instructions> — inline guidance for how to handle this specific event. Always follow these instructions.
|
|
45
48
|
|
|
@@ -76,7 +79,9 @@ export type EventType =
|
|
|
76
79
|
| "schedule-trigger"
|
|
77
80
|
| "background-agent-start"
|
|
78
81
|
| "background-agent-result"
|
|
79
|
-
| "
|
|
82
|
+
| "background-agent-progress"
|
|
83
|
+
| "peek"
|
|
84
|
+
| "health-check";
|
|
80
85
|
|
|
81
86
|
export interface EventInput {
|
|
82
87
|
name: string;
|
|
@@ -90,6 +95,7 @@ export interface EventInput {
|
|
|
90
95
|
originalEvent?: string;
|
|
91
96
|
targetEvent?: string;
|
|
92
97
|
instructions?: string;
|
|
98
|
+
progress?: string;
|
|
93
99
|
result?: { text: string; files?: string[] };
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -121,6 +127,11 @@ export function buildEvent(input: EventInput): string {
|
|
|
121
127
|
lines.push(`<target-event name="${escapeXml(input.targetEvent)}" />`);
|
|
122
128
|
}
|
|
123
129
|
|
|
130
|
+
// Progress (for background-agent-progress)
|
|
131
|
+
if (input.progress) {
|
|
132
|
+
lines.push(`<progress>${escapeXml(input.progress)}</progress>`);
|
|
133
|
+
}
|
|
134
|
+
|
|
124
135
|
// Result block (for background-agent-result)
|
|
125
136
|
if (input.result) {
|
|
126
137
|
lines.push("<result>");
|