@bubblebrain-ai/bubble 0.0.6 → 0.0.7
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/dist/agent/execution-governor.d.ts +5 -13
- package/dist/agent/execution-governor.js +33 -142
- package/dist/main.js +57 -42
- package/dist/orchestrator/default-hooks.js +3 -15
- package/dist/prompt/reminders.d.ts +2 -1
- package/dist/prompt/reminders.js +4 -3
- package/dist/slash-commands/commands.js +0 -17
- package/package.json +1 -1
|
@@ -3,21 +3,16 @@ import type { TaskType } from "./task-classifier.js";
|
|
|
3
3
|
export interface GovernorDecision {
|
|
4
4
|
blockedResult?: ToolResult;
|
|
5
5
|
}
|
|
6
|
-
type WorkPhase = "explore" | "modify" | "verify";
|
|
7
6
|
export declare class ExecutionGovernor {
|
|
8
|
-
private taskType;
|
|
9
7
|
private budget;
|
|
10
8
|
private history;
|
|
11
9
|
private totalSteps;
|
|
12
10
|
private searchSteps;
|
|
13
11
|
private readSteps;
|
|
14
|
-
private
|
|
15
|
-
private searchFrozen;
|
|
16
|
-
private explorationFrozen;
|
|
17
|
-
private phase;
|
|
18
|
-
private codeChanged;
|
|
12
|
+
private mutationVersion;
|
|
19
13
|
private reminderQueue;
|
|
20
14
|
private warnedFamilies;
|
|
15
|
+
private warnedSignatures;
|
|
21
16
|
private softTotalWarned;
|
|
22
17
|
private softSearchWarned;
|
|
23
18
|
private softReadWarned;
|
|
@@ -29,16 +24,13 @@ export declare class ExecutionGovernor {
|
|
|
29
24
|
readSteps: number;
|
|
30
25
|
searchFrozen: boolean;
|
|
31
26
|
explorationFrozen: boolean;
|
|
32
|
-
phase:
|
|
27
|
+
phase: "observe";
|
|
33
28
|
};
|
|
34
29
|
filterToolDefinitions(toolDefinitions: ToolRegistryEntry[]): ToolRegistryEntry[];
|
|
35
30
|
beforeToolCall(toolCall: ParsedToolCall): GovernorDecision;
|
|
36
31
|
afterToolResult(toolCall: ParsedToolCall, result: ToolResult): void;
|
|
37
32
|
private trailingNoProgressCount;
|
|
38
|
-
private
|
|
39
|
-
private
|
|
40
|
-
private isModificationTask;
|
|
41
|
-
private historyCount;
|
|
33
|
+
private hasCurrentMutationObservation;
|
|
34
|
+
private warnOnce;
|
|
42
35
|
private maybeWarnOnSoftBudgets;
|
|
43
36
|
}
|
|
44
|
-
export {};
|
|
@@ -1,118 +1,84 @@
|
|
|
1
1
|
import { analyzeToolIntent } from "./tool-intent.js";
|
|
2
|
-
import {
|
|
2
|
+
import { buildInvestigationReminder, buildLoopWarningReminder } from "../prompt/reminders.js";
|
|
3
3
|
const BUDGETS = {
|
|
4
4
|
security_investigation: {
|
|
5
5
|
softTotalSteps: 14,
|
|
6
6
|
softSearchSteps: 6,
|
|
7
7
|
softReadSteps: 8,
|
|
8
|
-
|
|
9
|
-
maxNoProgressReadExactRepeats: 2,
|
|
10
|
-
maxNoProgressExactRepeats: 2,
|
|
11
|
-
maxNoProgressFamilyRepeats: 3,
|
|
8
|
+
warningExactRepeats: 2,
|
|
12
9
|
warningFamilyRepeats: 2,
|
|
13
10
|
},
|
|
14
11
|
code_search: {
|
|
15
12
|
softTotalSteps: 16,
|
|
16
13
|
softSearchSteps: 8,
|
|
17
14
|
softReadSteps: 10,
|
|
18
|
-
|
|
19
|
-
maxNoProgressReadExactRepeats: 2,
|
|
20
|
-
maxNoProgressExactRepeats: 3,
|
|
21
|
-
maxNoProgressFamilyRepeats: 4,
|
|
15
|
+
warningExactRepeats: 2,
|
|
22
16
|
warningFamilyRepeats: 3,
|
|
23
17
|
},
|
|
24
18
|
debugging: {
|
|
25
19
|
softTotalSteps: 18,
|
|
26
20
|
softSearchSteps: 8,
|
|
27
21
|
softReadSteps: 7,
|
|
28
|
-
|
|
29
|
-
maxNoProgressReadExactRepeats: 1,
|
|
30
|
-
maxNoProgressExactRepeats: 3,
|
|
31
|
-
maxNoProgressFamilyRepeats: 4,
|
|
22
|
+
warningExactRepeats: 1,
|
|
32
23
|
warningFamilyRepeats: 3,
|
|
33
24
|
},
|
|
34
25
|
implementation: {
|
|
35
26
|
softTotalSteps: 18,
|
|
36
27
|
softSearchSteps: 8,
|
|
37
28
|
softReadSteps: 6,
|
|
38
|
-
|
|
39
|
-
maxNoProgressReadExactRepeats: 1,
|
|
40
|
-
maxNoProgressExactRepeats: 3,
|
|
41
|
-
maxNoProgressFamilyRepeats: 4,
|
|
29
|
+
warningExactRepeats: 1,
|
|
42
30
|
warningFamilyRepeats: 3,
|
|
43
31
|
},
|
|
44
32
|
code_review: {
|
|
45
33
|
softTotalSteps: 14,
|
|
46
34
|
softSearchSteps: 6,
|
|
47
35
|
softReadSteps: 8,
|
|
48
|
-
|
|
49
|
-
maxNoProgressReadExactRepeats: 2,
|
|
50
|
-
maxNoProgressExactRepeats: 3,
|
|
51
|
-
maxNoProgressFamilyRepeats: 4,
|
|
36
|
+
warningExactRepeats: 2,
|
|
52
37
|
warningFamilyRepeats: 3,
|
|
53
38
|
},
|
|
54
39
|
code_explanation: {
|
|
55
40
|
softTotalSteps: 12,
|
|
56
41
|
softSearchSteps: 6,
|
|
57
42
|
softReadSteps: 8,
|
|
58
|
-
|
|
59
|
-
maxNoProgressReadExactRepeats: 2,
|
|
60
|
-
maxNoProgressExactRepeats: 3,
|
|
61
|
-
maxNoProgressFamilyRepeats: 4,
|
|
43
|
+
warningExactRepeats: 2,
|
|
62
44
|
warningFamilyRepeats: 3,
|
|
63
45
|
},
|
|
64
46
|
repo_orientation: {
|
|
65
47
|
softTotalSteps: 12,
|
|
66
48
|
softSearchSteps: 6,
|
|
67
49
|
softReadSteps: 8,
|
|
68
|
-
|
|
69
|
-
maxNoProgressReadExactRepeats: 2,
|
|
70
|
-
maxNoProgressExactRepeats: 3,
|
|
71
|
-
maxNoProgressFamilyRepeats: 4,
|
|
50
|
+
warningExactRepeats: 2,
|
|
72
51
|
warningFamilyRepeats: 3,
|
|
73
52
|
},
|
|
74
53
|
product_discussion: {
|
|
75
54
|
softTotalSteps: 10,
|
|
76
55
|
softSearchSteps: 4,
|
|
77
56
|
softReadSteps: 4,
|
|
78
|
-
|
|
79
|
-
maxNoProgressReadExactRepeats: 2,
|
|
80
|
-
maxNoProgressExactRepeats: 2,
|
|
81
|
-
maxNoProgressFamilyRepeats: 3,
|
|
57
|
+
warningExactRepeats: 2,
|
|
82
58
|
warningFamilyRepeats: 2,
|
|
83
59
|
},
|
|
84
60
|
general: {
|
|
85
61
|
softTotalSteps: 18,
|
|
86
62
|
softSearchSteps: 8,
|
|
87
63
|
softReadSteps: 10,
|
|
88
|
-
|
|
89
|
-
maxNoProgressReadExactRepeats: 2,
|
|
90
|
-
maxNoProgressExactRepeats: 3,
|
|
91
|
-
maxNoProgressFamilyRepeats: 4,
|
|
64
|
+
warningExactRepeats: 2,
|
|
92
65
|
warningFamilyRepeats: 3,
|
|
93
66
|
},
|
|
94
67
|
};
|
|
95
|
-
const SEARCH_TOOLS_DISABLED = new Set(["grep", "web_search", "web_fetch"]);
|
|
96
|
-
const EXPLORATION_TOOLS_DISABLED = new Set(["read", "glob", "grep", "web_search", "web_fetch", "spawn_agent", "wait_agent", "send_input", "tool_search"]);
|
|
97
68
|
export class ExecutionGovernor {
|
|
98
|
-
taskType;
|
|
99
69
|
budget;
|
|
100
70
|
history = [];
|
|
101
71
|
totalSteps = 0;
|
|
102
72
|
searchSteps = 0;
|
|
103
73
|
readSteps = 0;
|
|
104
|
-
|
|
105
|
-
searchFrozen = false;
|
|
106
|
-
explorationFrozen = false;
|
|
107
|
-
phase = "explore";
|
|
108
|
-
codeChanged = false;
|
|
74
|
+
mutationVersion = 0;
|
|
109
75
|
reminderQueue = [];
|
|
110
76
|
warnedFamilies = new Set();
|
|
77
|
+
warnedSignatures = new Set();
|
|
111
78
|
softTotalWarned = false;
|
|
112
79
|
softSearchWarned = false;
|
|
113
80
|
softReadWarned = false;
|
|
114
81
|
constructor(taskType) {
|
|
115
|
-
this.taskType = taskType;
|
|
116
82
|
this.budget = BUDGETS[taskType];
|
|
117
83
|
if (taskType === "security_investigation") {
|
|
118
84
|
this.reminderQueue.push(buildInvestigationReminder());
|
|
@@ -128,62 +94,33 @@ export class ExecutionGovernor {
|
|
|
128
94
|
totalSteps: this.totalSteps,
|
|
129
95
|
searchSteps: this.searchSteps,
|
|
130
96
|
readSteps: this.readSteps,
|
|
131
|
-
searchFrozen:
|
|
132
|
-
explorationFrozen:
|
|
133
|
-
phase:
|
|
97
|
+
searchFrozen: false,
|
|
98
|
+
explorationFrozen: false,
|
|
99
|
+
phase: "observe",
|
|
134
100
|
};
|
|
135
101
|
}
|
|
136
102
|
filterToolDefinitions(toolDefinitions) {
|
|
137
|
-
|
|
138
|
-
if (this.explorationFrozen) {
|
|
139
|
-
filtered = filtered.filter((tool) => !EXPLORATION_TOOLS_DISABLED.has(tool.name));
|
|
140
|
-
}
|
|
141
|
-
else if (this.searchFrozen) {
|
|
142
|
-
filtered = filtered.filter((tool) => !SEARCH_TOOLS_DISABLED.has(tool.name));
|
|
143
|
-
}
|
|
144
|
-
return filtered;
|
|
103
|
+
return toolDefinitions;
|
|
145
104
|
}
|
|
146
105
|
beforeToolCall(toolCall) {
|
|
147
106
|
const intent = analyzeToolIntent(toolCall);
|
|
148
|
-
if (
|
|
149
|
-
return {
|
|
150
|
-
blockedResult: blockedResult("Exploration blocked: this implementation task already has enough context. Use edit/write, verify an existing change, or explain the blocker.", "blocked", "Exploration frozen because tool calls stopped producing task progress.", metadataKindForFamily(intent.family)),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
if (this.isModificationTask() && !this.codeChanged && intent.family === "read") {
|
|
107
|
+
if (intent.family === "read") {
|
|
154
108
|
const signature = intent.read?.signature;
|
|
155
|
-
if (signature && this.
|
|
156
|
-
this.
|
|
157
|
-
return {
|
|
158
|
-
blockedResult: blockedResult("Read blocked: this file range was already read. You have enough context to make the requested change; use edit/write now or explain the blocker.", "blocked", "Repeated identical read before modification.", "read"),
|
|
159
|
-
};
|
|
109
|
+
if (signature && this.hasCurrentMutationObservation((entry) => entry.signature === signature)) {
|
|
110
|
+
this.warnOnce(`read:${signature}`, "This exact file range was already read since the last successful edit/write. If the content is still available and nothing changed, use the prior result; otherwise it is okay to re-read to recover context or verify a change.");
|
|
160
111
|
}
|
|
161
112
|
}
|
|
162
113
|
if (intent.family === "search") {
|
|
163
|
-
if (this.searchFrozen) {
|
|
164
|
-
return {
|
|
165
|
-
blockedResult: blockedResult("Search blocked: repeated low-yield searching is now frozen for this task.", "blocked", "Search frozen due to repeated low-yield searching.", "search"),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
114
|
const signature = intent.search?.signature;
|
|
169
115
|
const familyKey = intent.search?.familyKey;
|
|
170
|
-
if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.
|
|
171
|
-
this.
|
|
172
|
-
return {
|
|
173
|
-
blockedResult: blockedResult("Search blocked: repeated the same search multiple times without new evidence.", "blocked", "Repeated identical search without progress.", "search"),
|
|
174
|
-
};
|
|
116
|
+
if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.warningExactRepeats) {
|
|
117
|
+
this.warnOnce(`search:${signature}`, "This search is very similar to one you already ran and it did not produce new evidence. Change the query/path, follow a concrete lead, or summarize the strongest findings.");
|
|
175
118
|
}
|
|
176
119
|
if (familyKey) {
|
|
177
120
|
const familyNoProgress = this.trailingNoProgressCount((entry) => entry.familyKey === familyKey);
|
|
178
|
-
if (familyNoProgress >= this.budget.maxNoProgressFamilyRepeats) {
|
|
179
|
-
this.freezeSearch(`Repeated the same search family without new evidence: ${familyKey}`);
|
|
180
|
-
return {
|
|
181
|
-
blockedResult: blockedResult("Search blocked: repeated the same search family without new evidence.", "blocked", "Repeated similar searches without progress.", "search"),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
121
|
if (familyNoProgress >= this.budget.warningFamilyRepeats && !this.warnedFamilies.has(familyKey)) {
|
|
185
122
|
this.warnedFamilies.add(familyKey);
|
|
186
|
-
this.reminderQueue.push(buildLoopWarningReminder("Repeated searches are yielding little new evidence. Change your hypothesis, narrow the path, or summarize current findings instead of repeating variants."));
|
|
123
|
+
this.reminderQueue.push(buildLoopWarningReminder("Repeated searches in the same family are yielding little new evidence. Change your hypothesis, narrow the path, follow a specific file lead, or summarize current findings instead of repeating variants."));
|
|
187
124
|
}
|
|
188
125
|
}
|
|
189
126
|
}
|
|
@@ -194,9 +131,6 @@ export class ExecutionGovernor {
|
|
|
194
131
|
if (intent.family === "read") {
|
|
195
132
|
this.readSteps += 1;
|
|
196
133
|
}
|
|
197
|
-
if (isExplorationIntent(intent) && !this.codeChanged) {
|
|
198
|
-
this.explorationStepsWithoutWrite += 1;
|
|
199
|
-
}
|
|
200
134
|
this.maybeWarnOnSoftBudgets(intent.family === "search", intent.family === "read");
|
|
201
135
|
return {};
|
|
202
136
|
}
|
|
@@ -211,23 +145,19 @@ export class ExecutionGovernor {
|
|
|
211
145
|
signature: intent.search?.signature ?? intent.read?.signature,
|
|
212
146
|
familyKey: intent.search?.familyKey ?? intent.read?.familyKey,
|
|
213
147
|
progress,
|
|
148
|
+
mutationVersion: this.mutationVersion,
|
|
214
149
|
});
|
|
215
150
|
if (isSuccessfulWriteIntent(intent, result)) {
|
|
216
|
-
this.
|
|
217
|
-
this.phase = "verify";
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (this.isModificationTask()
|
|
221
|
-
&& !this.codeChanged
|
|
222
|
-
&& isExplorationIntent(intent)
|
|
223
|
-
&& this.explorationStepsWithoutWrite >= this.budget.maxExplorationStepsWithoutWrite) {
|
|
224
|
-
this.enterModifyPhase(`Used ${this.explorationStepsWithoutWrite} exploration tools without editing files.`);
|
|
151
|
+
this.mutationVersion += 1;
|
|
225
152
|
}
|
|
226
153
|
}
|
|
227
154
|
trailingNoProgressCount(predicate) {
|
|
228
155
|
let count = 0;
|
|
229
156
|
for (let index = this.history.length - 1; index >= 0; index--) {
|
|
230
157
|
const entry = this.history[index];
|
|
158
|
+
if (entry.mutationVersion !== this.mutationVersion) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
231
161
|
if (!predicate(entry)) {
|
|
232
162
|
break;
|
|
233
163
|
}
|
|
@@ -238,27 +168,15 @@ export class ExecutionGovernor {
|
|
|
238
168
|
}
|
|
239
169
|
return count;
|
|
240
170
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
this.searchFrozen = true;
|
|
246
|
-
this.reminderQueue.push(buildSearchFreezeReminder(reason));
|
|
171
|
+
hasCurrentMutationObservation(predicate) {
|
|
172
|
+
return this.history.some((entry) => entry.mutationVersion === this.mutationVersion && predicate(entry));
|
|
247
173
|
}
|
|
248
|
-
|
|
249
|
-
if (this.
|
|
174
|
+
warnOnce(key, reason) {
|
|
175
|
+
if (this.warnedSignatures.has(key)) {
|
|
250
176
|
return;
|
|
251
177
|
}
|
|
252
|
-
this.
|
|
253
|
-
this.
|
|
254
|
-
this.searchFrozen = true;
|
|
255
|
-
this.reminderQueue.push(buildExplorationFreezeReminder(reason));
|
|
256
|
-
}
|
|
257
|
-
isModificationTask() {
|
|
258
|
-
return this.taskType === "implementation" || this.taskType === "debugging";
|
|
259
|
-
}
|
|
260
|
-
historyCount(predicate) {
|
|
261
|
-
return this.history.reduce((count, entry) => count + (predicate(entry) ? 1 : 0), 0);
|
|
178
|
+
this.warnedSignatures.add(key);
|
|
179
|
+
this.reminderQueue.push(buildLoopWarningReminder(reason));
|
|
262
180
|
}
|
|
263
181
|
maybeWarnOnSoftBudgets(isSearchStep, isReadStep) {
|
|
264
182
|
if (!this.softTotalWarned && this.totalSteps >= this.budget.softTotalSteps) {
|
|
@@ -275,28 +193,12 @@ export class ExecutionGovernor {
|
|
|
275
193
|
}
|
|
276
194
|
}
|
|
277
195
|
}
|
|
278
|
-
function isExplorationIntent(intent) {
|
|
279
|
-
return intent.family === "search" || intent.family === "read" || intent.family === "web";
|
|
280
|
-
}
|
|
281
196
|
function isSuccessfulWriteIntent(intent, result) {
|
|
282
197
|
if (result.isError || result.status === "blocked" || result.status === "command_error") {
|
|
283
198
|
return false;
|
|
284
199
|
}
|
|
285
200
|
return intent.family === "write" || intent.family === "edit" || result.metadata?.kind === "write" || result.metadata?.kind === "edit";
|
|
286
201
|
}
|
|
287
|
-
function metadataKindForFamily(family) {
|
|
288
|
-
switch (family) {
|
|
289
|
-
case "search":
|
|
290
|
-
case "read":
|
|
291
|
-
case "write":
|
|
292
|
-
case "edit":
|
|
293
|
-
case "shell":
|
|
294
|
-
case "web":
|
|
295
|
-
return family;
|
|
296
|
-
default:
|
|
297
|
-
return "security";
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
202
|
function inferProgress(intent, result) {
|
|
301
203
|
if (result.status === "blocked" || result.status === "timeout" || result.status === "command_error") {
|
|
302
204
|
return false;
|
|
@@ -314,14 +216,3 @@ function inferProgress(intent, result) {
|
|
|
314
216
|
}
|
|
315
217
|
return !result.isError;
|
|
316
218
|
}
|
|
317
|
-
function blockedResult(content, status, reason, kind = "security") {
|
|
318
|
-
return {
|
|
319
|
-
content,
|
|
320
|
-
isError: true,
|
|
321
|
-
status,
|
|
322
|
-
metadata: {
|
|
323
|
-
kind,
|
|
324
|
-
reason,
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}
|
package/dist/main.js
CHANGED
|
@@ -125,9 +125,8 @@ async function main() {
|
|
|
125
125
|
const { registry: slashRegistry } = await import("./slash-commands/index.js");
|
|
126
126
|
slashRegistry.addDynamicSource(() => mcpManager.getPromptCommands());
|
|
127
127
|
}
|
|
128
|
-
// Signal-based shutdown for Ctrl-C / kill.
|
|
129
|
-
//
|
|
130
|
-
// runs synchronously and can't await async work, so relying on it is a trap.
|
|
128
|
+
// Signal-based shutdown for Ctrl-C / kill. Normal /quit cleanup happens after
|
|
129
|
+
// the TUI renderer has been destroyed, avoiding native teardown races.
|
|
131
130
|
const shutdownMcp = async () => {
|
|
132
131
|
try {
|
|
133
132
|
await mcpManager.shutdown();
|
|
@@ -261,6 +260,18 @@ async function main() {
|
|
|
261
260
|
// Codex-style memory runs at startup over historical rollouts. Exit should
|
|
262
261
|
// not perform an ad-hoc extraction of the just-finished session.
|
|
263
262
|
};
|
|
263
|
+
const shutdownRuntime = async () => {
|
|
264
|
+
const results = await Promise.allSettled([
|
|
265
|
+
flushMemory(),
|
|
266
|
+
shutdownMcp(),
|
|
267
|
+
lspService.shutdown(),
|
|
268
|
+
]);
|
|
269
|
+
for (const result of results) {
|
|
270
|
+
if (result.status === "rejected") {
|
|
271
|
+
// Shutdown is best-effort; never turn exit into a fatal error.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
264
275
|
const runMemoryCompaction = async () => formatMemoryStartupResult(await runMemoryStartupPipeline({
|
|
265
276
|
cwd: args.cwd,
|
|
266
277
|
complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
|
|
@@ -296,49 +307,53 @@ async function main() {
|
|
|
296
307
|
console.log(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
|
|
297
308
|
}
|
|
298
309
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
for await (const event of agent.run(prompt, args.cwd)) {
|
|
307
|
-
if (event.type === "text_delta") {
|
|
308
|
-
process.stdout.write(event.content);
|
|
309
|
-
}
|
|
310
|
-
else if (event.type === "tool_start") {
|
|
311
|
-
console.log(chalk.cyan(`\n[Tool: ${event.name}]`));
|
|
310
|
+
try {
|
|
311
|
+
// Print mode: single prompt, then exit
|
|
312
|
+
if (args.print || args.prompt) {
|
|
313
|
+
const prompt = args.prompt || (await readPipedStdin()) || "";
|
|
314
|
+
if (!prompt) {
|
|
315
|
+
console.error(chalk.red("Error: No prompt provided."));
|
|
316
|
+
process.exit(1);
|
|
312
317
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
318
|
+
for await (const event of agent.run(prompt, args.cwd)) {
|
|
319
|
+
if (event.type === "text_delta") {
|
|
320
|
+
process.stdout.write(event.content);
|
|
321
|
+
}
|
|
322
|
+
else if (event.type === "tool_start") {
|
|
323
|
+
console.log(chalk.cyan(`\n[Tool: ${event.name}]`));
|
|
324
|
+
}
|
|
325
|
+
else if (event.type === "tool_end") {
|
|
326
|
+
const color = event.result.isError ? chalk.red : chalk.dim;
|
|
327
|
+
console.log(color(`[Result: ${event.result.content.slice(0, 200)}${event.result.content.length > 200 ? "..." : ""}]`));
|
|
328
|
+
}
|
|
316
329
|
}
|
|
330
|
+
console.log();
|
|
331
|
+
return;
|
|
317
332
|
}
|
|
318
|
-
|
|
319
|
-
|
|
333
|
+
// Interactive mode: OpenTUI uses Bun native FFI, matching opencode's TUI stack.
|
|
334
|
+
const { runTui } = await import("./tui/run.js");
|
|
335
|
+
await runTui(agent, args, {
|
|
336
|
+
sessionManager,
|
|
337
|
+
createProvider,
|
|
338
|
+
registry,
|
|
339
|
+
skillRegistry,
|
|
340
|
+
planHandlerRef,
|
|
341
|
+
approvalHandlerRef,
|
|
342
|
+
questionController,
|
|
343
|
+
bashAllowlist,
|
|
344
|
+
settingsManager,
|
|
345
|
+
lspService,
|
|
346
|
+
mcpManager,
|
|
347
|
+
theme: userConfig.getTheme(),
|
|
348
|
+
flushMemory,
|
|
349
|
+
runMemoryCompaction,
|
|
350
|
+
runMemorySummary,
|
|
351
|
+
runMemoryRefresh,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
await shutdownRuntime();
|
|
320
356
|
}
|
|
321
|
-
// Interactive mode: OpenTUI uses Bun native FFI, matching opencode's TUI stack.
|
|
322
|
-
const { runTui } = await import("./tui/run.js");
|
|
323
|
-
await runTui(agent, args, {
|
|
324
|
-
sessionManager,
|
|
325
|
-
createProvider,
|
|
326
|
-
registry,
|
|
327
|
-
skillRegistry,
|
|
328
|
-
planHandlerRef,
|
|
329
|
-
approvalHandlerRef,
|
|
330
|
-
questionController,
|
|
331
|
-
bashAllowlist,
|
|
332
|
-
settingsManager,
|
|
333
|
-
lspService,
|
|
334
|
-
mcpManager,
|
|
335
|
-
theme: userConfig.getTheme(),
|
|
336
|
-
flushMemory,
|
|
337
|
-
runMemoryCompaction,
|
|
338
|
-
runMemorySummary,
|
|
339
|
-
runMemoryRefresh,
|
|
340
|
-
});
|
|
341
|
-
await flushMemory();
|
|
342
357
|
}
|
|
343
358
|
async function readPipedStdin() {
|
|
344
359
|
if (process.stdin.isTTY)
|
|
@@ -36,18 +36,15 @@ export function createDefaultHooks() {
|
|
|
36
36
|
},
|
|
37
37
|
beforeModelCall(ctx) {
|
|
38
38
|
ctx.agent.compactResidentHistory();
|
|
39
|
-
if (ctx.state.governor) {
|
|
40
|
-
ctx.toolEntries = ctx.state.governor.filterToolDefinitions(ctx.toolEntries);
|
|
41
|
-
}
|
|
42
39
|
if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker && ctx.state.governor) {
|
|
43
40
|
const coverage = ctx.state.evidenceTracker.snapshot();
|
|
44
41
|
const phase = resolveWorkflowPhase({
|
|
45
42
|
coreCoverageComplete: ctx.state.evidenceTracker.isCoreCoverageComplete(),
|
|
46
|
-
searchFrozen:
|
|
43
|
+
searchFrozen: false,
|
|
47
44
|
});
|
|
48
45
|
ctx.state.workflowPhase = phase;
|
|
49
46
|
const summary = formatCoverageSummary(coverage);
|
|
50
|
-
const key = `${phase}:${ctx.state.evidenceTracker.key()}
|
|
47
|
+
const key = `${phase}:${ctx.state.evidenceTracker.key()}:0`;
|
|
51
48
|
if (ctx.state.workflowKey !== key) {
|
|
52
49
|
ctx.state.workflowKey = key;
|
|
53
50
|
ctx.queueReminder(buildWorkflowPhaseReminder({
|
|
@@ -66,10 +63,7 @@ export function createDefaultHooks() {
|
|
|
66
63
|
beforeToolCall(ctx) {
|
|
67
64
|
const arbitration = arbitrateToolCall(ctx.toolCall);
|
|
68
65
|
ctx.replaceToolCall({ ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) });
|
|
69
|
-
|
|
70
|
-
if (decision?.blockedResult) {
|
|
71
|
-
ctx.blockToolCall(decision.blockedResult);
|
|
72
|
-
}
|
|
66
|
+
ctx.state.governor?.beforeToolCall(ctx.toolCall);
|
|
73
67
|
},
|
|
74
68
|
afterToolCall(ctx) {
|
|
75
69
|
if (ctx.toolCall.arbiterNote) {
|
|
@@ -149,12 +143,6 @@ export function createDefaultHooks() {
|
|
|
149
143
|
ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
|
|
150
144
|
return;
|
|
151
145
|
}
|
|
152
|
-
const allSearchResultsWereLowSignal = ctx.toolCalls.length > 0
|
|
153
|
-
&& ctx.toolCalls.every((toolCall) => ["glob", "grep", "bash", "web_search", "web_fetch"].includes(toolCall.name))
|
|
154
|
-
&& ctx.toolResults.every((result) => result.status === "no_match" || result.status === "blocked");
|
|
155
|
-
if (ctx.state.governor?.snapshot().searchFrozen && allSearchResultsWereLowSignal) {
|
|
156
|
-
ctx.requestTextOnlyTurn("Search continuation has become low-yield. Summarize the strongest evidence already collected instead of continuing broad exploration.");
|
|
157
|
-
}
|
|
158
146
|
// Verification reminders intentionally removed. See afterToolCall.
|
|
159
147
|
},
|
|
160
148
|
afterTurn() {
|
|
@@ -41,7 +41,8 @@ export declare function buildTaskSummaryReminder(): string;
|
|
|
41
41
|
export declare function buildEditRetryEscalationReminder(reason: string): string;
|
|
42
42
|
/**
|
|
43
43
|
* Fired the FIRST time the model re-reads a file it already read in this turn.
|
|
44
|
-
* Soft — does not freeze the tool.
|
|
44
|
+
* Soft — does not freeze the tool. The model may still re-read when context was
|
|
45
|
+
* pruned, the requested range changed, or a later mutation needs verification.
|
|
45
46
|
*/
|
|
46
47
|
export declare function buildRedundantReadReminder(path: string): string;
|
|
47
48
|
/**
|
package/dist/prompt/reminders.js
CHANGED
|
@@ -200,12 +200,13 @@ Stop retrying the same call. Pick one of:
|
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
202
|
* Fired the FIRST time the model re-reads a file it already read in this turn.
|
|
203
|
-
* Soft — does not freeze the tool.
|
|
203
|
+
* Soft — does not freeze the tool. The model may still re-read when context was
|
|
204
|
+
* pruned, the requested range changed, or a later mutation needs verification.
|
|
204
205
|
*/
|
|
205
206
|
export function buildRedundantReadReminder(path) {
|
|
206
207
|
return wrapInSystemReminder(`
|
|
207
|
-
You already read ${path} earlier in this turn.
|
|
208
|
-
|
|
208
|
+
You already read ${path} earlier in this turn. If that content is still available and nothing changed, rely on it rather than re-reading.
|
|
209
|
+
It is okay to re-read when you need to recover pruned context, inspect a different range, or verify a later edit/write/bash change.
|
|
209
210
|
`);
|
|
210
211
|
}
|
|
211
212
|
/**
|
|
@@ -274,24 +274,7 @@ const builtinSlashCommandEntries = [
|
|
|
274
274
|
name: "quit",
|
|
275
275
|
description: "Exit the application",
|
|
276
276
|
async handler(args, ctx) {
|
|
277
|
-
// Shut MCP stdio children down first; their stdout/stderr listeners
|
|
278
|
-
// otherwise hold the Node event loop open even after ink unmounts.
|
|
279
|
-
try {
|
|
280
|
-
await ctx.mcpManager?.shutdown();
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// ignore — we're quitting anyway
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
await ctx.flushMemory?.();
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
// memory shutdown hooks are best-effort during exit
|
|
290
|
-
}
|
|
291
277
|
ctx.exit();
|
|
292
|
-
// Belt-and-braces: if anything else (raw-mode tty handle, pending
|
|
293
|
-
// timer, etc.) still holds the loop, force-exit shortly after.
|
|
294
|
-
setTimeout(() => process.exit(0), 100).unref();
|
|
295
278
|
},
|
|
296
279
|
},
|
|
297
280
|
{
|