@bubblebrain-ai/bubble 0.0.2 → 0.0.4
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 +8 -3
- package/dist/agent/execution-governor.d.ts +14 -0
- package/dist/agent/execution-governor.js +172 -14
- package/dist/agent/task-classifier.d.ts +1 -1
- package/dist/agent/task-classifier.js +60 -0
- package/dist/agent/tool-intent.d.ts +14 -0
- package/dist/agent/tool-intent.js +125 -1
- package/dist/agent.js +4 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +45 -0
- package/dist/orchestrator/default-hooks.js +53 -1
- package/dist/orchestrator/hooks.d.ts +5 -0
- package/dist/prompt/compose.js +12 -0
- package/dist/prompt/provider-prompts/deepseek.d.ts +1 -0
- package/dist/prompt/provider-prompts/deepseek.js +8 -0
- package/dist/prompt/provider-prompts/glm.d.ts +1 -0
- package/dist/prompt/provider-prompts/glm.js +7 -0
- package/dist/prompt/provider-prompts/kimi.d.ts +1 -0
- package/dist/prompt/provider-prompts/kimi.js +7 -0
- package/dist/prompt/reminders.d.ts +2 -0
- package/dist/prompt/reminders.js +28 -2
- package/dist/prompt/runtime.js +15 -2
- package/dist/prompt/task-reminders.d.ts +2 -0
- package/dist/prompt/task-reminders.js +56 -0
- package/dist/slash-commands/commands.js +2 -3
- package/dist/tools/bash.js +10 -7
- package/dist/tools/edit.js +5 -0
- package/dist/tools/write.js +8 -1
- package/dist/tui/image-paste.d.ts +41 -0
- package/dist/tui/image-paste.js +217 -0
- package/dist/tui/run.js +219 -11
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@ Bubble is a terminal coding agent for working inside local project folders. It c
|
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- Node.js 20+ and npm for installation
|
|
8
|
+
- Bun for running Bubble
|
|
9
9
|
|
|
10
10
|
Install Bun if it is not already available:
|
|
11
11
|
|
|
@@ -24,9 +24,14 @@ npm install -g @bubblebrain-ai/bubble
|
|
|
24
24
|
From a local package tarball:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
npm install -g ./bubblebrain-ai-bubble-0.0.
|
|
27
|
+
npm install -g ./bubblebrain-ai-bubble-0.0.3.tgz
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
The npm command installs a small Node.js launcher named `bubble`. When you run
|
|
31
|
+
`bubble`, the launcher checks for Bun and starts the real Bubble runtime with
|
|
32
|
+
`bun`. If Bun is missing, it prints the install command above instead of failing
|
|
33
|
+
with a low-level runtime error.
|
|
34
|
+
|
|
30
35
|
## Usage
|
|
31
36
|
|
|
32
37
|
Start Bubble in the current directory:
|
|
@@ -3,28 +3,42 @@ import type { TaskType } from "./task-classifier.js";
|
|
|
3
3
|
export interface GovernorDecision {
|
|
4
4
|
blockedResult?: ToolResult;
|
|
5
5
|
}
|
|
6
|
+
type WorkPhase = "explore" | "modify" | "verify";
|
|
6
7
|
export declare class ExecutionGovernor {
|
|
7
8
|
private taskType;
|
|
8
9
|
private budget;
|
|
9
10
|
private history;
|
|
10
11
|
private totalSteps;
|
|
11
12
|
private searchSteps;
|
|
13
|
+
private readSteps;
|
|
14
|
+
private explorationStepsWithoutWrite;
|
|
12
15
|
private searchFrozen;
|
|
16
|
+
private explorationFrozen;
|
|
17
|
+
private phase;
|
|
18
|
+
private codeChanged;
|
|
13
19
|
private reminderQueue;
|
|
14
20
|
private warnedFamilies;
|
|
15
21
|
private softTotalWarned;
|
|
16
22
|
private softSearchWarned;
|
|
23
|
+
private softReadWarned;
|
|
17
24
|
constructor(taskType: TaskType);
|
|
18
25
|
consumePendingReminders(): string[];
|
|
19
26
|
snapshot(): {
|
|
20
27
|
totalSteps: number;
|
|
21
28
|
searchSteps: number;
|
|
29
|
+
readSteps: number;
|
|
22
30
|
searchFrozen: boolean;
|
|
31
|
+
explorationFrozen: boolean;
|
|
32
|
+
phase: WorkPhase;
|
|
23
33
|
};
|
|
24
34
|
filterToolDefinitions(toolDefinitions: ToolRegistryEntry[]): ToolRegistryEntry[];
|
|
25
35
|
beforeToolCall(toolCall: ParsedToolCall): GovernorDecision;
|
|
26
36
|
afterToolResult(toolCall: ParsedToolCall, result: ToolResult): void;
|
|
27
37
|
private trailingNoProgressCount;
|
|
28
38
|
private freezeSearch;
|
|
39
|
+
private enterModifyPhase;
|
|
40
|
+
private isModificationTask;
|
|
41
|
+
private historyCount;
|
|
29
42
|
private maybeWarnOnSoftBudgets;
|
|
30
43
|
}
|
|
44
|
+
export {};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { analyzeToolIntent } from "./tool-intent.js";
|
|
2
|
-
import { buildInvestigationReminder, buildLoopWarningReminder, buildSearchFreezeReminder } from "../prompt/reminders.js";
|
|
2
|
+
import { buildExplorationFreezeReminder, buildInvestigationReminder, buildLoopWarningReminder, buildSearchFreezeReminder } from "../prompt/reminders.js";
|
|
3
3
|
const BUDGETS = {
|
|
4
4
|
security_investigation: {
|
|
5
5
|
softTotalSteps: 14,
|
|
6
6
|
softSearchSteps: 6,
|
|
7
|
+
softReadSteps: 8,
|
|
8
|
+
maxExplorationStepsWithoutWrite: 12,
|
|
9
|
+
maxNoProgressReadExactRepeats: 2,
|
|
7
10
|
maxNoProgressExactRepeats: 2,
|
|
8
11
|
maxNoProgressFamilyRepeats: 3,
|
|
9
12
|
warningFamilyRepeats: 2,
|
|
@@ -11,30 +14,103 @@ const BUDGETS = {
|
|
|
11
14
|
code_search: {
|
|
12
15
|
softTotalSteps: 16,
|
|
13
16
|
softSearchSteps: 8,
|
|
17
|
+
softReadSteps: 10,
|
|
18
|
+
maxExplorationStepsWithoutWrite: 14,
|
|
19
|
+
maxNoProgressReadExactRepeats: 2,
|
|
14
20
|
maxNoProgressExactRepeats: 3,
|
|
15
21
|
maxNoProgressFamilyRepeats: 4,
|
|
16
22
|
warningFamilyRepeats: 3,
|
|
17
23
|
},
|
|
24
|
+
debugging: {
|
|
25
|
+
softTotalSteps: 18,
|
|
26
|
+
softSearchSteps: 8,
|
|
27
|
+
softReadSteps: 7,
|
|
28
|
+
maxExplorationStepsWithoutWrite: 10,
|
|
29
|
+
maxNoProgressReadExactRepeats: 1,
|
|
30
|
+
maxNoProgressExactRepeats: 3,
|
|
31
|
+
maxNoProgressFamilyRepeats: 4,
|
|
32
|
+
warningFamilyRepeats: 3,
|
|
33
|
+
},
|
|
34
|
+
implementation: {
|
|
35
|
+
softTotalSteps: 18,
|
|
36
|
+
softSearchSteps: 8,
|
|
37
|
+
softReadSteps: 6,
|
|
38
|
+
maxExplorationStepsWithoutWrite: 8,
|
|
39
|
+
maxNoProgressReadExactRepeats: 1,
|
|
40
|
+
maxNoProgressExactRepeats: 3,
|
|
41
|
+
maxNoProgressFamilyRepeats: 4,
|
|
42
|
+
warningFamilyRepeats: 3,
|
|
43
|
+
},
|
|
44
|
+
code_review: {
|
|
45
|
+
softTotalSteps: 14,
|
|
46
|
+
softSearchSteps: 6,
|
|
47
|
+
softReadSteps: 8,
|
|
48
|
+
maxExplorationStepsWithoutWrite: 12,
|
|
49
|
+
maxNoProgressReadExactRepeats: 2,
|
|
50
|
+
maxNoProgressExactRepeats: 3,
|
|
51
|
+
maxNoProgressFamilyRepeats: 4,
|
|
52
|
+
warningFamilyRepeats: 3,
|
|
53
|
+
},
|
|
54
|
+
code_explanation: {
|
|
55
|
+
softTotalSteps: 12,
|
|
56
|
+
softSearchSteps: 6,
|
|
57
|
+
softReadSteps: 8,
|
|
58
|
+
maxExplorationStepsWithoutWrite: 12,
|
|
59
|
+
maxNoProgressReadExactRepeats: 2,
|
|
60
|
+
maxNoProgressExactRepeats: 3,
|
|
61
|
+
maxNoProgressFamilyRepeats: 4,
|
|
62
|
+
warningFamilyRepeats: 3,
|
|
63
|
+
},
|
|
64
|
+
repo_orientation: {
|
|
65
|
+
softTotalSteps: 12,
|
|
66
|
+
softSearchSteps: 6,
|
|
67
|
+
softReadSteps: 8,
|
|
68
|
+
maxExplorationStepsWithoutWrite: 12,
|
|
69
|
+
maxNoProgressReadExactRepeats: 2,
|
|
70
|
+
maxNoProgressExactRepeats: 3,
|
|
71
|
+
maxNoProgressFamilyRepeats: 4,
|
|
72
|
+
warningFamilyRepeats: 3,
|
|
73
|
+
},
|
|
74
|
+
product_discussion: {
|
|
75
|
+
softTotalSteps: 10,
|
|
76
|
+
softSearchSteps: 4,
|
|
77
|
+
softReadSteps: 4,
|
|
78
|
+
maxExplorationStepsWithoutWrite: 8,
|
|
79
|
+
maxNoProgressReadExactRepeats: 2,
|
|
80
|
+
maxNoProgressExactRepeats: 2,
|
|
81
|
+
maxNoProgressFamilyRepeats: 3,
|
|
82
|
+
warningFamilyRepeats: 2,
|
|
83
|
+
},
|
|
18
84
|
general: {
|
|
19
85
|
softTotalSteps: 18,
|
|
20
86
|
softSearchSteps: 8,
|
|
87
|
+
softReadSteps: 10,
|
|
88
|
+
maxExplorationStepsWithoutWrite: 14,
|
|
89
|
+
maxNoProgressReadExactRepeats: 2,
|
|
21
90
|
maxNoProgressExactRepeats: 3,
|
|
22
91
|
maxNoProgressFamilyRepeats: 4,
|
|
23
92
|
warningFamilyRepeats: 3,
|
|
24
93
|
},
|
|
25
94
|
};
|
|
26
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", "task", "tool_search"]);
|
|
27
97
|
export class ExecutionGovernor {
|
|
28
98
|
taskType;
|
|
29
99
|
budget;
|
|
30
100
|
history = [];
|
|
31
101
|
totalSteps = 0;
|
|
32
102
|
searchSteps = 0;
|
|
103
|
+
readSteps = 0;
|
|
104
|
+
explorationStepsWithoutWrite = 0;
|
|
33
105
|
searchFrozen = false;
|
|
106
|
+
explorationFrozen = false;
|
|
107
|
+
phase = "explore";
|
|
108
|
+
codeChanged = false;
|
|
34
109
|
reminderQueue = [];
|
|
35
110
|
warnedFamilies = new Set();
|
|
36
111
|
softTotalWarned = false;
|
|
37
112
|
softSearchWarned = false;
|
|
113
|
+
softReadWarned = false;
|
|
38
114
|
constructor(taskType) {
|
|
39
115
|
this.taskType = taskType;
|
|
40
116
|
this.budget = BUDGETS[taskType];
|
|
@@ -51,21 +127,42 @@ export class ExecutionGovernor {
|
|
|
51
127
|
return {
|
|
52
128
|
totalSteps: this.totalSteps,
|
|
53
129
|
searchSteps: this.searchSteps,
|
|
130
|
+
readSteps: this.readSteps,
|
|
54
131
|
searchFrozen: this.searchFrozen,
|
|
132
|
+
explorationFrozen: this.explorationFrozen,
|
|
133
|
+
phase: this.phase,
|
|
55
134
|
};
|
|
56
135
|
}
|
|
57
136
|
filterToolDefinitions(toolDefinitions) {
|
|
58
|
-
|
|
59
|
-
|
|
137
|
+
let filtered = toolDefinitions;
|
|
138
|
+
if (this.explorationFrozen) {
|
|
139
|
+
filtered = filtered.filter((tool) => !EXPLORATION_TOOLS_DISABLED.has(tool.name));
|
|
60
140
|
}
|
|
61
|
-
|
|
141
|
+
else if (this.searchFrozen) {
|
|
142
|
+
filtered = filtered.filter((tool) => !SEARCH_TOOLS_DISABLED.has(tool.name));
|
|
143
|
+
}
|
|
144
|
+
return filtered;
|
|
62
145
|
}
|
|
63
146
|
beforeToolCall(toolCall) {
|
|
64
147
|
const intent = analyzeToolIntent(toolCall);
|
|
148
|
+
if (this.explorationFrozen && isExplorationIntent(intent)) {
|
|
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") {
|
|
154
|
+
const signature = intent.read?.signature;
|
|
155
|
+
if (signature && this.historyCount((entry) => entry.signature === signature) >= this.budget.maxNoProgressReadExactRepeats) {
|
|
156
|
+
this.enterModifyPhase(`Repeated the same file range without making progress: ${signature}`);
|
|
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
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
65
162
|
if (intent.family === "search") {
|
|
66
163
|
if (this.searchFrozen) {
|
|
67
164
|
return {
|
|
68
|
-
blockedResult: blockedResult("Search blocked: repeated low-yield searching is now frozen for this task.", "blocked", "Search frozen due to repeated low-yield searching."),
|
|
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"),
|
|
69
166
|
};
|
|
70
167
|
}
|
|
71
168
|
const signature = intent.search?.signature;
|
|
@@ -73,7 +170,7 @@ export class ExecutionGovernor {
|
|
|
73
170
|
if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.maxNoProgressExactRepeats) {
|
|
74
171
|
this.freezeSearch(`Repeated the same search signature without new evidence: ${signature}`);
|
|
75
172
|
return {
|
|
76
|
-
blockedResult: blockedResult("Search blocked: repeated the same search multiple times without new evidence.", "blocked", "Repeated identical search without progress."),
|
|
173
|
+
blockedResult: blockedResult("Search blocked: repeated the same search multiple times without new evidence.", "blocked", "Repeated identical search without progress.", "search"),
|
|
77
174
|
};
|
|
78
175
|
}
|
|
79
176
|
if (familyKey) {
|
|
@@ -81,7 +178,7 @@ export class ExecutionGovernor {
|
|
|
81
178
|
if (familyNoProgress >= this.budget.maxNoProgressFamilyRepeats) {
|
|
82
179
|
this.freezeSearch(`Repeated the same search family without new evidence: ${familyKey}`);
|
|
83
180
|
return {
|
|
84
|
-
blockedResult: blockedResult("Search blocked: repeated the same search family without new evidence.", "blocked", "Repeated similar searches without progress."),
|
|
181
|
+
blockedResult: blockedResult("Search blocked: repeated the same search family without new evidence.", "blocked", "Repeated similar searches without progress.", "search"),
|
|
85
182
|
};
|
|
86
183
|
}
|
|
87
184
|
if (familyNoProgress >= this.budget.warningFamilyRepeats && !this.warnedFamilies.has(familyKey)) {
|
|
@@ -94,18 +191,38 @@ export class ExecutionGovernor {
|
|
|
94
191
|
if (intent.family === "search") {
|
|
95
192
|
this.searchSteps += 1;
|
|
96
193
|
}
|
|
97
|
-
|
|
194
|
+
if (intent.family === "read") {
|
|
195
|
+
this.readSteps += 1;
|
|
196
|
+
}
|
|
197
|
+
if (isExplorationIntent(intent) && !this.codeChanged) {
|
|
198
|
+
this.explorationStepsWithoutWrite += 1;
|
|
199
|
+
}
|
|
200
|
+
this.maybeWarnOnSoftBudgets(intent.family === "search", intent.family === "read");
|
|
98
201
|
return {};
|
|
99
202
|
}
|
|
100
203
|
afterToolResult(toolCall, result) {
|
|
101
204
|
const intent = analyzeToolIntent(toolCall);
|
|
102
|
-
const
|
|
205
|
+
const repeatedRead = intent.family === "read"
|
|
206
|
+
&& !!intent.read?.signature
|
|
207
|
+
&& this.history.some((entry) => entry.signature === intent.read?.signature);
|
|
208
|
+
const progress = inferProgress(intent, result) && !repeatedRead;
|
|
103
209
|
this.history.push({
|
|
104
210
|
family: intent.family,
|
|
105
|
-
signature: intent.search?.signature,
|
|
106
|
-
familyKey: intent.search?.familyKey,
|
|
211
|
+
signature: intent.search?.signature ?? intent.read?.signature,
|
|
212
|
+
familyKey: intent.search?.familyKey ?? intent.read?.familyKey,
|
|
107
213
|
progress,
|
|
108
214
|
});
|
|
215
|
+
if (isSuccessfulWriteIntent(intent, result)) {
|
|
216
|
+
this.codeChanged = true;
|
|
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.`);
|
|
225
|
+
}
|
|
109
226
|
}
|
|
110
227
|
trailingNoProgressCount(predicate) {
|
|
111
228
|
let count = 0;
|
|
@@ -128,7 +245,22 @@ export class ExecutionGovernor {
|
|
|
128
245
|
this.searchFrozen = true;
|
|
129
246
|
this.reminderQueue.push(buildSearchFreezeReminder(reason));
|
|
130
247
|
}
|
|
131
|
-
|
|
248
|
+
enterModifyPhase(reason) {
|
|
249
|
+
if (this.explorationFrozen) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.phase = "modify";
|
|
253
|
+
this.explorationFrozen = true;
|
|
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);
|
|
262
|
+
}
|
|
263
|
+
maybeWarnOnSoftBudgets(isSearchStep, isReadStep) {
|
|
132
264
|
if (!this.softTotalWarned && this.totalSteps >= this.budget.softTotalSteps) {
|
|
133
265
|
this.softTotalWarned = true;
|
|
134
266
|
this.reminderQueue.push(buildLoopWarningReminder("This task has already used many tool steps. Do not keep exploring by default; synthesize what you know unless a concrete missing gap remains."));
|
|
@@ -137,6 +269,32 @@ export class ExecutionGovernor {
|
|
|
137
269
|
this.softSearchWarned = true;
|
|
138
270
|
this.reminderQueue.push(buildLoopWarningReminder("This task has already used many search steps. Stop broad searching unless you can point to a specific remaining evidence gap."));
|
|
139
271
|
}
|
|
272
|
+
if (isReadStep && !this.softReadWarned && this.readSteps >= this.budget.softReadSteps) {
|
|
273
|
+
this.softReadWarned = true;
|
|
274
|
+
this.reminderQueue.push(buildLoopWarningReminder("This task has already used many file reads. Stop re-reading context unless a concrete edit requires one exact missing snippet."));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function isExplorationIntent(intent) {
|
|
279
|
+
return intent.family === "search" || intent.family === "read" || intent.family === "web";
|
|
280
|
+
}
|
|
281
|
+
function isSuccessfulWriteIntent(intent, result) {
|
|
282
|
+
if (result.isError || result.status === "blocked" || result.status === "command_error") {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return intent.family === "write" || intent.family === "edit" || result.metadata?.kind === "write" || result.metadata?.kind === "edit";
|
|
286
|
+
}
|
|
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";
|
|
140
298
|
}
|
|
141
299
|
}
|
|
142
300
|
function inferProgress(intent, result) {
|
|
@@ -156,13 +314,13 @@ function inferProgress(intent, result) {
|
|
|
156
314
|
}
|
|
157
315
|
return !result.isError;
|
|
158
316
|
}
|
|
159
|
-
function blockedResult(content, status, reason) {
|
|
317
|
+
function blockedResult(content, status, reason, kind = "security") {
|
|
160
318
|
return {
|
|
161
319
|
content,
|
|
162
320
|
isError: true,
|
|
163
321
|
status,
|
|
164
322
|
metadata: {
|
|
165
|
-
kind
|
|
323
|
+
kind,
|
|
166
324
|
reason,
|
|
167
325
|
},
|
|
168
326
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ContentPart } from "../types.js";
|
|
2
|
-
export type TaskType = "security_investigation" | "code_search" | "general";
|
|
2
|
+
export type TaskType = "security_investigation" | "debugging" | "implementation" | "code_review" | "code_explanation" | "repo_orientation" | "product_discussion" | "code_search" | "general";
|
|
3
3
|
export declare function classifyTask(input: string | ContentPart[]): TaskType;
|
|
@@ -18,6 +18,48 @@ const SEARCH_PATTERNS = [
|
|
|
18
18
|
/\blocate\b/i,
|
|
19
19
|
/\btrace\b/i,
|
|
20
20
|
];
|
|
21
|
+
const REVIEW_PATTERNS = [
|
|
22
|
+
/\breview\b/i,
|
|
23
|
+
/\bcode review\b/i,
|
|
24
|
+
/帮我看看.*(代码|改动|diff)/i,
|
|
25
|
+
/看下.*(风险|问题|bug)/i,
|
|
26
|
+
];
|
|
27
|
+
const DEBUG_PATTERNS = [
|
|
28
|
+
/\bdebug\b/i,
|
|
29
|
+
/\bbug\b/i,
|
|
30
|
+
/\bfail(ing|ed|ure)?\b/i,
|
|
31
|
+
/\berror\b/i,
|
|
32
|
+
/\bregression\b/i,
|
|
33
|
+
/报错|失败|不对|有问题|修复|定位/i,
|
|
34
|
+
];
|
|
35
|
+
const IMPLEMENTATION_PATTERNS = [
|
|
36
|
+
/\bimplement\b/i,
|
|
37
|
+
/\bbuild\b/i,
|
|
38
|
+
/\badd\b/i,
|
|
39
|
+
/\bchange\b/i,
|
|
40
|
+
/\bupdate\b/i,
|
|
41
|
+
/\brefactor\b/i,
|
|
42
|
+
/实现|开发|改一下|加一个|调整|优化/i,
|
|
43
|
+
];
|
|
44
|
+
const EXPLANATION_PATTERNS = [
|
|
45
|
+
/\bexplain\b/i,
|
|
46
|
+
/\bhow does\b/i,
|
|
47
|
+
/\bwhat does\b/i,
|
|
48
|
+
/解释|讲讲|怎么看|在干嘛|如何运转/i,
|
|
49
|
+
];
|
|
50
|
+
const ORIENTATION_PATTERNS = [
|
|
51
|
+
/\bwhat is this project\b/i,
|
|
52
|
+
/\borient/i,
|
|
53
|
+
/\boverview\b/i,
|
|
54
|
+
/这个项目.*(干嘛|做什么)|看下这个项目|项目.*概览/i,
|
|
55
|
+
];
|
|
56
|
+
const PRODUCT_PATTERNS = [
|
|
57
|
+
/\bproduct\b/i,
|
|
58
|
+
/\bdesign\b/i,
|
|
59
|
+
/\bstrategy\b/i,
|
|
60
|
+
/\broadmap\b/i,
|
|
61
|
+
/产品|方案|设计|体验|取舍|方向/i,
|
|
62
|
+
];
|
|
21
63
|
export function classifyTask(input) {
|
|
22
64
|
const text = typeof input === "string"
|
|
23
65
|
? input
|
|
@@ -29,6 +71,24 @@ export function classifyTask(input) {
|
|
|
29
71
|
if (securityHits >= 2) {
|
|
30
72
|
return "security_investigation";
|
|
31
73
|
}
|
|
74
|
+
if (REVIEW_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
75
|
+
return "code_review";
|
|
76
|
+
}
|
|
77
|
+
if (DEBUG_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
78
|
+
return "debugging";
|
|
79
|
+
}
|
|
80
|
+
if (ORIENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
81
|
+
return "repo_orientation";
|
|
82
|
+
}
|
|
83
|
+
if (EXPLANATION_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
84
|
+
return "code_explanation";
|
|
85
|
+
}
|
|
86
|
+
if (IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
87
|
+
return "implementation";
|
|
88
|
+
}
|
|
89
|
+
if (PRODUCT_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
90
|
+
return "product_discussion";
|
|
91
|
+
}
|
|
32
92
|
if (SEARCH_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
33
93
|
return "code_search";
|
|
34
94
|
}
|
|
@@ -7,9 +7,17 @@ export interface SearchIntent {
|
|
|
7
7
|
signature: string;
|
|
8
8
|
familyKey: string;
|
|
9
9
|
}
|
|
10
|
+
export interface ReadIntent {
|
|
11
|
+
path: string;
|
|
12
|
+
offset?: number | string;
|
|
13
|
+
limit?: number | string;
|
|
14
|
+
signature: string;
|
|
15
|
+
familyKey: string;
|
|
16
|
+
}
|
|
10
17
|
export interface ToolIntent {
|
|
11
18
|
family: ToolFamily;
|
|
12
19
|
search?: SearchIntent;
|
|
20
|
+
read?: ReadIntent;
|
|
13
21
|
}
|
|
14
22
|
export declare function analyzeToolIntent(toolCall: Pick<ParsedToolCall, "name" | "parsedArgs">): ToolIntent;
|
|
15
23
|
export interface ParsedSearchCommand {
|
|
@@ -17,4 +25,10 @@ export interface ParsedSearchCommand {
|
|
|
17
25
|
path?: string;
|
|
18
26
|
include?: string;
|
|
19
27
|
}
|
|
28
|
+
export interface ParsedReadCommand {
|
|
29
|
+
path: string;
|
|
30
|
+
offset?: number | string;
|
|
31
|
+
limit?: number | string;
|
|
32
|
+
}
|
|
20
33
|
export declare function parseSearchBashCommand(command: string): ParsedSearchCommand | undefined;
|
|
34
|
+
export declare function parseReadBashCommand(command: string): ParsedReadCommand | undefined;
|
|
@@ -38,10 +38,20 @@ export function analyzeToolIntent(toolCall) {
|
|
|
38
38
|
search: buildSearchIntent(parsed.pattern, parsed.path, parsed.include),
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
const parsedRead = parseReadBashCommand(stringArg(toolCall.parsedArgs.command));
|
|
42
|
+
if (parsedRead) {
|
|
43
|
+
return {
|
|
44
|
+
family: "read",
|
|
45
|
+
read: buildReadIntent(parsedRead.path, parsedRead.offset, parsedRead.limit),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
41
48
|
return { family: "shell" };
|
|
42
49
|
}
|
|
43
50
|
case "read":
|
|
44
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
family: "read",
|
|
53
|
+
read: buildReadIntent(stringArg(toolCall.parsedArgs.path ?? toolCall.parsedArgs.file), numberOrStringArg(toolCall.parsedArgs.offset), numberOrStringArg(toolCall.parsedArgs.limit)),
|
|
54
|
+
};
|
|
45
55
|
case "write":
|
|
46
56
|
return { family: "write" };
|
|
47
57
|
case "edit":
|
|
@@ -99,6 +109,36 @@ export function parseSearchBashCommand(command) {
|
|
|
99
109
|
include,
|
|
100
110
|
};
|
|
101
111
|
}
|
|
112
|
+
export function parseReadBashCommand(command) {
|
|
113
|
+
const trimmed = command.trim();
|
|
114
|
+
if (!trimmed) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (/[|;&><`]/.test(trimmed)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const tokens = shellSplit(trimmed);
|
|
121
|
+
if (tokens.length === 0) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const binary = tokens[0];
|
|
125
|
+
if (binary === "cat" || binary === "nl") {
|
|
126
|
+
const path = lastPositional(tokens.slice(1));
|
|
127
|
+
return path ? { path } : undefined;
|
|
128
|
+
}
|
|
129
|
+
if (binary === "head") {
|
|
130
|
+
const { path, lineCount } = parseHeadTailArgs(tokens.slice(1));
|
|
131
|
+
return path ? { path, offset: 1, limit: lineCount ?? "head" } : undefined;
|
|
132
|
+
}
|
|
133
|
+
if (binary === "tail") {
|
|
134
|
+
const { path, lineCount } = parseHeadTailArgs(tokens.slice(1));
|
|
135
|
+
return path ? { path, offset: `tail:${lineCount ?? "default"}`, limit: lineCount ?? "tail" } : undefined;
|
|
136
|
+
}
|
|
137
|
+
if (binary === "sed") {
|
|
138
|
+
return parseSedReadCommand(tokens.slice(1));
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
102
142
|
function buildSearchIntent(pattern, path, include) {
|
|
103
143
|
const normalizedPath = normalizePath(path ?? ".");
|
|
104
144
|
const rawNormalizedPattern = normalizeRawPattern(pattern);
|
|
@@ -114,6 +154,18 @@ function buildSearchIntent(pattern, path, include) {
|
|
|
114
154
|
familyKey,
|
|
115
155
|
};
|
|
116
156
|
}
|
|
157
|
+
function buildReadIntent(path, offset, limit) {
|
|
158
|
+
const normalizedPath = normalizePath(path || ".");
|
|
159
|
+
const normalizedOffset = offset ?? 1;
|
|
160
|
+
const normalizedLimit = limit ?? "default";
|
|
161
|
+
return {
|
|
162
|
+
path,
|
|
163
|
+
offset,
|
|
164
|
+
limit,
|
|
165
|
+
signature: `${normalizedPath}::${normalizedOffset}::${normalizedLimit}`,
|
|
166
|
+
familyKey: normalizedPath,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
117
169
|
function canonicalizeSearchTokens(pattern) {
|
|
118
170
|
const normalized = normalizeRawPattern(pattern);
|
|
119
171
|
const tokens = normalized.split(/[^a-z0-9_]+/).filter(Boolean);
|
|
@@ -132,6 +184,78 @@ function normalizePath(pathValue) {
|
|
|
132
184
|
function stringArg(value) {
|
|
133
185
|
return typeof value === "string" ? value : "";
|
|
134
186
|
}
|
|
187
|
+
function numberOrStringArg(value) {
|
|
188
|
+
return typeof value === "number" || typeof value === "string" ? value : undefined;
|
|
189
|
+
}
|
|
190
|
+
function lastPositional(tokens) {
|
|
191
|
+
for (let index = tokens.length - 1; index >= 0; index--) {
|
|
192
|
+
const token = tokens[index];
|
|
193
|
+
if (!token.startsWith("-")) {
|
|
194
|
+
return token;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
function parseHeadTailArgs(tokens) {
|
|
200
|
+
let lineCount;
|
|
201
|
+
const positional = [];
|
|
202
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
203
|
+
const token = tokens[index];
|
|
204
|
+
if (token === "-n" || token === "--lines") {
|
|
205
|
+
lineCount = parseLineCount(tokens[index + 1]);
|
|
206
|
+
index += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (token.startsWith("-n") && token.length > 2) {
|
|
210
|
+
lineCount = parseLineCount(token.slice(2));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (token.startsWith("--lines=")) {
|
|
214
|
+
lineCount = parseLineCount(token.slice("--lines=".length));
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (token.startsWith("-")) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
positional.push(token);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
path: positional[positional.length - 1],
|
|
224
|
+
lineCount,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function parseSedReadCommand(tokens) {
|
|
228
|
+
const positional = [];
|
|
229
|
+
for (const token of tokens) {
|
|
230
|
+
if (token === "-n" || token.startsWith("-")) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
positional.push(token);
|
|
234
|
+
}
|
|
235
|
+
if (positional.length < 2) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const expression = positional[0];
|
|
239
|
+
const path = positional[positional.length - 1];
|
|
240
|
+
const range = expression.match(/^(\d+)(?:,(\d+))?p$/);
|
|
241
|
+
if (!range) {
|
|
242
|
+
return { path, offset: expression };
|
|
243
|
+
}
|
|
244
|
+
const start = Number(range[1]);
|
|
245
|
+
const end = range[2] ? Number(range[2]) : start;
|
|
246
|
+
return {
|
|
247
|
+
path,
|
|
248
|
+
offset: start,
|
|
249
|
+
limit: Math.max(1, end - start + 1),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function parseLineCount(value) {
|
|
253
|
+
if (!value) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const parsed = Number(value.replace(/^\+/, ""));
|
|
257
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
258
|
+
}
|
|
135
259
|
function shellSplit(command) {
|
|
136
260
|
const tokens = [];
|
|
137
261
|
let current = "";
|
package/dist/agent.js
CHANGED
|
@@ -460,6 +460,10 @@ export class Agent {
|
|
|
460
460
|
});
|
|
461
461
|
flushGovernorReminders();
|
|
462
462
|
yield { type: "turn_end", usage: turnUsage };
|
|
463
|
+
if (hookState.forceContinuationReason) {
|
|
464
|
+
delete hookState.forceContinuationReason;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
463
467
|
break;
|
|
464
468
|
}
|
|
465
469
|
yield { type: "agent_end" };
|
package/dist/bin.d.ts
ADDED