@bubblebrain-ai/bubble 0.0.6 → 0.0.8
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.d.ts +6 -0
- package/dist/agent.js +36 -3
- package/dist/context/budget.d.ts +1 -0
- package/dist/context/budget.js +1 -1
- package/dist/context/usage.d.ts +34 -0
- package/dist/context/usage.js +213 -0
- package/dist/diff-stats.d.ts +5 -0
- package/dist/diff-stats.js +21 -0
- package/dist/main.js +83 -44
- package/dist/mcp/transports.d.ts +1 -0
- package/dist/mcp/transports.js +8 -0
- package/dist/model-catalog.js +1 -1
- package/dist/orchestrator/default-hooks.js +9 -33
- package/dist/prompt/compose.js +2 -1
- package/dist/prompt/provider-prompts/kimi.js +3 -1
- package/dist/prompt/reminders.d.ts +2 -1
- package/dist/prompt/reminders.js +4 -3
- package/dist/provider-registry.js +3 -3
- package/dist/provider-transform.d.ts +3 -1
- package/dist/provider-transform.js +15 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +89 -4
- package/dist/reasoning-debug.d.ts +7 -0
- package/dist/reasoning-debug.js +30 -0
- package/dist/session-log.js +13 -2
- package/dist/session-types.d.ts +1 -1
- package/dist/slash-commands/commands.js +36 -19
- package/dist/tools/edit.js +5 -0
- package/dist/tools/file-state.d.ts +19 -0
- package/dist/tools/file-state.js +15 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +92 -11
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/run.js +93 -23
- package/dist/tui-ink/app.d.ts +43 -0
- package/dist/tui-ink/app.js +1016 -0
- package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
- package/dist/tui-ink/approval/approval-dialog.js +129 -0
- package/dist/tui-ink/approval/diff-view.d.ts +7 -0
- package/dist/tui-ink/approval/diff-view.js +43 -0
- package/dist/tui-ink/approval/select.d.ts +35 -0
- package/dist/tui-ink/approval/select.js +87 -0
- package/dist/tui-ink/code-highlight.d.ts +6 -0
- package/dist/tui-ink/code-highlight.js +94 -0
- package/dist/tui-ink/display-history.d.ts +38 -0
- package/dist/tui-ink/display-history.js +130 -0
- package/dist/tui-ink/edit-diff.d.ts +11 -0
- package/dist/tui-ink/edit-diff.js +52 -0
- package/dist/tui-ink/file-mentions.d.ts +29 -0
- package/dist/tui-ink/file-mentions.js +174 -0
- package/dist/tui-ink/footer.d.ts +19 -0
- package/dist/tui-ink/footer.js +44 -0
- package/dist/tui-ink/image-paste.d.ts +54 -0
- package/dist/tui-ink/image-paste.js +288 -0
- package/dist/tui-ink/input-box.d.ts +41 -0
- package/dist/tui-ink/input-box.js +637 -0
- package/dist/tui-ink/markdown.d.ts +38 -0
- package/dist/tui-ink/markdown.js +384 -0
- package/dist/tui-ink/message-list.d.ts +33 -0
- package/dist/tui-ink/message-list.js +571 -0
- package/dist/tui-ink/model-picker.d.ts +43 -0
- package/dist/tui-ink/model-picker.js +326 -0
- package/dist/tui-ink/plan-confirm.d.ts +7 -0
- package/dist/tui-ink/plan-confirm.js +104 -0
- package/dist/tui-ink/question-dialog.d.ts +8 -0
- package/dist/tui-ink/question-dialog.js +98 -0
- package/dist/tui-ink/recent-activity.d.ts +8 -0
- package/dist/tui-ink/recent-activity.js +71 -0
- package/dist/tui-ink/run.d.ts +33 -0
- package/dist/tui-ink/run.js +25 -0
- package/dist/tui-ink/theme.d.ts +37 -0
- package/dist/tui-ink/theme.js +42 -0
- package/dist/tui-ink/todos.d.ts +7 -0
- package/dist/tui-ink/todos.js +44 -0
- package/dist/tui-ink/trace-groups.d.ts +25 -0
- package/dist/tui-ink/trace-groups.js +310 -0
- package/dist/tui-ink/use-terminal-size.d.ts +4 -0
- package/dist/tui-ink/use-terminal-size.js +21 -0
- package/dist/tui-ink/welcome.d.ts +18 -0
- package/dist/tui-ink/welcome.js +119 -0
- package/dist/types.d.ts +4 -0
- package/package.json +6 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { getModelContextWindow } from "../model-catalog.js";
|
|
2
|
+
import { formatSkillsPrompt } from "../skills/format.js";
|
|
3
|
+
import { buildDeferredToolsReminder } from "../prompt/reminders.js";
|
|
4
|
+
import { AUTOCOMPACT_BUFFER_TOKENS, estimateMessageTokens, estimateTextTokens, MIN_WINDOW_FOR_RESERVE, OUTPUT_RESERVE_TOKENS, } from "./budget.js";
|
|
5
|
+
export function buildContextUsageSnapshot(input) {
|
|
6
|
+
const systemMessages = input.messages.filter((message) => message.role === "system");
|
|
7
|
+
const otherMessages = input.messages.filter((message) => message.role !== "system");
|
|
8
|
+
const deferredToolEntries = input.deferredToolEntries ?? [];
|
|
9
|
+
const systemContent = systemMessages.map((message) => message.content).join("\n\n");
|
|
10
|
+
const skillsPrompt = formatSkillsPrompt(input.skills);
|
|
11
|
+
const skillsInSystemPrompt = !!skillsPrompt && systemContent.includes(skillsPrompt);
|
|
12
|
+
const skillsTokens = skillsInSystemPrompt ? estimateTextTokens(skillsPrompt) : 0;
|
|
13
|
+
const systemPromptTokens = Math.max(0, estimateTextTokens(systemContent) - skillsTokens);
|
|
14
|
+
const toolsTokens = estimateToolEntriesTokens(input.toolEntries);
|
|
15
|
+
const deferredToolsTokens = estimateDeferredToolsReminderTokens(deferredToolEntries);
|
|
16
|
+
const rawOtherTokens = otherMessages.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
|
|
17
|
+
const otherTokens = Math.max(0, rawOtherTokens - deferredToolsTokens);
|
|
18
|
+
const usedTokens = systemPromptTokens + toolsTokens + skillsTokens + deferredToolsTokens + otherTokens;
|
|
19
|
+
const contextWindow = getModelContextWindow(input.providerId, input.modelId);
|
|
20
|
+
return {
|
|
21
|
+
providerId: input.providerId,
|
|
22
|
+
modelId: input.modelId,
|
|
23
|
+
contextWindow,
|
|
24
|
+
usedTokens,
|
|
25
|
+
freeTokens: contextWindow === undefined ? undefined : Math.max(0, contextWindow - usedTokens),
|
|
26
|
+
buckets: {
|
|
27
|
+
systemPrompt: {
|
|
28
|
+
label: "System prompt",
|
|
29
|
+
tokens: systemPromptTokens,
|
|
30
|
+
detail: systemMessages.length > 0 ? `${systemMessages.length} system message${systemMessages.length === 1 ? "" : "s"}` : "none",
|
|
31
|
+
},
|
|
32
|
+
tools: {
|
|
33
|
+
label: "Tools",
|
|
34
|
+
tokens: toolsTokens,
|
|
35
|
+
detail: input.toolEntries.length > 0 ? `${input.toolEntries.length} active schema${input.toolEntries.length === 1 ? "" : "s"}` : "none",
|
|
36
|
+
},
|
|
37
|
+
skills: {
|
|
38
|
+
label: "Skills",
|
|
39
|
+
tokens: skillsTokens,
|
|
40
|
+
detail: skillsInSystemPrompt && input.skills.length > 0 ? `${input.skills.length} advertised skill${input.skills.length === 1 ? "" : "s"}` : "none in current prompt",
|
|
41
|
+
},
|
|
42
|
+
deferredTools: {
|
|
43
|
+
label: "Deferred/MCP",
|
|
44
|
+
tokens: deferredToolsTokens,
|
|
45
|
+
detail: deferredToolEntries.length > 0
|
|
46
|
+
? `${deferredToolEntries.length} deferred tool name${deferredToolEntries.length === 1 ? "" : "s"} in reminder`
|
|
47
|
+
: "none",
|
|
48
|
+
},
|
|
49
|
+
other: {
|
|
50
|
+
label: "Other",
|
|
51
|
+
tokens: otherTokens,
|
|
52
|
+
detail: otherMessages.length > 0 ? `${otherMessages.length} conversation/meta/tool message${otherMessages.length === 1 ? "" : "s"}` : "none",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
toolCount: input.toolEntries.length,
|
|
56
|
+
deferredToolCount: deferredToolEntries.length,
|
|
57
|
+
skillCount: skillsInSystemPrompt ? input.skills.length : 0,
|
|
58
|
+
messageCount: input.messages.length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function formatContextUsage(snapshot) {
|
|
62
|
+
const freeTokens = snapshot.freeTokens ?? 0;
|
|
63
|
+
const rows = [
|
|
64
|
+
{ key: "system", marker: "█", color: ANSI_ORANGE, bucket: snapshot.buckets.systemPrompt },
|
|
65
|
+
{ key: "tools", marker: "▓", color: ANSI_TEAL, bucket: snapshot.buckets.tools },
|
|
66
|
+
{ key: "skills", marker: "▒", color: ANSI_PURPLE, bucket: snapshot.buckets.skills },
|
|
67
|
+
{ key: "deferred", marker: "◆", color: ANSI_BLUE, bucket: snapshot.buckets.deferredTools },
|
|
68
|
+
{ key: "other", marker: "▪", color: ANSI_GRAY, bucket: snapshot.buckets.other },
|
|
69
|
+
];
|
|
70
|
+
const freeRow = {
|
|
71
|
+
key: "free",
|
|
72
|
+
marker: "░",
|
|
73
|
+
color: ANSI_DARK_GRAY,
|
|
74
|
+
bucket: {
|
|
75
|
+
label: "Free space",
|
|
76
|
+
tokens: freeTokens,
|
|
77
|
+
detail: snapshot.freeTokens === undefined ? "unknown window" : "available before context limit",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const barRows = snapshot.contextWindow === undefined ? rows : [...rows, freeRow];
|
|
81
|
+
const barTotal = snapshot.contextWindow ?? Math.max(1, snapshot.usedTokens);
|
|
82
|
+
const compactAt = snapshot.contextWindow === undefined
|
|
83
|
+
? "unknown"
|
|
84
|
+
: formatTokens(compactionThreshold(snapshot.contextWindow));
|
|
85
|
+
const lines = [
|
|
86
|
+
colorize("• Context Usage", ANSI_BOLD),
|
|
87
|
+
`${colorize(snapshot.providerId || "unknown", ANSI_TEAL)}:${snapshot.modelId || "unknown"} · ${formatUsedWindow(snapshot)} · compaction at ${compactAt}`,
|
|
88
|
+
`Free space: ${snapshot.freeTokens === undefined ? "unknown" : colorize(`${formatTokens(snapshot.freeTokens)} (${formatPercent(snapshot.freeTokens, snapshot.contextWindow)})`, ANSI_DARK_GRAY)}`,
|
|
89
|
+
"",
|
|
90
|
+
buildSegmentedBar(barRows, barTotal),
|
|
91
|
+
"",
|
|
92
|
+
colorize("Estimated usage by category", ANSI_BOLD),
|
|
93
|
+
...rows.map((row) => formatBucket(row.marker, row.bucket, snapshot.contextWindow)),
|
|
94
|
+
formatBucket(freeRow.marker, freeRow.bucket, snapshot.contextWindow),
|
|
95
|
+
"",
|
|
96
|
+
"Note: estimates include resident messages and active tool schemas; provider tokenization and hidden overhead can differ.",
|
|
97
|
+
];
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
function estimateToolEntriesTokens(entries) {
|
|
101
|
+
return entries.reduce((sum, entry) => {
|
|
102
|
+
const payload = JSON.stringify({
|
|
103
|
+
name: entry.name,
|
|
104
|
+
description: entry.description,
|
|
105
|
+
parameters: entry.parameters,
|
|
106
|
+
});
|
|
107
|
+
return sum + estimateTextTokens(payload) + 8;
|
|
108
|
+
}, 0);
|
|
109
|
+
}
|
|
110
|
+
function estimateDeferredToolsReminderTokens(entries) {
|
|
111
|
+
if (entries.length === 0)
|
|
112
|
+
return 0;
|
|
113
|
+
return estimateTextTokens(buildDeferredToolsReminder(entries.map((entry) => entry.name)));
|
|
114
|
+
}
|
|
115
|
+
function buildSegmentedBar(rows, totalTokens) {
|
|
116
|
+
const width = 54;
|
|
117
|
+
if (rows.every((row) => row.bucket.tokens <= 0)) {
|
|
118
|
+
return "░".repeat(width);
|
|
119
|
+
}
|
|
120
|
+
const safeTotal = Math.max(1, totalTokens);
|
|
121
|
+
const rawSegments = rows.map((row) => {
|
|
122
|
+
const exact = (Math.max(0, row.bucket.tokens) / safeTotal) * width;
|
|
123
|
+
const minWidth = row.marker !== "░" && row.bucket.tokens > 0 ? 1 : 0;
|
|
124
|
+
return { ...row, exact, width: minWidth };
|
|
125
|
+
});
|
|
126
|
+
let assigned = rawSegments.reduce((sum, segment) => sum + segment.width, 0);
|
|
127
|
+
while (assigned < width && rawSegments.length > 0) {
|
|
128
|
+
const segment = rawSegments.reduce((best, item) => {
|
|
129
|
+
const itemDeficit = item.exact - item.width;
|
|
130
|
+
const bestDeficit = best.exact - best.width;
|
|
131
|
+
return itemDeficit > bestDeficit ? item : best;
|
|
132
|
+
}, rawSegments[0]);
|
|
133
|
+
segment.width += 1;
|
|
134
|
+
assigned += 1;
|
|
135
|
+
}
|
|
136
|
+
while (assigned > width) {
|
|
137
|
+
const segment = rawSegments
|
|
138
|
+
.filter((item) => item.width > 0)
|
|
139
|
+
.sort((a, b) => b.width - a.width)[0];
|
|
140
|
+
if (!segment)
|
|
141
|
+
break;
|
|
142
|
+
segment.width -= 1;
|
|
143
|
+
assigned -= 1;
|
|
144
|
+
}
|
|
145
|
+
return rawSegments.map((segment) => colorize(segment.marker.repeat(segment.width), segment.color)).join("");
|
|
146
|
+
}
|
|
147
|
+
function formatBucket(marker, bucket, contextWindow) {
|
|
148
|
+
const label = bucket.label.padEnd(13, " ");
|
|
149
|
+
const count = contextWindow === undefined && bucket.label === "Free space"
|
|
150
|
+
? "unknown".padStart(14, " ")
|
|
151
|
+
: formatTokens(bucket.tokens).padStart(14, " ");
|
|
152
|
+
const percent = contextWindow === undefined ? "" : ` ${formatPercent(bucket.tokens, contextWindow).padStart(7, " ")}`;
|
|
153
|
+
const color = colorForLabel(bucket.label);
|
|
154
|
+
return `${colorize(marker, color)} ${colorize(label, color)} ${count}${percent} ${bucket.detail ?? "unknown"}`;
|
|
155
|
+
}
|
|
156
|
+
function formatPercent(tokens, contextWindow) {
|
|
157
|
+
if (!contextWindow || contextWindow <= 0)
|
|
158
|
+
return "";
|
|
159
|
+
const percent = (tokens / contextWindow) * 100;
|
|
160
|
+
if (percent > 0 && percent < 0.1)
|
|
161
|
+
return "<0.1%";
|
|
162
|
+
return `${percent.toFixed(1)}%`;
|
|
163
|
+
}
|
|
164
|
+
function formatUsedWindow(snapshot) {
|
|
165
|
+
if (snapshot.contextWindow === undefined)
|
|
166
|
+
return `~${formatTokens(snapshot.usedTokens)} used`;
|
|
167
|
+
return `${formatTokenNumber(snapshot.usedTokens)}/${formatTokenNumber(snapshot.contextWindow)} tokens (${formatPercent(snapshot.usedTokens, snapshot.contextWindow)})`;
|
|
168
|
+
}
|
|
169
|
+
function compactionThreshold(contextWindow) {
|
|
170
|
+
if (contextWindow >= MIN_WINDOW_FOR_RESERVE) {
|
|
171
|
+
return Math.max(0, contextWindow - OUTPUT_RESERVE_TOKENS - AUTOCOMPACT_BUFFER_TOKENS);
|
|
172
|
+
}
|
|
173
|
+
return Math.floor(contextWindow * 0.75);
|
|
174
|
+
}
|
|
175
|
+
function formatTokens(count) {
|
|
176
|
+
return `${formatTokenNumber(count)} tokens`;
|
|
177
|
+
}
|
|
178
|
+
function formatTokenNumber(count) {
|
|
179
|
+
if (count < 1000)
|
|
180
|
+
return `${Math.round(count)}`;
|
|
181
|
+
if (count < 1_000_000)
|
|
182
|
+
return `${formatFixed(count / 1000)}K`;
|
|
183
|
+
return `${formatFixed(count / 1_000_000)}M`;
|
|
184
|
+
}
|
|
185
|
+
function formatFixed(value) {
|
|
186
|
+
return value >= 10 ? value.toFixed(0) : value.toFixed(1);
|
|
187
|
+
}
|
|
188
|
+
const ANSI_RESET = "\u001b[0m";
|
|
189
|
+
const ANSI_BOLD = "\u001b[1m";
|
|
190
|
+
const ANSI_ORANGE = "\u001b[38;5;208m";
|
|
191
|
+
const ANSI_TEAL = "\u001b[38;5;73m";
|
|
192
|
+
const ANSI_PURPLE = "\u001b[38;5;141m";
|
|
193
|
+
const ANSI_BLUE = "\u001b[38;5;75m";
|
|
194
|
+
const ANSI_GRAY = "\u001b[38;5;245m";
|
|
195
|
+
const ANSI_DARK_GRAY = "\u001b[38;5;240m";
|
|
196
|
+
function colorize(text, color) {
|
|
197
|
+
if (!text)
|
|
198
|
+
return text;
|
|
199
|
+
return `${color}${text}${ANSI_RESET}`;
|
|
200
|
+
}
|
|
201
|
+
function colorForLabel(label) {
|
|
202
|
+
if (label === "System prompt")
|
|
203
|
+
return ANSI_ORANGE;
|
|
204
|
+
if (label === "Tools")
|
|
205
|
+
return ANSI_TEAL;
|
|
206
|
+
if (label === "Skills")
|
|
207
|
+
return ANSI_PURPLE;
|
|
208
|
+
if (label === "Deferred/MCP")
|
|
209
|
+
return ANSI_BLUE;
|
|
210
|
+
if (label === "Free space")
|
|
211
|
+
return ANSI_DARK_GRAY;
|
|
212
|
+
return ANSI_GRAY;
|
|
213
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function countUnifiedDiffChanges(diff) {
|
|
2
|
+
let added = 0;
|
|
3
|
+
let removed = 0;
|
|
4
|
+
for (const line of diff.replace(/\r\n/g, "\n").split("\n")) {
|
|
5
|
+
if (isUnifiedDiffMetadataLine(line))
|
|
6
|
+
continue;
|
|
7
|
+
if (line.startsWith("+"))
|
|
8
|
+
added++;
|
|
9
|
+
else if (line.startsWith("-"))
|
|
10
|
+
removed++;
|
|
11
|
+
}
|
|
12
|
+
return { added, removed };
|
|
13
|
+
}
|
|
14
|
+
function isUnifiedDiffMetadataLine(line) {
|
|
15
|
+
return (line.startsWith("+++") ||
|
|
16
|
+
line.startsWith("---") ||
|
|
17
|
+
line.startsWith("@@") ||
|
|
18
|
+
line.startsWith("Index:") ||
|
|
19
|
+
line.startsWith("===") ||
|
|
20
|
+
line.startsWith("\\ No newline"));
|
|
21
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -14,6 +14,7 @@ import { SessionManager } from "./session.js";
|
|
|
14
14
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
15
15
|
import { SkillRegistry } from "./skills/registry.js";
|
|
16
16
|
import { createAllTools } from "./tools/index.js";
|
|
17
|
+
import { FileStateTracker } from "./tools/file-state.js";
|
|
17
18
|
import { PermissionAwareApprovalController } from "./approval/controller.js";
|
|
18
19
|
import { BashAllowlist } from "./approval/session-cache.js";
|
|
19
20
|
import { SettingsManager } from "./permissions/settings.js";
|
|
@@ -92,6 +93,7 @@ async function main() {
|
|
|
92
93
|
unlock: (names) => agentRef?.unlockDeferredTools(names),
|
|
93
94
|
};
|
|
94
95
|
const lspService = getLspService(args.cwd, settingsManager.getMerged().lsp);
|
|
96
|
+
const fileStateTracker = new FileStateTracker(args.cwd);
|
|
95
97
|
const tools = createAllTools(args.cwd, skillRegistry, {
|
|
96
98
|
todoStore,
|
|
97
99
|
planController,
|
|
@@ -99,6 +101,7 @@ async function main() {
|
|
|
99
101
|
questionController: printMode ? undefined : questionController,
|
|
100
102
|
toolSearchController,
|
|
101
103
|
lspService,
|
|
104
|
+
fileStateTracker,
|
|
102
105
|
});
|
|
103
106
|
// Bring up MCP servers (if any). Failures are captured per-server and never
|
|
104
107
|
// block the rest of startup; /mcp surfaces status at runtime.
|
|
@@ -125,9 +128,8 @@ async function main() {
|
|
|
125
128
|
const { registry: slashRegistry } = await import("./slash-commands/index.js");
|
|
126
129
|
slashRegistry.addDynamicSource(() => mcpManager.getPromptCommands());
|
|
127
130
|
}
|
|
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.
|
|
131
|
+
// Signal-based shutdown for Ctrl-C / kill. Normal /quit cleanup happens after
|
|
132
|
+
// the TUI renderer has been destroyed, avoiding native teardown races.
|
|
131
133
|
const shutdownMcp = async () => {
|
|
132
134
|
try {
|
|
133
135
|
await mcpManager.shutdown();
|
|
@@ -247,6 +249,7 @@ async function main() {
|
|
|
247
249
|
budgetLedger,
|
|
248
250
|
skills: skillSummaries,
|
|
249
251
|
memoryPrompt,
|
|
252
|
+
fileStateTracker,
|
|
250
253
|
});
|
|
251
254
|
agentRef = agent;
|
|
252
255
|
if (sessionManager) {
|
|
@@ -261,6 +264,18 @@ async function main() {
|
|
|
261
264
|
// Codex-style memory runs at startup over historical rollouts. Exit should
|
|
262
265
|
// not perform an ad-hoc extraction of the just-finished session.
|
|
263
266
|
};
|
|
267
|
+
const shutdownRuntime = async () => {
|
|
268
|
+
const results = await Promise.allSettled([
|
|
269
|
+
flushMemory(),
|
|
270
|
+
shutdownMcp(),
|
|
271
|
+
lspService.shutdown(),
|
|
272
|
+
]);
|
|
273
|
+
for (const result of results) {
|
|
274
|
+
if (result.status === "rejected") {
|
|
275
|
+
// Shutdown is best-effort; never turn exit into a fatal error.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
264
279
|
const runMemoryCompaction = async () => formatMemoryStartupResult(await runMemoryStartupPipeline({
|
|
265
280
|
cwd: args.cwd,
|
|
266
281
|
complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
|
|
@@ -296,49 +311,55 @@ async function main() {
|
|
|
296
311
|
console.log(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
|
|
297
312
|
}
|
|
298
313
|
}
|
|
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}]`));
|
|
314
|
+
try {
|
|
315
|
+
// Print mode: single prompt, then exit
|
|
316
|
+
if (args.print || args.prompt) {
|
|
317
|
+
const prompt = args.prompt || (await readPipedStdin()) || "";
|
|
318
|
+
if (!prompt) {
|
|
319
|
+
console.error(chalk.red("Error: No prompt provided."));
|
|
320
|
+
process.exit(1);
|
|
312
321
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
for await (const event of agent.run(prompt, args.cwd)) {
|
|
323
|
+
if (event.type === "text_delta") {
|
|
324
|
+
process.stdout.write(event.content);
|
|
325
|
+
}
|
|
326
|
+
else if (event.type === "tool_start") {
|
|
327
|
+
console.log(chalk.cyan(`\n[Tool: ${event.name}]`));
|
|
328
|
+
}
|
|
329
|
+
else if (event.type === "tool_end") {
|
|
330
|
+
const color = event.result.isError ? chalk.red : chalk.dim;
|
|
331
|
+
console.log(color(`[Result: ${event.result.content.slice(0, 200)}${event.result.content.length > 200 ? "..." : ""}]`));
|
|
332
|
+
}
|
|
316
333
|
}
|
|
334
|
+
console.log();
|
|
335
|
+
return;
|
|
317
336
|
}
|
|
318
|
-
|
|
319
|
-
|
|
337
|
+
const tuiRuntime = process.env.BUBBLE_TUI === "opentui" ? "opentui" : "ink";
|
|
338
|
+
const { runTui } = tuiRuntime === "opentui"
|
|
339
|
+
? await import("./tui/run.js")
|
|
340
|
+
: await import("./tui-ink/run.js");
|
|
341
|
+
await runTui(agent, args, {
|
|
342
|
+
sessionManager,
|
|
343
|
+
createProvider,
|
|
344
|
+
registry,
|
|
345
|
+
skillRegistry,
|
|
346
|
+
planHandlerRef,
|
|
347
|
+
approvalHandlerRef,
|
|
348
|
+
questionController,
|
|
349
|
+
bashAllowlist,
|
|
350
|
+
settingsManager,
|
|
351
|
+
lspService,
|
|
352
|
+
mcpManager,
|
|
353
|
+
theme: userConfig.getTheme(),
|
|
354
|
+
flushMemory,
|
|
355
|
+
runMemoryCompaction,
|
|
356
|
+
runMemorySummary,
|
|
357
|
+
runMemoryRefresh,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
await shutdownRuntime();
|
|
320
362
|
}
|
|
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
363
|
}
|
|
343
364
|
async function readPipedStdin() {
|
|
344
365
|
if (process.stdin.isTTY)
|
|
@@ -351,7 +372,25 @@ async function readPipedStdin() {
|
|
|
351
372
|
process.stdin.resume();
|
|
352
373
|
});
|
|
353
374
|
}
|
|
354
|
-
main()
|
|
375
|
+
main()
|
|
376
|
+
.then(() => {
|
|
377
|
+
void exitAfterFlush(0);
|
|
378
|
+
})
|
|
379
|
+
.catch((err) => {
|
|
355
380
|
console.error(chalk.red(`Fatal error: ${err.message}`));
|
|
356
|
-
|
|
381
|
+
void exitAfterFlush(1);
|
|
357
382
|
});
|
|
383
|
+
async function exitAfterFlush(code) {
|
|
384
|
+
await Promise.all([
|
|
385
|
+
flushStream(process.stdout),
|
|
386
|
+
flushStream(process.stderr),
|
|
387
|
+
]);
|
|
388
|
+
process.exit(code);
|
|
389
|
+
}
|
|
390
|
+
function flushStream(stream) {
|
|
391
|
+
if (stream.destroyed || stream.writableEnded)
|
|
392
|
+
return Promise.resolve();
|
|
393
|
+
return new Promise((resolve) => {
|
|
394
|
+
stream.write("", () => resolve());
|
|
395
|
+
});
|
|
396
|
+
}
|
package/dist/mcp/transports.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { HttpServerConfig, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, McpTransport, SseServerConfig, StdioServerConfig } from "./types.js";
|
|
16
16
|
type IncomingHandler = (msg: JsonRpcResponse | JsonRpcNotification | JsonRpcRequest) => void;
|
|
17
|
+
export declare const MCP_HTTP_CLOSE_TIMEOUT_MS = 750;
|
|
17
18
|
export declare class StdioTransport implements McpTransport {
|
|
18
19
|
private readonly config;
|
|
19
20
|
private child?;
|
package/dist/mcp/transports.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* JSON-RPC calls on the same pipe.
|
|
14
14
|
*/
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
|
+
export const MCP_HTTP_CLOSE_TIMEOUT_MS = 750;
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Stdio
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -233,15 +234,22 @@ export class HttpTransport {
|
|
|
233
234
|
this.closed = true;
|
|
234
235
|
// Best-effort session termination. Per spec, DELETE /mcp with the session id.
|
|
235
236
|
if (this.sessionId) {
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const timeout = setTimeout(() => controller.abort(), MCP_HTTP_CLOSE_TIMEOUT_MS);
|
|
239
|
+
timeout.unref?.();
|
|
236
240
|
try {
|
|
237
241
|
await fetch(this.url, {
|
|
238
242
|
method: "DELETE",
|
|
239
243
|
headers: { "Mcp-Session-Id": this.sessionId, ...this.baseHeaders },
|
|
244
|
+
signal: controller.signal,
|
|
240
245
|
});
|
|
241
246
|
}
|
|
242
247
|
catch {
|
|
243
248
|
// ignore
|
|
244
249
|
}
|
|
250
|
+
finally {
|
|
251
|
+
clearTimeout(timeout);
|
|
252
|
+
}
|
|
245
253
|
}
|
|
246
254
|
this.closeHandler?.();
|
|
247
255
|
}
|
package/dist/model-catalog.js
CHANGED
|
@@ -77,7 +77,7 @@ export const BUILTIN_MODELS = [
|
|
|
77
77
|
{ id: "gemma-2-9b-it", name: "gemma-2-9b-it", providerId: "groq", reasoningLevels: ["off"], contextWindow: 32768 },
|
|
78
78
|
{ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", name: "meta-llama/Llama-3.3-70B-Instruct-Turbo", providerId: "together", reasoningLevels: ["off"], contextWindow: 32768 },
|
|
79
79
|
{ id: "Qwen/Qwen2.5-72B-Instruct", name: "Qwen/Qwen2.5-72B-Instruct", providerId: "together", reasoningLevels: ["off"], contextWindow: 32768 },
|
|
80
|
-
{ id: "accounts/fireworks/models/kimi-k2p6", name: "Kimi
|
|
80
|
+
{ id: "accounts/fireworks/models/kimi-k2p6", name: "Kimi-K2.6", providerId: "fireworks", reasoningLevels: ["off"], contextWindow: 256000 },
|
|
81
81
|
{ id: "llama3.1", name: "llama3.1", providerId: "local", reasoningLevels: ["off"], contextWindow: 32768 },
|
|
82
82
|
{ id: "qwen2.5", name: "qwen2.5", providerId: "local", reasoningLevels: ["off"], contextWindow: 32768 },
|
|
83
83
|
{ id: "deepseek-coder-v2", name: "deepseek-coder-v2", providerId: "local", reasoningLevels: ["off"], contextWindow: 32768 },
|
|
@@ -3,7 +3,7 @@ import { classifyTaskSize } from "../agent/task-size.js";
|
|
|
3
3
|
import { EvidenceTracker } from "../agent/evidence-tracker.js";
|
|
4
4
|
import { ExecutionGovernor } from "../agent/execution-governor.js";
|
|
5
5
|
import { arbitrateToolCall } from "../agent/tool-arbiter.js";
|
|
6
|
-
import { buildEditRetryEscalationReminder,
|
|
6
|
+
import { buildEditRetryEscalationReminder, buildSmallTaskHint, buildTaskSummaryReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
|
|
7
7
|
import { reminderForTaskType } from "../prompt/task-reminders.js";
|
|
8
8
|
import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
|
|
9
9
|
export function createDefaultHooks() {
|
|
@@ -36,18 +36,15 @@ export function createDefaultHooks() {
|
|
|
36
36
|
},
|
|
37
37
|
beforeModelCall(ctx) {
|
|
38
38
|
ctx.agent.compactResidentHistory();
|
|
39
|
-
if (ctx.state.governor) {
|
|
40
|
-
ctx.toolEntries = ctx.state.governor.filterToolDefinitions(ctx.toolEntries);
|
|
41
|
-
}
|
|
42
39
|
if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker && ctx.state.governor) {
|
|
43
40
|
const coverage = ctx.state.evidenceTracker.snapshot();
|
|
44
41
|
const phase = resolveWorkflowPhase({
|
|
45
42
|
coreCoverageComplete: ctx.state.evidenceTracker.isCoreCoverageComplete(),
|
|
46
|
-
searchFrozen:
|
|
43
|
+
searchFrozen: false,
|
|
47
44
|
});
|
|
48
45
|
ctx.state.workflowPhase = phase;
|
|
49
46
|
const summary = formatCoverageSummary(coverage);
|
|
50
|
-
const key = `${phase}:${ctx.state.evidenceTracker.key()}
|
|
47
|
+
const key = `${phase}:${ctx.state.evidenceTracker.key()}:0`;
|
|
51
48
|
if (ctx.state.workflowKey !== key) {
|
|
52
49
|
ctx.state.workflowKey = key;
|
|
53
50
|
ctx.queueReminder(buildWorkflowPhaseReminder({
|
|
@@ -66,10 +63,7 @@ export function createDefaultHooks() {
|
|
|
66
63
|
beforeToolCall(ctx) {
|
|
67
64
|
const arbitration = arbitrateToolCall(ctx.toolCall);
|
|
68
65
|
ctx.replaceToolCall({ ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) });
|
|
69
|
-
|
|
70
|
-
if (decision?.blockedResult) {
|
|
71
|
-
ctx.blockToolCall(decision.blockedResult);
|
|
72
|
-
}
|
|
66
|
+
ctx.state.governor?.beforeToolCall(ctx.toolCall);
|
|
73
67
|
},
|
|
74
68
|
afterToolCall(ctx) {
|
|
75
69
|
if (ctx.toolCall.arbiterNote) {
|
|
@@ -107,23 +101,11 @@ export function createDefaultHooks() {
|
|
|
107
101
|
ctx.state.recentEditFailures = [];
|
|
108
102
|
ctx.state.editRetryReminderSent = false;
|
|
109
103
|
}
|
|
110
|
-
// Redundant-Read detection
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (path) {
|
|
116
|
-
const seen = ctx.state.recentReadPaths ?? (ctx.state.recentReadPaths = []);
|
|
117
|
-
const flagged = ctx.state.redundantReadReminded ?? (ctx.state.redundantReadReminded = new Set());
|
|
118
|
-
if (seen.includes(path) && !flagged.has(path)) {
|
|
119
|
-
flagged.add(path);
|
|
120
|
-
ctx.queueReminder(buildRedundantReadReminder(path));
|
|
121
|
-
}
|
|
122
|
-
seen.push(path);
|
|
123
|
-
if (seen.length > 16)
|
|
124
|
-
seen.shift();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
104
|
+
// Redundant-Read detection moved into the read tool itself: it now
|
|
105
|
+
// returns a FILE_UNCHANGED_STUB (or auto-advances to the next page)
|
|
106
|
+
// when the same args land on an unchanged file. Hook-level reminder
|
|
107
|
+
// is removed to avoid duplicate signals and to let the structural
|
|
108
|
+
// dedup do the work.
|
|
127
109
|
if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
|
|
128
110
|
markCodeChanged(ctx.state);
|
|
129
111
|
}
|
|
@@ -149,12 +131,6 @@ export function createDefaultHooks() {
|
|
|
149
131
|
ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
|
|
150
132
|
return;
|
|
151
133
|
}
|
|
152
|
-
const allSearchResultsWereLowSignal = ctx.toolCalls.length > 0
|
|
153
|
-
&& ctx.toolCalls.every((toolCall) => ["glob", "grep", "bash", "web_search", "web_fetch"].includes(toolCall.name))
|
|
154
|
-
&& ctx.toolResults.every((result) => result.status === "no_match" || result.status === "blocked");
|
|
155
|
-
if (ctx.state.governor?.snapshot().searchFrozen && allSearchResultsWereLowSignal) {
|
|
156
|
-
ctx.requestTextOnlyTurn("Search continuation has become low-yield. Summarize the strongest evidence already collected instead of continuing broad exploration.");
|
|
157
|
-
}
|
|
158
134
|
// Verification reminders intentionally removed. See afterToolCall.
|
|
159
135
|
},
|
|
160
136
|
afterTurn() {
|
package/dist/prompt/compose.js
CHANGED
|
@@ -40,6 +40,7 @@ function buildProviderPrompt(agentName, providerId, modelId, modelName) {
|
|
|
40
40
|
const provider = providerId ?? "";
|
|
41
41
|
const rawModel = modelId ?? modelName ?? "";
|
|
42
42
|
const model = rawModel.includes(":") ? rawModel.split(":").slice(1).join(":") : rawModel;
|
|
43
|
+
const lowerModel = model.toLowerCase();
|
|
43
44
|
if (provider === "anthropic" || model.startsWith("claude")) {
|
|
44
45
|
return buildAnthropicProviderPrompt(agentName);
|
|
45
46
|
}
|
|
@@ -52,7 +53,7 @@ function buildProviderPrompt(agentName, providerId, modelId, modelName) {
|
|
|
52
53
|
if (provider === "deepseek" || model.startsWith("deepseek")) {
|
|
53
54
|
return buildDeepSeekProviderPrompt(agentName);
|
|
54
55
|
}
|
|
55
|
-
if (["moonshot-cn", "moonshot-intl", "kimi-for-coding"].includes(provider) ||
|
|
56
|
+
if (["moonshot-cn", "moonshot-intl", "kimi-for-coding"].includes(provider) || lowerModel.includes("kimi") || lowerModel.includes("k2.")) {
|
|
56
57
|
return buildKimiProviderPrompt(agentName);
|
|
57
58
|
}
|
|
58
59
|
if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(provider) || model.startsWith("glm")) {
|
|
@@ -2,5 +2,7 @@ export function buildKimiProviderPrompt(agentName) {
|
|
|
2
2
|
return `You are ${agentName}, a terminal coding agent running on a Kimi/Moonshot model.
|
|
3
3
|
|
|
4
4
|
Keep tool use disciplined: pursue one concrete hypothesis at a time, read results carefully, and converge after evidence is sufficient.
|
|
5
|
-
Do not fan out into many parallel search directions unless the task truly requires it
|
|
5
|
+
Do not fan out into many parallel search directions unless the task truly requires it.
|
|
6
|
+
|
|
7
|
+
Evidence-first project exploration: use observed filesystem evidence as the source of truth. Do not assume conventional project files or directories exist. Before reading or operating on a path, ensure it was observed, directly derived from an observed path, or explicitly provided by the user. If a path is missing, adapt to the observed structure instead of probing more conventional paths.`;
|
|
6
8
|
}
|
|
@@ -41,7 +41,8 @@ export declare function buildTaskSummaryReminder(): string;
|
|
|
41
41
|
export declare function buildEditRetryEscalationReminder(reason: string): string;
|
|
42
42
|
/**
|
|
43
43
|
* Fired the FIRST time the model re-reads a file it already read in this turn.
|
|
44
|
-
* Soft — does not freeze the tool.
|
|
44
|
+
* Soft — does not freeze the tool. The model may still re-read when context was
|
|
45
|
+
* pruned, the requested range changed, or a later mutation needs verification.
|
|
45
46
|
*/
|
|
46
47
|
export declare function buildRedundantReadReminder(path: string): string;
|
|
47
48
|
/**
|
package/dist/prompt/reminders.js
CHANGED
|
@@ -200,12 +200,13 @@ Stop retrying the same call. Pick one of:
|
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
202
|
* Fired the FIRST time the model re-reads a file it already read in this turn.
|
|
203
|
-
* Soft — does not freeze the tool.
|
|
203
|
+
* Soft — does not freeze the tool. The model may still re-read when context was
|
|
204
|
+
* pruned, the requested range changed, or a later mutation needs verification.
|
|
204
205
|
*/
|
|
205
206
|
export function buildRedundantReadReminder(path) {
|
|
206
207
|
return wrapInSystemReminder(`
|
|
207
|
-
You already read ${path} earlier in this turn.
|
|
208
|
-
|
|
208
|
+
You already read ${path} earlier in this turn. If that content is still available and nothing changed, rely on it rather than re-reading.
|
|
209
|
+
It is okay to re-read when you need to recover pruned context, inspect a different range, or verify a later edit/write/bash change.
|
|
209
210
|
`);
|
|
210
211
|
}
|
|
211
212
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Supports OpenAI-compatible providers with dynamic or static model lists.
|
|
5
5
|
* Reads provider configuration from models.json first, then falls back to config.json.
|
|
6
6
|
*/
|
|
7
|
-
import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, getBuiltinProvider, listBuiltinModels, } from "./model-catalog.js";
|
|
7
|
+
import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, getBuiltinModel, getBuiltinProvider, listBuiltinModels, } from "./model-catalog.js";
|
|
8
8
|
import { ModelConfig } from "./model-config.js";
|
|
9
9
|
import { AuthStorage } from "./oauth/index.js";
|
|
10
10
|
import { fetchOpenAICodexModels } from "./provider-openai-codex.js";
|
|
@@ -232,8 +232,8 @@ export function decodeModel(value) {
|
|
|
232
232
|
}
|
|
233
233
|
/** Strip provider prefix for concise display. */
|
|
234
234
|
export function displayModel(model) {
|
|
235
|
-
const { modelId } = decodeModel(model);
|
|
236
|
-
return modelId;
|
|
235
|
+
const { providerId, modelId } = decodeModel(model);
|
|
236
|
+
return providerId ? getBuiltinModel(providerId, modelId)?.name ?? modelId : modelId;
|
|
237
237
|
}
|
|
238
238
|
/** Normalize user input to provider:model format when possible. */
|
|
239
239
|
export function normalizeModel(model, defaultProvider = "openai") {
|
|
@@ -3,7 +3,9 @@ export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
|
|
|
3
3
|
export interface ProviderRequestConfig {
|
|
4
4
|
effectiveThinkingLevel: ThinkingLevel;
|
|
5
5
|
reasoningEffort?: ThinkingLevel;
|
|
6
|
-
reasoningContentEcho?: "tool_calls" | "all";
|
|
6
|
+
reasoningContentEcho?: "tool_calls" | "all" | "none";
|
|
7
|
+
parallelToolCalls?: boolean;
|
|
8
|
+
maxTokens?: number;
|
|
7
9
|
extraBody?: Record<string, unknown>;
|
|
8
10
|
omitTemperature?: boolean;
|
|
9
11
|
}
|