@bubblebrain-ai/bubble 0.0.1
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 +70 -0
- package/dist/agent/evidence-tracker.d.ts +15 -0
- package/dist/agent/evidence-tracker.js +93 -0
- package/dist/agent/execution-governor.d.ts +30 -0
- package/dist/agent/execution-governor.js +169 -0
- package/dist/agent/subtask-policy.d.ts +14 -0
- package/dist/agent/subtask-policy.js +60 -0
- package/dist/agent/task-classifier.d.ts +3 -0
- package/dist/agent/task-classifier.js +36 -0
- package/dist/agent/tool-arbiter.d.ts +7 -0
- package/dist/agent/tool-arbiter.js +33 -0
- package/dist/agent/tool-intent.d.ts +20 -0
- package/dist/agent/tool-intent.js +176 -0
- package/dist/agent.d.ts +95 -0
- package/dist/agent.js +672 -0
- package/dist/approval/controller.d.ts +48 -0
- package/dist/approval/controller.js +78 -0
- package/dist/approval/danger.d.ts +13 -0
- package/dist/approval/danger.js +55 -0
- package/dist/approval/diff-hunks.d.ts +12 -0
- package/dist/approval/diff-hunks.js +32 -0
- package/dist/approval/session-cache.d.ts +35 -0
- package/dist/approval/session-cache.js +68 -0
- package/dist/approval/tool-helper.d.ts +14 -0
- package/dist/approval/tool-helper.js +32 -0
- package/dist/approval/types.d.ts +56 -0
- package/dist/approval/types.js +8 -0
- package/dist/bubble-home.d.ts +8 -0
- package/dist/bubble-home.js +19 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +82 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +144 -0
- package/dist/context/budget.d.ts +21 -0
- package/dist/context/budget.js +72 -0
- package/dist/context/compact-llm.d.ts +16 -0
- package/dist/context/compact-llm.js +132 -0
- package/dist/context/compact.d.ts +15 -0
- package/dist/context/compact.js +251 -0
- package/dist/context/overflow.d.ts +9 -0
- package/dist/context/overflow.js +46 -0
- package/dist/context/projector.d.ts +26 -0
- package/dist/context/projector.js +150 -0
- package/dist/context/prune.d.ts +9 -0
- package/dist/context/prune.js +111 -0
- package/dist/lsp/config.d.ts +18 -0
- package/dist/lsp/config.js +58 -0
- package/dist/lsp/diagnostics.d.ts +24 -0
- package/dist/lsp/diagnostics.js +103 -0
- package/dist/lsp/index.d.ts +3 -0
- package/dist/lsp/index.js +3 -0
- package/dist/lsp/service.d.ts +85 -0
- package/dist/lsp/service.js +695 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +352 -0
- package/dist/mcp/client.d.ts +68 -0
- package/dist/mcp/client.js +163 -0
- package/dist/mcp/config.d.ts +26 -0
- package/dist/mcp/config.js +127 -0
- package/dist/mcp/manager.d.ts +55 -0
- package/dist/mcp/manager.js +296 -0
- package/dist/mcp/name.d.ts +26 -0
- package/dist/mcp/name.js +40 -0
- package/dist/mcp/transports.d.ts +53 -0
- package/dist/mcp/transports.js +248 -0
- package/dist/mcp/types.d.ts +111 -0
- package/dist/mcp/types.js +14 -0
- package/dist/memory/db.d.ts +62 -0
- package/dist/memory/db.js +313 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +9 -0
- package/dist/memory/paths.d.ts +18 -0
- package/dist/memory/paths.js +38 -0
- package/dist/memory/phase1.d.ts +23 -0
- package/dist/memory/phase1.js +172 -0
- package/dist/memory/phase2.d.ts +19 -0
- package/dist/memory/phase2.js +100 -0
- package/dist/memory/prompts.d.ts +19 -0
- package/dist/memory/prompts.js +99 -0
- package/dist/memory/reset.d.ts +1 -0
- package/dist/memory/reset.js +13 -0
- package/dist/memory/start.d.ts +24 -0
- package/dist/memory/start.js +50 -0
- package/dist/memory/storage.d.ts +10 -0
- package/dist/memory/storage.js +82 -0
- package/dist/memory/store.d.ts +43 -0
- package/dist/memory/store.js +193 -0
- package/dist/memory/usage.d.ts +1 -0
- package/dist/memory/usage.js +38 -0
- package/dist/model-catalog.d.ts +20 -0
- package/dist/model-catalog.js +99 -0
- package/dist/model-config.d.ts +32 -0
- package/dist/model-config.js +59 -0
- package/dist/model-pricing.d.ts +23 -0
- package/dist/model-pricing.js +46 -0
- package/dist/oauth/index.d.ts +3 -0
- package/dist/oauth/index.js +2 -0
- package/dist/oauth/openai-codex.d.ts +9 -0
- package/dist/oauth/openai-codex.js +173 -0
- package/dist/oauth/storage.d.ts +18 -0
- package/dist/oauth/storage.js +60 -0
- package/dist/oauth/types.d.ts +15 -0
- package/dist/oauth/types.js +1 -0
- package/dist/orchestrator/default-hooks.d.ts +2 -0
- package/dist/orchestrator/default-hooks.js +96 -0
- package/dist/orchestrator/hooks.d.ts +78 -0
- package/dist/orchestrator/hooks.js +52 -0
- package/dist/orchestrator/workflow.d.ts +10 -0
- package/dist/orchestrator/workflow.js +22 -0
- package/dist/permission/mode.d.ts +23 -0
- package/dist/permission/mode.js +20 -0
- package/dist/permissions/rule.d.ts +39 -0
- package/dist/permissions/rule.js +234 -0
- package/dist/permissions/settings.d.ts +71 -0
- package/dist/permissions/settings.js +202 -0
- package/dist/permissions/types.d.ts +61 -0
- package/dist/permissions/types.js +14 -0
- package/dist/prompt/compose.d.ts +12 -0
- package/dist/prompt/compose.js +67 -0
- package/dist/prompt/environment.d.ts +12 -0
- package/dist/prompt/environment.js +38 -0
- package/dist/prompt/provider-prompts/anthropic.d.ts +1 -0
- package/dist/prompt/provider-prompts/anthropic.js +5 -0
- package/dist/prompt/provider-prompts/codex.d.ts +1 -0
- package/dist/prompt/provider-prompts/codex.js +5 -0
- package/dist/prompt/provider-prompts/default.d.ts +1 -0
- package/dist/prompt/provider-prompts/default.js +6 -0
- package/dist/prompt/provider-prompts/gemini.d.ts +1 -0
- package/dist/prompt/provider-prompts/gemini.js +5 -0
- package/dist/prompt/provider-prompts/gpt.d.ts +1 -0
- package/dist/prompt/provider-prompts/gpt.js +5 -0
- package/dist/prompt/reminders.d.ts +30 -0
- package/dist/prompt/reminders.js +164 -0
- package/dist/prompt/runtime.d.ts +12 -0
- package/dist/prompt/runtime.js +31 -0
- package/dist/prompt/skills.d.ts +2 -0
- package/dist/prompt/skills.js +4 -0
- package/dist/provider-openai-codex.d.ts +14 -0
- package/dist/provider-openai-codex.js +409 -0
- package/dist/provider-registry.d.ts +56 -0
- package/dist/provider-registry.js +244 -0
- package/dist/provider-transform.d.ts +10 -0
- package/dist/provider-transform.js +69 -0
- package/dist/provider.d.ts +31 -0
- package/dist/provider.js +269 -0
- package/dist/question/controller.d.ts +22 -0
- package/dist/question/controller.js +97 -0
- package/dist/question/index.d.ts +2 -0
- package/dist/question/index.js +2 -0
- package/dist/question/types.d.ts +42 -0
- package/dist/question/types.js +6 -0
- package/dist/session-log.d.ts +16 -0
- package/dist/session-log.js +267 -0
- package/dist/session-types.d.ts +55 -0
- package/dist/session-types.js +1 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.js +135 -0
- package/dist/skills/discovery.d.ts +12 -0
- package/dist/skills/discovery.js +148 -0
- package/dist/skills/format.d.ts +2 -0
- package/dist/skills/format.js +47 -0
- package/dist/skills/frontmatter.d.ts +5 -0
- package/dist/skills/frontmatter.js +60 -0
- package/dist/skills/invocation.d.ts +8 -0
- package/dist/skills/invocation.js +51 -0
- package/dist/skills/registry.d.ts +17 -0
- package/dist/skills/registry.js +42 -0
- package/dist/skills/types.d.ts +32 -0
- package/dist/skills/types.js +1 -0
- package/dist/slash-commands/commands.d.ts +7 -0
- package/dist/slash-commands/commands.js +779 -0
- package/dist/slash-commands/index.d.ts +4 -0
- package/dist/slash-commands/index.js +8 -0
- package/dist/slash-commands/registry.d.ts +31 -0
- package/dist/slash-commands/registry.js +70 -0
- package/dist/slash-commands/types.d.ts +44 -0
- package/dist/slash-commands/types.js +1 -0
- package/dist/slash-commands/unified.d.ts +38 -0
- package/dist/slash-commands/unified.js +38 -0
- package/dist/system-prompt.d.ts +34 -0
- package/dist/system-prompt.js +7 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.js +135 -0
- package/dist/tools/edit.d.ts +16 -0
- package/dist/tools/edit.js +95 -0
- package/dist/tools/exa-mcp.d.ts +3 -0
- package/dist/tools/exa-mcp.js +74 -0
- package/dist/tools/exit-plan-mode.d.ts +17 -0
- package/dist/tools/exit-plan-mode.js +68 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +129 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +111 -0
- package/dist/tools/index.d.ts +36 -0
- package/dist/tools/index.js +59 -0
- package/dist/tools/lsp.d.ts +4 -0
- package/dist/tools/lsp.js +92 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.js +90 -0
- package/dist/tools/question.d.ts +3 -0
- package/dist/tools/question.js +174 -0
- package/dist/tools/read.d.ts +7 -0
- package/dist/tools/read.js +83 -0
- package/dist/tools/sensitive-paths.d.ts +3 -0
- package/dist/tools/sensitive-paths.js +24 -0
- package/dist/tools/skill.d.ts +5 -0
- package/dist/tools/skill.js +51 -0
- package/dist/tools/task.d.ts +2 -0
- package/dist/tools/task.js +57 -0
- package/dist/tools/todo.d.ts +12 -0
- package/dist/tools/todo.js +151 -0
- package/dist/tools/tool-search.d.ts +23 -0
- package/dist/tools/tool-search.js +124 -0
- package/dist/tools/web-fetch.d.ts +6 -0
- package/dist/tools/web-fetch.js +75 -0
- package/dist/tools/web-search.d.ts +5 -0
- package/dist/tools/web-search.js +49 -0
- package/dist/tools/write.d.ts +11 -0
- package/dist/tools/write.js +77 -0
- package/dist/tui/display-history.d.ts +35 -0
- package/dist/tui/display-history.js +243 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/image-paste.d.ts +54 -0
- package/dist/tui/image-paste.js +288 -0
- package/dist/tui/markdown-theme-rules.d.ts +23 -0
- package/dist/tui/markdown-theme-rules.js +164 -0
- package/dist/tui/markdown-theme.d.ts +5 -0
- package/dist/tui/markdown-theme.js +27 -0
- package/dist/tui/opencode-spinner.d.ts +21 -0
- package/dist/tui/opencode-spinner.js +216 -0
- package/dist/tui/prompt-keybindings.d.ts +41 -0
- package/dist/tui/prompt-keybindings.js +28 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/run.d.ts +39 -0
- package/dist/tui/run.js +5696 -0
- package/dist/tui/sidebar-mcp.d.ts +31 -0
- package/dist/tui/sidebar-mcp.js +62 -0
- package/dist/tui/sidebar-state.d.ts +12 -0
- package/dist/tui/sidebar-state.js +69 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.js +4 -0
- package/dist/variant/thinking-level.d.ts +5 -0
- package/dist/variant/thinking-level.js +25 -0
- package/dist/variant/variant-resolver.d.ts +4 -0
- package/dist/variant/variant-resolver.js +12 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Bubble
|
|
2
|
+
|
|
3
|
+
Bubble is a terminal coding agent for working inside local project folders. It can read and edit files, run commands with approval controls, use project skills, connect MCP tools, and keep persistent memory across sessions.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Bun
|
|
8
|
+
- Node.js and npm
|
|
9
|
+
|
|
10
|
+
Install Bun if it is not already available:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
curl -fsSL https://bun.sh/install | bash
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
From npm:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @bubblebrain-ai/bubble
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
From a local package tarball:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g ./bubblebrain-ai-bubble-0.0.1.tgz
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Start Bubble in the current directory:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bubble
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Start Bubble for a specific project:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bubble --cwd /path/to/project
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Show CLI options:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bubble --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Bubble stores user configuration, sessions, permissions, skills, and memory under:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
~/.bubble
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
In the app, use `/login` or provider commands to configure model access.
|
|
59
|
+
|
|
60
|
+
## Memory
|
|
61
|
+
|
|
62
|
+
Bubble maintains persistent memory automatically from prior sessions. Useful commands:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
/memory status
|
|
66
|
+
/memory search <query>
|
|
67
|
+
/memory refresh
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Memory is maintained by a background pipeline and can be refreshed manually when you want new session information to be indexed immediately.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ParsedToolCall, ToolResult } from "../types.js";
|
|
2
|
+
export interface InvestigationCoverage {
|
|
3
|
+
configLoadPaths: boolean;
|
|
4
|
+
envReads: boolean;
|
|
5
|
+
persistencePaths: boolean;
|
|
6
|
+
exposurePaths: boolean;
|
|
7
|
+
maskingSignals: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class EvidenceTracker {
|
|
10
|
+
private coverage;
|
|
11
|
+
observe(toolCall: Pick<ParsedToolCall, "name" | "parsedArgs">, result: ToolResult): void;
|
|
12
|
+
snapshot(): InvestigationCoverage;
|
|
13
|
+
key(): string;
|
|
14
|
+
isCoreCoverageComplete(): boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const DEFAULT_COVERAGE = {
|
|
2
|
+
configLoadPaths: false,
|
|
3
|
+
envReads: false,
|
|
4
|
+
persistencePaths: false,
|
|
5
|
+
exposurePaths: false,
|
|
6
|
+
maskingSignals: false,
|
|
7
|
+
};
|
|
8
|
+
const CONFIG_PATTERNS = [
|
|
9
|
+
/\bconfig\.(json|ya?ml|toml|ini)\b/i,
|
|
10
|
+
/\bsettings\.(json|ya?ml|toml)\b/i,
|
|
11
|
+
/\bauth\.json\b/i,
|
|
12
|
+
/\buserconfig\b/i,
|
|
13
|
+
/\breadfilesync\b/i,
|
|
14
|
+
];
|
|
15
|
+
const ENV_PATTERNS = [
|
|
16
|
+
/\bprocess\.env\b/i,
|
|
17
|
+
/\bimport\.meta\.env\b/i,
|
|
18
|
+
/\bgetenv\(/i,
|
|
19
|
+
/\bloadenv\b/i,
|
|
20
|
+
/\bdotenv\b/i,
|
|
21
|
+
/\b\.env\b/i,
|
|
22
|
+
];
|
|
23
|
+
const PERSISTENCE_PATTERNS = [
|
|
24
|
+
/\bwritefilesync\b/i,
|
|
25
|
+
/\bwritefile\b/i,
|
|
26
|
+
/\bauth\.json\b/i,
|
|
27
|
+
/\bconfig\.json\b/i,
|
|
28
|
+
/\blocalstorage\b/i,
|
|
29
|
+
/\bsqlite\b/i,
|
|
30
|
+
/\bkeychain\b/i,
|
|
31
|
+
/\bcredential\b/i,
|
|
32
|
+
/\bstore\b/i,
|
|
33
|
+
];
|
|
34
|
+
const EXPOSURE_PATTERNS = [
|
|
35
|
+
/\bconsole\.log\b/i,
|
|
36
|
+
/\blogger\./i,
|
|
37
|
+
/\bres\.json\b/i,
|
|
38
|
+
/\breturn\s+\{/i,
|
|
39
|
+
/\bwindow\./i,
|
|
40
|
+
/\bdocument\./i,
|
|
41
|
+
/\bserialize\b/i,
|
|
42
|
+
/\bresponse\b/i,
|
|
43
|
+
];
|
|
44
|
+
const MASKING_PATTERNS = [
|
|
45
|
+
/\bmask(key|ed)?\b/i,
|
|
46
|
+
/\bredact(ed|ion)?\b/i,
|
|
47
|
+
/\bhidden\b/i,
|
|
48
|
+
/\bobfuscated\b/i,
|
|
49
|
+
/\*\*\*\*/i,
|
|
50
|
+
/\.\.\./i,
|
|
51
|
+
];
|
|
52
|
+
export class EvidenceTracker {
|
|
53
|
+
coverage = { ...DEFAULT_COVERAGE };
|
|
54
|
+
observe(toolCall, result) {
|
|
55
|
+
const haystack = [
|
|
56
|
+
typeof toolCall.parsedArgs.path === "string" ? toolCall.parsedArgs.path : "",
|
|
57
|
+
typeof toolCall.parsedArgs.command === "string" ? toolCall.parsedArgs.command : "",
|
|
58
|
+
typeof toolCall.parsedArgs.pattern === "string" ? toolCall.parsedArgs.pattern : "",
|
|
59
|
+
result.content,
|
|
60
|
+
].join("\n");
|
|
61
|
+
if (!this.coverage.configLoadPaths && matchesAny(CONFIG_PATTERNS, haystack)) {
|
|
62
|
+
this.coverage.configLoadPaths = true;
|
|
63
|
+
}
|
|
64
|
+
if (!this.coverage.envReads && matchesAny(ENV_PATTERNS, haystack)) {
|
|
65
|
+
this.coverage.envReads = true;
|
|
66
|
+
}
|
|
67
|
+
if (!this.coverage.persistencePaths && matchesAny(PERSISTENCE_PATTERNS, haystack)) {
|
|
68
|
+
this.coverage.persistencePaths = true;
|
|
69
|
+
}
|
|
70
|
+
if (!this.coverage.exposurePaths && matchesAny(EXPOSURE_PATTERNS, haystack)) {
|
|
71
|
+
this.coverage.exposurePaths = true;
|
|
72
|
+
}
|
|
73
|
+
if (!this.coverage.maskingSignals && matchesAny(MASKING_PATTERNS, haystack)) {
|
|
74
|
+
this.coverage.maskingSignals = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
snapshot() {
|
|
78
|
+
return { ...this.coverage };
|
|
79
|
+
}
|
|
80
|
+
key() {
|
|
81
|
+
const coverage = this.snapshot();
|
|
82
|
+
return Object.values(coverage).map((value) => (value ? "1" : "0")).join("");
|
|
83
|
+
}
|
|
84
|
+
isCoreCoverageComplete() {
|
|
85
|
+
return this.coverage.configLoadPaths
|
|
86
|
+
&& this.coverage.envReads
|
|
87
|
+
&& this.coverage.persistencePaths
|
|
88
|
+
&& this.coverage.exposurePaths;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function matchesAny(patterns, haystack) {
|
|
92
|
+
return patterns.some((pattern) => pattern.test(haystack));
|
|
93
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ParsedToolCall, ToolRegistryEntry, ToolResult } from "../types.js";
|
|
2
|
+
import type { TaskType } from "./task-classifier.js";
|
|
3
|
+
export interface GovernorDecision {
|
|
4
|
+
blockedResult?: ToolResult;
|
|
5
|
+
}
|
|
6
|
+
export declare class ExecutionGovernor {
|
|
7
|
+
private taskType;
|
|
8
|
+
private budget;
|
|
9
|
+
private history;
|
|
10
|
+
private totalSteps;
|
|
11
|
+
private searchSteps;
|
|
12
|
+
private searchFrozen;
|
|
13
|
+
private reminderQueue;
|
|
14
|
+
private warnedFamilies;
|
|
15
|
+
private softTotalWarned;
|
|
16
|
+
private softSearchWarned;
|
|
17
|
+
constructor(taskType: TaskType);
|
|
18
|
+
consumePendingReminders(): string[];
|
|
19
|
+
snapshot(): {
|
|
20
|
+
totalSteps: number;
|
|
21
|
+
searchSteps: number;
|
|
22
|
+
searchFrozen: boolean;
|
|
23
|
+
};
|
|
24
|
+
filterToolDefinitions(toolDefinitions: ToolRegistryEntry[]): ToolRegistryEntry[];
|
|
25
|
+
beforeToolCall(toolCall: ParsedToolCall): GovernorDecision;
|
|
26
|
+
afterToolResult(toolCall: ParsedToolCall, result: ToolResult): void;
|
|
27
|
+
private trailingNoProgressCount;
|
|
28
|
+
private freezeSearch;
|
|
29
|
+
private maybeWarnOnSoftBudgets;
|
|
30
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { analyzeToolIntent } from "./tool-intent.js";
|
|
2
|
+
import { buildInvestigationReminder, buildLoopWarningReminder, buildSearchFreezeReminder } from "../prompt/reminders.js";
|
|
3
|
+
const BUDGETS = {
|
|
4
|
+
security_investigation: {
|
|
5
|
+
softTotalSteps: 14,
|
|
6
|
+
softSearchSteps: 6,
|
|
7
|
+
maxNoProgressExactRepeats: 2,
|
|
8
|
+
maxNoProgressFamilyRepeats: 3,
|
|
9
|
+
warningFamilyRepeats: 2,
|
|
10
|
+
},
|
|
11
|
+
code_search: {
|
|
12
|
+
softTotalSteps: 16,
|
|
13
|
+
softSearchSteps: 8,
|
|
14
|
+
maxNoProgressExactRepeats: 3,
|
|
15
|
+
maxNoProgressFamilyRepeats: 4,
|
|
16
|
+
warningFamilyRepeats: 3,
|
|
17
|
+
},
|
|
18
|
+
general: {
|
|
19
|
+
softTotalSteps: 18,
|
|
20
|
+
softSearchSteps: 8,
|
|
21
|
+
maxNoProgressExactRepeats: 3,
|
|
22
|
+
maxNoProgressFamilyRepeats: 4,
|
|
23
|
+
warningFamilyRepeats: 3,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const SEARCH_TOOLS_DISABLED = new Set(["grep", "web_search", "web_fetch"]);
|
|
27
|
+
export class ExecutionGovernor {
|
|
28
|
+
taskType;
|
|
29
|
+
budget;
|
|
30
|
+
history = [];
|
|
31
|
+
totalSteps = 0;
|
|
32
|
+
searchSteps = 0;
|
|
33
|
+
searchFrozen = false;
|
|
34
|
+
reminderQueue = [];
|
|
35
|
+
warnedFamilies = new Set();
|
|
36
|
+
softTotalWarned = false;
|
|
37
|
+
softSearchWarned = false;
|
|
38
|
+
constructor(taskType) {
|
|
39
|
+
this.taskType = taskType;
|
|
40
|
+
this.budget = BUDGETS[taskType];
|
|
41
|
+
if (taskType === "security_investigation") {
|
|
42
|
+
this.reminderQueue.push(buildInvestigationReminder());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
consumePendingReminders() {
|
|
46
|
+
const reminders = [...this.reminderQueue];
|
|
47
|
+
this.reminderQueue.length = 0;
|
|
48
|
+
return reminders;
|
|
49
|
+
}
|
|
50
|
+
snapshot() {
|
|
51
|
+
return {
|
|
52
|
+
totalSteps: this.totalSteps,
|
|
53
|
+
searchSteps: this.searchSteps,
|
|
54
|
+
searchFrozen: this.searchFrozen,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
filterToolDefinitions(toolDefinitions) {
|
|
58
|
+
if (!this.searchFrozen) {
|
|
59
|
+
return toolDefinitions;
|
|
60
|
+
}
|
|
61
|
+
return toolDefinitions.filter((tool) => !SEARCH_TOOLS_DISABLED.has(tool.name));
|
|
62
|
+
}
|
|
63
|
+
beforeToolCall(toolCall) {
|
|
64
|
+
const intent = analyzeToolIntent(toolCall);
|
|
65
|
+
if (intent.family === "search") {
|
|
66
|
+
if (this.searchFrozen) {
|
|
67
|
+
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."),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const signature = intent.search?.signature;
|
|
72
|
+
const familyKey = intent.search?.familyKey;
|
|
73
|
+
if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.maxNoProgressExactRepeats) {
|
|
74
|
+
this.freezeSearch(`Repeated the same search signature without new evidence: ${signature}`);
|
|
75
|
+
return {
|
|
76
|
+
blockedResult: blockedResult("Search blocked: repeated the same search multiple times without new evidence.", "blocked", "Repeated identical search without progress."),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (familyKey) {
|
|
80
|
+
const familyNoProgress = this.trailingNoProgressCount((entry) => entry.familyKey === familyKey);
|
|
81
|
+
if (familyNoProgress >= this.budget.maxNoProgressFamilyRepeats) {
|
|
82
|
+
this.freezeSearch(`Repeated the same search family without new evidence: ${familyKey}`);
|
|
83
|
+
return {
|
|
84
|
+
blockedResult: blockedResult("Search blocked: repeated the same search family without new evidence.", "blocked", "Repeated similar searches without progress."),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (familyNoProgress >= this.budget.warningFamilyRepeats && !this.warnedFamilies.has(familyKey)) {
|
|
88
|
+
this.warnedFamilies.add(familyKey);
|
|
89
|
+
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."));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.totalSteps += 1;
|
|
94
|
+
if (intent.family === "search") {
|
|
95
|
+
this.searchSteps += 1;
|
|
96
|
+
}
|
|
97
|
+
this.maybeWarnOnSoftBudgets(intent.family === "search");
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
afterToolResult(toolCall, result) {
|
|
101
|
+
const intent = analyzeToolIntent(toolCall);
|
|
102
|
+
const progress = inferProgress(intent, result);
|
|
103
|
+
this.history.push({
|
|
104
|
+
family: intent.family,
|
|
105
|
+
signature: intent.search?.signature,
|
|
106
|
+
familyKey: intent.search?.familyKey,
|
|
107
|
+
progress,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
trailingNoProgressCount(predicate) {
|
|
111
|
+
let count = 0;
|
|
112
|
+
for (let index = this.history.length - 1; index >= 0; index--) {
|
|
113
|
+
const entry = this.history[index];
|
|
114
|
+
if (!predicate(entry)) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
if (entry.progress) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
count += 1;
|
|
121
|
+
}
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
freezeSearch(reason) {
|
|
125
|
+
if (this.searchFrozen) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.searchFrozen = true;
|
|
129
|
+
this.reminderQueue.push(buildSearchFreezeReminder(reason));
|
|
130
|
+
}
|
|
131
|
+
maybeWarnOnSoftBudgets(isSearchStep) {
|
|
132
|
+
if (!this.softTotalWarned && this.totalSteps >= this.budget.softTotalSteps) {
|
|
133
|
+
this.softTotalWarned = true;
|
|
134
|
+
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."));
|
|
135
|
+
}
|
|
136
|
+
if (isSearchStep && !this.softSearchWarned && this.searchSteps >= this.budget.softSearchSteps) {
|
|
137
|
+
this.softSearchWarned = true;
|
|
138
|
+
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
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function inferProgress(intent, result) {
|
|
143
|
+
if (result.status === "blocked" || result.status === "timeout" || result.status === "command_error") {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (intent.family === "search") {
|
|
147
|
+
const matches = result.metadata?.matches;
|
|
148
|
+
if (typeof matches === "number") {
|
|
149
|
+
return matches > 0;
|
|
150
|
+
}
|
|
151
|
+
const normalized = result.content.toLowerCase();
|
|
152
|
+
if (normalized.includes("no matches found") || normalized.includes("(no matches)")) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return !result.isError;
|
|
156
|
+
}
|
|
157
|
+
return !result.isError;
|
|
158
|
+
}
|
|
159
|
+
function blockedResult(content, status, reason) {
|
|
160
|
+
return {
|
|
161
|
+
content,
|
|
162
|
+
isError: true,
|
|
163
|
+
status,
|
|
164
|
+
metadata: {
|
|
165
|
+
kind: "security",
|
|
166
|
+
reason,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ToolRegistryEntry, ToolResultStatus } from "../types.js";
|
|
2
|
+
export type SubtaskType = "search" | "security_investigation" | "evidence_correlation" | "general_readonly";
|
|
3
|
+
export interface SubtaskPolicy {
|
|
4
|
+
type: SubtaskType;
|
|
5
|
+
allowedTools: string[];
|
|
6
|
+
reminder: string;
|
|
7
|
+
resultStatus: ToolResultStatus;
|
|
8
|
+
maxTurns?: number;
|
|
9
|
+
taskBudget?: {
|
|
10
|
+
total: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function getSubtaskPolicy(type: SubtaskType | undefined): SubtaskPolicy;
|
|
14
|
+
export declare function filterToolsForSubtask(tools: ToolRegistryEntry[], type: SubtaskType | undefined): ToolRegistryEntry[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const POLICY_MAP = {
|
|
2
|
+
search: {
|
|
3
|
+
type: "search",
|
|
4
|
+
allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill", "todo_write"],
|
|
5
|
+
reminder: [
|
|
6
|
+
"Subtask policy: search",
|
|
7
|
+
"- Focus on locating relevant files, symbols, and evidence quickly.",
|
|
8
|
+
"- Use glob for file discovery and grep for content search.",
|
|
9
|
+
"- Return a concise summary of what you found and where.",
|
|
10
|
+
].join("\n"),
|
|
11
|
+
resultStatus: "success",
|
|
12
|
+
maxTurns: 6,
|
|
13
|
+
taskBudget: { total: 180_000 },
|
|
14
|
+
},
|
|
15
|
+
security_investigation: {
|
|
16
|
+
type: "security_investigation",
|
|
17
|
+
allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill", "todo_write"],
|
|
18
|
+
reminder: [
|
|
19
|
+
"Subtask policy: security_investigation",
|
|
20
|
+
"- Investigate only in read-only mode.",
|
|
21
|
+
"- Collect evidence about config load paths, environment reads, persistence paths, masking, and exposure paths.",
|
|
22
|
+
"- Do not loop on broad keyword search; summarize evidence and uncertainty.",
|
|
23
|
+
].join("\n"),
|
|
24
|
+
resultStatus: "success",
|
|
25
|
+
maxTurns: 8,
|
|
26
|
+
taskBudget: { total: 220_000 },
|
|
27
|
+
},
|
|
28
|
+
evidence_correlation: {
|
|
29
|
+
type: "evidence_correlation",
|
|
30
|
+
allowedTools: ["read", "skill", "todo_write"],
|
|
31
|
+
reminder: [
|
|
32
|
+
"Subtask policy: evidence_correlation",
|
|
33
|
+
"- Correlate evidence already discovered.",
|
|
34
|
+
"- Avoid new broad searches; read only the specific files that matter.",
|
|
35
|
+
"- Produce a reasoning-focused summary that states what the evidence supports.",
|
|
36
|
+
].join("\n"),
|
|
37
|
+
resultStatus: "success",
|
|
38
|
+
maxTurns: 4,
|
|
39
|
+
taskBudget: { total: 120_000 },
|
|
40
|
+
},
|
|
41
|
+
general_readonly: {
|
|
42
|
+
type: "general_readonly",
|
|
43
|
+
allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill", "todo_write"],
|
|
44
|
+
reminder: [
|
|
45
|
+
"Subtask policy: general_readonly",
|
|
46
|
+
"- Stay in read-only mode.",
|
|
47
|
+
"- Keep the scope tightly bounded and summarize findings concisely.",
|
|
48
|
+
].join("\n"),
|
|
49
|
+
resultStatus: "success",
|
|
50
|
+
maxTurns: 6,
|
|
51
|
+
taskBudget: { total: 180_000 },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
export function getSubtaskPolicy(type) {
|
|
55
|
+
return POLICY_MAP[type ?? "general_readonly"];
|
|
56
|
+
}
|
|
57
|
+
export function filterToolsForSubtask(tools, type) {
|
|
58
|
+
const policy = getSubtaskPolicy(type);
|
|
59
|
+
return tools.filter((tool) => policy.allowedTools.includes(tool.name));
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const SECURITY_PATTERNS = [
|
|
2
|
+
/\bapi[\s_-]?key\b/i,
|
|
3
|
+
/\bsecret(s)?\b/i,
|
|
4
|
+
/\btoken(s)?\b/i,
|
|
5
|
+
/\bcredential(s)?\b/i,
|
|
6
|
+
/\bleak(ed|age)?\b/i,
|
|
7
|
+
/\bexpos(e|ed|ure)\b/i,
|
|
8
|
+
/\bstored?\b/i,
|
|
9
|
+
/\bwhere\b/i,
|
|
10
|
+
/\bconfig\b/i,
|
|
11
|
+
/\benv\b/i,
|
|
12
|
+
];
|
|
13
|
+
const SEARCH_PATTERNS = [
|
|
14
|
+
/\bfind\b/i,
|
|
15
|
+
/\bsearch\b/i,
|
|
16
|
+
/\blook for\b/i,
|
|
17
|
+
/\bwhere\b/i,
|
|
18
|
+
/\blocate\b/i,
|
|
19
|
+
/\btrace\b/i,
|
|
20
|
+
];
|
|
21
|
+
export function classifyTask(input) {
|
|
22
|
+
const text = typeof input === "string"
|
|
23
|
+
? input
|
|
24
|
+
: input
|
|
25
|
+
.filter((part) => part.type === "text")
|
|
26
|
+
.map((part) => part.text)
|
|
27
|
+
.join("\n");
|
|
28
|
+
const securityHits = SECURITY_PATTERNS.filter((pattern) => pattern.test(text)).length;
|
|
29
|
+
if (securityHits >= 2) {
|
|
30
|
+
return "security_investigation";
|
|
31
|
+
}
|
|
32
|
+
if (SEARCH_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
33
|
+
return "code_search";
|
|
34
|
+
}
|
|
35
|
+
return "general";
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ParsedToolCall } from "../types.js";
|
|
2
|
+
export interface ToolArbitration {
|
|
3
|
+
toolCall: ParsedToolCall;
|
|
4
|
+
note?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function arbitrateToolCall(toolCall: ParsedToolCall): ToolArbitration;
|
|
7
|
+
export declare function isSearchLikeToolCall(toolCall: ParsedToolCall): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { analyzeToolIntent, parseSearchBashCommand } from "./tool-intent.js";
|
|
2
|
+
export function arbitrateToolCall(toolCall) {
|
|
3
|
+
if (toolCall.name !== "bash") {
|
|
4
|
+
return { toolCall };
|
|
5
|
+
}
|
|
6
|
+
const command = typeof toolCall.parsedArgs.command === "string"
|
|
7
|
+
? toolCall.parsedArgs.command
|
|
8
|
+
: "";
|
|
9
|
+
const parsedSearch = parseSearchBashCommand(command);
|
|
10
|
+
if (!parsedSearch) {
|
|
11
|
+
return { toolCall };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
toolCall: {
|
|
15
|
+
...toolCall,
|
|
16
|
+
name: "grep",
|
|
17
|
+
parsedArgs: {
|
|
18
|
+
pattern: parsedSearch.pattern,
|
|
19
|
+
...(parsedSearch.path ? { path: parsedSearch.path } : {}),
|
|
20
|
+
...(parsedSearch.include ? { glob: parsedSearch.include } : {}),
|
|
21
|
+
},
|
|
22
|
+
arguments: JSON.stringify({
|
|
23
|
+
pattern: parsedSearch.pattern,
|
|
24
|
+
...(parsedSearch.path ? { path: parsedSearch.path } : {}),
|
|
25
|
+
...(parsedSearch.include ? { glob: parsedSearch.include } : {}),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
note: `Rewrote bash search to grep for structured execution: ${command}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function isSearchLikeToolCall(toolCall) {
|
|
32
|
+
return analyzeToolIntent(toolCall).family === "search";
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParsedToolCall } from "../types.js";
|
|
2
|
+
export type ToolFamily = "search" | "read" | "write" | "edit" | "shell" | "web" | "other";
|
|
3
|
+
export interface SearchIntent {
|
|
4
|
+
pattern: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
include?: string;
|
|
7
|
+
signature: string;
|
|
8
|
+
familyKey: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolIntent {
|
|
11
|
+
family: ToolFamily;
|
|
12
|
+
search?: SearchIntent;
|
|
13
|
+
}
|
|
14
|
+
export declare function analyzeToolIntent(toolCall: Pick<ParsedToolCall, "name" | "parsedArgs">): ToolIntent;
|
|
15
|
+
export interface ParsedSearchCommand {
|
|
16
|
+
pattern: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
include?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseSearchBashCommand(command: string): ParsedSearchCommand | undefined;
|