@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
|
@@ -2,7 +2,10 @@ import { listBuiltinModels } from "./model-catalog.js";
|
|
|
2
2
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
3
3
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
4
4
|
const OPENAI_BETA_RESPONSES = "responses=experimental";
|
|
5
|
-
|
|
5
|
+
// OpenAI gates new codex models server-side by client_version (each model carries a
|
|
6
|
+
// `minimal_client_version`). Track a recent real Codex CLI release; override via env
|
|
7
|
+
// when OpenAI lifts the gate again before we cut a new release.
|
|
8
|
+
const CODEX_CLIENT_VERSION = process.env.BUBBLE_CODEX_CLIENT_VERSION?.trim() || "0.150.0";
|
|
6
9
|
const MODEL_DISCOVERY_PATHS = [
|
|
7
10
|
`/codex/models?client_version=${CODEX_CLIENT_VERSION}`,
|
|
8
11
|
"/models",
|
|
@@ -205,9 +208,9 @@ export async function fetchOpenAICodexModels(options) {
|
|
|
205
208
|
if (!response?.ok)
|
|
206
209
|
continue;
|
|
207
210
|
const payload = await response.json().catch(() => undefined);
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
return
|
|
211
|
+
const descriptors = extractCodexModelDescriptors(payload);
|
|
212
|
+
if (descriptors.length > 0) {
|
|
213
|
+
return sortCodexModelDescriptors(descriptors);
|
|
211
214
|
}
|
|
212
215
|
}
|
|
213
216
|
return [];
|
|
@@ -361,18 +364,54 @@ function resolveRelativeUrl(baseURL, path) {
|
|
|
361
364
|
const normalized = (baseURL.trim() || DEFAULT_CODEX_BASE_URL).replace(/\/+$/, "");
|
|
362
365
|
return `${normalized}${path}`;
|
|
363
366
|
}
|
|
364
|
-
|
|
365
|
-
|
|
367
|
+
const REASONING_EFFORTS = [
|
|
368
|
+
"off", "minimal", "low", "medium", "high", "xhigh", "max",
|
|
369
|
+
];
|
|
370
|
+
function extractCodexModelDescriptors(payload) {
|
|
371
|
+
const out = [];
|
|
366
372
|
const seen = new Set();
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
const isCodexId = (value) => typeof value === "string" && /^gpt-|^codex-/i.test(value);
|
|
374
|
+
const pickId = (record) => {
|
|
375
|
+
for (const key of ["slug", "id", "model_slug", "model"]) {
|
|
376
|
+
const v = record[key];
|
|
377
|
+
if (isCodexId(v))
|
|
378
|
+
return v;
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
};
|
|
382
|
+
const buildDescriptor = (record, id) => {
|
|
383
|
+
const desc = { id };
|
|
384
|
+
const displayName = record.display_name;
|
|
385
|
+
if (typeof displayName === "string" && displayName)
|
|
386
|
+
desc.displayName = displayName;
|
|
387
|
+
const ctx = record.context_window;
|
|
388
|
+
if (typeof ctx === "number" && ctx > 0)
|
|
389
|
+
desc.contextWindow = ctx;
|
|
390
|
+
const visibility = record.visibility;
|
|
391
|
+
if (typeof visibility === "string")
|
|
392
|
+
desc.visibility = visibility;
|
|
393
|
+
const minVer = record.minimal_client_version;
|
|
394
|
+
if (typeof minVer === "string")
|
|
395
|
+
desc.minimalClientVersion = minVer;
|
|
396
|
+
const levels = record.supported_reasoning_levels;
|
|
397
|
+
if (Array.isArray(levels)) {
|
|
398
|
+
const efforts = new Set(["off"]);
|
|
399
|
+
for (const level of levels) {
|
|
400
|
+
const effort = level?.effort;
|
|
401
|
+
if (typeof effort === "string" && REASONING_EFFORTS.includes(effort)) {
|
|
402
|
+
efforts.add(effort);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
desc.reasoningLevels = REASONING_EFFORTS.filter((e) => efforts.has(e));
|
|
406
|
+
}
|
|
407
|
+
const truncPolicy = record.truncation_policy;
|
|
408
|
+
if (truncPolicy && truncPolicy.mode === "tokens") {
|
|
409
|
+
const limit = truncPolicy.limit;
|
|
410
|
+
if (typeof limit === "number" && limit > 0) {
|
|
411
|
+
desc.toolOutputTokenLimit = limit;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return desc;
|
|
376
415
|
};
|
|
377
416
|
const visit = (value) => {
|
|
378
417
|
if (Array.isArray(value)) {
|
|
@@ -380,32 +419,42 @@ function extractModelIds(payload) {
|
|
|
380
419
|
visit(item);
|
|
381
420
|
return;
|
|
382
421
|
}
|
|
383
|
-
if (!value || typeof value !== "object")
|
|
384
|
-
maybeAdd(value);
|
|
422
|
+
if (!value || typeof value !== "object")
|
|
385
423
|
return;
|
|
386
|
-
}
|
|
387
424
|
const record = value;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
425
|
+
const id = pickId(record);
|
|
426
|
+
if (id && !seen.has(id)) {
|
|
427
|
+
seen.add(id);
|
|
428
|
+
out.push(buildDescriptor(record, id));
|
|
429
|
+
}
|
|
392
430
|
for (const child of Object.values(record)) {
|
|
393
|
-
if (child
|
|
431
|
+
if (child && typeof child === "object")
|
|
394
432
|
visit(child);
|
|
395
|
-
}
|
|
396
433
|
}
|
|
397
434
|
};
|
|
398
435
|
visit(payload);
|
|
399
|
-
return
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
// Extracts the family version from a codex slug (e.g. "gpt-5.5-codex" → 5005).
|
|
439
|
+
// Used so models from a newer family float to the top even before the static
|
|
440
|
+
// catalog knows about them.
|
|
441
|
+
function parseCodexFamilyRank(id) {
|
|
442
|
+
const match = id.match(/(\d+)\.(\d+)/);
|
|
443
|
+
if (!match)
|
|
444
|
+
return 0;
|
|
445
|
+
return parseInt(match[1], 10) * 1000 + parseInt(match[2], 10);
|
|
400
446
|
}
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
447
|
+
export function sortCodexModelDescriptors(descriptors) {
|
|
448
|
+
const preferred = new Map(getOpenAICodexFallbackModels().map((id, index) => [id, index]));
|
|
449
|
+
return [...descriptors].sort((left, right) => {
|
|
450
|
+
const leftFamily = parseCodexFamilyRank(left.id);
|
|
451
|
+
const rightFamily = parseCodexFamilyRank(right.id);
|
|
452
|
+
if (leftFamily !== rightFamily)
|
|
453
|
+
return rightFamily - leftFamily;
|
|
454
|
+
const leftRank = preferred.get(left.id) ?? Number.MAX_SAFE_INTEGER;
|
|
455
|
+
const rightRank = preferred.get(right.id) ?? Number.MAX_SAFE_INTEGER;
|
|
407
456
|
if (leftRank !== rightRank)
|
|
408
457
|
return leftRank - rightRank;
|
|
409
|
-
return left.localeCompare(right);
|
|
458
|
+
return left.id.localeCompare(right.id);
|
|
410
459
|
});
|
|
411
460
|
}
|
|
@@ -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, registerDynamicModelMetadata, } 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";
|
|
@@ -194,12 +194,28 @@ export class ProviderRegistry {
|
|
|
194
194
|
}
|
|
195
195
|
if (provider.id === "openai" && provider.authType === "oauth" && provider.apiKey) {
|
|
196
196
|
try {
|
|
197
|
-
const
|
|
197
|
+
const descriptors = await fetchOpenAICodexModels({
|
|
198
198
|
baseURL: provider.baseURL,
|
|
199
199
|
accessToken: provider.apiKey,
|
|
200
200
|
});
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
const visible = descriptors.filter((d) => d.visibility !== "hide");
|
|
202
|
+
if (visible.length > 0) {
|
|
203
|
+
for (const d of visible) {
|
|
204
|
+
const catalogEntry = getBuiltinModel("openai-codex", d.id);
|
|
205
|
+
registerDynamicModelMetadata({
|
|
206
|
+
id: d.id,
|
|
207
|
+
name: d.displayName || catalogEntry?.name || d.id,
|
|
208
|
+
providerId: "openai-codex",
|
|
209
|
+
reasoningLevels: d.reasoningLevels ?? catalogEntry?.reasoningLevels ?? ["off"],
|
|
210
|
+
contextWindow: d.contextWindow ?? catalogEntry?.contextWindow,
|
|
211
|
+
toolOutputTokenLimit: d.toolOutputTokenLimit ?? catalogEntry?.toolOutputTokenLimit,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return visible.map((d) => ({
|
|
215
|
+
id: d.id,
|
|
216
|
+
name: d.displayName || d.id,
|
|
217
|
+
providerId: provider.id,
|
|
218
|
+
}));
|
|
203
219
|
}
|
|
204
220
|
}
|
|
205
221
|
catch {
|
|
@@ -232,8 +248,8 @@ export function decodeModel(value) {
|
|
|
232
248
|
}
|
|
233
249
|
/** Strip provider prefix for concise display. */
|
|
234
250
|
export function displayModel(model) {
|
|
235
|
-
const { modelId } = decodeModel(model);
|
|
236
|
-
return modelId;
|
|
251
|
+
const { providerId, modelId } = decodeModel(model);
|
|
252
|
+
return providerId ? getBuiltinModel(providerId, modelId)?.name ?? modelId : modelId;
|
|
237
253
|
}
|
|
238
254
|
/** Normalize user input to provider:model format when possible. */
|
|
239
255
|
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
|
}
|
|
@@ -3,6 +3,13 @@ export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
|
|
|
3
3
|
const MOONSHOT_PROVIDER_IDS = new Set(["moonshot-cn", "moonshot-intl", "kimi-for-coding"]);
|
|
4
4
|
const KIMI_K25_FAMILY = new Set(["kimi-k2.5", "k2.6-code-preview", "kimi-k2.6"]);
|
|
5
5
|
const KIMI_THINKING_FAMILY = new Set(["kimi-k2-thinking", "kimi-k2-thinking-turbo"]);
|
|
6
|
+
const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
|
|
7
|
+
function isFireworksKimi(providerId, modelId) {
|
|
8
|
+
const model = modelId.toLowerCase();
|
|
9
|
+
return providerId === "fireworks" && (model.includes("kimi")
|
|
10
|
+
|| model.includes("k2p6")
|
|
11
|
+
|| model === "k2.6");
|
|
12
|
+
}
|
|
6
13
|
export function resolveProviderRequestConfig(providerId, modelId, requestedLevel) {
|
|
7
14
|
const supportedLevels = getAvailableThinkingLevels(providerId, modelId);
|
|
8
15
|
const effectiveThinkingLevel = normalizeThinkingLevel(requestedLevel, supportedLevels);
|
|
@@ -11,6 +18,14 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
|
|
|
11
18
|
if (providerId === "openai-codex") {
|
|
12
19
|
return { effectiveThinkingLevel };
|
|
13
20
|
}
|
|
21
|
+
if (isFireworksKimi(providerId, modelId)) {
|
|
22
|
+
return {
|
|
23
|
+
effectiveThinkingLevel,
|
|
24
|
+
reasoningContentEcho: "none",
|
|
25
|
+
parallelToolCalls: false,
|
|
26
|
+
maxTokens: KIMI_K26_DEFAULT_MAX_TOKENS,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
14
29
|
if (providerId === "deepseek" && (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro")) {
|
|
15
30
|
return {
|
|
16
31
|
effectiveThinkingLevel,
|
package/dist/provider.d.ts
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
|
|
5
5
|
*/
|
|
6
6
|
import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
|
|
7
|
-
type ReasoningContentEcho = "tool_calls" | "all";
|
|
7
|
+
type ReasoningContentEcho = "tool_calls" | "all" | "none";
|
|
8
8
|
export type ToolArgsMergeMode = "delta" | "snapshot";
|
|
9
9
|
export interface TranslateOpenAIStreamOptions {
|
|
10
10
|
toolArgsMergeMode?: ToolArgsMergeMode;
|
|
11
|
+
reasoningMergeMode?: ToolArgsMergeMode;
|
|
12
|
+
debugProviderId?: string;
|
|
13
|
+
debugModelId?: string;
|
|
11
14
|
}
|
|
12
15
|
export declare function toChatCompletionsMessage(message: ProviderMessage, options?: {
|
|
13
16
|
reasoningContentEcho?: ReasoningContentEcho;
|
package/dist/provider.js
CHANGED
|
@@ -8,6 +8,7 @@ import { appendFileSync } from "node:fs";
|
|
|
8
8
|
import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
|
|
9
9
|
import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
|
|
10
10
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
11
|
+
import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
|
|
11
12
|
// Diagnostic logger for tool-args byte-loss investigation. Activate with
|
|
12
13
|
// BUBBLE_DEBUG_TOOL_ARGS=/path/to/log.jsonl (any writable path)
|
|
13
14
|
// Each line is a JSON record describing a transition. When debugging is off,
|
|
@@ -101,6 +102,12 @@ export function createProviderInstance(options) {
|
|
|
101
102
|
if (requestConfig.extraBody) {
|
|
102
103
|
Object.assign(body, requestConfig.extraBody);
|
|
103
104
|
}
|
|
105
|
+
if (tools && tools.length > 0 && requestConfig.parallelToolCalls !== undefined) {
|
|
106
|
+
body.parallel_tool_calls = requestConfig.parallelToolCalls;
|
|
107
|
+
}
|
|
108
|
+
if (requestConfig.maxTokens !== undefined) {
|
|
109
|
+
body.max_tokens = requestConfig.maxTokens;
|
|
110
|
+
}
|
|
104
111
|
if (requestConfig.reasoningEffort && requestConfig.reasoningEffort !== "off") {
|
|
105
112
|
body.reasoning = { enabled: true };
|
|
106
113
|
}
|
|
@@ -109,6 +116,9 @@ export function createProviderInstance(options) {
|
|
|
109
116
|
}));
|
|
110
117
|
yield* translateOpenAIStream(stream, {
|
|
111
118
|
toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL),
|
|
119
|
+
reasoningMergeMode: resolveReasoningMergeMode(options.providerId || "", options.baseURL),
|
|
120
|
+
debugProviderId: options.providerId || "",
|
|
121
|
+
debugModelId: chatOptions.model,
|
|
112
122
|
});
|
|
113
123
|
yield { type: "done" };
|
|
114
124
|
}
|
|
@@ -126,6 +136,9 @@ export function createProviderInstance(options) {
|
|
|
126
136
|
if (requestConfig.extraBody) {
|
|
127
137
|
Object.assign(body, requestConfig.extraBody);
|
|
128
138
|
}
|
|
139
|
+
if (requestConfig.maxTokens !== undefined) {
|
|
140
|
+
body.max_tokens = requestConfig.maxTokens;
|
|
141
|
+
}
|
|
129
142
|
if (requestConfig.reasoningEffort && requestConfig.reasoningEffort !== "off") {
|
|
130
143
|
body.reasoning = { enabled: true };
|
|
131
144
|
}
|
|
@@ -188,6 +201,13 @@ function resolveToolArgsMergeMode(providerId, baseURL) {
|
|
|
188
201
|
return "snapshot";
|
|
189
202
|
return "delta";
|
|
190
203
|
}
|
|
204
|
+
function resolveReasoningMergeMode(providerId, baseURL) {
|
|
205
|
+
const id = providerId.toLowerCase();
|
|
206
|
+
const url = baseURL.toLowerCase();
|
|
207
|
+
if (id === "fireworks" || url.includes("fireworks.ai"))
|
|
208
|
+
return "snapshot";
|
|
209
|
+
return "delta";
|
|
210
|
+
}
|
|
191
211
|
function extractBalancedJson(s, start) {
|
|
192
212
|
if (s[start] !== "{")
|
|
193
213
|
return null;
|
|
@@ -232,6 +252,9 @@ export async function* translateOpenAIStream(stream, options = {}) {
|
|
|
232
252
|
const toolCalls = new Map();
|
|
233
253
|
const textFilter = createProviderProtocolArtifactFilter();
|
|
234
254
|
const toolArgsMergeMode = options.toolArgsMergeMode ?? "delta";
|
|
255
|
+
const reasoningMergeMode = options.reasoningMergeMode ?? "delta";
|
|
256
|
+
let reasoningBuffer = "";
|
|
257
|
+
let rawChunkSeq = 0;
|
|
235
258
|
// DeepSeek (and some inference re-hosts) sometimes deliver reasoning twice:
|
|
236
259
|
// once via a dedicated `reasoning_content` / `thinking` field, and again
|
|
237
260
|
// embedded as `<think>...</think>` inside `delta.content`. Track whether we
|
|
@@ -277,8 +300,21 @@ export async function* translateOpenAIStream(stream, options = {}) {
|
|
|
277
300
|
}
|
|
278
301
|
}
|
|
279
302
|
for await (const chunk of stream) {
|
|
303
|
+
rawChunkSeq += 1;
|
|
280
304
|
const delta = chunk.choices?.[0]?.delta;
|
|
281
305
|
const usage = chunk.usage;
|
|
306
|
+
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
307
|
+
debugReasoningStream({
|
|
308
|
+
stage: "provider_raw",
|
|
309
|
+
providerId: options.debugProviderId,
|
|
310
|
+
modelId: options.debugModelId,
|
|
311
|
+
chunkSeq: rawChunkSeq,
|
|
312
|
+
finishReason,
|
|
313
|
+
content: summarizeDebugText(delta?.content),
|
|
314
|
+
reasoning: summarizeDebugText(delta?.reasoning),
|
|
315
|
+
thinking: summarizeDebugText(delta?.thinking),
|
|
316
|
+
reasoningContent: summarizeDebugText(delta?.reasoning_content),
|
|
317
|
+
});
|
|
282
318
|
if (usage) {
|
|
283
319
|
yield {
|
|
284
320
|
type: "usage",
|
|
@@ -294,16 +330,53 @@ export async function* translateOpenAIStream(stream, options = {}) {
|
|
|
294
330
|
},
|
|
295
331
|
};
|
|
296
332
|
}
|
|
297
|
-
const
|
|
333
|
+
const reasoningField = delta?.reasoning !== undefined
|
|
334
|
+
? "reasoning"
|
|
335
|
+
: delta?.thinking !== undefined
|
|
336
|
+
? "thinking"
|
|
337
|
+
: delta?.reasoning_content !== undefined
|
|
338
|
+
? "reasoning_content"
|
|
339
|
+
: undefined;
|
|
340
|
+
const reasoning = reasoningField ? delta[reasoningField] : undefined;
|
|
298
341
|
if (reasoning) {
|
|
299
342
|
hasDedicatedReasoningChannel = true;
|
|
300
|
-
|
|
343
|
+
const merged = mergeStreamingText(reasoningBuffer, reasoning, reasoningMergeMode);
|
|
344
|
+
reasoningBuffer = merged.args;
|
|
345
|
+
debugReasoningStream({
|
|
346
|
+
stage: "provider_emit",
|
|
347
|
+
providerId: options.debugProviderId,
|
|
348
|
+
modelId: options.debugModelId,
|
|
349
|
+
chunkSeq: rawChunkSeq,
|
|
350
|
+
source: reasoningField,
|
|
351
|
+
mergeMode: reasoningMergeMode,
|
|
352
|
+
suppressed: !merged.delta,
|
|
353
|
+
emitted: summarizeDebugText(merged.delta),
|
|
354
|
+
buffer: summarizeDebugText(reasoningBuffer),
|
|
355
|
+
});
|
|
356
|
+
if (merged.delta) {
|
|
357
|
+
yield { type: "reasoning_delta", content: merged.delta };
|
|
358
|
+
}
|
|
301
359
|
}
|
|
302
360
|
if (delta?.content) {
|
|
303
361
|
const thinkMatch = delta.content.match(/<think>([\s\S]*?)<\/think>/);
|
|
304
362
|
if (thinkMatch) {
|
|
305
363
|
if (thinkMatch[1] && !hasDedicatedReasoningChannel) {
|
|
306
|
-
|
|
364
|
+
const merged = mergeStreamingText(reasoningBuffer, thinkMatch[1], reasoningMergeMode);
|
|
365
|
+
reasoningBuffer = merged.args;
|
|
366
|
+
debugReasoningStream({
|
|
367
|
+
stage: "provider_emit",
|
|
368
|
+
providerId: options.debugProviderId,
|
|
369
|
+
modelId: options.debugModelId,
|
|
370
|
+
chunkSeq: rawChunkSeq,
|
|
371
|
+
source: "content_think",
|
|
372
|
+
mergeMode: reasoningMergeMode,
|
|
373
|
+
suppressed: !merged.delta,
|
|
374
|
+
emitted: summarizeDebugText(merged.delta),
|
|
375
|
+
buffer: summarizeDebugText(reasoningBuffer),
|
|
376
|
+
});
|
|
377
|
+
if (merged.delta) {
|
|
378
|
+
yield { type: "reasoning_delta", content: merged.delta };
|
|
379
|
+
}
|
|
307
380
|
}
|
|
308
381
|
const remaining = delta.content.replace(/<think>[\s\S]*?<\/think>/, "");
|
|
309
382
|
const cleaned = textFilter.push(remaining);
|
|
@@ -348,7 +421,6 @@ export async function* translateOpenAIStream(stream, options = {}) {
|
|
|
348
421
|
}
|
|
349
422
|
}
|
|
350
423
|
}
|
|
351
|
-
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
352
424
|
if (finishReason === "tool_calls") {
|
|
353
425
|
yield* flushToolCalls();
|
|
354
426
|
}
|
|
@@ -386,3 +458,16 @@ function mergeToolArgumentDelta(current, incoming, mode) {
|
|
|
386
458
|
debugToolArgs({ stage: "merge", branch: mode === "delta" ? "delta-append" : "snapshot-fallback-concat", current, incoming, args: current + incoming, delta: incoming });
|
|
387
459
|
return { args: current + incoming, delta: incoming };
|
|
388
460
|
}
|
|
461
|
+
function mergeStreamingText(current, incoming, mode) {
|
|
462
|
+
if (!current)
|
|
463
|
+
return { args: incoming, delta: incoming };
|
|
464
|
+
if (!incoming)
|
|
465
|
+
return { args: current, delta: "" };
|
|
466
|
+
if (mode === "snapshot") {
|
|
467
|
+
if (incoming === current)
|
|
468
|
+
return { args: current, delta: "" };
|
|
469
|
+
if (incoming.startsWith(current))
|
|
470
|
+
return { args: incoming, delta: incoming.slice(current.length) };
|
|
471
|
+
}
|
|
472
|
+
return { args: current + incoming, delta: incoming };
|
|
473
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface DebugTextSummary {
|
|
2
|
+
length: number;
|
|
3
|
+
hash: string;
|
|
4
|
+
preview?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function summarizeDebugText(value: unknown): DebugTextSummary | undefined;
|
|
7
|
+
export declare function debugReasoningStream(event: Record<string, unknown>): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
const DEBUG_PATH = process.env.BUBBLE_DEBUG_REASONING_STREAM?.trim();
|
|
5
|
+
const INCLUDE_PREVIEW = process.env.BUBBLE_DEBUG_REASONING_PREVIEW !== "0";
|
|
6
|
+
const PREVIEW_CHARS = 180;
|
|
7
|
+
let sequence = 0;
|
|
8
|
+
export function summarizeDebugText(value) {
|
|
9
|
+
if (!DEBUG_PATH)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (typeof value !== "string" || value.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
const hash = createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
14
|
+
const summary = { length: value.length, hash };
|
|
15
|
+
if (INCLUDE_PREVIEW) {
|
|
16
|
+
summary.preview = value.replace(/\s+/g, " ").slice(0, PREVIEW_CHARS);
|
|
17
|
+
}
|
|
18
|
+
return summary;
|
|
19
|
+
}
|
|
20
|
+
export function debugReasoningStream(event) {
|
|
21
|
+
if (!DEBUG_PATH)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
mkdirSync(dirname(DEBUG_PATH), { recursive: true });
|
|
25
|
+
appendFileSync(DEBUG_PATH, JSON.stringify({ t: Date.now(), seq: ++sequence, ...event }) + "\n", "utf-8");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Debug logging must never affect an agent run.
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/session-log.js
CHANGED
|
@@ -72,6 +72,9 @@ export class SessionLog {
|
|
|
72
72
|
getTodos() {
|
|
73
73
|
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
74
74
|
const entry = this.entries[i];
|
|
75
|
+
if (entry.type === "marker" && entry.kind === "conversation_clear") {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
75
78
|
if (entry.type === "todos_snapshot") {
|
|
76
79
|
return entry.todos.map((todo) => ({ ...todo }));
|
|
77
80
|
}
|
|
@@ -92,20 +95,28 @@ export class SessionLog {
|
|
|
92
95
|
toMessages() {
|
|
93
96
|
const messages = [];
|
|
94
97
|
let latestSummaryIndex = -1;
|
|
98
|
+
let latestClearIndex = -1;
|
|
95
99
|
for (let index = this.entries.length - 1; index >= 0; index--) {
|
|
96
100
|
if (this.entries[index].type === "summary") {
|
|
97
101
|
latestSummaryIndex = index;
|
|
98
102
|
break;
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
|
-
|
|
105
|
+
for (let index = this.entries.length - 1; index >= 0; index--) {
|
|
106
|
+
const entry = this.entries[index];
|
|
107
|
+
if (entry.type === "marker" && entry.kind === "conversation_clear") {
|
|
108
|
+
latestClearIndex = index;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (latestSummaryIndex > latestClearIndex) {
|
|
102
113
|
const summary = this.entries[latestSummaryIndex];
|
|
103
114
|
messages.push({
|
|
104
115
|
role: "system",
|
|
105
116
|
content: `Previous conversation summary: ${summary.summary}`,
|
|
106
117
|
});
|
|
107
118
|
}
|
|
108
|
-
const startIndex = latestSummaryIndex
|
|
119
|
+
const startIndex = Math.max(latestSummaryIndex > latestClearIndex ? latestSummaryIndex + 1 : 0, latestClearIndex + 1);
|
|
109
120
|
for (let index = startIndex; index < this.entries.length; index++) {
|
|
110
121
|
const entry = this.entries[index];
|
|
111
122
|
switch (entry.type) {
|
package/dist/session-types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface SessionMetadata {
|
|
|
5
5
|
reasoningEffort?: ThinkingLevel;
|
|
6
6
|
cwd?: string;
|
|
7
7
|
}
|
|
8
|
-
export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch";
|
|
8
|
+
export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch" | "conversation_clear";
|
|
9
9
|
interface BaseSessionLogEntry {
|
|
10
10
|
id: string;
|
|
11
11
|
timestamp: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UserConfig, maskKey } from "../config.js";
|
|
2
|
+
import { formatContextUsage } from "../context/usage.js";
|
|
2
3
|
import { formatDiagnostics } from "../lsp/index.js";
|
|
3
4
|
import { normalizeNameForMCP } from "../mcp/name.js";
|
|
4
5
|
import { parseRule } from "../permissions/rule.js";
|
|
@@ -70,6 +71,29 @@ function syncSystemPrompt(ctx, model) {
|
|
|
70
71
|
memoryPrompt: buildMemoryPrompt(ctx.cwd),
|
|
71
72
|
}));
|
|
72
73
|
}
|
|
74
|
+
function formatMcpContextStatus(ctx) {
|
|
75
|
+
const states = ctx.mcpManager?.getStates() ?? [];
|
|
76
|
+
const lines = ["MCP"];
|
|
77
|
+
if (!ctx.mcpManager || states.length === 0) {
|
|
78
|
+
lines.push("- No MCP servers configured for this session.");
|
|
79
|
+
lines.push("- Context impact: none.");
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
for (const state of states) {
|
|
83
|
+
if (state.status.kind === "connected") {
|
|
84
|
+
lines.push(`- ${state.name} (${state.scope}): connected · ${state.status.tools.length} deferred tool${state.status.tools.length === 1 ? "" : "s"} · ${state.status.prompts.length} prompt${state.status.prompts.length === 1 ? "" : "s"}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (state.status.kind === "failed") {
|
|
88
|
+
lines.push(`- ${state.name} (${state.scope}): failed · ${state.status.error}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
lines.push(`- ${state.name} (${state.scope}): ${state.status.kind}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push("- Context impact: MCP tool schemas are deferred. The prompt pays only a small deferred-tool reminder until tool_search unlocks a tool; unlocked MCP schemas then count under Tools.");
|
|
94
|
+
lines.push("- MCP prompts are slash commands; they do not enter context until invoked.");
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
73
97
|
function switchToProviderModel(providerId, modelId, ctx, thinkingLevel) {
|
|
74
98
|
const provider = ctx.registry.getConfigured().find((item) => item.id === providerId);
|
|
75
99
|
if (!provider?.apiKey) {
|
|
@@ -270,6 +294,13 @@ const builtinSlashCommandEntries = [
|
|
|
270
294
|
return handleMemoryCommand(args, ctx);
|
|
271
295
|
},
|
|
272
296
|
},
|
|
297
|
+
{
|
|
298
|
+
name: "context",
|
|
299
|
+
description: "Show current context window usage and breakdown",
|
|
300
|
+
async handler(args, ctx) {
|
|
301
|
+
return `${formatContextUsage(ctx.agent.getContextUsageSnapshot())}\n\n${formatMcpContextStatus(ctx)}`;
|
|
302
|
+
},
|
|
303
|
+
},
|
|
273
304
|
{
|
|
274
305
|
name: "quit",
|
|
275
306
|
description: "Exit the application",
|
|
@@ -277,13 +308,40 @@ const builtinSlashCommandEntries = [
|
|
|
277
308
|
ctx.exit();
|
|
278
309
|
},
|
|
279
310
|
},
|
|
311
|
+
{
|
|
312
|
+
name: "theme",
|
|
313
|
+
description: "Switch the color theme. Usage: /theme [auto|light|dark]",
|
|
314
|
+
async handler(args, ctx) {
|
|
315
|
+
if (!ctx.setThemeMode || !ctx.getThemeMode || !ctx.getResolvedTheme) {
|
|
316
|
+
return "Theme switching is only available inside the TUI.";
|
|
317
|
+
}
|
|
318
|
+
const arg = args.trim().toLowerCase();
|
|
319
|
+
if (!arg) {
|
|
320
|
+
const order = ["auto", "light", "dark"];
|
|
321
|
+
const current = ctx.getThemeMode();
|
|
322
|
+
const next = order[(order.indexOf(current) + 1) % order.length];
|
|
323
|
+
ctx.setThemeMode(next);
|
|
324
|
+
const resolved = next === "auto" ? ctx.getResolvedTheme() : next;
|
|
325
|
+
return `Theme: ${next}${next === "auto" ? ` (resolved to ${resolved})` : ""}`;
|
|
326
|
+
}
|
|
327
|
+
if (arg !== "auto" && arg !== "light" && arg !== "dark") {
|
|
328
|
+
return "Usage: /theme [auto|light|dark]";
|
|
329
|
+
}
|
|
330
|
+
ctx.setThemeMode(arg);
|
|
331
|
+
const resolved = arg === "auto" ? ctx.getResolvedTheme() : arg;
|
|
332
|
+
return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
|
|
333
|
+
},
|
|
334
|
+
},
|
|
280
335
|
{
|
|
281
336
|
name: "clear",
|
|
282
337
|
description: "Clear the current conversation history",
|
|
283
338
|
async handler(args, ctx) {
|
|
339
|
+
ctx.agent.messages = ctx.agent.messages.filter((m) => m.role === "system" || m.role === "meta");
|
|
340
|
+
ctx.sessionManager?.appendMarker("conversation_clear", "");
|
|
341
|
+
if (ctx.agent.getTodos().length > 0) {
|
|
342
|
+
ctx.agent.setTodos([]);
|
|
343
|
+
}
|
|
284
344
|
ctx.clearMessages();
|
|
285
|
-
ctx.agent.messages = ctx.agent.messages.filter((m) => m.role === "system");
|
|
286
|
-
return "Conversation cleared.";
|
|
287
345
|
},
|
|
288
346
|
},
|
|
289
347
|
{
|
|
@@ -8,6 +8,7 @@ import type { SettingsManager } from "../permissions/settings.js";
|
|
|
8
8
|
import type { McpManager } from "../mcp/manager.js";
|
|
9
9
|
import type { LspService } from "../lsp/index.js";
|
|
10
10
|
import type { MemoryScope } from "../memory/index.js";
|
|
11
|
+
import type { ThemeMode } from "../config.js";
|
|
11
12
|
export interface SlashCommandContext {
|
|
12
13
|
agent: Agent;
|
|
13
14
|
addMessage: (role: "user" | "assistant" | "error", content: string) => void;
|
|
@@ -27,6 +28,12 @@ export interface SlashCommandContext {
|
|
|
27
28
|
runMemoryCompaction?: () => Promise<string>;
|
|
28
29
|
runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
|
|
29
30
|
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
31
|
+
/** Get the current theme mode (auto/light/dark) — undefined when running in non-TUI contexts. */
|
|
32
|
+
getThemeMode?: () => ThemeMode;
|
|
33
|
+
/** Get the resolved active theme (always light or dark) — undefined when running in non-TUI contexts. */
|
|
34
|
+
getResolvedTheme?: () => "light" | "dark";
|
|
35
|
+
/** Persist a new theme mode AND apply it to the running TUI. */
|
|
36
|
+
setThemeMode?: (mode: ThemeMode) => void;
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
39
|
* Return types for a slash command handler:
|