@cjhyy/code-shell-core 0.6.0-rc.16 → 0.6.0-rc.18
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/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cli/agent-server-stdio.js +2 -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.js +5 -5
- package/dist/credentials/use-credential-tool.d.ts +8 -1
- package/dist/credentials/use-credential-tool.js +55 -45
- package/dist/engine/engine.d.ts +3 -0
- package/dist/engine/engine.js +40 -13
- 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 +4 -4
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- 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 +4 -0
- package/dist/engine/turn-loop.js +106 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/preset/index.js +10 -9
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +18 -2
- package/dist/protocol/chat-session.d.ts +3 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +9 -4
- package/dist/protocol/client.js +18 -1
- package/dist/protocol/server.d.ts +12 -0
- package/dist/protocol/server.js +116 -38
- package/dist/protocol/types.d.ts +37 -0
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/tool-system/builtin/drive-claude-code.d.ts +1 -0
- package/dist/tool-system/builtin/drive-claude-code.js +137 -18
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +15 -13
- 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 +114 -5
- package/dist/tool-system/builtin/view-image.js +9 -0
- 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 +2 -0
- package/dist/tool-system/mcp-manager.js +23 -8
- package/dist/tool-system/path-policy.js +13 -0
- 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/types.d.ts +23 -4
- package/package.json +4 -3
|
@@ -30,7 +30,7 @@ export class ModelFacade {
|
|
|
30
30
|
this.client = client;
|
|
31
31
|
this.transcript = transcript;
|
|
32
32
|
}
|
|
33
|
-
async call(systemPrompt, messages, tools, onStream, signal) {
|
|
33
|
+
async call(systemPrompt, messages, tools, onStream, signal, recordingOptions) {
|
|
34
34
|
const startMs = Date.now();
|
|
35
35
|
const msgCount = messages.length;
|
|
36
36
|
const sid = getCurrentSid();
|
|
@@ -42,7 +42,7 @@ export class ModelFacade {
|
|
|
42
42
|
// Image base64 payloads stay out of recorded prompts — see
|
|
43
43
|
// logging/sanitize-messages.ts. Transcripts keep the full bytes
|
|
44
44
|
// (needed for replay); logs do not.
|
|
45
|
-
messages: sanitizeMessages(messages),
|
|
45
|
+
messages: sanitizeMessages(messages, recordingOptions),
|
|
46
46
|
tools,
|
|
47
47
|
systemPrompt,
|
|
48
48
|
}, reqId);
|
|
@@ -111,7 +111,7 @@ export class ModelFacade {
|
|
|
111
111
|
/**
|
|
112
112
|
* Call without streaming — used as fallback when streaming fails.
|
|
113
113
|
*/
|
|
114
|
-
async callWithoutStreaming(systemPrompt, messages, tools, signal) {
|
|
114
|
+
async callWithoutStreaming(systemPrompt, messages, tools, signal, recordingOptions) {
|
|
115
115
|
const startMs = Date.now();
|
|
116
116
|
const msgCount = messages.length;
|
|
117
117
|
const sid = getCurrentSid();
|
|
@@ -122,7 +122,7 @@ export class ModelFacade {
|
|
|
122
122
|
stream: false,
|
|
123
123
|
// Image base64 payloads stay out of recorded prompts — same rule
|
|
124
124
|
// as the streaming path above.
|
|
125
|
-
messages: sanitizeMessages(messages),
|
|
125
|
+
messages: sanitizeMessages(messages, recordingOptions),
|
|
126
126
|
tools,
|
|
127
127
|
systemPrompt,
|
|
128
128
|
}, reqId);
|
|
@@ -29,6 +29,16 @@ export interface ParsedImage {
|
|
|
29
29
|
dataUrl: string;
|
|
30
30
|
/** Just the base64 payload, with the data-URL prefix stripped. */
|
|
31
31
|
base64: string;
|
|
32
|
+
/** Stable on-disk path, usually cwd-relative for staged desktop attachments. */
|
|
33
|
+
path?: string;
|
|
34
|
+
/** Wire hash attr, usually `sha256:<hex>`. */
|
|
35
|
+
hash?: string;
|
|
36
|
+
/** Size in bytes from wire metadata. Core must still verify with stat when reading. */
|
|
37
|
+
size?: number;
|
|
38
|
+
/** Source channel that produced this attachment. */
|
|
39
|
+
origin?: string;
|
|
40
|
+
/** Session directory that owns the staged attachment. */
|
|
41
|
+
sessionId?: string;
|
|
32
42
|
}
|
|
33
43
|
export interface ParsedTask {
|
|
34
44
|
/** Remaining plain-text portion (image blocks removed, surrounding whitespace trimmed). */
|
|
@@ -117,6 +117,11 @@ export function parseTaskWithImages(task) {
|
|
|
117
117
|
name: attrs.name ?? "",
|
|
118
118
|
dataUrl: `data:${finalMime};base64,${base64}`,
|
|
119
119
|
base64,
|
|
120
|
+
...(attrs.path ? { path: attrs.path } : {}),
|
|
121
|
+
...(attrs.hash ? { hash: attrs.hash } : {}),
|
|
122
|
+
...(attrs.size && Number.isFinite(Number(attrs.size)) ? { size: Number(attrs.size) } : {}),
|
|
123
|
+
...(attrs.origin ? { origin: attrs.origin } : {}),
|
|
124
|
+
...(attrs.sessionid ? { sessionId: attrs.sessionid } : {}),
|
|
120
125
|
});
|
|
121
126
|
return "";
|
|
122
127
|
});
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Tool execution queue for a completed model response.
|
|
3
|
+
*
|
|
4
|
+
* Despite the historical name, TurnLoop currently enqueues tool calls only
|
|
5
|
+
* after `callModelWithFallback()` returns a complete LLMResponse. The queue's
|
|
6
|
+
* job is concurrency policy inside that post-response batch: start
|
|
7
|
+
* concurrency-safe tools as soon as they are enqueued, keep unsafe tools
|
|
8
|
+
* sequential, and return results in original tool-call order.
|
|
5
9
|
*
|
|
6
10
|
* Usage:
|
|
7
11
|
* const queue = new StreamingToolQueue(executor);
|
|
8
|
-
* //
|
|
12
|
+
* // After the full LLMResponse is available:
|
|
9
13
|
* queue.enqueue(toolCall);
|
|
10
|
-
* // After streaming completes:
|
|
11
14
|
* const results = await queue.drain();
|
|
12
15
|
*/
|
|
13
16
|
import type { ToolCall, ToolResult } from "../types.js";
|
|
@@ -21,8 +24,9 @@ export declare class StreamingToolQueue {
|
|
|
21
24
|
private draining;
|
|
22
25
|
constructor(executor: ToolExecutor);
|
|
23
26
|
/**
|
|
24
|
-
* Enqueue a tool
|
|
25
|
-
* unsafe tools are queued for
|
|
27
|
+
* Enqueue a tool from the completed response. Concurrency-safe tools start
|
|
28
|
+
* immediately within this post-response phase; unsafe tools are queued for
|
|
29
|
+
* sequential execution during drain().
|
|
26
30
|
*/
|
|
27
31
|
enqueue(call: ToolCall): void;
|
|
28
32
|
/**
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Tool execution queue for a completed model response.
|
|
3
|
+
*
|
|
4
|
+
* Despite the historical name, TurnLoop currently enqueues tool calls only
|
|
5
|
+
* after `callModelWithFallback()` returns a complete LLMResponse. The queue's
|
|
6
|
+
* job is concurrency policy inside that post-response batch: start
|
|
7
|
+
* concurrency-safe tools as soon as they are enqueued, keep unsafe tools
|
|
8
|
+
* sequential, and return results in original tool-call order.
|
|
5
9
|
*
|
|
6
10
|
* Usage:
|
|
7
11
|
* const queue = new StreamingToolQueue(executor);
|
|
8
|
-
* //
|
|
12
|
+
* // After the full LLMResponse is available:
|
|
9
13
|
* queue.enqueue(toolCall);
|
|
10
|
-
* // After streaming completes:
|
|
11
14
|
* const results = await queue.drain();
|
|
12
15
|
*/
|
|
13
16
|
export class StreamingToolQueue {
|
|
@@ -21,8 +24,9 @@ export class StreamingToolQueue {
|
|
|
21
24
|
this.executor = executor;
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
|
-
* Enqueue a tool
|
|
25
|
-
* unsafe tools are queued for
|
|
27
|
+
* Enqueue a tool from the completed response. Concurrency-safe tools start
|
|
28
|
+
* immediately within this post-response phase; unsafe tools are queued for
|
|
29
|
+
* sequential execution during drain().
|
|
26
30
|
*/
|
|
27
31
|
enqueue(call) {
|
|
28
32
|
this.callOrder.push(call.id);
|
|
@@ -143,6 +143,7 @@ export declare class TurnLoop {
|
|
|
143
143
|
private lastCtxEmit;
|
|
144
144
|
private currentTurnUsage;
|
|
145
145
|
private currentCumulativeUsage;
|
|
146
|
+
private readonly sensitiveToolResultRedactions;
|
|
146
147
|
private readonly pendingImageMessages;
|
|
147
148
|
/**
|
|
148
149
|
* Consecutive on_stop blocks (Goal mode kept the agent going). Reset to 0
|
|
@@ -203,6 +204,8 @@ export declare class TurnLoop {
|
|
|
203
204
|
private markStopped;
|
|
204
205
|
private prepareMessagesForModel;
|
|
205
206
|
private markPendingImagesConsumed;
|
|
207
|
+
private redactConsumedSensitiveToolResults;
|
|
208
|
+
private modelCallRecordingOptions;
|
|
206
209
|
private trackFreshImageMessage;
|
|
207
210
|
private emitHook;
|
|
208
211
|
/**
|
|
@@ -219,6 +222,7 @@ export declare class TurnLoop {
|
|
|
219
222
|
* Run the multi-turn agent loop until completion.
|
|
220
223
|
*/
|
|
221
224
|
run(initialMessages: Message[]): Promise<TurnLoopResult>;
|
|
225
|
+
private runUnredacted;
|
|
222
226
|
/**
|
|
223
227
|
* Call model with streaming fallback.
|
|
224
228
|
* If streaming fails, emit tombstone and retry non-streaming.
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -17,6 +17,7 @@ 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";
|
|
20
21
|
import { addTokenUsage, cacheHitRateFromUsage, cumulativeCacheHitRate, } from "./session-usage.js";
|
|
21
22
|
import { createGoalBudgetTracker, recordGoalUsage, goalBudgetExceeded, applyGoalExtension, limitProximity, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "./goal.js";
|
|
22
23
|
/**
|
|
@@ -59,6 +60,7 @@ export class TurnLoop {
|
|
|
59
60
|
cacheCreationTokens: 0,
|
|
60
61
|
};
|
|
61
62
|
currentCumulativeUsage;
|
|
63
|
+
sensitiveToolResultRedactions = new Map();
|
|
62
64
|
pendingImageMessages = new Set();
|
|
63
65
|
/**
|
|
64
66
|
* Consecutive on_stop blocks (Goal mode kept the agent going). Reset to 0
|
|
@@ -237,6 +239,20 @@ export class TurnLoop {
|
|
|
237
239
|
}
|
|
238
240
|
return result.messages;
|
|
239
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
|
+
}
|
|
240
256
|
trackFreshImageMessage(message) {
|
|
241
257
|
if (messageHasBase64ImagePayload(message)) {
|
|
242
258
|
this.pendingImageMessages.add(message);
|
|
@@ -356,6 +372,10 @@ export class TurnLoop {
|
|
|
356
372
|
* Run the multi-turn agent loop until completion.
|
|
357
373
|
*/
|
|
358
374
|
async run(initialMessages) {
|
|
375
|
+
const result = await this.runUnredacted(initialMessages);
|
|
376
|
+
return { ...result, messages: this.redactConsumedSensitiveToolResults(result.messages) };
|
|
377
|
+
}
|
|
378
|
+
async runUnredacted(initialMessages) {
|
|
359
379
|
let messages = [...initialMessages];
|
|
360
380
|
let finalText = "";
|
|
361
381
|
const budgetTracker = createBudgetTracker();
|
|
@@ -414,13 +434,18 @@ export class TurnLoop {
|
|
|
414
434
|
// returns; instead, each return-causing branch logs its own terminal
|
|
415
435
|
// event (model_error, completed, etc.).
|
|
416
436
|
const turnId = newTurnId();
|
|
437
|
+
const assistantMessageId = `assistant_${turnId}`;
|
|
417
438
|
const tlog = logger.child({ turn: this.turnCount, turnId });
|
|
418
439
|
this.currentTurnLog = tlog;
|
|
419
440
|
const turnStartedAt = Date.now();
|
|
420
441
|
tlog.info("turn.start", { cat: "turn", messageCount: messages.length });
|
|
421
442
|
// Tag downstream tool-exec / permission lines with this turn's IDs.
|
|
422
443
|
this.deps.toolExecutor.setLogger(tlog);
|
|
423
|
-
this.config.onStream?.({
|
|
444
|
+
this.config.onStream?.({
|
|
445
|
+
type: "stream_request_start",
|
|
446
|
+
turnNumber: this.turnCount,
|
|
447
|
+
messageId: assistantMessageId,
|
|
448
|
+
});
|
|
424
449
|
const turnStartHook = await this.emitHook("on_turn_start", {
|
|
425
450
|
turnNumber: this.turnCount,
|
|
426
451
|
});
|
|
@@ -460,16 +485,25 @@ export class TurnLoop {
|
|
|
460
485
|
// Pre-check: downgrade image payloads that have already had their one
|
|
461
486
|
// model-consumption turn, then run context management. Fresh images in
|
|
462
487
|
// pendingImageMessages are preserved through this next model request.
|
|
488
|
+
const hasPendingSensitiveToolResults = this.sensitiveToolResultRedactions.size > 0;
|
|
463
489
|
messages = this.prepareMessagesForModel(messages);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
+
}
|
|
473
507
|
}
|
|
474
508
|
// No pre-llm ctx emit here: the messages-only estimate would be ~16k
|
|
475
509
|
// smaller than the real prompt (system + tools not included), making
|
|
@@ -483,7 +517,9 @@ export class TurnLoop {
|
|
|
483
517
|
// call. Microcompact is lossless (just clearing redundant
|
|
484
518
|
// tool_results) so we suppress hook emits for it to keep token
|
|
485
519
|
// overhead down.
|
|
486
|
-
const pending =
|
|
520
|
+
const pending = hasPendingSensitiveToolResults
|
|
521
|
+
? null
|
|
522
|
+
: this.deps.consumePendingCompactInfo?.();
|
|
487
523
|
if (pending && pending.strategy !== "micro") {
|
|
488
524
|
const compactHook = await this.emitHook("post_compact", {
|
|
489
525
|
strategy: pending.strategy,
|
|
@@ -498,11 +534,12 @@ export class TurnLoop {
|
|
|
498
534
|
// Model call (with streaming fallback and max_output_tokens continuation)
|
|
499
535
|
// Track tool IDs streamed during this turn to avoid duplicate UI events
|
|
500
536
|
this.streamedToolIds.clear();
|
|
501
|
-
//
|
|
537
|
+
// Tool queue is created before the call, but enqueue happens only after
|
|
538
|
+
// the complete LLMResponse is available below.
|
|
502
539
|
const streamingQueue = new StreamingToolQueue(this.deps.toolExecutor);
|
|
503
540
|
let response;
|
|
504
541
|
try {
|
|
505
|
-
response = await this.callModelWithFallback(messages);
|
|
542
|
+
response = await this.callModelWithFallback(messages, assistantMessageId);
|
|
506
543
|
}
|
|
507
544
|
catch (err) {
|
|
508
545
|
if (err instanceof ContextLimitError) {
|
|
@@ -513,13 +550,14 @@ export class TurnLoop {
|
|
|
513
550
|
tlog.warn("turn.ptl_recovery", { cat: "turn", retry, roundsToDrop: retry });
|
|
514
551
|
messages = dropOldestRounds(messages, retry);
|
|
515
552
|
try {
|
|
516
|
-
response = await this.callModelWithFallback(messages);
|
|
553
|
+
response = await this.callModelWithFallback(messages, assistantMessageId);
|
|
517
554
|
recovered = true;
|
|
518
555
|
break;
|
|
519
556
|
}
|
|
520
557
|
catch (retryErr) {
|
|
521
558
|
if (!(retryErr instanceof ContextLimitError)) {
|
|
522
559
|
this.config.onStream?.({ type: "error", error: formatFriendlyError(retryErr) });
|
|
560
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
523
561
|
return { text: finalText, reason: "model_error", messages };
|
|
524
562
|
}
|
|
525
563
|
}
|
|
@@ -530,6 +568,7 @@ export class TurnLoop {
|
|
|
530
568
|
type: "error",
|
|
531
569
|
error: "Context limit exceeded after 3 recovery attempts",
|
|
532
570
|
});
|
|
571
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
533
572
|
return { text: finalText, reason: "prompt_too_long", messages };
|
|
534
573
|
}
|
|
535
574
|
}
|
|
@@ -543,14 +582,17 @@ export class TurnLoop {
|
|
|
543
582
|
// interrupted turn folds behind the process-card header on reload).
|
|
544
583
|
this.patchOrphanedToolUses(messages);
|
|
545
584
|
this.markStopped();
|
|
585
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
546
586
|
return { text: finalText, reason: "aborted_streaming", messages };
|
|
547
587
|
}
|
|
548
588
|
else {
|
|
549
589
|
this.patchOrphanedToolUses(messages);
|
|
550
590
|
this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
|
|
591
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
551
592
|
return { text: finalText, reason: "model_error", messages };
|
|
552
593
|
}
|
|
553
594
|
}
|
|
595
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
554
596
|
// Record the response once into current-turn and whole-session counters.
|
|
555
597
|
if (response.usage?.promptTokens !== undefined) {
|
|
556
598
|
this.recordResponseUsage(response.usage);
|
|
@@ -659,6 +701,7 @@ export class TurnLoop {
|
|
|
659
701
|
});
|
|
660
702
|
this.config.onStream?.({
|
|
661
703
|
type: "assistant_message",
|
|
704
|
+
messageId: assistantMessageId,
|
|
662
705
|
message: {
|
|
663
706
|
role: "assistant",
|
|
664
707
|
content: "(Goal 预算已耗尽,强制停止。)",
|
|
@@ -671,6 +714,7 @@ export class TurnLoop {
|
|
|
671
714
|
// No tool use — final answer
|
|
672
715
|
this.config.onStream?.({
|
|
673
716
|
type: "assistant_message",
|
|
717
|
+
messageId: assistantMessageId,
|
|
674
718
|
message: { role: "assistant", content: finalText },
|
|
675
719
|
});
|
|
676
720
|
await this.emitHook("on_turn_end", {
|
|
@@ -746,6 +790,7 @@ export class TurnLoop {
|
|
|
746
790
|
});
|
|
747
791
|
this.config.onStream?.({
|
|
748
792
|
type: "assistant_message",
|
|
793
|
+
messageId: assistantMessageId,
|
|
749
794
|
message: {
|
|
750
795
|
role: "assistant",
|
|
751
796
|
content: `(Goal 续跑已达 ${maxStopBlocks} 次上限,先停下。)`,
|
|
@@ -765,6 +810,7 @@ export class TurnLoop {
|
|
|
765
810
|
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
766
811
|
continue;
|
|
767
812
|
}
|
|
813
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
768
814
|
return { text: finalText, reason: "completed", messages };
|
|
769
815
|
}
|
|
770
816
|
// Tool execution phase
|
|
@@ -809,8 +855,13 @@ export class TurnLoop {
|
|
|
809
855
|
const resultBlocks = [];
|
|
810
856
|
for (const result of results) {
|
|
811
857
|
resultBlocks.push(toolResultToBlock(result));
|
|
812
|
-
|
|
813
|
-
|
|
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 });
|
|
814
865
|
}
|
|
815
866
|
// Fire-and-forget tool use summary (non-blocking). The whole chain is
|
|
816
867
|
// best-effort observability — a thrown onStream handler, a failed dynamic
|
|
@@ -821,9 +872,13 @@ export class TurnLoop {
|
|
|
821
872
|
.then(({ generateToolUseSummary }) => {
|
|
822
873
|
if (!this.deps.model.summarize)
|
|
823
874
|
return;
|
|
824
|
-
return generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
|
|
875
|
+
return generateToolUseSummary(toolCalls, toolResultsForDisplay(results), this.deps.model.summarize).then((summary) => {
|
|
825
876
|
if (summary) {
|
|
826
|
-
this.config.onStream?.({
|
|
877
|
+
this.config.onStream?.({
|
|
878
|
+
type: "tool_summary",
|
|
879
|
+
summary,
|
|
880
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id),
|
|
881
|
+
});
|
|
827
882
|
}
|
|
828
883
|
});
|
|
829
884
|
})
|
|
@@ -878,6 +933,7 @@ export class TurnLoop {
|
|
|
878
933
|
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
879
934
|
continue;
|
|
880
935
|
}
|
|
936
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
881
937
|
return { text: finalText, reason: "completed", messages };
|
|
882
938
|
}
|
|
883
939
|
// Goal mode: user-initiated cancellation. cancel_goal is the "strong
|
|
@@ -906,12 +962,14 @@ export class TurnLoop {
|
|
|
906
962
|
});
|
|
907
963
|
this.config.onStream?.({
|
|
908
964
|
type: "assistant_message",
|
|
965
|
+
messageId: assistantMessageId,
|
|
909
966
|
message: { role: "assistant", content: finalText },
|
|
910
967
|
});
|
|
911
968
|
messages.push({ role: "assistant", content: finalText });
|
|
912
969
|
if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
913
970
|
continue;
|
|
914
971
|
}
|
|
972
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
915
973
|
return { text: finalText, reason: "completed", messages };
|
|
916
974
|
}
|
|
917
975
|
if (budgetDecision === "nudge") {
|
|
@@ -972,6 +1030,7 @@ export class TurnLoop {
|
|
|
972
1030
|
// no error event, so the UI shows only the "你停止了本轮" line.
|
|
973
1031
|
if (isAbortError(err) || this.config.signal?.aborted) {
|
|
974
1032
|
this.markStopped();
|
|
1033
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
975
1034
|
return { text: finalText, reason: "aborted_streaming", messages };
|
|
976
1035
|
}
|
|
977
1036
|
this.currentTurnLog.error("turn.unhandled_error", {
|
|
@@ -980,6 +1039,7 @@ export class TurnLoop {
|
|
|
980
1039
|
stack: err.stack?.split("\n").slice(0, 4).join("\n"),
|
|
981
1040
|
});
|
|
982
1041
|
this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
|
|
1042
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
983
1043
|
return { text: finalText, reason: "model_error", messages };
|
|
984
1044
|
}
|
|
985
1045
|
// Max turns reached — do one final summarization call (no tools)
|
|
@@ -989,8 +1049,18 @@ export class TurnLoop {
|
|
|
989
1049
|
turnCount: this.turnCount,
|
|
990
1050
|
});
|
|
991
1051
|
this.consumeQueuedSteer(messages, "finalize_backfill");
|
|
1052
|
+
const hasPendingSensitiveToolResults = this.sensitiveToolResultRedactions.size > 0;
|
|
992
1053
|
messages = this.prepareMessagesForModel(messages);
|
|
993
|
-
|
|
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
|
+
}
|
|
994
1064
|
messages.push({
|
|
995
1065
|
role: "user",
|
|
996
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>",
|
|
@@ -998,7 +1068,7 @@ export class TurnLoop {
|
|
|
998
1068
|
this.emitCtxFromMessages(messages);
|
|
999
1069
|
try {
|
|
1000
1070
|
const summaryResponse = await this.deps.model.call(this.deps.systemPrompt, this.prepareMessagesForModel(messages), [], // No tools available for summary turn
|
|
1001
|
-
this.config.onStream, this.config.signal);
|
|
1071
|
+
this.config.onStream, this.config.signal, this.modelCallRecordingOptions());
|
|
1002
1072
|
messages = this.markPendingImagesConsumed(messages);
|
|
1003
1073
|
if (summaryResponse.text) {
|
|
1004
1074
|
finalText = summaryResponse.text;
|
|
@@ -1015,14 +1085,14 @@ export class TurnLoop {
|
|
|
1015
1085
|
});
|
|
1016
1086
|
messages.push({ role: "assistant", content: finalText });
|
|
1017
1087
|
}
|
|
1018
|
-
this.
|
|
1088
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1019
1089
|
return { text: finalText, reason: "max_turns", messages };
|
|
1020
1090
|
}
|
|
1021
1091
|
/**
|
|
1022
1092
|
* Call model with streaming fallback.
|
|
1023
1093
|
* If streaming fails, emit tombstone and retry non-streaming.
|
|
1024
1094
|
*/
|
|
1025
|
-
async callModelWithFallback(messages) {
|
|
1095
|
+
async callModelWithFallback(messages, assistantMessageId) {
|
|
1026
1096
|
// Wrap stream callback to track tool_use_start events and reactive compaction
|
|
1027
1097
|
let streamingResponseTokens = 0;
|
|
1028
1098
|
let reactiveBucket = -1;
|
|
@@ -1054,7 +1124,7 @@ export class TurnLoop {
|
|
|
1054
1124
|
}
|
|
1055
1125
|
: undefined;
|
|
1056
1126
|
try {
|
|
1057
|
-
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());
|
|
1058
1128
|
}
|
|
1059
1129
|
catch (err) {
|
|
1060
1130
|
// If it's a context or rate limit error, don't fallback — propagate
|
|
@@ -1074,14 +1144,25 @@ export class TurnLoop {
|
|
|
1074
1144
|
// really just a cancel. The signal is the authoritative cancel source.
|
|
1075
1145
|
if (isAbortError(err) || this.config.signal?.aborted)
|
|
1076
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
|
+
}
|
|
1077
1158
|
// Streaming might have partially emitted — send tombstone to revoke
|
|
1078
|
-
this.config.onStream?.({ type: "tombstone", messageId:
|
|
1159
|
+
this.config.onStream?.({ type: "tombstone", messageId: assistantMessageId });
|
|
1079
1160
|
this.currentTurnLog.warn("turn.streaming_fallback", {
|
|
1080
1161
|
cat: "turn",
|
|
1081
1162
|
error: err.message,
|
|
1082
1163
|
});
|
|
1083
1164
|
// Retry without streaming
|
|
1084
|
-
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());
|
|
1085
1166
|
}
|
|
1086
1167
|
}
|
|
1087
1168
|
get currentTurn() {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "0.6.0-rc.
|
|
6
|
+
export declare const VERSION = "0.6.0-rc.18";
|
|
7
7
|
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionWorkspace, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
export { Engine, loadAgentDefinitionsForCwd } from "./engine/engine.js";
|
|
@@ -109,7 +109,7 @@ export { migrateConfig, configVersionOf, CURRENT_CONFIG_VERSION, type MigrationS
|
|
|
109
109
|
export { SettingsSchema, validateSettings } from "./settings/schema.js";
|
|
110
110
|
export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
|
|
111
111
|
export { personalizationFrom, type PersonalizationConfig } from "./settings/personalization.js";
|
|
112
|
-
export { CredentialStore, type CredentialScope, type MaskedCredential, type Credential, type CredentialType, type CredentialStoreFile, formatNetscapeCookies, parseCookieJar, type CookieLike, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, type EncryptionCipher, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
|
|
112
|
+
export { CredentialStore, type CredentialScope, type MaskedCredential, type Credential, type CredentialType, type CredentialStoreFile, formatNetscapeCookies, parseCookieJar, type CookieLike, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, type CredentialAccess, type CredentialAccessScope, type CredentialMetadata, type CredentialSnapshot, type CredentialSnapshotEntry, type EncryptionCipher, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
|
|
113
113
|
/** @internal Shared with the in-repo TUI/desktop hosts; not stable SDK surface. */
|
|
114
114
|
export { getSessionId, switchSession, getOriginalCwd, setOriginalCwd, getProjectRoot, setProjectRoot, getCwdState, getIsInteractive, updateLastInteractionTime, flushInteractionTime, markScrollActivity, type AttributedCounter, type ChannelEntry, } from "./state.js";
|
|
115
115
|
/** @internal Shared primitives for the in-repo TUI/desktop hosts; not stable SDK surface. */
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.6.0-rc.
|
|
6
|
+
export const VERSION = "0.6.0-rc.18";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Engine (primary API) ────────────────────────────────────────
|
|
@@ -127,7 +127,7 @@ export { migrateConfig, configVersionOf, CURRENT_CONFIG_VERSION, } from "./setti
|
|
|
127
127
|
export { SettingsSchema, validateSettings } from "./settings/schema.js";
|
|
128
128
|
export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
|
|
129
129
|
export { personalizationFrom } from "./settings/personalization.js";
|
|
130
|
-
export { CredentialStore, formatNetscapeCookies, parseCookieJar, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
|
|
130
|
+
export { CredentialStore, formatNetscapeCookies, parseCookieJar, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
|
|
131
131
|
// ─── State (runtime singletons shared with TUI) ──────────────────
|
|
132
132
|
/** @internal Shared with the in-repo TUI/desktop hosts; not stable SDK surface. */
|
|
133
133
|
export { getSessionId, switchSession, getOriginalCwd, setOriginalCwd, getProjectRoot, setProjectRoot, getCwdState, getIsInteractive, updateLastInteractionTime, flushInteractionTime, markScrollActivity, } from "./state.js";
|
|
@@ -21,17 +21,25 @@
|
|
|
21
21
|
* pure-text turns pay essentially nothing on the fast path.
|
|
22
22
|
*/
|
|
23
23
|
import type { Message } from "../types.js";
|
|
24
|
+
export interface SanitizeMessagesOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Tool-result IDs whose model-facing plaintext must be replaced before the
|
|
27
|
+
* request is written to any recorder/log sink. The original messages still
|
|
28
|
+
* go to the provider; this option is only for diagnostic copies.
|
|
29
|
+
*/
|
|
30
|
+
sensitiveToolResultRedactions?: ReadonlyMap<string, string>;
|
|
31
|
+
}
|
|
24
32
|
/**
|
|
25
33
|
* Walk a single message's content and replace image-payload blocks with
|
|
26
34
|
* metadata stubs. Returns a new value — the input is not mutated.
|
|
27
35
|
*/
|
|
28
|
-
export declare function sanitizeContent(content: Message["content"]): Message["content"];
|
|
36
|
+
export declare function sanitizeContent(content: Message["content"], options?: SanitizeMessagesOptions): Message["content"];
|
|
29
37
|
/**
|
|
30
38
|
* Walk a {@link Message}[] array and sanitize every entry's content.
|
|
31
39
|
* Returns the *same* array reference when no message held an image (fast
|
|
32
40
|
* path for the overwhelmingly common pure-text case).
|
|
33
41
|
*/
|
|
34
|
-
export declare function sanitizeMessages(messages: readonly Message[]): Message[];
|
|
42
|
+
export declare function sanitizeMessages(messages: readonly Message[], options?: SanitizeMessagesOptions): Message[];
|
|
35
43
|
/**
|
|
36
44
|
* Recursively walk a value and return a *new* value with secret-looking
|
|
37
45
|
* fields replaced by "[redacted]". Used by the logger (so anything ending up
|
|
@@ -75,19 +75,28 @@ function sanitizeImageUrlPart(part) {
|
|
|
75
75
|
image_url: {
|
|
76
76
|
// Keep the data-URL header so reviewers can see the MIME, but strip
|
|
77
77
|
// the payload. "data:image/png;base64,<omitted, 12345 bytes>"
|
|
78
|
-
url: comma === -1
|
|
79
|
-
? url
|
|
80
|
-
: `${url.slice(0, comma + 1)}<omitted, ${payloadLen} bytes>`,
|
|
78
|
+
url: comma === -1 ? url : `${url.slice(0, comma + 1)}<omitted, ${payloadLen} bytes>`,
|
|
81
79
|
omitted: true,
|
|
82
80
|
bytes: payloadLen,
|
|
83
81
|
},
|
|
84
82
|
};
|
|
85
83
|
}
|
|
84
|
+
function getSensitiveToolResultReplacement(block, options) {
|
|
85
|
+
const redactions = options?.sensitiveToolResultRedactions;
|
|
86
|
+
if (!redactions || redactions.size === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (!block || typeof block !== "object")
|
|
89
|
+
return undefined;
|
|
90
|
+
const b = block;
|
|
91
|
+
if (b.type !== "tool_result" || typeof b.tool_use_id !== "string")
|
|
92
|
+
return undefined;
|
|
93
|
+
return redactions.get(b.tool_use_id);
|
|
94
|
+
}
|
|
86
95
|
/**
|
|
87
96
|
* Walk a single message's content and replace image-payload blocks with
|
|
88
97
|
* metadata stubs. Returns a new value — the input is not mutated.
|
|
89
98
|
*/
|
|
90
|
-
export function sanitizeContent(content) {
|
|
99
|
+
export function sanitizeContent(content, options) {
|
|
91
100
|
if (typeof content === "string")
|
|
92
101
|
return content;
|
|
93
102
|
if (!Array.isArray(content))
|
|
@@ -95,6 +104,12 @@ export function sanitizeContent(content) {
|
|
|
95
104
|
let touched = false;
|
|
96
105
|
const out = [];
|
|
97
106
|
for (const block of content) {
|
|
107
|
+
const sensitiveReplacement = getSensitiveToolResultReplacement(block, options);
|
|
108
|
+
if (sensitiveReplacement !== undefined) {
|
|
109
|
+
out.push({ ...block, content: sensitiveReplacement });
|
|
110
|
+
touched = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
98
113
|
if (isImageBlock(block)) {
|
|
99
114
|
out.push(sanitizeImageBlock(block));
|
|
100
115
|
touched = true;
|
|
@@ -114,11 +129,11 @@ export function sanitizeContent(content) {
|
|
|
114
129
|
* Returns the *same* array reference when no message held an image (fast
|
|
115
130
|
* path for the overwhelmingly common pure-text case).
|
|
116
131
|
*/
|
|
117
|
-
export function sanitizeMessages(messages) {
|
|
132
|
+
export function sanitizeMessages(messages, options) {
|
|
118
133
|
let touched = false;
|
|
119
134
|
const out = [];
|
|
120
135
|
for (const m of messages) {
|
|
121
|
-
const sanitized = sanitizeContent(m.content);
|
|
136
|
+
const sanitized = sanitizeContent(m.content, options);
|
|
122
137
|
if (sanitized !== m.content) {
|
|
123
138
|
touched = true;
|
|
124
139
|
out.push({ ...m, content: sanitized });
|