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.
- package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +130 -10
- package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +1 -1
- package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +9 -2
- package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +8 -0
- package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +2 -0
- package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +29 -0
- package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +242 -2
- package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.d.ts +210 -0
- package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.js +566 -0
- package/node_modules/@comis/agent/dist/context-engine/context-engine.js +8 -6
- package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -30
- package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +109 -36
- package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +5 -1
- package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +22 -20
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +2 -0
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +111 -15
- package/node_modules/@comis/agent/dist/executor/executor-response-filter.d.ts +20 -17
- package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +132 -52
- package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +16 -3
- package/node_modules/@comis/agent/dist/executor/model-retry.d.ts +14 -0
- package/node_modules/@comis/agent/dist/executor/model-retry.js +72 -1
- package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +3 -0
- package/node_modules/@comis/agent/dist/executor/pi-executor.js +68 -9
- package/node_modules/@comis/agent/dist/executor/post-batch-continuation.d.ts +82 -0
- package/node_modules/@comis/agent/dist/executor/post-batch-continuation.js +200 -0
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +1 -9
- package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +37 -2
- package/node_modules/@comis/agent/dist/executor/tool-deferral.js +45 -3
- package/node_modules/@comis/agent/dist/executor/tool-parallelism.js +0 -1
- package/node_modules/@comis/agent/dist/executor/types.d.ts +11 -2
- package/node_modules/@comis/agent/dist/index.d.ts +3 -1
- package/node_modules/@comis/agent/dist/index.js +2 -0
- package/node_modules/@comis/agent/dist/model/last-known-model.d.ts +36 -0
- package/node_modules/@comis/agent/dist/model/last-known-model.js +49 -0
- package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +16 -4
- package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +65 -21
- package/node_modules/@comis/agent/dist/planner/types.d.ts +0 -2
- package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +10 -0
- package/node_modules/@comis/agent/dist/session/comis-session-manager.js +5 -0
- package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +7 -0
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/dist/client/rpc-client.js +6 -1
- package/node_modules/@comis/cli/dist/commands/doctor.js +5 -3
- package/node_modules/@comis/cli/dist/commands/health.js +5 -2
- package/node_modules/@comis/cli/dist/wizard/json-output.js +7 -3
- package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +130 -0
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/dist/config/immutable-keys.d.ts +2 -2
- package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
- package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
- package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
- package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
- package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
- package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
- package/node_modules/@comis/core/dist/config/schema.js +14 -0
- package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
- package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
- package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
- package/node_modules/@comis/core/dist/exports/config.js +1 -1
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
- package/node_modules/@comis/daemon/dist/daemon.js +42 -0
- package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
- package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
- package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
- package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
- package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
- package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
- package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
- package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
- package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
- package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
- package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
- package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
- package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
- package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/dist/index.d.ts +1 -0
- package/node_modules/@comis/infra/dist/index.js +2 -0
- package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
- package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
- package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
- package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
- package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
- package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
- package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
- package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
- package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
- package/node_modules/@comis/skills/dist/index.d.ts +1 -1
- package/node_modules/@comis/skills/dist/index.js +2 -2
- package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
- package/node_modules/@comis/skills/package.json +1 -1
- package/node_modules/@comis/web/dist/assets/{agent-detail-ru-AhppM.js → agent-detail-DqL6Artv.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{agent-editor-hjwRuFVp.js → agent-editor-CNM_h94Y.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{agent-list-6Uotjatr.js → agent-list-Dbh-xD_F.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{billing-view-CxysXH0p.js → billing-view-C1DmtyzK.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{channel-detail-BBCKtmne.js → channel-detail-CtCH22N1.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{channel-list-FkfeOLBQ.js → channel-list-C7xXn-60.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{chat-console-BumBaIgO.js → chat-console-C51pjFwk.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{config-editor-C9BSwHGy.js → config-editor-BLArYRB7.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{context-dag-browser-BHm00mJD.js → context-dag-browser-fuyMinNI.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{context-engine-BENY3pWE.js → context-engine-Bngf2bH0.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{delivery-view-BCnkPsAp.js → delivery-view-C80hucxX.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{diagnostics-view-C_jQFG2H.js → diagnostics-view-Cl4VbHZ6.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{ic-chat-message-FdQcZsSQ.js → ic-chat-message-ByFUoMm6.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{ic-connection-dot-BgYiK2N4.js → ic-connection-dot-C4nDHgY2.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{ic-tool-call-DMPHsLyx.js → ic-tool-call-Bh5kq-yY.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{index-FLPhHz8p.js → index-BBkuC-EU.js} +2 -2
- package/node_modules/@comis/web/dist/assets/{mcp-management-5jyScQis.js → mcp-management-DB-phOo7.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{media-config-J9oT9PPs.js → media-config-CRqZ1ZUH.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{media-test-DGTCtM8-.js → media-test-C9vE20Oy.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{memory-inspector-D5Re9ptG.js → memory-inspector-CeqfnxMZ.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{message-center-cRLK6ZmG.js → message-center-Daup7Mof.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{models-D5vu07MR.js → models-DLYnEU8E.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{observe-view-CalNNEmd.js → observe-view-BTSt_PO5.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{pipeline-builder-DUYDGwZf.js → pipeline-builder-DknfzyLt.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{pipeline-history-BAO8brOe.js → pipeline-history-JnHZdeU_.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-DectIoQt.js → pipeline-history-detail-Dg4knsEb.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{pipeline-list-BHlaBKww.js → pipeline-list-AEnibjsp.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{pipeline-monitor-BhtpNEHf.js → pipeline-monitor-DG7RbIOO.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{scheduler-VafN_8xi.js → scheduler-uL1fYKAT.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{security-QQXMRTlo.js → security-C3DywRLH.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{session-detail-BpZ_8Yih.js → session-detail-BtqCNWXV.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{session-list-DfCm8Cec.js → session-list-CJXWa2XT.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{setup-wizard-C-z477CG.js → setup-wizard-ywn7oJvu.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{skills-BCOGPf6s.js → skills-DX0KYnWD.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{subagents-l-auUraL.js → subagents-B8p5YJEB.js} +1 -1
- package/node_modules/@comis/web/dist/assets/{workspace-manager-DlvBixiq.js → workspace-manager-CgzNIrw1.js} +1 -1
- package/node_modules/@comis/web/dist/index.html +1 -1
- package/node_modules/@comis/web/package.json +1 -1
- package/package.json +14 -14
- package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
- 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,
|
|
48
|
-
* message that contained visible text blocks.
|
|
47
|
+
* turns, recover a meaningful user-visible response.
|
|
49
48
|
*
|
|
50
|
-
* Two-pass strategy:
|
|
51
|
-
* 1.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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,
|
|
97
|
-
* message that contained visible text blocks.
|
|
95
|
+
* turns, recover a meaningful user-visible response.
|
|
98
96
|
*
|
|
99
|
-
* Two-pass strategy:
|
|
100
|
-
* 1.
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
-
//
|
|
128
|
-
//
|
|
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
|
-
|
|
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,
|