@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1
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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +38 -12
- package/dist/engine/engine.js +477 -163
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +34 -2
- package/dist/engine/turn-loop.js +269 -52
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
package/dist/engine/turn-loop.js
CHANGED
|
@@ -11,12 +11,14 @@ import { ContextLimitError } from "../exceptions.js";
|
|
|
11
11
|
import { logger } from "../logging/logger.js";
|
|
12
12
|
import { checkTokenBudget, createBudgetTracker } from "./token-budget.js";
|
|
13
13
|
import { StreamingToolQueue } from "./streaming-tool-queue.js";
|
|
14
|
-
import { estimateTokens } from "../context/compaction.js";
|
|
14
|
+
import { downgradeImagePayloadsInHistory, estimateTokens, messageHasBase64ImagePayload, } from "../context/compaction.js";
|
|
15
15
|
import { isTruncatedStop } from "../llm/stop-reason.js";
|
|
16
16
|
import { isAbortError } from "../llm/client-base.js";
|
|
17
17
|
import { crossedReactiveThreshold } from "./reactive-threshold.js";
|
|
18
18
|
import { COMPLETE_GOAL_TOOL_NAME } from "../tool-system/builtin/complete-goal.js";
|
|
19
19
|
import { CANCEL_GOAL_TOOL_NAME } from "../tool-system/builtin/cancel-goal.js";
|
|
20
|
+
import { redactSensitiveToolResultsInMessages, toolResultForDisplay, toolResultTranscriptText, toolResultsForDisplay, } from "../tool-system/tool-result-redaction.js";
|
|
21
|
+
import { addTokenUsage, cacheHitRateFromUsage, cumulativeCacheHitRate, } from "./session-usage.js";
|
|
20
22
|
import { createGoalBudgetTracker, recordGoalUsage, goalBudgetExceeded, applyGoalExtension, limitProximity, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "./goal.js";
|
|
21
23
|
/**
|
|
22
24
|
* 把一个 ToolResult 映射成发给 LLM 的 tool_result ContentBlock。
|
|
@@ -29,7 +31,7 @@ export function toolResultToBlock(result) {
|
|
|
29
31
|
tool_use_id: result.id,
|
|
30
32
|
content: result.error
|
|
31
33
|
? `Error: ${result.error}`
|
|
32
|
-
: result.contentBlocks ??
|
|
34
|
+
: (result.contentBlocks ?? result.result ?? "(no output)"),
|
|
33
35
|
};
|
|
34
36
|
if (result.isError || result.error)
|
|
35
37
|
block.is_error = true;
|
|
@@ -50,6 +52,16 @@ export class TurnLoop {
|
|
|
50
52
|
currentTurnLog = logger;
|
|
51
53
|
/** Last emitted ctx token estimate; used to skip no-op usage_update events. */
|
|
52
54
|
lastCtxEmit = -1;
|
|
55
|
+
currentTurnUsage = {
|
|
56
|
+
promptTokens: 0,
|
|
57
|
+
completionTokens: 0,
|
|
58
|
+
totalTokens: 0,
|
|
59
|
+
cacheReadTokens: 0,
|
|
60
|
+
cacheCreationTokens: 0,
|
|
61
|
+
};
|
|
62
|
+
currentCumulativeUsage;
|
|
63
|
+
sensitiveToolResultRedactions = new Map();
|
|
64
|
+
pendingImageMessages = new Set();
|
|
53
65
|
/**
|
|
54
66
|
* Consecutive on_stop blocks (Goal mode kept the agent going). Reset to 0
|
|
55
67
|
* on any unblocked completion. When it reaches config.maxStopBlocks the
|
|
@@ -145,6 +157,9 @@ export class TurnLoop {
|
|
|
145
157
|
config) {
|
|
146
158
|
this.deps = deps;
|
|
147
159
|
this.config = config;
|
|
160
|
+
for (const msg of this.config.freshImageMessages ?? []) {
|
|
161
|
+
this.pendingImageMessages.add(msg);
|
|
162
|
+
}
|
|
148
163
|
// Wrap onStream so a single throwing handler can't silently break
|
|
149
164
|
// the channel for the rest of the run. A 2026-05-25 incident saw a
|
|
150
165
|
// sub-agent's events stop reaching the renderer ~23s into its run —
|
|
@@ -197,6 +212,52 @@ export class TurnLoop {
|
|
|
197
212
|
return;
|
|
198
213
|
this.deps.transcript.appendTurnStopped();
|
|
199
214
|
}
|
|
215
|
+
prepareMessagesForModel(messages) {
|
|
216
|
+
const preserveMessages = this.pendingImageMessages.size > 0 ? this.pendingImageMessages : undefined;
|
|
217
|
+
const result = downgradeImagePayloadsInHistory(messages, { preserveMessages });
|
|
218
|
+
if (result.replacedCount > 0) {
|
|
219
|
+
this.currentTurnLog.info("context.image_payload_downgrade", {
|
|
220
|
+
cat: "context",
|
|
221
|
+
images: result.replacedCount,
|
|
222
|
+
pendingFresh: this.pendingImageMessages.size,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return result.messages;
|
|
226
|
+
}
|
|
227
|
+
markPendingImagesConsumed(messages) {
|
|
228
|
+
if (this.pendingImageMessages.size === 0)
|
|
229
|
+
return messages;
|
|
230
|
+
const consumedMessages = this.pendingImageMessages.size;
|
|
231
|
+
this.pendingImageMessages.clear();
|
|
232
|
+
const result = downgradeImagePayloadsInHistory(messages);
|
|
233
|
+
if (result.replacedCount > 0) {
|
|
234
|
+
this.currentTurnLog.info("context.image_payload_consumed", {
|
|
235
|
+
cat: "context",
|
|
236
|
+
images: result.replacedCount,
|
|
237
|
+
messages: consumedMessages,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return result.messages;
|
|
241
|
+
}
|
|
242
|
+
redactConsumedSensitiveToolResults(messages) {
|
|
243
|
+
if (this.sensitiveToolResultRedactions.size === 0)
|
|
244
|
+
return messages;
|
|
245
|
+
const redacted = redactSensitiveToolResultsInMessages(messages, this.sensitiveToolResultRedactions);
|
|
246
|
+
this.sensitiveToolResultRedactions.clear();
|
|
247
|
+
return redacted;
|
|
248
|
+
}
|
|
249
|
+
modelCallRecordingOptions() {
|
|
250
|
+
if (this.sensitiveToolResultRedactions.size === 0)
|
|
251
|
+
return undefined;
|
|
252
|
+
return {
|
|
253
|
+
sensitiveToolResultRedactions: new Map(this.sensitiveToolResultRedactions),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
trackFreshImageMessage(message) {
|
|
257
|
+
if (messageHasBase64ImagePayload(message)) {
|
|
258
|
+
this.pendingImageMessages.add(message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
200
261
|
async emitHook(event, data = {}) {
|
|
201
262
|
return this.deps.hooks.emit(event, {
|
|
202
263
|
...data,
|
|
@@ -236,7 +297,16 @@ export class TurnLoop {
|
|
|
236
297
|
if (ctx === this.lastCtxEmit)
|
|
237
298
|
return;
|
|
238
299
|
this.lastCtxEmit = ctx;
|
|
239
|
-
this.config.onStream({
|
|
300
|
+
this.config.onStream({
|
|
301
|
+
type: "usage_update",
|
|
302
|
+
promptTokens: ctx,
|
|
303
|
+
promptTokensSource: overhead > 0 ? "calibrated_estimate" : "heuristic_estimate",
|
|
304
|
+
promptTokensConfidence: overhead > 0 ? "medium" : "low",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
recordResponseUsage(usage) {
|
|
308
|
+
this.currentTurnUsage = addTokenUsage(this.currentTurnUsage, usage);
|
|
309
|
+
this.currentCumulativeUsage = this.deps.recordCumulativeUsage?.(usage);
|
|
240
310
|
}
|
|
241
311
|
emitCtxFromUsage(usage, messages) {
|
|
242
312
|
if (!this.config.onStream)
|
|
@@ -254,29 +324,58 @@ export class TurnLoop {
|
|
|
254
324
|
derivedOverhead: overhead,
|
|
255
325
|
prev: this.lastCtxEmit,
|
|
256
326
|
});
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
327
|
+
const promptChanged = promptTokens !== this.lastCtxEmit;
|
|
328
|
+
if (promptChanged)
|
|
329
|
+
this.lastCtxEmit = promptTokens;
|
|
260
330
|
// Forward the provider's cache counts so the UI can show a hit rate. Only
|
|
261
331
|
// attach fields the provider actually reported — a spread keeps them off
|
|
262
332
|
// the event entirely when undefined, so the renderer can tell "no cache
|
|
263
333
|
// info this turn" from "0 cached". Estimate-path emits don't call this and
|
|
264
334
|
// so carry no cache fields (correct: an estimate has no cache reading).
|
|
335
|
+
const singleTurnCacheHitRate = cacheHitRateFromUsage(this.currentTurnUsage);
|
|
336
|
+
const cumulative = this.currentCumulativeUsage;
|
|
337
|
+
const cumulativeHitRate = cumulative ? cumulativeCacheHitRate(cumulative) : undefined;
|
|
338
|
+
if (!promptChanged && singleTurnCacheHitRate === undefined && cumulativeHitRate === undefined) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
265
341
|
this.config.onStream({
|
|
266
342
|
type: "usage_update",
|
|
267
343
|
promptTokens,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
344
|
+
promptTokensSource: "provider_usage",
|
|
345
|
+
promptTokensConfidence: "high",
|
|
346
|
+
...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
271
347
|
...(usage.cacheCreationTokens !== undefined
|
|
272
348
|
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
273
349
|
: {}),
|
|
350
|
+
singleTurnPromptTokens: this.currentTurnUsage.promptTokens,
|
|
351
|
+
singleTurnCacheReadTokens: this.currentTurnUsage.cacheReadTokens ?? 0,
|
|
352
|
+
singleTurnCacheCreationTokens: this.currentTurnUsage.cacheCreationTokens ?? 0,
|
|
353
|
+
...(singleTurnCacheHitRate !== undefined ? { singleTurnCacheHitRate } : {}),
|
|
354
|
+
...(cumulative
|
|
355
|
+
? {
|
|
356
|
+
cumulativePromptTokens: cumulative.cumulativePromptTokens,
|
|
357
|
+
cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
358
|
+
cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
359
|
+
...(cumulativeHitRate !== undefined
|
|
360
|
+
? { cumulativeCacheHitRate: cumulativeHitRate }
|
|
361
|
+
: {}),
|
|
362
|
+
// Legacy aliases for existing renderer builds. New UI code reads the
|
|
363
|
+
// cumulative* fields above.
|
|
364
|
+
sessionPromptTokens: cumulative.cumulativePromptTokens,
|
|
365
|
+
sessionCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
366
|
+
sessionCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
367
|
+
}
|
|
368
|
+
: {}),
|
|
274
369
|
});
|
|
275
370
|
}
|
|
276
371
|
/**
|
|
277
372
|
* Run the multi-turn agent loop until completion.
|
|
278
373
|
*/
|
|
279
374
|
async run(initialMessages) {
|
|
375
|
+
const result = await this.runUnredacted(initialMessages);
|
|
376
|
+
return { ...result, messages: this.redactConsumedSensitiveToolResults(result.messages) };
|
|
377
|
+
}
|
|
378
|
+
async runUnredacted(initialMessages) {
|
|
280
379
|
let messages = [...initialMessages];
|
|
281
380
|
let finalText = "";
|
|
282
381
|
const budgetTracker = createBudgetTracker();
|
|
@@ -300,6 +399,13 @@ export class TurnLoop {
|
|
|
300
399
|
try {
|
|
301
400
|
while (this.turnCount < this.config.maxTurns) {
|
|
302
401
|
this.turnCount++;
|
|
402
|
+
this.currentTurnUsage = {
|
|
403
|
+
promptTokens: 0,
|
|
404
|
+
completionTokens: 0,
|
|
405
|
+
totalTokens: 0,
|
|
406
|
+
cacheReadTokens: 0,
|
|
407
|
+
cacheCreationTokens: 0,
|
|
408
|
+
};
|
|
303
409
|
// Abort fast-path: bail at the loop TOP before doing any per-turn work.
|
|
304
410
|
// Without this, an aborted child (parent abort, or the 30min per-call
|
|
305
411
|
// registry timeout) would run a full contextManager.manageAsync (itself
|
|
@@ -319,14 +425,7 @@ export class TurnLoop {
|
|
|
319
425
|
// they join THIS step's request — no abort, no lost in-flight work. Same
|
|
320
426
|
// loop-top user-push pattern as turnStartInjection / turn-limit warnings
|
|
321
427
|
// below. Push to transcript too so they persist + survive resume.
|
|
322
|
-
|
|
323
|
-
for (const { id, text } of steered) {
|
|
324
|
-
if (!text)
|
|
325
|
-
continue;
|
|
326
|
-
messages.push({ role: "user", content: text });
|
|
327
|
-
this.deps.transcript.appendMessage("user", text);
|
|
328
|
-
this.config.onStream?.({ type: "steer_injected", text, id });
|
|
329
|
-
}
|
|
428
|
+
this.consumeQueuedSteer(messages, "normal_step");
|
|
330
429
|
const state = initialTurnState(this.turnCount);
|
|
331
430
|
// Per-turn correlation ID. Every log written through `tlog` (or any
|
|
332
431
|
// child derived from it) is stamped with `turn` + `turnId`, so
|
|
@@ -335,13 +434,18 @@ export class TurnLoop {
|
|
|
335
434
|
// returns; instead, each return-causing branch logs its own terminal
|
|
336
435
|
// event (model_error, completed, etc.).
|
|
337
436
|
const turnId = newTurnId();
|
|
437
|
+
const assistantMessageId = `assistant_${turnId}`;
|
|
338
438
|
const tlog = logger.child({ turn: this.turnCount, turnId });
|
|
339
439
|
this.currentTurnLog = tlog;
|
|
340
440
|
const turnStartedAt = Date.now();
|
|
341
441
|
tlog.info("turn.start", { cat: "turn", messageCount: messages.length });
|
|
342
442
|
// Tag downstream tool-exec / permission lines with this turn's IDs.
|
|
343
443
|
this.deps.toolExecutor.setLogger(tlog);
|
|
344
|
-
this.config.onStream?.({
|
|
444
|
+
this.config.onStream?.({
|
|
445
|
+
type: "stream_request_start",
|
|
446
|
+
turnNumber: this.turnCount,
|
|
447
|
+
messageId: assistantMessageId,
|
|
448
|
+
});
|
|
345
449
|
const turnStartHook = await this.emitHook("on_turn_start", {
|
|
346
450
|
turnNumber: this.turnCount,
|
|
347
451
|
});
|
|
@@ -378,15 +482,28 @@ export class TurnLoop {
|
|
|
378
482
|
// Goal mode: announce once when nearing EITHER stop ceiling (turns or
|
|
379
483
|
// stop-blocks) so the UI can offer a "再续" button while still live.
|
|
380
484
|
this.maybeAnnounceApproachingLimit();
|
|
381
|
-
// Pre-check:
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
485
|
+
// Pre-check: downgrade image payloads that have already had their one
|
|
486
|
+
// model-consumption turn, then run context management. Fresh images in
|
|
487
|
+
// pendingImageMessages are preserved through this next model request.
|
|
488
|
+
const hasPendingSensitiveToolResults = this.sensitiveToolResultRedactions.size > 0;
|
|
489
|
+
messages = this.prepareMessagesForModel(messages);
|
|
490
|
+
if (hasPendingSensitiveToolResults) {
|
|
491
|
+
tlog.info("turn.sensitive_tool_result_context_management_skipped", {
|
|
492
|
+
cat: "turn",
|
|
493
|
+
count: this.sensitiveToolResultRedactions.size,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Context management (async — may trigger LLM summarization)
|
|
498
|
+
messages = await this.deps.contextManager.manageAsync(messages);
|
|
499
|
+
// manageAsync can itself issue an LLM summarization call lasting several
|
|
500
|
+
// seconds; if the signal aborted during it, stop here rather than
|
|
501
|
+
// proceeding into the (expensive) main model call. Belt to the loop-top
|
|
502
|
+
// brace: this catches an abort that landed *inside* context management.
|
|
503
|
+
if (this.config.signal?.aborted) {
|
|
504
|
+
this.markStopped();
|
|
505
|
+
return { text: finalText, reason: "aborted_streaming", messages };
|
|
506
|
+
}
|
|
390
507
|
}
|
|
391
508
|
// No pre-llm ctx emit here: the messages-only estimate would be ~16k
|
|
392
509
|
// smaller than the real prompt (system + tools not included), making
|
|
@@ -400,7 +517,9 @@ export class TurnLoop {
|
|
|
400
517
|
// call. Microcompact is lossless (just clearing redundant
|
|
401
518
|
// tool_results) so we suppress hook emits for it to keep token
|
|
402
519
|
// overhead down.
|
|
403
|
-
const pending =
|
|
520
|
+
const pending = hasPendingSensitiveToolResults
|
|
521
|
+
? null
|
|
522
|
+
: this.deps.consumePendingCompactInfo?.();
|
|
404
523
|
if (pending && pending.strategy !== "micro") {
|
|
405
524
|
const compactHook = await this.emitHook("post_compact", {
|
|
406
525
|
strategy: pending.strategy,
|
|
@@ -415,11 +534,12 @@ export class TurnLoop {
|
|
|
415
534
|
// Model call (with streaming fallback and max_output_tokens continuation)
|
|
416
535
|
// Track tool IDs streamed during this turn to avoid duplicate UI events
|
|
417
536
|
this.streamedToolIds.clear();
|
|
418
|
-
//
|
|
537
|
+
// Tool queue is created before the call, but enqueue happens only after
|
|
538
|
+
// the complete LLMResponse is available below.
|
|
419
539
|
const streamingQueue = new StreamingToolQueue(this.deps.toolExecutor);
|
|
420
540
|
let response;
|
|
421
541
|
try {
|
|
422
|
-
response = await this.callModelWithFallback(messages);
|
|
542
|
+
response = await this.callModelWithFallback(messages, assistantMessageId);
|
|
423
543
|
}
|
|
424
544
|
catch (err) {
|
|
425
545
|
if (err instanceof ContextLimitError) {
|
|
@@ -430,13 +550,14 @@ export class TurnLoop {
|
|
|
430
550
|
tlog.warn("turn.ptl_recovery", { cat: "turn", retry, roundsToDrop: retry });
|
|
431
551
|
messages = dropOldestRounds(messages, retry);
|
|
432
552
|
try {
|
|
433
|
-
response = await this.callModelWithFallback(messages);
|
|
553
|
+
response = await this.callModelWithFallback(messages, assistantMessageId);
|
|
434
554
|
recovered = true;
|
|
435
555
|
break;
|
|
436
556
|
}
|
|
437
557
|
catch (retryErr) {
|
|
438
558
|
if (!(retryErr instanceof ContextLimitError)) {
|
|
439
559
|
this.config.onStream?.({ type: "error", error: formatFriendlyError(retryErr) });
|
|
560
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
440
561
|
return { text: finalText, reason: "model_error", messages };
|
|
441
562
|
}
|
|
442
563
|
}
|
|
@@ -447,6 +568,7 @@ export class TurnLoop {
|
|
|
447
568
|
type: "error",
|
|
448
569
|
error: "Context limit exceeded after 3 recovery attempts",
|
|
449
570
|
});
|
|
571
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
450
572
|
return { text: finalText, reason: "prompt_too_long", messages };
|
|
451
573
|
}
|
|
452
574
|
}
|
|
@@ -460,24 +582,31 @@ export class TurnLoop {
|
|
|
460
582
|
// interrupted turn folds behind the process-card header on reload).
|
|
461
583
|
this.patchOrphanedToolUses(messages);
|
|
462
584
|
this.markStopped();
|
|
585
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
463
586
|
return { text: finalText, reason: "aborted_streaming", messages };
|
|
464
587
|
}
|
|
465
588
|
else {
|
|
466
589
|
this.patchOrphanedToolUses(messages);
|
|
467
590
|
this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
|
|
591
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
468
592
|
return { text: finalText, reason: "model_error", messages };
|
|
469
593
|
}
|
|
470
594
|
}
|
|
471
|
-
|
|
595
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
596
|
+
// Record the response once into current-turn and whole-session counters.
|
|
472
597
|
if (response.usage?.promptTokens !== undefined) {
|
|
598
|
+
this.recordResponseUsage(response.usage);
|
|
473
599
|
this.emitCtxFromUsage(response.usage, messages);
|
|
474
600
|
}
|
|
475
601
|
// Feed actual token usage back to the context manager so subsequent
|
|
476
602
|
// compaction decisions use hybrid (actual + delta) estimation rather than
|
|
477
603
|
// pure heuristics. Without this the manager falls back to char/4 estimates.
|
|
478
604
|
if (response.usage?.promptTokens !== undefined) {
|
|
479
|
-
this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length);
|
|
605
|
+
const anchor = this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
|
|
606
|
+
if (anchor)
|
|
607
|
+
this.deps.recordContextUsageAnchor?.(anchor);
|
|
480
608
|
}
|
|
609
|
+
messages = this.markPendingImagesConsumed(messages);
|
|
481
610
|
// Truncation that cut off a TOOL CALL: the model overflowed
|
|
482
611
|
// max_output_tokens mid tool-call, so the arg JSON is incomplete (e.g. a
|
|
483
612
|
// Write whose `content` was clipped, leaving file_path unset). Executing
|
|
@@ -506,6 +635,7 @@ export class TurnLoop {
|
|
|
506
635
|
response.toolCalls.length === 0 &&
|
|
507
636
|
response.text) {
|
|
508
637
|
let combinedText = response.text;
|
|
638
|
+
let continuedResponse = false;
|
|
509
639
|
for (let retry = 0; retry < 3; retry++) {
|
|
510
640
|
// Don't fire another continuation call if the user cancelled — without
|
|
511
641
|
// this an abort during a truncated response could still issue up to 3
|
|
@@ -522,7 +652,11 @@ export class TurnLoop {
|
|
|
522
652
|
},
|
|
523
653
|
];
|
|
524
654
|
try {
|
|
525
|
-
const contResponse = await this.deps.model.call(this.deps.systemPrompt, contMessages, this.deps.tools, this.config.onStream, this.config.signal);
|
|
655
|
+
const contResponse = await this.deps.model.call(this.deps.systemPrompt, this.prepareMessagesForModel(contMessages), this.deps.tools, this.config.onStream, this.config.signal);
|
|
656
|
+
if (contResponse.usage?.promptTokens !== undefined) {
|
|
657
|
+
this.recordResponseUsage(contResponse.usage);
|
|
658
|
+
continuedResponse = true;
|
|
659
|
+
}
|
|
526
660
|
combinedText += contResponse.text;
|
|
527
661
|
if (!isTruncatedStop(contResponse.stopReason) || contResponse.toolCalls.length > 0) {
|
|
528
662
|
response = { ...contResponse, text: combinedText };
|
|
@@ -534,10 +668,9 @@ export class TurnLoop {
|
|
|
534
668
|
}
|
|
535
669
|
}
|
|
536
670
|
response = { ...response, text: combinedText };
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
this.emitCtxFromUsage(response.usage, messages);
|
|
671
|
+
if (continuedResponse && response.usage?.promptTokens !== undefined) {
|
|
672
|
+
this.emitCtxFromUsage(response.usage, messages);
|
|
673
|
+
}
|
|
541
674
|
}
|
|
542
675
|
// Goal-mode run-scoped accounting: add this turn's total token usage
|
|
543
676
|
// (prompt + completion) to the running total. Done after continuation so
|
|
@@ -568,6 +701,7 @@ export class TurnLoop {
|
|
|
568
701
|
});
|
|
569
702
|
this.config.onStream?.({
|
|
570
703
|
type: "assistant_message",
|
|
704
|
+
messageId: assistantMessageId,
|
|
571
705
|
message: {
|
|
572
706
|
role: "assistant",
|
|
573
707
|
content: "(Goal 预算已耗尽,强制停止。)",
|
|
@@ -580,6 +714,7 @@ export class TurnLoop {
|
|
|
580
714
|
// No tool use — final answer
|
|
581
715
|
this.config.onStream?.({
|
|
582
716
|
type: "assistant_message",
|
|
717
|
+
messageId: assistantMessageId,
|
|
583
718
|
message: { role: "assistant", content: finalText },
|
|
584
719
|
});
|
|
585
720
|
await this.emitHook("on_turn_end", {
|
|
@@ -587,6 +722,9 @@ export class TurnLoop {
|
|
|
587
722
|
hasToolUse: false,
|
|
588
723
|
});
|
|
589
724
|
messages.push({ role: "assistant", content: finalText });
|
|
725
|
+
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
590
728
|
// on_stop seam: the model wants to stop. Give handlers (Goal mode)
|
|
591
729
|
// a chance to BLOCK termination and keep the agent working. A
|
|
592
730
|
// handler returning continueSession=true injects its messages and
|
|
@@ -652,6 +790,7 @@ export class TurnLoop {
|
|
|
652
790
|
});
|
|
653
791
|
this.config.onStream?.({
|
|
654
792
|
type: "assistant_message",
|
|
793
|
+
messageId: assistantMessageId,
|
|
655
794
|
message: {
|
|
656
795
|
role: "assistant",
|
|
657
796
|
content: `(Goal 续跑已达 ${maxStopBlocks} 次上限,先停下。)`,
|
|
@@ -668,10 +807,17 @@ export class TurnLoop {
|
|
|
668
807
|
});
|
|
669
808
|
}
|
|
670
809
|
this.stopBlockCount = 0;
|
|
810
|
+
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
671
814
|
return { text: finalText, reason: "completed", messages };
|
|
672
815
|
}
|
|
673
816
|
// Tool execution phase
|
|
674
|
-
tlog.info("turn.tool_use", {
|
|
817
|
+
tlog.info("turn.tool_use", {
|
|
818
|
+
cat: "turn",
|
|
819
|
+
tools: response.toolCalls.map((t) => t.toolName),
|
|
820
|
+
});
|
|
675
821
|
const toolCalls = response.toolCalls.slice(0, this.config.maxToolCallsPerTurn);
|
|
676
822
|
// Per-turn cap: any calls beyond maxToolCallsPerTurn are NOT executed and
|
|
677
823
|
// NOT added to the assistant message below, so the model never sees a
|
|
@@ -709,8 +855,13 @@ export class TurnLoop {
|
|
|
709
855
|
const resultBlocks = [];
|
|
710
856
|
for (const result of results) {
|
|
711
857
|
resultBlocks.push(toolResultToBlock(result));
|
|
712
|
-
|
|
713
|
-
|
|
858
|
+
const streamResult = toolResultForDisplay(result);
|
|
859
|
+
const transcriptResult = toolResultTranscriptText(result);
|
|
860
|
+
if (result.sensitive && transcriptResult !== undefined) {
|
|
861
|
+
this.sensitiveToolResultRedactions.set(result.id, transcriptResult);
|
|
862
|
+
}
|
|
863
|
+
this.deps.transcript.appendToolResult(result.id, result.toolName, transcriptResult, result.error, result.sensitive ? undefined : result.contentBlocks);
|
|
864
|
+
this.config.onStream?.({ type: "tool_result", result: streamResult });
|
|
714
865
|
}
|
|
715
866
|
// Fire-and-forget tool use summary (non-blocking). The whole chain is
|
|
716
867
|
// best-effort observability — a thrown onStream handler, a failed dynamic
|
|
@@ -721,9 +872,13 @@ export class TurnLoop {
|
|
|
721
872
|
.then(({ generateToolUseSummary }) => {
|
|
722
873
|
if (!this.deps.model.summarize)
|
|
723
874
|
return;
|
|
724
|
-
return generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
|
|
875
|
+
return generateToolUseSummary(toolCalls, toolResultsForDisplay(results), this.deps.model.summarize).then((summary) => {
|
|
725
876
|
if (summary) {
|
|
726
|
-
this.config.onStream?.({
|
|
877
|
+
this.config.onStream?.({
|
|
878
|
+
type: "tool_summary",
|
|
879
|
+
summary,
|
|
880
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id),
|
|
881
|
+
});
|
|
727
882
|
}
|
|
728
883
|
});
|
|
729
884
|
})
|
|
@@ -734,7 +889,9 @@ export class TurnLoop {
|
|
|
734
889
|
});
|
|
735
890
|
});
|
|
736
891
|
}
|
|
737
|
-
|
|
892
|
+
const toolResultMessage = { role: "user", content: resultBlocks };
|
|
893
|
+
messages.push(toolResultMessage);
|
|
894
|
+
this.trackFreshImageMessage(toolResultMessage);
|
|
738
895
|
// B-3: tell the model which of its requested tool calls were dropped by
|
|
739
896
|
// the per-turn cap so it can re-issue them, instead of silently assuming
|
|
740
897
|
// they ran. Appended to the same user message that carries the results.
|
|
@@ -773,6 +930,10 @@ export class TurnLoop {
|
|
|
773
930
|
tlog.info("turn.goal_self_reported_complete", { cat: "goal" });
|
|
774
931
|
this.stopBlockCount = 0;
|
|
775
932
|
this.deps.clearPersistedGoal?.();
|
|
933
|
+
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
776
937
|
return { text: finalText, reason: "completed", messages };
|
|
777
938
|
}
|
|
778
939
|
// Goal mode: user-initiated cancellation. cancel_goal is the "strong
|
|
@@ -785,6 +946,9 @@ export class TurnLoop {
|
|
|
785
946
|
tlog.info("turn.goal_user_cancelled", { cat: "goal" });
|
|
786
947
|
this.stopBlockCount = 0;
|
|
787
948
|
this.deps.clearPersistedGoal?.();
|
|
949
|
+
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
788
952
|
return { text: finalText, reason: "completed", messages };
|
|
789
953
|
}
|
|
790
954
|
// Token budget check
|
|
@@ -798,9 +962,14 @@ export class TurnLoop {
|
|
|
798
962
|
});
|
|
799
963
|
this.config.onStream?.({
|
|
800
964
|
type: "assistant_message",
|
|
965
|
+
messageId: assistantMessageId,
|
|
801
966
|
message: { role: "assistant", content: finalText },
|
|
802
967
|
});
|
|
803
968
|
messages.push({ role: "assistant", content: finalText });
|
|
969
|
+
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
804
973
|
return { text: finalText, reason: "completed", messages };
|
|
805
974
|
}
|
|
806
975
|
if (budgetDecision === "nudge") {
|
|
@@ -861,6 +1030,7 @@ export class TurnLoop {
|
|
|
861
1030
|
// no error event, so the UI shows only the "你停止了本轮" line.
|
|
862
1031
|
if (isAbortError(err) || this.config.signal?.aborted) {
|
|
863
1032
|
this.markStopped();
|
|
1033
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
864
1034
|
return { text: finalText, reason: "aborted_streaming", messages };
|
|
865
1035
|
}
|
|
866
1036
|
this.currentTurnLog.error("turn.unhandled_error", {
|
|
@@ -869,6 +1039,7 @@ export class TurnLoop {
|
|
|
869
1039
|
stack: err.stack?.split("\n").slice(0, 4).join("\n"),
|
|
870
1040
|
});
|
|
871
1041
|
this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
|
|
1042
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
872
1043
|
return { text: finalText, reason: "model_error", messages };
|
|
873
1044
|
}
|
|
874
1045
|
// Max turns reached — do one final summarization call (no tools)
|
|
@@ -877,15 +1048,28 @@ export class TurnLoop {
|
|
|
877
1048
|
maxTurns: this.config.maxTurns,
|
|
878
1049
|
turnCount: this.turnCount,
|
|
879
1050
|
});
|
|
880
|
-
|
|
1051
|
+
this.consumeQueuedSteer(messages, "finalize_backfill");
|
|
1052
|
+
const hasPendingSensitiveToolResults = this.sensitiveToolResultRedactions.size > 0;
|
|
1053
|
+
messages = this.prepareMessagesForModel(messages);
|
|
1054
|
+
if (hasPendingSensitiveToolResults) {
|
|
1055
|
+
logger.info("turn.sensitive_tool_result_context_management_skipped", {
|
|
1056
|
+
cat: "turn",
|
|
1057
|
+
count: this.sensitiveToolResultRedactions.size,
|
|
1058
|
+
phase: "max_turns_summary",
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
messages = this.deps.contextManager.manage(messages);
|
|
1063
|
+
}
|
|
881
1064
|
messages.push({
|
|
882
1065
|
role: "user",
|
|
883
1066
|
content: "<system-reminder>Turn limit reached. Provide a final summary of what you accomplished and what remains to be done. Do NOT call any tools.</system-reminder>",
|
|
884
1067
|
});
|
|
885
1068
|
this.emitCtxFromMessages(messages);
|
|
886
1069
|
try {
|
|
887
|
-
const summaryResponse = await this.deps.model.call(this.deps.systemPrompt, messages, [], // No tools available for summary turn
|
|
888
|
-
this.config.onStream, this.config.signal);
|
|
1070
|
+
const summaryResponse = await this.deps.model.call(this.deps.systemPrompt, this.prepareMessagesForModel(messages), [], // No tools available for summary turn
|
|
1071
|
+
this.config.onStream, this.config.signal, this.modelCallRecordingOptions());
|
|
1072
|
+
messages = this.markPendingImagesConsumed(messages);
|
|
889
1073
|
if (summaryResponse.text) {
|
|
890
1074
|
finalText = summaryResponse.text;
|
|
891
1075
|
}
|
|
@@ -901,14 +1085,14 @@ export class TurnLoop {
|
|
|
901
1085
|
});
|
|
902
1086
|
messages.push({ role: "assistant", content: finalText });
|
|
903
1087
|
}
|
|
904
|
-
this.
|
|
1088
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
905
1089
|
return { text: finalText, reason: "max_turns", messages };
|
|
906
1090
|
}
|
|
907
1091
|
/**
|
|
908
1092
|
* Call model with streaming fallback.
|
|
909
1093
|
* If streaming fails, emit tombstone and retry non-streaming.
|
|
910
1094
|
*/
|
|
911
|
-
async callModelWithFallback(messages) {
|
|
1095
|
+
async callModelWithFallback(messages, assistantMessageId) {
|
|
912
1096
|
// Wrap stream callback to track tool_use_start events and reactive compaction
|
|
913
1097
|
let streamingResponseTokens = 0;
|
|
914
1098
|
let reactiveBucket = -1;
|
|
@@ -940,7 +1124,7 @@ export class TurnLoop {
|
|
|
940
1124
|
}
|
|
941
1125
|
: undefined;
|
|
942
1126
|
try {
|
|
943
|
-
return await this.deps.model.call(this.deps.systemPrompt, messages, this.deps.tools, wrappedStream, this.config.signal);
|
|
1127
|
+
return await this.deps.model.call(this.deps.systemPrompt, messages, this.deps.tools, wrappedStream, this.config.signal, this.modelCallRecordingOptions());
|
|
944
1128
|
}
|
|
945
1129
|
catch (err) {
|
|
946
1130
|
// If it's a context or rate limit error, don't fallback — propagate
|
|
@@ -960,19 +1144,52 @@ export class TurnLoop {
|
|
|
960
1144
|
// really just a cancel. The signal is the authoritative cancel source.
|
|
961
1145
|
if (isAbortError(err) || this.config.signal?.aborted)
|
|
962
1146
|
throw err;
|
|
1147
|
+
// Sensitive tool results are model-facing exactly once. A streaming
|
|
1148
|
+
// fallback would re-send the same pending plaintext in a second request,
|
|
1149
|
+
// so fail the turn and let the unified exit redact returned history.
|
|
1150
|
+
if (this.sensitiveToolResultRedactions.size > 0) {
|
|
1151
|
+
this.config.onStream?.({ type: "tombstone", messageId: assistantMessageId });
|
|
1152
|
+
this.currentTurnLog.warn("turn.streaming_fallback_skipped_sensitive", {
|
|
1153
|
+
cat: "turn",
|
|
1154
|
+
error: err.message,
|
|
1155
|
+
});
|
|
1156
|
+
throw err;
|
|
1157
|
+
}
|
|
963
1158
|
// Streaming might have partially emitted — send tombstone to revoke
|
|
964
|
-
this.config.onStream?.({ type: "tombstone", messageId:
|
|
1159
|
+
this.config.onStream?.({ type: "tombstone", messageId: assistantMessageId });
|
|
965
1160
|
this.currentTurnLog.warn("turn.streaming_fallback", {
|
|
966
1161
|
cat: "turn",
|
|
967
1162
|
error: err.message,
|
|
968
1163
|
});
|
|
969
1164
|
// Retry without streaming
|
|
970
|
-
return await this.deps.model.callWithoutStreaming(this.deps.systemPrompt, messages, this.deps.tools, this.config.signal);
|
|
1165
|
+
return await this.deps.model.callWithoutStreaming(this.deps.systemPrompt, messages, this.deps.tools, this.config.signal, this.modelCallRecordingOptions());
|
|
971
1166
|
}
|
|
972
1167
|
}
|
|
973
1168
|
get currentTurn() {
|
|
974
1169
|
return this.turnCount;
|
|
975
1170
|
}
|
|
1171
|
+
consumeQueuedSteer(messages, source) {
|
|
1172
|
+
const steered = this.deps.consumeSteer?.(source) ?? [];
|
|
1173
|
+
let consumed = false;
|
|
1174
|
+
for (const { id, text, clientMessageId } of steered) {
|
|
1175
|
+
if (!text)
|
|
1176
|
+
continue;
|
|
1177
|
+
if (clientMessageId && this.deps.claimClientMessageId?.(clientMessageId, "steer") === false) {
|
|
1178
|
+
logger.info("steer.submit.duplicate_ignored", {
|
|
1179
|
+
clientMessageId,
|
|
1180
|
+
steerId: id,
|
|
1181
|
+
sessionId: this.deps.sessionId,
|
|
1182
|
+
source,
|
|
1183
|
+
});
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
consumed = true;
|
|
1187
|
+
messages.push({ role: "user", content: text });
|
|
1188
|
+
this.deps.transcript.appendMessage("user", text, { steerId: id, clientMessageId });
|
|
1189
|
+
this.config.onStream?.({ type: "steer_injected", text, id });
|
|
1190
|
+
}
|
|
1191
|
+
return consumed;
|
|
1192
|
+
}
|
|
976
1193
|
/**
|
|
977
1194
|
* Generate synthetic error tool_results for any dangling tool_use blocks
|
|
978
1195
|
* that never received results (e.g. because the API call failed).
|