@bubblebrain-ai/bubble 0.0.7 → 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.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 +28 -4
- 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 +6 -18
- package/dist/prompt/compose.js +2 -1
- package/dist/prompt/provider-prompts/kimi.js +3 -1
- 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 -2
- 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
package/dist/agent.d.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Agent - The core decision loop.
|
|
3
3
|
* It maintains message state, calls the LLM, executes tools, and auto-continues.
|
|
4
4
|
*/
|
|
5
|
+
import { type ContextUsageSnapshot } from "./context/usage.js";
|
|
5
6
|
import type { AgentEvent, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
|
|
6
7
|
import { type TurnHooks } from "./orchestrator/hooks.js";
|
|
7
8
|
import { BudgetLedger } from "./agent/budget-ledger.js";
|
|
8
9
|
import { type AgentProfile, type SubagentRunResult } from "./agent/profiles.js";
|
|
9
10
|
import { type SubagentThreadSnapshot } from "./agent/subagent-control.js";
|
|
10
11
|
import type { SkillSummary } from "./skills/types.js";
|
|
12
|
+
import type { FileStateTracker } from "./tools/file-state.js";
|
|
11
13
|
export declare class AgentAbortError extends Error {
|
|
12
14
|
constructor(message?: string);
|
|
13
15
|
}
|
|
@@ -39,6 +41,7 @@ export interface AgentOptions {
|
|
|
39
41
|
};
|
|
40
42
|
skills?: SkillSummary[];
|
|
41
43
|
memoryPrompt?: string;
|
|
44
|
+
fileStateTracker?: FileStateTracker;
|
|
42
45
|
}
|
|
43
46
|
export declare class Agent {
|
|
44
47
|
messages: Message[];
|
|
@@ -65,6 +68,7 @@ export declare class Agent {
|
|
|
65
68
|
private budgetSource;
|
|
66
69
|
private skillSummaries;
|
|
67
70
|
private memoryPrompt?;
|
|
71
|
+
private fileStateTracker?;
|
|
68
72
|
private subagentThreads;
|
|
69
73
|
private pendingSubagentUpdates;
|
|
70
74
|
private lastInputTokens;
|
|
@@ -74,8 +78,10 @@ export declare class Agent {
|
|
|
74
78
|
unlockDeferredTools(names: string[]): void;
|
|
75
79
|
/** All deferred tools in this session (for tool_search to inspect). */
|
|
76
80
|
listDeferredTools(): ToolRegistryEntry[];
|
|
81
|
+
getContextUsageSnapshot(): ContextUsageSnapshot;
|
|
77
82
|
/** Whether a given tool is deferred and not yet unlocked. */
|
|
78
83
|
isDeferredAndLocked(name: string): boolean;
|
|
84
|
+
private getActiveToolEntries;
|
|
79
85
|
injectSystemReminder(content: string): void;
|
|
80
86
|
injectModeReminder(): void;
|
|
81
87
|
get model(): string;
|
package/dist/agent.js
CHANGED
|
@@ -6,6 +6,7 @@ import { compactMessages } from "./context/compact.js";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { compactMessagesWithLLM } from "./context/compact-llm.js";
|
|
8
8
|
import { getContextBudget } from "./context/budget.js";
|
|
9
|
+
import { buildContextUsageSnapshot } from "./context/usage.js";
|
|
9
10
|
import { isContextOverflowError } from "./context/overflow.js";
|
|
10
11
|
import { projectMessages } from "./context/projector.js";
|
|
11
12
|
import { aggressivePruneMessages } from "./context/prune.js";
|
|
@@ -18,6 +19,7 @@ import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAg
|
|
|
18
19
|
import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
|
|
19
20
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
20
21
|
import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
|
|
22
|
+
import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
|
|
21
23
|
const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
|
|
22
24
|
const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
|
|
23
25
|
const RESIDENT_HISTORY_MESSAGE_LIMIT = 160;
|
|
@@ -56,6 +58,7 @@ export class Agent {
|
|
|
56
58
|
budgetSource;
|
|
57
59
|
skillSummaries;
|
|
58
60
|
memoryPrompt;
|
|
61
|
+
fileStateTracker;
|
|
59
62
|
subagentThreads = new Map();
|
|
60
63
|
pendingSubagentUpdates = [];
|
|
61
64
|
lastInputTokens = null;
|
|
@@ -80,6 +83,7 @@ export class Agent {
|
|
|
80
83
|
this.budgetSource = options.budgetSource ?? { runId: this.sessionID ?? "agent" };
|
|
81
84
|
this.skillSummaries = options.skills ?? [];
|
|
82
85
|
this.memoryPrompt = options.memoryPrompt;
|
|
86
|
+
this.fileStateTracker = options.fileStateTracker;
|
|
83
87
|
if (options.systemPrompt) {
|
|
84
88
|
this.messages.push({ role: "system", content: options.systemPrompt });
|
|
85
89
|
}
|
|
@@ -111,11 +115,26 @@ export class Agent {
|
|
|
111
115
|
listDeferredTools() {
|
|
112
116
|
return [...this.tools.values()].filter((t) => t.deferred);
|
|
113
117
|
}
|
|
118
|
+
getContextUsageSnapshot() {
|
|
119
|
+
return buildContextUsageSnapshot({
|
|
120
|
+
providerId: this.providerId,
|
|
121
|
+
modelId: this.apiModel,
|
|
122
|
+
messages: this.messages,
|
|
123
|
+
toolEntries: this.getActiveToolEntries(),
|
|
124
|
+
deferredToolEntries: this.listDeferredTools(),
|
|
125
|
+
skills: this.skillSummaries,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
114
128
|
/** Whether a given tool is deferred and not yet unlocked. */
|
|
115
129
|
isDeferredAndLocked(name) {
|
|
116
130
|
const tool = this.tools.get(name);
|
|
117
131
|
return !!tool?.deferred && !this.unlockedDeferred.has(name);
|
|
118
132
|
}
|
|
133
|
+
getActiveToolEntries() {
|
|
134
|
+
return [...this.tools.values()]
|
|
135
|
+
.filter((tool) => !tool.deferred || this.unlockedDeferred.has(tool.name))
|
|
136
|
+
.filter((tool) => this._mode === "plan" || tool.name !== "exit_plan_mode");
|
|
137
|
+
}
|
|
119
138
|
injectSystemReminder(content) {
|
|
120
139
|
this.appendMessage({ role: "meta", kind: "system-reminder", content });
|
|
121
140
|
}
|
|
@@ -320,6 +339,15 @@ export class Agent {
|
|
|
320
339
|
yield { type: "text_delta", content: chunk.content };
|
|
321
340
|
break;
|
|
322
341
|
case "reasoning_delta":
|
|
342
|
+
debugReasoningStream({
|
|
343
|
+
stage: "agent_receive",
|
|
344
|
+
providerId: this._providerId,
|
|
345
|
+
modelId: this.apiModel,
|
|
346
|
+
turnStep: step,
|
|
347
|
+
beforeLength: assistantMsg.reasoning?.length ?? 0,
|
|
348
|
+
delta: summarizeDebugText(chunk.content),
|
|
349
|
+
afterLength: (assistantMsg.reasoning?.length ?? 0) + chunk.content.length,
|
|
350
|
+
});
|
|
323
351
|
assistantMsg.reasoning = (assistantMsg.reasoning || "") + chunk.content;
|
|
324
352
|
yield { type: "reasoning_delta", content: chunk.content };
|
|
325
353
|
break;
|
|
@@ -533,7 +561,7 @@ export class Agent {
|
|
|
533
561
|
},
|
|
534
562
|
});
|
|
535
563
|
flushGovernorReminders();
|
|
536
|
-
yield { type: "turn_end", usage: turnUsage };
|
|
564
|
+
yield { type: "turn_end", usage: turnUsage, willContinue: true };
|
|
537
565
|
// Auto-continue: if we have tool results, the LLM needs to respond to them.
|
|
538
566
|
// Emitting the turn boundary keeps UI renderers aligned with the persisted
|
|
539
567
|
// assistant/tool message sequence instead of merging the next answer into
|
|
@@ -549,8 +577,9 @@ export class Agent {
|
|
|
549
577
|
flushReminders: flushGovernorReminders,
|
|
550
578
|
});
|
|
551
579
|
flushGovernorReminders();
|
|
552
|
-
|
|
553
|
-
|
|
580
|
+
const willContinue = !!hookState.forceContinuationReason;
|
|
581
|
+
yield { type: "turn_end", usage: turnUsage, willContinue };
|
|
582
|
+
if (willContinue) {
|
|
554
583
|
delete hookState.forceContinuationReason;
|
|
555
584
|
continue;
|
|
556
585
|
}
|
|
@@ -569,6 +598,7 @@ export class Agent {
|
|
|
569
598
|
if (afterTokens < beforeTokens) {
|
|
570
599
|
this.lastInputTokens = null;
|
|
571
600
|
this.lastAnchorMessageCount = null;
|
|
601
|
+
this.fileStateTracker?.invalidateReadHistory();
|
|
572
602
|
return before - this.messages.length;
|
|
573
603
|
}
|
|
574
604
|
}
|
|
@@ -583,6 +613,7 @@ export class Agent {
|
|
|
583
613
|
this.messages = llmResult.messages;
|
|
584
614
|
this.lastInputTokens = null;
|
|
585
615
|
this.lastAnchorMessageCount = null;
|
|
616
|
+
this.fileStateTracker?.invalidateReadHistory();
|
|
586
617
|
return before - this.messages.length;
|
|
587
618
|
}
|
|
588
619
|
const fallback = compactMessages(this.messages, { keepRecentTurns });
|
|
@@ -590,6 +621,7 @@ export class Agent {
|
|
|
590
621
|
this.messages = fallback.messages;
|
|
591
622
|
this.lastInputTokens = null;
|
|
592
623
|
this.lastAnchorMessageCount = null;
|
|
624
|
+
this.fileStateTracker?.invalidateReadHistory();
|
|
593
625
|
return before - this.messages.length;
|
|
594
626
|
}
|
|
595
627
|
return 0;
|
|
@@ -1056,6 +1088,7 @@ export class Agent {
|
|
|
1056
1088
|
this.messages = candidate;
|
|
1057
1089
|
this.lastInputTokens = null;
|
|
1058
1090
|
this.lastAnchorMessageCount = null;
|
|
1091
|
+
this.fileStateTracker?.invalidateReadHistory();
|
|
1059
1092
|
}
|
|
1060
1093
|
}
|
|
1061
1094
|
appendMessage(message) {
|
package/dist/context/budget.d.ts
CHANGED
|
@@ -19,3 +19,4 @@ export interface ContextBudgetOptions {
|
|
|
19
19
|
export declare function estimateMessageTokens(message: Message): number;
|
|
20
20
|
export declare function estimateContextTokens(messages: Message[]): number;
|
|
21
21
|
export declare function getContextBudget(providerId: string, modelId: string, messages: Message[], options?: ContextBudgetOptions): ContextBudget;
|
|
22
|
+
export declare function estimateTextTokens(text: string): number;
|
package/dist/context/budget.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SkillSummary } from "../skills/types.js";
|
|
2
|
+
import type { Message, ToolRegistryEntry } from "../types.js";
|
|
3
|
+
export interface ContextUsageBucket {
|
|
4
|
+
label: string;
|
|
5
|
+
tokens: number;
|
|
6
|
+
detail?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ContextUsageSnapshot {
|
|
9
|
+
providerId: string;
|
|
10
|
+
modelId: string;
|
|
11
|
+
contextWindow?: number;
|
|
12
|
+
usedTokens: number;
|
|
13
|
+
freeTokens?: number;
|
|
14
|
+
buckets: {
|
|
15
|
+
systemPrompt: ContextUsageBucket;
|
|
16
|
+
tools: ContextUsageBucket;
|
|
17
|
+
skills: ContextUsageBucket;
|
|
18
|
+
deferredTools: ContextUsageBucket;
|
|
19
|
+
other: ContextUsageBucket;
|
|
20
|
+
};
|
|
21
|
+
toolCount: number;
|
|
22
|
+
deferredToolCount: number;
|
|
23
|
+
skillCount: number;
|
|
24
|
+
messageCount: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function buildContextUsageSnapshot(input: {
|
|
27
|
+
providerId: string;
|
|
28
|
+
modelId: string;
|
|
29
|
+
messages: Message[];
|
|
30
|
+
toolEntries: ToolRegistryEntry[];
|
|
31
|
+
deferredToolEntries?: ToolRegistryEntry[];
|
|
32
|
+
skills: SkillSummary[];
|
|
33
|
+
}): ContextUsageSnapshot;
|
|
34
|
+
export declare function formatContextUsage(snapshot: ContextUsageSnapshot): string;
|
|
@@ -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.
|
|
@@ -246,6 +249,7 @@ async function main() {
|
|
|
246
249
|
budgetLedger,
|
|
247
250
|
skills: skillSummaries,
|
|
248
251
|
memoryPrompt,
|
|
252
|
+
fileStateTracker,
|
|
249
253
|
});
|
|
250
254
|
agentRef = agent;
|
|
251
255
|
if (sessionManager) {
|
|
@@ -330,8 +334,10 @@ async function main() {
|
|
|
330
334
|
console.log();
|
|
331
335
|
return;
|
|
332
336
|
}
|
|
333
|
-
|
|
334
|
-
const { runTui } =
|
|
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");
|
|
335
341
|
await runTui(agent, args, {
|
|
336
342
|
sessionManager,
|
|
337
343
|
createProvider,
|
|
@@ -366,7 +372,25 @@ async function readPipedStdin() {
|
|
|
366
372
|
process.stdin.resume();
|
|
367
373
|
});
|
|
368
374
|
}
|
|
369
|
-
main()
|
|
375
|
+
main()
|
|
376
|
+
.then(() => {
|
|
377
|
+
void exitAfterFlush(0);
|
|
378
|
+
})
|
|
379
|
+
.catch((err) => {
|
|
370
380
|
console.error(chalk.red(`Fatal error: ${err.message}`));
|
|
371
|
-
|
|
381
|
+
void exitAfterFlush(1);
|
|
372
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() {
|
|
@@ -101,23 +101,11 @@ export function createDefaultHooks() {
|
|
|
101
101
|
ctx.state.recentEditFailures = [];
|
|
102
102
|
ctx.state.editRetryReminderSent = false;
|
|
103
103
|
}
|
|
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
|
-
}
|
|
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.
|
|
121
109
|
if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
|
|
122
110
|
markCodeChanged(ctx.state);
|
|
123
111
|
}
|
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
|
}
|