pilotswarm-sdk 0.3.0 → 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.
Files changed (102) hide show
  1. package/dist/cms-migrations.d.ts.map +1 -1
  2. package/dist/cms-migrations.js +840 -1
  3. package/dist/cms-migrations.js.map +1 -1
  4. package/dist/cms.d.ts +31 -1
  5. package/dist/cms.d.ts.map +1 -1
  6. package/dist/cms.js +46 -1
  7. package/dist/cms.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/inspect-tools.d.ts.map +1 -1
  11. package/dist/inspect-tools.js +27 -0
  12. package/dist/inspect-tools.js.map +1 -1
  13. package/dist/managed-session.d.ts +1 -0
  14. package/dist/managed-session.d.ts.map +1 -1
  15. package/dist/managed-session.js +243 -21
  16. package/dist/managed-session.js.map +1 -1
  17. package/dist/management-client.d.ts +14 -1
  18. package/dist/management-client.d.ts.map +1 -1
  19. package/dist/management-client.js +49 -1
  20. package/dist/management-client.js.map +1 -1
  21. package/dist/orchestration/index.d.ts +2 -2
  22. package/dist/orchestration/index.js +1 -1
  23. package/dist/orchestration/lifecycle.d.ts.map +1 -1
  24. package/dist/orchestration/lifecycle.js +74 -4
  25. package/dist/orchestration/lifecycle.js.map +1 -1
  26. package/dist/orchestration/runtime.d.ts +1 -1
  27. package/dist/orchestration/state.d.ts +1 -0
  28. package/dist/orchestration/state.d.ts.map +1 -1
  29. package/dist/orchestration/state.js +1 -0
  30. package/dist/orchestration/state.js.map +1 -1
  31. package/dist/orchestration/turn.d.ts +11 -0
  32. package/dist/orchestration/turn.d.ts.map +1 -1
  33. package/dist/orchestration/turn.js +61 -4
  34. package/dist/orchestration/turn.js.map +1 -1
  35. package/dist/orchestration-registry.d.ts.map +1 -1
  36. package/dist/orchestration-registry.js +4 -2
  37. package/dist/orchestration-registry.js.map +1 -1
  38. package/dist/orchestration-version.d.ts +1 -1
  39. package/dist/orchestration-version.js +1 -1
  40. package/dist/orchestration.d.ts +2 -2
  41. package/dist/orchestration.js +1 -1
  42. package/dist/orchestration_1_0_46.d.ts +1 -1
  43. package/dist/orchestration_1_0_47.d.ts +1 -1
  44. package/dist/orchestration_1_0_48.d.ts +1 -1
  45. package/dist/orchestration_1_0_49.d.ts +1 -1
  46. package/dist/orchestration_1_0_50.d.ts +1 -1
  47. package/dist/orchestration_1_0_54/agents.d.ts +41 -0
  48. package/dist/orchestration_1_0_54/agents.d.ts.map +1 -0
  49. package/dist/orchestration_1_0_54/agents.js +758 -0
  50. package/dist/orchestration_1_0_54/agents.js.map +1 -0
  51. package/dist/orchestration_1_0_54/index.d.ts +24 -0
  52. package/dist/orchestration_1_0_54/index.d.ts.map +1 -0
  53. package/dist/orchestration_1_0_54/index.js +13 -0
  54. package/dist/orchestration_1_0_54/index.js.map +1 -0
  55. package/dist/orchestration_1_0_54/lifecycle.d.ts +41 -0
  56. package/dist/orchestration_1_0_54/lifecycle.d.ts.map +1 -0
  57. package/dist/orchestration_1_0_54/lifecycle.js +510 -0
  58. package/dist/orchestration_1_0_54/lifecycle.js.map +1 -0
  59. package/dist/orchestration_1_0_54/queue.d.ts +7 -0
  60. package/dist/orchestration_1_0_54/queue.d.ts.map +1 -0
  61. package/dist/orchestration_1_0_54/queue.js +644 -0
  62. package/dist/orchestration_1_0_54/queue.js.map +1 -0
  63. package/dist/orchestration_1_0_54/runtime.d.ts +29 -0
  64. package/dist/orchestration_1_0_54/runtime.d.ts.map +1 -0
  65. package/dist/orchestration_1_0_54/runtime.js +194 -0
  66. package/dist/orchestration_1_0_54/runtime.js.map +1 -0
  67. package/dist/orchestration_1_0_54/state.d.ts +125 -0
  68. package/dist/orchestration_1_0_54/state.d.ts.map +1 -0
  69. package/dist/orchestration_1_0_54/state.js +105 -0
  70. package/dist/orchestration_1_0_54/state.js.map +1 -0
  71. package/dist/orchestration_1_0_54/turn.d.ts +6 -0
  72. package/dist/orchestration_1_0_54/turn.d.ts.map +1 -0
  73. package/dist/orchestration_1_0_54/turn.js +864 -0
  74. package/dist/orchestration_1_0_54/turn.js.map +1 -0
  75. package/dist/orchestration_1_0_54/utils.d.ts +22 -0
  76. package/dist/orchestration_1_0_54/utils.d.ts.map +1 -0
  77. package/dist/orchestration_1_0_54/utils.js +226 -0
  78. package/dist/orchestration_1_0_54/utils.js.map +1 -0
  79. package/dist/session-manager.d.ts +4 -0
  80. package/dist/session-manager.d.ts.map +1 -1
  81. package/dist/session-manager.js +22 -0
  82. package/dist/session-manager.js.map +1 -1
  83. package/dist/session-proxy.d.ts +12 -0
  84. package/dist/session-proxy.d.ts.map +1 -1
  85. package/dist/session-proxy.js +137 -13
  86. package/dist/session-proxy.js.map +1 -1
  87. package/dist/sweeper-tools.d.ts +1 -1
  88. package/dist/sweeper-tools.d.ts.map +1 -1
  89. package/dist/sweeper-tools.js +153 -92
  90. package/dist/sweeper-tools.js.map +1 -1
  91. package/dist/system-agents.d.ts +2 -0
  92. package/dist/system-agents.d.ts.map +1 -1
  93. package/dist/system-agents.js +3 -0
  94. package/dist/system-agents.js.map +1 -1
  95. package/dist/types.d.ts +14 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/types.js.map +1 -1
  98. package/package.json +3 -2
  99. package/plugins/mgmt/agents/agent-tuner.agent.md +36 -2
  100. package/plugins/mgmt/agents/sweeper.agent.md +7 -4
  101. package/plugins/mgmt/skills/sweeper/SKILL.md +4 -1
  102. package/plugins/system/agents/default.agent.md +3 -1
@@ -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='quiet' when nothing material changed, status='material' when the parent should be notified, " +
290
- "and status='blocked' when the cycle found a blocker or failure that needs parent attention. " +
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='quiet' when nothing material changed, status='material' when the parent should be notified, " +
642
- "and status='blocked' when the cycle found a blocker or failure that needs parent attention. " +
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 opts?.modelSummary || "No model providers configured.";
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;