@cjhyy/code-shell-core 0.8.13 → 0.8.20

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.
Files changed (42) hide show
  1. package/dist/cli/agent-server-stdio.js +4 -3
  2. package/dist/engine/engine.js +24 -11
  3. package/dist/engine/run-session-open.js +1 -1
  4. package/dist/engine/run-types.d.ts +2 -0
  5. package/dist/engine/turn-loop.js +17 -0
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.internal.d.ts +1 -0
  8. package/dist/index.internal.js +1 -0
  9. package/dist/index.js +2 -2
  10. package/dist/llm/client-base.d.ts +2 -1
  11. package/dist/llm/client-base.js +3 -1
  12. package/dist/llm/providers/anthropic.js +24 -25
  13. package/dist/llm/providers/openai.d.ts +4 -1
  14. package/dist/llm/providers/openai.js +76 -13
  15. package/dist/panel-apps/index.d.ts +1 -1
  16. package/dist/panel-apps/index.js +1 -1
  17. package/dist/panel-apps/installer.d.ts +29 -0
  18. package/dist/panel-apps/installer.js +124 -15
  19. package/dist/plugins/pluginCatalog.d.ts +6 -0
  20. package/dist/plugins/pluginCatalog.js +7 -2
  21. package/dist/plugins/pluginContent.d.ts +1 -1
  22. package/dist/plugins/pluginContent.js +2 -10
  23. package/dist/protocol/chat-session.d.ts +2 -0
  24. package/dist/protocol/server.js +7 -0
  25. package/dist/protocol/types.d.ts +2 -0
  26. package/dist/session/session-manager.d.ts +7 -0
  27. package/dist/session/session-manager.js +19 -6
  28. package/dist/session/transcript.d.ts +9 -0
  29. package/dist/session/transcript.js +81 -0
  30. package/dist/settings/manager.d.ts +12 -0
  31. package/dist/settings/manager.js +31 -0
  32. package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
  33. package/dist/tool-system/builtin/configure-model-connection.js +396 -0
  34. package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
  35. package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
  36. package/dist/tool-system/builtin/index.js +14 -0
  37. package/dist/tool-system/builtin/install-capability.js +22 -8
  38. package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
  39. package/dist/tool-system/builtin/settings-changed.js +18 -0
  40. package/dist/tool-system/context.d.ts +8 -2
  41. package/dist/types.d.ts +5 -0
  42. package/package.json +1 -1
@@ -333,9 +333,10 @@ setDefaultCredentialAccess(createIpcCredentialAccess(stdioTransport));
333
333
  setCronChangedSink(() => {
334
334
  stdioTransport.send(createNotification("agent/cronChanged", {}));
335
335
  });
336
- // EditModelCatalog writes in this worker process, while the mounted settings
337
- // and connection pages live in the desktop renderer. Notify them immediately
338
- // after a successful write instead of relying on a later turn_complete event.
336
+ // Catalog and model-connection tools write in this worker process, while the
337
+ // mounted settings/connection pages live in the desktop renderer. Notify them
338
+ // immediately after a successful write instead of relying on a later
339
+ // turn_complete event.
339
340
  setModelCatalogChangedSink(() => {
340
341
  stdioTransport.send(createNotification(Methods.SettingsChanged, {}));
341
342
  });
@@ -1104,7 +1104,7 @@ export class Engine {
1104
1104
  // the entire old topic. Fail open to full history if summarization fails.
1105
1105
  if (options?.archiveBeforeCurrentTurn && options.clientMessageId) {
1106
1106
  try {
1107
- const archived = await this.archiveTurnRange(session.state.sessionId, { start: 0, end: 0 }, {
1107
+ const anchors = {
1108
1108
  toClientMessageId: options.clientMessageId,
1109
1109
  ...(options.archiveBeforeCurrentTurn.fromClientMessageId
1110
1110
  ? {
@@ -1114,7 +1114,24 @@ export class Engine {
1114
1114
  ...(options.archiveBeforeCurrentTurn.segmentId
1115
1115
  ? { segmentId: options.archiveBeforeCurrentTurn.segmentId }
1116
1116
  : {}),
1117
- });
1117
+ };
1118
+ const before = estimateTokens(messages);
1119
+ if (options.archiveBeforeCurrentTurn.summary) {
1120
+ await this.appendArchiveMarker(session.state.sessionId, {
1121
+ ...anchors,
1122
+ summary: options.archiveBeforeCurrentTurn.summary,
1123
+ });
1124
+ }
1125
+ else {
1126
+ await this.archiveTurnRange(session.state.sessionId, { start: 0, end: 0 }, anchors);
1127
+ }
1128
+ // openRunSession captured the pre-marker replay. Rebuild from the
1129
+ // persisted marker so this very first post-boundary model call gets
1130
+ // the archived view rather than waiting until the following turn.
1131
+ messages =
1132
+ this.compactedMessagesBySession.get(session.state.sessionId) ??
1133
+ this.sessionManager.resumeForRun(session.state.sessionId).transcript.toMessages();
1134
+ const archived = { before, after: estimateTokens(messages) };
1118
1135
  if (archived.before > archived.after) {
1119
1136
  options.onStream?.({
1120
1137
  type: "context_compact",
@@ -1123,12 +1140,6 @@ export class Engine {
1123
1140
  after: archived.after,
1124
1141
  });
1125
1142
  }
1126
- // openRunSession captured the pre-marker replay. Rebuild from the
1127
- // persisted marker so this very first post-boundary model call gets
1128
- // the archived view rather than waiting until the following turn.
1129
- messages =
1130
- this.compactedMessagesBySession.get(session.state.sessionId) ??
1131
- this.sessionManager.resume(session.state.sessionId).transcript.toMessages();
1132
1143
  }
1133
1144
  catch (error) {
1134
1145
  logger.warn("engine.pre_run_archive.failed", {
@@ -2018,9 +2029,11 @@ export class Engine {
2018
2029
  runAllowedToolNames.has("cancel_goal")
2019
2030
  ? { hasGoal: hasRunnableGoal }
2020
2031
  : undefined,
2021
- capabilityPromptSections: profile?.disableCapabilityContext
2022
- ? {}
2023
- : this.capabilityPromptSections,
2032
+ // Static sections named by the active preset are part of that preset's
2033
+ // contract. `disableCapabilityContext` suppresses ambient/dynamic
2034
+ // capability context, but removing these definitions leaves presets
2035
+ // with dangling section names (for example terminal-coding → coding).
2036
+ capabilityPromptSections: this.capabilityPromptSections,
2024
2037
  dynamicContextProviders: this.capabilityDynamicContextProviders,
2025
2038
  getSettingsManager: () => this.getSettingsManager(),
2026
2039
  toolCatalog: this.toolCatalog,
@@ -29,7 +29,7 @@ export function openRunSession(args) {
29
29
  };
30
30
  if (options?.sessionId && args.sessionManager.exists(options.sessionId)) {
31
31
  resumedFromDisk = true;
32
- session = args.sessionManager.resume(options.sessionId);
32
+ session = args.sessionManager.resumeForRun(options.sessionId);
33
33
  const cachedCompacted = args.cachedCompactedMessages;
34
34
  messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
35
35
  // If the previous run was Ctrl+C'd or crashed between an assistant
@@ -106,6 +106,8 @@ export interface EngineRunOptions {
106
106
  archiveBeforeCurrentTurn?: {
107
107
  fromClientMessageId?: string;
108
108
  segmentId?: string;
109
+ /** Host-authored replacement for the archived span; skips the summarizer. */
110
+ summary?: string;
109
111
  };
110
112
  attachments?: InputAttachmentMeta[];
111
113
  /** Named per-run behavior profile supplied by interactive product surfaces. */
@@ -836,6 +836,23 @@ export class TurnLoop {
836
836
  if (response.text) {
837
837
  messages.push({ role: "assistant", content: response.text });
838
838
  }
839
+ // Anthropic streams tool_use_start as soon as the content block
840
+ // opens. If max_tokens cuts the JSON arguments off, we deliberately
841
+ // do not execute that call, but the renderer still needs a terminal
842
+ // event for the already-open card. Without it Claude tool cards stay
843
+ // "working" forever (often until a later context compaction hides
844
+ // them), even though the loop has moved on to a retry.
845
+ for (const toolCall of response.toolCalls) {
846
+ this.config.onStream?.({
847
+ type: "tool_result",
848
+ result: {
849
+ id: toolCall.id,
850
+ toolName: toolCall.toolName,
851
+ error: "Tool call was not executed because its arguments were truncated.",
852
+ isError: true,
853
+ },
854
+ });
855
+ }
839
856
  messages.push({
840
857
  role: "user",
841
858
  content: "<system-reminder>Your previous response was truncated by the max output token limit before the tool call finished, so its arguments are incomplete. Do not assume it ran. Either retry with a smaller/more focused tool call (e.g. write the file in sections via Edit), or raise this model's maxOutputTokens.</system-reminder>",
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export declare const VERSION = "0.8.13";
6
+ export declare const VERSION = "0.8.20";
7
7
  export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
8
8
  export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
9
9
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, CompositionError, SandboxUnavailableError, } from "./exceptions.js";
@@ -98,7 +98,7 @@ export { mergePluginMcpServers, readPluginMcp } from "./plugins/installer/loadPl
98
98
  export { approvePluginMcp, listPluginMcpTrust, revokePluginMcp, type PluginMcpApprovalResult, type PluginMcpTrustEntry, } from "./plugins/pluginMcpApproval.js";
99
99
  export { pluginMcpApprovalState, type PluginMcpApprovalState, } from "./plugins/pluginMcpIntegrity.js";
100
100
  export { pluginsRoot } from "./plugins/installer/paths.js";
101
- export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppManifest, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, previewLocalPanelApp, previewInstalledPanelAppUpdate, isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, uninstallPanelApp, type InstalledPanelApp, type InstalledPanelAppSource, type GitPanelAppSourceInput, type LocalPanelAppSourceInput, type PanelAppSourceInput, type PanelAppManifestData, type PanelAppBindingPolicy, type PanelAppPreview, } from "./panel-apps/index.js";
101
+ export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppManifest, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, previewLocalPanelApp, previewInstalledPanelAppUpdate, isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, uninstallPanelApp, type InstalledPanelApp, type InstalledPanelAppSource, type GitPanelAppDiscovery, type GitPanelAppDiscoveryCandidate, type GitPanelAppDiscoveryIssue, type GitPanelAppSourceInput, type LocalPanelAppSourceInput, type PanelAppSourceInput, type PanelAppManifestData, type PanelAppBindingPolicy, type PanelAppPreview, } from "./panel-apps/index.js";
102
102
  export { previewLocalTheme, installReviewedLocalTheme, listInstalledThemes, uninstallTheme, themesRoot, themeInstallDir, assertSafeThemeName, ThemeInstallError, ThemeReviewChangedError, THEME_ASSET_DIR, THEME_VAR_NAMES, detectThemeImage, type ThemePreview, type InstalledTheme, type ThemeManifest, } from "./themes/index.js";
103
103
  export { resolveSafePluginPath } from "./plugins/pluginInstaller.js";
104
104
  export { listPluginHooks, pluginHookKey, type PluginHookEntry } from "./plugins/loadPluginHooks.js";
@@ -26,6 +26,7 @@ export { recordUIEvent } from "./logging/session-recorder.js";
26
26
  export { getInteractiveApprovalBackend } from "./tool-system/permission.js";
27
27
  export { defaultSandboxConfig, type SandboxConfig } from "./tool-system/sandbox/index.js";
28
28
  export { buildNotificationMessage, buildNotificationSummary, notificationQueue, agentNotificationBus, notificationItemToStreamEvent, type NotificationItem, } from "./tool-system/builtin/agent-notifications.js";
29
+ export { backgroundJobRegistry } from "./tool-system/builtin/background-jobs.js";
29
30
  export type { BackgroundAgentCompletedEvent } from "./types.js";
30
31
  export { startAutomation, type StartAutomationDeps, type AutomationHandle, CronScheduler, cronScheduler, type CronExecutionOutcome, type CronJob, type CronJobLifecycleEvent, type CronPermissionLevel, type CronTemplateSource, type CreateJobOptions, type UpdateJobPatch, CronStore, defaultCronStorePath, bindCronToEngine, bindCronToRunManager, type CronRunner, type CronRunRequest, type CronRunResult, type RunSubmitter, isCronExpression, parseCronExpression, nextCronTime, validateSchedule, type ParsedCron, resolveWritePolicy, wrapUntrustedInput, type WritePolicy, runWriteJobInWorktree, type WriteJobGitOps, type RunWriteJobInput, type RunWriteJobResult, } from "./automation/index.js";
31
32
  export { asyncAgentRegistry, type AsyncAgentEntry } from "./tool-system/builtin/agent-registry.js";
@@ -29,6 +29,7 @@ export { recordUIEvent } from "./logging/session-recorder.js";
29
29
  export { getInteractiveApprovalBackend } from "./tool-system/permission.js";
30
30
  export { defaultSandboxConfig } from "./tool-system/sandbox/index.js";
31
31
  export { buildNotificationMessage, buildNotificationSummary, notificationQueue, agentNotificationBus, notificationItemToStreamEvent, } from "./tool-system/builtin/agent-notifications.js";
32
+ export { backgroundJobRegistry } from "./tool-system/builtin/background-jobs.js";
32
33
  export { startAutomation, CronScheduler, cronScheduler, CronStore, defaultCronStorePath, bindCronToEngine, bindCronToRunManager, isCronExpression, parseCronExpression, nextCronTime, validateSchedule, resolveWritePolicy, wrapUntrustedInput, runWriteJobInWorktree, } from "./automation/index.js";
33
34
  export { asyncAgentRegistry } from "./tool-system/builtin/agent-registry.js";
34
35
  export { backgroundShellManager, BackgroundShellManager, } from "./runtime/background-shell.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export const VERSION = "0.8.13";
6
+ export const VERSION = "0.8.20";
7
7
  // ─── Exceptions ──────────────────────────────────────────────────
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, CompositionError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  // ─── Composition (AgentModule / ResolvedComposition) ─────────────
@@ -82,7 +82,7 @@ export { mergePluginMcpServers, readPluginMcp } from "./plugins/installer/loadPl
82
82
  export { approvePluginMcp, listPluginMcpTrust, revokePluginMcp, } from "./plugins/pluginMcpApproval.js";
83
83
  export { pluginMcpApprovalState, } from "./plugins/pluginMcpIntegrity.js";
84
84
  export { pluginsRoot } from "./plugins/installer/paths.js";
85
- export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppManifest, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, previewLocalPanelApp, previewInstalledPanelAppUpdate, isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, uninstallPanelApp, } from "./panel-apps/index.js";
85
+ export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppManifest, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, previewLocalPanelApp, previewInstalledPanelAppUpdate, isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, uninstallPanelApp, } from "./panel-apps/index.js";
86
86
  export { previewLocalTheme, installReviewedLocalTheme, listInstalledThemes, uninstallTheme, themesRoot, themeInstallDir, assertSafeThemeName, ThemeInstallError, ThemeReviewChangedError, THEME_ASSET_DIR, THEME_VAR_NAMES, detectThemeImage, } from "./themes/index.js";
87
87
  export { resolveSafePluginPath } from "./plugins/pluginInstaller.js";
88
88
  export { listPluginHooks, pluginHookKey } from "./plugins/loadPluginHooks.js";
@@ -12,6 +12,7 @@ export declare abstract class LLMClientBase {
12
12
  readonly timeout: number;
13
13
  readonly retryMaxAttempts: number;
14
14
  readonly imageDetail?: ClientDefaults["imageDetail"];
15
+ protected readonly fetch?: ClientDefaults["fetch"];
15
16
  /**
16
17
  * Process-wide hook fired on every LLM response. The CLI installs this in
17
18
  * main.ts to feed the cost tracker; lives on the base class so every code
@@ -23,7 +24,7 @@ export declare abstract class LLMClientBase {
23
24
  /**
24
25
  * `config` carries model identity (provider/model/apiKey/baseUrl/maxTokens/
25
26
  * thinking/providerKind). `defaults` carries cross-model runtime knobs
26
- * (temperature/timeout/retryMaxAttempts/imageDetail) — those are owned by
27
+ * (temperature/timeout/retryMaxAttempts/imageDetail/fetch) — those are owned by
27
28
  * the Engine and stay stable across hot model switches.
28
29
  */
29
30
  constructor(config: LLMConfig, defaults?: ClientDefaults);
@@ -23,6 +23,7 @@ export class LLMClientBase {
23
23
  timeout;
24
24
  retryMaxAttempts;
25
25
  imageDetail;
26
+ fetch;
26
27
  /**
27
28
  * Process-wide hook fired on every LLM response. The CLI installs this in
28
29
  * main.ts to feed the cost tracker; lives on the base class so every code
@@ -34,7 +35,7 @@ export class LLMClientBase {
34
35
  /**
35
36
  * `config` carries model identity (provider/model/apiKey/baseUrl/maxTokens/
36
37
  * thinking/providerKind). `defaults` carries cross-model runtime knobs
37
- * (temperature/timeout/retryMaxAttempts/imageDetail) — those are owned by
38
+ * (temperature/timeout/retryMaxAttempts/imageDetail/fetch) — those are owned by
38
39
  * the Engine and stay stable across hot model switches.
39
40
  */
40
41
  constructor(config, defaults) {
@@ -51,6 +52,7 @@ export class LLMClientBase {
51
52
  this.timeout = defaults?.timeout ?? 120_000;
52
53
  this.retryMaxAttempts = defaults?.retryMaxAttempts ?? 3;
53
54
  this.imageDetail = defaults?.imageDetail;
55
+ this.fetch = defaults?.fetch;
54
56
  this.initClient();
55
57
  }
56
58
  recordUsage(usage, options) {
@@ -21,6 +21,25 @@ const ANTHROPIC_FALLBACK_MAX_TOKENS = 4096;
21
21
  * no explicit token budget was given. Clamped up to the model's minimum.
22
22
  */
23
23
  const ANTHROPIC_DEFAULT_THINKING_BUDGET = 4096;
24
+ /**
25
+ * Anthropic reports uncached input, cache writes, and cache reads as three
26
+ * disjoint counters. `TokenUsage.promptTokens` is the whole prompt throughout
27
+ * CodeShell (OpenAI includes cached tokens in prompt_tokens), so normalize the
28
+ * Anthropic shape before context accounting, compaction, cache-hit math, and
29
+ * cost tracking consume it.
30
+ */
31
+ function tokenUsageFromAnthropic(usage) {
32
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
33
+ const cacheCreationTokens = usage.cache_creation_input_tokens ?? 0;
34
+ const promptTokens = usage.input_tokens + cacheReadTokens + cacheCreationTokens;
35
+ return {
36
+ promptTokens,
37
+ completionTokens: usage.output_tokens,
38
+ totalTokens: promptTokens + usage.output_tokens,
39
+ ...(usage.cache_read_input_tokens != null ? { cacheReadTokens } : {}),
40
+ ...(usage.cache_creation_input_tokens != null ? { cacheCreationTokens } : {}),
41
+ };
42
+ }
24
43
  export class AnthropicClient extends LLMClientBase {
25
44
  _client = null;
26
45
  constructor(config, defaults) {
@@ -37,6 +56,7 @@ export class AnthropicClient extends LLMClientBase {
37
56
  apiKey: resolveApiKey(this.config, process.env.ANTHROPIC_API_KEY),
38
57
  ...(this.config.baseUrl ? { baseURL: this.config.baseUrl } : {}),
39
58
  ...(Object.keys(headers).length > 0 ? { defaultHeaders: headers } : {}),
59
+ ...(this.fetch ? { fetch: this.fetch } : {}),
40
60
  timeout: this.timeout,
41
61
  });
42
62
  }
@@ -106,9 +126,7 @@ export class AnthropicClient extends LLMClientBase {
106
126
  if (ceiling < min) {
107
127
  return undefined;
108
128
  }
109
- let budget = reasoning.mode === "budget"
110
- ? reasoning.budgetTokens
111
- : ANTHROPIC_DEFAULT_THINKING_BUDGET; // "on" or "effort" → default budget
129
+ let budget = reasoning.mode === "budget" ? reasoning.budgetTokens : ANTHROPIC_DEFAULT_THINKING_BUDGET; // "on" or "effort" → default budget
112
130
  // Clamp into [min, ceiling] — both bounds are now guaranteed ≥ min.
113
131
  budget = Math.min(Math.max(budget, min), ceiling);
114
132
  return { type: "enabled", budget_tokens: budget };
@@ -172,13 +190,7 @@ export class AnthropicClient extends LLMClientBase {
172
190
  ? { temperature: options.temperature }
173
191
  : { temperature: this.temperature }),
174
192
  }, { signal: requestSignal ?? options.signal });
175
- const usage = {
176
- promptTokens: response.usage.input_tokens,
177
- completionTokens: response.usage.output_tokens,
178
- totalTokens: response.usage.input_tokens + response.usage.output_tokens,
179
- cacheReadTokens: response.usage.cache_read_input_tokens,
180
- cacheCreationTokens: response.usage.cache_creation_input_tokens,
181
- };
193
+ const usage = tokenUsageFromAnthropic(response.usage);
182
194
  this.recordUsage(usage, options);
183
195
  return this.processResponse(response, usage);
184
196
  }
@@ -209,10 +221,8 @@ export class AnthropicClient extends LLMClientBase {
209
221
  ? { temperature: options.temperature }
210
222
  : { temperature: this.temperature }),
211
223
  }, { signal: requestSignal ?? options.signal });
212
- let currentText = "";
213
224
  let currentToolName = "";
214
225
  let currentToolId = "";
215
- let currentToolInput = "";
216
226
  // Abort-guarded emit: once the turn is cancelled, stop forwarding chunks
217
227
  // to the UI. The SDK's event emitter can keep firing buffered text/
218
228
  // contentBlock/inputJson events after abort() until its HTTP stream tears
@@ -249,14 +259,12 @@ export class AnthropicClient extends LLMClientBase {
249
259
  ttft_ms: Date.now() - streamStartedAt,
250
260
  });
251
261
  }
252
- currentText += text;
253
262
  emit({ type: "text", text, tokens: countTokens(text) });
254
263
  });
255
264
  stream.on("contentBlock", (block) => {
256
265
  if (block.type === "tool_use") {
257
266
  currentToolName = block.name;
258
267
  currentToolId = block.id;
259
- currentToolInput = "";
260
268
  emit({
261
269
  type: "tool_use_start",
262
270
  toolCall: { id: block.id, toolName: block.name, args: {} },
@@ -264,7 +272,6 @@ export class AnthropicClient extends LLMClientBase {
264
272
  }
265
273
  });
266
274
  stream.on("inputJson", (_delta, snapshot) => {
267
- currentToolInput = JSON.stringify(snapshot);
268
275
  if (currentToolId) {
269
276
  emit({
270
277
  type: "tool_use_delta",
@@ -277,13 +284,7 @@ export class AnthropicClient extends LLMClientBase {
277
284
  }
278
285
  });
279
286
  const finalMessage = await stream.finalMessage();
280
- const usage = {
281
- promptTokens: finalMessage.usage.input_tokens,
282
- completionTokens: finalMessage.usage.output_tokens,
283
- totalTokens: finalMessage.usage.input_tokens + finalMessage.usage.output_tokens,
284
- cacheReadTokens: finalMessage.usage.cache_read_input_tokens,
285
- cacheCreationTokens: finalMessage.usage.cache_creation_input_tokens,
286
- };
287
+ const usage = tokenUsageFromAnthropic(finalMessage.usage);
287
288
  this.recordUsage(usage, options);
288
289
  options.onChunk?.({ type: "stop", stopReason: finalMessage.stop_reason ?? undefined });
289
290
  return this.processResponse(finalMessage, usage);
@@ -415,9 +416,7 @@ export class AnthropicClient extends LLMClientBase {
415
416
  // marking them would 400. buildMessages never emits them today (thinking
416
417
  // is a top-level request field, not history content), but guard anyway
417
418
  // so a future block type can't silently break the request.
418
- if (lastBlock &&
419
- lastBlock.type !== "thinking" &&
420
- lastBlock.type !== "redacted_thinking") {
419
+ if (lastBlock && lastBlock.type !== "thinking" && lastBlock.type !== "redacted_thinking") {
421
420
  lastBlock.cache_control = { type: "ephemeral" };
422
421
  }
423
422
  }
@@ -49,9 +49,12 @@ interface RunStreamOpts {
49
49
  export declare function runStreamWithWatchdog<T = any>(stream: AsyncIterable<T>, opts?: RunStreamOpts): Promise<string>;
50
50
  export declare class OpenAIClient extends LLMClientBase {
51
51
  private _client;
52
+ private readonly dangerouslyAllowBrowser;
52
53
  private _forceMaxCompletionTokens;
53
54
  private _dropReasoningEffort;
54
- constructor(config: LLMConfig, defaults?: ClientDefaults);
55
+ constructor(config: LLMConfig, defaults?: ClientDefaults, runtimeOptions?: {
56
+ dangerouslyAllowBrowser?: boolean;
57
+ });
55
58
  protected initClient(): void;
56
59
  private get client();
57
60
  /**
@@ -41,6 +41,24 @@ function cachedTokensOf(usage) {
41
41
  out.cacheCreationTokens = details.cache_write_tokens;
42
42
  return out;
43
43
  }
44
+ /** Read the effective output-token ceiling from a built Chat Completions request. */
45
+ function outputTokenLimitOf(requestBody) {
46
+ const value = requestBody.max_completion_tokens ?? requestBody.max_tokens;
47
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
48
+ }
49
+ /**
50
+ * Some OpenAI-compatible gateways return finish_reason `tool_calls` when the
51
+ * output ceiling cuts a function-argument JSON string off. The malformed JSON
52
+ * plus exact cap usage is the evidence that distinguishes this from an ordinary
53
+ * completed tool call.
54
+ */
55
+ function didHitToolArgumentOutputLimit(input) {
56
+ return (input.finishReason === "tool_calls" &&
57
+ input.malformedToolArguments &&
58
+ input.outputTokenLimit !== undefined &&
59
+ input.completionTokens !== undefined &&
60
+ input.completionTokens >= input.outputTokenLimit);
61
+ }
44
62
  /**
45
63
  * Consume an async iterable of stream chunks with an idle watchdog.
46
64
  * Returns the accumulated text — either from onChunk return values, or
@@ -193,6 +211,7 @@ function normalizeOpenAIToolMessagePairs(messages) {
193
211
  }
194
212
  export class OpenAIClient extends LLMClientBase {
195
213
  _client = null;
214
+ dangerouslyAllowBrowser;
196
215
  // Sticky override: once the endpoint tells us `max_tokens` is rejected for
197
216
  // this model, switch to `max_completion_tokens` for the lifetime of the
198
217
  // client. Cheaper and more reliable than re-deriving from the model id when
@@ -205,8 +224,9 @@ export class OpenAIClient extends LLMClientBase {
205
224
  // succeed. Omitting the field just means "model default reasoning", which is
206
225
  // fine for our background/aux calls.
207
226
  _dropReasoningEffort = false;
208
- constructor(config, defaults) {
227
+ constructor(config, defaults, runtimeOptions = {}) {
209
228
  super(config, defaults);
229
+ this.dangerouslyAllowBrowser = runtimeOptions.dangerouslyAllowBrowser === true;
210
230
  }
211
231
  initClient() {
212
232
  // Lazy init — client created on first use
@@ -222,6 +242,8 @@ export class OpenAIClient extends LLMClientBase {
222
242
  (Object.keys(headers).length > 0 ? "x-headers-auth" : undefined),
223
243
  ...(this.config.baseUrl ? { baseURL: this.config.baseUrl } : {}),
224
244
  ...(Object.keys(headers).length > 0 ? { defaultHeaders: headers } : {}),
245
+ ...(this.fetch ? { fetch: this.fetch } : {}),
246
+ ...(this.dangerouslyAllowBrowser ? { dangerouslyAllowBrowser: true } : {}),
225
247
  timeout: this.timeout,
226
248
  });
227
249
  }
@@ -473,7 +495,8 @@ export class OpenAIClient extends LLMClientBase {
473
495
  }
474
496
  async nonStreamMessage(options, messages, tools, reasoning, requestSignal) {
475
497
  try {
476
- const response = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, false), { signal: requestSignal ?? options.signal });
498
+ const requestBody = this.buildRequestBody(options, messages, tools, reasoning, false);
499
+ const response = await this.client.chat.completions.create(requestBody, { signal: requestSignal ?? options.signal });
477
500
  const choice = response.choices[0];
478
501
  if (!choice)
479
502
  throw new LLMError("No response from OpenAI", "openai");
@@ -486,7 +509,7 @@ export class OpenAIClient extends LLMClientBase {
486
509
  ...cachedTokensOf(response.usage),
487
510
  };
488
511
  this.recordUsage(usage, options);
489
- return this.processChoice(choice, usage);
512
+ return this.processChoice(choice, usage, outputTokenLimitOf(requestBody));
490
513
  }
491
514
  catch (err) {
492
515
  this.handleApiError(err);
@@ -496,7 +519,9 @@ export class OpenAIClient extends LLMClientBase {
496
519
  async streamMessage(options, messages, tools, reasoning, requestSignal) {
497
520
  const sdkSignal = requestSignal ?? options.signal;
498
521
  try {
499
- const stream = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, true), { signal: sdkSignal });
522
+ const requestBody = this.buildRequestBody(options, messages, tools, reasoning, true);
523
+ const stream = await this.client.chat.completions.create(requestBody, { signal: sdkSignal });
524
+ const outputTokenLimit = outputTokenLimitOf(requestBody);
500
525
  let text = "";
501
526
  let reasoningContent = "";
502
527
  const toolCallsMap = new Map();
@@ -605,6 +630,7 @@ export class OpenAIClient extends LLMClientBase {
605
630
  signal: sdkSignal,
606
631
  });
607
632
  const toolCalls = [];
633
+ let malformedToolArguments = false;
608
634
  for (const [, tc] of toolCallsMap) {
609
635
  // Drop incomplete tool calls: an empty id or name (the fallbacks set
610
636
  // when a delta never delivered them) would become a malformed call
@@ -621,9 +647,16 @@ export class OpenAIClient extends LLMClientBase {
621
647
  args = JSON.parse(tc.args || "{}");
622
648
  }
623
649
  catch {
624
- /* intentional: model emitted malformed tool-call JSON — fall back to
625
- empty args rather than crashing the stream; the tool layer reports
626
- the resulting validation error back to the model. */
650
+ malformedToolArguments = true;
651
+ // Keep the historical empty-args fallback for genuinely malformed
652
+ // model output. If usage also proves the configured output ceiling was
653
+ // reached, the stop reason is normalized to "length" below so the turn
654
+ // loop retries instead of executing this misleading empty call.
655
+ logger.warn("openai.malformed_tool_arguments", {
656
+ id: tc.id,
657
+ name: tc.name,
658
+ argumentChars: tc.args.length,
659
+ });
627
660
  }
628
661
  toolCalls.push({ id: tc.id, toolName: tc.name, args });
629
662
  }
@@ -634,11 +667,31 @@ export class OpenAIClient extends LLMClientBase {
634
667
  ...cachedTokensOf(streamUsage),
635
668
  };
636
669
  this.recordUsage(usage, options);
670
+ const inferredToolArgumentTruncation = didHitToolArgumentOutputLimit({
671
+ finishReason,
672
+ malformedToolArguments,
673
+ completionTokens: usage.completionTokens,
674
+ outputTokenLimit,
675
+ });
676
+ if (inferredToolArgumentTruncation) {
677
+ logger.warn("openai.tool_arguments_truncation_inferred", {
678
+ provider: this.provider,
679
+ model: this.model,
680
+ finishReason,
681
+ completionTokens: usage.completionTokens,
682
+ outputTokenLimit,
683
+ toolCount: toolCalls.length,
684
+ });
685
+ }
637
686
  return {
638
687
  text,
639
688
  toolCalls,
640
689
  usage,
641
- stopReason: finishReason ?? "stop",
690
+ // OpenRouter can report `tool_calls` even when max_tokens cuts a tool's
691
+ // argument JSON off. Normalize that proven cap hit to the standard
692
+ // OpenAI `length` spelling so the shared continuation guard can skip
693
+ // execution and ask the model to retry with a smaller call.
694
+ stopReason: inferredToolArgumentTruncation ? "length" : (finishReason ?? "stop"),
642
695
  ...(reasoningContent ? { reasoningContent } : {}),
643
696
  };
644
697
  }
@@ -647,9 +700,10 @@ export class OpenAIClient extends LLMClientBase {
647
700
  throw err;
648
701
  }
649
702
  }
650
- processChoice(choice, usage) {
703
+ processChoice(choice, usage, outputTokenLimit) {
651
704
  const text = choice.message.content ?? "";
652
705
  const toolCalls = [];
706
+ let malformedToolArguments = false;
653
707
  if (choice.message.tool_calls) {
654
708
  for (const tc of choice.message.tool_calls) {
655
709
  let args = {};
@@ -657,9 +711,12 @@ export class OpenAIClient extends LLMClientBase {
657
711
  args = JSON.parse(tc.function.arguments || "{}");
658
712
  }
659
713
  catch {
660
- /* intentional: model emitted malformed tool-call JSON — fall back to
661
- empty args rather than crashing; the tool layer reports the
662
- resulting validation error back to the model. */
714
+ malformedToolArguments = true;
715
+ logger.warn("openai.malformed_tool_arguments", {
716
+ id: tc.id,
717
+ name: tc.function.name,
718
+ argumentChars: tc.function.arguments?.length ?? 0,
719
+ });
663
720
  }
664
721
  toolCalls.push({
665
722
  id: tc.id,
@@ -669,11 +726,17 @@ export class OpenAIClient extends LLMClientBase {
669
726
  }
670
727
  }
671
728
  const reasoningContent = extractReasoningContent(choice.message);
729
+ const inferredToolArgumentTruncation = didHitToolArgumentOutputLimit({
730
+ finishReason: choice.finish_reason ?? undefined,
731
+ malformedToolArguments,
732
+ completionTokens: usage.completionTokens,
733
+ outputTokenLimit,
734
+ });
672
735
  return {
673
736
  text,
674
737
  toolCalls,
675
738
  usage,
676
- stopReason: choice.finish_reason ?? undefined,
739
+ stopReason: inferredToolArgumentTruncation ? "length" : (choice.finish_reason ?? undefined),
677
740
  ...(reasoningContent ? { reasoningContent } : {}),
678
741
  };
679
742
  }
@@ -1,4 +1,4 @@
1
1
  export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppAgentContribution, PanelAppAgentTool, PanelAppManifest, type PanelAppAgentContribution as PanelAppAgentContributionData, type PanelAppAgentTool as PanelAppAgentToolData, type PanelAppManifest as PanelAppManifestData, } from "./manifest.js";
2
2
  export { PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, } from "./paths.js";
3
- export { installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, type InstalledPanelApp, type InstalledPanelAppSource, type GitPanelAppSourceInput, type LocalPanelAppSourceInput, type PanelAppSourceInput, type PanelAppPreview, } from "./installer.js";
3
+ export { discoverGitPanelApps, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, type InstalledPanelApp, type InstalledPanelAppSource, type GitPanelAppSourceInput, type GitPanelAppDiscovery, type GitPanelAppDiscoveryCandidate, type GitPanelAppDiscoveryIssue, type LocalPanelAppSourceInput, type PanelAppSourceInput, type PanelAppPreview, } from "./installer.js";
4
4
  export { isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, type PanelAppBindingPolicy, } from "./bindings.js";
@@ -1,4 +1,4 @@
1
1
  export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppAgentContribution, PanelAppAgentTool, PanelAppManifest, } from "./manifest.js";
2
2
  export { PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, } from "./paths.js";
3
- export { installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, } from "./installer.js";
3
+ export { discoverGitPanelApps, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, } from "./installer.js";
4
4
  export { isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, } from "./bindings.js";
@@ -33,6 +33,29 @@ export interface PanelAppPreview {
33
33
  };
34
34
  warnings: string[];
35
35
  }
36
+ export interface GitPanelAppDiscoveryCandidate {
37
+ /** Repository-relative path, or "." when the repository root is the app. */
38
+ subdir: string;
39
+ source: GitPanelAppSourceInput;
40
+ id: string;
41
+ version: string;
42
+ title: {
43
+ default: string;
44
+ en?: string;
45
+ "zh-CN"?: string;
46
+ };
47
+ description?: string;
48
+ icon: PanelAppManifest["icon"];
49
+ }
50
+ export interface GitPanelAppDiscoveryIssue {
51
+ subdir: string;
52
+ error: string;
53
+ }
54
+ export interface GitPanelAppDiscovery {
55
+ source: GitPanelAppSourceInput;
56
+ panels: GitPanelAppDiscoveryCandidate[];
57
+ issues: GitPanelAppDiscoveryIssue[];
58
+ }
36
59
  export interface InstalledPanelApp {
37
60
  id: string;
38
61
  version: string;
@@ -52,6 +75,12 @@ export interface InstalledPanelApp {
52
75
  installedAt: string;
53
76
  lastUpdated: string;
54
77
  }
78
+ /**
79
+ * Download a public GitHub repository once and enumerate every independently
80
+ * installable Panel App beneath it. Discovery is read-only: selecting a result
81
+ * still runs the normal full review and digest-bound install flow.
82
+ */
83
+ export declare function discoverGitPanelApps(input: GitPanelAppSourceInput): Promise<GitPanelAppDiscovery>;
55
84
  export declare function previewLocalPanelApp(input: PanelAppSourceInput): Promise<PanelAppPreview>;
56
85
  /**
57
86
  * Re-open the original folder, archive, or GitHub source through the same