comisai 1.0.25 → 1.0.27

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 (145) hide show
  1. package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +130 -10
  2. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +1 -1
  3. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +9 -2
  4. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +8 -0
  5. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +2 -0
  6. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +29 -0
  7. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +242 -2
  8. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.d.ts +210 -0
  9. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.js +566 -0
  10. package/node_modules/@comis/agent/dist/context-engine/context-engine.js +8 -6
  11. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -30
  12. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +109 -36
  13. package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +5 -1
  14. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +22 -20
  15. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +2 -0
  16. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +111 -15
  17. package/node_modules/@comis/agent/dist/executor/executor-response-filter.d.ts +20 -17
  18. package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +132 -52
  19. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +16 -3
  20. package/node_modules/@comis/agent/dist/executor/model-retry.d.ts +14 -0
  21. package/node_modules/@comis/agent/dist/executor/model-retry.js +72 -1
  22. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +3 -0
  23. package/node_modules/@comis/agent/dist/executor/pi-executor.js +68 -9
  24. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.d.ts +82 -0
  25. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.js +200 -0
  26. package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +1 -9
  27. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +37 -2
  28. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +45 -3
  29. package/node_modules/@comis/agent/dist/executor/tool-parallelism.js +0 -1
  30. package/node_modules/@comis/agent/dist/executor/types.d.ts +11 -2
  31. package/node_modules/@comis/agent/dist/index.d.ts +3 -1
  32. package/node_modules/@comis/agent/dist/index.js +2 -0
  33. package/node_modules/@comis/agent/dist/model/last-known-model.d.ts +36 -0
  34. package/node_modules/@comis/agent/dist/model/last-known-model.js +49 -0
  35. package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +16 -4
  36. package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +65 -21
  37. package/node_modules/@comis/agent/dist/planner/types.d.ts +0 -2
  38. package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +10 -0
  39. package/node_modules/@comis/agent/dist/session/comis-session-manager.js +5 -0
  40. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +7 -0
  41. package/node_modules/@comis/agent/package.json +1 -1
  42. package/node_modules/@comis/channels/package.json +1 -1
  43. package/node_modules/@comis/cli/dist/client/rpc-client.js +6 -1
  44. package/node_modules/@comis/cli/dist/commands/doctor.js +5 -3
  45. package/node_modules/@comis/cli/dist/commands/health.js +5 -2
  46. package/node_modules/@comis/cli/dist/wizard/json-output.js +7 -3
  47. package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +130 -0
  48. package/node_modules/@comis/cli/package.json +1 -1
  49. package/node_modules/@comis/core/dist/config/immutable-keys.d.ts +2 -2
  50. package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
  51. package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
  52. package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
  53. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
  54. package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
  55. package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
  56. package/node_modules/@comis/core/dist/config/schema.js +14 -0
  57. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
  58. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
  59. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  60. package/node_modules/@comis/core/dist/exports/config.js +1 -1
  61. package/node_modules/@comis/core/package.json +1 -1
  62. package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
  63. package/node_modules/@comis/daemon/dist/daemon.js +42 -0
  64. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
  65. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
  66. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
  67. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
  68. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
  69. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
  70. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
  71. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
  72. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
  73. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
  74. package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
  75. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
  76. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
  77. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
  78. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
  79. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
  80. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
  81. package/node_modules/@comis/daemon/package.json +1 -1
  82. package/node_modules/@comis/gateway/package.json +1 -1
  83. package/node_modules/@comis/infra/dist/index.d.ts +1 -0
  84. package/node_modules/@comis/infra/dist/index.js +2 -0
  85. package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
  86. package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
  87. package/node_modules/@comis/infra/package.json +1 -1
  88. package/node_modules/@comis/memory/package.json +1 -1
  89. package/node_modules/@comis/scheduler/package.json +1 -1
  90. package/node_modules/@comis/shared/package.json +1 -1
  91. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
  92. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
  93. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
  94. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
  95. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
  96. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
  97. package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
  98. package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
  99. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
  100. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
  101. package/node_modules/@comis/skills/dist/index.d.ts +1 -1
  102. package/node_modules/@comis/skills/dist/index.js +2 -2
  103. package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
  104. package/node_modules/@comis/skills/package.json +1 -1
  105. package/node_modules/@comis/web/dist/assets/{agent-detail-ru-AhppM.js → agent-detail-DqL6Artv.js} +1 -1
  106. package/node_modules/@comis/web/dist/assets/{agent-editor-hjwRuFVp.js → agent-editor-CNM_h94Y.js} +1 -1
  107. package/node_modules/@comis/web/dist/assets/{agent-list-6Uotjatr.js → agent-list-Dbh-xD_F.js} +1 -1
  108. package/node_modules/@comis/web/dist/assets/{billing-view-CxysXH0p.js → billing-view-C1DmtyzK.js} +1 -1
  109. package/node_modules/@comis/web/dist/assets/{channel-detail-BBCKtmne.js → channel-detail-CtCH22N1.js} +1 -1
  110. package/node_modules/@comis/web/dist/assets/{channel-list-FkfeOLBQ.js → channel-list-C7xXn-60.js} +1 -1
  111. package/node_modules/@comis/web/dist/assets/{chat-console-BumBaIgO.js → chat-console-C51pjFwk.js} +1 -1
  112. package/node_modules/@comis/web/dist/assets/{config-editor-C9BSwHGy.js → config-editor-BLArYRB7.js} +1 -1
  113. package/node_modules/@comis/web/dist/assets/{context-dag-browser-BHm00mJD.js → context-dag-browser-fuyMinNI.js} +1 -1
  114. package/node_modules/@comis/web/dist/assets/{context-engine-BENY3pWE.js → context-engine-Bngf2bH0.js} +1 -1
  115. package/node_modules/@comis/web/dist/assets/{delivery-view-BCnkPsAp.js → delivery-view-C80hucxX.js} +1 -1
  116. package/node_modules/@comis/web/dist/assets/{diagnostics-view-C_jQFG2H.js → diagnostics-view-Cl4VbHZ6.js} +1 -1
  117. package/node_modules/@comis/web/dist/assets/{ic-chat-message-FdQcZsSQ.js → ic-chat-message-ByFUoMm6.js} +1 -1
  118. package/node_modules/@comis/web/dist/assets/{ic-connection-dot-BgYiK2N4.js → ic-connection-dot-C4nDHgY2.js} +1 -1
  119. package/node_modules/@comis/web/dist/assets/{ic-tool-call-DMPHsLyx.js → ic-tool-call-Bh5kq-yY.js} +1 -1
  120. package/node_modules/@comis/web/dist/assets/{index-FLPhHz8p.js → index-BBkuC-EU.js} +2 -2
  121. package/node_modules/@comis/web/dist/assets/{mcp-management-5jyScQis.js → mcp-management-DB-phOo7.js} +1 -1
  122. package/node_modules/@comis/web/dist/assets/{media-config-J9oT9PPs.js → media-config-CRqZ1ZUH.js} +1 -1
  123. package/node_modules/@comis/web/dist/assets/{media-test-DGTCtM8-.js → media-test-C9vE20Oy.js} +1 -1
  124. package/node_modules/@comis/web/dist/assets/{memory-inspector-D5Re9ptG.js → memory-inspector-CeqfnxMZ.js} +1 -1
  125. package/node_modules/@comis/web/dist/assets/{message-center-cRLK6ZmG.js → message-center-Daup7Mof.js} +1 -1
  126. package/node_modules/@comis/web/dist/assets/{models-D5vu07MR.js → models-DLYnEU8E.js} +1 -1
  127. package/node_modules/@comis/web/dist/assets/{observe-view-CalNNEmd.js → observe-view-BTSt_PO5.js} +1 -1
  128. package/node_modules/@comis/web/dist/assets/{pipeline-builder-DUYDGwZf.js → pipeline-builder-DknfzyLt.js} +1 -1
  129. package/node_modules/@comis/web/dist/assets/{pipeline-history-BAO8brOe.js → pipeline-history-JnHZdeU_.js} +1 -1
  130. package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-DectIoQt.js → pipeline-history-detail-Dg4knsEb.js} +1 -1
  131. package/node_modules/@comis/web/dist/assets/{pipeline-list-BHlaBKww.js → pipeline-list-AEnibjsp.js} +1 -1
  132. package/node_modules/@comis/web/dist/assets/{pipeline-monitor-BhtpNEHf.js → pipeline-monitor-DG7RbIOO.js} +1 -1
  133. package/node_modules/@comis/web/dist/assets/{scheduler-VafN_8xi.js → scheduler-uL1fYKAT.js} +1 -1
  134. package/node_modules/@comis/web/dist/assets/{security-QQXMRTlo.js → security-C3DywRLH.js} +1 -1
  135. package/node_modules/@comis/web/dist/assets/{session-detail-BpZ_8Yih.js → session-detail-BtqCNWXV.js} +1 -1
  136. package/node_modules/@comis/web/dist/assets/{session-list-DfCm8Cec.js → session-list-CJXWa2XT.js} +1 -1
  137. package/node_modules/@comis/web/dist/assets/{setup-wizard-C-z477CG.js → setup-wizard-ywn7oJvu.js} +1 -1
  138. package/node_modules/@comis/web/dist/assets/{skills-BCOGPf6s.js → skills-DX0KYnWD.js} +1 -1
  139. package/node_modules/@comis/web/dist/assets/{subagents-l-auUraL.js → subagents-B8p5YJEB.js} +1 -1
  140. package/node_modules/@comis/web/dist/assets/{workspace-manager-DlvBixiq.js → workspace-manager-CgzNIrw1.js} +1 -1
  141. package/node_modules/@comis/web/dist/index.html +1 -1
  142. package/node_modules/@comis/web/package.json +1 -1
  143. package/package.json +14 -14
  144. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
  145. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.js +0 -39
@@ -44,16 +44,27 @@ export declare function scanWithOutputGuard(params: {
44
44
  /**
45
45
  * When the final assistant message is thinking-only or a
46
46
  * silent token (NO_REPLY, HEARTBEAT_OK) but text was emitted in earlier
47
- * turns, walk backward through session messages to find the last assistant
48
- * message that contained visible text blocks.
47
+ * turns, recover a meaningful user-visible response.
49
48
  *
50
- * Two-pass strategy:
51
- * 1. Backward walk skipping tool-call turnsfinds the most recent
52
- * standalone response (text-only, no toolCall/tool_use blocks).
53
- * 2. Forward walk from userMessageIndex including tool-call turns — finds
54
- * the earliest pre-tool commentary, which is typically the framing
55
- * response (e.g. "I'm going to build..."), not a late step annotation
56
- * (e.g. "Step 4/4: sanity-testing...").
49
+ * Two-pass strategy (gated):
50
+ * 1. **Tool-call synthesis** (primary) if ≥1 prior assistant turn within the
51
+ * current execution window contains tool-call blocks, synthesize a
52
+ * structured `[comis: tool-call summary recovered ...]` reply listing each
53
+ * tool + primary identifying argument. This avoids surfacing earlier
54
+ * planning prose ("let me plan this out before building...") AS the final
55
+ * reply when the work was actually completed via tools.
56
+ * 2. **Standalone walk-backward** (fallback) — when zero prior tool calls were
57
+ * collected (pure-conversational case), preserve the original behavior of
58
+ * walking backward through messages to find the most recent assistant turn
59
+ * with visible text-only content (no tool calls).
60
+ *
61
+ * The synthesis-gate (a single early-return — see `tool-call-synthesis-gate`
62
+ * comment below) ensures the standalone walk only fires when no tool calls
63
+ * were observed; this keeps the pass selection mutually exclusive.
64
+ *
65
+ * Suppressed when a delivery tool (`message`, `notify`) was used — the agent
66
+ * already delivered content via side-channel and the silent final token is
67
+ * intentional.
57
68
  *
58
69
  * Returns the recovered text, or the original response if no recovery needed.
59
70
  */
@@ -81,11 +92,3 @@ export declare function extractExecutionPlan(params: {
81
92
  eventBus: TypedEventBus;
82
93
  logger: ComisLogger;
83
94
  }): ExecutionPlan | undefined;
84
- /**
85
- * Generate a completeness nudge when the LLM stopped but steps remain.
86
- * Returns the nudge text or undefined if no nudge is needed.
87
- */
88
- export declare function generateCompletenessNudge(params: {
89
- plan: ExecutionPlan;
90
- verificationNudge: boolean;
91
- }): string | undefined;
@@ -13,7 +13,6 @@
13
13
  * @module
14
14
  */
15
15
  import { extractPlanFromResponse } from "../planner/plan-extractor.js";
16
- import { formatChecklistForInjection } from "../planner/checklist-formatter.js";
17
16
  import { stripReasoningTagsFromText } from "../response-filter/reasoning-tags.js";
18
17
  import { isVisibleTextBlock } from "./phase-filter.js";
19
18
  /**
@@ -93,16 +92,27 @@ const DELIVERY_TOOL_NAMES = ["message", "notify"];
93
92
  /**
94
93
  * When the final assistant message is thinking-only or a
95
94
  * silent token (NO_REPLY, HEARTBEAT_OK) but text was emitted in earlier
96
- * turns, walk backward through session messages to find the last assistant
97
- * message that contained visible text blocks.
95
+ * turns, recover a meaningful user-visible response.
98
96
  *
99
- * Two-pass strategy:
100
- * 1. Backward walk skipping tool-call turnsfinds the most recent
101
- * standalone response (text-only, no toolCall/tool_use blocks).
102
- * 2. Forward walk from userMessageIndex including tool-call turns — finds
103
- * the earliest pre-tool commentary, which is typically the framing
104
- * response (e.g. "I'm going to build..."), not a late step annotation
105
- * (e.g. "Step 4/4: sanity-testing...").
97
+ * Two-pass strategy (gated):
98
+ * 1. **Tool-call synthesis** (primary) if ≥1 prior assistant turn within the
99
+ * current execution window contains tool-call blocks, synthesize a
100
+ * structured `[comis: tool-call summary recovered ...]` reply listing each
101
+ * tool + primary identifying argument. This avoids surfacing earlier
102
+ * planning prose ("let me plan this out before building...") AS the final
103
+ * reply when the work was actually completed via tools.
104
+ * 2. **Standalone walk-backward** (fallback) — when zero prior tool calls were
105
+ * collected (pure-conversational case), preserve the original behavior of
106
+ * walking backward through messages to find the most recent assistant turn
107
+ * with visible text-only content (no tool calls).
108
+ *
109
+ * The synthesis-gate (a single early-return — see `tool-call-synthesis-gate`
110
+ * comment below) ensures the standalone walk only fires when no tool calls
111
+ * were observed; this keeps the pass selection mutually exclusive.
112
+ *
113
+ * Suppressed when a delivery tool (`message`, `notify`) was used — the agent
114
+ * already delivered content via side-channel and the silent final token is
115
+ * intentional.
106
116
  *
107
117
  * Returns the recovered text, or the original response if no recovery needed.
108
118
  */
@@ -124,8 +134,55 @@ export function recoverEmptyFinalResponse(params) {
124
134
  return extractedResponse;
125
135
  }
126
136
  /* eslint-disable @typescript-eslint/no-explicit-any */
127
- // Pass 1: backward walk prefer the most recent standalone response
128
- // (assistant turns that have text but NO tool call blocks)
137
+ // Collect tool-call summaries from prior assistant turns within the
138
+ // current execution window (lowerBound .. messages.length).
139
+ //
140
+ // Note: blocks with non-string `name` are still summarized (the helper
141
+ // renders them as "unknown_tool") but are NOT added to `toolNamesSet`.
142
+ // Consequence: a batch of purely malformed blocks emits `toolNames: []`
143
+ // in the INFO log while `toolCallCount` reflects the bullet count. This
144
+ // is intentional — `toolNames` is a deduplicated set of well-typed
145
+ // identifiers for log aggregation, not a per-bullet identifier list.
146
+ const toolCallSummaries = [];
147
+ const toolNamesSet = new Set();
148
+ for (let i = lowerBound; i < messages.length; i++) {
149
+ const msg = messages[i]; // eslint-disable-line security/detect-object-injection
150
+ if (msg?.role !== "assistant" || !Array.isArray(msg.content))
151
+ continue;
152
+ for (const block of msg.content) {
153
+ if (block?.type === "toolCall" || block?.type === "tool_use") {
154
+ toolCallSummaries.push(summarizeToolCall(block));
155
+ // Only well-typed names enter the set — malformed blocks are still
156
+ // summarized as "unknown_tool" but excluded from toolNames.
157
+ if (typeof block?.name === "string")
158
+ toolNamesSet.add(block.name);
159
+ }
160
+ }
161
+ }
162
+ // Synthesis-only-when-tool-calls contract (grep anchor: "tool-call-synthesis-gate"):
163
+ // Returning here is the ONE place that prevents the `standalone` walk-backward
164
+ // (below) from ever firing alongside synthesis. Do not add code paths
165
+ // that fall through to standalone after toolCallSummaries are non-empty.
166
+ if (toolCallSummaries.length > 0) {
167
+ const bullets = toolCallSummaries.map(s => ` • ${s}`).join("\n");
168
+ const synthesis = `[comis: tool-call summary recovered from successful operations — the assistant's final message was empty]\n` +
169
+ `Completed ${toolCallSummaries.length} tool call${toolCallSummaries.length === 1 ? "" : "s"} in this batch:\n` +
170
+ `${bullets}\n` +
171
+ `The work was done; the assistant did not summarize. Please ask "what did you do?" if details are needed.`;
172
+ logger.info({
173
+ module: "agent.executor.empty-turn-recovery",
174
+ recoveryPass: "tool-call-synthesis",
175
+ toolCallCount: toolCallSummaries.length,
176
+ toolNames: [...toolNamesSet],
177
+ synthesisLength: synthesis.length,
178
+ hint: "Final assistant message was empty after tool batch; synthesized completion summary from tool-call history.",
179
+ }, "Empty-turn recovery: synthesized from tool-call history");
180
+ return synthesis; // tool-call-synthesis-gate — see comment above.
181
+ }
182
+ // Standalone walk-backward (pure-conversational fallback): reachable
183
+ // ONLY when toolCallSummaries.length === 0, guaranteed by the early-
184
+ // return above. Do NOT wrap in an additional conditional — the single
185
+ // gate above is the contract anchor.
129
186
  for (let i = messages.length - 1; i >= lowerBound; i--) {
130
187
  const msg = messages[i]; // eslint-disable-line security/detect-object-injection
131
188
  if (msg?.role === "assistant" && Array.isArray(msg.content)) {
@@ -145,25 +202,6 @@ export function recoverEmptyFinalResponse(params) {
145
202
  }
146
203
  }
147
204
  }
148
- // Pass 2: forward walk — fall back to the earliest pre-tool commentary.
149
- // Walking forward prefers the framing/introduction message over late
150
- // step annotations (e.g. "I'm going to build..." over "Step 4/4: ...").
151
- for (let i = lowerBound; i < messages.length; i++) {
152
- const msg = messages[i]; // eslint-disable-line security/detect-object-injection
153
- if (msg?.role === "assistant" && Array.isArray(msg.content)) {
154
- const recovered = extractVisibleText(msg.content);
155
- if (recovered) {
156
- logger.info({
157
- hint: "Final assistant message was empty or silent-token-only; recovered pre-tool commentary from earlier turn",
158
- errorKind: "transient",
159
- turnIndex: i,
160
- recoveredLength: recovered.length,
161
- recoveryPass: "pre-tool-commentary",
162
- }, "recovered visible text from earlier turn");
163
- return recovered;
164
- }
165
- }
166
- }
167
205
  /* eslint-enable @typescript-eslint/no-explicit-any */
168
206
  }
169
207
  }
@@ -205,6 +243,69 @@ function hasDeliveryToolCall(messages, lowerBound) {
205
243
  return false;
206
244
  }
207
245
  /* eslint-enable @typescript-eslint/no-explicit-any */
246
+ /** Summarize a single tool-call content block as `toolName({primary_arg: "value"})`.
247
+ * Reads `name` from the block, and `input` (Anthropic native) or `arguments`
248
+ * (internal mapped convention) for args. Returns bare tool name on malformed
249
+ * input — never throws. */
250
+ /* eslint-disable @typescript-eslint/no-explicit-any */
251
+ function summarizeToolCall(call) {
252
+ const name = typeof call?.name === "string" ? call.name : "unknown_tool";
253
+ // Both Anthropic native (`input`) and internal mapped (`arguments`) shapes.
254
+ const args = (call?.input && typeof call.input === "object" ? call.input : undefined) ??
255
+ (call?.arguments && typeof call.arguments === "object" ? call.arguments : undefined);
256
+ if (!args)
257
+ return name;
258
+ switch (name) {
259
+ case "agents_manage": {
260
+ const action = typeof args.action === "string" ? args.action : undefined;
261
+ const agentId = typeof args.agent_id === "string" ? args.agent_id : undefined;
262
+ if (action && agentId)
263
+ return `agents_manage.${action}({agent_id: "${agentId}"})`;
264
+ if (action)
265
+ return `agents_manage.${action}`;
266
+ return "agents_manage";
267
+ }
268
+ case "write":
269
+ case "edit":
270
+ case "read": {
271
+ const p = typeof args.path === "string" ? args.path : undefined;
272
+ return p ? `${name}({path: "${p}"})` : name;
273
+ }
274
+ case "gateway": {
275
+ const action = typeof args.action === "string" ? args.action : undefined;
276
+ const section = typeof args.section === "string" ? args.section : undefined;
277
+ if (action && section)
278
+ return `gateway({action: "${action}", section: "${section}"})`;
279
+ if (action)
280
+ return `gateway({action: "${action}"})`;
281
+ return "gateway";
282
+ }
283
+ case "exec": {
284
+ const cmd = typeof args.command === "string" ? args.command : undefined;
285
+ if (cmd) {
286
+ const preview = cmd.length > 60 ? `${cmd.slice(0, 60)}…` : cmd;
287
+ return `exec({command: "${preview}"})`;
288
+ }
289
+ return "exec";
290
+ }
291
+ case "pipeline": {
292
+ const pname = typeof args.name === "string" ? args.name : undefined;
293
+ return pname ? `pipeline({name: "${pname}"})` : "pipeline";
294
+ }
295
+ case "sessions_spawn": {
296
+ const agentId = typeof args.agent_id === "string" ? args.agent_id : undefined;
297
+ return agentId ? `sessions_spawn({agent_id: "${agentId}"})` : "sessions_spawn";
298
+ }
299
+ case "message":
300
+ case "notify": {
301
+ const action = typeof args.action === "string" ? args.action : undefined;
302
+ return action ? `${name}({action: "${action}"})` : name;
303
+ }
304
+ default:
305
+ return name;
306
+ }
307
+ }
308
+ /* eslint-enable @typescript-eslint/no-explicit-any */
208
309
  // ---------------------------------------------------------------------------
209
310
  // SEP plan extraction (extracted from execute() success path)
210
311
  // ---------------------------------------------------------------------------
@@ -221,7 +322,6 @@ export function extractExecutionPlan(params) {
221
322
  request: messageText.slice(0, 200),
222
323
  steps,
223
324
  completedCount: 0,
224
- nudged: false,
225
325
  createdAtMs: Date.now(),
226
326
  };
227
327
  logger.info({ agentId, stepCount: steps.length, durationMs: Date.now() - executionStartMs }, "SEP plan extracted");
@@ -235,23 +335,3 @@ export function extractExecutionPlan(params) {
235
335
  }
236
336
  return undefined;
237
337
  }
238
- // ---------------------------------------------------------------------------
239
- // SEP completeness nudge (extracted from execute() success path)
240
- // ---------------------------------------------------------------------------
241
- /**
242
- * Generate a completeness nudge when the LLM stopped but steps remain.
243
- * Returns the nudge text or undefined if no nudge is needed.
244
- */
245
- export function generateCompletenessNudge(params) {
246
- const { plan, verificationNudge } = params;
247
- if (!plan.active || plan.nudged)
248
- return undefined;
249
- const remaining = plan.steps.filter(s => s.status === "pending" || s.status === "in_progress");
250
- if (remaining.length > 0 && plan.completedCount > 0) {
251
- const checklist = formatChecklistForInjection(plan, verificationNudge);
252
- return checklist
253
- ? `${checklist}\n\nPlease continue with the remaining steps. If any step is no longer needed, explain why.`
254
- : `You indicated completion but ${remaining.length} step(s) remain:\n${remaining.map(s => `- ${s.description}`).join("\n")}\nPlease continue. If these steps are no longer needed, explain why.`;
255
- }
256
- return undefined;
257
- }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { SettingsManager, } from "@mariozechner/pi-coding-agent";
17
17
  import { formatSessionKey, } from "@comis/core";
18
- import { applyToolDeferral, buildDeferredToolsContext, createDiscoverTool, createAutoDiscoveryStubs, extractRecentlyUsedToolNames, resolveModelTier, CORE_TOOLS } from "./tool-deferral.js";
18
+ import { applyToolDeferral, buildDeferredToolsContext, createDiscoverTool, createAutoDiscoveryStubs, extractRecentlyUsedToolNames, resolveModelTier, supportsToolSearch, CORE_TOOLS } from "./tool-deferral.js";
19
19
  import { getOrCreateDiscoveryTracker } from "./discovery-tracker.js";
20
20
  import { getOrCreateTracker, DEFAULT_LIFECYCLE_CONFIG } from "./tool-lifecycle.js";
21
21
  import { isAnthropicFamily, isGoogleFamily } from "../provider/capabilities.js";
@@ -320,10 +320,23 @@ export async function assembleTools(params) {
320
320
  const stubs = createAutoDiscoveryStubs(deferralResult.deferredEntries, discoveryTracker, deps.logger);
321
321
  mergedCustomTools.push(...stubs);
322
322
  }
323
- // Build deferred context for dynamic preamble injection
323
+ // Build deferred context for dynamic preamble injection.
324
+ //
325
+ // 260428-oyc: under Anthropic Sonnet/Opus 4.x, request-body-injector.ts
326
+ // strips client-side `discover_tools` from the API payload and replaces it
327
+ // with the server-side `tool_search_tool_regex` + per-tool `defer_loading`
328
+ // flag. Pass `useToolSearch=true` so the preamble teaches the model that
329
+ // deferred tools auto-load on first direct invocation, rather than telling
330
+ // it to call a tool the model can no longer see.
331
+ //
332
+ // resolvedModel is in scope here (param of assembleToolsForAgent, see
333
+ // function signature ~line 143). When undefined (test paths / fallback),
334
+ // useToolSearch defaults to false, preserving backward-compatible
335
+ // discover_tools wording.
324
336
  let deferredContext = "";
325
337
  if (deferralResult.deferredEntries.length > 0) {
326
- deferredContext = buildDeferredToolsContext(deferralResult.deferredEntries);
338
+ const useToolSearch = supportsToolSearch(resolvedModel?.id ?? "");
339
+ deferredContext = buildDeferredToolsContext(deferralResult.deferredEntries, { useToolSearch });
327
340
  }
328
341
  // -------------------------------------------------------------------
329
342
  // 8. JIT guide wrapping, schema pruning, snapshot, normalization, serializer
@@ -29,6 +29,7 @@ import type { TypedEventBus } from "@comis/core";
29
29
  import type { ComisLogger } from "@comis/infra";
30
30
  import type { AuthRotationAdapter } from "../model/auth-rotation-adapter.js";
31
31
  import type { ProviderHealthMonitor } from "../safety/provider-health-monitor.js";
32
+ import type { LastKnownModelTracker } from "../model/last-known-model.js";
32
33
  /** Parameters for the model failover pipeline (auth rotation + model fallback). */
33
34
  export interface ModelRetryParams {
34
35
  session: AgentSession;
@@ -54,6 +55,8 @@ export interface ModelRetryParams {
54
55
  sessionKey?: string;
55
56
  /** Optional provider health monitor for failure aggregation. */
56
57
  providerHealth?: ProviderHealthMonitor;
58
+ /** Optional last-known-working model tracker for auth-failure fallback. */
59
+ lastKnownModel?: LastKnownModelTracker;
57
60
  /** Callback to receive the resetTimer function from the resettable prompt timeout. */
58
61
  onResetTimer?: (resetFn: () => void) => void;
59
62
  };
@@ -62,6 +65,11 @@ export interface ModelRetryParams {
62
65
  export interface ModelRetryResult {
63
66
  succeeded: boolean;
64
67
  error?: unknown;
68
+ /** The model that ultimately succeeded (primary, fallback, or LKW). */
69
+ effectiveModel?: {
70
+ provider: string;
71
+ model: string;
72
+ };
65
73
  }
66
74
  /**
67
75
  * Parse a "provider:modelId" string into provider and modelId components.
@@ -71,6 +79,12 @@ export declare function parseModelString(modelStr: string): {
71
79
  provider: string;
72
80
  modelId: string;
73
81
  } | undefined;
82
+ /**
83
+ * Check whether an error is an authentication/authorization failure (401/403).
84
+ * Used to gate the last-known-working model fallback -- LKW only fires for
85
+ * auth errors, not for rate limits or transient failures.
86
+ */
87
+ export declare function isAuthError(error: unknown): boolean;
74
88
  /**
75
89
  * Execute a prompt with auth rotation and model failover.
76
90
  *
@@ -98,6 +98,23 @@ function parseRetryAfterMs(error) {
98
98
  return null;
99
99
  }
100
100
  // ---------------------------------------------------------------------------
101
+ // Auth error detection
102
+ // ---------------------------------------------------------------------------
103
+ /**
104
+ * Check whether an error is an authentication/authorization failure (401/403).
105
+ * Used to gate the last-known-working model fallback -- LKW only fires for
106
+ * auth errors, not for rate limits or transient failures.
107
+ */
108
+ export function isAuthError(error) {
109
+ const status = getErrorStatus(error);
110
+ if (status === 401 || status === 403)
111
+ return true;
112
+ if (error instanceof Error) {
113
+ return /invalid.?api.?key|authentication|unauthorized|401|403|permission.?denied/i.test(error.message);
114
+ }
115
+ return false;
116
+ }
117
+ // ---------------------------------------------------------------------------
101
118
  // Main function
102
119
  // ---------------------------------------------------------------------------
103
120
  /**
@@ -122,6 +139,7 @@ export async function runWithModelRetry(params) {
122
139
  const maxRetries = 1 + (authRotation?.hasProfiles(config.provider) ? 1 : 0) + fallbackModels.length;
123
140
  let promptError = undefined;
124
141
  let promptSucceeded = false;
142
+ let effectiveModel;
125
143
  try {
126
144
  // Primary prompt uses resettable timeout so tool completions can reset the
127
145
  // deadline. Retry/fallback paths use the original withPromptTimeout (fresh timeout).
@@ -133,6 +151,7 @@ export async function runWithModelRetry(params) {
133
151
  deps.onResetTimer?.(resettable.resetTimer);
134
152
  await resettable.promise;
135
153
  promptSucceeded = true;
154
+ effectiveModel = { provider: config.provider, model: config.model };
136
155
  // Record success for auth rotation cooldown tracking
137
156
  if (authRotation?.hasProfiles(config.provider)) {
138
157
  authRotation.recordSuccess(config.provider);
@@ -174,6 +193,7 @@ export async function runWithModelRetry(params) {
174
193
  await withPromptTimeout(session.prompt(messageText, { expandPromptTemplates: false, images: promptImages }), timeoutConfig.retryPromptTimeoutMs, () => session.abort());
175
194
  promptSucceeded = true;
176
195
  promptError = undefined;
196
+ effectiveModel = { provider: config.provider, model: config.model };
177
197
  // Record success for auth rotation tracking
178
198
  if (authRotation?.hasProfiles(config.provider)) {
179
199
  authRotation.recordSuccess(config.provider);
@@ -198,6 +218,7 @@ export async function runWithModelRetry(params) {
198
218
  await withPromptTimeout(session.prompt(messageText, { expandPromptTemplates: false, images: promptImages }), timeoutConfig.retryPromptTimeoutMs, () => session.abort());
199
219
  promptSucceeded = true;
200
220
  promptError = undefined;
221
+ effectiveModel = { provider: config.provider, model: config.model };
201
222
  authRotation.recordSuccess(config.provider);
202
223
  logger.info({ provider: config.provider }, "Retry with rotated key succeeded");
203
224
  }
@@ -257,6 +278,9 @@ export async function runWithModelRetry(params) {
257
278
  }), timeoutConfig.retryPromptTimeoutMs, () => session.abort());
258
279
  promptSucceeded = true;
259
280
  promptError = undefined;
281
+ if (parsed) {
282
+ effectiveModel = { provider: parsed.provider, model: parsed.modelId };
283
+ }
260
284
  logger.info({ fallbackModel: fallbackModelStr }, "Fallback model succeeded");
261
285
  break;
262
286
  }
@@ -296,6 +320,53 @@ export async function runWithModelRetry(params) {
296
320
  timestamp: Date.now(),
297
321
  });
298
322
  }
323
+ // Last-known-working model fallback: when all configured models fail
324
+ // with an auth error, try a model that recently succeeded somewhere
325
+ // on this daemon (per-agent first, then daemon-wide from a different provider).
326
+ if (!promptSucceeded && isAuthError(promptError) && deps.lastKnownModel) {
327
+ const lkw = deps.lastKnownModel.getLastKnown(deps.agentId ?? "") ??
328
+ deps.lastKnownModel.getAnyKnown(config.provider);
329
+ if (lkw && (lkw.provider !== config.provider || lkw.model !== config.model)) {
330
+ eventBus.emit("model:lkw_fallback_attempt", {
331
+ fromProvider: config.provider,
332
+ fromModel: config.model,
333
+ toProvider: lkw.provider,
334
+ toModel: lkw.model,
335
+ timestamp: Date.now(),
336
+ });
337
+ logger.info({ lkwProvider: lkw.provider, lkwModel: lkw.model }, "Attempting last-known-working model fallback");
338
+ try {
339
+ const normalizedLkw = normalizeModelId(lkw.provider, lkw.model);
340
+ const lkwModelObj = modelRegistry.find(lkw.provider, normalizedLkw.modelId);
341
+ if (lkwModelObj) {
342
+ await session.setModel(lkwModelObj);
343
+ }
344
+ await withPromptTimeout(session.prompt(messageText, {
345
+ expandPromptTemplates: false,
346
+ images: promptImages,
347
+ }), timeoutConfig.retryPromptTimeoutMs, () => session.abort());
348
+ promptSucceeded = true;
349
+ promptError = undefined;
350
+ effectiveModel = { provider: lkw.provider, model: lkw.model };
351
+ eventBus.emit("model:lkw_fallback_succeeded", {
352
+ provider: lkw.provider,
353
+ model: lkw.model,
354
+ timestamp: Date.now(),
355
+ });
356
+ logger.info({ lkwProvider: lkw.provider, lkwModel: lkw.model }, "Last-known-working model fallback succeeded");
357
+ }
358
+ catch (lkwError) {
359
+ promptError = lkwError;
360
+ logger.warn({
361
+ err: lkwError,
362
+ lkwProvider: lkw.provider,
363
+ lkwModel: lkw.model,
364
+ hint: "Last-known-working model also failed",
365
+ errorKind: "dependency",
366
+ }, "Last-known-working model fallback failed");
367
+ }
368
+ }
369
+ }
299
370
  }
300
- return { succeeded: promptSucceeded, error: promptError };
371
+ return { succeeded: promptSucceeded, error: promptError, effectiveModel };
301
372
  }
@@ -85,6 +85,8 @@ export interface PiExecutorDeps {
85
85
  circuitBreaker: CircuitBreaker;
86
86
  /** Optional provider health monitor for cross-agent pre-check. */
87
87
  providerHealth?: ProviderHealthMonitor;
88
+ /** Optional last-known-working model tracker for auth-failure fallback. */
89
+ lastKnownModel?: import("../model/last-known-model.js").LastKnownModelTracker;
88
90
  budgetGuard: BudgetGuard;
89
91
  costTracker: CostTracker;
90
92
  stepCounter: StepCounter;
@@ -92,6 +94,7 @@ export interface PiExecutorDeps {
92
94
  logger: ComisLogger;
93
95
  authStorage: AuthStorage;
94
96
  modelRegistry: ModelRegistry;
97
+ providerAliases?: Map<string, string>;
95
98
  sessionAdapter: ComisSessionManager;
96
99
  workspaceDir: string;
97
100
  customTools: ToolDefinition[];
@@ -30,6 +30,7 @@ import { createMessageSendLimiter } from "../safety/message-send-limiter.js";
30
30
  import { repairOrphanedMessages, scrubPoisonedThinkingBlocks } from "../session/orphaned-message-repair.js";
31
31
  import { scrubRedactedToolCalls } from "../session/scrub-redacted-tool-calls.js";
32
32
  import { createPiEventBridge } from "../bridge/pi-event-bridge.js";
33
+ import { assertThinkingBlocksUnchanged, restoreCanonicalThinkingBlocks } from "../bridge/thinking-block-hash-invariant.js";
33
34
  import { createAdaptiveCacheRetention, createStaticRetention } from "./adaptive-cache-retention.js";
34
35
  // SessionLatch types and createSessionLatch moved to executor-session-state.ts
35
36
  import { createContextWindowGuard } from "../safety/context-window-guard.js";
@@ -47,9 +48,7 @@ import { getDeliveredGuides, setDeliveredGuides, setBreakpointIndex,
47
48
  // deleteBreakpointIndex, getBreakpointIndexMapSize moved to executor-post-execution.ts
48
49
  setCacheWarm, clearSessionCacheWarm, clearSessionLatches, getCacheBreakDetector, setEvictionCooldown, decrementEvictionCooldown as decrementEvictionCooldownForSession, recordCacheSavings, getCacheSavings, clearSessionCacheSavings, } from "./executor-session-state.js";
49
50
  import { validateInput } from "./executor-input-guard.js";
50
- import { scanWithOutputGuard,
51
- // recoverEmptyFinalResponse, extractExecutionPlan, generateCompletenessNudge moved to executor-prompt-runner.ts
52
- } from "./executor-response-filter.js";
51
+ import { scanWithOutputGuard } from "./executor-response-filter.js";
53
52
  import { normalizeModelCompat } from "../provider/model-compat.js";
54
53
  import { normalizeModelId } from "../provider/model-id-normalize.js";
55
54
  import { isAnthropicFamily, isGoogleFamily } from "../provider/capabilities.js";
@@ -321,15 +320,15 @@ export function createPiExecutor(config, deps) {
321
320
  // Apply per-node model override from ExecutionOverrides and normalize shortcuts before registry lookup
322
321
  const normalizedPrimary = normalizeModelId(config.provider, config.model);
323
322
  let resolvedModel = deps.modelRegistry.find(config.provider, normalizedPrimary.modelId);
323
+ if (!resolvedModel && deps.providerAliases) {
324
+ const builtInName = deps.providerAliases.get(config.provider);
325
+ if (builtInName) {
326
+ resolvedModel = deps.modelRegistry.find(builtInName, normalizedPrimary.modelId);
327
+ }
328
+ }
324
329
  if (normalizedPrimary.normalized) {
325
330
  deps.logger.debug({ original: config.model, resolved: normalizedPrimary.modelId }, "Model ID normalized via shortcut");
326
331
  }
327
- // Surface the silent-fallback case where pi-coding-agent picks a different
328
- // provider than the user configured. When find() returns undefined for an
329
- // explicit (non-default) provider/model, pi will silently shop `findInitialModel`
330
- // and pick whatever built-in has env-var auth -- e.g., GEMINI_API_KEY → google.
331
- // The wiring fix in setup-agents.ts should cover the YAML-provider case; this
332
- // log catches stragglers (typos, disabled providers, missing API keys).
333
332
  if (!resolvedModel
334
333
  && config.provider.toLowerCase() !== "default"
335
334
  && config.model.toLowerCase() !== "default") {
@@ -836,6 +835,65 @@ export function createPiExecutor(config, deps) {
836
835
  timestamp: Date.now(),
837
836
  };
838
837
  },
838
+ // 260428-hoy: pre-LLM-call hook -- runs once per `turn_start`,
839
+ // BEFORE pi-ai serializes the next request. Asserts the
840
+ // cross-turn hash invariant (logs ERROR per mutated block, with
841
+ // module:"agent.bridge.hash-invariant"), then heals any mutated
842
+ // thinking blocks against the canonical stream-close snapshot
843
+ // and writes the healed array back into session.agent.state.messages
844
+ // so persistence and downstream layers see the same shape pi-ai
845
+ // serializes. Order matters: assert FIRST so the diagnostic
846
+ // captures every mutation before the heal overwrites it. Both
847
+ // helpers swallow throws internally; the outer try/catch is a
848
+ // belt-and-braces fallback -- the pre-call hook must NEVER abort
849
+ // agent flow.
850
+ getSessionMessages: () => {
851
+ const live = session.agent.state.messages;
852
+ if (!Array.isArray(live))
853
+ return live;
854
+ try {
855
+ const stores = bridge.getThinkingBlockStores();
856
+ if (stores.hashes.size > 0) {
857
+ for (const sessMsg of live) {
858
+ if (!sessMsg || typeof sessMsg !== "object")
859
+ continue;
860
+ const sm = sessMsg;
861
+ if (sm.role !== "assistant")
862
+ continue;
863
+ if (typeof sm.responseId !== "string")
864
+ continue;
865
+ const prior = stores.hashes.get(sm.responseId);
866
+ if (!prior)
867
+ continue;
868
+ const currentContent = Array.isArray(sm.content)
869
+ ? sm.content
870
+ : [];
871
+ assertThinkingBlocksUnchanged(prior, currentContent, sm.responseId, {
872
+ logger: deps.logger,
873
+ });
874
+ }
875
+ }
876
+ if (stores.canonical.size > 0) {
877
+ const result = restoreCanonicalThinkingBlocks(live, stores.canonical, { logger: deps.logger });
878
+ if (result.restoredCount > 0) {
879
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK interop boundary; healed array preserves AgentMessage shape
880
+ session.agent.state.messages = result.messages;
881
+ return result.messages;
882
+ }
883
+ }
884
+ }
885
+ catch {
886
+ // Pre-call hook must NEVER abort agent flow.
887
+ }
888
+ return live;
889
+ },
890
+ // 260428-iag wire-edge diagnostic: resolves the per-session JSONL
891
+ // path on demand. The bridge invokes this only after detecting the
892
+ // signed-replay rejection signature on a 400 — never on the happy
893
+ // path. Path comes from the same sessionAdapter that already
894
+ // governs read/write of the file, so safePath / sessionKey routing
895
+ // is reused (sessionKeyToPath -> safePath under the hood).
896
+ getSessionJsonlPath: () => sessionAdapter.getSessionPath(sessionKey),
839
897
  // Budget trajectory warning: shared ref and per-execution cap
840
898
  perExecutionBudgetCap: config.budgets?.perExecution,
841
899
  budgetWarningRef,
@@ -961,6 +1019,7 @@ export function createPiExecutor(config, deps) {
961
1019
  fallbackModels: deps.fallbackModels,
962
1020
  modelRegistry: deps.modelRegistry,
963
1021
  providerHealth: deps.providerHealth,
1022
+ lastKnownModel: deps.lastKnownModel,
964
1023
  envelopeConfig: deps.envelopeConfig,
965
1024
  outputGuard: deps.outputGuard,
966
1025
  canaryToken: deps.canaryToken,