pilotswarm-sdk 0.3.1 → 0.3.2
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 +840 -1
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +31 -1
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +46 -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 +1 -0
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +243 -21
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +14 -1
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +49 -1
- 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 +11 -0
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +61 -4
- package/dist/orchestration/turn.js.map +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_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/session-manager.d.ts +4 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +22 -0
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +12 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +137 -13
- 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 +14 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- 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 {
|
|
@@ -285,9 +356,9 @@ export class ManagedSession {
|
|
|
285
356
|
handler: async () => "stub",
|
|
286
357
|
});
|
|
287
358
|
const reportCycleTool = defineTool("report_cycle", {
|
|
288
|
-
description: "Report the outcome of the current recurring cron/cron_at watcher cycle. " +
|
|
289
|
-
"Use status='
|
|
290
|
-
"
|
|
359
|
+
description: "Report the outcome of the current recurring cron/cron_at watcher cycle when something material happened. " +
|
|
360
|
+
"Use status='material' when the parent should be notified, and status='blocked' when the cycle found a blocker or failure that needs parent attention. " +
|
|
361
|
+
"On an uneventful cycle, prefer NOT calling this tool at all — just end the turn silently; status='quiet' is accepted but unnecessary. " +
|
|
291
362
|
"This tool does not end the turn; after calling it, finish normally. It is ignored outside recurring watcher cycles.",
|
|
292
363
|
parameters: {
|
|
293
364
|
type: "object",
|
|
@@ -303,6 +374,7 @@ export class ManagedSession {
|
|
|
303
374
|
const listModelsTool = defineTool("list_available_models", {
|
|
304
375
|
description: "List all available LLM models across all configured providers. " +
|
|
305
376
|
"Returns each model's exact qualified name (provider:model), description, and cost tier. " +
|
|
377
|
+
"Also returns this session's current configured provider, model, and reasoning effort for the current turn. " +
|
|
306
378
|
"This output is the authoritative source for model selection. " +
|
|
307
379
|
"Use this when choosing the best model for a sub-agent task, or when the user asks about available models. " +
|
|
308
380
|
"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 +387,28 @@ export class ManagedSession {
|
|
|
315
387
|
},
|
|
316
388
|
handler: async () => "stub",
|
|
317
389
|
});
|
|
390
|
+
const setSessionModelTool = defineTool("set_session_model", {
|
|
391
|
+
description: "Switch this session's model for the next turn boundary. " +
|
|
392
|
+
"Call list_available_models first and pass an exact provider:model value returned there. " +
|
|
393
|
+
"This ends the current turn. After it succeeds, stop; the runtime will continue on the selected model.",
|
|
394
|
+
parameters: {
|
|
395
|
+
type: "object",
|
|
396
|
+
properties: {
|
|
397
|
+
model: { type: "string", description: "Exact provider:model value from list_available_models." },
|
|
398
|
+
reasoning_effort: { type: "string", enum: ["low", "medium", "high", "xhigh"], description: "Optional reasoning effort supported by the selected model." },
|
|
399
|
+
},
|
|
400
|
+
required: ["model"],
|
|
401
|
+
},
|
|
402
|
+
handler: async () => "stub",
|
|
403
|
+
});
|
|
318
404
|
const updateSessionSummaryTool = defineTool("update_session_summary", {
|
|
319
|
-
description: "Update this session's short live summary for session lists, discovery, and the Summary tab. " +
|
|
405
|
+
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
406
|
"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. " +
|
|
407
|
+
"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
408
|
"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
409
|
"Do not paste long transcripts, raw logs, or bulky JSON into summary fields. " +
|
|
323
410
|
"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,
|
|
411
|
+
"Do not pass a string for summary_state. summary_state is optional only when title is provided. " + SESSION_SUMMARY_STATE_TEMPLATE,
|
|
325
412
|
parameters: {
|
|
326
413
|
type: "object",
|
|
327
414
|
properties: {
|
|
@@ -330,8 +417,8 @@ export class ManagedSession {
|
|
|
330
417
|
description: "Structured live summary state. Must be an object, not a string. Missing arrays should be [].",
|
|
331
418
|
},
|
|
332
419
|
short_summary: { type: "string", description: "Optional concise summary for session lists. If omitted, summary_state.summary is used." },
|
|
420
|
+
title: { type: "string", description: "Optional sticky session title. When set, it behaves like a manual rename and prevents future automatic title changes." },
|
|
333
421
|
},
|
|
334
|
-
required: ["summary_state"],
|
|
335
422
|
},
|
|
336
423
|
handler: async () => "stub",
|
|
337
424
|
});
|
|
@@ -368,7 +455,7 @@ export class ManagedSession {
|
|
|
368
455
|
},
|
|
369
456
|
handler: async () => "stub",
|
|
370
457
|
});
|
|
371
|
-
return [waitTool, waitOnWorkerTool, cronTool, cronAtTool, askUserTool, reportCycleTool, listModelsTool, updateSessionSummaryTool, sendSessionMessageTool, replySessionMessageTool];
|
|
458
|
+
return [waitTool, waitOnWorkerTool, cronTool, cronAtTool, askUserTool, reportCycleTool, listModelsTool, setSessionModelTool, updateSessionSummaryTool, sendSessionMessageTool, replySessionMessageTool];
|
|
372
459
|
}
|
|
373
460
|
/**
|
|
374
461
|
* Sub-agent tool definitions.
|
|
@@ -637,9 +724,9 @@ export class ManagedSession {
|
|
|
637
724
|
},
|
|
638
725
|
});
|
|
639
726
|
const reportCycleTool = defineTool("report_cycle", {
|
|
640
|
-
description: "Report the outcome of the current recurring cron/cron_at watcher cycle. " +
|
|
641
|
-
"Use status='
|
|
642
|
-
"
|
|
727
|
+
description: "Report the outcome of the current recurring cron/cron_at watcher cycle when something material happened. " +
|
|
728
|
+
"Use status='material' when the parent should be notified, and status='blocked' when the cycle found a blocker or failure that needs parent attention. " +
|
|
729
|
+
"On an uneventful cycle, prefer NOT calling this tool at all — just end the turn silently; status='quiet' is accepted but unnecessary. " +
|
|
643
730
|
"This tool does not end the turn; after calling it, finish normally. It is ignored outside recurring watcher cycles.",
|
|
644
731
|
parameters: {
|
|
645
732
|
type: "object",
|
|
@@ -892,6 +979,7 @@ export class ManagedSession {
|
|
|
892
979
|
const listModelsTool = defineTool("list_available_models", {
|
|
893
980
|
description: "List all available LLM models across all configured providers. " +
|
|
894
981
|
"Returns each model's exact qualified name (provider:model), description, and cost tier. " +
|
|
982
|
+
"Also returns this session's current configured provider, model, and reasoning effort for the current turn. " +
|
|
895
983
|
"This output is the authoritative source for model selection. " +
|
|
896
984
|
"Use this when choosing the best model for a sub-agent task, or when the user asks about available models. " +
|
|
897
985
|
"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 +991,62 @@ export class ManagedSession {
|
|
|
903
991
|
properties: {},
|
|
904
992
|
},
|
|
905
993
|
handler: async () => {
|
|
906
|
-
return
|
|
994
|
+
return [
|
|
995
|
+
formatCurrentModelConfig(this.config),
|
|
996
|
+
opts?.modelSummary || "No model providers configured.",
|
|
997
|
+
].join("\n\n");
|
|
998
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
const setSessionModelTool = defineTool("set_session_model", {
|
|
1001
|
+
description: "Switch this session's model for the next turn boundary. " +
|
|
1002
|
+
"Call list_available_models first and pass an exact provider:model value returned there. " +
|
|
1003
|
+
"This ends the current turn. After it succeeds, stop; the runtime will continue on the selected model.",
|
|
1004
|
+
parameters: {
|
|
1005
|
+
type: "object",
|
|
1006
|
+
properties: {
|
|
1007
|
+
model: { type: "string", description: "Exact provider:model value from list_available_models." },
|
|
1008
|
+
reasoning_effort: { type: "string", enum: ["low", "medium", "high", "xhigh"], description: "Optional reasoning effort supported by the selected model." },
|
|
1009
|
+
},
|
|
1010
|
+
required: ["model"],
|
|
1011
|
+
},
|
|
1012
|
+
handler: async (args) => {
|
|
1013
|
+
if (hasTerminalTurnBoundary(turnState))
|
|
1014
|
+
return blockedAfterTurnBoundary("set_session_model");
|
|
1015
|
+
const model = String(args.model || "").trim();
|
|
1016
|
+
if (!model)
|
|
1017
|
+
return "Error: model is required.";
|
|
1018
|
+
const reasoningEffort = args.reasoning_effort ? normalizeReasoningEffort(args.reasoning_effort) : undefined;
|
|
1019
|
+
if (args.reasoning_effort && !reasoningEffort) {
|
|
1020
|
+
return "Error: reasoning_effort must be one of low, medium, high, xhigh.";
|
|
1021
|
+
}
|
|
1022
|
+
if (!controlBridge)
|
|
1023
|
+
return "Error: set_session_model is unavailable in this session.";
|
|
1024
|
+
const result = await controlBridge.setSessionModel({ model, ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}) });
|
|
1025
|
+
if (/model switch accepted/i.test(String(result))) {
|
|
1026
|
+
turnState.pendingActions.push({
|
|
1027
|
+
type: "completed",
|
|
1028
|
+
content: "Model switch requested. Continuing on the selected model.",
|
|
1029
|
+
});
|
|
1030
|
+
return `${result}\n${acknowledgeTurnBoundary("set_session_model")}`;
|
|
1031
|
+
}
|
|
1032
|
+
if (/set_session_model failed/i.test(String(result)) || !/model switch accepted/i.test(String(result))) {
|
|
1033
|
+
turnState.pendingActions.push({
|
|
1034
|
+
type: "completed",
|
|
1035
|
+
content: "Model switch failed. Continuing on the unchanged model.",
|
|
1036
|
+
});
|
|
1037
|
+
return `${result}\n${acknowledgeTurnBoundary("set_session_model")}`;
|
|
1038
|
+
}
|
|
1039
|
+
return result;
|
|
907
1040
|
},
|
|
908
1041
|
});
|
|
909
1042
|
const updateSessionSummaryTool = defineTool("update_session_summary", {
|
|
910
|
-
description: "Update this session's short live summary for session lists, discovery, and the Summary tab. " +
|
|
1043
|
+
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
1044
|
"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. " +
|
|
1045
|
+
"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
1046
|
"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
1047
|
"Do not paste long transcripts, raw logs, or bulky JSON into summary fields. " +
|
|
914
1048
|
"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,
|
|
1049
|
+
"Do not pass a string for summary_state. summary_state is optional only when title is provided. " + SESSION_SUMMARY_STATE_TEMPLATE,
|
|
916
1050
|
parameters: {
|
|
917
1051
|
type: "object",
|
|
918
1052
|
properties: {
|
|
@@ -921,8 +1055,8 @@ export class ManagedSession {
|
|
|
921
1055
|
description: "Structured live summary state. Must be an object, not a string. Missing arrays should be [].",
|
|
922
1056
|
},
|
|
923
1057
|
short_summary: { type: "string", description: "Optional concise summary for session lists. If omitted, summary_state.summary is used." },
|
|
1058
|
+
title: { type: "string", description: "Optional sticky session title. When set, it behaves like a manual rename and prevents future automatic title changes." },
|
|
924
1059
|
},
|
|
925
|
-
required: ["summary_state"],
|
|
926
1060
|
},
|
|
927
1061
|
handler: async (args) => {
|
|
928
1062
|
if (hasTerminalTurnBoundary(turnState))
|
|
@@ -1271,7 +1405,7 @@ export class ManagedSession {
|
|
|
1271
1405
|
return acknowledgeTurnBoundary("delete_agent");
|
|
1272
1406
|
},
|
|
1273
1407
|
});
|
|
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"]);
|
|
1408
|
+
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
1409
|
// Merge user tools with system tools
|
|
1276
1410
|
const userTools = this.config.tools ?? [];
|
|
1277
1411
|
// Wrap user tool handlers to augment invocation with the PilotSwarm
|
|
@@ -1310,6 +1444,7 @@ export class ManagedSession {
|
|
|
1310
1444
|
askUserTool,
|
|
1311
1445
|
reportCycleTool,
|
|
1312
1446
|
listModelsTool,
|
|
1447
|
+
setSessionModelTool,
|
|
1313
1448
|
updateSessionSummaryTool,
|
|
1314
1449
|
sendSessionMessageTool,
|
|
1315
1450
|
replySessionMessageTool,
|
|
@@ -1342,6 +1477,7 @@ export class ManagedSession {
|
|
|
1342
1477
|
let lastPublishedReasoning = "";
|
|
1343
1478
|
let lastReasoningPublishAt = 0;
|
|
1344
1479
|
let deferredSessionError = null;
|
|
1480
|
+
const textEmittedToolCallRef = { current: null };
|
|
1345
1481
|
// Streaming progress + turn timing state.
|
|
1346
1482
|
// Token-level deltas (`assistant.message_delta`,
|
|
1347
1483
|
// `assistant.streaming_delta`, `assistant.reasoning_delta`) stay
|
|
@@ -1480,6 +1616,16 @@ export class ManagedSession {
|
|
|
1480
1616
|
if (isEmptyAssistantTranscriptEvent(eventType, eventData)) {
|
|
1481
1617
|
return;
|
|
1482
1618
|
}
|
|
1619
|
+
if (eventType === "assistant.message") {
|
|
1620
|
+
const content = extractAssistantMessageContent({ data: eventData });
|
|
1621
|
+
const textToolCall = detectTextEmittedToolCall(content);
|
|
1622
|
+
if (textToolCall) {
|
|
1623
|
+
textEmittedToolCallRef.current = textToolCall;
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
finalContent = content ?? finalContent;
|
|
1627
|
+
publishReasoningSnapshot("assistant.message", true);
|
|
1628
|
+
}
|
|
1483
1629
|
// Track turn boundaries so we can stamp turn_end with a
|
|
1484
1630
|
// durationMs and the streaming counters.
|
|
1485
1631
|
if (eventType === "assistant.turn_start") {
|
|
@@ -1547,11 +1693,6 @@ export class ManagedSession {
|
|
|
1547
1693
|
catch { }
|
|
1548
1694
|
}
|
|
1549
1695
|
}));
|
|
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
1696
|
unsubscribers.push(this.copilotSession.on("assistant.reasoning", (event) => {
|
|
1556
1697
|
currentReasoning = String(extractReasoningText(event?.data ?? event) || "").trim();
|
|
1557
1698
|
if (currentReasoning) {
|
|
@@ -1594,6 +1735,18 @@ export class ManagedSession {
|
|
|
1594
1735
|
setTimeout(() => reject(new Error("Turn timed out")), TURN_TIMEOUT);
|
|
1595
1736
|
})
|
|
1596
1737
|
: null;
|
|
1738
|
+
// Re-armable idle waiter used by the tool-call-as-text guard to wait for
|
|
1739
|
+
// the model's response to a mid-turn correction without tearing down the
|
|
1740
|
+
// event subscriptions set up above.
|
|
1741
|
+
const waitForNextIdle = () => new Promise((resolve) => {
|
|
1742
|
+
const unsub = this.copilotSession.on("session.idle", () => {
|
|
1743
|
+
flushStreamingProgress(true);
|
|
1744
|
+
publishReasoningSnapshot("session.idle", true);
|
|
1745
|
+
unsub();
|
|
1746
|
+
resolve();
|
|
1747
|
+
});
|
|
1748
|
+
unsubscribers.push(unsub);
|
|
1749
|
+
});
|
|
1597
1750
|
const effectivePrompt = opts?.requiredTool
|
|
1598
1751
|
? [
|
|
1599
1752
|
`[SYSTEM: For this request, you MUST invoke the tool "${opts.requiredTool}" before giving your answer.`,
|
|
@@ -1618,6 +1771,61 @@ export class ManagedSession {
|
|
|
1618
1771
|
else {
|
|
1619
1772
|
await turnComplete;
|
|
1620
1773
|
}
|
|
1774
|
+
// ── Guard: tool call emitted as text instead of executed ──────────
|
|
1775
|
+
// If the model typed a tool call as `<invoke .../>` text rather than
|
|
1776
|
+
// calling it, that call did not run. Re-prompt it (bounded) to
|
|
1777
|
+
// actually invoke the tool so a consequential call is never silently
|
|
1778
|
+
// dropped. If a control tool already scheduled a turn boundary, we
|
|
1779
|
+
// still return an error rather than accepting a transcript that
|
|
1780
|
+
// implied another unexecuted tool side effect.
|
|
1781
|
+
let textToolCallCorrections = 0;
|
|
1782
|
+
while (textToolCallCorrections < MAX_TEXT_TOOL_CALL_CORRECTIONS &&
|
|
1783
|
+
!hasTerminalTurnBoundary(turnState) &&
|
|
1784
|
+
textEmittedToolCallRef.current) {
|
|
1785
|
+
const offendingTool = textEmittedToolCallRef.current.toolName;
|
|
1786
|
+
const rawContent = textEmittedToolCallRef.current.rawContent;
|
|
1787
|
+
textEmittedToolCallRef.current = null;
|
|
1788
|
+
textToolCallCorrections++;
|
|
1789
|
+
const diagnostic = {
|
|
1790
|
+
eventType: "runtime.tool_call_as_text",
|
|
1791
|
+
data: { toolName: offendingTool, rawContent, attempt: textToolCallCorrections, sessionId: this.sessionId },
|
|
1792
|
+
};
|
|
1793
|
+
collectedEvents.push(diagnostic);
|
|
1794
|
+
if (opts?.onEvent) {
|
|
1795
|
+
try {
|
|
1796
|
+
opts.onEvent(diagnostic);
|
|
1797
|
+
}
|
|
1798
|
+
catch { }
|
|
1799
|
+
}
|
|
1800
|
+
finalContent = undefined;
|
|
1801
|
+
const nextIdle = waitForNextIdle();
|
|
1802
|
+
await this.copilotSession.send({ prompt: buildTextEmittedToolCallCorrection(offendingTool) });
|
|
1803
|
+
if (timeoutPromise) {
|
|
1804
|
+
await Promise.race([nextIdle, timeoutPromise]);
|
|
1805
|
+
}
|
|
1806
|
+
else {
|
|
1807
|
+
await nextIdle;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (textEmittedToolCallRef.current) {
|
|
1811
|
+
const diagnostic = {
|
|
1812
|
+
eventType: "runtime.tool_call_as_text",
|
|
1813
|
+
data: { toolName: textEmittedToolCallRef.current.toolName, rawContent: textEmittedToolCallRef.current.rawContent, final: true, sessionId: this.sessionId },
|
|
1814
|
+
};
|
|
1815
|
+
collectedEvents.push(diagnostic);
|
|
1816
|
+
if (opts?.onEvent) {
|
|
1817
|
+
try {
|
|
1818
|
+
opts.onEvent(diagnostic);
|
|
1819
|
+
}
|
|
1820
|
+
catch { }
|
|
1821
|
+
}
|
|
1822
|
+
const message = buildTextEmittedToolCallCorrection(textEmittedToolCallRef.current.toolName);
|
|
1823
|
+
return {
|
|
1824
|
+
type: "error",
|
|
1825
|
+
message,
|
|
1826
|
+
events: collectedEvents,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1621
1829
|
}
|
|
1622
1830
|
catch (err) {
|
|
1623
1831
|
// Timeout — kill it
|
|
@@ -1648,6 +1856,8 @@ export class ManagedSession {
|
|
|
1648
1856
|
const combinedQueuedActions = [...turnState.queuedActions, ...remainingActions];
|
|
1649
1857
|
const queuedActions = combinedQueuedActions.length > 0 ? combinedQueuedActions : undefined;
|
|
1650
1858
|
switch (firstAction.type) {
|
|
1859
|
+
case "completed":
|
|
1860
|
+
return { ...firstAction, events: collectedEvents, queuedActions };
|
|
1651
1861
|
case "input_required":
|
|
1652
1862
|
return { ...firstAction, events: collectedEvents, queuedActions };
|
|
1653
1863
|
case "wait":
|
|
@@ -1723,6 +1933,8 @@ export class ManagedSession {
|
|
|
1723
1933
|
updateConfig(config) {
|
|
1724
1934
|
if (config.model !== undefined)
|
|
1725
1935
|
this.config.model = config.model;
|
|
1936
|
+
if (config.reasoningEffort !== undefined)
|
|
1937
|
+
this.config.reasoningEffort = config.reasoningEffort;
|
|
1726
1938
|
if (config.tools !== undefined)
|
|
1727
1939
|
this.config.tools = config.tools;
|
|
1728
1940
|
if (config.systemMessage !== undefined)
|
|
@@ -1732,6 +1944,16 @@ export class ManagedSession {
|
|
|
1732
1944
|
if (config.waitThreshold !== undefined)
|
|
1733
1945
|
this.config.waitThreshold = config.waitThreshold;
|
|
1734
1946
|
}
|
|
1947
|
+
requiresModelRebind(config) {
|
|
1948
|
+
const currentModel = this.config.model;
|
|
1949
|
+
const nextModel = config.model ?? this.config.model;
|
|
1950
|
+
const currentReasoningEffort = this.config.reasoningEffort ?? null;
|
|
1951
|
+
const nextReasoningEffort = config.reasoningEffort !== undefined
|
|
1952
|
+
? config.reasoningEffort ?? null
|
|
1953
|
+
: this.config.reasoningEffort ?? null;
|
|
1954
|
+
return Boolean((currentModel || nextModel)
|
|
1955
|
+
&& (currentModel !== nextModel || currentReasoningEffort !== nextReasoningEffort));
|
|
1956
|
+
}
|
|
1735
1957
|
/** Get the underlying CopilotSession (for direct access when needed). */
|
|
1736
1958
|
getCopilotSession() {
|
|
1737
1959
|
return this.copilotSession;
|