@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.
Files changed (35) hide show
  1. package/dist/agents/observer.js +8 -3
  2. package/dist/agents/send-message-tool.js +112 -81
  3. package/dist/agents/subagent.d.ts +10 -4
  4. package/dist/agents/subagent.js +103 -53
  5. package/dist/core/memory-engine/dual-root.js +2 -0
  6. package/dist/core/memory-engine/engine.d.ts +4 -0
  7. package/dist/core/memory-engine/engine.js +6 -1
  8. package/dist/core/runner/prepare-memory.d.ts +4 -0
  9. package/dist/core/runner/prepare-memory.js +4 -1
  10. package/dist/core/runner/prepare-task.d.ts +8 -2
  11. package/dist/core/runner/prepare-task.js +39 -8
  12. package/dist/core/runner/runtask.js +910 -865
  13. package/dist/core/runner/turn-attachments.d.ts +15 -1
  14. package/dist/core/runner/turn-attachments.js +68 -9
  15. package/dist/core/tool-result-budget.js +2 -2
  16. package/dist/core/tool-result-store.d.ts +2 -0
  17. package/dist/core/tool-result-store.js +27 -2
  18. package/dist/core/types.d.ts +1 -1
  19. package/dist/core/workflow-journal-store.d.ts +13 -0
  20. package/dist/engine/session/import-validate.js +29 -0
  21. package/dist/orchestration/workflow.js +28 -1
  22. package/dist/prompt-assembly/event-registry.js +1 -1
  23. package/dist/stores/cc/task-list-store.js +3 -3
  24. package/dist/stores/file/memory-store.d.ts +3 -0
  25. package/dist/stores/file/memory-store.js +39 -12
  26. package/dist/stores/file/tool-result-store.js +16 -2
  27. package/dist/stores/file/workflow-journal-store.d.ts +17 -0
  28. package/dist/stores/file/workflow-journal-store.js +102 -2
  29. package/dist/tools/fs/bash-readonly-classifier.js +1 -1
  30. package/dist/tools/fs/fs-read.js +2 -2
  31. package/dist/tools/fs/safety.js +13 -6
  32. package/dist/tools/task-list.d.ts +1 -0
  33. package/dist/tools/task-list.js +13 -2
  34. package/dist/tools/web.js +36 -6
  35. package/package.json +1 -1
@@ -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
- this.pending.push({ type: "user_message", text: e.message ?? "(steering)" });
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: "idempotent",
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.map((m) => `<teammate-message teammate_id="${m.from ?? "main"}">\n${m.content}\n</teammate-message>`).join("\n");
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
- const s2Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
358
- const fromLabel = opts.senderName ?? senderId ?? "main";
359
- const teammateXml = `<teammate-message teammate_id="${fromLabel}"${s2Summary !== undefined ? ` summary=${JSON.stringify(s2Summary)}` : ""}>\n${message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]` : message}\n</teammate-message>`;
360
- const delivered = await opts.registry.deliverToRunningAgent(targetId, resolvedAccess, {
361
- task_id: senderId ?? "main",
362
- task_type: "background_agent",
363
- status: "event",
364
- summary: `message from ${fromLabel}${s2Summary !== undefined ? `: ${s2Summary}` : ""}`,
365
- result: teammateXml,
366
- seq: ++uplinkSeqGlobal,
367
- }, { priority: "next" });
368
- if (delivered.ok) {
369
- const receiptText = delivered.disposition === "parked"
370
- ? `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.`
371
- : delivered.disposition === "pending"
372
- ? `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.`
373
- : `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.`;
374
- return {
375
- content: receiptText,
376
- details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, ...(s2Summary !== undefined ? { summary: s2Summary } : {}) },
377
- };
378
- }
379
- if (delivered.reason === "no_channel") {
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} is still running and this deployment has no mid-run delivery channel for it. Wait for its completion notification, then SendMessage to continue it.`,
382
- details: { error: "still_running", to },
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
- if (!ledger || row.toolUseId === undefined) {
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
- const resume = createSubagentResume({
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 } : {}),
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, getSessionRetainLedger, releaseSessionRetainLedger, type SubagentRetainEntry } from "./retain-ledger.js";
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[];
@@ -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, getSessionRetainLedger, getOrCreateSessionRetainLedger, releaseSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
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 ?? 5;
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 rawType = a.subagent_type ?? "";
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 forkObjective = `${FORK_DIRECTIVE_FRAME}${prompt}`;
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: `Async agent launched successfully.
1678
- task_id: ${taskId}
1679
- ` +
1680
- `The forked agent inherits your full current context and works on its prompt in the background. ${notify ? "You will be notified automatically when it completes." : "Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted."}
1681
- ` +
1682
- (modelNote ? `${modelNote}\n` : "") +
1683
- (extraToolsNote ? `${extraToolsNote}\n` : "") +
1684
- (worktreeDir !== undefined
1685
- ? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)
1686
- `
1687
- : "") +
1688
- (sessionScopedBg
1689
- ? `It is session-scoped: it keeps running even after this task ends.
1690
- `
1691
- : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.
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: `Async agent launched successfully.
2312
- task_id: ${taskId}
2313
- ` +
2314
- `The agent is working in the background. ${notify ? "You will be notified automatically when it completes." : "Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted."}
2315
- ` +
2316
- (modelNote ? `${modelNote}\n` : "") +
2317
- (extraToolsNote ? `${extraToolsNote}\n` : "") +
2318
- (worktreeDir !== undefined
2319
- ? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)
2320
- `
2321
- : "") +
2322
- (sessionScopedBg
2323
- ? `It is session-scoped: it keeps running even after this task ends.
2324
- `
2325
- : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.
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
  }
@@ -26,6 +26,10 @@ export interface MemoryInjection {
26
26
  announcements?: MemoryAnnouncement[];
27
27
  announceBlock?: string;
28
28
  block: string;
29
+ indexSeed?: {
30
+ path: string;
31
+ content: string;
32
+ };
29
33
  }
30
34
  export declare class MemoryEngine {
31
35
  private readonly backend;
@@ -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 onDisk = readSafe(join(handle.writableRoot, MEMORY_INDEX_FILENAME));
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>;