@bubblebrain-ai/bubble 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +8 -3
  2. package/dist/agent/budget-ledger.d.ts +20 -0
  3. package/dist/agent/budget-ledger.js +51 -0
  4. package/dist/agent/execution-governor.d.ts +14 -0
  5. package/dist/agent/execution-governor.js +172 -14
  6. package/dist/agent/profiles.d.ts +59 -0
  7. package/dist/agent/profiles.js +460 -0
  8. package/dist/agent/subagent-control.d.ts +52 -0
  9. package/dist/agent/subagent-control.js +38 -0
  10. package/dist/agent/task-classifier.d.ts +1 -1
  11. package/dist/agent/task-classifier.js +60 -0
  12. package/dist/agent/tool-intent.d.ts +14 -0
  13. package/dist/agent/tool-intent.js +125 -1
  14. package/dist/agent.d.ts +60 -1
  15. package/dist/agent.js +606 -53
  16. package/dist/bin.d.ts +2 -0
  17. package/dist/bin.js +45 -0
  18. package/dist/context/budget.js +1 -0
  19. package/dist/context/compact-llm.js +7 -6
  20. package/dist/context/compact.js +6 -6
  21. package/dist/context/projector.d.ts +3 -3
  22. package/dist/context/projector.js +32 -18
  23. package/dist/context/prune.d.ts +2 -2
  24. package/dist/context/prune.js +1 -4
  25. package/dist/main.d.ts +1 -1
  26. package/dist/main.js +13 -6
  27. package/dist/mcp/manager.js +1 -0
  28. package/dist/orchestrator/default-hooks.js +92 -1
  29. package/dist/orchestrator/hooks.d.ts +10 -0
  30. package/dist/prompt/compose.d.ts +1 -0
  31. package/dist/prompt/compose.js +20 -1
  32. package/dist/prompt/environment.js +21 -2
  33. package/dist/prompt/provider-prompts/deepseek.d.ts +1 -0
  34. package/dist/prompt/provider-prompts/deepseek.js +8 -0
  35. package/dist/prompt/provider-prompts/glm.d.ts +1 -0
  36. package/dist/prompt/provider-prompts/glm.js +7 -0
  37. package/dist/prompt/provider-prompts/kimi.d.ts +1 -0
  38. package/dist/prompt/provider-prompts/kimi.js +7 -0
  39. package/dist/prompt/reminders.d.ts +5 -1
  40. package/dist/prompt/reminders.js +51 -6
  41. package/dist/prompt/runtime.d.ts +1 -1
  42. package/dist/prompt/runtime.js +16 -3
  43. package/dist/prompt/task-reminders.d.ts +2 -0
  44. package/dist/prompt/task-reminders.js +56 -0
  45. package/dist/provider-artifacts.d.ts +7 -0
  46. package/dist/provider-artifacts.js +60 -0
  47. package/dist/provider.d.ts +6 -7
  48. package/dist/provider.js +77 -15
  49. package/dist/session-log.js +3 -1
  50. package/dist/slash-commands/commands.js +2 -3
  51. package/dist/system-prompt.d.ts +2 -0
  52. package/dist/tools/agent-lifecycle.d.ts +6 -0
  53. package/dist/tools/agent-lifecycle.js +355 -0
  54. package/dist/tools/bash.js +12 -7
  55. package/dist/tools/edit-apply.d.ts +25 -0
  56. package/dist/tools/edit-apply.js +197 -0
  57. package/dist/tools/edit.js +64 -52
  58. package/dist/tools/exit-plan-mode.js +3 -1
  59. package/dist/tools/file-mutation-queue.d.ts +1 -0
  60. package/dist/tools/file-mutation-queue.js +32 -0
  61. package/dist/tools/glob.js +1 -0
  62. package/dist/tools/grep.js +1 -0
  63. package/dist/tools/index.d.ts +1 -1
  64. package/dist/tools/index.js +3 -3
  65. package/dist/tools/lsp.js +2 -0
  66. package/dist/tools/memory.js +2 -0
  67. package/dist/tools/question.js +2 -0
  68. package/dist/tools/read.js +1 -0
  69. package/dist/tools/skill.js +1 -0
  70. package/dist/tools/task.js +1 -0
  71. package/dist/tools/todo.js +1 -0
  72. package/dist/tools/tool-search.js +2 -1
  73. package/dist/tools/web-fetch.js +1 -0
  74. package/dist/tools/web-search.js +1 -0
  75. package/dist/tools/write.js +10 -1
  76. package/dist/tui/display-history.d.ts +8 -1
  77. package/dist/tui/image-paste.d.ts +41 -0
  78. package/dist/tui/image-paste.js +217 -0
  79. package/dist/tui/markdown-inline.d.ts +22 -0
  80. package/dist/tui/markdown-inline.js +68 -0
  81. package/dist/tui/render-signature.d.ts +1 -0
  82. package/dist/tui/render-signature.js +7 -0
  83. package/dist/tui/run.js +814 -269
  84. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  85. package/dist/tui/tool-renderers/fallback.js +75 -0
  86. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  87. package/dist/tui/tool-renderers/registry.js +11 -0
  88. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  89. package/dist/tui/tool-renderers/subagent.js +114 -0
  90. package/dist/tui/tool-renderers/types.d.ts +36 -0
  91. package/dist/tui/tool-renderers/types.js +1 -0
  92. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  93. package/dist/tui/tool-renderers/write-preview.js +22 -0
  94. package/dist/tui/tool-renderers/write.d.ts +6 -0
  95. package/dist/tui/tool-renderers/write.js +82 -0
  96. package/dist/types.d.ts +90 -10
  97. package/package.json +3 -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
- - Bun
8
- - Node.js and npm
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.1.tgz
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:
@@ -0,0 +1,20 @@
1
+ import type { TokenUsage } from "../types.js";
2
+ export interface BudgetUsageSource {
3
+ runId: string;
4
+ subAgentId?: string;
5
+ }
6
+ export interface BudgetSnapshot {
7
+ spent: number;
8
+ limit?: number;
9
+ exhausted: boolean;
10
+ }
11
+ export declare class BudgetLedger {
12
+ private readonly limit?;
13
+ private spent;
14
+ private readonly controller;
15
+ constructor(limit?: number | undefined);
16
+ get signal(): AbortSignal;
17
+ recordUsage(usage: TokenUsage, source: BudgetUsageSource): void;
18
+ snapshot(): BudgetSnapshot;
19
+ }
20
+ export declare function composeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
@@ -0,0 +1,51 @@
1
+ export class BudgetLedger {
2
+ limit;
3
+ spent = 0;
4
+ controller = new AbortController();
5
+ constructor(limit) {
6
+ this.limit = limit;
7
+ }
8
+ get signal() {
9
+ return this.controller.signal;
10
+ }
11
+ recordUsage(usage, source) {
12
+ const delta = usage.promptTokens + usage.completionTokens;
13
+ this.spent += delta;
14
+ if (this.limit !== undefined && this.spent >= this.limit && !this.controller.signal.aborted) {
15
+ this.controller.abort(budgetAbortError("Budget exhausted"));
16
+ }
17
+ }
18
+ snapshot() {
19
+ return {
20
+ spent: this.spent,
21
+ limit: this.limit,
22
+ exhausted: this.limit !== undefined && this.spent >= this.limit,
23
+ };
24
+ }
25
+ }
26
+ function budgetAbortError(message) {
27
+ const error = new Error(message);
28
+ error.name = "AbortError";
29
+ return error;
30
+ }
31
+ export function composeAbortSignals(signals) {
32
+ const active = signals.filter((signal) => !!signal);
33
+ if (active.length === 0)
34
+ return undefined;
35
+ if (active.length === 1)
36
+ return active[0];
37
+ const controller = new AbortController();
38
+ const abort = (signal) => {
39
+ if (controller.signal.aborted)
40
+ return;
41
+ controller.abort(signal.reason);
42
+ };
43
+ for (const signal of active) {
44
+ if (signal.aborted) {
45
+ abort(signal);
46
+ break;
47
+ }
48
+ signal.addEventListener("abort", () => abort(signal), { once: true });
49
+ }
50
+ return controller.signal;
51
+ }
@@ -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", "spawn_agent", "wait_agent", "send_input", "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
- if (!this.searchFrozen) {
59
- return toolDefinitions;
137
+ let filtered = toolDefinitions;
138
+ if (this.explorationFrozen) {
139
+ filtered = filtered.filter((tool) => !EXPLORATION_TOOLS_DISABLED.has(tool.name));
60
140
  }
61
- return toolDefinitions.filter((tool) => !SEARCH_TOOLS_DISABLED.has(tool.name));
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
- this.maybeWarnOnSoftBudgets(intent.family === "search");
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 progress = inferProgress(intent, result);
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
- maybeWarnOnSoftBudgets(isSearchStep) {
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: "security",
323
+ kind,
166
324
  reason,
167
325
  },
168
326
  };
@@ -0,0 +1,59 @@
1
+ import type { ToolRegistryEntry, TokenUsage } from "../types.js";
2
+ import { type SubtaskType } from "./subtask-policy.js";
3
+ export type AgentProfileSource = "user" | "project" | "builtin";
4
+ export type AgentProfileMode = "readonly" | "write_patch" | "write_worktree";
5
+ export type AgentProfileApproval = "fail" | "disabled";
6
+ export type AgentProfileToolPreset = "readonly" | "none" | "explicit";
7
+ export interface AgentProfileTools {
8
+ preset: AgentProfileToolPreset;
9
+ include?: string[];
10
+ exclude?: string[];
11
+ }
12
+ export interface AgentProfile {
13
+ name: string;
14
+ description: string;
15
+ source: AgentProfileSource;
16
+ filePath?: string;
17
+ mode: AgentProfileMode;
18
+ model?: string | "inherit";
19
+ tools: AgentProfileTools;
20
+ maxTurns?: number;
21
+ approval: AgentProfileApproval;
22
+ nicknameCandidates?: string[];
23
+ prompt: string;
24
+ subtaskType?: SubtaskType;
25
+ }
26
+ export interface SubagentRunResult {
27
+ subAgentId: string;
28
+ agentName: string;
29
+ nickname?: string;
30
+ status: "completed" | "failed" | "blocked" | "cancelled";
31
+ profileSource: AgentProfileSource;
32
+ task: string;
33
+ summary: string;
34
+ toolNotes: string[];
35
+ usage?: {
36
+ promptTokens: number;
37
+ completionTokens: number;
38
+ totalTokens: number;
39
+ };
40
+ error?: string;
41
+ }
42
+ export interface DiscoverAgentProfilesResult {
43
+ profiles: AgentProfile[];
44
+ projectAgentsDir: string | null;
45
+ diagnostics: string[];
46
+ }
47
+ export interface AgentProfileDiagnostic {
48
+ severity: "warning" | "error";
49
+ message: string;
50
+ toolName?: string;
51
+ }
52
+ export type AgentProfileScope = "user" | "project" | "both";
53
+ export declare function discoverAgentProfiles(cwd: string, scope?: AgentProfileScope): DiscoverAgentProfilesResult;
54
+ export declare function builtinAgentProfiles(): AgentProfile[];
55
+ export declare function findAgentProfile(profiles: AgentProfile[], name: string): AgentProfile | undefined;
56
+ export declare function assignAgentNickname(profile: AgentProfile, activeNicknames?: Iterable<string>): string;
57
+ export declare function selectToolsForAgentProfile(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): ToolRegistryEntry[];
58
+ export declare function validateAgentProfileTools(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): AgentProfileDiagnostic[];
59
+ export declare function mergeUsage(current: SubagentRunResult["usage"], usage: TokenUsage): SubagentRunResult["usage"];