@sema-agent/core 1.452.0 → 2.0.1

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 (167) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/repair-loop.d.ts +1 -1
  5. package/dist/agents/repair-loop.js +1 -1
  6. package/dist/agents/retain-ledger.d.ts +60 -0
  7. package/dist/agents/retain-ledger.js +225 -0
  8. package/dist/agents/roster-store.d.ts +1 -0
  9. package/dist/agents/roster-store.js +9 -0
  10. package/dist/agents/send-message-tool.d.ts +41 -0
  11. package/dist/agents/send-message-tool.js +469 -0
  12. package/dist/agents/subagent.d.ts +33 -108
  13. package/dist/agents/subagent.js +22 -834
  14. package/dist/agents/teacher.d.ts +0 -1
  15. package/dist/agents/teacher.js +0 -9
  16. package/dist/bin/sema-tb.js +3 -3
  17. package/dist/brain/anthropic.js +15 -40
  18. package/dist/brain/openai.js +15 -40
  19. package/dist/brain/reasoning.d.ts +1 -1
  20. package/dist/brain/stream-engine.js +2 -2
  21. package/dist/brain/stream-shared.d.ts +17 -0
  22. package/dist/brain/stream-shared.js +50 -0
  23. package/dist/brain/timeout.d.ts +1 -1
  24. package/dist/brain/timeout.js +1 -1
  25. package/dist/config/catalog.js +1 -3
  26. package/dist/config/defaults.d.ts +6 -0
  27. package/dist/config/defaults.js +6 -0
  28. package/dist/core/ask-question.d.ts +1 -1
  29. package/dist/core/background-shell.d.ts +1 -1
  30. package/dist/core/checkpoint-store.d.ts +0 -1
  31. package/dist/core/exec-gate.d.ts +1 -1
  32. package/dist/core/exec-output-tail.d.ts +1 -0
  33. package/dist/core/exec-output-tail.js +6 -0
  34. package/dist/core/file-snapshot-store.d.ts +1 -1
  35. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  36. package/dist/core/git-worktree-env.d.ts +1 -1
  37. package/dist/core/hooks.d.ts +1 -1
  38. package/dist/core/image-downsample.d.ts +44 -0
  39. package/dist/core/image-downsample.js +97 -0
  40. package/dist/core/lsp.d.ts +1 -1
  41. package/dist/core/mcp.d.ts +3 -45
  42. package/dist/core/mcp.js +2 -97
  43. package/dist/core/media-byte-cap.d.ts +1 -1
  44. package/dist/core/memory-recall.d.ts +0 -12
  45. package/dist/core/memory-recall.js +0 -222
  46. package/dist/core/memory.d.ts +0 -6
  47. package/dist/core/memory.js +0 -88
  48. package/dist/core/message-utils.d.ts +1 -1
  49. package/dist/core/oracle-isolation.d.ts +1 -1
  50. package/dist/core/remote-env.d.ts +1 -1
  51. package/dist/core/roles.d.ts +1 -1
  52. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  53. package/dist/core/runner/assemble-result.d.ts +0 -2
  54. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  55. package/dist/core/runner/prepare-memory.d.ts +16 -0
  56. package/dist/core/runner/prepare-memory.js +191 -0
  57. package/dist/core/runner/prepare-task.d.ts +4 -28
  58. package/dist/core/runner/prepare-task.js +19 -207
  59. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  60. package/dist/core/runner/runtask.d.ts +2 -3
  61. package/dist/core/runner/runtask.js +337 -529
  62. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  63. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  64. package/dist/core/runner/synthetic-tools.js +4 -4
  65. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  66. package/dist/core/runner/tool-disclosure.js +2 -2
  67. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  68. package/dist/core/runner/tool-output-projection.js +81 -0
  69. package/dist/core/runtime.d.ts +1 -1
  70. package/dist/core/scheduler.d.ts +1 -1
  71. package/dist/core/sensitive-path-policy.d.ts +1 -1
  72. package/dist/core/session-reconcile.d.ts +1 -1
  73. package/dist/core/session-store.d.ts +2 -1
  74. package/dist/core/session-store.js +2 -1
  75. package/dist/core/session.d.ts +0 -1
  76. package/dist/core/task-registry-agent.d.ts +115 -0
  77. package/dist/core/task-registry-agent.js +1163 -0
  78. package/dist/core/task-registry-monitor.d.ts +13 -0
  79. package/dist/core/task-registry-monitor.js +347 -0
  80. package/dist/core/task-registry-shared.d.ts +294 -0
  81. package/dist/core/task-registry-shared.js +271 -0
  82. package/dist/core/task-registry-workflow.d.ts +5 -0
  83. package/dist/core/task-registry-workflow.js +143 -0
  84. package/dist/core/task-registry.d.ts +51 -216
  85. package/dist/core/task-registry.js +127 -1940
  86. package/dist/core/task-tool-shape.d.ts +32 -0
  87. package/dist/core/task-tool-shape.js +83 -0
  88. package/dist/core/tool-policy.d.ts +1 -2
  89. package/dist/core/tool-result-budget.d.ts +1 -1
  90. package/dist/core/tool-result-store.d.ts +2 -2
  91. package/dist/core/tool-result-store.js +1 -1
  92. package/dist/core/tools.d.ts +2 -3
  93. package/dist/core/tools.js +1 -8
  94. package/dist/core/trace.d.ts +2 -2
  95. package/dist/core/types.d.ts +0 -29
  96. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  97. package/dist/core/workflow-run-store-contract.js +19 -19
  98. package/dist/engine/execution-env/node-execution-env.d.ts +13 -0
  99. package/dist/engine/execution-env/node-execution-env.js +55 -25
  100. package/dist/engine/loop/agent-loop.d.ts +1 -1
  101. package/dist/engine/loop/agent-loop.js +10 -9
  102. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  103. package/dist/engine/lsp/frame-decoder.js +1 -1
  104. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  105. package/dist/engine/lsp/stdio-lsp-transport.js +2 -2
  106. package/dist/index.d.ts +16 -14
  107. package/dist/index.js +13 -11
  108. package/dist/internal/harness-types.d.ts +7 -0
  109. package/dist/internal/harness-types.js +1 -0
  110. package/dist/internal/harness.d.ts +1 -7
  111. package/dist/internal/harness.js +1 -0
  112. package/dist/orchestration/goal.js +2 -2
  113. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  114. package/dist/orchestration/workflow-types.d.ts +193 -0
  115. package/dist/orchestration/workflow-types.js +54 -0
  116. package/dist/orchestration/workflow.d.ts +3 -193
  117. package/dist/orchestration/workflow.js +7 -59
  118. package/dist/prompt-assembly/assemble.d.ts +3 -3
  119. package/dist/prompt-assembly/assemble.js +0 -16
  120. package/dist/prompt-assembly/explain.d.ts +1 -1
  121. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  122. package/dist/prompts/default.d.ts +1 -6
  123. package/dist/prompts/default.js +3 -11
  124. package/dist/scenarios/env.d.ts +1 -1
  125. package/dist/scenarios/full-body.d.ts +0 -3
  126. package/dist/scenarios/full-body.js +0 -2
  127. package/dist/stores/file/background-agent-store.d.ts +2 -15
  128. package/dist/stores/file/background-agent-store.js +30 -100
  129. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  130. package/dist/stores/file/checkpoint-store.js +47 -119
  131. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  132. package/dist/stores/file/mailbox-store.js +33 -2
  133. package/dist/stores/file/shared-ledger.d.ts +35 -0
  134. package/dist/stores/file/shared-ledger.js +94 -0
  135. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  136. package/dist/stores/file/workflow-run-store.js +24 -93
  137. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  138. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  139. package/dist/tools/fs/fs-bash.d.ts +31 -0
  140. package/dist/tools/fs/fs-bash.js +672 -0
  141. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  142. package/dist/tools/fs/fs-pdf.js +236 -0
  143. package/dist/tools/fs/fs-read.d.ts +8 -0
  144. package/dist/tools/fs/fs-read.js +286 -0
  145. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  146. package/dist/tools/fs/fs-search-tools.js +175 -0
  147. package/dist/tools/fs/fs-shared.d.ts +56 -0
  148. package/dist/tools/fs/fs-shared.js +148 -0
  149. package/dist/tools/fs/fs-write.d.ts +8 -0
  150. package/dist/tools/fs/fs-write.js +421 -0
  151. package/dist/tools/fs/index.d.ts +12 -65
  152. package/dist/tools/fs/index.js +26 -2090
  153. package/dist/tools/fs/pdf.d.ts +1 -1
  154. package/dist/tools/fs/repo-map.d.ts +3 -3
  155. package/dist/tools/fs/repo-map.js +1 -1
  156. package/dist/tools/fs/safety.d.ts +4 -1
  157. package/dist/tools/fs/safety.js +7 -1
  158. package/dist/tools/fs/search.d.ts +2 -1
  159. package/dist/tools/fs/search.js +21 -3
  160. package/dist/tools/monitor.d.ts +1 -1
  161. package/dist/tools/scheduler-tools.d.ts +1 -1
  162. package/dist/tools/web.d.ts +2 -2
  163. package/dist/tools/web.js +46 -8
  164. package/dist/tools/worktree.d.ts +1 -1
  165. package/package.json +1 -1
  166. package/dist/core/dynamic-recall.d.ts +0 -32
  167. package/dist/core/dynamic-recall.js +0 -76
@@ -0,0 +1,469 @@
1
+ import { Type } from "typebox";
2
+ import { defineTool } from "../core/tools.js";
3
+ import { normalizeAgentName, DURABLE_AGENT_HANDLE_RE, DURABLE_AGENT_HEARTBEAT_MS } from "../core/task-registry.js";
4
+ import { canAccessAgentRecord } from "../core/background-agent-store.js";
5
+ import { isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
6
+ import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
7
+ import { createSubagentResume } from "./subagent.js";
8
+ export const SEND_MESSAGE_TOOL_NAME = "SendMessage";
9
+ let uplinkSeqGlobal = Date.now();
10
+ const UPLINK_RESULT_MAX = 8000;
11
+ const REVIVE_LEASE_TTL_MS = 5 * 60_000;
12
+ export function createSendMessageTool(opts) {
13
+ const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
14
+ return defineTool({
15
+ name: SEND_MESSAGE_TOOL_NAME,
16
+ contract: { contractId: "core.send_message@1", implementationRevision: "1" },
17
+ executionMode: "parallel",
18
+ description: `Send a follow-up message to a previously spawned background agent. ` +
19
+ `'to' is the agent's name (set at spawn via Agent({name})) or its task_id (a…). A RUNNING agent receives the ` +
20
+ `message at its next turn (queued — never interrupts its current work); a FINISHED agent resumes as a new ` +
21
+ `background run with its full prior conversation preserved, so don't re-explain what it already knows. Names ` +
22
+ `keep working after an agent completes; when a newer agent took the name, the newest wins — use the task_id to ` +
23
+ `address an older one. You will be notified automatically when it completes — prefer ending your turn; do not ` +
24
+ `poll. ` +
25
+ (tier3Capable
26
+ ? `Continuing a finished agent works for NAMED agents (revived from their durable record, even across restarts) and for runs that retain sub-agent sessions; when neither covers it you get an honest error and should launch a new agent with the needed context instead.`
27
+ : `Continuing a finished agent requires the run to retain sub-agent sessions; when the session was not ` +
28
+ `retained (or has expired) you get an honest error and should launch a new agent with the needed context instead.`),
29
+ parameters: Type.Object({
30
+ to: Type.String({ description: 'Recipient: the agent\'s name, or its task_id (a…) returned by the Agent tool with run_in_background. "main" is reserved for the spawning conversation.' }),
31
+ message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
32
+ summary: Type.Optional(Type.String({ maxLength: 200, description: "A short (3-5 word) description of the follow-up." })),
33
+ }),
34
+ execute: async (args, ctx) => {
35
+ const a = args;
36
+ const to = String(a.to ?? "").trim();
37
+ const message = String(a.message ?? "").trim();
38
+ if (!to)
39
+ return { content: "Message not sent: 'to' was empty. Pass the agent's task_id (a…).", details: { error: "empty to" }, isError: true };
40
+ if (!message)
41
+ return { content: "Message not sent: 'message' was empty.", details: { error: "empty message" }, isError: true };
42
+ const senderId = ctx.taskId ?? opts.owner;
43
+ if (senderId !== undefined && isObserverTaskId(senderId)) {
44
+ return { content: OBSERVER_SENDMESSAGE_SENDER_REFUSAL, details: { error: "observer_sender" }, isError: true };
45
+ }
46
+ if (isObserverTaskId(to)) {
47
+ return { content: OBSERVER_SENDMESSAGE_TARGET_REFUSAL, details: { error: "observer_target", to }, isError: true };
48
+ }
49
+ if (normalizeAgentName(to) === "main") {
50
+ if (opts.uplink && senderId !== undefined) {
51
+ const uplinkSummaryRaw = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : message.slice(0, 80);
52
+ const fromLabel = opts.senderName !== undefined ? opts.senderName : senderId;
53
+ try {
54
+ opts.uplink({
55
+ task_id: senderId,
56
+ task_type: "background_agent",
57
+ status: "event",
58
+ summary: `message from ${fromLabel}: ${uplinkSummaryRaw}`,
59
+ result: message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[uplink truncated: ${message.length} chars total — read the agent's transcript for the rest]` : message,
60
+ seq: ++uplinkSeqGlobal,
61
+ }, { priority: "next" });
62
+ }
63
+ catch (e) {
64
+ return { content: `Message not sent: the parent's notification lane rejected it (${e instanceof Error ? e.message : String(e)}).`, details: { error: "uplink_failed", to }, isError: true };
65
+ }
66
+ return {
67
+ content: `Message sent to main — it will reach the spawning conversation at its next turn boundary. Continue with your task; do not wait for a reply.`,
68
+ details: { type: "send-message", status: "uplinked", to: "main", seq: uplinkSeqGlobal },
69
+ };
70
+ }
71
+ return {
72
+ content: `Message not sent: "main" (the spawning conversation) is not a deliverable target here. ` +
73
+ `Your completion is reported to it automatically — finish your task and your final report will be relayed.`,
74
+ details: { error: "main_not_deliverable", to },
75
+ isError: true,
76
+ };
77
+ }
78
+ const access = {
79
+ owner: ctx.taskId ?? opts.owner,
80
+ scope: ctx.principal ?? opts.scope,
81
+ ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}),
82
+ };
83
+ const tier3Revive = async (handle) => {
84
+ if (opts.agentStore === undefined || opts.mailbox === undefined || opts.reviveSpawn === undefined)
85
+ return undefined;
86
+ const scope = access.scope;
87
+ if (scope === undefined)
88
+ return undefined;
89
+ let row;
90
+ try {
91
+ row = await opts.agentStore.get(handle, scope);
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ if (!row)
97
+ return undefined;
98
+ if (!canAccessAgentRecord(row, access)) {
99
+ if (ctx.parentTaskId === undefined)
100
+ return undefined;
101
+ const parentView = { ...access, owner: ctx.parentTaskId, ...(ctx.parentSessionId !== undefined ? { sessionId: ctx.parentSessionId } : {}) };
102
+ if (!canAccessAgentRecord(row, parentView))
103
+ return undefined;
104
+ }
105
+ if (row.name === undefined || row.agentType === "fork")
106
+ return undefined;
107
+ const whoT3 = handle === to ? `agent ${handle}` : `agent "${to}" (${handle})`;
108
+ if (row.status === "killed") {
109
+ return {
110
+ 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.`,
111
+ details: { error: "killed", to, ...(row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}) },
112
+ isError: true,
113
+ };
114
+ }
115
+ if (row.status === "parked") {
116
+ return {
117
+ content: `Message not sent: ${whoT3} is parked on a pending approval — it resumes when the approval is decided (durable approval inbox), not by message delivery. Send again after it resumes.`,
118
+ details: { error: "parked_pending_approval", to },
119
+ isError: true,
120
+ };
121
+ }
122
+ if (row.status === "running") {
123
+ if (row.writerId === opts.registry.writerId) {
124
+ return {
125
+ content: `Message not sent: ${whoT3} is currently running in this process but its mid-run delivery channel is not reachable from here. Wait for its completion notification, then send again to continue it.`,
126
+ details: { error: "still_running", to },
127
+ isError: true,
128
+ };
129
+ }
130
+ if (Date.now() - row.updatedAt > 10 * DURABLE_AGENT_HEARTBEAT_MS) {
131
+ return {
132
+ content: `Message not sent: ${whoT3} last ran on another host that has stopped renewing its record (no heartbeat). Its row will be settled by the stale-row sweep — send again after that to revive it.`,
133
+ details: { error: "host_gone", to },
134
+ isError: true,
135
+ };
136
+ }
137
+ return {
138
+ content: `Message not sent: ${whoT3} is running on another host — this process has no delivery channel to it. Wait for its completion notification, then send again to continue it.`,
139
+ details: { error: "running_elsewhere", to },
140
+ isError: true,
141
+ };
142
+ }
143
+ if (row.sessionId === undefined)
144
+ return undefined;
145
+ if (!opts.registry.beginDurableClaim(handle)) {
146
+ return {
147
+ content: `Message not sent: ${whoT3} is being revived or recycled right now — send again in a moment.`,
148
+ details: { error: "claim_contended", to },
149
+ isError: true,
150
+ };
151
+ }
152
+ try {
153
+ const now = Date.now();
154
+ const nextSeq = (row.seq ?? 1) + 1;
155
+ const claimed = { ...row, status: "running", writerId: opts.registry.writerId, writerEpoch: (row.writerEpoch ?? 0) + 1, updatedAt: now, seq: nextSeq };
156
+ delete claimed.settledAt;
157
+ delete claimed.stoppedBy;
158
+ delete claimed.finalOutput;
159
+ delete claimed.error;
160
+ delete claimed.resultIsPartial;
161
+ delete claimed.summary;
162
+ delete claimed.recentSteps;
163
+ delete claimed.editedFiles;
164
+ delete claimed.usage;
165
+ let won = false;
166
+ try {
167
+ won = await opts.agentStore.update(handle, scope, claimed, { rev: row.rev });
168
+ }
169
+ catch {
170
+ won = false;
171
+ }
172
+ if (!won) {
173
+ let fresh = null;
174
+ try {
175
+ fresh = await opts.agentStore.get(handle, scope);
176
+ }
177
+ catch {
178
+ }
179
+ if (fresh?.status === "running") {
180
+ return {
181
+ content: `Message not sent: ${whoT3} was just revived by another delivery — it is running now. Send again to reach the running agent.`,
182
+ details: { error: "claim_lost", to },
183
+ isError: true,
184
+ };
185
+ }
186
+ return {
187
+ content: `Message not sent: ${whoT3}'s durable record changed underneath this delivery — send again.`,
188
+ details: { error: "claim_lost", to },
189
+ isError: true,
190
+ };
191
+ }
192
+ const claimedRev = row.rev + 1;
193
+ const rollback = async () => {
194
+ try {
195
+ await opts.agentStore.update(handle, scope, { ...row, updatedAt: Date.now() }, { rev: claimedRev });
196
+ }
197
+ catch {
198
+ }
199
+ };
200
+ try {
201
+ await opts.runner.sessions.acquire(row.sessionId, { requireExisting: true });
202
+ }
203
+ catch (e) {
204
+ await rollback();
205
+ if (e?.code === "not_found") {
206
+ return {
207
+ content: `Message not sent: ${whoT3}'s transcript session no longer exists (evicted or reaped) — it cannot be revived. Launch a new agent with the needed context instead.`,
208
+ details: { error: "resume.session_not_found", to },
209
+ isError: true,
210
+ };
211
+ }
212
+ return {
213
+ content: `Message not sent: ${whoT3}'s transcript store did not answer (${e instanceof Error ? e.message : String(e)}) — nothing was parked; send again.`,
214
+ details: { error: "session_store_unavailable", to },
215
+ isError: true,
216
+ };
217
+ }
218
+ const clipped = message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]` : message;
219
+ const t3Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
220
+ const t3From = opts.senderName ?? senderId;
221
+ try {
222
+ await opts.mailbox.append(scope, handle, { ...(t3From !== undefined ? { from: t3From } : {}), content: t3Summary !== undefined ? `[${t3Summary}] ${clipped}` : clipped, sentAt: now });
223
+ }
224
+ catch (e) {
225
+ await rollback();
226
+ return {
227
+ content: `Message not sent: the durable mailbox refused the message (${e instanceof Error ? e.message : String(e)}) — nothing was parked and the agent was not revived.`,
228
+ details: { error: "mailbox_failed", to },
229
+ isError: true,
230
+ };
231
+ }
232
+ const leaseOwner = `${opts.registry.writerId}:${ctx.toolCallId}`;
233
+ let lease = null;
234
+ try {
235
+ lease = await opts.mailbox.claimLease(scope, handle, leaseOwner, REVIVE_LEASE_TTL_MS);
236
+ }
237
+ catch {
238
+ lease = null;
239
+ }
240
+ if (lease === null) {
241
+ await rollback();
242
+ return {
243
+ content: `Message not sent now: ${whoT3}'s mailbox is leased by another in-flight revival. Your message is queued and will be delivered with the next successful revival — do not resend the same content.`,
244
+ details: { error: "mailbox_leased", to },
245
+ };
246
+ }
247
+ const revivePrompt = lease.messages.map((m) => `<teammate-message teammate_id="${m.from ?? "main"}">\n${m.content}\n</teammate-message>`).join("\n");
248
+ let spawned;
249
+ try {
250
+ spawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: revivePrompt });
251
+ }
252
+ catch (e) {
253
+ spawned = { isError: true, content: e instanceof Error ? e.message : String(e), details: { error: "revive_failed" } };
254
+ }
255
+ if (spawned.isError === true) {
256
+ try {
257
+ await opts.mailbox.releaseLease(scope, handle, leaseOwner);
258
+ }
259
+ catch {
260
+ }
261
+ await rollback();
262
+ const code = spawned.details?.error;
263
+ return {
264
+ content: `Message not sent: ${whoT3} could not be revived — ${spawned.content}\nYour message remains queued and will be delivered if a later revival succeeds; do not resend the same content.`,
265
+ details: { error: code ?? "revive_failed", to },
266
+ };
267
+ }
268
+ try {
269
+ await opts.mailbox.ack(scope, handle, leaseOwner, lease.maxSeq);
270
+ }
271
+ catch {
272
+ }
273
+ const priorCount = lease.messages.length - 1;
274
+ return {
275
+ content: `Message sent — ${whoT3} was revived in the background with its prior context intact${priorCount > 0 ? ` (${priorCount} earlier pending message(s) delivered with it)` : ""}. ` +
276
+ `You will be notified automatically when it completes. Continue with other work — do not poll.`,
277
+ details: { type: "send-message", status: "revived", to, task_id: handle, seq: nextSeq, ...(priorCount > 0 ? { priorMessages: priorCount } : {}) },
278
+ };
279
+ }
280
+ finally {
281
+ opts.registry.endDurableClaim(handle);
282
+ }
283
+ };
284
+ let idRow = opts.registry.getAccessibleTask(to, access);
285
+ let resolvedAccess = access;
286
+ if (!idRow && ctx.parentTaskId !== undefined) {
287
+ const parentView = { ...access, owner: ctx.parentTaskId, ...(ctx.parentSessionId !== undefined ? { sessionId: ctx.parentSessionId } : {}) };
288
+ const siblingById = opts.registry.getAccessibleTask(to, parentView);
289
+ if (siblingById) {
290
+ idRow = siblingById;
291
+ resolvedAccess = parentView;
292
+ }
293
+ }
294
+ if (!idRow && DURABLE_AGENT_HANDLE_RE.test(to)) {
295
+ const revived = await tier3Revive(to);
296
+ if (revived !== undefined)
297
+ return revived;
298
+ }
299
+ let target = idRow;
300
+ if (!target) {
301
+ let byName = opts.registry.resolveBackgroundAgentByName(to, access);
302
+ if (byName.status === "not_found" && ctx.parentTaskId !== undefined) {
303
+ const parentView = { ...access, owner: ctx.parentTaskId, ...(ctx.parentSessionId !== undefined ? { sessionId: ctx.parentSessionId } : {}) };
304
+ const sibling = opts.registry.resolveBackgroundAgentByName(to, parentView);
305
+ if (sibling.status === "found" || sibling.status === "ambiguous") {
306
+ byName = sibling;
307
+ resolvedAccess = parentView;
308
+ }
309
+ }
310
+ if (byName.status === "ambiguous") {
311
+ return { content: `Message not sent: ${byName.message}`, details: { error: "ambiguous", to }, isError: true };
312
+ }
313
+ if (byName.status === "found") {
314
+ const h = byName.handle;
315
+ target = { task_id: h.id, type: h.type, status: h.status, ...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}), ...(h.sessionScoped ? { sessionScoped: true } : {}), ...(h.description !== undefined ? { description: h.description } : {}), ...(h.name !== undefined ? { name: h.name } : {}), ...(h.agentType !== undefined ? { agentType: h.agentType } : {}), ...(h.owner !== undefined ? { owner: h.owner } : {}), ...(h.scope !== undefined ? { scope: h.scope } : {}), ...(h.parentTaskId !== undefined ? { parentTaskId: h.parentTaskId } : {}), ...(h.parentSessionId !== undefined ? { parentSessionId: h.parentSessionId } : {}), ...(h.rootSessionId !== undefined ? { rootSessionId: h.rootSessionId } : {}) };
316
+ }
317
+ else {
318
+ let rosterHit;
319
+ if (opts.roster) {
320
+ try {
321
+ rosterHit = await opts.roster.resolve(to, access);
322
+ }
323
+ catch {
324
+ }
325
+ }
326
+ if (rosterHit !== undefined) {
327
+ const revived = await tier3Revive(rosterHit.agentId);
328
+ if (revived !== undefined)
329
+ return revived;
330
+ return {
331
+ content: `Message not sent: "${to}" is on the durable roster (agent ${rosterHit.agentId}) but not reachable in this run — its session belongs to an earlier process. ` +
332
+ `Launch a new agent with the needed context instead.`,
333
+ details: { error: "roster_only", to, agentId: rosterHit.agentId },
334
+ isError: true,
335
+ };
336
+ }
337
+ const labels = opts.registry.runningBackgroundAgentLabels(access);
338
+ return {
339
+ content: `Message not sent: no agent matches "${to}" (unknown id or name, not yours, or expired).` +
340
+ (byName.suggestion !== undefined ? ` Did you mean: ${byName.suggestion}?` : "") +
341
+ (labels.length > 0 ? ` Running background agents: ${labels.join(", ")}.` : "") +
342
+ ` Note: a completed foreground agent is not resumable (its transcript is not retained) — spawn with run_in_background to keep an agent addressable, or launch a new agent.`,
343
+ details: { error: "not_found", to, ...(byName.suggestion !== undefined ? { suggestion: byName.suggestion } : {}) },
344
+ isError: true,
345
+ };
346
+ }
347
+ }
348
+ const row = target;
349
+ if (row === undefined)
350
+ return { content: `Message not sent: no agent named "${to}" is reachable.`, details: { error: "not_found", to }, isError: true };
351
+ const targetId = row.task_id;
352
+ const who = targetId === to ? `agent ${targetId}` : `agent "${to}" (${targetId})`;
353
+ if (row.type !== "background_agent") {
354
+ return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to }, isError: true };
355
+ }
356
+ 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 {
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 },
383
+ isError: true,
384
+ };
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
+ };
391
+ }
392
+ if (row.status === "killed") {
393
+ const by = opts.registry.getStopAttribution(targetId);
394
+ return {
395
+ 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.`,
396
+ details: { error: "killed", to, ...(by !== undefined ? { stoppedBy: by } : {}) },
397
+ isError: true,
398
+ };
399
+ }
400
+ const runLedger = ctx.subagentRetain ?? opts.retain;
401
+ const smLedgerSessionId = ctx.sessionId ?? opts.sessionId;
402
+ const sessionLedger = smLedgerSessionId !== undefined ? getSessionRetainLedger(smLedgerSessionId) : undefined;
403
+ const knows = (l) => l !== undefined && row.toolUseId !== undefined && (l.get(row.toolUseId) !== undefined || l.wasEvicted(row.toolUseId));
404
+ const siblingLedger = opts.siblingRetain;
405
+ const liveRetainEntry = row.toolUseId !== undefined ? (sessionLedger?.get(row.toolUseId) ?? runLedger?.get(row.toolUseId) ?? siblingLedger?.get(row.toolUseId)) : undefined;
406
+ if (liveRetainEntry === undefined) {
407
+ const revived = await tier3Revive(targetId);
408
+ if (revived !== undefined)
409
+ return revived;
410
+ }
411
+ const ledger = (knows(sessionLedger) ? sessionLedger : undefined) ?? (knows(runLedger) ? runLedger : undefined) ?? (knows(siblingLedger) ? siblingLedger : undefined) ?? runLedger ?? sessionLedger ?? siblingLedger;
412
+ if (!ledger || row.toolUseId === undefined) {
413
+ return {
414
+ 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
+ details: { error: "not_retained", to },
416
+ isError: true,
417
+ };
418
+ }
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 } : {}),
439
+ });
440
+ const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
441
+ const fromPrefix = ctx.parentTaskId !== undefined ? `(message from teammate "${opts.senderName ?? senderId ?? "unknown"}")\n` : "";
442
+ try {
443
+ const marker = await resume(`${fromPrefix}${summary ? `[${summary}] ${message}` : message}`);
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
+ },
468
+ });
469
+ }
@@ -1,9 +1,14 @@
1
- import { Type } from "typebox";
2
1
  import type { Runner } from "../core/runner/runtask.js";
3
- import type { AgentDefinition, Model, ModelRef, TaskSpec, ToolSpec } from "../core/types.js";
2
+ import type { AgentDefinition, Model, ModelRef, ToolSpec } from "../core/types.js";
4
3
  import { type ExecutionEnv } from "../internal/harness.js";
5
4
  import type { RunInternals } from "../core/runner/prepare-task.js";
6
5
  import type { TaskNotificationPayload } from "../core/task-notification.js";
6
+ import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
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";
7
12
  export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
8
13
  export declare function notifyResultField(result: string | undefined): string | undefined;
9
14
  export declare function inheritedManifestScopeFor(snapshot: readonly unknown[] | undefined): RunInternals["inheritedManifestScope"];
@@ -23,7 +28,6 @@ export interface SubagentToolStats {
23
28
  }
24
29
  export declare const FORK_SUBAGENT_TYPE = "fork";
25
30
  export declare const GENERAL_PURPOSE_SUBAGENT_TYPE = "general-purpose";
26
- export declare const FORK_DEFAULT_MAX_TURNS = 200;
27
31
  export declare function forkGovernanceDenial(enableFork: boolean | undefined, allowFork: boolean | undefined): "task" | "principal" | undefined;
28
32
  export interface SubagentWorktreeIsolation {
29
33
  mint(childId: string): Promise<{
@@ -32,7 +36,7 @@ export interface SubagentWorktreeIsolation {
32
36
  finish(worktreeDir: string): Promise<"pruned" | "kept">;
33
37
  }
34
38
  export declare function createSubagentWorktreeHelper(baseEnv: ExecutionEnv, repoRoot: string): SubagentWorktreeIsolation;
35
- export declare const SESSION_BG_DEFAULT_TIMEOUT_SEC: number;
39
+ export declare const REPORT_FIELD_MAX = 300;
36
40
  export type SubagentErrorKind = "rate_limit" | "overloaded" | "timeout" | "network" | "logic";
37
41
  export declare function classifySubagentError(child: {
38
42
  status: string;
@@ -50,60 +54,31 @@ export interface SubagentSteerHandle {
50
54
  childSessionId?: string;
51
55
  resume?: (content: string) => Promise<string>;
52
56
  }
53
- export declare const SUBAGENT_RESUME_CAP = 8;
54
- export declare const RETAIN_DEFAULT_TTL_MS: number;
55
- export declare const RETAIN_DEFAULT_MAX = 16;
56
- export interface SubagentRetainEntry {
57
- childSessionId: string;
58
- runner?: Runner;
59
- agentName?: string;
60
- ttlMs?: number;
61
- specSnapshot: Readonly<TaskSpec>;
62
- internalsSnapshot: RunInternals;
63
- running: boolean;
64
- settled: boolean;
65
- settledAt: number;
66
- resumeCount: number;
67
- cycleSeq: number;
68
- deferredNotify?: {
69
- seq?: number;
70
- cancel: () => void;
71
- flush: () => void;
72
- };
73
- activeAbort?: AbortController;
74
- release: () => Promise<void>;
75
- }
76
- export declare class SubagentRetainLedger {
77
- readonly ttlMs: number;
78
- readonly max: number;
79
- private entries;
80
- private evictedIds;
81
- private ttlTimers;
82
- private isDisposed;
83
- constructor(config: true | {
84
- ttlMs?: number;
85
- max?: number;
86
- });
87
- private static readonly TOMBSTONES_MAX;
88
- private tombstone;
89
- private effectiveTtlMs;
90
- get disposed(): boolean;
91
- get size(): number;
92
- get(parentToolCallId: string): SubagentRetainEntry | undefined;
93
- wasEvicted(parentToolCallId: string): boolean;
94
- sweepExpired(now?: number): void;
95
- register(parentToolCallId: string, entry: Omit<SubagentRetainEntry, "running" | "settled" | "settledAt" | "resumeCount" | "cycleSeq" | "activeAbort" | "deferredNotify">): SubagentRetainEntry | undefined;
96
- markSettled(parentToolCallId: string): void;
97
- private armTtlTimer;
98
- private clearTtlTimer;
99
- private flushDeferredNotify;
100
- abandon(parentToolCallId: string): void;
101
- evict(parentToolCallId: string): Promise<void>;
102
- disposeAll(): Promise<void>;
103
- }
104
- export declare function getSessionRetainLedger(sessionId: string): SubagentRetainLedger | undefined;
105
- export declare function releaseSessionRetainLedger(sessionId: string): Promise<void>;
106
- export declare function makeResumePrompt(marker: string, content: string): string;
57
+ export declare function createSubagentResume(deps: {
58
+ ledger: SubagentRetainLedger | undefined;
59
+ parentToolCallId: string;
60
+ runner: Runner;
61
+ notify?: (n: TaskNotificationPayload, opts?: {
62
+ priority?: "now" | "next" | "later";
63
+ }) => void;
64
+ sink?: (handle: SubagentSteerHandle) => void;
65
+ registry?: import("../core/task-registry.js").TaskRegistry;
66
+ currentParentNotify?: (n: TaskNotificationPayload, opts?: {
67
+ priority?: "now" | "next" | "later";
68
+ }) => void;
69
+ taskId?: string;
70
+ taskAccess?: import("../core/task-registry.js").TaskAccess;
71
+ bgSink?: (event: import("../core/types.js").BackgroundChildEvent) => void;
72
+ sessionScoped?: boolean;
73
+ rowDescription?: string;
74
+ rowName?: string;
75
+ rowAgentType?: string;
76
+ rowOwner?: string;
77
+ rowScope?: string;
78
+ rowParentTaskId?: string;
79
+ rowParentSessionId?: string;
80
+ rowRootSessionId?: string;
81
+ }): (content: string) => Promise<string>;
107
82
  export interface SubagentToolOptions {
108
83
  runner: Runner;
109
84
  background?: {
@@ -163,53 +138,3 @@ export declare function agentWhenToUseText(def: {
163
138
  whenToUse?: string;
164
139
  whenToUseLean?: string;
165
140
  }, lean?: boolean): string | undefined;
166
- export declare const SEND_MESSAGE_TOOL_NAME = "SendMessage";
167
- export interface SendMessageToolOptions {
168
- runner: Runner;
169
- registry: import("../core/task-registry.js").TaskRegistry;
170
- retain?: SubagentRetainLedger;
171
- owner?: string;
172
- scope?: string;
173
- sessionId?: string;
174
- notify?: (n: TaskNotificationPayload, opts?: {
175
- priority?: "now" | "next" | "later";
176
- }) => void;
177
- sink?: (handle: SubagentSteerHandle) => void;
178
- uplink?: (n: TaskNotificationPayload, opts?: {
179
- priority?: "now" | "next" | "later";
180
- }) => void;
181
- senderName?: string;
182
- roster?: import("./roster-store.js").RosterStore;
183
- siblingRetain?: SubagentRetainLedger;
184
- onBackgroundChildEvent?: (event: import("../core/types.js").BackgroundChildEvent) => void;
185
- agentStore?: import("../core/background-agent-store.js").BackgroundAgentStore;
186
- mailbox?: import("../core/mailbox-store.js").MailboxStore;
187
- reviveSpawn?: (req: {
188
- row: import("../core/background-agent-store.js").BackgroundAgentRecord;
189
- rev: number;
190
- prompt: string;
191
- }) => Promise<{
192
- isError?: boolean;
193
- content: string;
194
- details?: unknown;
195
- }>;
196
- }
197
- export declare function recordRosterSpawn(roster: import("./roster-store.js").RosterStore | undefined, entry: import("./roster-store.js").RosterEntry): void;
198
- export declare function createSendMessageTool(opts: SendMessageToolOptions): import("../core/types.js").AgentTool<Type.TObject<{
199
- to: Type.TString;
200
- message: Type.TString;
201
- summary: Type.TOptional<Type.TString>;
202
- }>, unknown>;
203
- export declare const AGENT_TRANSCRIPT_TOOL_NAME = "AgentTranscript";
204
- export interface AgentTranscriptToolOptions {
205
- runner: Runner;
206
- registry: import("../core/task-registry.js").TaskRegistry;
207
- agentStore?: import("../core/background-agent-store.js").BackgroundAgentStore;
208
- owner?: string;
209
- scope?: string;
210
- sessionId?: string;
211
- }
212
- export declare function createAgentTranscriptTool(opts: AgentTranscriptToolOptions): import("../core/types.js").AgentTool<Type.TObject<{
213
- id: Type.TString;
214
- lastN: Type.TOptional<Type.TInteger>;
215
- }>, unknown>;