@bubblebrain-ai/bubble 0.0.15 → 0.0.16

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/README.md CHANGED
@@ -62,6 +62,30 @@ Bubble stores user configuration, sessions, permissions, skills, and memory unde
62
62
 
63
63
  In the app, use `/login` or provider commands to configure model access.
64
64
 
65
+ ### ChatGPT Network Configuration
66
+
67
+ ChatGPT OAuth and GPT/Codex requests respect standard proxy variables:
68
+
69
+ ```bash
70
+ export HTTPS_PROXY=http://proxy.example.com:8080
71
+ export HTTP_PROXY=http://proxy.example.com:8080
72
+ export NO_PROXY=localhost,127.0.0.1
73
+ ```
74
+
75
+ If your network uses a corporate or custom HTTPS CA, start Bubble with:
76
+
77
+ ```bash
78
+ NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
79
+ ```
80
+
81
+ You can also use `BUBBLE_EXTRA_CA_CERTS` for Bubble's ChatGPT requests:
82
+
83
+ ```bash
84
+ BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
85
+ ```
86
+
87
+ Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
88
+
65
89
  ## Memory
66
90
 
67
91
  Bubble maintains persistent memory automatically from prior sessions. Useful commands:
@@ -0,0 +1,21 @@
1
+ import type { ContentPart, ParsedToolCall, ToolResult, ToolResultMetadata } from "../types.js";
2
+ export interface DiscoveryBarrierOptions {
3
+ cwd: string;
4
+ input: string | ContentPart[];
5
+ enabled: boolean;
6
+ }
7
+ export declare class DiscoveryBarrier {
8
+ private readonly cwd;
9
+ private readonly enabled;
10
+ private readonly knownPaths;
11
+ private readonly userMentionedPaths;
12
+ constructor(options: DiscoveryBarrierOptions);
13
+ isEnabled(): boolean;
14
+ shouldBufferStreamingToolCall(name: string): boolean;
15
+ orderToolCalls<T extends ParsedToolCall>(toolCalls: T[]): T[];
16
+ beforeToolCall(toolCall: ParsedToolCall): ToolResult | undefined;
17
+ afterToolCall(toolCall: ParsedToolCall, result: ToolResult): void;
18
+ observeMetadata(metadata: ToolResultMetadata | undefined): void;
19
+ }
20
+ export declare function isHiddenToolResult(result: ToolResult | undefined): result is ToolResult;
21
+ export declare function isHiddenToolMetadata(metadata: ToolResultMetadata | undefined): boolean;
@@ -0,0 +1,173 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import { analyzeToolIntent } from "./tool-intent.js";
4
+ import { resolveToolPath } from "../tools/path-utils.js";
5
+ export class DiscoveryBarrier {
6
+ cwd;
7
+ enabled;
8
+ knownPaths = new Set();
9
+ userMentionedPaths = new Set();
10
+ constructor(options) {
11
+ this.cwd = options.cwd;
12
+ this.enabled = options.enabled;
13
+ for (const path of extractUserMentionedPaths(options.input)) {
14
+ this.userMentionedPaths.add(canonicalPath(this.cwd, path));
15
+ }
16
+ }
17
+ isEnabled() {
18
+ return this.enabled;
19
+ }
20
+ shouldBufferStreamingToolCall(name) {
21
+ return this.enabled && (name === "read" || name === "bash" || name === "lsp");
22
+ }
23
+ orderToolCalls(toolCalls) {
24
+ if (!this.enabled || toolCalls.length < 2)
25
+ return toolCalls;
26
+ const discovery = [];
27
+ const rest = [];
28
+ for (const toolCall of toolCalls) {
29
+ if (isDiscoveryToolCall(toolCall))
30
+ discovery.push(toolCall);
31
+ else
32
+ rest.push(toolCall);
33
+ }
34
+ if (discovery.length === 0 || rest.length === 0)
35
+ return toolCalls;
36
+ return [...discovery, ...rest];
37
+ }
38
+ beforeToolCall(toolCall) {
39
+ if (!this.enabled)
40
+ return undefined;
41
+ const target = pathSensitiveReadTarget(toolCall);
42
+ if (!target)
43
+ return undefined;
44
+ const canonical = canonicalPath(this.cwd, target.path);
45
+ if (this.knownPaths.has(canonical) || this.userMentionedPaths.has(canonical))
46
+ return undefined;
47
+ return {
48
+ content: `Blocked speculative ${target.kind}. The path "${target.path}" has not been discovered in this repository context ` +
49
+ "and was not explicitly requested by the user. Run glob/grep/lsp discovery first, then read only returned paths. " +
50
+ "Do not infer from this blocked call whether the path exists.",
51
+ isError: true,
52
+ status: "blocked",
53
+ metadata: {
54
+ kind: "internal",
55
+ reason: "speculative_read_blocked",
56
+ hiddenFromTranscript: true,
57
+ path: canonical,
58
+ requestedPath: target.path,
59
+ toolName: toolCall.name,
60
+ },
61
+ };
62
+ }
63
+ afterToolCall(toolCall, result) {
64
+ if (!this.enabled || result.metadata?.hiddenFromTranscript === true)
65
+ return;
66
+ this.observeMetadata(result.metadata);
67
+ if (isSuccessfulAccessResult(result)) {
68
+ const target = pathSensitiveReadTarget(toolCall);
69
+ if (target)
70
+ this.knownPaths.add(canonicalPath(this.cwd, target.path));
71
+ }
72
+ }
73
+ observeMetadata(metadata) {
74
+ if (!metadata)
75
+ return;
76
+ for (const path of metadataPaths(metadata)) {
77
+ this.knownPaths.add(canonicalPath(this.cwd, path));
78
+ }
79
+ }
80
+ }
81
+ export function isHiddenToolResult(result) {
82
+ return result?.metadata?.hiddenFromTranscript === true;
83
+ }
84
+ export function isHiddenToolMetadata(metadata) {
85
+ return metadata?.hiddenFromTranscript === true;
86
+ }
87
+ function isDiscoveryToolCall(toolCall) {
88
+ if (toolCall.name === "glob" || toolCall.name === "grep")
89
+ return true;
90
+ return analyzeToolIntent(toolCall).family === "search";
91
+ }
92
+ function pathSensitiveReadTarget(toolCall) {
93
+ if (toolCall.name === "read") {
94
+ const path = stringArg(toolCall.parsedArgs.path ?? toolCall.parsedArgs.file);
95
+ return path ? { kind: "read", path } : undefined;
96
+ }
97
+ if (toolCall.name === "bash") {
98
+ const intent = analyzeToolIntent(toolCall);
99
+ const path = intent.read?.path;
100
+ return path ? { kind: "bash read", path } : undefined;
101
+ }
102
+ if (toolCall.name === "lsp") {
103
+ const path = stringArg(toolCall.parsedArgs.filePath);
104
+ return path ? { kind: "lsp", path } : undefined;
105
+ }
106
+ return undefined;
107
+ }
108
+ function metadataPaths(metadata) {
109
+ const paths = new Set();
110
+ addPathValue(paths, metadata.path);
111
+ addPathValue(paths, metadata.paths);
112
+ return [...paths];
113
+ }
114
+ function addPathValue(out, value) {
115
+ if (typeof value === "string" && value.trim()) {
116
+ out.add(value);
117
+ return;
118
+ }
119
+ if (!Array.isArray(value))
120
+ return;
121
+ for (const item of value) {
122
+ if (typeof item === "string" && item.trim())
123
+ out.add(item);
124
+ }
125
+ }
126
+ function isSuccessfulAccessResult(result) {
127
+ return !result.isError
128
+ && result.status !== "blocked"
129
+ && result.status !== "cancelled"
130
+ && result.status !== "command_error"
131
+ && result.status !== "timeout";
132
+ }
133
+ function canonicalPath(cwd, value) {
134
+ const absolute = resolveToolPath(cwd, value);
135
+ try {
136
+ if (existsSync(absolute))
137
+ return realpathSync.native(absolute);
138
+ }
139
+ catch {
140
+ // Fall back to lexical resolution for unreadable or racing paths.
141
+ }
142
+ return resolve(absolute);
143
+ }
144
+ function extractUserMentionedPaths(input) {
145
+ const text = typeof input === "string"
146
+ ? input
147
+ : input
148
+ .filter((part) => part.type === "text")
149
+ .map((part) => part.text)
150
+ .join("\n");
151
+ const paths = new Set();
152
+ const regex = /(?:^|[\s`'"])(~?\.{0,2}\/[^\s`'"]+|[\w@.-]+\.[A-Za-z0-9][\w.-]*)(?=$|[\s`'",.;:!?,。;:!?))])/g;
153
+ let match;
154
+ while ((match = regex.exec(text)) !== null) {
155
+ const value = match[1]?.trim();
156
+ if (value && isLikelyFilePath(value))
157
+ paths.add(stripTrailingPunctuation(value));
158
+ }
159
+ return [...paths];
160
+ }
161
+ function isLikelyFilePath(value) {
162
+ const stripped = stripTrailingPunctuation(value);
163
+ if (stripped.startsWith("./") || stripped.startsWith("../") || stripped.startsWith("~/") || stripped.startsWith("/")) {
164
+ return true;
165
+ }
166
+ return /\.[A-Za-z0-9][\w.-]*$/.test(basename(stripped));
167
+ }
168
+ function stripTrailingPunctuation(value) {
169
+ return value.replace(/[.,;:!?,。;:!?))]+$/u, "");
170
+ }
171
+ function stringArg(value) {
172
+ return typeof value === "string" ? value.trim() : "";
173
+ }
@@ -0,0 +1,7 @@
1
+ export declare function formatInternalReminderBlock(kind: string, content: string): string;
2
+ export declare function formatInternalContextBlock(kind: string, content: string): string;
3
+ export declare function sanitizeInternalReminderBlocks(text: string): string;
4
+ export declare function createStreamingInternalReminderSanitizer(): {
5
+ push(delta: string): string;
6
+ flush(): string;
7
+ };
@@ -0,0 +1,171 @@
1
+ const INTERNAL_TAG_PREFIX = "<bubble_internal_";
2
+ const INTERNAL_TAG_NAMES = ["reminder", "context"];
3
+ const LEGACY_RUNTIME_MARKERS = [
4
+ "Runtime reminder:\n",
5
+ "Runtime context:\n",
6
+ ];
7
+ const STREAM_MARKERS = [
8
+ INTERNAL_TAG_PREFIX,
9
+ ...LEGACY_RUNTIME_MARKERS,
10
+ ];
11
+ const LEGACY_REMINDER_END_PHRASES = [
12
+ "Debugging workflow: find the failing boundary.",
13
+ "Code explanation workflow: answer directly.",
14
+ "Verify the specific failure path after the change.",
15
+ "Run a narrow verification command or explain why it cannot be run.",
16
+ "Keep summaries secondary to findings.",
17
+ "Avoid proposing changes unless the user asks for them.",
18
+ "Keep the first pass read-only unless the user asks for changes or runtime verification.",
19
+ "Avoid drifting into code changes unless the user explicitly asks to execute.",
20
+ "On rejection, remain in plan mode and iterate.",
21
+ "Do not perform destructive operations, credential exposure, or unrelated reversions just because approvals are bypassed.",
22
+ "Execute the requested change end to end; do not stop at analysis unless blocked or the user explicitly asks for discussion only.",
23
+ "If current evidence is sufficient, summarize your findings now.",
24
+ "- If you cannot determine the cause, ask the user for clarification.",
25
+ "- Skip the \"investigate the codebase\" step that applies to larger changes.",
26
+ "Do not put the final answer only in hidden reasoning.",
27
+ ];
28
+ export function formatInternalReminderBlock(kind, content) {
29
+ return formatInternalBlock("reminder", kind, content);
30
+ }
31
+ export function formatInternalContextBlock(kind, content) {
32
+ return formatInternalBlock("context", kind, content);
33
+ }
34
+ export function sanitizeInternalReminderBlocks(text) {
35
+ if (!text)
36
+ return text;
37
+ const sanitizer = createStreamingInternalReminderSanitizer();
38
+ return sanitizer.push(text) + sanitizer.flush();
39
+ }
40
+ export function createStreamingInternalReminderSanitizer() {
41
+ let pending = "";
42
+ const drain = (final) => {
43
+ let out = "";
44
+ while (pending.length > 0) {
45
+ const block = consumeInternalBlockAtStart(pending, final);
46
+ if (block?.hold)
47
+ break;
48
+ if (block?.consume !== undefined) {
49
+ pending = pending.slice(block.consume);
50
+ continue;
51
+ }
52
+ const markerIndex = findEarliestCompleteMarker(pending);
53
+ if (markerIndex >= 0) {
54
+ if (markerIndex > 0) {
55
+ out += pending.slice(0, markerIndex);
56
+ pending = pending.slice(markerIndex);
57
+ continue;
58
+ }
59
+ // A marker is present but the block is not yet complete enough to
60
+ // consume. Hold it in streaming mode; drop it on final flush.
61
+ if (final) {
62
+ pending = "";
63
+ }
64
+ break;
65
+ }
66
+ if (!final) {
67
+ const partialIndex = findEarliestMarkerPrefixSuffix(pending);
68
+ if (partialIndex >= 0) {
69
+ out += pending.slice(0, partialIndex);
70
+ pending = pending.slice(partialIndex);
71
+ break;
72
+ }
73
+ }
74
+ out += pending;
75
+ pending = "";
76
+ }
77
+ return out;
78
+ };
79
+ return {
80
+ push(delta) {
81
+ if (!delta)
82
+ return "";
83
+ pending += delta;
84
+ return drain(false);
85
+ },
86
+ flush() {
87
+ return drain(true);
88
+ },
89
+ };
90
+ }
91
+ function formatInternalBlock(type, kind, content) {
92
+ const safeKind = kind.replace(/[^a-zA-Z0-9_-]/g, "-");
93
+ return `<bubble_internal_${type} kind="${safeKind}">\n${content}\n</bubble_internal_${type}>`;
94
+ }
95
+ function consumeInternalBlockAtStart(text, final) {
96
+ if (text.startsWith(INTERNAL_TAG_PREFIX)) {
97
+ return consumeStructuredInternalBlock(text, final);
98
+ }
99
+ if (text.startsWith("Runtime reminder:\n")) {
100
+ return consumeLegacyRuntimeReminder(text, final);
101
+ }
102
+ if (text.startsWith("Runtime context:\n")) {
103
+ return final ? { consume: text.length } : { hold: true };
104
+ }
105
+ return undefined;
106
+ }
107
+ function consumeStructuredInternalBlock(text, final) {
108
+ for (const tagName of INTERNAL_TAG_NAMES) {
109
+ const openMatch = text.match(new RegExp(`^<bubble_internal_${tagName}\\b[^>]*>`));
110
+ if (!openMatch)
111
+ continue;
112
+ const closeTag = `</bubble_internal_${tagName}>`;
113
+ const closeIndex = text.indexOf(closeTag, openMatch[0].length);
114
+ if (closeIndex < 0) {
115
+ return final ? { consume: text.length } : { hold: true };
116
+ }
117
+ return { consume: consumeTrailingLineBreaks(text, closeIndex + closeTag.length) };
118
+ }
119
+ if (isPrefixOf(INTERNAL_TAG_PREFIX, text)) {
120
+ return final ? { consume: text.length } : { hold: true };
121
+ }
122
+ return undefined;
123
+ }
124
+ function consumeLegacyRuntimeReminder(text, final) {
125
+ for (const phrase of LEGACY_REMINDER_END_PHRASES) {
126
+ const endIndex = text.indexOf(phrase, LEGACY_RUNTIME_MARKERS[0].length);
127
+ if (endIndex >= 0) {
128
+ return { consume: consumeTrailingLineBreaks(text, endIndex + phrase.length) };
129
+ }
130
+ }
131
+ if (final) {
132
+ return { consume: text.length };
133
+ }
134
+ return { hold: true };
135
+ }
136
+ function consumeTrailingLineBreaks(text, index) {
137
+ let next = index;
138
+ while (text[next] === "\n")
139
+ next += 1;
140
+ return next;
141
+ }
142
+ function findEarliestCompleteMarker(text) {
143
+ let earliest = -1;
144
+ for (const marker of STREAM_MARKERS) {
145
+ const index = text.indexOf(marker);
146
+ if (index >= 0 && (earliest < 0 || index < earliest)) {
147
+ earliest = index;
148
+ }
149
+ }
150
+ return earliest;
151
+ }
152
+ function findEarliestMarkerPrefixSuffix(text) {
153
+ let earliest = -1;
154
+ for (const marker of STREAM_MARKERS) {
155
+ const max = Math.min(marker.length - 1, text.length);
156
+ for (let length = max; length > 0; length -= 1) {
157
+ const suffix = text.slice(text.length - length);
158
+ if (isPrefixOf(marker, suffix)) {
159
+ const index = text.length - length;
160
+ if (earliest < 0 || index < earliest) {
161
+ earliest = index;
162
+ }
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ return earliest;
168
+ }
169
+ function isPrefixOf(value, possiblePrefix) {
170
+ return value.startsWith(possiblePrefix);
171
+ }
@@ -49,9 +49,17 @@ const EXPLANATION_PATTERNS = [
49
49
  ];
50
50
  const ORIENTATION_PATTERNS = [
51
51
  /\bwhat is this project\b/i,
52
+ /\bwhat is this (repo|repository)\b/i,
52
53
  /\borient/i,
53
54
  /\boverview\b/i,
54
- /这个项目.*(干嘛|做什么)|看下这个项目|项目.*概览/i,
55
+ /\b(repo|repository|project) overview\b/i,
56
+ /这个项目.*(干嘛|做什么)|这项目.*(干嘛|做什么)|这个\s*repo.*(干嘛|做什么)|看下(这个|这)?项目|项目.*概览|快速了解.*项目/i,
57
+ ];
58
+ const CONCRETE_ORIENTATION_EXCLUSIONS = [
59
+ /项目[中里内]\s*[,,]?\s*\S{2,}/i,
60
+ /帮我.*(实现|开发|改|加|调整|优化|修复)/i,
61
+ /(实现|开发|改一下|加一个|调整|优化|修复|支持|报错|失败|不对|有问题|bug|commit|push)/i,
62
+ /(?:\.[a-z][\w-]*|\b[A-Z]{2,}\b)/,
55
63
  ];
56
64
  const PRODUCT_PATTERNS = [
57
65
  /\bproduct\b/i,
@@ -77,15 +85,15 @@ export function classifyTask(input) {
77
85
  if (DEBUG_PATTERNS.some((pattern) => pattern.test(text))) {
78
86
  return "debugging";
79
87
  }
80
- if (ORIENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
88
+ if (IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
89
+ return "implementation";
90
+ }
91
+ if (isLikelyRepoOrientation(text)) {
81
92
  return "repo_orientation";
82
93
  }
83
94
  if (EXPLANATION_PATTERNS.some((pattern) => pattern.test(text))) {
84
95
  return "code_explanation";
85
96
  }
86
- if (IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
87
- return "implementation";
88
- }
89
97
  if (PRODUCT_PATTERNS.some((pattern) => pattern.test(text))) {
90
98
  return "product_discussion";
91
99
  }
@@ -94,3 +102,13 @@ export function classifyTask(input) {
94
102
  }
95
103
  return "general";
96
104
  }
105
+ function isLikelyRepoOrientation(text) {
106
+ const normalized = text.trim().replace(/\s+/g, " ");
107
+ if (!ORIENTATION_PATTERNS.some((pattern) => pattern.test(normalized))) {
108
+ return false;
109
+ }
110
+ if (CONCRETE_ORIENTATION_EXCLUSIONS.some((pattern) => pattern.test(normalized))) {
111
+ return false;
112
+ }
113
+ return normalized.length <= 80;
114
+ }
package/dist/agent.js CHANGED
@@ -19,6 +19,8 @@ import { getSubtaskPolicy } from "./agent/subtask-policy.js";
19
19
  import { composeAbortSignals } from "./agent/budget-ledger.js";
20
20
  import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAgentProfile, validateAgentProfileTools } from "./agent/profiles.js";
21
21
  import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
22
+ import { isHiddenToolResult } from "./agent/discovery-barrier.js";
23
+ import { createStreamingInternalReminderSanitizer, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
22
24
  import { buildSystemPrompt } from "./system-prompt.js";
23
25
  import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
24
26
  import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
@@ -374,6 +376,7 @@ export class Agent {
374
376
  modelId: this.apiModel,
375
377
  };
376
378
  const streamingToolCalls = new Map();
379
+ const reasoningSanitizer = createStreamingInternalReminderSanitizer();
377
380
  let turnUsage;
378
381
  let assistantAppended = false;
379
382
  currentAssistantMsg = assistantMsg;
@@ -410,6 +413,8 @@ export class Agent {
410
413
  // budget. If it fails (network error, etc.), the projector's existing
411
414
  // algorithmic fallback still kicks in.
412
415
  await this.maybeCompactWithLLM();
416
+ const bufferedStreamingToolCallIds = new Set();
417
+ const discoveryBarrier = hookState.discoveryBarrier;
413
418
  try {
414
419
  const projectedMessages = projectMessages(this.messages, {
415
420
  mode: "budgeted",
@@ -445,23 +450,34 @@ export class Agent {
445
450
  yield emit({ type: "text_delta", content: chunk.content });
446
451
  break;
447
452
  case "reasoning_delta":
448
- debugReasoningStream({
449
- stage: "agent_receive",
450
- providerId: this._providerId,
451
- modelId: this.apiModel,
452
- turnStep: step,
453
- beforeLength: assistantMsg.reasoning?.length ?? 0,
454
- delta: summarizeDebugText(chunk.content),
455
- afterLength: (assistantMsg.reasoning?.length ?? 0) + chunk.content.length,
456
- });
457
- assistantMsg.reasoning = (assistantMsg.reasoning || "") + chunk.content;
458
- streamReasoningChars += chunk.content.length;
459
- yield emit({ type: "reasoning_delta", content: chunk.content });
453
+ {
454
+ const sanitizedDelta = reasoningSanitizer.push(chunk.content);
455
+ if (sanitizedDelta) {
456
+ debugReasoningStream({
457
+ stage: "agent_receive",
458
+ providerId: this._providerId,
459
+ modelId: this.apiModel,
460
+ turnStep: step,
461
+ beforeLength: assistantMsg.reasoning?.length ?? 0,
462
+ delta: summarizeDebugText(sanitizedDelta),
463
+ afterLength: (assistantMsg.reasoning?.length ?? 0) + sanitizedDelta.length,
464
+ });
465
+ assistantMsg.reasoning = (assistantMsg.reasoning || "") + sanitizedDelta;
466
+ streamReasoningChars += sanitizedDelta.length;
467
+ yield emit({ type: "reasoning_delta", content: sanitizedDelta });
468
+ }
469
+ }
460
470
  break;
461
471
  case "tool_call":
472
+ if (discoveryBarrier?.isEnabled()
473
+ && (bufferedStreamingToolCallIds.has(chunk.id) || discoveryBarrier.shouldBufferStreamingToolCall(chunk.name))) {
474
+ bufferedStreamingToolCallIds.add(chunk.id);
475
+ }
462
476
  if (chunk.isStart) {
463
477
  streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
464
- yield emit({ type: "tool_call_start", id: chunk.id, name: chunk.name });
478
+ if (!bufferedStreamingToolCallIds.has(chunk.id)) {
479
+ yield emit({ type: "tool_call_start", id: chunk.id, name: chunk.name });
480
+ }
465
481
  }
466
482
  if (!streamingToolCalls.has(chunk.id)) {
467
483
  streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
@@ -478,13 +494,15 @@ export class Agent {
478
494
  }
479
495
  if (chunk.arguments) {
480
496
  streamToolCallDeltas += 1;
481
- yield emit({
482
- type: "tool_call_delta",
483
- id: currentToolCall.id,
484
- name: currentToolCall.name,
485
- argumentsDelta: chunk.arguments,
486
- arguments: currentToolCall.args,
487
- });
497
+ if (!bufferedStreamingToolCallIds.has(chunk.id)) {
498
+ yield emit({
499
+ type: "tool_call_delta",
500
+ id: currentToolCall.id,
501
+ name: currentToolCall.name,
502
+ argumentsDelta: chunk.arguments,
503
+ arguments: currentToolCall.args,
504
+ });
505
+ }
488
506
  }
489
507
  }
490
508
  if (chunk.isEnd && currentToolCall) {
@@ -494,12 +512,14 @@ export class Agent {
494
512
  arguments: currentToolCall.args,
495
513
  ...(currentToolCall.argsCorrupt ? { argsCorrupt: true } : {}),
496
514
  });
497
- yield emit({
498
- type: "tool_call_end",
499
- id: currentToolCall.id,
500
- name: currentToolCall.name,
501
- arguments: currentToolCall.args,
502
- });
515
+ if (!bufferedStreamingToolCallIds.has(chunk.id)) {
516
+ yield emit({
517
+ type: "tool_call_end",
518
+ id: currentToolCall.id,
519
+ name: currentToolCall.name,
520
+ arguments: currentToolCall.args,
521
+ });
522
+ }
503
523
  streamingToolCalls.delete(chunk.id);
504
524
  }
505
525
  break;
@@ -520,6 +540,21 @@ export class Agent {
520
540
  for (const update of this.drainSubagentToolUpdates())
521
541
  yield emit(update);
522
542
  }
543
+ const flushedReasoning = reasoningSanitizer.flush();
544
+ if (flushedReasoning) {
545
+ debugReasoningStream({
546
+ stage: "agent_receive_flush",
547
+ providerId: this._providerId,
548
+ modelId: this.apiModel,
549
+ turnStep: step,
550
+ beforeLength: assistantMsg.reasoning?.length ?? 0,
551
+ delta: summarizeDebugText(flushedReasoning),
552
+ afterLength: (assistantMsg.reasoning?.length ?? 0) + flushedReasoning.length,
553
+ });
554
+ assistantMsg.reasoning = (assistantMsg.reasoning || "") + flushedReasoning;
555
+ streamReasoningChars += flushedReasoning.length;
556
+ yield emit({ type: "reasoning_delta", content: flushedReasoning });
557
+ }
523
558
  traceEvent("provider_stream_end", {
524
559
  elapsedMs: Date.now() - providerStartedAt,
525
560
  textChars: streamTextChars,
@@ -590,6 +625,16 @@ export class Agent {
590
625
  parsedCalls.push({ ...tc, parsedArgs: {}, argsCorrupt: true });
591
626
  }
592
627
  }
628
+ const orderedCalls = hookState.discoveryBarrier?.orderToolCalls(parsedCalls) ?? parsedCalls;
629
+ if (orderedCalls !== parsedCalls) {
630
+ parsedCalls.splice(0, parsedCalls.length, ...orderedCalls);
631
+ assistantMsg.toolCalls = parsedCalls.map((tc) => ({
632
+ id: tc.id,
633
+ name: tc.name,
634
+ arguments: tc.arguments,
635
+ ...(tc.argsCorrupt ? { argsCorrupt: true } : {}),
636
+ }));
637
+ }
593
638
  const executedResults = [];
594
639
  const appendCancelledToolMessages = (startIndex) => {
595
640
  for (let pendingIndex = startIndex; pendingIndex < parsedCalls.length; pendingIndex++) {
@@ -634,6 +679,51 @@ export class Agent {
634
679
  arguments: tc.arguments,
635
680
  };
636
681
  flushGovernorReminders();
682
+ if (bufferedStreamingToolCallIds.has(tc.id) && !isHiddenToolResult(blockedResult)) {
683
+ yield emit({ type: "tool_call_start", id: tc.id, name: tc.name });
684
+ if (tc.arguments) {
685
+ yield emit({
686
+ type: "tool_call_delta",
687
+ id: tc.id,
688
+ name: tc.name,
689
+ argumentsDelta: tc.arguments,
690
+ arguments: tc.arguments,
691
+ });
692
+ }
693
+ yield emit({ type: "tool_call_end", id: tc.id, name: tc.name, arguments: tc.arguments });
694
+ }
695
+ if (isHiddenToolResult(blockedResult)) {
696
+ let result = blockedResult;
697
+ await hookBus.runAfterToolCall({
698
+ agent: this,
699
+ cwd,
700
+ input: userInput,
701
+ state: hookState,
702
+ queueReminder,
703
+ flushReminders: flushGovernorReminders,
704
+ toolCall: tc,
705
+ result,
706
+ replaceResult: (next) => {
707
+ result = next;
708
+ },
709
+ });
710
+ traceEvent("speculative_read_blocked", {
711
+ id: tc.id,
712
+ name: tc.name,
713
+ args: summarizeTraceValue(tc.parsedArgs),
714
+ result: summarizeTraceToolResult(result),
715
+ }, traceContext);
716
+ this.appendMessage({
717
+ role: "tool",
718
+ toolCallId: tc.id,
719
+ content: result.content,
720
+ metadata: result.metadata,
721
+ isError: result.isError,
722
+ });
723
+ executedResults.push(result);
724
+ flushGovernorReminders();
725
+ continue;
726
+ }
637
727
  const toolStartedAt = Date.now();
638
728
  traceEvent("tool_execute_start", {
639
729
  id: tc.id,
@@ -1435,6 +1525,9 @@ export class Agent {
1435
1525
  }
1436
1526
  }
1437
1527
  appendMessage(message) {
1528
+ if (message.role === "assistant" && message.reasoning) {
1529
+ message.reasoning = sanitizeInternalReminderBlocks(message.reasoning);
1530
+ }
1438
1531
  this.messages.push(message);
1439
1532
  traceEvent("agent_message_append", {
1440
1533
  message: summarizeTraceMessage(message),