@bubblebrain-ai/bubble 0.0.5 → 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/agent/task-size.d.ts +9 -0
- package/dist/agent/task-size.js +33 -0
- package/dist/agent/tool-intent.d.ts +1 -0
- package/dist/agent/tool-intent.js +1 -1
- package/dist/agent.js +46 -2
- package/dist/main.js +57 -42
- package/dist/orchestrator/default-hooks.js +83 -84
- package/dist/orchestrator/hooks.d.ts +5 -8
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/provider-prompts/deepseek.js +1 -2
- package/dist/prompt/provider-prompts/kimi.js +1 -2
- package/dist/prompt/reminders.d.ts +21 -3
- package/dist/prompt/reminders.js +44 -17
- package/dist/prompt/runtime.js +17 -23
- package/dist/provider.d.ts +10 -1
- package/dist/provider.js +87 -34
- package/dist/slash-commands/commands.js +0 -17
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +1 -1
- package/dist/tools/edit-apply.js +37 -6
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +18 -6
- package/dist/tools/file-state.d.ts +25 -0
- package/dist/tools/file-state.js +52 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +6 -4
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.js +5 -1
- package/dist/tools/write.d.ts +4 -3
- package/dist/tools/write.js +133 -54
- package/dist/tui/display-history.d.ts +2 -0
- package/dist/tui/run.js +115 -23
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +1 -1
- package/dist/tui/tool-renderers/write-preview.js +9 -1
- package/dist/tui/tool-renderers/write.js +13 -7
- package/dist/types.d.ts +15 -0
- 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
|
-
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContentPart } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Coarse "is this a small / focused task" classifier. Used to inject a hint
|
|
4
|
+
* that suppresses the default exploration-first protocol when the user's
|
|
5
|
+
* request is clearly a one-shot create or single-file tweak. Counterpart to
|
|
6
|
+
* `task-classifier.ts` which categorizes by *kind*, not *size*.
|
|
7
|
+
*/
|
|
8
|
+
export type TaskSize = "small" | "normal";
|
|
9
|
+
export declare function classifyTaskSize(input: string | ContentPart[]): TaskSize;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Two-part match: a "create" verb AND a "deliverable" noun in the same
|
|
2
|
+
// sentence is a strong signal of a focused, one-shot task.
|
|
3
|
+
const SMALL_TASK_VERBS = [
|
|
4
|
+
/\b(write|create|generate|make|draft|build|add)\b/i,
|
|
5
|
+
/帮我写|帮我创建|帮我生成|写(个|一个|一份)|新建一个|做(个|一个|一份)|生成(个|一个|一份)|搞(个|一个|一份)/i,
|
|
6
|
+
];
|
|
7
|
+
const SMALL_TASK_NOUNS = [
|
|
8
|
+
/\b(file|page|component|script|snippet|function|class|test|html|css|js|ts|tsx|jsx|md|markdown|hello world)\b/i,
|
|
9
|
+
/(文件|页面|组件|脚本|片段|函数|类|测试|html|css|介绍|文章|页|说明|示例|demo)/i,
|
|
10
|
+
];
|
|
11
|
+
function matchesSmallTaskPattern(text) {
|
|
12
|
+
return SMALL_TASK_VERBS.some((re) => re.test(text))
|
|
13
|
+
&& SMALL_TASK_NOUNS.some((re) => re.test(text));
|
|
14
|
+
}
|
|
15
|
+
const LARGE_TASK_NEGATIONS = [
|
|
16
|
+
/\b(refactor|rewrite|migrate|overhaul|integrate|architect|review the codebase|whole project|across the codebase)\b/i,
|
|
17
|
+
/多个|批量|全部|所有|整个项目|整个仓库|架构|重构|端到端|跨模块/i,
|
|
18
|
+
];
|
|
19
|
+
export function classifyTaskSize(input) {
|
|
20
|
+
const text = (typeof input === "string"
|
|
21
|
+
? input
|
|
22
|
+
: input
|
|
23
|
+
.filter((part) => part.type === "text")
|
|
24
|
+
.map((part) => part.text)
|
|
25
|
+
.join("\n")).trim();
|
|
26
|
+
if (!text)
|
|
27
|
+
return "normal";
|
|
28
|
+
// Up to ~120 chars with a small-task verb+noun match and no negation: small.
|
|
29
|
+
if (text.length <= 120 && matchesSmallTaskPattern(text)) {
|
|
30
|
+
return LARGE_TASK_NEGATIONS.some((re) => re.test(text)) ? "normal" : "small";
|
|
31
|
+
}
|
|
32
|
+
return "normal";
|
|
33
|
+
}
|
|
@@ -32,3 +32,4 @@ export interface ParsedReadCommand {
|
|
|
32
32
|
}
|
|
33
33
|
export declare function parseSearchBashCommand(command: string): ParsedSearchCommand | undefined;
|
|
34
34
|
export declare function parseReadBashCommand(command: string): ParsedReadCommand | undefined;
|
|
35
|
+
export declare function shellSplit(command: string): string[];
|
|
@@ -256,7 +256,7 @@ function parseLineCount(value) {
|
|
|
256
256
|
const parsed = Number(value.replace(/^\+/, ""));
|
|
257
257
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
258
258
|
}
|
|
259
|
-
function shellSplit(command) {
|
|
259
|
+
export function shellSplit(command) {
|
|
260
260
|
const tokens = [];
|
|
261
261
|
let current = "";
|
|
262
262
|
let quote = null;
|
package/dist/agent.js
CHANGED
|
@@ -338,6 +338,9 @@ export class Agent {
|
|
|
338
338
|
if (chunk.argumentsFull !== undefined) {
|
|
339
339
|
currentToolCall.args = chunk.argumentsFull;
|
|
340
340
|
}
|
|
341
|
+
if (chunk.argumentsCorrupt) {
|
|
342
|
+
currentToolCall.argsCorrupt = true;
|
|
343
|
+
}
|
|
341
344
|
if (chunk.arguments) {
|
|
342
345
|
yield {
|
|
343
346
|
type: "tool_call_delta",
|
|
@@ -353,6 +356,7 @@ export class Agent {
|
|
|
353
356
|
id: currentToolCall.id,
|
|
354
357
|
name: currentToolCall.name,
|
|
355
358
|
arguments: currentToolCall.args,
|
|
359
|
+
...(currentToolCall.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
356
360
|
});
|
|
357
361
|
yield {
|
|
358
362
|
type: "tool_call_end",
|
|
@@ -405,10 +409,14 @@ export class Agent {
|
|
|
405
409
|
for (let index = 0; index < assistantMsg.toolCalls.length; index++) {
|
|
406
410
|
const tc = assistantMsg.toolCalls[index];
|
|
407
411
|
try {
|
|
408
|
-
parsedCalls.push({
|
|
412
|
+
parsedCalls.push({
|
|
413
|
+
...tc,
|
|
414
|
+
parsedArgs: JSON.parse(tc.arguments),
|
|
415
|
+
...(tc.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
416
|
+
});
|
|
409
417
|
}
|
|
410
418
|
catch {
|
|
411
|
-
parsedCalls.push({ ...tc, parsedArgs: {} });
|
|
419
|
+
parsedCalls.push({ ...tc, parsedArgs: {}, argsCorrupt: true });
|
|
412
420
|
}
|
|
413
421
|
}
|
|
414
422
|
const executedResults = [];
|
|
@@ -1084,6 +1092,25 @@ export class Agent {
|
|
|
1084
1092
|
isError: true,
|
|
1085
1093
|
};
|
|
1086
1094
|
}
|
|
1095
|
+
if (toolCall.argsCorrupt) {
|
|
1096
|
+
return {
|
|
1097
|
+
content: `Error: The arguments for "${toolCall.name}" failed to parse as JSON, indicating the tool call was truncated or malformed mid-stream. ` +
|
|
1098
|
+
`Re-issue the call with valid JSON arguments; do not assume the previous attempt ran.`,
|
|
1099
|
+
isError: true,
|
|
1100
|
+
status: "blocked",
|
|
1101
|
+
metadata: { kind: "security", reason: "args_corrupt" },
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const missingRequired = findMissingRequiredArgs(tool.parameters, toolCall.parsedArgs);
|
|
1105
|
+
if (missingRequired.length > 0) {
|
|
1106
|
+
return {
|
|
1107
|
+
content: `Error: Tool "${toolCall.name}" was called without required argument${missingRequired.length === 1 ? "" : "s"}: ${missingRequired.map((name) => `"${name}"`).join(", ")}. ` +
|
|
1108
|
+
`Re-issue the call with all required fields filled. Do not assume the previous attempt ran with default values.`,
|
|
1109
|
+
isError: true,
|
|
1110
|
+
status: "blocked",
|
|
1111
|
+
metadata: { kind: "security", reason: "missing_required_args", missing: missingRequired },
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1087
1114
|
try {
|
|
1088
1115
|
return await tool.execute(toolCall.parsedArgs, {
|
|
1089
1116
|
cwd,
|
|
@@ -1102,6 +1129,23 @@ export class Agent {
|
|
|
1102
1129
|
}
|
|
1103
1130
|
}
|
|
1104
1131
|
}
|
|
1132
|
+
function findMissingRequiredArgs(schema, args) {
|
|
1133
|
+
const required = schema?.required;
|
|
1134
|
+
if (!required || required.length === 0)
|
|
1135
|
+
return [];
|
|
1136
|
+
const missing = [];
|
|
1137
|
+
for (const name of required) {
|
|
1138
|
+
const value = args ? args[name] : undefined;
|
|
1139
|
+
// Empty strings/arrays are intentionally allowed — writing an empty file
|
|
1140
|
+
// or passing an empty list can be legitimate. Only undefined/null counts
|
|
1141
|
+
// as "missing", because the observed failure mode is `finalArgs: "{}"`
|
|
1142
|
+
// where the field is entirely absent.
|
|
1143
|
+
if (value === undefined || value === null) {
|
|
1144
|
+
missing.push(name);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return missing;
|
|
1148
|
+
}
|
|
1105
1149
|
function estimateResidentChars(messages) {
|
|
1106
1150
|
let total = 0;
|
|
1107
1151
|
for (const message of messages) {
|
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)
|