pilotswarm-sdk 0.3.1 → 0.3.3
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/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +1090 -1
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +41 -1
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +51 -1
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/inspect-tools.d.ts.map +1 -1
- package/dist/inspect-tools.js +27 -0
- package/dist/inspect-tools.js.map +1 -1
- package/dist/managed-session.d.ts +36 -0
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +317 -21
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +36 -2
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +114 -2
- package/dist/management-client.js.map +1 -1
- package/dist/orchestration/index.d.ts +2 -2
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/lifecycle.d.ts.map +1 -1
- package/dist/orchestration/lifecycle.js +74 -4
- package/dist/orchestration/lifecycle.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration/state.d.ts +1 -0
- package/dist/orchestration/state.d.ts.map +1 -1
- package/dist/orchestration/state.js +1 -0
- package/dist/orchestration/state.js.map +1 -1
- package/dist/orchestration/turn.d.ts +24 -0
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +378 -167
- package/dist/orchestration/turn.js.map +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +6 -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_54/agents.d.ts +41 -0
- package/dist/orchestration_1_0_54/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/agents.js +758 -0
- package/dist/orchestration_1_0_54/agents.js.map +1 -0
- package/dist/orchestration_1_0_54/index.d.ts +24 -0
- package/dist/orchestration_1_0_54/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/index.js +13 -0
- package/dist/orchestration_1_0_54/index.js.map +1 -0
- package/dist/orchestration_1_0_54/lifecycle.d.ts +41 -0
- package/dist/orchestration_1_0_54/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/lifecycle.js +510 -0
- package/dist/orchestration_1_0_54/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_54/queue.d.ts +7 -0
- package/dist/orchestration_1_0_54/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/queue.js +644 -0
- package/dist/orchestration_1_0_54/queue.js.map +1 -0
- package/dist/orchestration_1_0_54/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_54/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/runtime.js +194 -0
- package/dist/orchestration_1_0_54/runtime.js.map +1 -0
- package/dist/orchestration_1_0_54/state.d.ts +125 -0
- package/dist/orchestration_1_0_54/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/state.js +105 -0
- package/dist/orchestration_1_0_54/state.js.map +1 -0
- package/dist/orchestration_1_0_54/turn.d.ts +6 -0
- package/dist/orchestration_1_0_54/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/turn.js +864 -0
- package/dist/orchestration_1_0_54/turn.js.map +1 -0
- package/dist/orchestration_1_0_54/utils.d.ts +22 -0
- package/dist/orchestration_1_0_54/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/utils.js +226 -0
- package/dist/orchestration_1_0_54/utils.js.map +1 -0
- package/dist/orchestration_1_0_55/agents.d.ts +41 -0
- package/dist/orchestration_1_0_55/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/agents.js +758 -0
- package/dist/orchestration_1_0_55/agents.js.map +1 -0
- package/dist/orchestration_1_0_55/index.d.ts +24 -0
- package/dist/orchestration_1_0_55/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/index.js +13 -0
- package/dist/orchestration_1_0_55/index.js.map +1 -0
- package/dist/orchestration_1_0_55/lifecycle.d.ts +41 -0
- package/dist/orchestration_1_0_55/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/lifecycle.js +580 -0
- package/dist/orchestration_1_0_55/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_55/queue.d.ts +7 -0
- package/dist/orchestration_1_0_55/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/queue.js +644 -0
- package/dist/orchestration_1_0_55/queue.js.map +1 -0
- package/dist/orchestration_1_0_55/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_55/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/runtime.js +194 -0
- package/dist/orchestration_1_0_55/runtime.js.map +1 -0
- package/dist/orchestration_1_0_55/state.d.ts +126 -0
- package/dist/orchestration_1_0_55/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/state.js +106 -0
- package/dist/orchestration_1_0_55/state.js.map +1 -0
- package/dist/orchestration_1_0_55/turn.d.ts +17 -0
- package/dist/orchestration_1_0_55/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/turn.js +921 -0
- package/dist/orchestration_1_0_55/turn.js.map +1 -0
- package/dist/orchestration_1_0_55/utils.d.ts +22 -0
- package/dist/orchestration_1_0_55/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/utils.js +226 -0
- package/dist/orchestration_1_0_55/utils.js.map +1 -0
- package/dist/session-manager.d.ts +24 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +84 -0
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +19 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +172 -14
- package/dist/session-proxy.js.map +1 -1
- package/dist/sweeper-tools.d.ts +1 -1
- package/dist/sweeper-tools.d.ts.map +1 -1
- package/dist/sweeper-tools.js +20 -75
- package/dist/sweeper-tools.js.map +1 -1
- package/dist/system-agents.d.ts +2 -0
- package/dist/system-agents.d.ts.map +1 -1
- package/dist/system-agents.js +3 -0
- package/dist/system-agents.js.map +1 -1
- package/dist/types.d.ts +46 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +10 -0
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/plugins/mgmt/agents/agent-tuner.agent.md +36 -2
- package/plugins/mgmt/agents/sweeper.agent.md +7 -7
- package/plugins/system/agents/default.agent.md +3 -1
package/dist/managed-session.js
CHANGED
|
@@ -111,7 +111,7 @@ function acknowledgeTurnBoundary(action) {
|
|
|
111
111
|
return `[SYSTEM: ${action} acknowledged. The runtime will suspend at the end of this turn. ` +
|
|
112
112
|
`Finish any remaining tool results for the current step, then stop.]`;
|
|
113
113
|
}
|
|
114
|
-
const TERMINAL_TURN_BOUNDARY_ACTIONS = new Set(["wait", "input_required", "wait_for_agents", "list_sessions", "check_agents"]);
|
|
114
|
+
const TERMINAL_TURN_BOUNDARY_ACTIONS = new Set(["completed", "wait", "input_required", "wait_for_agents", "list_sessions", "check_agents"]);
|
|
115
115
|
function hasTerminalTurnBoundary(turnState) {
|
|
116
116
|
return turnState.pendingActions.some((action) => TERMINAL_TURN_BOUNDARY_ACTIONS.has(action.type));
|
|
117
117
|
}
|
|
@@ -119,6 +119,77 @@ function blockedAfterTurnBoundary(toolName) {
|
|
|
119
119
|
return `[SYSTEM: ${toolName} was not executed because a previous control tool already scheduled this turn to suspend. ` +
|
|
120
120
|
`Stop now; the runtime will resume with the control-tool result.]`;
|
|
121
121
|
}
|
|
122
|
+
function splitQualifiedModel(model) {
|
|
123
|
+
const configured = String(model || "").trim();
|
|
124
|
+
if (!configured)
|
|
125
|
+
return { provider: "(default)", model: "(default)" };
|
|
126
|
+
const separator = configured.indexOf(":");
|
|
127
|
+
if (separator <= 0)
|
|
128
|
+
return { provider: "(unqualified)", model: configured };
|
|
129
|
+
return {
|
|
130
|
+
provider: configured.slice(0, separator),
|
|
131
|
+
model: configured.slice(separator + 1),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function formatCurrentModelConfig(config) {
|
|
135
|
+
const configured = String(config.model || "").trim() || "(default)";
|
|
136
|
+
const { provider, model } = splitQualifiedModel(config.model);
|
|
137
|
+
const reasoningEffort = config.reasoningEffort ?? "(default)";
|
|
138
|
+
return [
|
|
139
|
+
"Current session configured model (this turn):",
|
|
140
|
+
`- provider: ${provider}`,
|
|
141
|
+
`- model: ${model}`,
|
|
142
|
+
`- qualified_model: ${configured}`,
|
|
143
|
+
`- reasoning_effort: ${reasoningEffort}`,
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
// ── Tool-call-as-text guard ──────────────────────────────────────
|
|
147
|
+
// Some models (observed on claude-opus-4.8, especially on repetitive keepalive
|
|
148
|
+
// cron cycles) intermittently emit a tool call as literal
|
|
149
|
+
// `<invoke name="...">`/`<parameter>` text inside the assistant message instead
|
|
150
|
+
// of a real tool_use block. That text is never executed, so a consequential
|
|
151
|
+
// call (store_fact, complete_agent, an ADO write, etc.) would be silently
|
|
152
|
+
// dropped while the transcript implies it happened. We detect the malformed
|
|
153
|
+
// text and re-prompt the model — bounded — to actually invoke the tool.
|
|
154
|
+
const MAX_TEXT_TOOL_CALL_CORRECTIONS = 2;
|
|
155
|
+
const TEXT_TOOL_CALL_INVOKE_RE = /<(?:antml:)?invoke\s+name\s*=\s*"([^"]+)"/i;
|
|
156
|
+
const TEXT_TOOL_CALL_STRUCTURE_RE = /<\/(?:antml:)?invoke\s*>|<(?:antml:)?parameter\b/i;
|
|
157
|
+
const FENCED_CODE_BLOCK_RE = /```[\s\S]*?```/g;
|
|
158
|
+
/**
|
|
159
|
+
* Detect a tool call the model emitted as literal text instead of a real
|
|
160
|
+
* tool_use block. Requires both the `<invoke name="...">` opener and a closing
|
|
161
|
+
* `</invoke>` or a `<parameter>` tag so prose that merely mentions the word
|
|
162
|
+
* "invoke" does not trip the guard. Returns the tool name, or null.
|
|
163
|
+
*/
|
|
164
|
+
function detectTextEmittedToolCall(content) {
|
|
165
|
+
if (typeof content !== "string" || content.length === 0)
|
|
166
|
+
return null;
|
|
167
|
+
const withoutExamples = content.replace(FENCED_CODE_BLOCK_RE, "");
|
|
168
|
+
const nameMatch = withoutExamples.match(TEXT_TOOL_CALL_INVOKE_RE);
|
|
169
|
+
if (!nameMatch)
|
|
170
|
+
return null;
|
|
171
|
+
if (!TEXT_TOOL_CALL_STRUCTURE_RE.test(withoutExamples))
|
|
172
|
+
return null;
|
|
173
|
+
const beforeInvoke = withoutExamples.slice(0, nameMatch.index ?? 0).trim();
|
|
174
|
+
const afterClose = withoutExamples.replace(/[\s\S]*<\/(?:antml:)?invoke\s*>/i, "").trim();
|
|
175
|
+
if (afterClose)
|
|
176
|
+
return null;
|
|
177
|
+
// Allow the common one-token junk prefix observed from claude-opus-4.8
|
|
178
|
+
// (for example "court\n<invoke ...>") but do not flag explanatory prose,
|
|
179
|
+
// markdown docs, or examples that happen to contain Anthropic XML syntax.
|
|
180
|
+
if (beforeInvoke && beforeInvoke.split(/\s+/).length > 1)
|
|
181
|
+
return null;
|
|
182
|
+
return { toolName: (nameMatch[1] || "").trim() || "the requested tool", rawContent: content };
|
|
183
|
+
}
|
|
184
|
+
function buildTextEmittedToolCallCorrection(toolName) {
|
|
185
|
+
const named = toolName && toolName !== "the requested tool" ? `the "${toolName}" tool` : "the intended tool";
|
|
186
|
+
return `[SYSTEM: Tool-call protocol error. Your previous message contained a tool call written as literal text ` +
|
|
187
|
+
`(for example \`<invoke name="${toolName}">\` with <parameter> tags). Text formatted like that is NOT executed — ` +
|
|
188
|
+
`the tool did not run and produced no result, so anything you implied there has NOT actually happened. ` +
|
|
189
|
+
`Come to your senses and actually invoke ${named} now using the real tool-calling mechanism, not text. ` +
|
|
190
|
+
`Do not write <invoke> or <parameter> tags as message content. ` +
|
|
191
|
+
`If you did not actually need a tool this turn, reply with plain prose only and no tool-call markup.]`;
|
|
192
|
+
}
|
|
122
193
|
function failureToolResult(error) {
|
|
123
194
|
const message = error instanceof Error ? error.message : String(error ?? "Tool failed");
|
|
124
195
|
return {
|
|
@@ -158,6 +229,12 @@ export class ManagedSession {
|
|
|
158
229
|
sessionId;
|
|
159
230
|
copilotSession;
|
|
160
231
|
config;
|
|
232
|
+
/** Set for the duration of runTurn(); read by the lock-bypassing stop path. */
|
|
233
|
+
activeTurn = null;
|
|
234
|
+
/** Set only by requestStop(); classifies the turn unwind as "stopped". */
|
|
235
|
+
stopRequest = null;
|
|
236
|
+
/** Resolver for the current turn's completion promise — hang escalation hook. */
|
|
237
|
+
settleTurnResolver = null;
|
|
161
238
|
constructor(sessionId, copilotSession, config) {
|
|
162
239
|
this.sessionId = sessionId;
|
|
163
240
|
this.copilotSession = copilotSession;
|
|
@@ -285,9 +362,9 @@ export class ManagedSession {
|
|
|
285
362
|
handler: async () => "stub",
|
|
286
363
|
});
|
|
287
364
|
const reportCycleTool = defineTool("report_cycle", {
|
|
288
|
-
description: "Report the outcome of the current recurring cron/cron_at watcher cycle. " +
|
|
289
|
-
"Use status='
|
|
290
|
-
"
|
|
365
|
+
description: "Report the outcome of the current recurring cron/cron_at watcher cycle when something material happened. " +
|
|
366
|
+
"Use status='material' when the parent should be notified, and status='blocked' when the cycle found a blocker or failure that needs parent attention. " +
|
|
367
|
+
"On an uneventful cycle, prefer NOT calling this tool at all — just end the turn silently; status='quiet' is accepted but unnecessary. " +
|
|
291
368
|
"This tool does not end the turn; after calling it, finish normally. It is ignored outside recurring watcher cycles.",
|
|
292
369
|
parameters: {
|
|
293
370
|
type: "object",
|
|
@@ -303,6 +380,7 @@ export class ManagedSession {
|
|
|
303
380
|
const listModelsTool = defineTool("list_available_models", {
|
|
304
381
|
description: "List all available LLM models across all configured providers. " +
|
|
305
382
|
"Returns each model's exact qualified name (provider:model), description, and cost tier. " +
|
|
383
|
+
"Also returns this session's current configured provider, model, and reasoning effort for the current turn. " +
|
|
306
384
|
"This output is the authoritative source for model selection. " +
|
|
307
385
|
"Use this when choosing the best model for a sub-agent task, or when the user asks about available models. " +
|
|
308
386
|
"If you plan to pass spawn_agent(model=...), you must choose an exact provider:model value from this list and must not invent or shorten names. " +
|
|
@@ -315,13 +393,28 @@ export class ManagedSession {
|
|
|
315
393
|
},
|
|
316
394
|
handler: async () => "stub",
|
|
317
395
|
});
|
|
396
|
+
const setSessionModelTool = defineTool("set_session_model", {
|
|
397
|
+
description: "Switch this session's model for the next turn boundary. " +
|
|
398
|
+
"Call list_available_models first and pass an exact provider:model value returned there. " +
|
|
399
|
+
"This ends the current turn. After it succeeds, stop; the runtime will continue on the selected model.",
|
|
400
|
+
parameters: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
model: { type: "string", description: "Exact provider:model value from list_available_models." },
|
|
404
|
+
reasoning_effort: { type: "string", enum: ["low", "medium", "high", "xhigh"], description: "Optional reasoning effort supported by the selected model." },
|
|
405
|
+
},
|
|
406
|
+
required: ["model"],
|
|
407
|
+
},
|
|
408
|
+
handler: async () => "stub",
|
|
409
|
+
});
|
|
318
410
|
const updateSessionSummaryTool = defineTool("update_session_summary", {
|
|
319
|
-
description: "Update this session's short live summary for session lists, discovery, and the Summary tab. " +
|
|
411
|
+
description: "Update this session's short live summary and optionally set this session's sticky title for session lists, discovery, and the Summary tab. " +
|
|
320
412
|
"Call it automatically after first meaningful work and after each notable update: changed intent, tangible progress toward the user's goal, received cross-session replies, delivered outputs, blockers, open questions, next actions, key links, schedule/delegate changes, or terminal state. " +
|
|
413
|
+
"Pass title when the user asks you to rename this session or when a durable human-readable title should stick; title updates lock the title against future automatic title summarization. " +
|
|
321
414
|
"Keep it concise and scannable; use compact bullets or short Markdown tables for structured progress, comparisons, rankings, decisions, or result sets instead of prose blobs. " +
|
|
322
415
|
"Do not paste long transcripts, raw logs, or bulky JSON into summary fields. " +
|
|
323
416
|
"Do not call it for no-op heartbeats, timer wakes, or unchanged cron cycles. " +
|
|
324
|
-
"Do not pass a string for summary_state. " + SESSION_SUMMARY_STATE_TEMPLATE,
|
|
417
|
+
"Do not pass a string for summary_state. summary_state is optional only when title is provided. " + SESSION_SUMMARY_STATE_TEMPLATE,
|
|
325
418
|
parameters: {
|
|
326
419
|
type: "object",
|
|
327
420
|
properties: {
|
|
@@ -330,8 +423,8 @@ export class ManagedSession {
|
|
|
330
423
|
description: "Structured live summary state. Must be an object, not a string. Missing arrays should be [].",
|
|
331
424
|
},
|
|
332
425
|
short_summary: { type: "string", description: "Optional concise summary for session lists. If omitted, summary_state.summary is used." },
|
|
426
|
+
title: { type: "string", description: "Optional sticky session title. When set, it behaves like a manual rename and prevents future automatic title changes." },
|
|
333
427
|
},
|
|
334
|
-
required: ["summary_state"],
|
|
335
428
|
},
|
|
336
429
|
handler: async () => "stub",
|
|
337
430
|
});
|
|
@@ -368,7 +461,7 @@ export class ManagedSession {
|
|
|
368
461
|
},
|
|
369
462
|
handler: async () => "stub",
|
|
370
463
|
});
|
|
371
|
-
return [waitTool, waitOnWorkerTool, cronTool, cronAtTool, askUserTool, reportCycleTool, listModelsTool, updateSessionSummaryTool, sendSessionMessageTool, replySessionMessageTool];
|
|
464
|
+
return [waitTool, waitOnWorkerTool, cronTool, cronAtTool, askUserTool, reportCycleTool, listModelsTool, setSessionModelTool, updateSessionSummaryTool, sendSessionMessageTool, replySessionMessageTool];
|
|
372
465
|
}
|
|
373
466
|
/**
|
|
374
467
|
* Sub-agent tool definitions.
|
|
@@ -578,8 +671,73 @@ export class ManagedSession {
|
|
|
578
671
|
*
|
|
579
672
|
* Similarly, if onUserInputRequest fires, we abort and return
|
|
580
673
|
* "input_required" so the orchestration can wait for the user's answer.
|
|
674
|
+
*
|
|
675
|
+
* Stop classification: a user stop (requestStop → abort) reclassifies the
|
|
676
|
+
* unwind as `{ type: "stopped" }` regardless of how the inner turn settled
|
|
677
|
+
* — checked BEFORE pendingActions so a stop that races a wait()/ask_user
|
|
678
|
+
* control-tool abort wins instead of being swallowed into a durable timer,
|
|
679
|
+
* and applied to error unwinds so a forced settle/disconnect is not
|
|
680
|
+
* misclassified as a retryable error.
|
|
581
681
|
*/
|
|
582
682
|
async runTurn(prompt, opts) {
|
|
683
|
+
this.activeTurn = { turnIndex: opts?.turnIndex ?? -1, startedAt: Date.now() };
|
|
684
|
+
try {
|
|
685
|
+
const result = await this._runTurnInner(prompt, opts);
|
|
686
|
+
if (this.stopRequest) {
|
|
687
|
+
return {
|
|
688
|
+
type: "stopped",
|
|
689
|
+
reason: this.stopRequest.reason,
|
|
690
|
+
...(result?.events ? { events: result.events } : {}),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
if (this.stopRequest) {
|
|
697
|
+
return { type: "stopped", reason: this.stopRequest.reason };
|
|
698
|
+
}
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
this.activeTurn = null;
|
|
703
|
+
this.stopRequest = null;
|
|
704
|
+
this.settleTurnResolver = null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/** The in-flight turn, if any. Read by the lock-bypassing stop path. */
|
|
708
|
+
getActiveTurn() {
|
|
709
|
+
return this.activeTurn;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Mark the in-flight turn as user-stopped so its unwind classifies as
|
|
713
|
+
* `stopped`. Returns the active turn info, or null when no turn is
|
|
714
|
+
* running. Does NOT abort by itself — callers pair this with abort().
|
|
715
|
+
*/
|
|
716
|
+
requestStop(reason) {
|
|
717
|
+
if (!this.activeTurn)
|
|
718
|
+
return null;
|
|
719
|
+
this.stopRequest = { reason, requestedAt: Date.now() };
|
|
720
|
+
return { turnIndex: this.activeTurn.turnIndex };
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Hang escalation: resolve the current turn's completion promise directly.
|
|
724
|
+
* runTurn() settles only on the SDK's `session.idle` event; if a wedged
|
|
725
|
+
* stream never fires it, this forces the unwind without depending on any
|
|
726
|
+
* further SDK behavior. Pair with requestStop() so the unwind classifies
|
|
727
|
+
* as `stopped`. Returns false when no turn is in flight.
|
|
728
|
+
*/
|
|
729
|
+
forceSettleTurn(reason) {
|
|
730
|
+
if (!this.activeTurn)
|
|
731
|
+
return false;
|
|
732
|
+
if (!this.stopRequest)
|
|
733
|
+
this.stopRequest = { reason, requestedAt: Date.now() };
|
|
734
|
+
try {
|
|
735
|
+
this.settleTurnResolver?.();
|
|
736
|
+
}
|
|
737
|
+
catch { }
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
async _runTurnInner(prompt, opts) {
|
|
583
741
|
const turnState = {
|
|
584
742
|
pendingActions: [],
|
|
585
743
|
queuedActions: [],
|
|
@@ -637,9 +795,9 @@ export class ManagedSession {
|
|
|
637
795
|
},
|
|
638
796
|
});
|
|
639
797
|
const reportCycleTool = defineTool("report_cycle", {
|
|
640
|
-
description: "Report the outcome of the current recurring cron/cron_at watcher cycle. " +
|
|
641
|
-
"Use status='
|
|
642
|
-
"
|
|
798
|
+
description: "Report the outcome of the current recurring cron/cron_at watcher cycle when something material happened. " +
|
|
799
|
+
"Use status='material' when the parent should be notified, and status='blocked' when the cycle found a blocker or failure that needs parent attention. " +
|
|
800
|
+
"On an uneventful cycle, prefer NOT calling this tool at all — just end the turn silently; status='quiet' is accepted but unnecessary. " +
|
|
643
801
|
"This tool does not end the turn; after calling it, finish normally. It is ignored outside recurring watcher cycles.",
|
|
644
802
|
parameters: {
|
|
645
803
|
type: "object",
|
|
@@ -892,6 +1050,7 @@ export class ManagedSession {
|
|
|
892
1050
|
const listModelsTool = defineTool("list_available_models", {
|
|
893
1051
|
description: "List all available LLM models across all configured providers. " +
|
|
894
1052
|
"Returns each model's exact qualified name (provider:model), description, and cost tier. " +
|
|
1053
|
+
"Also returns this session's current configured provider, model, and reasoning effort for the current turn. " +
|
|
895
1054
|
"This output is the authoritative source for model selection. " +
|
|
896
1055
|
"Use this when choosing the best model for a sub-agent task, or when the user asks about available models. " +
|
|
897
1056
|
"If you plan to pass spawn_agent(model=...), you must choose an exact provider:model value from this list and must not invent or shorten names. " +
|
|
@@ -903,16 +1062,62 @@ export class ManagedSession {
|
|
|
903
1062
|
properties: {},
|
|
904
1063
|
},
|
|
905
1064
|
handler: async () => {
|
|
906
|
-
return
|
|
1065
|
+
return [
|
|
1066
|
+
formatCurrentModelConfig(this.config),
|
|
1067
|
+
opts?.modelSummary || "No model providers configured.",
|
|
1068
|
+
].join("\n\n");
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
const setSessionModelTool = defineTool("set_session_model", {
|
|
1072
|
+
description: "Switch this session's model for the next turn boundary. " +
|
|
1073
|
+
"Call list_available_models first and pass an exact provider:model value returned there. " +
|
|
1074
|
+
"This ends the current turn. After it succeeds, stop; the runtime will continue on the selected model.",
|
|
1075
|
+
parameters: {
|
|
1076
|
+
type: "object",
|
|
1077
|
+
properties: {
|
|
1078
|
+
model: { type: "string", description: "Exact provider:model value from list_available_models." },
|
|
1079
|
+
reasoning_effort: { type: "string", enum: ["low", "medium", "high", "xhigh"], description: "Optional reasoning effort supported by the selected model." },
|
|
1080
|
+
},
|
|
1081
|
+
required: ["model"],
|
|
1082
|
+
},
|
|
1083
|
+
handler: async (args) => {
|
|
1084
|
+
if (hasTerminalTurnBoundary(turnState))
|
|
1085
|
+
return blockedAfterTurnBoundary("set_session_model");
|
|
1086
|
+
const model = String(args.model || "").trim();
|
|
1087
|
+
if (!model)
|
|
1088
|
+
return "Error: model is required.";
|
|
1089
|
+
const reasoningEffort = args.reasoning_effort ? normalizeReasoningEffort(args.reasoning_effort) : undefined;
|
|
1090
|
+
if (args.reasoning_effort && !reasoningEffort) {
|
|
1091
|
+
return "Error: reasoning_effort must be one of low, medium, high, xhigh.";
|
|
1092
|
+
}
|
|
1093
|
+
if (!controlBridge)
|
|
1094
|
+
return "Error: set_session_model is unavailable in this session.";
|
|
1095
|
+
const result = await controlBridge.setSessionModel({ model, ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}) });
|
|
1096
|
+
if (/model switch accepted/i.test(String(result))) {
|
|
1097
|
+
turnState.pendingActions.push({
|
|
1098
|
+
type: "completed",
|
|
1099
|
+
content: "Model switch requested. Continuing on the selected model.",
|
|
1100
|
+
});
|
|
1101
|
+
return `${result}\n${acknowledgeTurnBoundary("set_session_model")}`;
|
|
1102
|
+
}
|
|
1103
|
+
if (/set_session_model failed/i.test(String(result)) || !/model switch accepted/i.test(String(result))) {
|
|
1104
|
+
turnState.pendingActions.push({
|
|
1105
|
+
type: "completed",
|
|
1106
|
+
content: "Model switch failed. Continuing on the unchanged model.",
|
|
1107
|
+
});
|
|
1108
|
+
return `${result}\n${acknowledgeTurnBoundary("set_session_model")}`;
|
|
1109
|
+
}
|
|
1110
|
+
return result;
|
|
907
1111
|
},
|
|
908
1112
|
});
|
|
909
1113
|
const updateSessionSummaryTool = defineTool("update_session_summary", {
|
|
910
|
-
description: "Update this session's short live summary for session lists, discovery, and the Summary tab. " +
|
|
1114
|
+
description: "Update this session's short live summary and optionally set this session's sticky title for session lists, discovery, and the Summary tab. " +
|
|
911
1115
|
"Call it automatically after first meaningful work and after each notable update: changed intent, tangible progress toward the user's goal, received cross-session replies, delivered outputs, blockers, open questions, next actions, key links, schedule/delegate changes, or terminal state. " +
|
|
1116
|
+
"Pass title when the user asks you to rename this session or when a durable human-readable title should stick; title updates lock the title against future automatic title summarization. " +
|
|
912
1117
|
"Keep it concise and scannable; use compact bullets or short Markdown tables for structured progress, comparisons, rankings, decisions, or result sets instead of prose blobs. " +
|
|
913
1118
|
"Do not paste long transcripts, raw logs, or bulky JSON into summary fields. " +
|
|
914
1119
|
"Do not call it for no-op heartbeats, timer wakes, or unchanged cron cycles. " +
|
|
915
|
-
"Do not pass a string for summary_state. " + SESSION_SUMMARY_STATE_TEMPLATE,
|
|
1120
|
+
"Do not pass a string for summary_state. summary_state is optional only when title is provided. " + SESSION_SUMMARY_STATE_TEMPLATE,
|
|
916
1121
|
parameters: {
|
|
917
1122
|
type: "object",
|
|
918
1123
|
properties: {
|
|
@@ -921,8 +1126,8 @@ export class ManagedSession {
|
|
|
921
1126
|
description: "Structured live summary state. Must be an object, not a string. Missing arrays should be [].",
|
|
922
1127
|
},
|
|
923
1128
|
short_summary: { type: "string", description: "Optional concise summary for session lists. If omitted, summary_state.summary is used." },
|
|
1129
|
+
title: { type: "string", description: "Optional sticky session title. When set, it behaves like a manual rename and prevents future automatic title changes." },
|
|
924
1130
|
},
|
|
925
|
-
required: ["summary_state"],
|
|
926
1131
|
},
|
|
927
1132
|
handler: async (args) => {
|
|
928
1133
|
if (hasTerminalTurnBoundary(turnState))
|
|
@@ -1271,7 +1476,7 @@ export class ManagedSession {
|
|
|
1271
1476
|
return acknowledgeTurnBoundary("delete_agent");
|
|
1272
1477
|
},
|
|
1273
1478
|
});
|
|
1274
|
-
const SYSTEM_TOOL_NAMES = new Set(["wait", "wait_on_worker", "cron", "cron_at", "ask_user", "report_cycle", "list_available_models", "update_session_summary", "send_session_message", "reply_session_message", "spawn_agent", "message_agent", "check_agents", "wait_for_agents", "list_sessions", "complete_agent", "cancel_agent", "delete_agent"]);
|
|
1479
|
+
const SYSTEM_TOOL_NAMES = new Set(["wait", "wait_on_worker", "cron", "cron_at", "ask_user", "report_cycle", "list_available_models", "set_session_model", "update_session_summary", "send_session_message", "reply_session_message", "spawn_agent", "message_agent", "check_agents", "wait_for_agents", "list_sessions", "complete_agent", "cancel_agent", "delete_agent"]);
|
|
1275
1480
|
// Merge user tools with system tools
|
|
1276
1481
|
const userTools = this.config.tools ?? [];
|
|
1277
1482
|
// Wrap user tool handlers to augment invocation with the PilotSwarm
|
|
@@ -1310,6 +1515,7 @@ export class ManagedSession {
|
|
|
1310
1515
|
askUserTool,
|
|
1311
1516
|
reportCycleTool,
|
|
1312
1517
|
listModelsTool,
|
|
1518
|
+
setSessionModelTool,
|
|
1313
1519
|
updateSessionSummaryTool,
|
|
1314
1520
|
sendSessionMessageTool,
|
|
1315
1521
|
replySessionMessageTool,
|
|
@@ -1342,6 +1548,7 @@ export class ManagedSession {
|
|
|
1342
1548
|
let lastPublishedReasoning = "";
|
|
1343
1549
|
let lastReasoningPublishAt = 0;
|
|
1344
1550
|
let deferredSessionError = null;
|
|
1551
|
+
const textEmittedToolCallRef = { current: null };
|
|
1345
1552
|
// Streaming progress + turn timing state.
|
|
1346
1553
|
// Token-level deltas (`assistant.message_delta`,
|
|
1347
1554
|
// `assistant.streaming_delta`, `assistant.reasoning_delta`) stay
|
|
@@ -1435,6 +1642,9 @@ export class ManagedSession {
|
|
|
1435
1642
|
}
|
|
1436
1643
|
}
|
|
1437
1644
|
const turnComplete = new Promise((resolve, reject) => {
|
|
1645
|
+
// Hang-escalation hook: forceSettleTurn() resolves this promise when
|
|
1646
|
+
// the SDK never fires session.idle (see stop-turn plan, edge E3).
|
|
1647
|
+
this.settleTurnResolver = resolve;
|
|
1438
1648
|
// Catch-all event handler — captures every event and fires onEvent immediately.
|
|
1439
1649
|
unsubscribers.push(this.copilotSession.on((event) => {
|
|
1440
1650
|
const eventType = event.type ?? event.eventType ?? "unknown";
|
|
@@ -1480,6 +1690,16 @@ export class ManagedSession {
|
|
|
1480
1690
|
if (isEmptyAssistantTranscriptEvent(eventType, eventData)) {
|
|
1481
1691
|
return;
|
|
1482
1692
|
}
|
|
1693
|
+
if (eventType === "assistant.message") {
|
|
1694
|
+
const content = extractAssistantMessageContent({ data: eventData });
|
|
1695
|
+
const textToolCall = detectTextEmittedToolCall(content);
|
|
1696
|
+
if (textToolCall) {
|
|
1697
|
+
textEmittedToolCallRef.current = textToolCall;
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
finalContent = content ?? finalContent;
|
|
1701
|
+
publishReasoningSnapshot("assistant.message", true);
|
|
1702
|
+
}
|
|
1483
1703
|
// Track turn boundaries so we can stamp turn_end with a
|
|
1484
1704
|
// durationMs and the streaming counters.
|
|
1485
1705
|
if (eventType === "assistant.turn_start") {
|
|
@@ -1547,11 +1767,6 @@ export class ManagedSession {
|
|
|
1547
1767
|
catch { }
|
|
1548
1768
|
}
|
|
1549
1769
|
}));
|
|
1550
|
-
// Capture the final assistant message
|
|
1551
|
-
unsubscribers.push(this.copilotSession.on("assistant.message", (event) => {
|
|
1552
|
-
finalContent = extractAssistantMessageContent(event) ?? finalContent;
|
|
1553
|
-
publishReasoningSnapshot("assistant.message", true);
|
|
1554
|
-
}));
|
|
1555
1770
|
unsubscribers.push(this.copilotSession.on("assistant.reasoning", (event) => {
|
|
1556
1771
|
currentReasoning = String(extractReasoningText(event?.data ?? event) || "").trim();
|
|
1557
1772
|
if (currentReasoning) {
|
|
@@ -1594,6 +1809,18 @@ export class ManagedSession {
|
|
|
1594
1809
|
setTimeout(() => reject(new Error("Turn timed out")), TURN_TIMEOUT);
|
|
1595
1810
|
})
|
|
1596
1811
|
: null;
|
|
1812
|
+
// Re-armable idle waiter used by the tool-call-as-text guard to wait for
|
|
1813
|
+
// the model's response to a mid-turn correction without tearing down the
|
|
1814
|
+
// event subscriptions set up above.
|
|
1815
|
+
const waitForNextIdle = () => new Promise((resolve) => {
|
|
1816
|
+
const unsub = this.copilotSession.on("session.idle", () => {
|
|
1817
|
+
flushStreamingProgress(true);
|
|
1818
|
+
publishReasoningSnapshot("session.idle", true);
|
|
1819
|
+
unsub();
|
|
1820
|
+
resolve();
|
|
1821
|
+
});
|
|
1822
|
+
unsubscribers.push(unsub);
|
|
1823
|
+
});
|
|
1597
1824
|
const effectivePrompt = opts?.requiredTool
|
|
1598
1825
|
? [
|
|
1599
1826
|
`[SYSTEM: For this request, you MUST invoke the tool "${opts.requiredTool}" before giving your answer.`,
|
|
@@ -1618,6 +1845,61 @@ export class ManagedSession {
|
|
|
1618
1845
|
else {
|
|
1619
1846
|
await turnComplete;
|
|
1620
1847
|
}
|
|
1848
|
+
// ── Guard: tool call emitted as text instead of executed ──────────
|
|
1849
|
+
// If the model typed a tool call as `<invoke .../>` text rather than
|
|
1850
|
+
// calling it, that call did not run. Re-prompt it (bounded) to
|
|
1851
|
+
// actually invoke the tool so a consequential call is never silently
|
|
1852
|
+
// dropped. If a control tool already scheduled a turn boundary, we
|
|
1853
|
+
// still return an error rather than accepting a transcript that
|
|
1854
|
+
// implied another unexecuted tool side effect.
|
|
1855
|
+
let textToolCallCorrections = 0;
|
|
1856
|
+
while (textToolCallCorrections < MAX_TEXT_TOOL_CALL_CORRECTIONS &&
|
|
1857
|
+
!hasTerminalTurnBoundary(turnState) &&
|
|
1858
|
+
textEmittedToolCallRef.current) {
|
|
1859
|
+
const offendingTool = textEmittedToolCallRef.current.toolName;
|
|
1860
|
+
const rawContent = textEmittedToolCallRef.current.rawContent;
|
|
1861
|
+
textEmittedToolCallRef.current = null;
|
|
1862
|
+
textToolCallCorrections++;
|
|
1863
|
+
const diagnostic = {
|
|
1864
|
+
eventType: "runtime.tool_call_as_text",
|
|
1865
|
+
data: { toolName: offendingTool, rawContent, attempt: textToolCallCorrections, sessionId: this.sessionId },
|
|
1866
|
+
};
|
|
1867
|
+
collectedEvents.push(diagnostic);
|
|
1868
|
+
if (opts?.onEvent) {
|
|
1869
|
+
try {
|
|
1870
|
+
opts.onEvent(diagnostic);
|
|
1871
|
+
}
|
|
1872
|
+
catch { }
|
|
1873
|
+
}
|
|
1874
|
+
finalContent = undefined;
|
|
1875
|
+
const nextIdle = waitForNextIdle();
|
|
1876
|
+
await this.copilotSession.send({ prompt: buildTextEmittedToolCallCorrection(offendingTool) });
|
|
1877
|
+
if (timeoutPromise) {
|
|
1878
|
+
await Promise.race([nextIdle, timeoutPromise]);
|
|
1879
|
+
}
|
|
1880
|
+
else {
|
|
1881
|
+
await nextIdle;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (textEmittedToolCallRef.current) {
|
|
1885
|
+
const diagnostic = {
|
|
1886
|
+
eventType: "runtime.tool_call_as_text",
|
|
1887
|
+
data: { toolName: textEmittedToolCallRef.current.toolName, rawContent: textEmittedToolCallRef.current.rawContent, final: true, sessionId: this.sessionId },
|
|
1888
|
+
};
|
|
1889
|
+
collectedEvents.push(diagnostic);
|
|
1890
|
+
if (opts?.onEvent) {
|
|
1891
|
+
try {
|
|
1892
|
+
opts.onEvent(diagnostic);
|
|
1893
|
+
}
|
|
1894
|
+
catch { }
|
|
1895
|
+
}
|
|
1896
|
+
const message = buildTextEmittedToolCallCorrection(textEmittedToolCallRef.current.toolName);
|
|
1897
|
+
return {
|
|
1898
|
+
type: "error",
|
|
1899
|
+
message,
|
|
1900
|
+
events: collectedEvents,
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1621
1903
|
}
|
|
1622
1904
|
catch (err) {
|
|
1623
1905
|
// Timeout — kill it
|
|
@@ -1648,6 +1930,8 @@ export class ManagedSession {
|
|
|
1648
1930
|
const combinedQueuedActions = [...turnState.queuedActions, ...remainingActions];
|
|
1649
1931
|
const queuedActions = combinedQueuedActions.length > 0 ? combinedQueuedActions : undefined;
|
|
1650
1932
|
switch (firstAction.type) {
|
|
1933
|
+
case "completed":
|
|
1934
|
+
return { ...firstAction, events: collectedEvents, queuedActions };
|
|
1651
1935
|
case "input_required":
|
|
1652
1936
|
return { ...firstAction, events: collectedEvents, queuedActions };
|
|
1653
1937
|
case "wait":
|
|
@@ -1723,6 +2007,8 @@ export class ManagedSession {
|
|
|
1723
2007
|
updateConfig(config) {
|
|
1724
2008
|
if (config.model !== undefined)
|
|
1725
2009
|
this.config.model = config.model;
|
|
2010
|
+
if (config.reasoningEffort !== undefined)
|
|
2011
|
+
this.config.reasoningEffort = config.reasoningEffort;
|
|
1726
2012
|
if (config.tools !== undefined)
|
|
1727
2013
|
this.config.tools = config.tools;
|
|
1728
2014
|
if (config.systemMessage !== undefined)
|
|
@@ -1732,6 +2018,16 @@ export class ManagedSession {
|
|
|
1732
2018
|
if (config.waitThreshold !== undefined)
|
|
1733
2019
|
this.config.waitThreshold = config.waitThreshold;
|
|
1734
2020
|
}
|
|
2021
|
+
requiresModelRebind(config) {
|
|
2022
|
+
const currentModel = this.config.model;
|
|
2023
|
+
const nextModel = config.model ?? this.config.model;
|
|
2024
|
+
const currentReasoningEffort = this.config.reasoningEffort ?? null;
|
|
2025
|
+
const nextReasoningEffort = config.reasoningEffort !== undefined
|
|
2026
|
+
? config.reasoningEffort ?? null
|
|
2027
|
+
: this.config.reasoningEffort ?? null;
|
|
2028
|
+
return Boolean((currentModel || nextModel)
|
|
2029
|
+
&& (currentModel !== nextModel || currentReasoningEffort !== nextReasoningEffort));
|
|
2030
|
+
}
|
|
1735
2031
|
/** Get the underlying CopilotSession (for direct access when needed). */
|
|
1736
2032
|
getCopilotSession() {
|
|
1737
2033
|
return this.copilotSession;
|