@sema-agent/core 2.0.1 → 2.2.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/dist/agents/observer.d.ts +14 -0
- package/dist/agents/observer.js +66 -12
- package/dist/agents/send-message-tool.js +164 -88
- package/dist/agents/subagent.d.ts +11 -4
- package/dist/agents/subagent.js +166 -62
- package/dist/core/context-edit.js +16 -3
- package/dist/core/file-snapshot-store.js +10 -1
- package/dist/core/memory-engine/dual-root.js +2 -0
- package/dist/core/memory-engine/engine.d.ts +4 -0
- package/dist/core/memory-engine/engine.js +6 -1
- package/dist/core/runner/prepare-memory.d.ts +4 -0
- package/dist/core/runner/prepare-memory.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +9 -2
- package/dist/core/runner/prepare-task.js +68 -13
- package/dist/core/runner/runtask.js +919 -865
- package/dist/core/runner/synthetic-tools.d.ts +1 -0
- package/dist/core/runner/synthetic-tools.js +18 -15
- package/dist/core/runner/turn-attachments.d.ts +27 -3
- package/dist/core/runner/turn-attachments.js +101 -12
- package/dist/core/task-registry-agent.d.ts +9 -1
- package/dist/core/task-registry-agent.js +23 -2
- package/dist/core/task-registry-monitor.js +79 -24
- package/dist/core/task-registry-shared.d.ts +13 -1
- package/dist/core/task-registry-shared.js +21 -0
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/task-registry.js +24 -26
- package/dist/core/tool-result-budget.js +2 -2
- package/dist/core/tool-result-store.d.ts +2 -0
- package/dist/core/tool-result-store.js +27 -2
- package/dist/core/types.d.ts +4 -2
- package/dist/core/workflow-journal-store.d.ts +13 -0
- package/dist/engine/session/import-validate.js +29 -0
- package/dist/engine/session/memory-repo.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow-size-guideline.d.ts +6 -1
- package/dist/orchestration/workflow-size-guideline.js +19 -9
- package/dist/orchestration/workflow.d.ts +1 -0
- package/dist/orchestration/workflow.js +44 -1
- package/dist/prompt-assembly/assemble.js +3 -7
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/prompt-assembly/packs/sema-default.js +8 -5
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/coordinator.js +45 -0
- package/dist/prompts/default.d.ts +4 -5
- package/dist/prompts/default.js +16 -18
- package/dist/prompts/simple-sections.d.ts +3 -1
- package/dist/prompts/simple-sections.js +11 -1
- package/dist/stores/cc/task-list-store.js +3 -3
- package/dist/stores/file/memory-store.d.ts +3 -0
- package/dist/stores/file/memory-store.js +39 -12
- package/dist/stores/file/tool-result-store.js +16 -2
- package/dist/stores/file/workflow-journal-store.d.ts +23 -0
- package/dist/stores/file/workflow-journal-store.js +140 -3
- package/dist/tools/fs/bash-readonly-classifier.js +21 -2
- package/dist/tools/fs/fs-bash.d.ts +1 -0
- package/dist/tools/fs/fs-bash.js +10 -3
- package/dist/tools/fs/fs-read.js +12 -12
- package/dist/tools/fs/fs-search-tools.js +42 -7
- package/dist/tools/fs/fs-write.js +18 -6
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +2 -1
- package/dist/tools/fs/safety.d.ts +4 -0
- package/dist/tools/fs/safety.js +113 -10
- package/dist/tools/fs/search.d.ts +1 -0
- package/dist/tools/fs/search.js +23 -3
- package/dist/tools/monitor.js +1 -1
- package/dist/tools/task-list.d.ts +1 -0
- package/dist/tools/task-list.js +13 -2
- package/dist/tools/web.js +36 -6
- package/package.json +3 -2
|
@@ -68,6 +68,15 @@ export declare class ObserverDigestTap {
|
|
|
68
68
|
finish(reason: string): void;
|
|
69
69
|
}
|
|
70
70
|
export type ObserverPairingState = "armed" | "denied" | "stopped" | "retired" | "blocked";
|
|
71
|
+
export declare const OBSERVER_STOPPED_BY_USER_ERROR_NAME = "AgentStoppedByUserError";
|
|
72
|
+
export declare const OBSERVER_RESUME_STATE_ERROR_NAME = "ResumeAgentStateError";
|
|
73
|
+
export declare class ObserverStoppedByUserError extends Error {
|
|
74
|
+
constructor(message: string);
|
|
75
|
+
}
|
|
76
|
+
export declare class ObserverResumeStateError extends Error {
|
|
77
|
+
constructor(message: string);
|
|
78
|
+
}
|
|
79
|
+
export declare const OBSERVER_FRESH_START_NOTE = "[Note: your previous observation context was lost; this is a fresh start mid-task.]";
|
|
71
80
|
export interface ObserverSpawner {
|
|
72
81
|
spawnFirstRun(args: {
|
|
73
82
|
framingPrompt: string;
|
|
@@ -76,6 +85,10 @@ export interface ObserverSpawner {
|
|
|
76
85
|
deliver(args: {
|
|
77
86
|
digest: string;
|
|
78
87
|
}): Promise<void>;
|
|
88
|
+
restartFresh?(args: {
|
|
89
|
+
framingPrompt: string;
|
|
90
|
+
digest: string;
|
|
91
|
+
}): Promise<void>;
|
|
79
92
|
}
|
|
80
93
|
export declare class ObserverPairing {
|
|
81
94
|
state: ObserverPairingState;
|
|
@@ -102,6 +115,7 @@ export declare class ObserverPairing {
|
|
|
102
115
|
private safeOnError;
|
|
103
116
|
drain(): Promise<void>;
|
|
104
117
|
private pump;
|
|
118
|
+
private deliverBatch;
|
|
105
119
|
retire(state: Exclude<ObserverPairingState, "armed">): void;
|
|
106
120
|
}
|
|
107
121
|
export declare function markObserverTaskId(taskId: string): void;
|
package/dist/agents/observer.js
CHANGED
|
@@ -157,10 +157,15 @@ export class ObserverDigestTap {
|
|
|
157
157
|
this.flushText();
|
|
158
158
|
this.pending.push({ type: "tool_result", content: truncateDigestPayload(digestTextOf(e.output)) });
|
|
159
159
|
return;
|
|
160
|
-
case "steering_injected":
|
|
160
|
+
case "steering_injected": {
|
|
161
161
|
this.flushText();
|
|
162
|
-
|
|
162
|
+
const injected = e.preview.trim();
|
|
163
|
+
this.pending.push({
|
|
164
|
+
type: "user_message",
|
|
165
|
+
text: injected ? `[injected: ${e.source}]\n${injected}` : `[injected: ${e.source}] (empty preview)`,
|
|
166
|
+
});
|
|
163
167
|
return;
|
|
168
|
+
}
|
|
164
169
|
case "turn_end":
|
|
165
170
|
this.flushSegment();
|
|
166
171
|
return;
|
|
@@ -204,6 +209,24 @@ export class ObserverDigestTap {
|
|
|
204
209
|
this.safeFlush(slice);
|
|
205
210
|
}
|
|
206
211
|
}
|
|
212
|
+
export const OBSERVER_STOPPED_BY_USER_ERROR_NAME = "AgentStoppedByUserError";
|
|
213
|
+
export const OBSERVER_RESUME_STATE_ERROR_NAME = "ResumeAgentStateError";
|
|
214
|
+
export class ObserverStoppedByUserError extends Error {
|
|
215
|
+
constructor(message) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.name = OBSERVER_STOPPED_BY_USER_ERROR_NAME;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export class ObserverResumeStateError extends Error {
|
|
221
|
+
constructor(message) {
|
|
222
|
+
super(message);
|
|
223
|
+
this.name = OBSERVER_RESUME_STATE_ERROR_NAME;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function errorName(err) {
|
|
227
|
+
return err instanceof Error ? err.name : undefined;
|
|
228
|
+
}
|
|
229
|
+
export const OBSERVER_FRESH_START_NOTE = "[Note: your previous observation context was lost; this is a fresh start mid-task.]";
|
|
207
230
|
export class ObserverPairing {
|
|
208
231
|
state = "armed";
|
|
209
232
|
observedEnvelopeName;
|
|
@@ -273,18 +296,22 @@ export class ObserverPairing {
|
|
|
273
296
|
const batch = this.buffer.splice(0, this.buffer.length);
|
|
274
297
|
try {
|
|
275
298
|
const digest = renderObserverDigestBatch(this, batch);
|
|
276
|
-
|
|
277
|
-
await this.spawner.spawnFirstRun({ framingPrompt: this.framingPrompt, digest });
|
|
278
|
-
this.firstRunDone = true;
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
await this.spawner.deliver({ digest });
|
|
282
|
-
}
|
|
299
|
+
await this.deliverBatch(digest);
|
|
283
300
|
}
|
|
284
301
|
catch (err) {
|
|
285
|
-
|
|
286
|
-
|
|
302
|
+
if (errorName(err) === OBSERVER_STOPPED_BY_USER_ERROR_NAME) {
|
|
303
|
+
this.state = "stopped";
|
|
304
|
+
this.buffer = [];
|
|
305
|
+
this.safeOnError(err);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
287
308
|
this.safeOnError(err);
|
|
309
|
+
if (this.buffer.length > 0) {
|
|
310
|
+
const strandedCount = this.buffer.length;
|
|
311
|
+
this.buffer = [];
|
|
312
|
+
this.safeOnError(new Error(`observer delivery: ${strandedCount} segment(s) enqueued mid-fault were stranded and dropped (never attempted) after: ${err instanceof Error ? err.message : String(err)}`));
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
288
315
|
}
|
|
289
316
|
}
|
|
290
317
|
}
|
|
@@ -292,6 +319,33 @@ export class ObserverPairing {
|
|
|
292
319
|
this.delivering = false;
|
|
293
320
|
}
|
|
294
321
|
}
|
|
322
|
+
async deliverBatch(digest) {
|
|
323
|
+
if (!this.firstRunDone) {
|
|
324
|
+
await this.spawner.spawnFirstRun({ framingPrompt: this.framingPrompt, digest });
|
|
325
|
+
this.firstRunDone = true;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
await this.spawner.deliver({ digest });
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (errorName(err) !== OBSERVER_RESUME_STATE_ERROR_NAME)
|
|
333
|
+
throw err;
|
|
334
|
+
const restart = this.spawner.restartFresh?.bind(this.spawner);
|
|
335
|
+
if (restart === undefined)
|
|
336
|
+
throw err;
|
|
337
|
+
try {
|
|
338
|
+
await restart({ framingPrompt: `${this.framingPrompt}\n\n${OBSERVER_FRESH_START_NOTE}`, digest });
|
|
339
|
+
this.firstRunDone = true;
|
|
340
|
+
}
|
|
341
|
+
catch (restartErr) {
|
|
342
|
+
this.firstRunDone = false;
|
|
343
|
+
throw restartErr instanceof Error
|
|
344
|
+
? new Error(`${restartErr.message} (after resume-state loss: ${err instanceof Error ? err.message : String(err)})`, { cause: restartErr })
|
|
345
|
+
: restartErr;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
295
349
|
retire(state) {
|
|
296
350
|
if (this.state !== "armed")
|
|
297
351
|
return;
|
|
@@ -326,7 +380,7 @@ export function createObserverReportToolSpec(opts) {
|
|
|
326
380
|
name: OBSERVER_REPORT_TOOL_NAME,
|
|
327
381
|
contract: { contractId: "core.observer_report@1", implementationRevision: "1" },
|
|
328
382
|
description: OBSERVER_REPORT_DESCRIPTION,
|
|
329
|
-
effect: "
|
|
383
|
+
effect: "write",
|
|
330
384
|
parameters: Type.Object({
|
|
331
385
|
report: Type.String({
|
|
332
386
|
minLength: 1,
|
|
@@ -2,13 +2,34 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { defineTool } from "../core/tools.js";
|
|
3
3
|
import { normalizeAgentName, DURABLE_AGENT_HANDLE_RE, DURABLE_AGENT_HEARTBEAT_MS } from "../core/task-registry.js";
|
|
4
4
|
import { canAccessAgentRecord } from "../core/background-agent-store.js";
|
|
5
|
-
import { isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
5
|
+
import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
6
6
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
|
|
7
7
|
import { createSubagentResume } from "./subagent.js";
|
|
8
8
|
export const SEND_MESSAGE_TOOL_NAME = "SendMessage";
|
|
9
9
|
let uplinkSeqGlobal = Date.now();
|
|
10
10
|
const UPLINK_RESULT_MAX = 8000;
|
|
11
|
+
const TEAMMATE_MESSAGE_TAG = "teammate-message";
|
|
12
|
+
function frameTeammateMessage(args) {
|
|
13
|
+
const summaryAttr = args.summary !== undefined ? ` summary="${escapeAttributeValue(args.summary)}"` : "";
|
|
14
|
+
const body = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, args.text);
|
|
15
|
+
return `<${TEAMMATE_MESSAGE_TAG} teammate_id="${escapeAttributeValue(args.from)}"${summaryAttr}>\n${body}\n</${TEAMMATE_MESSAGE_TAG}>`;
|
|
16
|
+
}
|
|
11
17
|
const REVIVE_LEASE_TTL_MS = 5 * 60_000;
|
|
18
|
+
const sendMessageTargetLanes = new Map();
|
|
19
|
+
function withTargetLane(key, fn) {
|
|
20
|
+
const prev = sendMessageTargetLanes.get(key) ?? Promise.resolve();
|
|
21
|
+
const run = prev.then(fn, fn);
|
|
22
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
23
|
+
sendMessageTargetLanes.set(key, tail);
|
|
24
|
+
void tail.then(() => {
|
|
25
|
+
if (sendMessageTargetLanes.get(key) === tail)
|
|
26
|
+
sendMessageTargetLanes.delete(key);
|
|
27
|
+
});
|
|
28
|
+
return run;
|
|
29
|
+
}
|
|
30
|
+
function targetLaneKey(scope, targetId) {
|
|
31
|
+
return JSON.stringify([scope ?? "", targetId]);
|
|
32
|
+
}
|
|
12
33
|
export function createSendMessageTool(opts) {
|
|
13
34
|
const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
|
|
14
35
|
return defineTool({
|
|
@@ -16,6 +37,7 @@ export function createSendMessageTool(opts) {
|
|
|
16
37
|
contract: { contractId: "core.send_message@1", implementationRevision: "1" },
|
|
17
38
|
executionMode: "parallel",
|
|
18
39
|
description: `Send a follow-up message to a previously spawned background agent. ` +
|
|
40
|
+
`Your plain text output is NOT visible to other agents — to communicate, you MUST call this tool. Messages addressed to you are delivered automatically; you don't check an inbox. ` +
|
|
19
41
|
`'to' is the agent's name (set at spawn via Agent({name})) or its task_id (a…). A RUNNING agent receives the ` +
|
|
20
42
|
`message at its next turn (queued — never interrupts its current work); a FINISHED agent resumes as a new ` +
|
|
21
43
|
`background run with its full prior conversation preserved, so don't re-explain what it already knows. Names ` +
|
|
@@ -29,7 +51,7 @@ export function createSendMessageTool(opts) {
|
|
|
29
51
|
parameters: Type.Object({
|
|
30
52
|
to: Type.String({ description: 'Recipient: the agent\'s name, or its task_id (a…) returned by the Agent tool with run_in_background. "main" is reserved for the spawning conversation.' }),
|
|
31
53
|
message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
|
|
32
|
-
summary: Type.Optional(Type.String({ maxLength: 200, description: "A
|
|
54
|
+
summary: Type.Optional(Type.String({ maxLength: 200, description: "A 5-10 word summary shown as a preview in the UI (required when message is a string)" })),
|
|
33
55
|
}),
|
|
34
56
|
execute: async (args, ctx) => {
|
|
35
57
|
const a = args;
|
|
@@ -39,6 +61,28 @@ export function createSendMessageTool(opts) {
|
|
|
39
61
|
return { content: "Message not sent: 'to' was empty. Pass the agent's task_id (a…).", details: { error: "empty to" }, isError: true };
|
|
40
62
|
if (!message)
|
|
41
63
|
return { content: "Message not sent: 'message' was empty.", details: { error: "empty message" }, isError: true };
|
|
64
|
+
if (to === "*") {
|
|
65
|
+
return {
|
|
66
|
+
content: 'Message not sent: broadcast (to: "*") is no longer supported — send a message per recipient.',
|
|
67
|
+
details: { error: "broadcast_unsupported", to },
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (to.includes("@")) {
|
|
72
|
+
return {
|
|
73
|
+
content: "Message not sent: to must be a bare teammate name — there is only one team per session.",
|
|
74
|
+
details: { error: "qualified_name_unsupported", to },
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const summaryArg = typeof a.summary === "string" ? a.summary.trim() : "";
|
|
79
|
+
if (summaryArg === "") {
|
|
80
|
+
return {
|
|
81
|
+
content: "Message not sent: summary is required when message is a string — pass a 5-10 word summary of the follow-up.",
|
|
82
|
+
details: { error: "summary_required", to },
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
42
86
|
const senderId = ctx.taskId ?? opts.owner;
|
|
43
87
|
if (senderId !== undefined && isObserverTaskId(senderId)) {
|
|
44
88
|
return { content: OBSERVER_SENDMESSAGE_SENDER_REFUSAL, details: { error: "observer_sender" }, isError: true };
|
|
@@ -68,6 +112,17 @@ export function createSendMessageTool(opts) {
|
|
|
68
112
|
details: { type: "send-message", status: "uplinked", to: "main", seq: uplinkSeqGlobal },
|
|
69
113
|
};
|
|
70
114
|
}
|
|
115
|
+
const hasParent = ctx.parentTaskId !== undefined ||
|
|
116
|
+
opts.uplink !== undefined ||
|
|
117
|
+
opts.senderName !== undefined ||
|
|
118
|
+
opts.siblingRetain !== undefined;
|
|
119
|
+
if (!hasParent) {
|
|
120
|
+
return {
|
|
121
|
+
content: `You are the main conversation — "main" addresses you. Send to a named agent instead.`,
|
|
122
|
+
details: { error: "main_is_self", to },
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
71
126
|
return {
|
|
72
127
|
content: `Message not sent: "main" (the spawning conversation) is not a deliverable target here. ` +
|
|
73
128
|
`Your completion is reported to it automatically — finish your task and your final report will be relayed.`,
|
|
@@ -105,10 +160,10 @@ export function createSendMessageTool(opts) {
|
|
|
105
160
|
if (row.name === undefined || row.agentType === "fork")
|
|
106
161
|
return undefined;
|
|
107
162
|
const whoT3 = handle === to ? `agent ${handle}` : `agent "${to}" (${handle})`;
|
|
108
|
-
if (row.status === "killed") {
|
|
163
|
+
if (row.status === "killed" && row.stoppedBy === "user") {
|
|
109
164
|
return {
|
|
110
|
-
content: `Message not sent: ${whoT3} was stopped
|
|
111
|
-
details: { error: "killed", to,
|
|
165
|
+
content: `Message not sent: ${whoT3} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
|
|
166
|
+
details: { error: "killed", to, stoppedBy: row.stoppedBy },
|
|
112
167
|
isError: true,
|
|
113
168
|
};
|
|
114
169
|
}
|
|
@@ -244,7 +299,9 @@ export function createSendMessageTool(opts) {
|
|
|
244
299
|
details: { error: "mailbox_leased", to },
|
|
245
300
|
};
|
|
246
301
|
}
|
|
247
|
-
const revivePrompt = lease.messages
|
|
302
|
+
const revivePrompt = lease.messages
|
|
303
|
+
.map((m) => frameTeammateMessage({ from: m.from ?? "main", text: m.content }))
|
|
304
|
+
.join("\n");
|
|
248
305
|
let spawned;
|
|
249
306
|
try {
|
|
250
307
|
spawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: revivePrompt });
|
|
@@ -354,48 +411,55 @@ export function createSendMessageTool(opts) {
|
|
|
354
411
|
return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to }, isError: true };
|
|
355
412
|
}
|
|
356
413
|
if (row.status === "running" || row.status === "pending") {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
414
|
+
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
415
|
+
const s2Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
|
|
416
|
+
const fromLabel = opts.senderName ?? senderId ?? "main";
|
|
417
|
+
const s2Clipped = message.length > UPLINK_RESULT_MAX
|
|
418
|
+
? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]`
|
|
419
|
+
: message;
|
|
420
|
+
const teammateXml = frameTeammateMessage({ from: fromLabel, ...(s2Summary !== undefined ? { summary: s2Summary } : {}), text: s2Clipped });
|
|
421
|
+
const delivered = await opts.registry.deliverToRunningAgent(targetId, resolvedAccess, {
|
|
422
|
+
task_id: senderId ?? "main",
|
|
423
|
+
task_type: "background_agent",
|
|
424
|
+
status: "event",
|
|
425
|
+
summary: `message from ${fromLabel}${s2Summary !== undefined ? `: ${s2Summary}` : ""}`,
|
|
426
|
+
result: teammateXml,
|
|
427
|
+
seq: ++uplinkSeqGlobal,
|
|
428
|
+
}, { priority: "next" });
|
|
429
|
+
if (delivered.ok) {
|
|
430
|
+
const receiptText = delivered.disposition === "parked"
|
|
431
|
+
? `Message parked for ${who}: the agent finished before reading it — it will be delivered when the agent is next continued. You will be notified of its completion; continue with other work.`
|
|
432
|
+
: delivered.disposition === "pending"
|
|
433
|
+
? `Message accepted for ${who} but delivery is UNCONFIRMED (its channel did not confirm within the wait window) — it stays queued and will deliver if the channel binds. You will be notified of the agent's completion either way; resend then if it went unanswered.`
|
|
434
|
+
: `Message queued for delivery to ${who} at its next turn. If it finishes before reading it, the message may not survive — you will be notified of its completion either way; resend then if it went unanswered. Continue with other work; do not poll.`;
|
|
435
|
+
return {
|
|
436
|
+
content: receiptText,
|
|
437
|
+
details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, ...(s2Summary !== undefined ? { summary: s2Summary } : {}) },
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (delivered.reason === "no_channel") {
|
|
441
|
+
return {
|
|
442
|
+
content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it. Wait for its completion notification, then SendMessage to continue it.`,
|
|
443
|
+
details: { error: "still_running", to },
|
|
444
|
+
isError: true,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
380
447
|
return {
|
|
381
|
-
content: `Message not sent: ${who}
|
|
382
|
-
details: { error: "
|
|
448
|
+
content: `Message not sent: ${who} just finished (delivery raced its completion). Send again to continue it from its transcript.`,
|
|
449
|
+
details: { error: "settle_race", to },
|
|
383
450
|
isError: true,
|
|
384
451
|
};
|
|
385
|
-
}
|
|
386
|
-
return {
|
|
387
|
-
content: `Message not sent: ${who} just finished (delivery raced its completion). Send again to continue it from its transcript.`,
|
|
388
|
-
details: { error: "settle_race", to },
|
|
389
|
-
isError: true,
|
|
390
|
-
};
|
|
452
|
+
});
|
|
391
453
|
}
|
|
392
454
|
if (row.status === "killed") {
|
|
393
455
|
const by = opts.registry.getStopAttribution(targetId);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
456
|
+
if (by === "user") {
|
|
457
|
+
return {
|
|
458
|
+
content: `Message not sent: ${who} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
|
|
459
|
+
details: { error: "killed", to, stoppedBy: by },
|
|
460
|
+
isError: true,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
399
463
|
}
|
|
400
464
|
const runLedger = ctx.subagentRetain ?? opts.retain;
|
|
401
465
|
const smLedgerSessionId = ctx.sessionId ?? opts.sessionId;
|
|
@@ -409,61 +473,73 @@ export function createSendMessageTool(opts) {
|
|
|
409
473
|
return revived;
|
|
410
474
|
}
|
|
411
475
|
const ledger = (knows(sessionLedger) ? sessionLedger : undefined) ?? (knows(runLedger) ? runLedger : undefined) ?? (knows(siblingLedger) ? siblingLedger : undefined) ?? runLedger ?? sessionLedger ?? siblingLedger;
|
|
412
|
-
|
|
476
|
+
const resumeToolUseId = row.toolUseId;
|
|
477
|
+
if (!ledger || resumeToolUseId === undefined) {
|
|
413
478
|
return {
|
|
414
479
|
content: `Message not sent: ${who}'s session was not retained (this run did not enable retainSubagentSessions), so it cannot be continued — relaunch a new agent with the needed context instead.`,
|
|
415
480
|
details: { error: "not_retained", to },
|
|
416
481
|
isError: true,
|
|
417
482
|
};
|
|
418
483
|
}
|
|
419
|
-
|
|
420
|
-
ledger,
|
|
421
|
-
parentToolCallId: row.toolUseId,
|
|
422
|
-
runner: opts.runner,
|
|
423
|
-
...(opts.notify ? { notify: opts.notify } : {}),
|
|
424
|
-
...(opts.sink ? { sink: opts.sink } : {}),
|
|
425
|
-
registry: opts.registry,
|
|
426
|
-
taskId: targetId,
|
|
427
|
-
taskAccess: resolvedAccess,
|
|
428
|
-
...((ctx.onBackgroundChildEvent ?? opts.onBackgroundChildEvent) ? { bgSink: ctx.onBackgroundChildEvent ?? opts.onBackgroundChildEvent } : {}),
|
|
429
|
-
sessionScoped: row.sessionScoped === true,
|
|
430
|
-
...(row.description !== undefined ? { rowDescription: row.description } : {}),
|
|
431
|
-
...(row.name !== undefined ? { rowName: row.name } : {}),
|
|
432
|
-
...(row.agentType !== undefined ? { rowAgentType: row.agentType } : {}),
|
|
433
|
-
...(row.owner !== undefined ? { rowOwner: row.owner } : {}),
|
|
434
|
-
...(row.scope !== undefined ? { rowScope: row.scope } : {}),
|
|
435
|
-
...(row.parentTaskId !== undefined ? { rowParentTaskId: row.parentTaskId } : {}),
|
|
436
|
-
...(row.parentSessionId !== undefined ? { rowParentSessionId: row.parentSessionId } : {}),
|
|
437
|
-
...(row.rootSessionId !== undefined ? { rowRootSessionId: row.rootSessionId } : {}),
|
|
438
|
-
...(opts.notify ? { currentParentNotify: opts.notify } : {}),
|
|
439
|
-
});
|
|
440
|
-
const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
|
|
441
|
-
const fromPrefix = ctx.parentTaskId !== undefined ? `(message from teammate "${opts.senderName ?? senderId ?? "unknown"}")\n` : "";
|
|
442
|
-
try {
|
|
443
|
-
const marker = await resume(`${fromPrefix}${summary ? `[${summary}] ${message}` : message}`);
|
|
484
|
+
if (ledger.get(resumeToolUseId)?.running === true) {
|
|
444
485
|
return {
|
|
445
|
-
content: `Message sent
|
|
446
|
-
|
|
447
|
-
|
|
486
|
+
content: `Message not sent: ${who} (or a prior follow-up to it) is still running — wait for its completion notification.`,
|
|
487
|
+
details: { error: "steering.still_running", to },
|
|
488
|
+
isError: true,
|
|
448
489
|
};
|
|
449
490
|
}
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
:
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
491
|
+
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
492
|
+
const resume = createSubagentResume({
|
|
493
|
+
ledger,
|
|
494
|
+
parentToolCallId: resumeToolUseId,
|
|
495
|
+
runner: opts.runner,
|
|
496
|
+
...(opts.notify ? { notify: opts.notify } : {}),
|
|
497
|
+
...(opts.sink ? { sink: opts.sink } : {}),
|
|
498
|
+
registry: opts.registry,
|
|
499
|
+
taskId: targetId,
|
|
500
|
+
taskAccess: resolvedAccess,
|
|
501
|
+
...((ctx.onBackgroundChildEvent ?? opts.onBackgroundChildEvent) ? { bgSink: ctx.onBackgroundChildEvent ?? opts.onBackgroundChildEvent } : {}),
|
|
502
|
+
sessionScoped: row.sessionScoped === true,
|
|
503
|
+
...(row.description !== undefined ? { rowDescription: row.description } : {}),
|
|
504
|
+
...(row.name !== undefined ? { rowName: row.name } : {}),
|
|
505
|
+
...(row.agentType !== undefined ? { rowAgentType: row.agentType } : {}),
|
|
506
|
+
...(row.owner !== undefined ? { rowOwner: row.owner } : {}),
|
|
507
|
+
...(row.scope !== undefined ? { rowScope: row.scope } : {}),
|
|
508
|
+
...(row.parentTaskId !== undefined ? { rowParentTaskId: row.parentTaskId } : {}),
|
|
509
|
+
...(row.parentSessionId !== undefined ? { rowParentSessionId: row.parentSessionId } : {}),
|
|
510
|
+
...(row.rootSessionId !== undefined ? { rowRootSessionId: row.rootSessionId } : {}),
|
|
511
|
+
...(opts.notify ? { currentParentNotify: opts.notify } : {}),
|
|
512
|
+
});
|
|
513
|
+
const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
|
|
514
|
+
const fromPrefix = ctx.parentTaskId !== undefined ? `(message from teammate "${opts.senderName ?? senderId ?? "unknown"}")\n` : "";
|
|
515
|
+
try {
|
|
516
|
+
const safeSummary = summary !== undefined ? escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary) : undefined;
|
|
517
|
+
const safeMessage = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, message);
|
|
518
|
+
const marker = await resume(`${fromPrefix}${safeSummary ? `[${safeSummary}] ${safeMessage}` : safeMessage}`);
|
|
519
|
+
return {
|
|
520
|
+
content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
|
|
521
|
+
`You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`,
|
|
522
|
+
details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, ...(summary !== undefined ? { summary } : {}) },
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
const code = e?.code;
|
|
527
|
+
const text = code === "resume.retain_off"
|
|
528
|
+
? `${who}'s session was not retained (retainSubagentSessions is off) — relaunch a new agent instead.`
|
|
529
|
+
: code === "resume.evicted"
|
|
530
|
+
? `${who}'s retained session was evicted (retain TTL / capacity / parent run ended) — relaunch a new agent instead.`
|
|
531
|
+
: code === "resume.session_not_found"
|
|
532
|
+
? `${who}'s session no longer exists — relaunch a new agent instead.`
|
|
533
|
+
: code === "resume.cap"
|
|
534
|
+
? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
|
|
535
|
+
: code === "steering.still_running"
|
|
536
|
+
? `${who} (or a prior follow-up to it) is still running — wait for its completion notification.`
|
|
537
|
+
: code === "resume.row_gone"
|
|
538
|
+
? `${who}'s registry row no longer exists (terminal GC) — relaunch a new agent instead.`
|
|
539
|
+
: `${e instanceof Error ? e.message : String(e)}`;
|
|
540
|
+
return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to }, isError: true };
|
|
541
|
+
}
|
|
542
|
+
});
|
|
467
543
|
},
|
|
468
544
|
});
|
|
469
545
|
}
|
|
@@ -5,16 +5,14 @@ import type { RunInternals } from "../core/runner/prepare-task.js";
|
|
|
5
5
|
import type { TaskNotificationPayload } from "../core/task-notification.js";
|
|
6
6
|
import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
|
|
7
7
|
export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
8
|
-
import { SubagentRetainLedger
|
|
9
|
-
export { SubagentRetainLedger, getSessionRetainLedger, releaseSessionRetainLedger, type SubagentRetainEntry };
|
|
10
|
-
export { AGENT_TRANSCRIPT_TOOL_NAME, createAgentTranscriptTool, type AgentTranscriptToolOptions } from "./agent-transcript-tool.js";
|
|
11
|
-
export { SEND_MESSAGE_TOOL_NAME, createSendMessageTool, type SendMessageToolOptions } from "./send-message-tool.js";
|
|
8
|
+
import { SubagentRetainLedger } from "./retain-ledger.js";
|
|
12
9
|
export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
|
|
13
10
|
export declare function notifyResultField(result: string | undefined): string | undefined;
|
|
14
11
|
export declare function inheritedManifestScopeFor(snapshot: readonly unknown[] | undefined): RunInternals["inheritedManifestScope"];
|
|
15
12
|
export declare const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
|
|
16
13
|
export declare const LEGACY_SUBAGENT_TOOL_NAME = "Task";
|
|
17
14
|
export declare const EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE = 64;
|
|
15
|
+
export declare const DEFAULT_SUBAGENT_MAX_DEPTH = 3;
|
|
18
16
|
export declare const EXTRA_TOOLS_FAILED_NOTE = "note: extraTools evaluation failed \u2014 the injected tool set was skipped for this spawn.";
|
|
19
17
|
export declare const EXTRA_TOOLS_BUDGET_NOTE = "note: extraTools evaluation was skipped \u2014 this spawn tree exhausted its factory-call budget (64); no dynamic tools were injected for this spawn.";
|
|
20
18
|
export interface SubagentToolStats {
|
|
@@ -127,8 +125,17 @@ export interface SubagentSpawnContext {
|
|
|
127
125
|
agentType?: string;
|
|
128
126
|
depth: number;
|
|
129
127
|
}
|
|
128
|
+
export declare function normalizeSubagentType(value: string): string;
|
|
130
129
|
export declare const SUBAGENT_SYSTEM_NOTE: string;
|
|
131
130
|
export declare const FORK_DIRECTIVE_FRAME = "<fork-boilerplate>\nYou are a worker fork. The transcript above is the parent's history \u2014 inherited reference, not your situation. You are NOT a continuation of that agent. Execute ONE directive, then stop.\n\nHard rules:\n- Do NOT spawn subagents with the Agent tool. The \"default to forking\" guidance is for the parent; you ARE the fork, execute directly.\n- One shot: report once and stop. No follow-up questions, no proposed next steps, no waiting for the user.\n\nGuidelines (your directive may override any of these):\n- Stay in scope. Other forks may be handling adjacent work; if you spot something outside your directive, note it in a sentence and move on.\n- Open with one line restating your task, so the parent can spot scope drift at a glance.\n- Be concise \u2014 as short as the answer allows, no shorter. Plain text, no preamble, no meta-commentary.\n- If you committed changes, list the paths and commit hashes in your report.\n</fork-boilerplate>\n\nYour directive: ";
|
|
131
|
+
export declare function forkWorktreeTranslationNote(parentCwd: string | undefined, worktreeDir: string): string;
|
|
132
|
+
export declare function asyncLaunchedReceipt(p: {
|
|
133
|
+
taskId: string;
|
|
134
|
+
workingLine: string;
|
|
135
|
+
notify: boolean;
|
|
136
|
+
oneShot?: boolean;
|
|
137
|
+
notes?: (string | undefined)[];
|
|
138
|
+
}): string;
|
|
132
139
|
export declare function createSubagentTool(opts: SubagentToolOptions): ToolSpec;
|
|
133
140
|
export declare function agentToolsNote(def: {
|
|
134
141
|
allowTools?: string[];
|