@cjhyy/code-shell-core 0.6.0-rc.1 → 0.6.0-rc.11

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 (69) hide show
  1. package/dist/context/compaction.d.ts +30 -0
  2. package/dist/context/compaction.js +93 -0
  3. package/dist/context/manager.d.ts +18 -0
  4. package/dist/context/manager.js +156 -44
  5. package/dist/context/token-counter.js +13 -0
  6. package/dist/engine/engine.d.ts +22 -12
  7. package/dist/engine/engine.js +263 -81
  8. package/dist/engine/model-connections-pool.js +1 -0
  9. package/dist/engine/model-facade.js +2 -12
  10. package/dist/engine/query.js +2 -0
  11. package/dist/engine/runtime.d.ts +2 -0
  12. package/dist/engine/runtime.js +25 -0
  13. package/dist/engine/session-usage.d.ts +12 -0
  14. package/dist/engine/session-usage.js +56 -0
  15. package/dist/engine/steer-queue.d.ts +2 -1
  16. package/dist/engine/steer-queue.js +2 -2
  17. package/dist/engine/turn-loop.d.ts +28 -2
  18. package/dist/engine/turn-loop.js +153 -26
  19. package/dist/git/utils.d.ts +12 -0
  20. package/dist/git/utils.js +33 -6
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +4 -3
  23. package/dist/llm/capabilities/rules.js +1 -1
  24. package/dist/llm/model-pool.d.ts +7 -0
  25. package/dist/llm/model-pool.js +8 -1
  26. package/dist/model-catalog/builtin.js +6 -1
  27. package/dist/preset/index.d.ts +5 -1
  28. package/dist/preset/index.js +21 -2
  29. package/dist/prompt/composer.d.ts +5 -0
  30. package/dist/prompt/composer.js +10 -2
  31. package/dist/prompt/sections/base.md +1 -0
  32. package/dist/protocol/chat-session-manager.d.ts +1 -0
  33. package/dist/protocol/chat-session-manager.js +2 -0
  34. package/dist/protocol/chat-session.d.ts +4 -1
  35. package/dist/protocol/chat-session.js +9 -3
  36. package/dist/protocol/client.d.ts +5 -1
  37. package/dist/protocol/client.js +8 -2
  38. package/dist/protocol/server.d.ts +13 -12
  39. package/dist/protocol/server.js +199 -67
  40. package/dist/protocol/types.d.ts +14 -0
  41. package/dist/runtime/background-shell.js +14 -0
  42. package/dist/runtime/safe-spawn.js +89 -11
  43. package/dist/runtime/spawn-common.d.ts +15 -4
  44. package/dist/runtime/spawn-common.js +113 -12
  45. package/dist/session/session-manager.js +7 -1
  46. package/dist/session/transcript.d.ts +4 -0
  47. package/dist/session/transcript.js +21 -0
  48. package/dist/tool-system/builtin/bash.js +3 -2
  49. package/dist/tool-system/builtin/cron.js +10 -2
  50. package/dist/tool-system/builtin/edit-model-catalog.js +15 -5
  51. package/dist/tool-system/builtin/generate-video.js +3 -0
  52. package/dist/tool-system/builtin/grep.d.ts +9 -0
  53. package/dist/tool-system/builtin/grep.js +100 -3
  54. package/dist/tool-system/builtin/index.d.ts +3 -1
  55. package/dist/tool-system/builtin/index.js +5 -5
  56. package/dist/tool-system/builtin/powershell.js +4 -1
  57. package/dist/tool-system/builtin/sleep.js +5 -0
  58. package/dist/tool-system/context.d.ts +10 -0
  59. package/dist/tool-system/executor.js +25 -2
  60. package/dist/tool-system/mcp-manager.js +17 -0
  61. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  62. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  63. package/dist/tool-system/permission.d.ts +3 -1
  64. package/dist/tool-system/permission.js +2 -1
  65. package/dist/tool-system/sandbox/off.js +7 -1
  66. package/dist/types.d.ts +35 -1
  67. package/dist/utils/exec.d.ts +8 -0
  68. package/dist/utils/exec.js +10 -0
  69. package/package.json +1 -1
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.5.0-rc.2";
6
+ export declare const VERSION = "0.6.0-rc.8";
7
7
  export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  export { Engine, loadAgentDefinitionsForCwd } from "./engine/engine.js";
@@ -116,7 +116,7 @@ export { getGraphemeSegmenter, firstGrapheme, lastGrapheme, getWordSegmenter, ge
116
116
  export { env } from "./utils/env.js";
117
117
  export { default as sliceAnsi } from "./utils/sliceAnsi.js";
118
118
  export { execFileNoThrow } from "./utils/execFileNoThrow.js";
119
- export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, } from "./utils/exec.js";
119
+ export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
120
120
  export { gte } from "./utils/semver.js";
121
121
  export { lock, lockSync, unlock, check } from "./utils/lockfile.js";
122
122
  export { logForDebugging } from "./utils/debug.js";
@@ -134,7 +134,7 @@ export { CostTracker, costTracker, installCostTracking } from "./cost-tracker.js
134
134
  export { NOOP_COLORIZER, type Colorizer } from "./colorizer.js";
135
135
  export { hasApiKey, resolveApiKey, appendOnboardingResult, detectEnvKeys, saveArenaSettingsByKeys, type OnboardingResult, } from "./onboarding.js";
136
136
  export { getCurrentVersion, checkForUpdate, scheduleAutoInstallOnExit, getUpdateAvailable, getAutoUpdateDisabledReason, type UpdateInfo, } from "./updater.js";
137
- export { isGitRepo, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
137
+ export { isGitRepo, resolveProjectRoot, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
138
138
  export { buildReviewPrompt, parseDimensions, ALL_DIMENSIONS, type ReviewDimension, type ReviewPromptOptions, } from "./review/review-prompt.js";
139
139
  /**
140
140
  * @internal Exports below are consumed by the in-repo TUI/desktop hosts, not
@@ -153,6 +153,7 @@ export { resolveQuotaCredentials } from "./quota/credentials.js";
153
153
  export type { QuotaResult, ProviderQuota, QuotaWindow, QuotaCredentials } from "./quota/types.js";
154
154
  export { asyncAgentRegistry, type AsyncAgentEntry, } from "./tool-system/builtin/agent-registry.js";
155
155
  export { backgroundShellManager, BackgroundShellManager, type BgShell, type BgShellStatus, } from "./runtime/background-shell.js";
156
+ export { ENV_DENY_REGEX, ENV_ALLOWLIST } from "./runtime/spawn-common.js";
156
157
  export { getImageProvider, DEFAULT_IMAGE_MODEL, type ImageProvider, type ImageProviderCreds, type ImageGenerateRequest, type ImageGenerateResult, } from "./tool-system/builtin/image-providers.js";
157
158
  export { transcribe, type TranscribeCreds, type TranscribeRequest, type TranscribeResult, } from "./stt/transcribe.js";
158
159
  export { resolveTranscribeProvider, isTranscribeAvailable, describeTranscribe, type ResolvedTranscribeProvider, type TranscribeDescription, } from "./stt/resolve-transcribe.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export const VERSION = "0.5.0-rc.2";
6
+ export const VERSION = "0.6.0-rc.8";
7
7
  // ─── Exceptions ──────────────────────────────────────────────────
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  // ─── Engine (primary API) ────────────────────────────────────────
@@ -136,7 +136,7 @@ export { getGraphemeSegmenter, firstGrapheme, lastGrapheme, getWordSegmenter, ge
136
136
  export { env } from "./utils/env.js";
137
137
  export { default as sliceAnsi } from "./utils/sliceAnsi.js";
138
138
  export { execFileNoThrow } from "./utils/execFileNoThrow.js";
139
- export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, } from "./utils/exec.js";
139
+ export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
140
140
  export { gte } from "./utils/semver.js";
141
141
  // Cross-process file lock (proper-lockfile via createRequire, ESM-safe). Used by
142
142
  // RunLock / CronStore internally; exported so other writers of shared files
@@ -163,7 +163,7 @@ export { hasApiKey, resolveApiKey, appendOnboardingResult, detectEnvKeys, saveAr
163
163
  // ─── Updater ─────────────────────────────────────────────────────
164
164
  export { getCurrentVersion, checkForUpdate, scheduleAutoInstallOnExit, getUpdateAvailable, getAutoUpdateDisabledReason, } from "./updater.js";
165
165
  // ─── Git Utilities ───────────────────────────────────────────────
166
- export { isGitRepo, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
166
+ export { isGitRepo, resolveProjectRoot, getCurrentBranch, getGitStatus, getGitDiff, getGitDiffStat, getGitLog, gitAdd, gitCommit, gitListBranches, gitCheckout, ghAvailable, ghPrComments, } from "./git/utils.js";
167
167
  // ─── Code review (TODO 7.3) ─────────────────────────────────────
168
168
  export { buildReviewPrompt, parseDimensions, ALL_DIMENSIONS, } from "./review/review-prompt.js";
169
169
  // ─── Tool-system (extended for TUI) ─────────────────────────────
@@ -187,6 +187,7 @@ export { checkQuota, formatQuota } from "./quota/index.js";
187
187
  export { resolveQuotaCredentials } from "./quota/credentials.js";
188
188
  export { asyncAgentRegistry, } from "./tool-system/builtin/agent-registry.js";
189
189
  export { backgroundShellManager, BackgroundShellManager, } from "./runtime/background-shell.js";
190
+ export { ENV_DENY_REGEX, ENV_ALLOWLIST } from "./runtime/spawn-common.js";
190
191
  export { getImageProvider, DEFAULT_IMAGE_MODEL, } from "./tool-system/builtin/image-providers.js";
191
192
  // ─── Speech-to-text (voice input / 听写) ────────────────────────
192
193
  export { transcribe, } from "./stt/transcribe.js";
@@ -190,7 +190,7 @@ export const RULES = [
190
190
  // Most modern frontier models on OpenRouter are multimodal; flag the
191
191
  // big families explicitly. The catch-all below stays vision=false so
192
192
  // older/text-only routes don't get a green checkmark they don't earn.
193
- match: /^(?:openai\/(?:gpt-4o|gpt-5|o[1-9])|anthropic\/claude-(?:3|4|opus|sonnet|haiku)|google\/gemini-[2-9]|x-ai\/grok-[4-9]|qwen\/qwen-?vl|meta\/llama-(?:3\.2|4))/i,
193
+ match: /^~?(?:openai\/(?:gpt-4o|gpt-5|o[1-9])|anthropic\/claude-(?:3|4|opus|sonnet|haiku|fable)|google\/gemini-(?:[2-9]|pro)|x-ai\/grok-[4-9]|qwen\/qwen-?vl|meta\/llama-(?:3\.2|4))/i,
194
194
  capability: {
195
195
  supportsVision: true,
196
196
  reasoning: { kind: "openrouter-reasoning" },
@@ -21,6 +21,11 @@ export interface ModelEntry {
21
21
  provider: string;
22
22
  /** Full model path: "anthropic/claude-opus-4-6" */
23
23
  model: string;
24
+ /**
25
+ * Catalog/provider kind used by capability rules. This can differ from
26
+ * `provider`, which only selects the client protocol family.
27
+ */
28
+ providerKind?: string;
24
29
  baseUrl?: string;
25
30
  apiKey?: string;
26
31
  /** Whether the catalog template requires a key (default true; false = local/
@@ -79,6 +84,8 @@ export declare class ModelPool {
79
84
  * The first entry (or the one matching `defaultKey`) becomes active.
80
85
  */
81
86
  constructor(entries?: ModelEntry[], defaultKey?: string);
87
+ /** Clear all registered models and active selection. */
88
+ clear(): void;
82
89
  /** Register a model at runtime. */
83
90
  register(entry: ModelEntry): void;
84
91
  /** Fill in known defaults (e.g. context window for DeepSeek V4) when the entry doesn't specify them. */
@@ -142,6 +142,11 @@ export class ModelPool {
142
142
  this.activeKey = entries[0].key;
143
143
  }
144
144
  }
145
+ /** Clear all registered models and active selection. */
146
+ clear() {
147
+ this.models.clear();
148
+ this.activeKey = undefined;
149
+ }
145
150
  /** Register a model at runtime. */
146
151
  register(entry) {
147
152
  this.models.set(entry.key, this.withBuiltinDefaults(entry));
@@ -216,6 +221,7 @@ export class ModelPool {
216
221
  return "anthropic";
217
222
  return "openai";
218
223
  };
224
+ const providerKind = entry.providerKind ?? fromCat?.kind;
219
225
  return {
220
226
  provider: entry.provider ||
221
227
  kindToClientProvider(fromCat?.kind) ||
@@ -228,6 +234,7 @@ export class ModelPool {
228
234
  // instead of fabricating 8192, which silently truncates long outputs and
229
235
  // masks the real per-model cap.
230
236
  maxTokens: entry.maxOutputTokens,
237
+ ...(entry.maxContextTokens !== undefined ? { maxContextTokens: entry.maxContextTokens } : {}),
231
238
  // reasoning: entry overrides catalog. No base fallback — see class doc.
232
239
  ...(entry.reasoning ?? fromCat?.reasoning
233
240
  ? { reasoning: entry.reasoning ?? fromCat?.reasoning }
@@ -244,7 +251,7 @@ export class ModelPool {
244
251
  ...(entry.reasoningSummary ? { reasoningSummary: entry.reasoningSummary } : {}),
245
252
  // Carry the catalog kind through so the capability layer can pick
246
253
  // per-(kind, model) request-shape rules.
247
- ...(fromCat?.kind ? { providerKind: fromCat.kind } : {}),
254
+ ...(providerKind ? { providerKind } : {}),
248
255
  ...(entry.extraBody && Object.keys(entry.extraBody).length > 0
249
256
  ? { extraBody: entry.extraBody }
250
257
  : {}),
@@ -8,6 +8,7 @@
8
8
  * ~/.code-shell/model-catalog.user.json) — no UI / adapter changes, as long as
9
9
  * its adapterKind points at an already-wired adapter.
10
10
  */
11
+ import { capabilitiesFor } from "../llm/capabilities/index.js";
11
12
  import { paramSpecsFromCapability } from "../llm/capabilities/param-specs.js";
12
13
  /**
13
14
  * Build a text model preset, projecting its params from the capability layer
@@ -15,11 +16,13 @@ import { paramSpecsFromCapability } from "../llm/capabilities/param-specs.js";
15
16
  * when the model has none, matching the "absent → no knobs" contract.
16
17
  */
17
18
  function textPreset(kind, value, label, ctx) {
19
+ const capability = capabilitiesFor(kind, value);
18
20
  const params = paramSpecsFromCapability(kind, value);
19
21
  return {
20
22
  value,
21
23
  ...(label ? { label } : {}),
22
24
  ...(ctx !== undefined ? { maxContextTokens: ctx } : {}),
25
+ supportsVision: capability.supportsVision,
23
26
  ...(params.length > 0 ? { params } : {}),
24
27
  };
25
28
  }
@@ -97,14 +100,16 @@ const ZHIPU_PARAMS = [
97
100
  ];
98
101
  /** Zhipu GLM preset — shared ZHIPU_PARAMS, with per-model context window. */
99
102
  function glmPreset(value, label, ctx) {
100
- return { value, label, maxContextTokens: ctx, params: ZHIPU_PARAMS };
103
+ return { value, label, maxContextTokens: ctx, supportsVision: false, params: ZHIPU_PARAMS };
101
104
  }
102
105
  /** OpenRouter preset with explicit params (no capability-layer projection). */
103
106
  function orPreset(value, label, params, ctx) {
107
+ const capability = capabilitiesFor("openrouter", value);
104
108
  return {
105
109
  value,
106
110
  label,
107
111
  ...(ctx !== undefined ? { maxContextTokens: ctx } : {}),
112
+ supportsVision: capability.supportsVision,
108
113
  ...(params && params.length > 0 ? { params } : {}),
109
114
  };
110
115
  }
@@ -49,6 +49,10 @@ export declare function registerPreset(preset: AgentPreset): void;
49
49
  /** List all available preset names (built-in + custom). */
50
50
  export declare function listPresetNames(): string[];
51
51
  export declare function resolveAgentPreset(name?: string): AgentPreset;
52
+ export interface BuildPresetSystemPromptOptions {
53
+ activeToolNames?: readonly string[];
54
+ platform?: NodeJS.Platform;
55
+ }
52
56
  /**
53
57
  * Build the full system prompt for a preset by loading and joining its sections.
54
58
  *
@@ -58,7 +62,7 @@ export declare function resolveAgentPreset(name?: string): AgentPreset;
58
62
  * (tools + usage instructions disappear together). Omit `activeToolNames` to
59
63
  * include all sections (e.g. when assembling a generic/preview prompt).
60
64
  */
61
- export declare function buildPresetSystemPrompt(preset: AgentPreset, activeToolNames?: readonly string[]): string;
65
+ export declare function buildPresetSystemPrompt(preset: AgentPreset, optionsOrActiveToolNames?: BuildPresetSystemPromptOptions | readonly string[]): string;
62
66
  export declare function resolveBuiltinToolNames(options?: {
63
67
  preset?: string;
64
68
  enabledBuiltinTools?: string[];
@@ -232,6 +232,19 @@ const BROWSER_SECTION_TOOLS = ["browser_observe", "browser_act", "browser_naviga
232
232
  const TOOL_GATED_SECTIONS = {
233
233
  browser: BROWSER_SECTION_TOOLS,
234
234
  };
235
+ function isActiveToolNamesList(value) {
236
+ return Array.isArray(value);
237
+ }
238
+ function platformShellGuidance(platform) {
239
+ if (platform !== "win32")
240
+ return "";
241
+ return [
242
+ "# Windows Shell Guidance",
243
+ "- On Windows, Bash uses Git Bash when it is available. Prefer Bash for ordinary shell commands, git operations, package-manager commands, tests, and POSIX-style command lines.",
244
+ "- Do not choose PowerShell merely because the OS is Windows. Use PowerShell only when the user explicitly asks for it or the task requires PowerShell-specific cmdlets, Windows APIs, registry access, or `.ps1` behavior.",
245
+ "- When writing Bash commands on Windows, use Git Bash paths such as `/d/github/project` instead of PowerShell-only syntax or raw `D:\\...` paths inside `cd` commands.",
246
+ ].join("\n");
247
+ }
235
248
  /**
236
249
  * Build the full system prompt for a preset by loading and joining its sections.
237
250
  *
@@ -241,8 +254,12 @@ const TOOL_GATED_SECTIONS = {
241
254
  * (tools + usage instructions disappear together). Omit `activeToolNames` to
242
255
  * include all sections (e.g. when assembling a generic/preview prompt).
243
256
  */
244
- export function buildPresetSystemPrompt(preset, activeToolNames) {
257
+ export function buildPresetSystemPrompt(preset, optionsOrActiveToolNames) {
245
258
  let sections = preset.promptSections;
259
+ const options = isActiveToolNamesList(optionsOrActiveToolNames)
260
+ ? { activeToolNames: optionsOrActiveToolNames }
261
+ : (optionsOrActiveToolNames ?? {});
262
+ const activeToolNames = options.activeToolNames;
246
263
  if (activeToolNames) {
247
264
  const active = new Set(activeToolNames);
248
265
  sections = sections.filter((s) => {
@@ -250,7 +267,9 @@ export function buildPresetSystemPrompt(preset, activeToolNames) {
250
267
  return !gate || gate.some((t) => active.has(t));
251
268
  });
252
269
  }
253
- return loadSections(sections);
270
+ return [loadSections(sections), platformShellGuidance(options.platform ?? process.platform)]
271
+ .filter(Boolean)
272
+ .join("\n\n");
254
273
  }
255
274
  export function resolveBuiltinToolNames(options) {
256
275
  const preset = resolveAgentPreset(options?.preset);
@@ -41,6 +41,10 @@ export interface ComposerOptions {
41
41
  * dispatch gate.
42
42
  */
43
43
  skillAllowlist?: string[];
44
+ /** Volatile goal state used only in the trailing dynamic-context message. */
45
+ goalToolState?: {
46
+ hasGoal: boolean;
47
+ };
44
48
  /**
45
49
  * settings.memories.maxAge (days). When > 0, memories whose file mtime is
46
50
  * older than this are dropped from the injected memory context (TODO 8.1).
@@ -79,6 +83,7 @@ export declare class PromptComposer {
79
83
  * breakpoint — a change here never re-bills the history prefix.
80
84
  */
81
85
  buildDynamicContextMessage(): Promise<Message | null>;
86
+ private buildGoalToolContext;
82
87
  invalidateCache(sectionName?: string): void;
83
88
  private getSections;
84
89
  private getInstructions;
@@ -113,7 +113,8 @@ export class PromptComposer {
113
113
  // prefix — so a memory change (extraction / recall usage++ / approve) never
114
114
  // re-bills the cached prefix. See buildUserContextMessage for the rationale.
115
115
  const memoryContext = this.getMemoryContext();
116
- const parts = [skillsListing, gitStatus, memoryContext].filter(Boolean);
116
+ const goalToolContext = this.buildGoalToolContext();
117
+ const parts = [skillsListing, gitStatus, memoryContext, goalToolContext].filter(Boolean);
117
118
  if (parts.length === 0)
118
119
  return null;
119
120
  return {
@@ -121,6 +122,13 @@ export class PromptComposer {
121
122
  content: `<system-reminder>\n${parts.join("\n\n")}\n</system-reminder>`,
122
123
  };
123
124
  }
125
+ buildGoalToolContext() {
126
+ if (!this.options.goalToolState)
127
+ return "";
128
+ return this.options.goalToolState.hasGoal
129
+ ? "当前存在 active goal。只有在目标完全完成时才可调用 complete_goal;只有用户明确要求取消/停止/放弃该目标时才可调用 cancel_goal。"
130
+ : "当前没有 active goal。不要调用 complete_goal/cancel_goal;如果误调用,系统会拒绝。";
131
+ }
124
132
  invalidateCache(sectionName) {
125
133
  this.sectionCache.invalidate(sectionName);
126
134
  if (!sectionName) {
@@ -179,7 +187,7 @@ export class PromptComposer {
179
187
  cacheBreak: true,
180
188
  compute: () => {
181
189
  const preset = this.options.preset ?? resolveAgentPreset();
182
- return buildPresetSystemPrompt(preset, activeToolNames);
190
+ return buildPresetSystemPrompt(preset, { activeToolNames, platform: process.platform });
183
191
  },
184
192
  });
185
193
  // Skills listing is intentionally NOT a system section: it changes when a
@@ -31,6 +31,7 @@ When you encounter an obstacle, do not use destructive actions as a shortcut to
31
31
  - To search for files use Glob instead of find or ls
32
32
  - To search the content of files, use Grep instead of grep or rg
33
33
  - Reserve using the Bash exclusively for system commands and terminal operations that require shell execution.
34
+ - Shell choice: use Bash for ordinary shell commands, git operations, package-manager commands, test/build scripts, and POSIX-style command lines. Use PowerShell only when the user explicitly asks for it or the task requires PowerShell-specific cmdlets, Windows APIs, registry access, or `.ps1` behavior.
34
35
  - For long-lived processes that don't exit on their own — a dev server (`npm run dev`, `vite`), a watcher, a tunnel — call Bash with `run_in_background: true`. It returns a `shell_id` immediately instead of blocking until a timeout. Then use `BashOutput(shell_id)` to read its logs (e.g. to confirm it started or to see an error), `ListShells()` to see what's running, and `KillShell(shell_id)` to stop it. Never run such a command in the foreground — it will just block until it's killed. Plain one-shot commands (build, test, git) stay foreground.
35
36
  - Break down and manage multi-step work with the TodoWrite tool. Pass the complete todo list each call; rewrite it as items move pending → in_progress → completed.
36
37
  - You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel.
@@ -17,6 +17,7 @@ export interface ChatSessionManagerOptions {
17
17
  }
18
18
  export declare class ChatSessionManager {
19
19
  private readonly sessions;
20
+ readonly runtime: EngineRuntime;
20
21
  private readonly factory;
21
22
  private readonly maxSessions;
22
23
  private readonly idleTtlMs;
@@ -3,11 +3,13 @@ import { backgroundShellManager } from "../runtime/background-shell.js";
3
3
  import { clearAgentOutputFiles } from "../tool-system/builtin/agent-output-file.js";
4
4
  export class ChatSessionManager {
5
5
  sessions = new Map();
6
+ runtime;
6
7
  factory;
7
8
  maxSessions;
8
9
  idleTtlMs;
9
10
  sweeper = null;
10
11
  constructor(opts) {
12
+ this.runtime = opts.runtime;
11
13
  this.factory = opts.engineFactory;
12
14
  this.maxSessions = opts.maxSessions ?? 16;
13
15
  this.idleTtlMs = opts.idleTtlMs ?? 30 * 60 * 1000;
@@ -1,4 +1,5 @@
1
1
  import type { Engine, EngineResult } from "../engine/engine.js";
2
+ import type { ModelEntry } from "../llm/model-pool.js";
2
3
  import type { StreamEvent } from "../types.js";
3
4
  export interface ChatSessionOptions {
4
5
  id: string;
@@ -17,6 +18,8 @@ export interface TurnOpts {
17
18
  * completion notification) rather than the user's own input — persisted so
18
19
  * the disk reader skips it as a user bubble on replay. See Engine.run. */
19
20
  injected?: boolean;
21
+ /** Stable id for this user-intent; forwarded to Engine.run for idempotency. */
22
+ clientMessageId?: string;
20
23
  }
21
24
  /**
22
25
  * One ChatSession per UI chat tab. Owns a single Engine, an AbortController
@@ -116,7 +119,7 @@ export declare class ChatSession {
116
119
  * turn is in flight so a hot switch never mutates the model under a
117
120
  * running LLM client.
118
121
  */
119
- requestModelSwitch(key: string): void;
122
+ requestModelSwitch(key: string): ModelEntry;
120
123
  /**
121
124
  * Perform the actual model switch for this session: rotate the engine's model
122
125
  * and reset the session's cumulative usage on disk — a different model has a
@@ -126,10 +126,14 @@ export class ChatSession {
126
126
  */
127
127
  requestModelSwitch(key) {
128
128
  if (this.isBusy()) {
129
+ const pool = this.engine.getModelPool();
130
+ const entry = pool.get(key);
131
+ if (!entry)
132
+ throw new Error(`Model not found: ${key}`);
129
133
  this.pendingModel = key;
130
- return;
134
+ return entry;
131
135
  }
132
- this.applyModelSwitch(key);
136
+ return this.applyModelSwitch(key);
133
137
  }
134
138
  /**
135
139
  * Perform the actual model switch for this session: rotate the engine's model
@@ -140,8 +144,9 @@ export class ChatSession {
140
144
  * path and the deferred run-boundary path.
141
145
  */
142
146
  applyModelSwitch(key) {
143
- this.engine.switchModel(key);
147
+ const entry = this.engine.switchModel(key);
144
148
  this.engine.resetSessionUsage(this.id);
149
+ return entry;
145
150
  }
146
151
  queueDepth() {
147
152
  return this.queue.length;
@@ -164,6 +169,7 @@ export class ChatSession {
164
169
  onStream,
165
170
  goal: next.opts.goal,
166
171
  injected: next.opts.injected,
172
+ clientMessageId: next.opts.clientMessageId,
167
173
  });
168
174
  this.lastActivityAt = Date.now();
169
175
  next.resolve(result);
@@ -23,6 +23,7 @@ export interface AgentRunOptions {
23
23
  cwd?: string;
24
24
  sessionId?: string;
25
25
  permissionMode?: PermissionMode;
26
+ model?: string;
26
27
  planMode?: boolean;
27
28
  }
28
29
  export type BackgroundAgentCompletedHandler = (sessionId: string, event: BackgroundAgentCompletedEvent) => void;
@@ -84,7 +85,10 @@ export declare class AgentClient {
84
85
  * the desktop preload's steer(); without it the SDK can't reach the method
85
86
  * (request() is private) — the protocol was asymmetric after the steer merge.
86
87
  */
87
- steer(sessionId: string, text: string, id?: string): Promise<void>;
88
+ steer(sessionId: string, text: string, id?: string, clientMessageId?: string): Promise<{
89
+ accepted: boolean;
90
+ id?: string;
91
+ }>;
88
92
  /**
89
93
  * Revoke a still-pending steer entry by id (before the loop consumes it).
90
94
  * Returns false when the loop already consumed it (not an error — it will
@@ -125,8 +125,14 @@ export class AgentClient {
125
125
  * the desktop preload's steer(); without it the SDK can't reach the method
126
126
  * (request() is private) — the protocol was asymmetric after the steer merge.
127
127
  */
128
- async steer(sessionId, text, id) {
129
- await this.request(Methods.Steer, { sessionId, text, id });
128
+ async steer(sessionId, text, id, clientMessageId) {
129
+ const res = (await this.request(Methods.Steer, {
130
+ sessionId,
131
+ text,
132
+ id,
133
+ clientMessageId,
134
+ }));
135
+ return { accepted: res?.accepted === true, id: res?.id };
130
136
  }
131
137
  /**
132
138
  * Revoke a still-pending steer entry by id (before the loop consumes it).
@@ -180,14 +180,11 @@ export declare class AgentServer {
180
180
  */
181
181
  private requestApprovalFromClient;
182
182
  /**
183
- * Ask the client to answer a question from the agent (legacy single-engine path).
184
- */
185
- /**
186
- * Per-session AskUserQuestion for the chatManager path. Mirrors
187
- * requestAskUserFromClient but resolves via the SESSION's pendingApprovals
188
- * (the chatManager approve handler looks there, keyed by sessionId+requestId)
189
- * and tags the notify with sessionId so the renderer routes the question to
190
- * the right chat tab.
183
+ * Per-session AskUserQuestion for the chatManager path. Resolves via the
184
+ * SESSION's pendingApprovals (the chatManager approve handler looks there,
185
+ * keyed by sessionId+requestId) and tags the notify with sessionId so the
186
+ * renderer routes the question to the right chat tab. This intentionally has
187
+ * no wall-clock timeout; Stop/cancel drains the pending ask.
191
188
  */
192
189
  private requestAskUserForSession;
193
190
  /**
@@ -206,6 +203,11 @@ export declare class AgentServer {
206
203
  * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
207
204
  */
208
205
  private requestCredentialInjectForSession;
206
+ /**
207
+ * Ask the client to answer a question from the agent (legacy single-engine
208
+ * path). This intentionally has no wall-clock timeout; Stop/cancel drains
209
+ * the pending ask.
210
+ */
209
211
  private requestAskUserFromClient;
210
212
  private notify;
211
213
  /**
@@ -220,11 +222,10 @@ export declare class AgentServer {
220
222
  private clearApprovalTimer;
221
223
  /**
222
224
  * Resolve all of a chat session's pending approvals as cancelled and clear
223
- * the matching server-side approval timers. Used by handleCancel's
225
+ * any matching server-side approval timers. Used by handleCancel's
224
226
  * per-session path so a Stop while a tool is awaiting approval doesn't leave
225
- * the tool hanging until APPROVAL_TIMEOUT_MS (and leak the timer). Each
226
- * requestId in the session map has a same-keyed entry in this.approvalTimers
227
- * (see requestAskUserForSession / makeBrowserBridge).
227
+ * the tool hanging. Bounded request types have same-keyed timer entries;
228
+ * AskUserQuestion does not.
228
229
  */
229
230
  private cancelSessionApprovals;
230
231
  private clearAllApprovalTimers;