@cjhyy/code-shell-core 0.6.0-rc.17 → 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.
Files changed (64) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  5. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  6. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  7. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  8. package/dist/cli/agent-server-stdio.js +2 -0
  9. package/dist/credentials/access.d.ts +56 -0
  10. package/dist/credentials/access.js +183 -0
  11. package/dist/credentials/index.d.ts +1 -0
  12. package/dist/credentials/index.js +1 -0
  13. package/dist/credentials/inject-credential-tool.js +5 -5
  14. package/dist/credentials/use-credential-tool.d.ts +8 -1
  15. package/dist/credentials/use-credential-tool.js +55 -45
  16. package/dist/engine/engine.d.ts +3 -0
  17. package/dist/engine/engine.js +40 -13
  18. package/dist/engine/image-policy.d.ts +6 -0
  19. package/dist/engine/image-policy.js +17 -6
  20. package/dist/engine/input-attachments.d.ts +13 -0
  21. package/dist/engine/input-attachments.js +255 -0
  22. package/dist/engine/model-facade.d.ts +5 -2
  23. package/dist/engine/model-facade.js +4 -4
  24. package/dist/engine/parse-task.d.ts +10 -0
  25. package/dist/engine/parse-task.js +5 -0
  26. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  27. package/dist/engine/streaming-tool-queue.js +11 -7
  28. package/dist/engine/turn-loop.d.ts +4 -0
  29. package/dist/engine/turn-loop.js +106 -25
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +2 -2
  32. package/dist/logging/sanitize-messages.d.ts +10 -2
  33. package/dist/logging/sanitize-messages.js +21 -6
  34. package/dist/preset/index.js +10 -9
  35. package/dist/protocol/chat-session-manager.d.ts +1 -0
  36. package/dist/protocol/chat-session-manager.js +18 -2
  37. package/dist/protocol/chat-session.d.ts +3 -0
  38. package/dist/protocol/chat-session.js +1 -0
  39. package/dist/protocol/client.d.ts +9 -4
  40. package/dist/protocol/client.js +18 -1
  41. package/dist/protocol/server.d.ts +12 -0
  42. package/dist/protocol/server.js +116 -38
  43. package/dist/protocol/types.d.ts +37 -0
  44. package/dist/runtime/spawn-common.js +10 -0
  45. package/dist/tool-system/builtin/drive-claude-code.d.ts +1 -0
  46. package/dist/tool-system/builtin/drive-claude-code.js +137 -18
  47. package/dist/tool-system/builtin/index.d.ts +8 -3
  48. package/dist/tool-system/builtin/index.js +15 -13
  49. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  50. package/dist/tool-system/builtin/powershell.js +11 -7
  51. package/dist/tool-system/builtin/read.js +114 -5
  52. package/dist/tool-system/builtin/view-image.js +9 -0
  53. package/dist/tool-system/executor.d.ts +1 -5
  54. package/dist/tool-system/executor.js +94 -115
  55. package/dist/tool-system/mcp-manager.d.ts +2 -0
  56. package/dist/tool-system/mcp-manager.js +23 -8
  57. package/dist/tool-system/path-policy.js +13 -0
  58. package/dist/tool-system/permission.d.ts +28 -7
  59. package/dist/tool-system/permission.js +130 -49
  60. package/dist/tool-system/registry.js +11 -4
  61. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  62. package/dist/tool-system/tool-result-redaction.js +48 -0
  63. package/dist/types.d.ts +23 -4
  64. 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
- * Streaming tool queue enqueues tools for execution during streaming,
3
- * starts concurrency-safe tools immediately, queues unsafe tools for
4
- * sequential execution.
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
- * // During streaming, as tool_use blocks arrive:
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 for execution. Concurrency-safe tools start immediately;
25
- * unsafe tools are queued for sequential execution during drain().
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
- * Streaming tool queue enqueues tools for execution during streaming,
3
- * starts concurrency-safe tools immediately, queues unsafe tools for
4
- * sequential execution.
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
- * // During streaming, as tool_use blocks arrive:
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 for execution. Concurrency-safe tools start immediately;
25
- * unsafe tools are queued for sequential execution during drain().
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.
@@ -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?.({ type: "stream_request_start", turnNumber: this.turnCount });
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
- // Context management (async — may trigger LLM summarization)
465
- messages = await this.deps.contextManager.manageAsync(messages);
466
- // manageAsync can itself issue an LLM summarization call lasting several
467
- // seconds; if the signal aborted during it, stop here rather than
468
- // proceeding into the (expensive) main model call. Belt to the loop-top
469
- // brace: this catches an abort that landed *inside* context management.
470
- if (this.config.signal?.aborted) {
471
- this.markStopped();
472
- return { text: finalText, reason: "aborted_streaming", 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
+ }
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 = this.deps.consumePendingCompactInfo?.();
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
- // Streaming tool queue: start concurrency-safe tools during streaming
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
- this.deps.transcript.appendToolResult(result.id, result.toolName, result.result, result.error, result.contentBlocks);
813
- this.config.onStream?.({ type: "tool_result", result });
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?.({ type: "tool_summary", summary });
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
- messages = this.deps.contextManager.manage(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
+ }
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.config.onStream?.({ type: "turn_complete", reason: "max_turns" });
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: `turn_${this.turnCount}` });
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.15";
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.15";
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 });