@bubblebrain-ai/bubble 0.0.7 → 0.0.9
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/categories.d.ts +34 -0
- package/dist/agent/categories.js +98 -0
- package/dist/agent/profiles.d.ts +4 -0
- package/dist/agent/profiles.js +2 -3
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-control.js +4 -0
- package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
- package/dist/agent/subagent-lifecycle-reminder.js +102 -0
- package/dist/agent/subagent-route-format.d.ts +8 -0
- package/dist/agent/subagent-route-format.js +18 -0
- package/dist/agent/subtask-policy.d.ts +0 -1
- package/dist/agent/subtask-policy.js +0 -4
- package/dist/agent.d.ts +18 -0
- package/dist/agent.js +188 -16
- package/dist/config.d.ts +23 -3
- package/dist/config.js +59 -6
- package/dist/context/budget.d.ts +3 -2
- package/dist/context/budget.js +29 -15
- package/dist/context/compact.d.ts +23 -0
- package/dist/context/compact.js +129 -0
- package/dist/context/llm-compactor.d.ts +19 -0
- package/dist/context/llm-compactor.js +200 -0
- package/dist/context/projector.js +28 -12
- package/dist/context/token-estimator.d.ts +14 -0
- package/dist/context/token-estimator.js +106 -0
- package/dist/context/tool-output-truncate.d.ts +8 -0
- package/dist/context/tool-output-truncate.js +59 -0
- 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 +68 -7
- package/dist/mcp/transports.d.ts +1 -0
- package/dist/mcp/transports.js +8 -0
- package/dist/model-catalog.d.ts +9 -0
- package/dist/model-catalog.js +17 -1
- package/dist/orchestrator/default-hooks.js +24 -18
- package/dist/prompt/compose.js +2 -1
- package/dist/prompt/provider-prompts/kimi.js +3 -1
- package/dist/provider-openai-codex.d.ts +13 -2
- package/dist/provider-openai-codex.js +81 -32
- package/dist/provider-registry.js +22 -6
- 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 +60 -2
- package/dist/slash-commands/types.d.ts +7 -0
- package/dist/tools/agent-lifecycle.js +22 -4
- package/dist/tools/edit.js +7 -2
- package/dist/tools/file-state.d.ts +19 -0
- package/dist/tools/file-state.js +15 -0
- package/dist/tools/glob.js +2 -1
- package/dist/tools/grep.js +2 -2
- package/dist/tools/lsp.js +2 -2
- package/dist/tools/path-utils.d.ts +2 -0
- package/dist/tools/path-utils.js +16 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +207 -14
- package/dist/tools/write.js +3 -2
- 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 +52 -0
- package/dist/tui-ink/app.js +1129 -0
- package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
- package/dist/tui-ink/approval/approval-dialog.js +132 -0
- package/dist/tui-ink/approval/diff-view.d.ts +7 -0
- package/dist/tui-ink/approval/diff-view.js +44 -0
- package/dist/tui-ink/approval/select.d.ts +35 -0
- package/dist/tui-ink/approval/select.js +88 -0
- package/dist/tui-ink/code-highlight.d.ts +8 -0
- package/dist/tui-ink/code-highlight.js +122 -0
- package/dist/tui-ink/detect-theme.d.ts +19 -0
- package/dist/tui-ink/detect-theme.js +123 -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 +45 -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 +694 -0
- package/dist/tui-ink/input-history.d.ts +16 -0
- package/dist/tui-ink/input-history.js +81 -0
- package/dist/tui-ink/markdown.d.ts +38 -0
- package/dist/tui-ink/markdown.js +394 -0
- package/dist/tui-ink/message-list.d.ts +33 -0
- package/dist/tui-ink/message-list.js +667 -0
- package/dist/tui-ink/model-picker.d.ts +43 -0
- package/dist/tui-ink/model-picker.js +331 -0
- package/dist/tui-ink/plan-confirm.d.ts +7 -0
- package/dist/tui-ink/plan-confirm.js +105 -0
- package/dist/tui-ink/question-dialog.d.ts +8 -0
- package/dist/tui-ink/question-dialog.js +99 -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 +37 -0
- package/dist/tui-ink/run.js +53 -0
- package/dist/tui-ink/theme.d.ts +66 -0
- package/dist/tui-ink/theme.js +115 -0
- package/dist/tui-ink/todos.d.ts +7 -0
- package/dist/tui-ink/todos.js +46 -0
- package/dist/tui-ink/trace-groups.d.ts +27 -0
- package/dist/tui-ink/trace-groups.js +389 -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 +138 -0
- package/dist/types.d.ts +10 -0
- package/package.json +7 -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, input.providerId) : 0;
|
|
13
|
+
const systemPromptTokens = Math.max(0, estimateTextTokens(systemContent, input.providerId) - skillsTokens);
|
|
14
|
+
const toolsTokens = estimateToolEntriesTokens(input.toolEntries, input.providerId);
|
|
15
|
+
const deferredToolsTokens = estimateDeferredToolsReminderTokens(deferredToolEntries, input.providerId);
|
|
16
|
+
const rawOtherTokens = otherMessages.reduce((sum, message) => sum + estimateMessageTokens(message, input.providerId), 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, providerId) {
|
|
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, providerId) + 8;
|
|
108
|
+
}, 0);
|
|
109
|
+
}
|
|
110
|
+
function estimateDeferredToolsReminderTokens(entries, providerId) {
|
|
111
|
+
if (entries.length === 0)
|
|
112
|
+
return 0;
|
|
113
|
+
return estimateTextTokens(buildDeferredToolsReminder(entries.map((entry) => entry.name)), providerId);
|
|
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";
|
|
@@ -55,6 +56,20 @@ async function main() {
|
|
|
55
56
|
})
|
|
56
57
|
: createUnavailableProvider(unavailableProviderMessage);
|
|
57
58
|
const createProvider = (providerId, apiKey, baseURL) => createProviderInstance({ providerId, apiKey, baseURL, thinkingLevel: args.thinkingLevel });
|
|
59
|
+
const createProviderForRoute = async (route) => {
|
|
60
|
+
const providerId = route.providerId;
|
|
61
|
+
if (!providerId) {
|
|
62
|
+
throw new Error(`Subagent route for model "${route.model}" did not include a provider.`);
|
|
63
|
+
}
|
|
64
|
+
if (registry.supportsOAuth(providerId) && registry.getAuthStorage().has(providerId)) {
|
|
65
|
+
await registry.prepareProvider(providerId);
|
|
66
|
+
}
|
|
67
|
+
const target = registry.getConfigured().find((item) => item.id === providerId);
|
|
68
|
+
if (!target?.enabled || !target.apiKey) {
|
|
69
|
+
throw new Error(`Subagent route requires provider "${providerId}", but it is not configured or has no active credentials.`);
|
|
70
|
+
}
|
|
71
|
+
return createProvider(providerId, target.apiKey, target.baseURL);
|
|
72
|
+
};
|
|
58
73
|
let agentRef;
|
|
59
74
|
const todoStore = {
|
|
60
75
|
getTodos: () => agentRef?.getTodos() ?? [],
|
|
@@ -92,6 +107,7 @@ async function main() {
|
|
|
92
107
|
unlock: (names) => agentRef?.unlockDeferredTools(names),
|
|
93
108
|
};
|
|
94
109
|
const lspService = getLspService(args.cwd, settingsManager.getMerged().lsp);
|
|
110
|
+
const fileStateTracker = new FileStateTracker(args.cwd);
|
|
95
111
|
const tools = createAllTools(args.cwd, skillRegistry, {
|
|
96
112
|
todoStore,
|
|
97
113
|
planController,
|
|
@@ -99,6 +115,7 @@ async function main() {
|
|
|
99
115
|
questionController: printMode ? undefined : questionController,
|
|
100
116
|
toolSearchController,
|
|
101
117
|
lspService,
|
|
118
|
+
fileStateTracker,
|
|
102
119
|
});
|
|
103
120
|
// Bring up MCP servers (if any). Failures are captured per-server and never
|
|
104
121
|
// block the rest of startup; /mcp surfaces status at runtime.
|
|
@@ -246,6 +263,9 @@ async function main() {
|
|
|
246
263
|
budgetLedger,
|
|
247
264
|
skills: skillSummaries,
|
|
248
265
|
memoryPrompt,
|
|
266
|
+
fileStateTracker,
|
|
267
|
+
agentCategories: userConfig.getAgentCategories(),
|
|
268
|
+
providerFactory: createProviderForRoute,
|
|
249
269
|
});
|
|
250
270
|
agentRef = agent;
|
|
251
271
|
if (sessionManager) {
|
|
@@ -330,9 +350,9 @@ async function main() {
|
|
|
330
350
|
console.log();
|
|
331
351
|
return;
|
|
332
352
|
}
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
353
|
+
const tuiRuntime = process.env.BUBBLE_TUI === "opentui" ? "opentui" : "ink";
|
|
354
|
+
const themeConfig = userConfig.getTheme();
|
|
355
|
+
const commonOptions = {
|
|
336
356
|
sessionManager,
|
|
337
357
|
createProvider,
|
|
338
358
|
registry,
|
|
@@ -344,12 +364,35 @@ async function main() {
|
|
|
344
364
|
settingsManager,
|
|
345
365
|
lspService,
|
|
346
366
|
mcpManager,
|
|
347
|
-
theme: userConfig.getTheme(),
|
|
348
367
|
flushMemory,
|
|
349
368
|
runMemoryCompaction,
|
|
350
369
|
runMemorySummary,
|
|
351
370
|
runMemoryRefresh,
|
|
352
|
-
}
|
|
371
|
+
};
|
|
372
|
+
if (tuiRuntime === "opentui") {
|
|
373
|
+
const { runTui } = await import("./tui/run.js");
|
|
374
|
+
await runTui(agent, args, {
|
|
375
|
+
...commonOptions,
|
|
376
|
+
theme: themeConfig.overrides,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Probe the terminal background BEFORE Ink takes over stdin. OSC 11
|
|
381
|
+
// needs raw mode, and once Ink owns stdin the reply never reaches us.
|
|
382
|
+
let detectedTheme = "dark";
|
|
383
|
+
if (themeConfig.mode === "auto") {
|
|
384
|
+
const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
|
|
385
|
+
detectedTheme = await detectTerminalTheme();
|
|
386
|
+
}
|
|
387
|
+
const { runTui } = await import("./tui-ink/run.js");
|
|
388
|
+
await runTui(agent, args, {
|
|
389
|
+
...commonOptions,
|
|
390
|
+
themeMode: themeConfig.mode,
|
|
391
|
+
themeOverrides: themeConfig.overrides,
|
|
392
|
+
detectedTheme,
|
|
393
|
+
onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
353
396
|
}
|
|
354
397
|
finally {
|
|
355
398
|
await shutdownRuntime();
|
|
@@ -366,7 +409,25 @@ async function readPipedStdin() {
|
|
|
366
409
|
process.stdin.resume();
|
|
367
410
|
});
|
|
368
411
|
}
|
|
369
|
-
main()
|
|
412
|
+
main()
|
|
413
|
+
.then(() => {
|
|
414
|
+
void exitAfterFlush(0);
|
|
415
|
+
})
|
|
416
|
+
.catch((err) => {
|
|
370
417
|
console.error(chalk.red(`Fatal error: ${err.message}`));
|
|
371
|
-
|
|
418
|
+
void exitAfterFlush(1);
|
|
372
419
|
});
|
|
420
|
+
async function exitAfterFlush(code) {
|
|
421
|
+
await Promise.all([
|
|
422
|
+
flushStream(process.stdout),
|
|
423
|
+
flushStream(process.stderr),
|
|
424
|
+
]);
|
|
425
|
+
process.exit(code);
|
|
426
|
+
}
|
|
427
|
+
function flushStream(stream) {
|
|
428
|
+
if (stream.destroyed || stream.writableEnded)
|
|
429
|
+
return Promise.resolve();
|
|
430
|
+
return new Promise((resolve) => {
|
|
431
|
+
stream.write("", () => resolve());
|
|
432
|
+
});
|
|
433
|
+
}
|
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.d.ts
CHANGED
|
@@ -11,10 +11,19 @@ export interface BuiltinModelDefinition {
|
|
|
11
11
|
providerId: string;
|
|
12
12
|
reasoningLevels: ReasoningEffort[];
|
|
13
13
|
contextWindow?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Server-declared cap on per-tool-output tokens. When set, the agent must
|
|
16
|
+
* truncate each tool result to this token budget before adding it to history
|
|
17
|
+
* — otherwise the server's input window is exceeded by raw tool dumps.
|
|
18
|
+
* (For codex models this comes from the API's `truncation_policy.limit`.)
|
|
19
|
+
*/
|
|
20
|
+
toolOutputTokenLimit?: number;
|
|
14
21
|
}
|
|
15
22
|
export declare const BUILTIN_PROVIDERS: BuiltinProviderDefinition[];
|
|
16
23
|
export declare const BUILTIN_MODELS: BuiltinModelDefinition[];
|
|
17
24
|
export declare function listBuiltinModels(providerId: string): BuiltinModelDefinition[];
|
|
25
|
+
export declare function registerDynamicModelMetadata(model: BuiltinModelDefinition): void;
|
|
18
26
|
export declare function getBuiltinModel(providerId: string, modelId: string): BuiltinModelDefinition | undefined;
|
|
19
27
|
export declare function getBuiltinProvider(providerId: string): BuiltinProviderDefinition | undefined;
|
|
20
28
|
export declare function getModelContextWindow(providerId: string, modelId: string): number | undefined;
|
|
29
|
+
export declare function getToolOutputTokenLimit(providerId: string, modelId: string): number | undefined;
|
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 },
|
|
@@ -85,7 +85,20 @@ export const BUILTIN_MODELS = [
|
|
|
85
85
|
export function listBuiltinModels(providerId) {
|
|
86
86
|
return BUILTIN_MODELS.filter((model) => model.providerId === providerId);
|
|
87
87
|
}
|
|
88
|
+
// Runtime overlay populated from provider-side discovery (e.g. ChatGPT codex /models).
|
|
89
|
+
// Looked up before the static catalog so newly-released models work without a code change.
|
|
90
|
+
const dynamicOverlay = new Map();
|
|
91
|
+
function overlayKey(providerId, modelId) {
|
|
92
|
+
return `${providerId}:${modelId}`;
|
|
93
|
+
}
|
|
94
|
+
export function registerDynamicModelMetadata(model) {
|
|
95
|
+
dynamicOverlay.set(overlayKey(model.providerId, model.id), model);
|
|
96
|
+
}
|
|
88
97
|
export function getBuiltinModel(providerId, modelId) {
|
|
98
|
+
const overlayHit = dynamicOverlay.get(overlayKey(providerId, modelId))
|
|
99
|
+
|| (providerId === "openai" ? dynamicOverlay.get(overlayKey("openai-codex", modelId)) : undefined);
|
|
100
|
+
if (overlayHit)
|
|
101
|
+
return overlayHit;
|
|
89
102
|
return BUILTIN_MODELS.find((model) => model.providerId === providerId && model.id === modelId)
|
|
90
103
|
|| (providerId === "openai"
|
|
91
104
|
? BUILTIN_MODELS.find((model) => model.providerId === "openai-codex" && model.id === modelId)
|
|
@@ -97,3 +110,6 @@ export function getBuiltinProvider(providerId) {
|
|
|
97
110
|
export function getModelContextWindow(providerId, modelId) {
|
|
98
111
|
return getBuiltinModel(providerId, modelId)?.contextWindow;
|
|
99
112
|
}
|
|
113
|
+
export function getToolOutputTokenLimit(providerId, modelId) {
|
|
114
|
+
return getBuiltinModel(providerId, modelId)?.toolOutputTokenLimit;
|
|
115
|
+
}
|
|
@@ -3,9 +3,10 @@ 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
|
+
import { buildSubagentLifecycleReminder } from "../agent/subagent-lifecycle-reminder.js";
|
|
9
10
|
export function createDefaultHooks() {
|
|
10
11
|
return [
|
|
11
12
|
{
|
|
@@ -101,23 +102,11 @@ export function createDefaultHooks() {
|
|
|
101
102
|
ctx.state.recentEditFailures = [];
|
|
102
103
|
ctx.state.editRetryReminderSent = false;
|
|
103
104
|
}
|
|
104
|
-
// Redundant-Read detection
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (path) {
|
|
110
|
-
const seen = ctx.state.recentReadPaths ?? (ctx.state.recentReadPaths = []);
|
|
111
|
-
const flagged = ctx.state.redundantReadReminded ?? (ctx.state.redundantReadReminded = new Set());
|
|
112
|
-
if (seen.includes(path) && !flagged.has(path)) {
|
|
113
|
-
flagged.add(path);
|
|
114
|
-
ctx.queueReminder(buildRedundantReadReminder(path));
|
|
115
|
-
}
|
|
116
|
-
seen.push(path);
|
|
117
|
-
if (seen.length > 16)
|
|
118
|
-
seen.shift();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
105
|
+
// Redundant-Read detection moved into the read tool itself: it now
|
|
106
|
+
// returns a FILE_UNCHANGED_STUB (or auto-advances to the next page)
|
|
107
|
+
// when the same args land on an unchanged file. Hook-level reminder
|
|
108
|
+
// is removed to avoid duplicate signals and to let the structural
|
|
109
|
+
// dedup do the work.
|
|
121
110
|
if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
|
|
122
111
|
markCodeChanged(ctx.state);
|
|
123
112
|
}
|
|
@@ -139,6 +128,12 @@ export function createDefaultHooks() {
|
|
|
139
128
|
}
|
|
140
129
|
},
|
|
141
130
|
beforeContinuation(ctx) {
|
|
131
|
+
if (hasSubagentLifecycleActivity(ctx.toolCalls, ctx.toolResults)) {
|
|
132
|
+
const reminder = buildSubagentLifecycleReminder(ctx.agent.listSubAgents(), ctx.toolResults);
|
|
133
|
+
if (reminder) {
|
|
134
|
+
ctx.queueReminder(reminder);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
142
137
|
if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker?.isCoreCoverageComplete()) {
|
|
143
138
|
ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
|
|
144
139
|
return;
|
|
@@ -161,6 +156,17 @@ function isCodeWriteResult(_toolCall, result) {
|
|
|
161
156
|
}
|
|
162
157
|
return result.metadata?.kind === "write" || result.metadata?.kind === "edit";
|
|
163
158
|
}
|
|
159
|
+
function hasSubagentLifecycleActivity(toolCalls, toolResults) {
|
|
160
|
+
return toolCalls.some((toolCall) => isSubagentLifecycleTool(toolCall.name))
|
|
161
|
+
|| toolResults.some((result) => result.metadata?.kind === "subagent");
|
|
162
|
+
}
|
|
163
|
+
function isSubagentLifecycleTool(name) {
|
|
164
|
+
return name === "spawn_agent"
|
|
165
|
+
|| name === "wait_agent"
|
|
166
|
+
|| name === "send_input"
|
|
167
|
+
|| name === "close_agent"
|
|
168
|
+
|| name === "task";
|
|
169
|
+
}
|
|
164
170
|
function hashEditCall(toolCall) {
|
|
165
171
|
// Cheap fingerprint that identifies "same edit/write call". JSON of the
|
|
166
172
|
// sorted parsed args is good enough — we only need stable equality between
|
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
|
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import type { Provider, ThinkingLevel } from "./types.js";
|
|
1
|
+
import type { Provider, ReasoningEffort, ThinkingLevel } from "./types.js";
|
|
2
|
+
export interface CodexModelDescriptor {
|
|
3
|
+
id: string;
|
|
4
|
+
displayName?: string;
|
|
5
|
+
contextWindow?: number;
|
|
6
|
+
reasoningLevels?: ReasoningEffort[];
|
|
7
|
+
visibility?: string;
|
|
8
|
+
minimalClientVersion?: string;
|
|
9
|
+
/** Server-declared per-tool-output token cap (truncation_policy.limit when mode=tokens). */
|
|
10
|
+
toolOutputTokenLimit?: number;
|
|
11
|
+
}
|
|
2
12
|
export declare function isOpenAICodexBaseUrl(baseURL: string): boolean;
|
|
3
13
|
export declare function getOpenAICodexFallbackModels(): string[];
|
|
4
14
|
export declare function extractChatGptAccountId(accessToken: string): string | undefined;
|
|
@@ -11,4 +21,5 @@ export declare function createOpenAICodexProvider(options: {
|
|
|
11
21
|
export declare function fetchOpenAICodexModels(options: {
|
|
12
22
|
baseURL: string;
|
|
13
23
|
accessToken: string;
|
|
14
|
-
}): Promise<
|
|
24
|
+
}): Promise<CodexModelDescriptor[]>;
|
|
25
|
+
export declare function sortCodexModelDescriptors(descriptors: CodexModelDescriptor[]): CodexModelDescriptor[];
|