@bubblebrain-ai/bubble 0.0.48 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/agent/abort-errors.d.ts +4 -0
- package/dist/agent/abort-errors.js +30 -0
- package/dist/agent/provider-fingerprint.d.ts +6 -0
- package/dist/agent/provider-fingerprint.js +89 -0
- package/dist/agent/resident-history.d.ts +7 -0
- package/dist/agent/resident-history.js +61 -0
- package/dist/agent/transcript-helpers.d.ts +20 -0
- package/dist/agent/transcript-helpers.js +62 -0
- package/dist/agent/update-queue.d.ts +13 -0
- package/dist/agent/update-queue.js +69 -0
- package/dist/agent.d.ts +33 -16
- package/dist/agent.js +90 -300
- package/dist/external-runtime/grok-runtime.js +1 -1
- package/dist/feishu/agent-host/run-driver.js +1 -1
- package/dist/lsp/service.js +2 -2
- package/dist/model-catalog.d.ts +13 -0
- package/dist/model-catalog.js +52 -8
- package/dist/permissions/settings.d.ts +0 -1
- package/dist/permissions/settings.js +0 -2
- package/dist/provider-grok.d.ts +16 -18
- package/dist/provider-grok.js +28 -20
- package/dist/provider-registry.d.ts +9 -0
- package/dist/provider-registry.js +109 -6
- package/dist/provider-transform.js +7 -8
- package/dist/provider.js +11 -0
- package/dist/session-log.js +1 -0
- package/dist/slash-commands/commands.js +13 -9
- package/dist/tools/grep.js +1 -1
- package/dist/tui-ink/app-helpers.d.ts +43 -0
- package/dist/tui-ink/app-helpers.js +150 -0
- package/dist/tui-ink/app.d.ts +4 -11
- package/dist/tui-ink/app.js +104 -635
- package/dist/tui-ink/command-palette.d.ts +32 -0
- package/dist/tui-ink/command-palette.js +158 -0
- package/dist/tui-ink/display-history.d.ts +2 -0
- package/dist/tui-ink/display-reconstruct.d.ts +3 -0
- package/dist/tui-ink/display-reconstruct.js +82 -0
- package/dist/tui-ink/footer.d.ts +14 -3
- package/dist/tui-ink/footer.js +19 -5
- package/dist/tui-ink/rewind-picker.d.ts +8 -0
- package/dist/tui-ink/rewind-picker.js +102 -0
- package/dist/tui-ink/session-picker.js +1 -1
- package/dist/tui-ink/stats-panel.d.ts +11 -0
- package/dist/tui-ink/stats-panel.js +65 -0
- package/dist/tui-ink/waiting-indicator.d.ts +12 -0
- package/dist/tui-ink/waiting-indicator.js +129 -0
- package/dist/tui-ink/wordmark.d.ts +8 -0
- package/dist/tui-ink/wordmark.js +103 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
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 { compactCurrentTurnToolGroups, compactMessages
|
|
5
|
+
import { compactCurrentTurnToolGroups, compactMessages } from "./context/compact.js";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { compactMessagesWithLLM } from "./context/compact-llm.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getContextBudget } from "./context/budget.js";
|
|
9
9
|
import { buildContextUsageSnapshot } from "./context/usage.js";
|
|
10
10
|
import { isContextOverflowError } from "./context/overflow.js";
|
|
11
11
|
import { computeRetryDelayMs, isProviderStreamInterruption, MAX_STREAM_INTERRUPTION_RETRIES, sleepBeforeRetry, } from "./network/retry.js";
|
|
@@ -21,7 +21,12 @@ import { DEFAULT_AGENT_ROUTING, sanitizeAgentRouting } from "./agent/routing-cat
|
|
|
21
21
|
import { SubagentRouter } from "./agent/subagent/router.js";
|
|
22
22
|
import { normalizeWaitTimeout, SubagentRuntime } from "./agent/subagent/runtime.js";
|
|
23
23
|
import { WorkflowLedger } from "./agent/workflow/runs.js";
|
|
24
|
-
import {
|
|
24
|
+
import { EMPTY_ASSISTANT_FALLBACK, throwIfAborted, isAbortLikeError, isAbortError } from "./agent/abort-errors.js";
|
|
25
|
+
import { appendProviderContentBlock, buildProviderRequestFingerprint } from "./agent/provider-fingerprint.js";
|
|
26
|
+
import { countUserTurns, estimateResidentChars, estimateToolPayloadChars, getCurrentHeapUsed, RESIDENT_HISTORY_HEAP_HARD_LIMIT, RESIDENT_HISTORY_KEEP_RECENT_TURNS } from "./agent/resident-history.js";
|
|
27
|
+
import { cancelledToolResult, createModelInterruptedMessage, findMissingRequiredArgs, isSubagentLifecycleTool, lastProviderMessage, shouldAppendModelInterruptedBoundary } from "./agent/transcript-helpers.js";
|
|
28
|
+
import { createUpdateQueue } from "./agent/update-queue.js";
|
|
29
|
+
export { createUpdateQueue } from "./agent/update-queue.js";
|
|
25
30
|
import { isHiddenToolResult } from "./agent/tool-visibility.js";
|
|
26
31
|
import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMetadata, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "./agent/internal-reminder-sanitizer.js";
|
|
27
32
|
import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
|
|
@@ -29,8 +34,6 @@ import { buildToolPromptOptions } from "./tools/prompt-metadata.js";
|
|
|
29
34
|
import { stopAutoServersForSession } from "./tools/server-manager.js";
|
|
30
35
|
import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceMessage, summarizeTraceToolResult, summarizeTraceValue, traceEvent, } from "./debug-trace.js";
|
|
31
36
|
const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
|
|
32
|
-
const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
|
|
33
|
-
const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
|
|
34
37
|
const MAX_EMPTY_ASSISTANT_RECOVERIES = 1;
|
|
35
38
|
const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained no user-visible assistant content and no tool calls. " +
|
|
36
39
|
"Respond now with a concise, user-visible answer, or call an available tool if more work is required. " +
|
|
@@ -74,7 +77,23 @@ function agentEventFromHookProgress(event) {
|
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
79
|
export class Agent {
|
|
77
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Backing store for the public `messages` accessor. All whole-array
|
|
82
|
+
* rewrites (clear/compact/rewind/session switch/subagent fork) go through
|
|
83
|
+
* the setter, which notifies `onContextChanged` subscribers — that push
|
|
84
|
+
* channel is what keeps derived UI (e.g. the footer usage readout) in sync
|
|
85
|
+
* without per-path refresh calls that are easy to forget.
|
|
86
|
+
*/
|
|
87
|
+
_messages = [];
|
|
88
|
+
contextChangedListeners = new Set();
|
|
89
|
+
notifyingContextChange = false;
|
|
90
|
+
get messages() {
|
|
91
|
+
return this._messages;
|
|
92
|
+
}
|
|
93
|
+
set messages(value) {
|
|
94
|
+
this._messages = value;
|
|
95
|
+
this.notifyContextChanged();
|
|
96
|
+
}
|
|
78
97
|
provider;
|
|
79
98
|
sessionID;
|
|
80
99
|
/**
|
|
@@ -216,7 +235,7 @@ export class Agent {
|
|
|
216
235
|
persistDir,
|
|
217
236
|
});
|
|
218
237
|
if (options.systemPrompt) {
|
|
219
|
-
this.
|
|
238
|
+
this._messages.push({ role: "system", content: options.systemPrompt });
|
|
220
239
|
}
|
|
221
240
|
for (const tool of options.tools) {
|
|
222
241
|
this.tools.set(tool.name, tool);
|
|
@@ -345,6 +364,9 @@ export class Agent {
|
|
|
345
364
|
// Rendered through the live accessor (design §1.5), so every host-
|
|
346
365
|
// triggered prompt rebuild picks up the current catalog.
|
|
347
366
|
modelRoutingPrompt: this.buildModelRoutingPromptSection(),
|
|
367
|
+
// Model switches rebuild the prompt from these options; without this the
|
|
368
|
+
// persistent memory (buildMemoryPrompt) is silently dropped on switch.
|
|
369
|
+
memoryPrompt: this.memoryPrompt,
|
|
348
370
|
};
|
|
349
371
|
}
|
|
350
372
|
/** Current routing menu (design §4); undefined when no accessor is wired. */
|
|
@@ -359,6 +381,46 @@ export class Agent {
|
|
|
359
381
|
renderModelRoutingPromptFor(parent) {
|
|
360
382
|
return this.router.promptSectionFor(parent);
|
|
361
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Push subscription for derived-context consumers (TUI footer, hosts).
|
|
386
|
+
* Fires whenever resident context mutates in a way that changes a
|
|
387
|
+
* `getContextUsageSnapshot()` reading: whole-array message rewrites,
|
|
388
|
+
* appended messages, system-prompt replacement, or model/provider switches
|
|
389
|
+
* (window size / estimation basis). Returns an unsubscribe function.
|
|
390
|
+
*/
|
|
391
|
+
onContextChanged(listener) {
|
|
392
|
+
this.contextChangedListeners.add(listener);
|
|
393
|
+
return () => {
|
|
394
|
+
this.contextChangedListeners.delete(listener);
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
notifyContextChanged() {
|
|
398
|
+
if (this.notifyingContextChange)
|
|
399
|
+
return;
|
|
400
|
+
this.notifyingContextChange = true;
|
|
401
|
+
try {
|
|
402
|
+
for (const listener of [...this.contextChangedListeners]) {
|
|
403
|
+
try {
|
|
404
|
+
listener();
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
// A broken subscriber must never corrupt agent state mid-mutation,
|
|
408
|
+
// but leave a trace — otherwise a throwing consumer (e.g. the footer
|
|
409
|
+
// readout hitting a pathological message) fails silently forever.
|
|
410
|
+
traceEvent("context_listener_error", {
|
|
411
|
+
error: error instanceof Error ? error.message : String(error),
|
|
412
|
+
}, {
|
|
413
|
+
sessionFile: this.sessionID,
|
|
414
|
+
provider: this._providerId || "none",
|
|
415
|
+
model: this.apiModel || "none",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
this.notifyingContextChange = false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
362
424
|
getContextUsageSnapshot() {
|
|
363
425
|
return buildContextUsageSnapshot({
|
|
364
426
|
providerId: this.providerId,
|
|
@@ -431,13 +493,21 @@ export class Agent {
|
|
|
431
493
|
return this._model;
|
|
432
494
|
}
|
|
433
495
|
set model(value) {
|
|
496
|
+
if (this._model === value)
|
|
497
|
+
return;
|
|
434
498
|
this._model = value;
|
|
499
|
+
// Context window (and thus usage readings) is model-derived.
|
|
500
|
+
this.notifyContextChanged();
|
|
435
501
|
}
|
|
436
502
|
get providerId() {
|
|
437
503
|
return this._providerId;
|
|
438
504
|
}
|
|
439
505
|
set providerId(value) {
|
|
506
|
+
if (this._providerId === value)
|
|
507
|
+
return;
|
|
440
508
|
this._providerId = value;
|
|
509
|
+
// Token estimation basis is provider-specific.
|
|
510
|
+
this.notifyContextChanged();
|
|
441
511
|
}
|
|
442
512
|
get apiModel() {
|
|
443
513
|
if (this._model.includes(":")) {
|
|
@@ -501,10 +571,14 @@ export class Agent {
|
|
|
501
571
|
setSystemPrompt(prompt) {
|
|
502
572
|
const systemMessage = { role: "system", content: prompt };
|
|
503
573
|
if (this.messages[0]?.role === "system") {
|
|
574
|
+
if (this.messages[0].content === prompt)
|
|
575
|
+
return;
|
|
504
576
|
this.messages[0] = systemMessage;
|
|
577
|
+
this.notifyContextChanged();
|
|
505
578
|
return;
|
|
506
579
|
}
|
|
507
580
|
this.messages.unshift(systemMessage);
|
|
581
|
+
this.notifyContextChanged();
|
|
508
582
|
}
|
|
509
583
|
async *run(userInput, cwd, options = {}) {
|
|
510
584
|
const abortSignal = options.abortSignal;
|
|
@@ -705,6 +779,7 @@ export class Agent {
|
|
|
705
779
|
const textSanitizer = createStreamingInternalReminderSanitizer();
|
|
706
780
|
const reasoningSanitizer = createStreamingInternalReminderSanitizer();
|
|
707
781
|
let turnUsage;
|
|
782
|
+
let turnSystemFingerprint;
|
|
708
783
|
let assistantAppended = false;
|
|
709
784
|
currentAssistantMsg = assistantMsg;
|
|
710
785
|
currentAssistantAppended = false;
|
|
@@ -898,6 +973,10 @@ export class Agent {
|
|
|
898
973
|
}
|
|
899
974
|
}
|
|
900
975
|
break;
|
|
976
|
+
case "response_metadata":
|
|
977
|
+
turnSystemFingerprint = chunk.systemFingerprint;
|
|
978
|
+
assistantMsg.systemFingerprint = chunk.systemFingerprint;
|
|
979
|
+
break;
|
|
901
980
|
}
|
|
902
981
|
for (const update of this.subagents.drainToolUpdates())
|
|
903
982
|
yield emit(update);
|
|
@@ -938,7 +1017,7 @@ export class Agent {
|
|
|
938
1017
|
if (consecutiveEmptyAssistantRecoveries < MAX_EMPTY_ASSISTANT_RECOVERIES) {
|
|
939
1018
|
consecutiveEmptyAssistantRecoveries += 1;
|
|
940
1019
|
this.injectSystemReminder(EMPTY_ASSISTANT_RECOVERY_REMINDER);
|
|
941
|
-
yield emit({ type: "turn_end", usage: turnUsage, willContinue: true });
|
|
1020
|
+
yield emit({ type: "turn_end", usage: turnUsage, systemFingerprint: turnSystemFingerprint, willContinue: true });
|
|
942
1021
|
continue;
|
|
943
1022
|
}
|
|
944
1023
|
assistantMsg.content = EMPTY_ASSISTANT_FALLBACK;
|
|
@@ -1328,7 +1407,7 @@ export class Agent {
|
|
|
1328
1407
|
},
|
|
1329
1408
|
});
|
|
1330
1409
|
flushQueuedReminders();
|
|
1331
|
-
yield emit({ type: "turn_end", usage: turnUsage, willContinue: true });
|
|
1410
|
+
yield emit({ type: "turn_end", usage: turnUsage, systemFingerprint: turnSystemFingerprint, willContinue: true });
|
|
1332
1411
|
// Auto-continue: if we have tool results, the LLM needs to respond to them.
|
|
1333
1412
|
// Emitting the turn boundary keeps UI renderers aligned with the persisted
|
|
1334
1413
|
// assistant/tool message sequence instead of merging the next answer into
|
|
@@ -1360,7 +1439,7 @@ export class Agent {
|
|
|
1360
1439
|
for (const event of stopHook.events)
|
|
1361
1440
|
yield emit(event);
|
|
1362
1441
|
const willContinue = !!hookState.forceContinuationReason;
|
|
1363
|
-
yield emit({ type: "turn_end", usage: turnUsage, willContinue });
|
|
1442
|
+
yield emit({ type: "turn_end", usage: turnUsage, systemFingerprint: turnSystemFingerprint, willContinue });
|
|
1364
1443
|
if (willContinue) {
|
|
1365
1444
|
delete hookState.forceContinuationReason;
|
|
1366
1445
|
continue;
|
|
@@ -1828,6 +1907,7 @@ export class Agent {
|
|
|
1828
1907
|
model: this.apiModel || "none",
|
|
1829
1908
|
});
|
|
1830
1909
|
this.onMessageAppend?.(message);
|
|
1910
|
+
this.notifyContextChanged();
|
|
1831
1911
|
}
|
|
1832
1912
|
appendInterruptedAssistantBoundary(currentAssistant, currentAssistantAppended) {
|
|
1833
1913
|
const last = lastProviderMessage(this.messages);
|
|
@@ -1938,293 +2018,3 @@ export class Agent {
|
|
|
1938
2018
|
}
|
|
1939
2019
|
}
|
|
1940
2020
|
}
|
|
1941
|
-
function findMissingRequiredArgs(schema, args) {
|
|
1942
|
-
const required = schema?.required;
|
|
1943
|
-
if (!required || required.length === 0)
|
|
1944
|
-
return [];
|
|
1945
|
-
const missing = [];
|
|
1946
|
-
for (const name of required) {
|
|
1947
|
-
const value = args ? args[name] : undefined;
|
|
1948
|
-
// Empty strings/arrays are intentionally allowed — writing an empty file
|
|
1949
|
-
// or passing an empty list can be legitimate. Only undefined/null counts
|
|
1950
|
-
// as "missing", because the observed failure mode is `finalArgs: "{}"`
|
|
1951
|
-
// where the field is entirely absent.
|
|
1952
|
-
if (value === undefined || value === null) {
|
|
1953
|
-
missing.push(name);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
return missing;
|
|
1957
|
-
}
|
|
1958
|
-
function estimateResidentChars(messages) {
|
|
1959
|
-
let total = 0;
|
|
1960
|
-
for (const message of messages) {
|
|
1961
|
-
switch (message.role) {
|
|
1962
|
-
case "system":
|
|
1963
|
-
case "meta":
|
|
1964
|
-
total += message.content.length;
|
|
1965
|
-
break;
|
|
1966
|
-
case "tool":
|
|
1967
|
-
total += message.content.length + message.toolCallId.length;
|
|
1968
|
-
break;
|
|
1969
|
-
case "assistant":
|
|
1970
|
-
total += message.content.length + (message.reasoning?.length ?? 0);
|
|
1971
|
-
total += message.toolCalls?.reduce((sum, toolCall) => sum + toolCall.id.length + toolCall.name.length + toolCall.arguments.length, 0) ?? 0;
|
|
1972
|
-
break;
|
|
1973
|
-
case "user":
|
|
1974
|
-
if (typeof message.content === "string") {
|
|
1975
|
-
total += message.content.length;
|
|
1976
|
-
}
|
|
1977
|
-
else {
|
|
1978
|
-
total += message.content.reduce((sum, part) => {
|
|
1979
|
-
if (part.type === "text") {
|
|
1980
|
-
return sum + part.text.length;
|
|
1981
|
-
}
|
|
1982
|
-
return sum + part.image_url.url.length;
|
|
1983
|
-
}, 0);
|
|
1984
|
-
}
|
|
1985
|
-
break;
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
return total;
|
|
1989
|
-
}
|
|
1990
|
-
function appendProviderContentBlock(message, provider, block) {
|
|
1991
|
-
const current = message.providerMetadata?.[provider]?.contentBlocks ?? [];
|
|
1992
|
-
message.providerMetadata = {
|
|
1993
|
-
...message.providerMetadata,
|
|
1994
|
-
[provider]: {
|
|
1995
|
-
...message.providerMetadata?.[provider],
|
|
1996
|
-
contentBlocks: [...current, cloneProviderRawContentBlock(block)],
|
|
1997
|
-
},
|
|
1998
|
-
};
|
|
1999
|
-
}
|
|
2000
|
-
function buildProviderRequestFingerprint(messages, tools, providerId, toolChoice) {
|
|
2001
|
-
const roleCounts = {};
|
|
2002
|
-
let contentChars = 0;
|
|
2003
|
-
let reasoningChars = 0;
|
|
2004
|
-
let toolResultChars = 0;
|
|
2005
|
-
let maxToolResultChars = 0;
|
|
2006
|
-
let assistantToolCalls = 0;
|
|
2007
|
-
let rawAnthropicBlocks = 0;
|
|
2008
|
-
let rawAnthropicThinkingBlocks = 0;
|
|
2009
|
-
let rawAnthropicSignatureChars = 0;
|
|
2010
|
-
for (const message of messages) {
|
|
2011
|
-
roleCounts[message.role] = (roleCounts[message.role] ?? 0) + 1;
|
|
2012
|
-
if (message.role === "assistant") {
|
|
2013
|
-
contentChars += message.content.length;
|
|
2014
|
-
reasoningChars += message.reasoning?.length ?? 0;
|
|
2015
|
-
assistantToolCalls += message.toolCalls?.length ?? 0;
|
|
2016
|
-
const blocks = message.providerMetadata?.anthropic?.contentBlocks ?? [];
|
|
2017
|
-
rawAnthropicBlocks += blocks.length;
|
|
2018
|
-
for (const block of blocks) {
|
|
2019
|
-
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
2020
|
-
rawAnthropicThinkingBlocks += 1;
|
|
2021
|
-
}
|
|
2022
|
-
if (typeof block.signature === "string") {
|
|
2023
|
-
rawAnthropicSignatureChars += block.signature.length;
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
else if (message.role === "tool") {
|
|
2028
|
-
toolResultChars += message.content.length;
|
|
2029
|
-
maxToolResultChars = Math.max(maxToolResultChars, message.content.length);
|
|
2030
|
-
}
|
|
2031
|
-
else if (message.role === "user") {
|
|
2032
|
-
contentChars += typeof message.content === "string"
|
|
2033
|
-
? message.content.length
|
|
2034
|
-
: message.content.reduce((sum, part) => sum + (part.type === "text" ? part.text.length : part.image_url.url.length), 0);
|
|
2035
|
-
}
|
|
2036
|
-
else {
|
|
2037
|
-
contentChars += message.content.length;
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
const systemMessages = messages.filter((message) => message.role === "system");
|
|
2041
|
-
const bodyMessages = messages.filter((message) => message.role !== "system");
|
|
2042
|
-
const systemJsonBytes = Buffer.byteLength(JSON.stringify(systemMessages), "utf8");
|
|
2043
|
-
const bodyJsonBytes = Buffer.byteLength(JSON.stringify(bodyMessages), "utf8");
|
|
2044
|
-
const toolSchemaJsonBytes = Buffer.byteLength(JSON.stringify(tools), "utf8");
|
|
2045
|
-
return {
|
|
2046
|
-
roleCounts,
|
|
2047
|
-
estimatedTokens: estimateContextTokens(messages, providerId),
|
|
2048
|
-
projectedJsonBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
|
|
2049
|
-
systemJsonBytes,
|
|
2050
|
-
bodyJsonBytes,
|
|
2051
|
-
toolSchemaJsonBytes,
|
|
2052
|
-
staticPrefixJsonBytes: Buffer.byteLength(JSON.stringify({
|
|
2053
|
-
system: systemMessages,
|
|
2054
|
-
tools,
|
|
2055
|
-
tool_choice: toolChoice,
|
|
2056
|
-
}), "utf8"),
|
|
2057
|
-
toolChoice,
|
|
2058
|
-
contentChars,
|
|
2059
|
-
reasoningChars,
|
|
2060
|
-
toolResultChars,
|
|
2061
|
-
maxToolResultChars,
|
|
2062
|
-
assistantToolCalls,
|
|
2063
|
-
rawAnthropicBlocks,
|
|
2064
|
-
rawAnthropicThinkingBlocks,
|
|
2065
|
-
rawAnthropicSignatureChars,
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
function cloneProviderRawContentBlock(block) {
|
|
2069
|
-
return JSON.parse(JSON.stringify(block));
|
|
2070
|
-
}
|
|
2071
|
-
function throwIfAborted(signal) {
|
|
2072
|
-
if (!signal?.aborted)
|
|
2073
|
-
return;
|
|
2074
|
-
const reason = signal.reason;
|
|
2075
|
-
if (reason instanceof Error)
|
|
2076
|
-
throw reason;
|
|
2077
|
-
throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
|
|
2078
|
-
}
|
|
2079
|
-
function isAbortLikeError(error, signal) {
|
|
2080
|
-
if (signal?.aborted)
|
|
2081
|
-
return true;
|
|
2082
|
-
if (error instanceof AgentAbortError)
|
|
2083
|
-
return true;
|
|
2084
|
-
if (error instanceof DOMException && error.name === "AbortError")
|
|
2085
|
-
return true;
|
|
2086
|
-
if (typeof error === "object" && error !== null && error.name === "AbortError")
|
|
2087
|
-
return true;
|
|
2088
|
-
return false;
|
|
2089
|
-
}
|
|
2090
|
-
function isAbortError(error, signal) {
|
|
2091
|
-
return isAbortLikeError(error, signal);
|
|
2092
|
-
}
|
|
2093
|
-
function shouldAppendModelInterruptedBoundary(messages) {
|
|
2094
|
-
return messages.at(-1)?.role === "tool";
|
|
2095
|
-
}
|
|
2096
|
-
function createModelInterruptedMessage(error, metadata) {
|
|
2097
|
-
return {
|
|
2098
|
-
role: "assistant",
|
|
2099
|
-
content: `[model request interrupted before a final answer was produced: ${summarizeInterruptError(error)}]`,
|
|
2100
|
-
model: metadata.model,
|
|
2101
|
-
providerId: metadata.providerId,
|
|
2102
|
-
modelId: metadata.modelId,
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
function summarizeInterruptError(error) {
|
|
2106
|
-
const message = error instanceof Error
|
|
2107
|
-
? error.message
|
|
2108
|
-
: typeof error === "string"
|
|
2109
|
-
? error
|
|
2110
|
-
: String(error);
|
|
2111
|
-
return message.replace(/\s+/g, " ").trim().slice(0, 240) || "unknown error";
|
|
2112
|
-
}
|
|
2113
|
-
function lastProviderMessage(messages) {
|
|
2114
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2115
|
-
const message = messages[i];
|
|
2116
|
-
if (message.role === "system" || message.role === "meta")
|
|
2117
|
-
continue;
|
|
2118
|
-
return message;
|
|
2119
|
-
}
|
|
2120
|
-
return undefined;
|
|
2121
|
-
}
|
|
2122
|
-
function cancelledToolResult(toolName) {
|
|
2123
|
-
return {
|
|
2124
|
-
content: `Tool "${toolName}" was cancelled.`,
|
|
2125
|
-
isError: true,
|
|
2126
|
-
status: "cancelled",
|
|
2127
|
-
metadata: { reason: "cancelled" },
|
|
2128
|
-
};
|
|
2129
|
-
}
|
|
2130
|
-
/** Exported for tests: the wake latch below is easiest to pin directly. */
|
|
2131
|
-
export function createUpdateQueue() {
|
|
2132
|
-
const items = [];
|
|
2133
|
-
let waiter;
|
|
2134
|
-
let abortCleanup;
|
|
2135
|
-
// Latch for wakes that arrive with no parked waiter — a wake() fired while
|
|
2136
|
-
// the consuming generator is suspended at a yield would otherwise be lost.
|
|
2137
|
-
// Callers today each pair their wake with a synchronously-checked flag
|
|
2138
|
-
// (items / settled / pendingSubagentUpdates), so no wake is currently
|
|
2139
|
-
// dropped; the latch keeps that correctness inside the queue instead of
|
|
2140
|
-
// depending on every future caller repeating the pairing.
|
|
2141
|
-
let signaled = false;
|
|
2142
|
-
return {
|
|
2143
|
-
push(item) {
|
|
2144
|
-
items.push(item);
|
|
2145
|
-
this.wake();
|
|
2146
|
-
},
|
|
2147
|
-
drain() {
|
|
2148
|
-
return items.splice(0, items.length);
|
|
2149
|
-
},
|
|
2150
|
-
hasItems() {
|
|
2151
|
-
return items.length > 0;
|
|
2152
|
-
},
|
|
2153
|
-
wait(signal) {
|
|
2154
|
-
if (items.length > 0)
|
|
2155
|
-
return Promise.resolve("woken");
|
|
2156
|
-
if (signaled) {
|
|
2157
|
-
signaled = false;
|
|
2158
|
-
return Promise.resolve("woken");
|
|
2159
|
-
}
|
|
2160
|
-
if (signal?.aborted)
|
|
2161
|
-
return Promise.resolve("aborted");
|
|
2162
|
-
return new Promise((resolve) => {
|
|
2163
|
-
abortCleanup?.();
|
|
2164
|
-
abortCleanup = undefined;
|
|
2165
|
-
const finish = (status) => {
|
|
2166
|
-
if (waiter !== resolve)
|
|
2167
|
-
return;
|
|
2168
|
-
waiter = undefined;
|
|
2169
|
-
abortCleanup?.();
|
|
2170
|
-
abortCleanup = undefined;
|
|
2171
|
-
resolve(status);
|
|
2172
|
-
};
|
|
2173
|
-
if (signal) {
|
|
2174
|
-
const onAbort = () => finish("aborted");
|
|
2175
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
2176
|
-
abortCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
2177
|
-
}
|
|
2178
|
-
waiter = resolve;
|
|
2179
|
-
});
|
|
2180
|
-
},
|
|
2181
|
-
wake() {
|
|
2182
|
-
const resolve = waiter;
|
|
2183
|
-
if (!resolve) {
|
|
2184
|
-
signaled = true;
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
waiter = undefined;
|
|
2188
|
-
abortCleanup?.();
|
|
2189
|
-
abortCleanup = undefined;
|
|
2190
|
-
resolve("woken");
|
|
2191
|
-
},
|
|
2192
|
-
};
|
|
2193
|
-
}
|
|
2194
|
-
function estimateToolPayloadChars(messages) {
|
|
2195
|
-
return messages.reduce((sum, message) => {
|
|
2196
|
-
if (message.role !== "tool") {
|
|
2197
|
-
return sum;
|
|
2198
|
-
}
|
|
2199
|
-
return sum + message.content.length;
|
|
2200
|
-
}, 0);
|
|
2201
|
-
}
|
|
2202
|
-
function countUserTurns(messages) {
|
|
2203
|
-
// Real user turns only: projected reminders and summary envelopes are
|
|
2204
|
-
// user-ROLE but not user TURNS — counting them used to shrink the keep
|
|
2205
|
-
// window and fire compaction almost every turn.
|
|
2206
|
-
return messages.reduce((count, message) => count + (isRealUserMessage(message) ? 1 : 0), 0);
|
|
2207
|
-
}
|
|
2208
|
-
function getCurrentHeapUsed() {
|
|
2209
|
-
try {
|
|
2210
|
-
return process.memoryUsage().heapUsed;
|
|
2211
|
-
}
|
|
2212
|
-
catch {
|
|
2213
|
-
return 0;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
function isSubagentLifecycleTool(name) {
|
|
2217
|
-
return name === "subagent"
|
|
2218
|
-
|| name === "spawn_agent"
|
|
2219
|
-
|| name === "wait_agent"
|
|
2220
|
-
|| name === "send_input"
|
|
2221
|
-
|| name === "close_agent"
|
|
2222
|
-
|| name === "list_agents"
|
|
2223
|
-
|| name === "run_workflow"
|
|
2224
|
-
|| name === "wait_workflow"
|
|
2225
|
-
// Legacy names: still present in transcripts recorded before the tools
|
|
2226
|
-
// were removed (2026-07-06); forked children must not inherit their
|
|
2227
|
-
// dangling tool_calls either.
|
|
2228
|
-
|| name === "agent_team"
|
|
2229
|
-
|| name === "agent_batch";
|
|
2230
|
-
}
|
|
@@ -530,7 +530,7 @@ export class GrokRuntimeManager {
|
|
|
530
530
|
try {
|
|
531
531
|
if (!this.lock)
|
|
532
532
|
temporaryLock = await this.acquireProfileLock();
|
|
533
|
-
const {
|
|
533
|
+
const { env, binary } = await this.prepareAndVerify();
|
|
534
534
|
const result = await runGrokCommand(this.spawn, binary.path, ["--no-auto-update", "--cwd", this.workspace, "models"], { cwd: this.workspace, env, maxStdoutBytes: 64 * 1024 });
|
|
535
535
|
if (result.code !== 0) {
|
|
536
536
|
throw new GrokRuntimeError("protocol_error", "Grok could not list subscription models.", result.stderr);
|
|
@@ -301,7 +301,7 @@ export class RunDriver {
|
|
|
301
301
|
this.opts.approvalUI.cancelForChat(req.chatId, "Run ended");
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
|
-
async resolveProvider(
|
|
304
|
+
async resolveProvider(_session, promptCacheKey) {
|
|
305
305
|
const registry = this.opts.deps.providerRegistry;
|
|
306
306
|
const userConfig = this.opts.deps.userConfig;
|
|
307
307
|
// Read session metadata for an explicit model preference, fall back to
|
package/dist/lsp/service.js
CHANGED
|
@@ -436,7 +436,7 @@ const TypeScriptServer = {
|
|
|
436
436
|
extensions: TS_EXTENSIONS,
|
|
437
437
|
root: nearestRoot(NODE_ROOT_MARKERS, DENO_ROOT_MARKERS),
|
|
438
438
|
languageId: languageIdFor,
|
|
439
|
-
async spawn(root,
|
|
439
|
+
async spawn(root, _ctx) {
|
|
440
440
|
const requireFromRoot = createRequire(join(root, "package.json"));
|
|
441
441
|
const requireFromSelf = createRequire(import.meta.url);
|
|
442
442
|
const tsserverPath = resolveModule(requireFromRoot, "typescript/lib/tsserver.js")
|
|
@@ -495,7 +495,7 @@ const ESLintServer = {
|
|
|
495
495
|
extensions: [...TS_EXTENSIONS, ".vue"],
|
|
496
496
|
root: eslintRoot,
|
|
497
497
|
languageId: languageIdFor,
|
|
498
|
-
async spawn(root,
|
|
498
|
+
async spawn(root, _ctx) {
|
|
499
499
|
const requireFromRoot = createRequire(join(root, "package.json"));
|
|
500
500
|
const requireFromSelf = createRequire(import.meta.url);
|
|
501
501
|
const eslint = resolveModule(requireFromRoot, "eslint");
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -48,5 +48,18 @@ export declare function listDynamicModelMetadata(providerId: string): BuiltinMod
|
|
|
48
48
|
export declare function getBuiltinModel(providerId: string, modelId: string): BuiltinModelDefinition | undefined;
|
|
49
49
|
export declare function getModelDefaultReasoningLevel(providerId: string, modelId: string): ReasoningEffort | undefined;
|
|
50
50
|
export declare function getBuiltinProvider(providerId: string): BuiltinProviderDefinition | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Infer capability metadata for a Grok model id the /models endpoint returned
|
|
53
|
+
* without metadata. Flagship reasoning models (grok-N.M) expose low/medium/high
|
|
54
|
+
* with thinking always on and a 500K context window — matching the curated
|
|
55
|
+
* grok-4.5 entry — while composer/fast/mini/flash variants have no effort
|
|
56
|
+
* control and a 200K window. Pure (id-only) so it works even before remote
|
|
57
|
+
* model discovery runs.
|
|
58
|
+
*/
|
|
59
|
+
export declare function inferGrokModelMetadata(modelId: string): {
|
|
60
|
+
levels: ReasoningEffort[];
|
|
61
|
+
defaultLevel?: ReasoningEffort;
|
|
62
|
+
contextWindow?: number;
|
|
63
|
+
};
|
|
51
64
|
export declare function getModelContextWindow(providerId: string, modelId: string): number | undefined;
|
|
52
65
|
export declare function getToolOutputTokenLimit(providerId: string, modelId: string): number | undefined;
|
package/dist/model-catalog.js
CHANGED
|
@@ -41,13 +41,12 @@ const OPENAI_CHAT_LEVELS = ["off"];
|
|
|
41
41
|
// Internal representation for APIs that expose thinking as enabled/disabled.
|
|
42
42
|
// UI code must render supported toggle models as on/off, not as "medium" effort.
|
|
43
43
|
const TOGGLE_THINKING_LEVELS = ["off", "medium"];
|
|
44
|
-
// GLM-5.2
|
|
45
|
-
//
|
|
46
|
-
// two effort tiers worth offering a coding agent) plus "off", which disables
|
|
47
|
-
// thinking outright via `thinking: {type: "disabled"}`. Order matters: "high"
|
|
48
|
-
// is first so it is the default (getDefaultThinkingLevel falls back to levels[0]
|
|
49
|
-
// when "medium" is absent), since GLM-5.2 is a thinking-on-by-default model.
|
|
44
|
+
// GLM-5.2 accepts OpenAI-style `reasoning_effort`. We expose high and max plus
|
|
45
|
+
// "off", which disables thinking outright via `thinking: {type: "disabled"}`.
|
|
50
46
|
const GLM_5_2_LEVELS = ["high", "max", "off"];
|
|
47
|
+
// GLM-5.3 Coding Plan normalizes efforts into exactly three real tiers. Its
|
|
48
|
+
// default is max, and disabling thinking maps to low rather than turning it off.
|
|
49
|
+
const GLM_5_3_LEVELS = ["low", "high", "max"];
|
|
51
50
|
// Kimi K2.7 Code variants only support thinking mode (disabling it errors), so
|
|
52
51
|
// "off" is not offered — the model is always in its thinking variant.
|
|
53
52
|
const KIMI_THINKING_ONLY_LEVELS = ["medium"];
|
|
@@ -124,6 +123,9 @@ export const BUILTIN_MODELS = [
|
|
|
124
123
|
{ id: "glm-5.1", name: "GLM-5.1", providerId: "zhipuai", tier: "balanced", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
|
|
125
124
|
{ id: "glm-4.7", name: "GLM-4.7", providerId: "zhipuai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
|
|
126
125
|
{ id: "glm-4.6", name: "GLM-4.6", providerId: "zhipuai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
|
|
126
|
+
// Coding Plan exposes GLM-5.3 before GET /models lists it (verified live
|
|
127
|
+
// 2026-08-14). The standard API is still marked "coming soon" in the docs.
|
|
128
|
+
{ id: "glm-5.3", name: "GLM-5.3", providerId: "zhipuai-coding-plan", tier: "strong", reasoningLevels: GLM_5_3_LEVELS, defaultReasoningLevel: "max", contextWindow: 1000000 },
|
|
127
129
|
{ id: "glm-5.2", name: "GLM-5.2", providerId: "zhipuai-coding-plan", tier: "strong", reasoningLevels: GLM_5_2_LEVELS, contextWindow: 1000000 },
|
|
128
130
|
{ id: "glm-5.1", name: "GLM-5.1", providerId: "zhipuai-coding-plan", tier: "balanced", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
|
|
129
131
|
{ id: "glm-4.7", name: "GLM-4.7", providerId: "zhipuai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
|
|
@@ -239,10 +241,28 @@ export function getBuiltinModel(providerId, modelId) {
|
|
|
239
241
|
|| (providerId === "openai" ? dynamicOverlay.get(overlayKey("openai-codex", modelId)) : undefined);
|
|
240
242
|
if (overlayHit)
|
|
241
243
|
return overlayHit;
|
|
242
|
-
|
|
244
|
+
const staticHit = BUILTIN_MODELS.find((model) => model.providerId === providerId && model.id === modelId)
|
|
243
245
|
|| (providerId === "openai"
|
|
244
246
|
? BUILTIN_MODELS.find((model) => model.providerId === "openai-codex" && model.id === modelId)
|
|
245
247
|
: undefined);
|
|
248
|
+
if (staticHit)
|
|
249
|
+
return staticHit;
|
|
250
|
+
// Dynamically-discovered grok models aren't in the static catalog until
|
|
251
|
+
// remote discovery runs. Synthesize their metadata from the id so reasoning
|
|
252
|
+
// levels, default effort, and context window resolve correctly on a fresh
|
|
253
|
+
// start or resumed session (before the first /model discovery).
|
|
254
|
+
if (providerId === "grok") {
|
|
255
|
+
const inferred = inferGrokModelMetadata(modelId);
|
|
256
|
+
return {
|
|
257
|
+
id: modelId,
|
|
258
|
+
name: modelId,
|
|
259
|
+
providerId: "grok",
|
|
260
|
+
reasoningLevels: inferred.levels,
|
|
261
|
+
defaultReasoningLevel: inferred.defaultLevel,
|
|
262
|
+
contextWindow: inferred.contextWindow,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
246
266
|
}
|
|
247
267
|
export function getModelDefaultReasoningLevel(providerId, modelId) {
|
|
248
268
|
return getBuiltinModel(providerId, modelId)?.defaultReasoningLevel;
|
|
@@ -250,8 +270,32 @@ export function getModelDefaultReasoningLevel(providerId, modelId) {
|
|
|
250
270
|
export function getBuiltinProvider(providerId) {
|
|
251
271
|
return BUILTIN_PROVIDERS.find((provider) => provider.id === providerId);
|
|
252
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Infer capability metadata for a Grok model id the /models endpoint returned
|
|
275
|
+
* without metadata. Flagship reasoning models (grok-N.M) expose low/medium/high
|
|
276
|
+
* with thinking always on and a 500K context window — matching the curated
|
|
277
|
+
* grok-4.5 entry — while composer/fast/mini/flash variants have no effort
|
|
278
|
+
* control and a 200K window. Pure (id-only) so it works even before remote
|
|
279
|
+
* model discovery runs.
|
|
280
|
+
*/
|
|
281
|
+
export function inferGrokModelMetadata(modelId) {
|
|
282
|
+
const id = modelId.toLowerCase();
|
|
283
|
+
if (/(composer|fast|mini|flash)/.test(id)) {
|
|
284
|
+
return { levels: ["off"], contextWindow: 200000 };
|
|
285
|
+
}
|
|
286
|
+
return { levels: ["low", "medium", "high"], defaultLevel: "high", contextWindow: 500000 };
|
|
287
|
+
}
|
|
253
288
|
export function getModelContextWindow(providerId, modelId) {
|
|
254
|
-
|
|
289
|
+
const builtin = getBuiltinModel(providerId, modelId);
|
|
290
|
+
if (builtin?.contextWindow !== undefined)
|
|
291
|
+
return builtin.contextWindow;
|
|
292
|
+
// Dynamically-discovered grok models aren't in the static catalog until
|
|
293
|
+
// discovery runs; infer the window from the id so context usage can render a
|
|
294
|
+
// percentage even before discovery (e.g. on a resumed session right after
|
|
295
|
+
// startup).
|
|
296
|
+
if (providerId === "grok")
|
|
297
|
+
return inferGrokModelMetadata(modelId).contextWindow;
|
|
298
|
+
return undefined;
|
|
255
299
|
}
|
|
256
300
|
export function getToolOutputTokenLimit(providerId, modelId) {
|
|
257
301
|
return getBuiltinModel(providerId, modelId)?.toolOutputTokenLimit;
|