@sema-agent/core 2.0.1 → 2.1.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.js +8 -3
- package/dist/agents/send-message-tool.js +112 -81
- package/dist/agents/subagent.d.ts +10 -4
- package/dist/agents/subagent.js +103 -53
- 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 +8 -2
- package/dist/core/runner/prepare-task.js +39 -8
- package/dist/core/runner/runtask.js +910 -865
- package/dist/core/runner/turn-attachments.d.ts +15 -1
- package/dist/core/runner/turn-attachments.js +68 -9
- 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 +1 -1
- package/dist/core/workflow-journal-store.d.ts +13 -0
- package/dist/engine/session/import-validate.js +29 -0
- package/dist/orchestration/workflow.js +28 -1
- package/dist/prompt-assembly/event-registry.js +1 -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 +17 -0
- package/dist/stores/file/workflow-journal-store.js +102 -2
- package/dist/tools/fs/bash-readonly-classifier.js +1 -1
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/safety.js +13 -6
- 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 +1 -1
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;
|
|
@@ -326,7 +331,7 @@ export function createObserverReportToolSpec(opts) {
|
|
|
326
331
|
name: OBSERVER_REPORT_TOOL_NAME,
|
|
327
332
|
contract: { contractId: "core.observer_report@1", implementationRevision: "1" },
|
|
328
333
|
description: OBSERVER_REPORT_DESCRIPTION,
|
|
329
|
-
effect: "
|
|
334
|
+
effect: "write",
|
|
330
335
|
parameters: Type.Object({
|
|
331
336
|
report: Type.String({
|
|
332
337
|
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({
|
|
@@ -244,7 +265,9 @@ export function createSendMessageTool(opts) {
|
|
|
244
265
|
details: { error: "mailbox_leased", to },
|
|
245
266
|
};
|
|
246
267
|
}
|
|
247
|
-
const revivePrompt = lease.messages
|
|
268
|
+
const revivePrompt = lease.messages
|
|
269
|
+
.map((m) => frameTeammateMessage({ from: m.from ?? "main", text: m.content }))
|
|
270
|
+
.join("\n");
|
|
248
271
|
let spawned;
|
|
249
272
|
try {
|
|
250
273
|
spawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: revivePrompt });
|
|
@@ -354,40 +377,45 @@ export function createSendMessageTool(opts) {
|
|
|
354
377
|
return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to }, isError: true };
|
|
355
378
|
}
|
|
356
379
|
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
|
-
|
|
380
|
+
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
381
|
+
const s2Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
|
|
382
|
+
const fromLabel = opts.senderName ?? senderId ?? "main";
|
|
383
|
+
const s2Clipped = message.length > UPLINK_RESULT_MAX
|
|
384
|
+
? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]`
|
|
385
|
+
: message;
|
|
386
|
+
const teammateXml = frameTeammateMessage({ from: fromLabel, ...(s2Summary !== undefined ? { summary: s2Summary } : {}), text: s2Clipped });
|
|
387
|
+
const delivered = await opts.registry.deliverToRunningAgent(targetId, resolvedAccess, {
|
|
388
|
+
task_id: senderId ?? "main",
|
|
389
|
+
task_type: "background_agent",
|
|
390
|
+
status: "event",
|
|
391
|
+
summary: `message from ${fromLabel}${s2Summary !== undefined ? `: ${s2Summary}` : ""}`,
|
|
392
|
+
result: teammateXml,
|
|
393
|
+
seq: ++uplinkSeqGlobal,
|
|
394
|
+
}, { priority: "next" });
|
|
395
|
+
if (delivered.ok) {
|
|
396
|
+
const receiptText = delivered.disposition === "parked"
|
|
397
|
+
? `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.`
|
|
398
|
+
: delivered.disposition === "pending"
|
|
399
|
+
? `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.`
|
|
400
|
+
: `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.`;
|
|
401
|
+
return {
|
|
402
|
+
content: receiptText,
|
|
403
|
+
details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, ...(s2Summary !== undefined ? { summary: s2Summary } : {}) },
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (delivered.reason === "no_channel") {
|
|
407
|
+
return {
|
|
408
|
+
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.`,
|
|
409
|
+
details: { error: "still_running", to },
|
|
410
|
+
isError: true,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
380
413
|
return {
|
|
381
|
-
content: `Message not sent: ${who}
|
|
382
|
-
details: { error: "
|
|
414
|
+
content: `Message not sent: ${who} just finished (delivery raced its completion). Send again to continue it from its transcript.`,
|
|
415
|
+
details: { error: "settle_race", to },
|
|
383
416
|
isError: true,
|
|
384
417
|
};
|
|
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
|
-
};
|
|
418
|
+
});
|
|
391
419
|
}
|
|
392
420
|
if (row.status === "killed") {
|
|
393
421
|
const by = opts.registry.getStopAttribution(targetId);
|
|
@@ -409,61 +437,64 @@ export function createSendMessageTool(opts) {
|
|
|
409
437
|
return revived;
|
|
410
438
|
}
|
|
411
439
|
const ledger = (knows(sessionLedger) ? sessionLedger : undefined) ?? (knows(runLedger) ? runLedger : undefined) ?? (knows(siblingLedger) ? siblingLedger : undefined) ?? runLedger ?? sessionLedger ?? siblingLedger;
|
|
412
|
-
|
|
440
|
+
const resumeToolUseId = row.toolUseId;
|
|
441
|
+
if (!ledger || resumeToolUseId === undefined) {
|
|
413
442
|
return {
|
|
414
443
|
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
444
|
details: { error: "not_retained", to },
|
|
416
445
|
isError: true,
|
|
417
446
|
};
|
|
418
447
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
449
|
+
const resume = createSubagentResume({
|
|
450
|
+
ledger,
|
|
451
|
+
parentToolCallId: resumeToolUseId,
|
|
452
|
+
runner: opts.runner,
|
|
453
|
+
...(opts.notify ? { notify: opts.notify } : {}),
|
|
454
|
+
...(opts.sink ? { sink: opts.sink } : {}),
|
|
455
|
+
registry: opts.registry,
|
|
456
|
+
taskId: targetId,
|
|
457
|
+
taskAccess: resolvedAccess,
|
|
458
|
+
...((ctx.onBackgroundChildEvent ?? opts.onBackgroundChildEvent) ? { bgSink: ctx.onBackgroundChildEvent ?? opts.onBackgroundChildEvent } : {}),
|
|
459
|
+
sessionScoped: row.sessionScoped === true,
|
|
460
|
+
...(row.description !== undefined ? { rowDescription: row.description } : {}),
|
|
461
|
+
...(row.name !== undefined ? { rowName: row.name } : {}),
|
|
462
|
+
...(row.agentType !== undefined ? { rowAgentType: row.agentType } : {}),
|
|
463
|
+
...(row.owner !== undefined ? { rowOwner: row.owner } : {}),
|
|
464
|
+
...(row.scope !== undefined ? { rowScope: row.scope } : {}),
|
|
465
|
+
...(row.parentTaskId !== undefined ? { rowParentTaskId: row.parentTaskId } : {}),
|
|
466
|
+
...(row.parentSessionId !== undefined ? { rowParentSessionId: row.parentSessionId } : {}),
|
|
467
|
+
...(row.rootSessionId !== undefined ? { rowRootSessionId: row.rootSessionId } : {}),
|
|
468
|
+
...(opts.notify ? { currentParentNotify: opts.notify } : {}),
|
|
469
|
+
});
|
|
470
|
+
const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
|
|
471
|
+
const fromPrefix = ctx.parentTaskId !== undefined ? `(message from teammate "${opts.senderName ?? senderId ?? "unknown"}")\n` : "";
|
|
472
|
+
try {
|
|
473
|
+
const marker = await resume(`${fromPrefix}${summary ? `[${summary}] ${message}` : message}`);
|
|
474
|
+
return {
|
|
475
|
+
content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
|
|
476
|
+
`You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`,
|
|
477
|
+
details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, ...(summary !== undefined ? { summary } : {}) },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
const code = e?.code;
|
|
482
|
+
const text = code === "resume.retain_off"
|
|
483
|
+
? `${who}'s session was not retained (retainSubagentSessions is off) — relaunch a new agent instead.`
|
|
484
|
+
: code === "resume.evicted"
|
|
485
|
+
? `${who}'s retained session was evicted (retain TTL / capacity / parent run ended) — relaunch a new agent instead.`
|
|
486
|
+
: code === "resume.session_not_found"
|
|
487
|
+
? `${who}'s session no longer exists — relaunch a new agent instead.`
|
|
488
|
+
: code === "resume.cap"
|
|
489
|
+
? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
|
|
490
|
+
: code === "steering.still_running"
|
|
491
|
+
? `${who} (or a prior follow-up to it) is still running — wait for its completion notification.`
|
|
492
|
+
: code === "resume.row_gone"
|
|
493
|
+
? `${who}'s registry row no longer exists (terminal GC) — relaunch a new agent instead.`
|
|
494
|
+
: `${e instanceof Error ? e.message : String(e)}`;
|
|
495
|
+
return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to }, isError: true };
|
|
496
|
+
}
|
|
439
497
|
});
|
|
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}`);
|
|
444
|
-
return {
|
|
445
|
-
content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
|
|
446
|
-
`You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`,
|
|
447
|
-
details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, ...(summary !== undefined ? { summary } : {}) },
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
catch (e) {
|
|
451
|
-
const code = e?.code;
|
|
452
|
-
const text = code === "resume.retain_off"
|
|
453
|
-
? `${who}'s session was not retained (retainSubagentSessions is off) — relaunch a new agent instead.`
|
|
454
|
-
: code === "resume.evicted"
|
|
455
|
-
? `${who}'s retained session was evicted (retain TTL / capacity / parent run ended) — relaunch a new agent instead.`
|
|
456
|
-
: code === "resume.session_not_found"
|
|
457
|
-
? `${who}'s session no longer exists — relaunch a new agent instead.`
|
|
458
|
-
: code === "resume.cap"
|
|
459
|
-
? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
|
|
460
|
-
: code === "steering.still_running"
|
|
461
|
-
? `${who} (or a prior follow-up to it) is still running — wait for its completion notification.`
|
|
462
|
-
: code === "resume.row_gone"
|
|
463
|
-
? `${who}'s registry row no longer exists (terminal GC) — relaunch a new agent instead.`
|
|
464
|
-
: `${e instanceof Error ? e.message : String(e)}`;
|
|
465
|
-
return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to }, isError: true };
|
|
466
|
-
}
|
|
467
498
|
},
|
|
468
499
|
});
|
|
469
500
|
}
|
|
@@ -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,16 @@ 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
|
+
notes?: (string | undefined)[];
|
|
137
|
+
}): string;
|
|
132
138
|
export declare function createSubagentTool(opts: SubagentToolOptions): ToolSpec;
|
|
133
139
|
export declare function agentToolsNote(def: {
|
|
134
140
|
allowTools?: string[];
|
package/dist/agents/subagent.js
CHANGED
|
@@ -19,10 +19,7 @@ import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
|
|
|
19
19
|
import { extractErrorCode } from "../brain/errors.js";
|
|
20
20
|
import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
|
|
21
21
|
export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
22
|
-
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger,
|
|
23
|
-
export { SubagentRetainLedger, getSessionRetainLedger, releaseSessionRetainLedger };
|
|
24
|
-
export { AGENT_TRANSCRIPT_TOOL_NAME, createAgentTranscriptTool } from "./agent-transcript-tool.js";
|
|
25
|
-
export { SEND_MESSAGE_TOOL_NAME, createSendMessageTool } from "./send-message-tool.js";
|
|
22
|
+
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
|
|
26
23
|
import { recordRosterSpawn } from "./roster-store.js";
|
|
27
24
|
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
|
|
28
25
|
import { SubagentStepRecorder } from "./subagent-steps.js";
|
|
@@ -68,6 +65,7 @@ export function inheritedManifestScopeFor(snapshot) {
|
|
|
68
65
|
export const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
|
|
69
66
|
export const LEGACY_SUBAGENT_TOOL_NAME = "Task";
|
|
70
67
|
export const EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE = 64;
|
|
68
|
+
export const DEFAULT_SUBAGENT_MAX_DEPTH = 3;
|
|
71
69
|
export const EXTRA_TOOLS_FAILED_NOTE = "note: extraTools evaluation failed — the injected tool set was skipped for this spawn.";
|
|
72
70
|
export const EXTRA_TOOLS_BUDGET_NOTE = `note: extraTools evaluation was skipped — this spawn tree exhausted its factory-call budget (${EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE}); no dynamic tools were injected for this spawn.`;
|
|
73
71
|
function countArgLines(v) {
|
|
@@ -580,6 +578,9 @@ function adaptLegacyForkArgs(args) {
|
|
|
580
578
|
}
|
|
581
579
|
return args;
|
|
582
580
|
}
|
|
581
|
+
export function normalizeSubagentType(value) {
|
|
582
|
+
return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
|
|
583
|
+
}
|
|
583
584
|
function foldGeneralPurposeAlias(args, shadowed) {
|
|
584
585
|
if (shadowed)
|
|
585
586
|
return args;
|
|
@@ -611,6 +612,26 @@ Guidelines (your directive may override any of these):
|
|
|
611
612
|
</fork-boilerplate>
|
|
612
613
|
|
|
613
614
|
Your directive: `;
|
|
615
|
+
export function forkWorktreeTranslationNote(parentCwd, worktreeDir) {
|
|
616
|
+
const where = parentCwd !== undefined ? parentCwd : "a different working directory";
|
|
617
|
+
return (`You've inherited the conversation context above from a parent agent working in ${where}. ` +
|
|
618
|
+
`You are operating in an isolated git worktree at ${worktreeDir} — same repository, same relative file structure, separate working copy. ` +
|
|
619
|
+
`Paths in the inherited context refer to the parent's working directory; translate them to your worktree root. ` +
|
|
620
|
+
`Re-read files before editing if the parent may have modified them since they appear in the context. ` +
|
|
621
|
+
`Your changes stay in this worktree and will not affect the parent's files.`);
|
|
622
|
+
}
|
|
623
|
+
export function asyncLaunchedReceipt(p) {
|
|
624
|
+
const noteLines = (p.notes ?? []).filter((n) => n !== undefined && n !== "");
|
|
625
|
+
return (`Async agent launched successfully. (This tool result is internal metadata — never quote or paste any part of it, including the task_id below, into a user-facing reply.)\n` +
|
|
626
|
+
`task_id: ${p.taskId} (internal ID - do not mention to user. Use SendMessage with to: '${p.taskId}', summary: '<5-10 word recap>' to continue this agent.)\n` +
|
|
627
|
+
(p.notify
|
|
628
|
+
? `${p.workingLine} You will be notified automatically when it completes. You know nothing about its results until that notification arrives — do not report, assume, or predict them; continue other work or respond to the user in the meantime.\n`
|
|
629
|
+
: `${p.workingLine} Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted.\n`) +
|
|
630
|
+
noteLines.map((n) => `${n}\n`).join("") +
|
|
631
|
+
(p.notify
|
|
632
|
+
? `In your own words, briefly tell the user what you launched — do not echo this tool result. Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
|
|
633
|
+
: `In your own words, briefly tell the user what you launched — do not echo this tool result. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
|
|
634
|
+
}
|
|
614
635
|
export function createSubagentTool(opts) {
|
|
615
636
|
const catalog = opts.runner.agentCatalog;
|
|
616
637
|
if (opts.agents === undefined && catalog?.agents)
|
|
@@ -683,7 +704,7 @@ export function agentWhenToUseText(def, lean = true) {
|
|
|
683
704
|
return (leanText || def.whenToUse) || undefined;
|
|
684
705
|
}
|
|
685
706
|
function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
686
|
-
const maxDepth = opts.maxDepth ??
|
|
707
|
+
const maxDepth = opts.maxDepth ?? DEFAULT_SUBAGENT_MAX_DEPTH;
|
|
687
708
|
const toolName = opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME;
|
|
688
709
|
const deploymentNames = new Set((opts.agents ?? []).map((a) => a.name));
|
|
689
710
|
const builtins = opts.builtinAgents === false ? [] : builtinAgentDefinitions(toolName).filter((d) => !deploymentNames.has(d.name));
|
|
@@ -700,6 +721,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
700
721
|
];
|
|
701
722
|
const rosterNames = opts.models ? Object.keys(opts.models) : undefined;
|
|
702
723
|
const agentListing = [
|
|
724
|
+
...(generalPurposeShadowed
|
|
725
|
+
? []
|
|
726
|
+
: [{
|
|
727
|
+
name: GENERAL_PURPOSE_SUBAGENT_TYPE,
|
|
728
|
+
description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
|
|
729
|
+
tools: "All tools",
|
|
730
|
+
}]),
|
|
703
731
|
...available.map((a) => ({ name: a.name, description: agentWhenToUseText(a) ?? "", tools: agentToolsNote(a) })),
|
|
704
732
|
...(forkOffered
|
|
705
733
|
? [{
|
|
@@ -821,12 +849,29 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
821
849
|
details: { error: "unknown subagent_type" },
|
|
822
850
|
};
|
|
823
851
|
}
|
|
824
|
-
const
|
|
852
|
+
const requestedType = a.subagent_type ?? "";
|
|
853
|
+
let rawType = requestedType;
|
|
854
|
+
if (requestedType !== "") {
|
|
855
|
+
const accepted = acceptedSubagentTypes();
|
|
856
|
+
if (!accepted.includes(requestedType)) {
|
|
857
|
+
const wanted = normalizeSubagentType(requestedType);
|
|
858
|
+
const hits = wanted === "" ? [] : accepted.filter((n) => normalizeSubagentType(n) === wanted);
|
|
859
|
+
if (hits.length > 1) {
|
|
860
|
+
return {
|
|
861
|
+
isError: true,
|
|
862
|
+
content: `Sub-agent not started: subagent_type "${requestedType}" is ambiguous — it matches ${hits.join(", ")}. Use the exact name.`,
|
|
863
|
+
details: { error: "ambiguous subagent_type" },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (hits.length === 1)
|
|
867
|
+
rawType = hits[0];
|
|
868
|
+
}
|
|
869
|
+
}
|
|
825
870
|
const wantsFork = forkOffered && rawType === FORK_SUBAGENT_TYPE;
|
|
826
871
|
if (reviveClaim !== undefined && wantsFork) {
|
|
827
872
|
return { isError: true, content: `Sub-agent not revived: a "${FORK_SUBAGENT_TYPE}" agent cannot be revived — launch a new agent with the needed context instead.`, details: { error: "revive.fork_unsupported" } };
|
|
828
873
|
}
|
|
829
|
-
const omitted = rawType === "";
|
|
874
|
+
const omitted = rawType === "" || (!generalPurposeShadowed && rawType === GENERAL_PURPOSE_SUBAGENT_TYPE);
|
|
830
875
|
const def = hasAgents && !wantsFork && !omitted ? agentMap.get(rawType) : undefined;
|
|
831
876
|
const spawnAgentType = wantsFork ? FORK_SUBAGENT_TYPE : omitted ? GENERAL_PURPOSE_SUBAGENT_TYPE : rawType;
|
|
832
877
|
if (!def && !wantsFork && !omitted) {
|
|
@@ -1074,19 +1119,31 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1074
1119
|
}
|
|
1075
1120
|
};
|
|
1076
1121
|
const observedSteerRef = {};
|
|
1122
|
+
let pendingTrigger = [];
|
|
1123
|
+
const takePendingTrigger = () => {
|
|
1124
|
+
if (pendingTrigger.length === 0)
|
|
1125
|
+
return undefined;
|
|
1126
|
+
const joined = pendingTrigger.join("\n\n");
|
|
1127
|
+
pendingTrigger = [];
|
|
1128
|
+
return joined;
|
|
1129
|
+
};
|
|
1130
|
+
const injectObservedRequest = async (text, origin) => {
|
|
1131
|
+
const steer = observedSteerRef.steer;
|
|
1132
|
+
if (!steer)
|
|
1133
|
+
throw new Error("observed child has no steer lane (report undeliverable)");
|
|
1134
|
+
if (origin !== "observer")
|
|
1135
|
+
pendingTrigger.push(text);
|
|
1136
|
+
await steer(text);
|
|
1137
|
+
};
|
|
1077
1138
|
if (observerArm) {
|
|
1139
|
+
pendingTrigger.push(prompt);
|
|
1078
1140
|
const observerDef = observerArm.observerDefinition;
|
|
1079
1141
|
const envelopeName = observerSlug(childAgentName ?? def.name);
|
|
1080
1142
|
const sid = uuidv7();
|
|
1081
1143
|
observerSessionId = sid;
|
|
1082
1144
|
markObserverTaskId(sid);
|
|
1083
1145
|
const reportOpts = {
|
|
1084
|
-
queueReport: async (framed) =>
|
|
1085
|
-
const steer = observedSteerRef.steer;
|
|
1086
|
-
if (!steer)
|
|
1087
|
-
throw new Error("observed child has no steer lane (report undeliverable)");
|
|
1088
|
-
await steer(framed);
|
|
1089
|
-
},
|
|
1146
|
+
queueReport: async (framed) => injectObservedRequest(framed, "observer"),
|
|
1090
1147
|
};
|
|
1091
1148
|
const reportTool = createObserverReportToolSpec(reportOpts);
|
|
1092
1149
|
const observerSpec = (objectiveText, resume) => ({
|
|
@@ -1125,7 +1182,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1125
1182
|
onError: noteObserverFailure,
|
|
1126
1183
|
});
|
|
1127
1184
|
reportOpts.pairing = observerPairing;
|
|
1128
|
-
observerTap = new ObserverDigestTap((activity) => observerPairing.enqueueSegment(activity));
|
|
1185
|
+
observerTap = new ObserverDigestTap((activity) => observerPairing.enqueueSegment(activity, takePendingTrigger()));
|
|
1129
1186
|
}
|
|
1130
1187
|
const closeObserverWindow = (status) => {
|
|
1131
1188
|
if (observerTap)
|
|
@@ -1440,7 +1497,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1440
1497
|
}
|
|
1441
1498
|
const forkInternals = { ...childInternals, insideFork: true };
|
|
1442
1499
|
stepRecorder.lockTo(forkedId);
|
|
1443
|
-
const
|
|
1500
|
+
const forkWorktreeNote = worktreeDir !== undefined ? forkWorktreeTranslationNote(ctx.parentCwd, worktreeDir) : undefined;
|
|
1501
|
+
const forkObjective = `${FORK_DIRECTIVE_FRAME}${prompt}${forkWorktreeNote !== undefined ? `\n\n${forkWorktreeNote}` : ""}`;
|
|
1444
1502
|
if (wantsBackground) {
|
|
1445
1503
|
const bg = opts.background;
|
|
1446
1504
|
const notify = ctx.onTaskNotification ?? bg.notify;
|
|
@@ -1674,25 +1732,21 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1674
1732
|
}
|
|
1675
1733
|
});
|
|
1676
1734
|
return {
|
|
1677
|
-
content:
|
|
1678
|
-
|
|
1679
|
-
`
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
`) +
|
|
1693
|
-
(notify
|
|
1694
|
-
? `Briefly tell the user what you launched and end your response. Do not generate any other text — agent results will arrive in a subsequent message.`
|
|
1695
|
-
: `Briefly tell the user what you launched. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`),
|
|
1735
|
+
content: asyncLaunchedReceipt({
|
|
1736
|
+
taskId,
|
|
1737
|
+
workingLine: `The forked agent inherits your full current context and works on its prompt in the background.`,
|
|
1738
|
+
notify: Boolean(notify),
|
|
1739
|
+
notes: [
|
|
1740
|
+
modelNote,
|
|
1741
|
+
extraToolsNote,
|
|
1742
|
+
worktreeDir !== undefined
|
|
1743
|
+
? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)`
|
|
1744
|
+
: undefined,
|
|
1745
|
+
sessionScopedBg
|
|
1746
|
+
? `It is session-scoped: it keeps running even after this task ends.`
|
|
1747
|
+
: `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
|
|
1748
|
+
],
|
|
1749
|
+
}),
|
|
1696
1750
|
details: { type: "agent", subagent_type: FORK_SUBAGENT_TYPE, status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
|
|
1697
1751
|
};
|
|
1698
1752
|
}
|
|
@@ -2308,25 +2362,21 @@ task_id: ${taskId}
|
|
|
2308
2362
|
}
|
|
2309
2363
|
}
|
|
2310
2364
|
return {
|
|
2311
|
-
content:
|
|
2312
|
-
|
|
2313
|
-
`
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
`) +
|
|
2327
|
-
(notify
|
|
2328
|
-
? `Briefly tell the user what you launched and end your response. Do not generate any other text — agent results will arrive in a subsequent message.`
|
|
2329
|
-
: `Briefly tell the user what you launched. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`),
|
|
2365
|
+
content: asyncLaunchedReceipt({
|
|
2366
|
+
taskId,
|
|
2367
|
+
workingLine: `The agent is working in the background.`,
|
|
2368
|
+
notify: Boolean(notify),
|
|
2369
|
+
notes: [
|
|
2370
|
+
modelNote,
|
|
2371
|
+
extraToolsNote,
|
|
2372
|
+
worktreeDir !== undefined
|
|
2373
|
+
? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)`
|
|
2374
|
+
: undefined,
|
|
2375
|
+
sessionScopedBg
|
|
2376
|
+
? `It is session-scoped: it keeps running even after this task ends.`
|
|
2377
|
+
: `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
|
|
2378
|
+
],
|
|
2379
|
+
}),
|
|
2330
2380
|
details: { type: "agent", status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
|
|
2331
2381
|
};
|
|
2332
2382
|
}
|
|
@@ -34,11 +34,13 @@ export function mergeInjections(project, personal) {
|
|
|
34
34
|
const announcements = [...(project.announcements ?? []), ...(personal.announcements ?? [])];
|
|
35
35
|
const announceParts = [project.announceBlock, personal.announceBlock].filter((s) => Boolean(s && s.trim()));
|
|
36
36
|
const blockParts = [instruction, ...indexParts, ...announceParts];
|
|
37
|
+
const indexSeed = project.indexSeed ?? personal.indexSeed;
|
|
37
38
|
return {
|
|
38
39
|
instruction,
|
|
39
40
|
...(indexParts.length > 0 ? { index: indexParts.join("\n\n") } : {}),
|
|
40
41
|
...(announcements.length > 0 ? { announcements } : {}),
|
|
41
42
|
...(announceParts.length > 0 ? { announceBlock: announceParts.join("\n\n") } : {}),
|
|
43
|
+
...(indexSeed !== undefined ? { indexSeed } : {}),
|
|
42
44
|
block: blockParts.filter((s) => s && s.trim()).join("\n\n"),
|
|
43
45
|
};
|
|
44
46
|
}
|
|
@@ -162,10 +162,14 @@ export class MemoryEngine {
|
|
|
162
162
|
}
|
|
163
163
|
inject(handle) {
|
|
164
164
|
const instruction = handle.writeScope !== null ? buildMemoryInstruction(handle.writableRoot) : "";
|
|
165
|
-
const
|
|
165
|
+
const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
|
|
166
|
+
const onDisk = readSafe(indexPath);
|
|
166
167
|
const indexText = onDisk !== undefined && onDisk.trim() !== "" ? onDisk : handle.indexText;
|
|
167
168
|
const truncated = truncateIndex(indexText);
|
|
168
169
|
const index = composeMemoryBlock(truncated, handle.writeScope ?? handle.scopes[0] ?? "memory");
|
|
170
|
+
const indexSeed = handle.writeScope !== null && onDisk !== undefined && (onDisk.trim() === "" || truncated === onDisk)
|
|
171
|
+
? { path: indexPath, content: onDisk }
|
|
172
|
+
: undefined;
|
|
169
173
|
let announcements;
|
|
170
174
|
let announceBlock;
|
|
171
175
|
try {
|
|
@@ -183,6 +187,7 @@ export class MemoryEngine {
|
|
|
183
187
|
...(index !== undefined ? { index } : {}),
|
|
184
188
|
...(announcements !== undefined ? { announcements } : {}),
|
|
185
189
|
...(announceBlock !== undefined ? { announceBlock } : {}),
|
|
190
|
+
...(indexSeed !== undefined ? { indexSeed } : {}),
|
|
186
191
|
block,
|
|
187
192
|
};
|
|
188
193
|
}
|
|
@@ -12,5 +12,9 @@ export interface PrepareMemoryInput {
|
|
|
12
12
|
export interface PrepareMemoryResult {
|
|
13
13
|
memoryEngineSession: Prepared["memoryEngineSession"];
|
|
14
14
|
memoryBlock: string | undefined;
|
|
15
|
+
seedFiles?: ReadonlyArray<{
|
|
16
|
+
path: string;
|
|
17
|
+
content: string;
|
|
18
|
+
}>;
|
|
15
19
|
}
|
|
16
20
|
export declare function prepareMemory(input: PrepareMemoryInput): Promise<PrepareMemoryResult>;
|