pilotswarm-sdk 0.5.76 → 0.5.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capability-catalog.d.ts +13 -0
- package/dist/capability-catalog.d.ts.map +1 -1
- package/dist/capability-catalog.js +43 -4
- package/dist/capability-catalog.js.map +1 -1
- package/dist/capability-runtime.d.ts +3 -3
- package/dist/capability-runtime.js +3 -3
- package/dist/capability-runtime.js.map +1 -1
- package/dist/managed-session.js +4 -4
- package/dist/managed-session.js.map +1 -1
- package/dist/orchestration/agents.d.ts.map +1 -1
- package/dist/orchestration/agents.js +4 -0
- package/dist/orchestration/agents.js.map +1 -1
- package/dist/orchestration/index.d.ts +1 -1
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +4 -2
- package/dist/orchestration-registry.js.map +1 -1
- package/dist/orchestration-version.d.ts +1 -1
- package/dist/orchestration-version.js +1 -1
- package/dist/orchestration.d.ts +2 -2
- package/dist/orchestration.js +1 -1
- package/dist/orchestration_1_0_46.d.ts +1 -1
- package/dist/orchestration_1_0_47.d.ts +1 -1
- package/dist/orchestration_1_0_48.d.ts +1 -1
- package/dist/orchestration_1_0_49.d.ts +1 -1
- package/dist/orchestration_1_0_50.d.ts +1 -1
- package/dist/orchestration_1_0_78/agents.d.ts +66 -0
- package/dist/orchestration_1_0_78/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/agents.js +894 -0
- package/dist/orchestration_1_0_78/agents.js.map +1 -0
- package/dist/orchestration_1_0_78/index.d.ts +24 -0
- package/dist/orchestration_1_0_78/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/index.js +13 -0
- package/dist/orchestration_1_0_78/index.js.map +1 -0
- package/dist/orchestration_1_0_78/lifecycle.d.ts +51 -0
- package/dist/orchestration_1_0_78/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/lifecycle.js +981 -0
- package/dist/orchestration_1_0_78/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_78/queue.d.ts +20 -0
- package/dist/orchestration_1_0_78/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/queue.js +894 -0
- package/dist/orchestration_1_0_78/queue.js.map +1 -0
- package/dist/orchestration_1_0_78/runtime.d.ts +31 -0
- package/dist/orchestration_1_0_78/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/runtime.js +244 -0
- package/dist/orchestration_1_0_78/runtime.js.map +1 -0
- package/dist/orchestration_1_0_78/state.d.ts +214 -0
- package/dist/orchestration_1_0_78/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/state.js +178 -0
- package/dist/orchestration_1_0_78/state.js.map +1 -0
- package/dist/orchestration_1_0_78/turn.d.ts +34 -0
- package/dist/orchestration_1_0_78/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/turn.js +1433 -0
- package/dist/orchestration_1_0_78/turn.js.map +1 -0
- package/dist/orchestration_1_0_78/utils.d.ts +39 -0
- package/dist/orchestration_1_0_78/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/utils.js +322 -0
- package/dist/orchestration_1_0_78/utils.js.map +1 -0
- package/dist/session-list-timestamps.d.ts +5 -0
- package/dist/session-list-timestamps.d.ts.map +1 -0
- package/dist/session-list-timestamps.js +16 -0
- package/dist/session-list-timestamps.js.map +1 -0
- package/dist/session-manager.d.ts +8 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +79 -7
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +1 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +6 -1
- package/dist/session-proxy.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +21 -2
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/plugins/system/prompts/base-v2.md +7 -3
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import { clearPendingChildDigest, publishStatus, queueFollowup, writeCommandResponse, } from "./lifecycle.js";
|
|
2
|
+
import { MAX_NESTING_LEVEL, MAX_SUB_AGENTS, SHUTDOWN_POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, } from "./state.js";
|
|
3
|
+
// ─── Pure helpers ───────────────────────────────────────────
|
|
4
|
+
export function isSubAgentTerminalStatus(status) {
|
|
5
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
6
|
+
}
|
|
7
|
+
/** Preserve child answers when a status probe only has orchestration exit output. */
|
|
8
|
+
export function getChildResultFromStatus(child, previous) {
|
|
9
|
+
if (typeof child.result !== "string" || !child.result)
|
|
10
|
+
return previous;
|
|
11
|
+
if (child.resultSource === "orchestration")
|
|
12
|
+
return previous;
|
|
13
|
+
return child.result;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A status that satisfies a parent's wait: the child either finished
|
|
17
|
+
* (terminal) or will never speak again on its own — idle after answering, or
|
|
18
|
+
* blocked asking for input. "running" and "waiting" (child on a timer) stay
|
|
19
|
+
* pending: those children will still act without help. Without the idle
|
|
20
|
+
* mapping a wait_for_agents parent polls an idle child forever (observed
|
|
21
|
+
* live: 70+ minutes of 30s polls until a human poked).
|
|
22
|
+
*/
|
|
23
|
+
export function isAgentWaitSettledStatus(status) {
|
|
24
|
+
return isSubAgentTerminalStatus(status) || status === "idle" || status === "input_required";
|
|
25
|
+
}
|
|
26
|
+
export function parseChildUpdate(promptText) {
|
|
27
|
+
if (typeof promptText !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
const match = promptText.match(/^\[CHILD_UPDATE\s+([^\]]+)\]/);
|
|
30
|
+
if (!match)
|
|
31
|
+
return null;
|
|
32
|
+
const fields = new Map();
|
|
33
|
+
for (const token of match[1].split(/\s+/)) {
|
|
34
|
+
const [key, ...rest] = token.split("=");
|
|
35
|
+
if (!key || rest.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
fields.set(key, rest.join("="));
|
|
38
|
+
}
|
|
39
|
+
const cycle = fields.get("cycle");
|
|
40
|
+
const status = fields.get("status");
|
|
41
|
+
const verdictField = fields.get("verdict");
|
|
42
|
+
const sessionId = fields.get("from") ?? "";
|
|
43
|
+
const updateType = fields.get("type") ?? "";
|
|
44
|
+
if (!sessionId || !updateType)
|
|
45
|
+
return null;
|
|
46
|
+
const cycleOrigin = cycle === "cron" || cycle === "cron_at" ? cycle : undefined;
|
|
47
|
+
const cycleStatus = status === "quiet" || status === "material" || status === "blocked" ? status : undefined;
|
|
48
|
+
const verdict = verdictField === "success" || verdictField === "partial" || verdictField === "blocked" || verdictField === "failed" || verdictField === "cancelled" || verdictField === "timed_out"
|
|
49
|
+
? verdictField
|
|
50
|
+
: undefined;
|
|
51
|
+
return {
|
|
52
|
+
sessionId,
|
|
53
|
+
updateType,
|
|
54
|
+
content: promptText.split("\n").slice(1).join("\n").trim(),
|
|
55
|
+
...(cycleOrigin ? { cycleOrigin } : {}),
|
|
56
|
+
...(cycleStatus ? { cycleStatus } : {}),
|
|
57
|
+
...(verdict ? { verdict } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function defaultShutdownReason(mode) {
|
|
61
|
+
switch (mode) {
|
|
62
|
+
case "done":
|
|
63
|
+
return "Completed by user";
|
|
64
|
+
case "cancel":
|
|
65
|
+
return "Cancelled by user";
|
|
66
|
+
case "delete":
|
|
67
|
+
return "Deleted by user";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function buildShutdownWaitReason(shutdown) {
|
|
71
|
+
switch (shutdown.mode) {
|
|
72
|
+
case "done":
|
|
73
|
+
return `Waiting for ${shutdown.targetAgentIds.length} child session(s) to complete before closing.`;
|
|
74
|
+
case "cancel":
|
|
75
|
+
return `Waiting for ${shutdown.targetAgentIds.length} child session(s) to cancel before closing.`;
|
|
76
|
+
case "delete":
|
|
77
|
+
return `Waiting for ${shutdown.targetAgentIds.length} child session(s) to cancel before deletion.`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function findTrackedAgentByOrchId(subAgents, orchId) {
|
|
81
|
+
return subAgents.find((agent) => agent.orchId === orchId);
|
|
82
|
+
}
|
|
83
|
+
export function areTrackedAgentsTerminal(subAgents, agentIds) {
|
|
84
|
+
return agentIds.every((agentId) => {
|
|
85
|
+
const agent = findTrackedAgentByOrchId(subAgents, agentId);
|
|
86
|
+
return Boolean(agent && isSubAgentTerminalStatus(agent.status));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function getStillRunningAgentIds(subAgents, agentIds) {
|
|
90
|
+
return agentIds.filter((agentId) => {
|
|
91
|
+
const agent = findTrackedAgentByOrchId(subAgents, agentId);
|
|
92
|
+
return agent && !isSubAgentTerminalStatus(agent.status);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function buildWaitForAgentsFollowup(subAgents, targetIds) {
|
|
96
|
+
const describeStatus = (agent) => {
|
|
97
|
+
if (agent.status === "idle") {
|
|
98
|
+
return "idle — went quiet after answering; treat its last message as its result (message_agent to re-task it, complete_agent to finish it)";
|
|
99
|
+
}
|
|
100
|
+
if (agent.status === "input_required") {
|
|
101
|
+
return "input_required — blocked waiting for an answer; relay the question to the user or answer it via message_agent";
|
|
102
|
+
}
|
|
103
|
+
return agent.status;
|
|
104
|
+
};
|
|
105
|
+
const summaries = targetIds
|
|
106
|
+
.map((targetId) => subAgents.find((agent) => agent.orchId === targetId))
|
|
107
|
+
.filter((agent) => Boolean(agent))
|
|
108
|
+
.map((agent) => ` - Agent ${agent.orchId}\n` +
|
|
109
|
+
` Task: "${agent.task.slice(0, 120)}"\n` +
|
|
110
|
+
` Status: ${describeStatus(agent)}\n` +
|
|
111
|
+
` Result: ${agent.result ?? "(no result)"}`);
|
|
112
|
+
if (summaries.length === 0) {
|
|
113
|
+
return `[SYSTEM: No tracked sub-agents produced a completion summary.]`;
|
|
114
|
+
}
|
|
115
|
+
if (summaries.length === 1) {
|
|
116
|
+
return `[SYSTEM: Sub-agent completed. If the user asked you to relay the child's final output, return the single sub-agent Result text verbatim.\n${summaries[0]}]`;
|
|
117
|
+
}
|
|
118
|
+
return `[SYSTEM: Sub-agents completed:\n${summaries.join("\n")}]`;
|
|
119
|
+
}
|
|
120
|
+
export function buildSubAgentSystemMessage(options) {
|
|
121
|
+
const { parentSessionId, childNestingLevel, maxNestingLevel, agentTask, agentIsSystem, parentSystemMessage, } = options;
|
|
122
|
+
const canSpawnMore = childNestingLevel < maxNestingLevel;
|
|
123
|
+
const timingInstruction = agentIsSystem
|
|
124
|
+
? `- For recurring or periodic work, use the \`cron\` or \`cron_at\` tool instead of ending every cycle with \`wait\`. ` +
|
|
125
|
+
`Call \`cron(seconds=<N>, reason="...")\` to start or update the durable recurring schedule, ` +
|
|
126
|
+
`or \`cron_at(minute=<M>, hour=<H>, tz="...", reason="...")\` for wall-clock schedules. ` +
|
|
127
|
+
`then finish turns normally so the orchestration wakes you automatically on each cron cycle. ` +
|
|
128
|
+
`Use \`wait\` only for one-shot delays inside a turn. ` +
|
|
129
|
+
`Call \`cron(action="cancel")\` or \`cron_at(action="cancel")\` only when you intentionally want to stop the recurring loop.\n`
|
|
130
|
+
: `- For ANY waiting, sleeping, delaying, or scheduling, you MUST use the \`wait\`, \`wait_on_worker\`, \`cron\`, or \`cron_at\` tools. ` +
|
|
131
|
+
`Use \`wait\` or \`wait_on_worker\` for one-shot delays. Use \`cron\` for fixed intervals and \`cron_at\` for wall-clock schedules. ` +
|
|
132
|
+
`Do NOT burn tokens polling inside one LLM turn; after a brief immediate re-check at most, yield with a durable timer. ` +
|
|
133
|
+
`NEVER use setTimeout, sleep, setInterval, or any other timing mechanism. ` +
|
|
134
|
+
`Durable waits survive process restarts.\n`;
|
|
135
|
+
const subAgentPreamble = `[SUB-AGENT CONTEXT]\n` +
|
|
136
|
+
`You are a sub-agent spawned by a parent session (ID: session-${parentSessionId}).\n` +
|
|
137
|
+
`Your nesting level: ${childNestingLevel} (max: ${maxNestingLevel}).\n` +
|
|
138
|
+
`Your task: "${agentTask.slice(0, 500)}"\n\n` +
|
|
139
|
+
`Instructions:\n` +
|
|
140
|
+
`- Focus exclusively on your assigned task.\n` +
|
|
141
|
+
`- Your final response will be automatically forwarded to the parent agent.\n` +
|
|
142
|
+
`- Be thorough but concise — the parent will synthesize results from multiple agents.\n` +
|
|
143
|
+
`- Do NOT ask the user for input — you are autonomous.\n` +
|
|
144
|
+
`- You are autonomous and goal-driven. If the task implies ongoing monitoring or follow-through until done, keep yourself alive with durable timers until the goal is complete or you can no longer make progress.\n` +
|
|
145
|
+
`- If it is ambiguous whether the task should become a long-running recurring workflow, report that ambiguity back to the parent instead of guessing or asking the user directly.\n` +
|
|
146
|
+
`- When your task is complete, provide a clear summary of your findings/results. Your final assistant message is automatically forwarded to the parent.\n` +
|
|
147
|
+
`- After you finish a task you stay ALIVE and idle, ready for the parent to send you a follow-up via \`message_agent\`. You are NOT auto-terminated when you produce a final answer.\n` +
|
|
148
|
+
`- Only the parent decides when you are no longer needed. The parent will close you with \`complete_agent\`, \`cancel_agent\`, or \`delete_agent\`. Do not assume you have been shut down just because you produced a final reply.\n` +
|
|
149
|
+
`- Prefer using \`store_fact\` for larger structured context handoffs across your spawn tree. Put the durable details in facts, then pass fact keys or \`read_facts\` pointers in messages/prompts instead of pasting large context blobs. Sibling and cousin agents under the same root can read your session-scoped facts directly via \`read_facts\` — you do NOT need to mark them \`shared=true\` just to share with peers.\n` +
|
|
150
|
+
`- FILESYSTEM ISOLATION: your parent, siblings, and sub-agents each run on their own worker pod — they can NEVER see your local files, and yours may vanish on the next turn, after a durable wait, or on worker restart. The artifact store is the ONLY shared byte channel. To hand off any file (especially binaries/archives): \`write_artifact({fromFile: "<path>"})\` on the producing side, \`read_artifact({toFile: "<path>"})\` on the consuming side. Bytes move server-side with a SHA-256 in every result — never read a file just to re-type its bytes as inline content, and never base64 payloads through messages.\n` +
|
|
151
|
+
`- \`write_artifact\` returns the artifact:// link — include it in your response whenever the user or your parent should see the file. Use \`store_fact\` for structured state, artifacts for files.\n` +
|
|
152
|
+
`- If you override a sub-agent model, you MUST first call list_available_models in this session and use only an exact provider:model value returned there. ` +
|
|
153
|
+
`NEVER invent, guess, shorten, or reuse a stale model name.\n` +
|
|
154
|
+
`- Worker-managed system agents are not valid spawn targets. If you expect one and it is missing, report that the workers likely need to be restarted.\n` +
|
|
155
|
+
timingInstruction +
|
|
156
|
+
(canSpawnMore
|
|
157
|
+
? `- If your parent task explicitly asks you to spawn sub-agents, delegate, fan out, or parallelize work, you SHOULD do so within runtime limits instead of collapsing the task into a direct answer. ` +
|
|
158
|
+
`If delegation was not explicitly requested, use your judgment and avoid unnecessary fan-out. ` +
|
|
159
|
+
`You have ${maxNestingLevel - childNestingLevel} level(s) of nesting remaining. ` +
|
|
160
|
+
`After spawning, finish the turn normally and let qualifying child updates wake you according to contract.wakeOn. ` +
|
|
161
|
+
`Do not schedule wait or cron solely to poll check_agents; use wait_for_agents only when you need an explicit synchronization barrier.\n`
|
|
162
|
+
: `- You CANNOT spawn sub-agents — you are at the maximum nesting depth. Handle everything directly.\n`);
|
|
163
|
+
return subAgentPreamble + (parentSystemMessage ? "\n\n" + parentSystemMessage : "");
|
|
164
|
+
}
|
|
165
|
+
// ─── Child agent tracking ───────────────────────────────────
|
|
166
|
+
export function* applyChildUpdate(runtime, update) {
|
|
167
|
+
runtime.ctx.traceInfo(`[orch] child update from=${update.sessionId} type=${update.updateType}`);
|
|
168
|
+
const agent = runtime.state.subAgents.find(a => a.sessionId === update.sessionId);
|
|
169
|
+
if (!agent) {
|
|
170
|
+
runtime.ctx.traceInfo(`[orch] ignoring child update from untracked session ${update.sessionId}`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (update.content) {
|
|
174
|
+
agent.result = update.content.slice(0, 2000);
|
|
175
|
+
}
|
|
176
|
+
// Any substantive update from the child satisfies the spawn-time
|
|
177
|
+
// report expectation.
|
|
178
|
+
agent.expectsReport = false;
|
|
179
|
+
if (update.updateType === "completed") {
|
|
180
|
+
agent.status = "completed";
|
|
181
|
+
}
|
|
182
|
+
else if (update.updateType === "cancelled" || update.updateType === "deleted") {
|
|
183
|
+
agent.status = "cancelled";
|
|
184
|
+
}
|
|
185
|
+
else if (update.updateType === "failed") {
|
|
186
|
+
agent.status = "failed";
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const rawStatus = yield runtime.manager.getSessionStatus(agent.sessionId);
|
|
190
|
+
const parsed = JSON.parse(rawStatus);
|
|
191
|
+
if (parsed.status === "failed") {
|
|
192
|
+
agent.status = "failed";
|
|
193
|
+
}
|
|
194
|
+
else if (parsed.status === "completed") {
|
|
195
|
+
agent.status = "completed";
|
|
196
|
+
}
|
|
197
|
+
else if (parsed.status === "cancelled") {
|
|
198
|
+
agent.status = "cancelled";
|
|
199
|
+
}
|
|
200
|
+
else if (parsed.status === "waiting" && update.updateType !== "completed") {
|
|
201
|
+
// A "completed" update means the child turn ended with a final
|
|
202
|
+
// answer; a concurrent "waiting" probe at that moment is almost
|
|
203
|
+
// always the auto-resumed remainder of a wait timer the parent's
|
|
204
|
+
// own message interrupted. Downgrading on it deadlocks
|
|
205
|
+
// wait_for_agents: the idle child may never speak again, and the
|
|
206
|
+
// fallback poll cannot re-derive "completed" from an idle probe.
|
|
207
|
+
// Deliberate continuation waits arrive as updateType "wait" and
|
|
208
|
+
// still downgrade here.
|
|
209
|
+
agent.status = "waiting";
|
|
210
|
+
}
|
|
211
|
+
else if (parsed.status === "idle" && !isSubAgentTerminalStatus(agent.status) && update.updateType !== "completed") {
|
|
212
|
+
// Quiescent child: answered and parked. Not terminal (it can be
|
|
213
|
+
// re-tasked), but it satisfies a parent wait.
|
|
214
|
+
agent.status = "idle";
|
|
215
|
+
}
|
|
216
|
+
else if (parsed.status === "input_required" && !isSubAgentTerminalStatus(agent.status)) {
|
|
217
|
+
// Blocked asking for input — it will never act unprompted; the
|
|
218
|
+
// parent must be told so it can answer or re-task.
|
|
219
|
+
agent.status = "input_required";
|
|
220
|
+
}
|
|
221
|
+
agent.result = getChildResultFromStatus(parsed, agent.result)?.slice(0, 2000);
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
export function* refreshTrackedSubAgents(runtime, options = {}) {
|
|
227
|
+
const preserveTerminalTaskStatus = options.preserveTerminalTaskStatus !== false;
|
|
228
|
+
try {
|
|
229
|
+
const rawChildren = yield runtime.manager.listChildSessions(runtime.input.sessionId);
|
|
230
|
+
const directChildren = JSON.parse(rawChildren);
|
|
231
|
+
runtime.state.subAgents = directChildren
|
|
232
|
+
.filter(child => !child.isSystem)
|
|
233
|
+
.map((child) => {
|
|
234
|
+
const existing = runtime.state.subAgents.find(agent => agent.sessionId === child.sessionId || agent.orchId === child.orchId);
|
|
235
|
+
const contract = child.contract ?? existing?.contract;
|
|
236
|
+
const localStatus = existing?.status;
|
|
237
|
+
if (preserveTerminalTaskStatus && localStatus && isSubAgentTerminalStatus(localStatus)) {
|
|
238
|
+
return {
|
|
239
|
+
orchId: child.orchId,
|
|
240
|
+
sessionId: child.sessionId,
|
|
241
|
+
task: existing?.task ?? child.title ?? "(spawned sub-agent)",
|
|
242
|
+
status: localStatus,
|
|
243
|
+
result: getChildResultFromStatus(child, existing?.result),
|
|
244
|
+
agentId: child.agentId ?? existing?.agentId,
|
|
245
|
+
contract,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const rawStatus = child.status ?? localStatus ?? "running";
|
|
249
|
+
const normalizedStatus = rawStatus === "failed" ? "failed"
|
|
250
|
+
: rawStatus === "cancelled" ? "cancelled"
|
|
251
|
+
: rawStatus === "waiting" ? "waiting"
|
|
252
|
+
: rawStatus === "completed" ? "completed"
|
|
253
|
+
: "running";
|
|
254
|
+
return {
|
|
255
|
+
orchId: child.orchId,
|
|
256
|
+
sessionId: child.sessionId,
|
|
257
|
+
task: existing?.task ?? child.title ?? "(spawned sub-agent)",
|
|
258
|
+
status: normalizedStatus,
|
|
259
|
+
result: getChildResultFromStatus(child, existing?.result),
|
|
260
|
+
agentId: child.agentId ?? existing?.agentId,
|
|
261
|
+
contract,
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
runtime.ctx.traceInfo(`[orch] refreshTrackedSubAgents failed (non-fatal): ${err.message ?? err}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ─── Graceful shutdown cascade ──────────────────────────────
|
|
270
|
+
export function* notifyParentOfTerminalState(runtime, updateType, reason, commandId, requestedBy) {
|
|
271
|
+
if (!runtime.options.parentSessionId)
|
|
272
|
+
return;
|
|
273
|
+
try {
|
|
274
|
+
// Acknowledge the parent's own cleanup without delivering another
|
|
275
|
+
// CHILD_UPDATE prompt. That prompt replaced real child results in a
|
|
276
|
+
// digest and woke an already-finished parent for no new work. Keep the
|
|
277
|
+
// lifecycle in CMS and an audit event; check_agents/wait_for_agents and
|
|
278
|
+
// graceful-shutdown polling still observe the actual terminal state.
|
|
279
|
+
// Only runtime-stamped provenance matching the direct parent qualifies.
|
|
280
|
+
// A user-supplied reason such as "Completed by parent" is not evidence.
|
|
281
|
+
if (commandId && requestedBy === runtime.options.parentSessionId) {
|
|
282
|
+
yield runtime.manager.recordSessionEvent(runtime.options.parentSessionId, [{
|
|
283
|
+
eventType: "session.child_cleanup_completed",
|
|
284
|
+
data: {
|
|
285
|
+
childSessionId: runtime.input.sessionId,
|
|
286
|
+
commandId,
|
|
287
|
+
requestedBy,
|
|
288
|
+
status: updateType,
|
|
289
|
+
},
|
|
290
|
+
}]);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const verdict = updateType === "completed" ? "success" : "cancelled";
|
|
294
|
+
yield runtime.manager.sendToSession(runtime.options.parentSessionId, `[CHILD_UPDATE from=${runtime.input.sessionId} type=${updateType} iter=${runtime.state.iteration} verdict=${verdict}]\n${reason}`);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
runtime.ctx.traceInfo(`[orch] parent notification on ${updateType} failed: ${err.message} (non-fatal)`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export function* completeSession(runtime, reason, commandId, requestedBy) {
|
|
301
|
+
runtime.state.pendingShutdown = null;
|
|
302
|
+
runtime.state.waitingForAgentIds = null;
|
|
303
|
+
clearPendingChildDigest(runtime);
|
|
304
|
+
runtime.state.activeTimer = null;
|
|
305
|
+
yield runtime.manager.updateCmsState(runtime.input.sessionId, "completed", null, null);
|
|
306
|
+
publishStatus(runtime, "completed");
|
|
307
|
+
yield* notifyParentOfTerminalState(runtime, "completed", reason, commandId, requestedBy);
|
|
308
|
+
try {
|
|
309
|
+
yield runtime.session.destroy();
|
|
310
|
+
}
|
|
311
|
+
catch { }
|
|
312
|
+
if (commandId) {
|
|
313
|
+
const resp = {
|
|
314
|
+
id: commandId,
|
|
315
|
+
cmd: "done",
|
|
316
|
+
result: { ok: true, message: "Session completed" },
|
|
317
|
+
};
|
|
318
|
+
yield* writeCommandResponse(runtime, resp);
|
|
319
|
+
}
|
|
320
|
+
runtime.state.orchestrationResult = "done";
|
|
321
|
+
}
|
|
322
|
+
export function* cancelSession(runtime, reason, commandId, deleteAfterCancel = false, requestedBy) {
|
|
323
|
+
runtime.state.pendingShutdown = null;
|
|
324
|
+
runtime.state.waitingForAgentIds = null;
|
|
325
|
+
clearPendingChildDigest(runtime);
|
|
326
|
+
runtime.state.activeTimer = null;
|
|
327
|
+
const commandName = deleteAfterCancel ? "delete" : "cancel";
|
|
328
|
+
if (!deleteAfterCancel) {
|
|
329
|
+
yield runtime.manager.updateCmsState(runtime.input.sessionId, "cancelled", null, null);
|
|
330
|
+
publishStatus(runtime, "cancelled");
|
|
331
|
+
}
|
|
332
|
+
yield* notifyParentOfTerminalState(runtime, "cancelled", reason, commandId, requestedBy);
|
|
333
|
+
try {
|
|
334
|
+
yield runtime.session.destroy();
|
|
335
|
+
}
|
|
336
|
+
catch { }
|
|
337
|
+
if (commandId) {
|
|
338
|
+
const resp = {
|
|
339
|
+
id: commandId,
|
|
340
|
+
cmd: commandName,
|
|
341
|
+
result: {
|
|
342
|
+
ok: true,
|
|
343
|
+
message: deleteAfterCancel ? "Session deleted" : "Session cancelled",
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
yield* writeCommandResponse(runtime, resp);
|
|
347
|
+
}
|
|
348
|
+
if (deleteAfterCancel) {
|
|
349
|
+
const deleteReason = reason || "Deleted by user";
|
|
350
|
+
let descendants = [];
|
|
351
|
+
try {
|
|
352
|
+
descendants = yield runtime.manager.getDescendantSessionIds(runtime.input.sessionId);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
runtime.ctx.traceInfo(`[orch] delete: failed to enumerate descendants: ${err.message}`);
|
|
356
|
+
}
|
|
357
|
+
for (const descendantId of descendants) {
|
|
358
|
+
try {
|
|
359
|
+
yield runtime.manager.deleteSession(descendantId, `Ancestor ${runtime.input.sessionId} deleted: ${deleteReason}`);
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
runtime.ctx.traceInfo(`[orch] delete: failed to delete descendant ${descendantId}: ${err.message} (non-fatal)`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
yield runtime.manager.deleteSession(runtime.input.sessionId, deleteReason);
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
runtime.ctx.traceInfo(`[orch] delete: failed to delete ${runtime.input.sessionId}: ${err.message}`);
|
|
370
|
+
}
|
|
371
|
+
runtime.state.orchestrationResult = "deleted";
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
runtime.state.orchestrationResult = "cancelled";
|
|
375
|
+
}
|
|
376
|
+
export function* failPendingShutdown(runtime, errorMessage) {
|
|
377
|
+
const shutdown = runtime.state.pendingShutdown;
|
|
378
|
+
runtime.state.pendingShutdown = null;
|
|
379
|
+
runtime.state.waitingForAgentIds = null;
|
|
380
|
+
clearPendingChildDigest(runtime);
|
|
381
|
+
runtime.state.activeTimer = null;
|
|
382
|
+
try {
|
|
383
|
+
yield runtime.session.destroy();
|
|
384
|
+
}
|
|
385
|
+
catch { }
|
|
386
|
+
if (shutdown?.commandId) {
|
|
387
|
+
const resp = {
|
|
388
|
+
id: shutdown.commandId,
|
|
389
|
+
cmd: shutdown.mode,
|
|
390
|
+
error: errorMessage,
|
|
391
|
+
};
|
|
392
|
+
yield* writeCommandResponse(runtime, resp);
|
|
393
|
+
}
|
|
394
|
+
publishStatus(runtime, "failed", { error: errorMessage });
|
|
395
|
+
yield runtime.manager.updateCmsState(runtime.input.sessionId, "failed", errorMessage, null);
|
|
396
|
+
runtime.state.orchestrationResult = "failed";
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Best-effort cancel of a distiller SERVICE SESSION still running when its
|
|
400
|
+
* regen is torn down (cancel_regen, a pre-flip failure, or the served session
|
|
401
|
+
* shutting down). Without this the distiller finishes its LLM turn then parks
|
|
402
|
+
* idle forever — the sweeper never reclaims a LIVE session, so each abort leaks
|
|
403
|
+
* one (adversarial-review finding). The cancel is a queued cmd (the activity
|
|
404
|
+
* swallows its own errors), so it is safe to yield from any teardown path.
|
|
405
|
+
*/
|
|
406
|
+
export function* cancelInFlightDistiller(runtime) {
|
|
407
|
+
const regen = runtime.state.regen;
|
|
408
|
+
if (regen?.stage === "distilling" && regen.distillerSessionId) {
|
|
409
|
+
yield runtime.manager.runRegenCancelDistiller(regen.distillerSessionId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
export function* finalizePendingShutdown(runtime) {
|
|
413
|
+
if (!runtime.state.pendingShutdown)
|
|
414
|
+
return;
|
|
415
|
+
yield* cancelInFlightDistiller(runtime);
|
|
416
|
+
const shutdown = runtime.state.pendingShutdown;
|
|
417
|
+
if (shutdown.mode === "done") {
|
|
418
|
+
yield* completeSession(runtime, shutdown.reason, shutdown.commandId, shutdown.requestedBy);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
yield* cancelSession(runtime, shutdown.reason, shutdown.commandId, shutdown.mode === "delete", shutdown.requestedBy);
|
|
422
|
+
}
|
|
423
|
+
export function* maybeResolveAgentWaitCompletion(runtime) {
|
|
424
|
+
const { state } = runtime;
|
|
425
|
+
const allSettled = state.waitingForAgentIds?.every((targetId) => {
|
|
426
|
+
const agent = state.subAgents.find((entry) => entry.orchId === targetId);
|
|
427
|
+
return agent ? isAgentWaitSettledStatus(agent.status) : true;
|
|
428
|
+
});
|
|
429
|
+
if (!state.waitingForAgentIds || !allSettled) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (state.pendingShutdown) {
|
|
433
|
+
yield* finalizePendingShutdown(runtime);
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
queueFollowup(runtime, buildWaitForAgentsFollowup(state.subAgents, state.waitingForAgentIds));
|
|
437
|
+
state.waitingForAgentIds = null;
|
|
438
|
+
clearPendingChildDigest(runtime);
|
|
439
|
+
state.activeTimer = null;
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
export function* beginGracefulShutdown(runtime, mode, cmdMsg) {
|
|
443
|
+
const { state } = runtime;
|
|
444
|
+
if (state.pendingShutdown) {
|
|
445
|
+
const now = yield runtime.ctx.utcNow();
|
|
446
|
+
const resp = {
|
|
447
|
+
id: cmdMsg.id,
|
|
448
|
+
cmd: cmdMsg.cmd,
|
|
449
|
+
result: {
|
|
450
|
+
ok: true,
|
|
451
|
+
message: `Shutdown already in progress (${state.pendingShutdown.mode}).`,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
yield* writeCommandResponse(runtime, resp);
|
|
455
|
+
publishStatus(runtime, "waiting", {
|
|
456
|
+
waitReason: buildShutdownWaitReason(state.pendingShutdown),
|
|
457
|
+
waitStartedAt: state.pendingShutdown.startedAtMs,
|
|
458
|
+
waitSeconds: Math.max(0, Math.ceil((state.pendingShutdown.deadlineAtMs - now) / 1000)),
|
|
459
|
+
});
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// Task completion does not make a child session terminal: non-system
|
|
463
|
+
// children stay alive for follow-up messages. During parent shutdown the
|
|
464
|
+
// CMS session lifecycle is authoritative, so an idle child must still
|
|
465
|
+
// receive done/cancel even if its last tracked task status was completed.
|
|
466
|
+
yield* refreshTrackedSubAgents(runtime, { preserveTerminalTaskStatus: false });
|
|
467
|
+
const shutdownReason = String(cmdMsg.args?.reason || defaultShutdownReason(mode));
|
|
468
|
+
const requestedBy = typeof cmdMsg.requestedBy === "string"
|
|
469
|
+
&& cmdMsg.requestedBy === runtime.options.parentSessionId
|
|
470
|
+
&& typeof cmdMsg.id === "string" && cmdMsg.id.trim()
|
|
471
|
+
? cmdMsg.requestedBy : undefined;
|
|
472
|
+
const targetAgents = state.subAgents.filter((agent) => !isSubAgentTerminalStatus(agent.status));
|
|
473
|
+
if (targetAgents.length === 0) {
|
|
474
|
+
if (mode === "done") {
|
|
475
|
+
yield* completeSession(runtime, shutdownReason, cmdMsg.id, requestedBy);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
yield* cancelSession(runtime, shutdownReason, cmdMsg.id, mode === "delete", requestedBy);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const childCmd = mode === "done" ? "done" : "cancel";
|
|
482
|
+
const childReason = mode === "done"
|
|
483
|
+
? "Parent session completing"
|
|
484
|
+
: shutdownReason;
|
|
485
|
+
runtime.ctx.traceInfo(`[orch] ${cmdMsg.cmd}: cascading ${childCmd} to ${targetAgents.length} child session(s)`);
|
|
486
|
+
for (const child of targetAgents) {
|
|
487
|
+
try {
|
|
488
|
+
const childCmdId = `${cmdMsg.cmd}-cascade-${state.iteration}-${child.sessionId.slice(0, 8)}`;
|
|
489
|
+
yield runtime.manager.sendCommandToSession(child.sessionId, { type: "cmd", cmd: childCmd, id: childCmdId, args: { reason: childReason }, requestedBy: runtime.input.sessionId });
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
runtime.ctx.traceInfo(`[orch] ${cmdMsg.cmd}: failed to signal child ${child.sessionId}: ${err.message} (non-fatal)`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const startedAtMs = yield runtime.ctx.utcNow();
|
|
496
|
+
state.pendingShutdown = {
|
|
497
|
+
mode,
|
|
498
|
+
reason: shutdownReason,
|
|
499
|
+
startedAtMs,
|
|
500
|
+
deadlineAtMs: startedAtMs + SHUTDOWN_TIMEOUT_MS,
|
|
501
|
+
targetAgentIds: targetAgents.map((agent) => agent.orchId),
|
|
502
|
+
commandId: cmdMsg.id,
|
|
503
|
+
...(requestedBy ? { requestedBy } : {}),
|
|
504
|
+
};
|
|
505
|
+
state.waitingForAgentIds = [...state.pendingShutdown.targetAgentIds];
|
|
506
|
+
clearPendingChildDigest(runtime);
|
|
507
|
+
state.activeTimer = {
|
|
508
|
+
deadlineMs: startedAtMs + SHUTDOWN_POLL_INTERVAL_MS,
|
|
509
|
+
originalDurationMs: SHUTDOWN_POLL_INTERVAL_MS,
|
|
510
|
+
reason: buildShutdownWaitReason(state.pendingShutdown),
|
|
511
|
+
type: "agent-poll",
|
|
512
|
+
agentIds: state.waitingForAgentIds,
|
|
513
|
+
};
|
|
514
|
+
publishStatus(runtime, "waiting", {
|
|
515
|
+
waitReason: buildShutdownWaitReason(state.pendingShutdown),
|
|
516
|
+
waitStartedAt: startedAtMs,
|
|
517
|
+
waitSeconds: Math.ceil(SHUTDOWN_TIMEOUT_MS / 1000),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
// ─── Sub-agent tool actions (spawn/message/check/wait/etc.) ─
|
|
521
|
+
export function* handleSubAgentAction(runtime, result) {
|
|
522
|
+
const { ctx, state } = runtime;
|
|
523
|
+
switch (result.type) {
|
|
524
|
+
case "spawn_agent": {
|
|
525
|
+
const childNestingLevel = runtime.options.nestingLevel + 1;
|
|
526
|
+
if (childNestingLevel > MAX_NESTING_LEVEL) {
|
|
527
|
+
ctx.traceInfo(`[orch] spawn_agent denied: nesting level ${runtime.options.nestingLevel} is at max (${MAX_NESTING_LEVEL})`);
|
|
528
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — you are already at nesting level ${runtime.options.nestingLevel} (max ${MAX_NESTING_LEVEL}). ` +
|
|
529
|
+
`Sub-agents at this depth cannot spawn further sub-agents. Handle the task directly instead.]`);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
const activeCount = state.subAgents.filter(a => a.status === "running").length;
|
|
533
|
+
if (activeCount >= MAX_SUB_AGENTS) {
|
|
534
|
+
ctx.traceInfo(`[orch] spawn_agent denied: ${activeCount}/${MAX_SUB_AGENTS} agents running`);
|
|
535
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — you already have ${activeCount} running sub-agents (max ${MAX_SUB_AGENTS}). ` +
|
|
536
|
+
`Wait for some to complete before spawning more.]`);
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
let agentTask = result.task;
|
|
540
|
+
let agentSystemMessage = result.systemMessage;
|
|
541
|
+
let agentToolNames = result.toolNames;
|
|
542
|
+
const agentModel = result.model;
|
|
543
|
+
const agentReasoningEffort = result.reasoningEffort;
|
|
544
|
+
let agentIsSystem = false;
|
|
545
|
+
const explicitAgentTitle = typeof result.title === "string" && result.title.trim() ? result.title.trim() : undefined;
|
|
546
|
+
let agentTitle = explicitAgentTitle;
|
|
547
|
+
let agentTitleIsExplicit = Boolean(explicitAgentTitle);
|
|
548
|
+
let agentId;
|
|
549
|
+
let agentSplash;
|
|
550
|
+
let bootstrapRequiredTool;
|
|
551
|
+
let boundAgentName;
|
|
552
|
+
let boundAgentPackageId;
|
|
553
|
+
let promptLayeringKind;
|
|
554
|
+
let resolvedAgentName = result.agentName;
|
|
555
|
+
if (Object.hasOwn(result, "requiredTool") || Object.hasOwn(result, "required_tool")) {
|
|
556
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — required_tool is no longer supported by spawn_agent. Use ps_list_agents to find a suitable named agent, then pass its exact agent_name and your assignment in task.]`);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
const applyAgentDef = (agentDef) => {
|
|
560
|
+
agentTask = result.task || agentDef.initialPrompt || `You are the ${agentDef.name} agent. Begin your work.`;
|
|
561
|
+
agentSystemMessage = undefined;
|
|
562
|
+
// A named child's package owns its complete extra-tool surface.
|
|
563
|
+
// Explicit [] also prevents inheriting a parent's specialist tools.
|
|
564
|
+
agentToolNames = agentDef.tools ?? [];
|
|
565
|
+
agentIsSystem = agentDef.system ?? false;
|
|
566
|
+
if (!agentTitleIsExplicit)
|
|
567
|
+
agentTitle = agentDef.title;
|
|
568
|
+
agentId = agentDef.id ?? agentDef.name;
|
|
569
|
+
agentSplash = agentDef.splash;
|
|
570
|
+
bootstrapRequiredTool = agentDef.initialRequiredTool;
|
|
571
|
+
boundAgentName = !agentDef.packageId && agentDef.namespace ? `${agentDef.namespace}:${agentDef.name}` : agentDef.name;
|
|
572
|
+
boundAgentPackageId = agentDef.packageId;
|
|
573
|
+
promptLayeringKind = agentDef.promptLayerKind
|
|
574
|
+
?? (agentDef.system
|
|
575
|
+
? ((agentDef.namespace || "pilotswarm") === "pilotswarm"
|
|
576
|
+
? "pilotswarm-system-agent"
|
|
577
|
+
: "app-system-agent")
|
|
578
|
+
: "app-agent");
|
|
579
|
+
};
|
|
580
|
+
let agentDef = null;
|
|
581
|
+
if (resolvedAgentName) {
|
|
582
|
+
ctx.traceInfo(`[orch] resolving agent config for: ${resolvedAgentName}`);
|
|
583
|
+
agentDef = yield runtime.manager.resolveAgentConfig(resolvedAgentName, runtime.input.sessionId);
|
|
584
|
+
if (!agentDef) {
|
|
585
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — agent "${resolvedAgentName}" not found. Use ps_list_agents to see available agents.]`);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (agentDef) {
|
|
590
|
+
if (agentDef.system && agentDef.creatable === false) {
|
|
591
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — agent "${resolvedAgentName}" is a worker-managed system agent and cannot be spawned from a session. ` +
|
|
592
|
+
`If it is missing, the workers likely need to be restarted.]`);
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
if (result.toolNames !== undefined) {
|
|
596
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — tool_names cannot override a bound named-agent definition. Use a custom task without agent_name, or remove tool_names.]`);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
if (result.systemMessage !== undefined) {
|
|
600
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — system_message cannot override a bound named-agent definition. Put the bounded assignment in task instead.]`);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
// The named definition owns its startup requirement.
|
|
604
|
+
applyAgentDef(agentDef);
|
|
605
|
+
}
|
|
606
|
+
// NOTE: a system parent does NOT make its children system.
|
|
607
|
+
// agentIsSystem stays purely definition-driven (agentDef.system —
|
|
608
|
+
// the worker-managed agents). Ad-hoc children of system sessions
|
|
609
|
+
// are ordinary deletable sessions that inherit the SYSTEM user as
|
|
610
|
+
// their OWNER instead (resolveEffectiveSpawnOwner in the
|
|
611
|
+
// spawnChildSession activity), which is how they reach the
|
|
612
|
+
// admin-stored System GitHub Copilot key.
|
|
613
|
+
if (agentModel && !agentModel.includes(":")) {
|
|
614
|
+
ctx.traceInfo(`[orch] spawn_agent denied: unqualified model override "${agentModel}"`);
|
|
615
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed — model "${agentModel}" is not allowed. ` +
|
|
616
|
+
`When overriding a sub-agent model, first call list_available_models and then use the exact provider:model value from that list. ` +
|
|
617
|
+
`If you are unsure, omit model so the sub-agent inherits your current model.]`);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
if (!agentTitle && agentIsSystem) {
|
|
621
|
+
const text = agentTask || "";
|
|
622
|
+
const titleMatch = text.match(/You are the \*{0,2}([^*\n]+?)\*{0,2}\s*[—–-]/i)
|
|
623
|
+
|| text.match(/You are the \*{0,2}([^*\n]+?Agent)\*{0,2}/i);
|
|
624
|
+
if (titleMatch) {
|
|
625
|
+
agentTitle = titleMatch[1].trim();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
ctx.traceInfo(`[orch] spawning sub-agent via SDK: task="${agentTask.slice(0, 80)}" model=${agentModel || "inherit"} agent=${resolvedAgentName || "custom"} nestingLevel=${childNestingLevel}`);
|
|
629
|
+
const { boundAgentName: _parentBoundAgentName, boundAgentPackageId: _parentBoundAgentPackageId, boundAgentSource: _parentBoundAgentSource, namedAgentToolAdditions: _parentNamedAgentToolAdditions, childContract: _parentChildContract, detachedPackageToolPolicy: _parentDetachedPackageToolPolicy, promptLayering: _parentPromptLayering, agentIdentity: _parentAgentIdentity, isCrawler: _parentIsCrawler, isHarvester: _parentIsHarvester, ...parentConfig } = state.config;
|
|
630
|
+
const childConfig = {
|
|
631
|
+
...parentConfig,
|
|
632
|
+
// Pass the assignment and sub-agent context to named children,
|
|
633
|
+
// not the parent's custom persona competing with their own.
|
|
634
|
+
...(boundAgentName ? { systemMessage: undefined } : {}),
|
|
635
|
+
...(agentModel ? { model: agentModel } : {}),
|
|
636
|
+
...(agentReasoningEffort ? { reasoningEffort: agentReasoningEffort } : {}),
|
|
637
|
+
...(result.contextTier !== undefined ? { contextTier: result.contextTier } : {}),
|
|
638
|
+
...(agentSystemMessage ? { systemMessage: agentSystemMessage } : {}),
|
|
639
|
+
...(boundAgentName ? { boundAgentName } : {}),
|
|
640
|
+
...(boundAgentPackageId ? { boundAgentPackageId } : {}),
|
|
641
|
+
...(boundAgentName && !boundAgentPackageId ? { boundAgentSource: "deployment" } : {}),
|
|
642
|
+
detachedPackageToolPolicy: boundAgentName || result.toolNames?.length ? "reject" : "drop",
|
|
643
|
+
...(promptLayeringKind ? { promptLayering: { kind: promptLayeringKind } } : {}),
|
|
644
|
+
...(agentToolNames ? { toolNames: agentToolNames } : {}),
|
|
645
|
+
...(result.contract ? { childContract: result.contract } : {}),
|
|
646
|
+
};
|
|
647
|
+
const parentSystemMsg = typeof childConfig.systemMessage === "string"
|
|
648
|
+
? childConfig.systemMessage
|
|
649
|
+
: childConfig.systemMessage?.content ?? "";
|
|
650
|
+
childConfig.systemMessage = buildSubAgentSystemMessage({
|
|
651
|
+
parentSessionId: runtime.input.sessionId,
|
|
652
|
+
childNestingLevel,
|
|
653
|
+
maxNestingLevel: MAX_NESTING_LEVEL,
|
|
654
|
+
agentTask,
|
|
655
|
+
agentIsSystem,
|
|
656
|
+
parentSystemMessage: parentSystemMsg,
|
|
657
|
+
});
|
|
658
|
+
let childSessionId;
|
|
659
|
+
try {
|
|
660
|
+
childSessionId = yield runtime.manager.spawnChildSession(runtime.input.sessionId, childConfig, agentTask, childNestingLevel, agentIsSystem, agentTitle, agentId, agentSplash, agentTitleIsExplicit, bootstrapRequiredTool);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
ctx.traceInfo(`[orch] spawnChildSession failed: ${err.message}`);
|
|
664
|
+
queueFollowup(runtime, `[SYSTEM: spawn_agent failed: ${err.message}]`);
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
const childOrchId = `session-${childSessionId}`;
|
|
668
|
+
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
669
|
+
eventType: "session.agent_spawned",
|
|
670
|
+
data: { childSessionId, agentId: agentId || undefined, task: agentTask.slice(0, 500) },
|
|
671
|
+
}]);
|
|
672
|
+
state.subAgents.push({
|
|
673
|
+
orchId: childOrchId,
|
|
674
|
+
sessionId: childSessionId,
|
|
675
|
+
task: agentTask.slice(0, 500),
|
|
676
|
+
status: "running",
|
|
677
|
+
agentId: agentId || undefined,
|
|
678
|
+
contract: result.contract,
|
|
679
|
+
expectsReport: true,
|
|
680
|
+
});
|
|
681
|
+
queueFollowup(runtime, `[SYSTEM: Sub-agent spawned successfully.\n` +
|
|
682
|
+
` Agent ID: ${childOrchId}\n` +
|
|
683
|
+
` ${resolvedAgentName ? `Agent: ${resolvedAgentName}\n ` : ``}Task: "${agentTask.slice(0, 200)}"\n` +
|
|
684
|
+
` The agent is now running autonomously. Continue your work in this SAME turn and keep following the user's remaining steps. ` +
|
|
685
|
+
`Do NOT stop just because the child started. If you need to pause, call wait or wait_for_agents explicitly. ` +
|
|
686
|
+
`You can also use check_agents to poll status, ` +
|
|
687
|
+
`or message_agent to send instructions.]`);
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
case "message_agent": {
|
|
691
|
+
const targetOrchId = result.agentId;
|
|
692
|
+
const agentEntry = state.subAgents.find(a => a.orchId === targetOrchId);
|
|
693
|
+
if (!agentEntry) {
|
|
694
|
+
ctx.traceInfo(`[orch] message_agent: unknown agent ${targetOrchId}`);
|
|
695
|
+
queueFollowup(runtime, `[SYSTEM: message_agent failed — agent "${targetOrchId}" not found. ` +
|
|
696
|
+
`Known agents: ${state.subAgents.map(a => a.orchId).join(", ") || "none"}]`);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
ctx.traceInfo(`[orch] message_agent via SDK: ${agentEntry.sessionId} msg="${result.message.slice(0, 60)}"`);
|
|
700
|
+
if (result.contractPatch && typeof result.contractPatch === "object") {
|
|
701
|
+
agentEntry.contract = {
|
|
702
|
+
...(agentEntry.contract ?? {}),
|
|
703
|
+
...result.contractPatch,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
yield runtime.manager.sendToSession(agentEntry.sessionId, result.message);
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
ctx.traceInfo(`[orch] message_agent failed: ${err.message}`);
|
|
711
|
+
queueFollowup(runtime, `[SYSTEM: message_agent failed: ${err.message}]`);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
queueFollowup(runtime, `[SYSTEM: Message sent to sub-agent ${targetOrchId}: "${result.message.slice(0, 200)}". ` +
|
|
715
|
+
`Continue your work in this SAME turn. If you are waiting on the child, call wait_for_agents explicitly rather than stopping here.]`);
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
case "check_agents": {
|
|
719
|
+
ctx.traceInfo(`[orch] check_agents: ${state.subAgents.length} agents tracked`);
|
|
720
|
+
if (state.subAgents.length === 0) {
|
|
721
|
+
queueFollowup(runtime, `[SYSTEM: No sub-agents have been spawned yet.]`);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
const statusLines = [];
|
|
725
|
+
for (const agent of state.subAgents) {
|
|
726
|
+
try {
|
|
727
|
+
const rawStatus = yield runtime.manager.getSessionStatus(agent.sessionId);
|
|
728
|
+
const parsed = JSON.parse(rawStatus);
|
|
729
|
+
if (parsed.status === "completed" || parsed.status === "failed" || parsed.status === "idle") {
|
|
730
|
+
agent.status = parsed.status === "failed" ? "failed" : "completed";
|
|
731
|
+
agent.result = getChildResultFromStatus(parsed, agent.result)?.slice(0, 1000);
|
|
732
|
+
}
|
|
733
|
+
statusLines.push(` - Agent ${agent.orchId}\n` +
|
|
734
|
+
` Task: "${agent.task.slice(0, 120)}"\n` +
|
|
735
|
+
` Status: ${parsed.status}\n` +
|
|
736
|
+
` Iterations: ${parsed.iterations ?? 0}\n` +
|
|
737
|
+
` Output: ${getChildResultFromStatus(parsed, agent.result) ?? "(no output yet)"}`);
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
statusLines.push(` - Agent ${agent.orchId}\n` +
|
|
741
|
+
` Task: "${agent.task.slice(0, 120)}"\n` +
|
|
742
|
+
` Status: unknown (error: ${err.message})`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
queueFollowup(runtime, `[SYSTEM: Sub-agent status report (${state.subAgents.length} agents):\n${statusLines.join("\n")}]`);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
case "list_sessions": {
|
|
749
|
+
ctx.traceInfo(`[orch] list_sessions`);
|
|
750
|
+
const rawSessions = yield runtime.manager.listSessions({
|
|
751
|
+
includeSystem: result.includeSystem,
|
|
752
|
+
ownerQuery: result.ownerQuery,
|
|
753
|
+
ownerKind: result.ownerKind,
|
|
754
|
+
});
|
|
755
|
+
const sessions = JSON.parse(rawSessions);
|
|
756
|
+
if (!Array.isArray(sessions) || sessions.length === 0) {
|
|
757
|
+
queueFollowup(runtime, "[SYSTEM: Active sessions (0). No sessions matched the requested filters.]");
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
const lines = sessions.map((s) => ` - ${s.sessionId}${s.sessionId === runtime.input.sessionId ? " (this session)" : ""}\n` +
|
|
761
|
+
` Title: ${s.title ?? "(untitled)"}\n` +
|
|
762
|
+
` Owner: ${s.ownerKind === "system"
|
|
763
|
+
? "system"
|
|
764
|
+
: s.ownerKind === "unowned"
|
|
765
|
+
? "unowned"
|
|
766
|
+
: (s.owner?.displayName || s.owner?.email || [s.owner?.provider, s.owner?.subject].filter(Boolean).join(":") || "user")}\n` +
|
|
767
|
+
` Status: ${s.status}, Iterations: ${s.iterations ?? 0}\n` +
|
|
768
|
+
` Parent: ${s.parentSessionId ?? "none"}`);
|
|
769
|
+
queueFollowup(runtime, `[SYSTEM: Active sessions (${sessions.length}):\n${lines.join("\n")}]`);
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
case "wait_for_agents": {
|
|
773
|
+
let targetIds = result.agentIds;
|
|
774
|
+
if (!targetIds || targetIds.length === 0) {
|
|
775
|
+
const runningAgentIds = state.subAgents.filter(a => a.status === "running").map(a => a.orchId);
|
|
776
|
+
targetIds = runningAgentIds.length > 0
|
|
777
|
+
? runningAgentIds
|
|
778
|
+
: state.subAgents.map(a => a.orchId);
|
|
779
|
+
}
|
|
780
|
+
if (targetIds.length === 0) {
|
|
781
|
+
ctx.traceInfo(`[orch] wait_for_agents: no running agents to wait for`);
|
|
782
|
+
queueFollowup(runtime, `[SYSTEM: No running sub-agents to wait for. All agents have already completed.]`);
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
const stillRunning = targetIds.filter((id) => {
|
|
786
|
+
const agent = state.subAgents.find(a => a.orchId === id);
|
|
787
|
+
return agent && !isSubAgentTerminalStatus(agent.status);
|
|
788
|
+
});
|
|
789
|
+
if (stillRunning.length === 0) {
|
|
790
|
+
queueFollowup(runtime, buildWaitForAgentsFollowup(state.subAgents, targetIds));
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
ctx.traceInfo(`[orch] wait_for_agents: waiting for ${targetIds.length} agents`);
|
|
794
|
+
state.waitingForAgentIds = targetIds;
|
|
795
|
+
const agentPollNow = yield ctx.utcNow();
|
|
796
|
+
// The turn is over and the session is parked (interruptible — an
|
|
797
|
+
// interactive prompt preempts the poll), so surface "waiting" with
|
|
798
|
+
// a reason like every other durable wait. Publishing "running"
|
|
799
|
+
// here made the UI spin "Working…" for the whole agent wait.
|
|
800
|
+
publishStatus(runtime, "waiting", {
|
|
801
|
+
waitReason: `waiting for ${targetIds.length} agent(s)`,
|
|
802
|
+
waitStartedAt: agentPollNow,
|
|
803
|
+
});
|
|
804
|
+
state.activeTimer = {
|
|
805
|
+
deadlineMs: agentPollNow + 30_000,
|
|
806
|
+
originalDurationMs: 30_000,
|
|
807
|
+
reason: `waiting for ${targetIds.length} agent(s)`,
|
|
808
|
+
type: "agent-poll",
|
|
809
|
+
agentIds: targetIds,
|
|
810
|
+
};
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
case "complete_agent": {
|
|
814
|
+
const targetOrchId = result.agentId;
|
|
815
|
+
const agentEntry = state.subAgents.find(a => a.orchId === targetOrchId);
|
|
816
|
+
if (!agentEntry) {
|
|
817
|
+
ctx.traceInfo(`[orch] complete_agent: unknown agent ${targetOrchId}`);
|
|
818
|
+
queueFollowup(runtime, `[SYSTEM: complete_agent failed — agent "${targetOrchId}" not found. ` +
|
|
819
|
+
`Known agents: ${state.subAgents.map(a => a.orchId).join(", ") || "none"}]`);
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
ctx.traceInfo(`[orch] complete_agent: sending /done to ${agentEntry.sessionId}`);
|
|
823
|
+
try {
|
|
824
|
+
const cmdId = `done-${state.iteration}`;
|
|
825
|
+
yield runtime.manager.sendCommandToSession(agentEntry.sessionId, { type: "cmd", cmd: "done", id: cmdId, args: { reason: "Completed by parent" }, requestedBy: runtime.input.sessionId });
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
ctx.traceInfo(`[orch] complete_agent failed: ${err.message}`);
|
|
829
|
+
queueFollowup(runtime, `[SYSTEM: complete_agent failed: ${err.message}]`);
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
queueFollowup(runtime, `[SYSTEM: Graceful completion requested for sub-agent ${targetOrchId}. ` +
|
|
833
|
+
`Use check_agents or wait_for_agents to observe final completion.]`);
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
case "cancel_agent": {
|
|
837
|
+
const targetOrchId = result.agentId;
|
|
838
|
+
const agentEntry = state.subAgents.find(a => a.orchId === targetOrchId);
|
|
839
|
+
if (!agentEntry) {
|
|
840
|
+
ctx.traceInfo(`[orch] cancel_agent: unknown agent ${targetOrchId}`);
|
|
841
|
+
queueFollowup(runtime, `[SYSTEM: cancel_agent failed — agent "${targetOrchId}" not found. ` +
|
|
842
|
+
`Known agents: ${state.subAgents.map(a => a.orchId).join(", ") || "none"}]`);
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
const cancelReason = result.reason ?? "Cancelled by parent";
|
|
846
|
+
ctx.traceInfo(`[orch] cancel_agent: sending cancel to ${agentEntry.sessionId} reason="${cancelReason}"`);
|
|
847
|
+
try {
|
|
848
|
+
const cmdId = `cancel-${state.iteration}-${agentEntry.sessionId.slice(0, 8)}`;
|
|
849
|
+
yield runtime.manager.sendCommandToSession(agentEntry.sessionId, { type: "cmd", cmd: "cancel", id: cmdId, args: { reason: cancelReason }, requestedBy: runtime.input.sessionId });
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
ctx.traceInfo(`[orch] cancel_agent failed: ${err.message}`);
|
|
853
|
+
queueFollowup(runtime, `[SYSTEM: cancel_agent failed: ${err.message}]`);
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
queueFollowup(runtime, `[SYSTEM: Graceful cancellation requested for sub-agent ${targetOrchId}. ` +
|
|
857
|
+
`Use check_agents or wait_for_agents to observe final termination.${result.reason ? ` Reason: ${result.reason}` : ""}]`);
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
case "delete_agent": {
|
|
861
|
+
const targetOrchId = result.agentId;
|
|
862
|
+
const agentEntry = state.subAgents.find(a => a.orchId === targetOrchId);
|
|
863
|
+
if (!agentEntry) {
|
|
864
|
+
ctx.traceInfo(`[orch] delete_agent: unknown agent ${targetOrchId}`);
|
|
865
|
+
queueFollowup(runtime, `[SYSTEM: delete_agent failed — agent "${targetOrchId}" not found. ` +
|
|
866
|
+
`Known agents: ${state.subAgents.map(a => a.orchId).join(", ") || "none"}]`);
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
const deleteReason = result.reason ?? "Deleted by parent";
|
|
870
|
+
ctx.traceInfo(`[orch] delete_agent: deleting ${agentEntry.sessionId} reason="${deleteReason}"`);
|
|
871
|
+
try {
|
|
872
|
+
if (isSubAgentTerminalStatus(agentEntry.status)) {
|
|
873
|
+
yield runtime.manager.deleteSession(agentEntry.sessionId, deleteReason);
|
|
874
|
+
state.subAgents = state.subAgents.filter((agent) => agent.orchId !== targetOrchId);
|
|
875
|
+
queueFollowup(runtime, `[SYSTEM: Sub-agent ${targetOrchId} has been deleted.${result.reason ? ` Reason: ${result.reason}` : ""}]`);
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
const cmdId = `delete-${state.iteration}-${agentEntry.sessionId.slice(0, 8)}`;
|
|
879
|
+
yield runtime.manager.sendCommandToSession(agentEntry.sessionId, { type: "cmd", cmd: "delete", id: cmdId, args: { reason: deleteReason }, requestedBy: runtime.input.sessionId });
|
|
880
|
+
}
|
|
881
|
+
catch (err) {
|
|
882
|
+
ctx.traceInfo(`[orch] delete_agent failed: ${err.message}`);
|
|
883
|
+
queueFollowup(runtime, `[SYSTEM: delete_agent failed: ${err.message}]`);
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
queueFollowup(runtime, `[SYSTEM: Graceful deletion requested for sub-agent ${targetOrchId}. ` +
|
|
887
|
+
`It will cancel its descendants first and then delete itself.${result.reason ? ` Reason: ${result.reason}` : ""}]`);
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
default:
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
//# sourceMappingURL=agents.js.map
|