@sema-agent/core 2.1.0 → 2.3.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 (61) hide show
  1. package/dist/agents/observer.d.ts +14 -0
  2. package/dist/agents/observer.js +58 -9
  3. package/dist/agents/send-message-tool.js +55 -10
  4. package/dist/agents/subagent.d.ts +1 -0
  5. package/dist/agents/subagent.js +69 -15
  6. package/dist/agents/teacher.js +51 -23
  7. package/dist/core/context-edit.js +16 -3
  8. package/dist/core/file-snapshot-store.js +10 -1
  9. package/dist/core/runner/assemble-result.d.ts +1 -0
  10. package/dist/core/runner/assemble-result.js +12 -8
  11. package/dist/core/runner/prepare-task.d.ts +1 -0
  12. package/dist/core/runner/prepare-task.js +40 -10
  13. package/dist/core/runner/runtask.js +27 -5
  14. package/dist/core/runner/synthetic-tools.d.ts +1 -0
  15. package/dist/core/runner/synthetic-tools.js +18 -15
  16. package/dist/core/runner/turn-attachments.d.ts +12 -2
  17. package/dist/core/runner/turn-attachments.js +33 -3
  18. package/dist/core/task-registry-agent.d.ts +11 -1
  19. package/dist/core/task-registry-agent.js +40 -3
  20. package/dist/core/task-registry-monitor.js +149 -29
  21. package/dist/core/task-registry-shared.d.ts +25 -2
  22. package/dist/core/task-registry-shared.js +26 -2
  23. package/dist/core/task-registry.d.ts +5 -0
  24. package/dist/core/task-registry.js +25 -26
  25. package/dist/core/tools.d.ts +2 -0
  26. package/dist/core/tools.js +9 -0
  27. package/dist/core/types.d.ts +3 -1
  28. package/dist/core/workflow-journal-store.d.ts +16 -0
  29. package/dist/core/workflow-journal-store.js +28 -0
  30. package/dist/engine/session/memory-repo.js +5 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/orchestration/workflow-size-guideline.d.ts +6 -1
  34. package/dist/orchestration/workflow-size-guideline.js +19 -9
  35. package/dist/orchestration/workflow.d.ts +16 -0
  36. package/dist/orchestration/workflow.js +80 -20
  37. package/dist/prompt-assembly/assemble.js +3 -7
  38. package/dist/prompt-assembly/packs/sema-default.js +8 -5
  39. package/dist/prompts/coordinator.d.ts +1 -1
  40. package/dist/prompts/coordinator.js +45 -0
  41. package/dist/prompts/default.d.ts +4 -5
  42. package/dist/prompts/default.js +16 -18
  43. package/dist/prompts/simple-sections.d.ts +3 -1
  44. package/dist/prompts/simple-sections.js +11 -1
  45. package/dist/stores/file/workflow-journal-store.d.ts +7 -1
  46. package/dist/stores/file/workflow-journal-store.js +70 -33
  47. package/dist/tools/fs/bash-readonly-classifier.js +20 -1
  48. package/dist/tools/fs/fs-bash.d.ts +1 -0
  49. package/dist/tools/fs/fs-bash.js +10 -3
  50. package/dist/tools/fs/fs-read.js +10 -10
  51. package/dist/tools/fs/fs-search-tools.js +42 -7
  52. package/dist/tools/fs/fs-write.js +18 -6
  53. package/dist/tools/fs/index.d.ts +1 -0
  54. package/dist/tools/fs/index.js +2 -1
  55. package/dist/tools/fs/safety.d.ts +4 -0
  56. package/dist/tools/fs/safety.js +102 -6
  57. package/dist/tools/fs/search.d.ts +1 -0
  58. package/dist/tools/fs/search.js +23 -3
  59. package/dist/tools/monitor.d.ts +2 -0
  60. package/dist/tools/monitor.js +3 -1
  61. 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;
@@ -209,6 +209,24 @@ export class ObserverDigestTap {
209
209
  this.safeFlush(slice);
210
210
  }
211
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.]";
212
230
  export class ObserverPairing {
213
231
  state = "armed";
214
232
  observedEnvelopeName;
@@ -278,18 +296,22 @@ export class ObserverPairing {
278
296
  const batch = this.buffer.splice(0, this.buffer.length);
279
297
  try {
280
298
  const digest = renderObserverDigestBatch(this, batch);
281
- if (!this.firstRunDone) {
282
- await this.spawner.spawnFirstRun({ framingPrompt: this.framingPrompt, digest });
283
- this.firstRunDone = true;
284
- }
285
- else {
286
- await this.spawner.deliver({ digest });
287
- }
299
+ await this.deliverBatch(digest);
288
300
  }
289
301
  catch (err) {
290
- this.state = "stopped";
291
- this.buffer = [];
302
+ if (errorName(err) === OBSERVER_STOPPED_BY_USER_ERROR_NAME) {
303
+ this.state = "stopped";
304
+ this.buffer = [];
305
+ this.safeOnError(err);
306
+ return;
307
+ }
292
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;
293
315
  }
294
316
  }
295
317
  }
@@ -297,6 +319,33 @@ export class ObserverPairing {
297
319
  this.delivering = false;
298
320
  }
299
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
+ }
300
349
  retire(state) {
301
350
  if (this.state !== "armed")
302
351
  return;
@@ -37,6 +37,7 @@ export function createSendMessageTool(opts) {
37
37
  contract: { contractId: "core.send_message@1", implementationRevision: "1" },
38
38
  executionMode: "parallel",
39
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. ` +
40
41
  `'to' is the agent's name (set at spawn via Agent({name})) or its task_id (a…). A RUNNING agent receives the ` +
41
42
  `message at its next turn (queued — never interrupts its current work); a FINISHED agent resumes as a new ` +
42
43
  `background run with its full prior conversation preserved, so don't re-explain what it already knows. Names ` +
@@ -50,7 +51,7 @@ export function createSendMessageTool(opts) {
50
51
  parameters: Type.Object({
51
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.' }),
52
53
  message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
53
- summary: Type.Optional(Type.String({ maxLength: 200, description: "A short (3-5 word) description of the follow-up." })),
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)" })),
54
55
  }),
55
56
  execute: async (args, ctx) => {
56
57
  const a = args;
@@ -60,6 +61,28 @@ export function createSendMessageTool(opts) {
60
61
  return { content: "Message not sent: 'to' was empty. Pass the agent's task_id (a…).", details: { error: "empty to" }, isError: true };
61
62
  if (!message)
62
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
+ }
63
86
  const senderId = ctx.taskId ?? opts.owner;
64
87
  if (senderId !== undefined && isObserverTaskId(senderId)) {
65
88
  return { content: OBSERVER_SENDMESSAGE_SENDER_REFUSAL, details: { error: "observer_sender" }, isError: true };
@@ -89,6 +112,17 @@ export function createSendMessageTool(opts) {
89
112
  details: { type: "send-message", status: "uplinked", to: "main", seq: uplinkSeqGlobal },
90
113
  };
91
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
+ }
92
126
  return {
93
127
  content: `Message not sent: "main" (the spawning conversation) is not a deliverable target here. ` +
94
128
  `Your completion is reported to it automatically — finish your task and your final report will be relayed.`,
@@ -126,10 +160,10 @@ export function createSendMessageTool(opts) {
126
160
  if (row.name === undefined || row.agentType === "fork")
127
161
  return undefined;
128
162
  const whoT3 = handle === to ? `agent ${handle}` : `agent "${to}" (${handle})`;
129
- if (row.status === "killed") {
163
+ if (row.status === "killed" && row.stoppedBy === "user") {
130
164
  return {
131
- content: `Message not sent: ${whoT3} was stopped${row.stoppedBy !== undefined ? ` (by ${row.stoppedBy})` : ""} a stopped agent is not revived. Launch a new agent with the needed context instead.`,
132
- details: { error: "killed", to, ...(row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}) },
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 },
133
167
  isError: true,
134
168
  };
135
169
  }
@@ -419,11 +453,13 @@ export function createSendMessageTool(opts) {
419
453
  }
420
454
  if (row.status === "killed") {
421
455
  const by = opts.registry.getStopAttribution(targetId);
422
- return {
423
- content: `Message not sent: ${who} was stopped${by ? ` (by ${by})` : ""} — a stopped agent is not resumable. Launch a new agent with the needed context instead.`,
424
- details: { error: "killed", to, ...(by !== undefined ? { stoppedBy: by } : {}) },
425
- isError: true,
426
- };
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
+ }
427
463
  }
428
464
  const runLedger = ctx.subagentRetain ?? opts.retain;
429
465
  const smLedgerSessionId = ctx.sessionId ?? opts.sessionId;
@@ -445,6 +481,13 @@ export function createSendMessageTool(opts) {
445
481
  isError: true,
446
482
  };
447
483
  }
484
+ if (ledger.get(resumeToolUseId)?.running === true) {
485
+ return {
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,
489
+ };
490
+ }
448
491
  return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
449
492
  const resume = createSubagentResume({
450
493
  ledger,
@@ -470,7 +513,9 @@ export function createSendMessageTool(opts) {
470
513
  const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
471
514
  const fromPrefix = ctx.parentTaskId !== undefined ? `(message from teammate "${opts.senderName ?? senderId ?? "unknown"}")\n` : "";
472
515
  try {
473
- const marker = await resume(`${fromPrefix}${summary ? `[${summary}] ${message}` : message}`);
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}`);
474
519
  return {
475
520
  content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
476
521
  `You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`,
@@ -133,6 +133,7 @@ export declare function asyncLaunchedReceipt(p: {
133
133
  taskId: string;
134
134
  workingLine: string;
135
135
  notify: boolean;
136
+ oneShot?: boolean;
136
137
  notes?: (string | undefined)[];
137
138
  }): string;
138
139
  export declare function createSubagentTool(opts: SubagentToolOptions): ToolSpec;
@@ -21,7 +21,7 @@ import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_
21
21
  export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
22
22
  import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
23
23
  import { recordRosterSpawn } from "./roster-store.js";
24
- import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
24
+ import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
25
25
  import { SubagentStepRecorder } from "./subagent-steps.js";
26
26
  const BG_AGENT_RESULT_MAX = 4_000;
27
27
  const BG_AGENT_RESULT_FULL_MAX = 200_000;
@@ -129,6 +129,9 @@ function createToolStatsCounter(delegationToolName) {
129
129
  snapshot() {
130
130
  return t.readCount + t.searchCount + t.bashCount + t.editFileCount + t.otherToolCount > 0 ? { ...t } : undefined;
131
131
  },
132
+ total() {
133
+ return t.readCount + t.searchCount + t.bashCount + t.editFileCount + t.otherToolCount;
134
+ },
132
135
  };
133
136
  }
134
137
  export const FORK_SUBAGENT_TYPE = "fork";
@@ -622,15 +625,20 @@ export function forkWorktreeTranslationNote(parentCwd, worktreeDir) {
622
625
  }
623
626
  export function asyncLaunchedReceipt(p) {
624
627
  const noteLines = (p.notes ?? []).filter((n) => n !== undefined && n !== "");
628
+ const oneShotBlocked = p.notify && p.oneShot === true;
625
629
  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
630
  `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`) +
631
+ (oneShotBlocked
632
+ ? `${p.workingLine} This is a ONE-SHOT submission there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput(task_id: "${p.taskId}", block: true). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.\n`
633
+ : p.notify
634
+ ? `${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`
635
+ : `${p.workingLine} Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted.\n`) +
630
636
  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.`));
637
+ (oneShotBlocked
638
+ ? `In your own words, briefly tell the user what you launched — do not echo this tool result. Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
639
+ : p.notify
640
+ ? `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.`
641
+ : `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
642
  }
635
643
  export function createSubagentTool(opts) {
636
644
  const catalog = opts.runner.agentCatalog;
@@ -1105,9 +1113,17 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1105
1113
  let observerTap;
1106
1114
  let observerSessionId;
1107
1115
  let observerFailure;
1116
+ let observerDegradedCount = 0;
1117
+ let observerDegradedFirst;
1108
1118
  const noteObserverFailure = (err) => {
1109
- if (observerFailure === undefined) {
1110
- observerFailure = (err instanceof Error ? err.message : String(err)).slice(0, REPORT_FIELD_MAX);
1119
+ const msg = (err instanceof Error ? err.message : String(err)).slice(0, REPORT_FIELD_MAX);
1120
+ if (observerPairing !== undefined && observerPairing.state === "armed") {
1121
+ observerDegradedCount++;
1122
+ if (observerDegradedFirst === undefined)
1123
+ observerDegradedFirst = msg;
1124
+ }
1125
+ else if (observerFailure === undefined) {
1126
+ observerFailure = msg;
1111
1127
  }
1112
1128
  try {
1113
1129
  opts.onObserverError?.(err, {
@@ -1139,7 +1155,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1139
1155
  pendingTrigger.push(prompt);
1140
1156
  const observerDef = observerArm.observerDefinition;
1141
1157
  const envelopeName = observerSlug(childAgentName ?? def.name);
1142
- const sid = uuidv7();
1158
+ let sid = uuidv7();
1143
1159
  observerSessionId = sid;
1144
1160
  markObserverTaskId(sid);
1145
1161
  const reportOpts = {
@@ -1163,15 +1179,36 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1163
1179
  });
1164
1180
  const runObserverLeg = async (spec) => {
1165
1181
  if (!isObserverTaskId(sid))
1166
- throw new Error("observer identity revoked — delivery refused");
1182
+ throw new ObserverStoppedByUserError("observer identity revoked — delivery refused");
1167
1183
  const r = await opts.runner.runTask(spec);
1168
1184
  if (r.status !== "completed") {
1185
+ if (r.errorCode === "resume.session_not_found") {
1186
+ throw new ObserverResumeStateError(`observer session no longer exists${r.errorMessage ? `: ${r.errorMessage}` : ""}`);
1187
+ }
1169
1188
  throw new Error(`observer run ended ${r.status}${r.errorMessage ? `: ${r.errorMessage}` : ""}`);
1170
1189
  }
1171
1190
  };
1172
1191
  const spawner = {
1173
1192
  spawnFirstRun: ({ framingPrompt, digest }) => runObserverLeg(observerSpec(`${framingPrompt}\n\n${digest}`, false)),
1174
1193
  deliver: ({ digest }) => runObserverLeg(observerSpec(digest, true)),
1194
+ restartFresh: async ({ framingPrompt, digest }) => {
1195
+ const dead = sid;
1196
+ const fresh = uuidv7();
1197
+ markObserverTaskId(fresh);
1198
+ sid = fresh;
1199
+ observerSessionId = fresh;
1200
+ try {
1201
+ await runObserverLeg(observerSpec(`${framingPrompt}\n\n${digest}`, false));
1202
+ }
1203
+ finally {
1204
+ unmarkObserverTaskId(dead);
1205
+ try {
1206
+ await opts.runner.sessions.release(dead);
1207
+ }
1208
+ catch {
1209
+ }
1210
+ }
1211
+ },
1175
1212
  };
1176
1213
  observerPairing = new ObserverPairing({
1177
1214
  observedEnvelopeName: envelopeName,
@@ -1209,9 +1246,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1209
1246
  t.unref?.();
1210
1247
  });
1211
1248
  const outcome = await Promise.race([observerPairing.drain().then(() => "drained"), timeout]);
1249
+ observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
1212
1250
  if (outcome === "timeout")
1213
1251
  noteObserverFailure(new Error("observer unresponsive at settle (delivery drain timed out)"));
1214
- observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
1215
1252
  if (observerSessionId !== undefined) {
1216
1253
  try {
1217
1254
  await opts.runner.sessions.release(observerSessionId);
@@ -1240,7 +1277,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1240
1277
  clearTimeout(timer);
1241
1278
  return outcome;
1242
1279
  };
1243
- const observerNoteFor = (outcome) => observerFailure !== undefined ? " [observer: failed]" : outcome === "still_draining" ? " [observer: still draining]" : "";
1280
+ const observerNoteFor = (outcome) => observerFailure !== undefined
1281
+ ? " [observer: failed]"
1282
+ : outcome === "still_draining"
1283
+ ? " [observer: still draining]"
1284
+ : observerDegradedCount > 0
1285
+ ? ` [observer: degraded, ${observerDegradedCount} digest batch(es) lost]`
1286
+ : "";
1244
1287
  const stepRecorder = new SubagentStepRecorder(ctx.toolCallId);
1245
1288
  const toolStatsCounter = createToolStatsCounter(opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME);
1246
1289
  let assistantTextTail = "";
@@ -1736,6 +1779,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1736
1779
  taskId,
1737
1780
  workingLine: `The forked agent inherits your full current context and works on its prompt in the background.`,
1738
1781
  notify: Boolean(notify),
1782
+ oneShot: ctx.oneShot === true,
1739
1783
  notes: [
1740
1784
  modelNote,
1741
1785
  extraToolsNote,
@@ -2366,6 +2410,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2366
2410
  taskId,
2367
2411
  workingLine: `The agent is working in the background.`,
2368
2412
  notify: Boolean(notify),
2413
+ oneShot: ctx.oneShot === true,
2369
2414
  notes: [
2370
2415
  modelNote,
2371
2416
  extraToolsNote,
@@ -2381,6 +2426,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2381
2426
  };
2382
2427
  }
2383
2428
  let child;
2429
+ const syncStartedAt = Date.now();
2384
2430
  let retainEntry;
2385
2431
  if (ctx.onSubagentSpawn || observerPairing) {
2386
2432
  retainEntry = ctx.onSubagentSpawn ? await tryRetainChild(ctx.subagentRetain) : undefined;
@@ -2488,9 +2534,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2488
2534
  child.blockedReason ? `blocked_reason: ${inlineUntrusted(child.blockedReason, REPORT_FIELD_MAX)}` : "",
2489
2535
  child.errorMessage ? `error: ${inlineUntrusted(child.errorMessage, REPORT_FIELD_MAX)}` : "",
2490
2536
  errClass ? `error_kind: ${errClass.errorKind} (retryable: ${errClass.retryable})` : "",
2491
- observerFailure !== undefined ? `observer: failed (${inlineUntrusted(observerFailure, REPORT_FIELD_MAX)})` : "",
2537
+ observerFailure !== undefined
2538
+ ? `observer: failed (${inlineUntrusted(observerFailure, REPORT_FIELD_MAX)})`
2539
+ : observerDegradedCount > 0
2540
+ ? `observer: degraded — ${observerDegradedCount} digest batch(es) never reached it (first: ${inlineUntrusted(observerDegradedFirst ?? "", REPORT_FIELD_MAX)})`
2541
+ : "",
2492
2542
  `stats: turns=${child.stats.turns} tokens=${child.stats.tokens}`,
2493
- retainEntry || failureRetained ? `transcript_id: ${child.sessionId}` : "",
2543
+ !retainEntry && failureRetained ? `transcript_id: ${child.sessionId} (internal ID - do not mention to user; inspect via AgentTranscript where mounted)` : "",
2494
2544
  retainEntry ? `resumable: true` : "",
2495
2545
  failureRetained
2496
2546
  ? `session_retained: this child's session is kept for a short time (~15 min) so its transcript stays inspectable via transcript_id; it is released automatically afterwards.`
@@ -2521,6 +2571,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2521
2571
  : "(no text result)",
2522
2572
  "",
2523
2573
  "Verify this result before concluding. If it leaves more to do, continue or delegate again.",
2574
+ retainEntry
2575
+ ? `transcript_id: ${child.sessionId} (internal ID - do not mention to user. Transcript inspectable via AgentTranscript where mounted; this child is continued through the deployment's resume handle, not by SendMessage.)`
2576
+ : "",
2577
+ `<usage>subagent_tokens: ${child.stats.tokens}\ntool_uses: ${toolStatsCounter.total()}\nduration_ms: ${Date.now() - syncStartedAt}</usage>`,
2524
2578
  ].filter(Boolean);
2525
2579
  return {
2526
2580
  content: lines.join("\n"),
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
3
+ import { isDefineToolProduct, stampDefineToolBrand } from "../core/tools.js";
3
4
  import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
4
5
  export const TEACHER_PROMPT = `You are an expert advisor to a less-capable "student" agent that got stuck.
5
6
  You receive the task and the student's recent failed attempts. Your job is to help the student RECOVER,
@@ -119,29 +120,56 @@ async function runTeacherCore(runner, studentSpec, teacher) {
119
120
  errorStreak = sig === lastErrSig ? errorStreak + 1 : 1;
120
121
  lastErrSig = sig;
121
122
  };
122
- const wrap = (t) => ({
123
- ...t,
124
- execute: async (args, ctx) => {
125
- try {
126
- const out = await t.execute(args, ctx);
127
- const text = typeof out === "string"
128
- ? out
129
- : out && typeof out === "object" && "content" in out
130
- ? String(out.content)
131
- : (JSON.stringify(out) ?? "");
132
- logTool(t.name, args, text);
133
- errorStreak = 0;
134
- lastErrSig = "";
135
- return out;
136
- }
137
- catch (e) {
138
- const errText = String(e);
139
- logTool(t.name, args, `ERROR: ${errText}`);
140
- onToolFail(t.name, errText);
141
- throw e;
142
- }
143
- },
144
- });
123
+ const wrap = (t) => {
124
+ if (isDefineToolProduct(t)) {
125
+ const product = t;
126
+ return stampDefineToolBrand({
127
+ ...t,
128
+ execute: async (toolCallId, rawParams, signal, onUpdate) => {
129
+ try {
130
+ const out = await product.execute(toolCallId, rawParams, signal, onUpdate);
131
+ const blocks = out && typeof out === "object" && "content" in out ? out.content : out;
132
+ const text = Array.isArray(blocks)
133
+ ? blocks.map((b) => (b && typeof b === "object" && "text" in b ? String(b.text) : "")).join("\n")
134
+ : String(blocks ?? "");
135
+ logTool(t.name, rawParams, text);
136
+ errorStreak = 0;
137
+ lastErrSig = "";
138
+ return out;
139
+ }
140
+ catch (e) {
141
+ const errText = String(e);
142
+ logTool(t.name, rawParams, `ERROR: ${errText}`);
143
+ onToolFail(t.name, errText);
144
+ throw e;
145
+ }
146
+ },
147
+ });
148
+ }
149
+ return {
150
+ ...t,
151
+ execute: async (args, ctx) => {
152
+ try {
153
+ const out = await t.execute(args, ctx);
154
+ const text = typeof out === "string"
155
+ ? out
156
+ : out && typeof out === "object" && "content" in out
157
+ ? String(out.content)
158
+ : (JSON.stringify(out) ?? "");
159
+ logTool(t.name, args, text);
160
+ errorStreak = 0;
161
+ lastErrSig = "";
162
+ return out;
163
+ }
164
+ catch (e) {
165
+ const errText = String(e);
166
+ logTool(t.name, args, `ERROR: ${errText}`);
167
+ onToolFail(t.name, errText);
168
+ throw e;
169
+ }
170
+ },
171
+ };
172
+ };
145
173
  const tools = studentSpec.tools?.map(wrap);
146
174
  const helperBase = () => teacher.helperModel
147
175
  ? { model: teacher.helperModel }
@@ -2,7 +2,20 @@ import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "
2
2
  import { isToolResult } from "./message-utils.js";
3
3
  const CLEARED_MARKER = "[tool result cleared to save context]";
4
4
  const refNote = (ref) => `full text persisted; call ReadToolResult with ref "${ref}" to read it back`;
5
- const mediaNote = (n) => `${n} attachment${n === 1 ? "" : "s"} no longer visible after this clear`;
5
+ const mediaNote = (blocks) => {
6
+ const byType = new Map();
7
+ for (const b of blocks) {
8
+ const t = b.type ?? "untyped";
9
+ byType.set(t, (byType.get(t) ?? 0) + 1);
10
+ }
11
+ const n = blocks.length;
12
+ if (byType.size === 1) {
13
+ const [t, c] = [...byType][0];
14
+ return `${c} ${t} attachment${c === 1 ? "" : "s"} no longer visible after this clear`;
15
+ }
16
+ const breakdown = [...byType].map(([t, c]) => `${c} ${t}`).join(", ");
17
+ return `${n} attachments (${breakdown}) no longer visible after this clear`;
18
+ };
6
19
  const clearedMarker = (notes) => {
7
20
  const present = notes.filter((n) => n !== undefined);
8
21
  return present.length > 0 ? `[tool result cleared to save context — ${present.join("; ")}]` : CLEARED_MARKER;
@@ -91,8 +104,8 @@ export function clearStaleToolResults(messages, opts) {
91
104
  }
92
105
  }
93
106
  }
94
- const mediaCount = rawContent.filter((c) => c?.type !== "text").length;
95
- const marker = clearedMarker([ref, mediaCount > 0 ? mediaNote(mediaCount) : undefined]);
107
+ const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
108
+ const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined]);
96
109
  const cleared = {
97
110
  ...out[idx],
98
111
  content: [{ type: "text", text: marker }],
@@ -204,9 +204,18 @@ export class InMemoryFileSnapshotStore {
204
204
  try {
205
205
  for (const hash of new Set(manifest.values())) {
206
206
  written.add(hash);
207
+ if (hash === "" || hash === "." || hash === ".." || !/^[A-Za-z0-9_.-]+$/.test(hash)) {
208
+ return { ok: false, error: { code: "read_failed", message: `unsafe blob hash ${JSON.stringify(hash)}` } };
209
+ }
207
210
  if (this.blobs.has(hash))
208
211
  continue;
209
- const bytes = await srcGetBlob(hash);
212
+ let bytes;
213
+ try {
214
+ bytes = await srcGetBlob(hash);
215
+ }
216
+ catch (err) {
217
+ return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fetch failed: ${err.message}` } };
218
+ }
210
219
  if (!bytes)
211
220
  return { ok: false, error: { code: "read_failed", message: `missing source blob ${hash}` } };
212
221
  if (createHash("sha256").update(bytes).digest("hex") !== hash) {
@@ -68,6 +68,7 @@ export interface Stats {
68
68
  export interface ResultFlags {
69
69
  threw: unknown;
70
70
  model?: string;
71
+ unpricedSpend?: boolean;
71
72
  abortedForTimeout: boolean;
72
73
  abortedForTurns: boolean;
73
74
  abortedLive?: boolean;
@@ -36,14 +36,16 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
36
36
  const taskId = spec.taskId ?? sessionId;
37
37
  const text = final ? assistantText(final) : "";
38
38
  stats.totalInputTokens = stats.promptTokens;
39
- const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
40
- const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
41
- stats.costBreakdown = {
42
- llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
43
- nestedSubagentMicroUsd,
44
- memoryConsolidationMicroUsd: 0,
45
- compactionMicroUsd,
46
- };
39
+ if (!flags.unpricedSpend) {
40
+ const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
41
+ const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
42
+ stats.costBreakdown = {
43
+ llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
44
+ nestedSubagentMicroUsd,
45
+ memoryConsolidationMicroUsd: 0,
46
+ compactionMicroUsd,
47
+ };
48
+ }
47
49
  let status;
48
50
  const result = text;
49
51
  let errorMessage;
@@ -139,5 +141,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
139
141
  }
140
142
  const { compactionMicroUsd: _internalCompaction, ...publicStats } = stats;
141
143
  void _internalCompaction;
144
+ if (flags.unpricedSpend)
145
+ delete publicStats.costMicroUsd;
142
146
  return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, stats: publicStats };
143
147
  }
@@ -114,6 +114,7 @@ export interface Prepared {
114
114
  apply: (committedArtifactDigest?: string) => void;
115
115
  } | undefined;
116
116
  activeTools: Set<string>;
117
+ deferredToolNames?: ReadonlySet<string>;
117
118
  memoryEngineSession?: {
118
119
  engine: MemoryEngine;
119
120
  handle: MemorySessionHandle;