@cjhyy/code-shell-core 0.6.0-rc.3 → 0.6.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/engine.js +15 -1
- package/dist/engine/runtime.d.ts +2 -0
- package/dist/engine/runtime.js +25 -0
- package/dist/git/utils.d.ts +12 -0
- package/dist/git/utils.js +33 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/llm/model-pool.d.ts +2 -0
- package/dist/llm/model-pool.js +6 -0
- package/dist/prompt/composer.d.ts +5 -0
- package/dist/prompt/composer.js +9 -1
- package/dist/prompt/sections/base.md +1 -0
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +2 -0
- package/dist/protocol/chat-session.d.ts +2 -1
- package/dist/protocol/chat-session.js +8 -3
- package/dist/protocol/client.d.ts +1 -0
- package/dist/protocol/server.js +80 -15
- package/dist/protocol/types.d.ts +8 -0
- package/dist/runtime/spawn-common.d.ts +6 -1
- package/dist/runtime/spawn-common.js +68 -5
- package/dist/tool-system/builtin/bash.js +4 -2
- package/dist/tool-system/builtin/index.d.ts +3 -1
- package/dist/tool-system/builtin/index.js +5 -5
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/context.d.ts +10 -0
- package/dist/tool-system/executor.js +21 -0
- package/dist/tool-system/sandbox/off.js +5 -3
- package/dist/types.d.ts +2 -0
- package/dist/utils/exec.d.ts +8 -0
- package/dist/utils/exec.js +10 -0
- package/package.json +1 -1
package/dist/engine/engine.js
CHANGED
|
@@ -1323,6 +1323,12 @@ export class Engine {
|
|
|
1323
1323
|
disabledPlugins,
|
|
1324
1324
|
skillAllowlist: this.config.skillAllowlist,
|
|
1325
1325
|
memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
|
|
1326
|
+
goalToolState: {
|
|
1327
|
+
hasGoal: this.config.isSubAgent !== true &&
|
|
1328
|
+
(normalizeGoal(options?.goal) !== undefined ||
|
|
1329
|
+
session.state.activeGoal !== undefined ||
|
|
1330
|
+
normalizeGoal(this.config.goal) !== undefined),
|
|
1331
|
+
},
|
|
1326
1332
|
});
|
|
1327
1333
|
// Connect MCP servers (if configured and not already connected).
|
|
1328
1334
|
// B1: prefer the Runtime-owned MCPManager so all sessions in a
|
|
@@ -1358,6 +1364,14 @@ export class Engine {
|
|
|
1358
1364
|
// cwd. Recomputed every message, so configuring a key takes effect on the
|
|
1359
1365
|
// NEXT message without a restart. Tools with no guard entry are always kept.
|
|
1360
1366
|
const guardCwd = toolCtx.cwd;
|
|
1367
|
+
const toolVisibility = {
|
|
1368
|
+
cwd: guardCwd,
|
|
1369
|
+
hasGoal: this.config.isSubAgent !== true &&
|
|
1370
|
+
(normalizeGoal(options?.goal) !== undefined ||
|
|
1371
|
+
session.state.activeGoal !== undefined ||
|
|
1372
|
+
normalizeGoal(this.config.goal) !== undefined),
|
|
1373
|
+
};
|
|
1374
|
+
toolCtx.toolVisibility = toolVisibility;
|
|
1361
1375
|
// #7: per-turn project builtin override. The toolRegistry's builtin tool
|
|
1362
1376
|
// SET is ctor-frozen (and may be shared via runtime), so a mid-session
|
|
1363
1377
|
// project override of a builtin can't rebuild the registry. But the tool
|
|
@@ -1407,7 +1421,7 @@ export class Engine {
|
|
|
1407
1421
|
.filter((t) => mcpVisible(t.name))
|
|
1408
1422
|
.filter((t) => {
|
|
1409
1423
|
const guard = BUILTIN_TOOL_GUARDS.get(t.name);
|
|
1410
|
-
return guard ? guard(
|
|
1424
|
+
return guard ? guard(toolVisibility) : true;
|
|
1411
1425
|
})
|
|
1412
1426
|
.filter((t) => {
|
|
1413
1427
|
const flag = TOOL_FEATURE_FLAGS.get(t.name);
|
package/dist/engine/runtime.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class EngineRuntime {
|
|
|
23
23
|
readonly costTracker: CostTracker;
|
|
24
24
|
private sandboxCache;
|
|
25
25
|
constructor(opts: EngineRuntimeOptions);
|
|
26
|
+
clearModels(): void;
|
|
27
|
+
reloadModelsFromSettings(): void;
|
|
26
28
|
/**
|
|
27
29
|
* Lazily resolve and cache a sandbox backend for `(mode, cwd)`.
|
|
28
30
|
*
|
package/dist/engine/runtime.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { getMergedCatalog } from "../model-catalog/index.js";
|
|
2
|
+
import { modelEntriesFromConnections } from "./model-connections-pool.js";
|
|
3
|
+
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
1
4
|
import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
2
5
|
import { sandboxCacheKey } from "./sandbox-cache-key.js";
|
|
3
6
|
/**
|
|
@@ -22,6 +25,28 @@ export class EngineRuntime {
|
|
|
22
25
|
this.mcpPool = opts.mcpPool;
|
|
23
26
|
this.costTracker = opts.costTracker;
|
|
24
27
|
}
|
|
28
|
+
clearModels() {
|
|
29
|
+
this.modelPool.clear();
|
|
30
|
+
}
|
|
31
|
+
reloadModelsFromSettings() {
|
|
32
|
+
const settings = this.settings.load();
|
|
33
|
+
const connections = settings.modelConnections ?? [];
|
|
34
|
+
const credentials = settings.credentials ?? [];
|
|
35
|
+
const catalog = getMergedCatalog();
|
|
36
|
+
const entries = modelEntriesFromConnections(connections, credentials, catalog);
|
|
37
|
+
this.modelPool.clear();
|
|
38
|
+
if (entries.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
for (const entry of entries)
|
|
41
|
+
this.modelPool.register(entry);
|
|
42
|
+
if (entries.length > 0) {
|
|
43
|
+
const defaultText = settings.defaults?.text;
|
|
44
|
+
const key = defaultText && entries.some((e) => e.key === defaultText) ? defaultText : entries[0].key;
|
|
45
|
+
this.modelPool.switch(key);
|
|
46
|
+
this.modelPool.setCacheDir(defaultCacheDir());
|
|
47
|
+
this.modelPool.reloadCachedContextWindows();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
25
50
|
/**
|
|
26
51
|
* Lazily resolve and cache a sandbox backend for `(mode, cwd)`.
|
|
27
52
|
*
|
package/dist/git/utils.d.ts
CHANGED
|
@@ -14,6 +14,18 @@ export interface GitStatusEntry {
|
|
|
14
14
|
}
|
|
15
15
|
export type { GitLogEntry };
|
|
16
16
|
export declare function isGitRepo(cwd: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a directory to its PROJECT ROOT: the enclosing git repository's
|
|
19
|
+
* top-level dir if `cwd` is inside a git repo, otherwise `cwd` unchanged.
|
|
20
|
+
*
|
|
21
|
+
* This is the project-boundary rule the desktop uses when adding/identifying a
|
|
22
|
+
* project: picking a SUBDIRECTORY of a git repo should belong to that one repo
|
|
23
|
+
* (its root), not spawn a separate project per subdir — mirrors how editors
|
|
24
|
+
* (and Claude Code) treat a repo as one workspace. A non-git folder is its own
|
|
25
|
+
* project (returned as-is). Never throws; on any git failure falls back to cwd.
|
|
26
|
+
* Returns the git-reported toplevel (already absolute, forward-slashed on win).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveProjectRoot(cwd: string): string;
|
|
17
29
|
export declare function getCurrentBranch(cwd: string): string;
|
|
18
30
|
export declare function getGitStatus(cwd: string): GitStatusEntry[];
|
|
19
31
|
export declare function getGitDiff(cwd: string, opts?: {
|
package/dist/git/utils.js
CHANGED
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
* into the string form.
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
11
12
|
import { parseGitLog } from "./parse-log.js";
|
|
12
|
-
import { resolveExecutable } from "../utils/exec.js";
|
|
13
|
+
import { resolveExecutable, resolveGit } from "../utils/exec.js";
|
|
13
14
|
// Resolve git/gh through PATH×PATHEXT on Windows so a .cmd/.exe shim is found
|
|
14
15
|
// (bare execFile doesn't walk PATHEXT). No-op on POSIX. See utils/exec.ts.
|
|
15
|
-
const GIT_BIN = resolveExecutable("git");
|
|
16
16
|
const GH_BIN = resolveExecutable("gh");
|
|
17
17
|
/** Run git with an argv array and return its trimmed stdout. */
|
|
18
18
|
function git(cwd, args, timeoutMs = 10000) {
|
|
19
|
-
return execFileSync(
|
|
19
|
+
return execFileSync(resolveGit(), args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
|
|
20
20
|
}
|
|
21
21
|
/** Run gh with an argv array and return its trimmed stdout. */
|
|
22
22
|
function gh(cwd, args, timeoutMs = 10000) {
|
|
@@ -31,6 +31,33 @@ export function isGitRepo(cwd) {
|
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a directory to its PROJECT ROOT: the enclosing git repository's
|
|
36
|
+
* top-level dir if `cwd` is inside a git repo, otherwise `cwd` unchanged.
|
|
37
|
+
*
|
|
38
|
+
* This is the project-boundary rule the desktop uses when adding/identifying a
|
|
39
|
+
* project: picking a SUBDIRECTORY of a git repo should belong to that one repo
|
|
40
|
+
* (its root), not spawn a separate project per subdir — mirrors how editors
|
|
41
|
+
* (and Claude Code) treat a repo as one workspace. A non-git folder is its own
|
|
42
|
+
* project (returned as-is). Never throws; on any git failure falls back to cwd.
|
|
43
|
+
* Returns the git-reported toplevel (already absolute, forward-slashed on win).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveProjectRoot(cwd) {
|
|
46
|
+
let realCwd = cwd;
|
|
47
|
+
try {
|
|
48
|
+
realCwd = realpathSync(cwd);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Non-existent paths are allowed to fall back unchanged below.
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const top = git(realCwd, ["rev-parse", "--show-toplevel"], 5000);
|
|
55
|
+
return top ? realpathSync(top) : realCwd;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return realCwd;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
34
61
|
export function getCurrentBranch(cwd) {
|
|
35
62
|
return git(cwd, ["branch", "--show-current"], 5000);
|
|
36
63
|
}
|
|
@@ -82,13 +109,13 @@ export function gitAdd(cwd, files = ["."]) {
|
|
|
82
109
|
// `--` ensures a path starting with `-` cannot be parsed as a flag.
|
|
83
110
|
// Each file is its own argv token, so spaces / quotes / non-ASCII pass
|
|
84
111
|
// through verbatim with no shell parsing.
|
|
85
|
-
execFileSync(
|
|
112
|
+
execFileSync(resolveGit(), ["add", "--", ...files], { cwd, timeout: 10000 });
|
|
86
113
|
}
|
|
87
114
|
export function gitCommit(cwd, message) {
|
|
88
115
|
// Pre-fix this used `JSON.stringify(message)` which only happened to be
|
|
89
116
|
// safe because JSON.stringify covers most shell metacharacters — but it's
|
|
90
117
|
// not real escaping. The argv form is.
|
|
91
|
-
return execFileSync(
|
|
118
|
+
return execFileSync(resolveGit(), ["commit", "-m", message], {
|
|
92
119
|
cwd,
|
|
93
120
|
encoding: "utf-8",
|
|
94
121
|
timeout: 30000,
|
|
@@ -115,7 +142,7 @@ export function gitCheckout(cwd, branch, create = false) {
|
|
|
115
142
|
throw new Error(`refusing branch name that starts with '-': ${branch}`);
|
|
116
143
|
}
|
|
117
144
|
const args = create ? ["checkout", "-b", branch] : ["checkout", branch];
|
|
118
|
-
execFileSync(
|
|
145
|
+
execFileSync(resolveGit(), args, { cwd, timeout: 10000 });
|
|
119
146
|
}
|
|
120
147
|
export function ghAvailable() {
|
|
121
148
|
try {
|
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.6.0-rc.
|
|
6
|
+
export declare const VERSION = "0.6.0-rc.5";
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.6.0-rc.
|
|
6
|
+
export const VERSION = "0.6.0-rc.5";
|
|
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) ─────────────────────────────
|
package/dist/llm/model-pool.d.ts
CHANGED
|
@@ -79,6 +79,8 @@ export declare class ModelPool {
|
|
|
79
79
|
* The first entry (or the one matching `defaultKey`) becomes active.
|
|
80
80
|
*/
|
|
81
81
|
constructor(entries?: ModelEntry[], defaultKey?: string);
|
|
82
|
+
/** Clear all registered models and active selection. */
|
|
83
|
+
clear(): void;
|
|
82
84
|
/** Register a model at runtime. */
|
|
83
85
|
register(entry: ModelEntry): void;
|
|
84
86
|
/** Fill in known defaults (e.g. context window for DeepSeek V4) when the entry doesn't specify them. */
|
package/dist/llm/model-pool.js
CHANGED
|
@@ -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));
|
|
@@ -228,6 +233,7 @@ export class ModelPool {
|
|
|
228
233
|
// instead of fabricating 8192, which silently truncates long outputs and
|
|
229
234
|
// masks the real per-model cap.
|
|
230
235
|
maxTokens: entry.maxOutputTokens,
|
|
236
|
+
...(entry.maxContextTokens !== undefined ? { maxContextTokens: entry.maxContextTokens } : {}),
|
|
231
237
|
// reasoning: entry overrides catalog. No base fallback — see class doc.
|
|
232
238
|
...(entry.reasoning ?? fromCat?.reasoning
|
|
233
239
|
? { reasoning: entry.reasoning ?? fromCat?.reasoning }
|
|
@@ -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;
|
package/dist/prompt/composer.js
CHANGED
|
@@ -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
|
|
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) {
|
|
@@ -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: when Bash is available, use it for ordinary shell commands, git operations, package-manager commands, test/build scripts, and POSIX-style command lines. On Windows, Bash uses Git Bash when it is available, so 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.
|
|
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.
|
|
@@ -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;
|
|
@@ -116,7 +117,7 @@ export declare class ChatSession {
|
|
|
116
117
|
* turn is in flight so a hot switch never mutates the model under a
|
|
117
118
|
* running LLM client.
|
|
118
119
|
*/
|
|
119
|
-
requestModelSwitch(key: string):
|
|
120
|
+
requestModelSwitch(key: string): ModelEntry;
|
|
120
121
|
/**
|
|
121
122
|
* Perform the actual model switch for this session: rotate the engine's model
|
|
122
123
|
* 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;
|
|
@@ -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;
|
package/dist/protocol/server.js
CHANGED
|
@@ -307,6 +307,19 @@ export class AgentServer {
|
|
|
307
307
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
|
+
if (params.model !== undefined) {
|
|
311
|
+
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
312
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
session.requestModelSwitch(params.model);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
310
323
|
if (typeof params.planMode === "boolean") {
|
|
311
324
|
session.engine.setPlanMode(params.planMode);
|
|
312
325
|
}
|
|
@@ -387,6 +400,19 @@ export class AgentServer {
|
|
|
387
400
|
// Engine.setPermissionMode now keeps this.permissionMode + this.planMode
|
|
388
401
|
// in sync and tools read them via ToolContext.permissionMode/planMode.
|
|
389
402
|
}
|
|
403
|
+
if (params.model !== undefined) {
|
|
404
|
+
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
405
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
this.legacyEngine.switchModel(params.model);
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
390
416
|
this.running = true;
|
|
391
417
|
this.abortController = new AbortController();
|
|
392
418
|
this.notify(Methods.Status, { status: "running" });
|
|
@@ -720,7 +746,11 @@ export class AgentServer {
|
|
|
720
746
|
const sid = params.sessionId;
|
|
721
747
|
const s = this.chatManager.get(sid);
|
|
722
748
|
if (!s) {
|
|
723
|
-
|
|
749
|
+
// Session not found (already cleaned by idle sweeper, or never created).
|
|
750
|
+
// Don't create one just for configure — let the subsequent run() do it
|
|
751
|
+
// with proper per-session config. Return OK since there's nothing to
|
|
752
|
+
// configure on a non-existent session.
|
|
753
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
724
754
|
return;
|
|
725
755
|
}
|
|
726
756
|
if (typeof params.planMode === "boolean")
|
|
@@ -728,13 +758,32 @@ export class AgentServer {
|
|
|
728
758
|
if (typeof params.permissionMode === "string") {
|
|
729
759
|
s.engine.setPermissionMode(params.permissionMode);
|
|
730
760
|
}
|
|
761
|
+
if (params.clearModels) {
|
|
762
|
+
this.chatManager.runtime.clearModels();
|
|
763
|
+
}
|
|
764
|
+
if (params.reloadModels) {
|
|
765
|
+
try {
|
|
766
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
731
773
|
// Per-session model switch — the missing piece that made model changes
|
|
732
774
|
// worker-global (session-isolation research §3). requestModelSwitch
|
|
733
775
|
// applies immediately when idle, defers to the run boundary when busy
|
|
734
776
|
// so it never swaps the model under a running LLM client.
|
|
735
777
|
if (typeof params.model === "string") {
|
|
736
778
|
try {
|
|
737
|
-
s.requestModelSwitch(params.model);
|
|
779
|
+
const entry = s.requestModelSwitch(params.model);
|
|
780
|
+
this.transport.send(createResponse(req.id, {
|
|
781
|
+
ok: true,
|
|
782
|
+
model: entry.model,
|
|
783
|
+
key: entry.key,
|
|
784
|
+
maxContextTokens: entry.maxContextTokens,
|
|
785
|
+
}));
|
|
786
|
+
return;
|
|
738
787
|
}
|
|
739
788
|
catch (err) {
|
|
740
789
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
@@ -757,22 +806,33 @@ export class AgentServer {
|
|
|
757
806
|
// Global configure — delegate to legacyEngine if available,
|
|
758
807
|
// or to any session's engine from chatManager for settings ops
|
|
759
808
|
const engine = this.legacyEngine ?? this.anyEngine();
|
|
809
|
+
if (params.clearModels) {
|
|
810
|
+
if (this.chatManager) {
|
|
811
|
+
this.chatManager.runtime.clearModels();
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
this.legacyEngine?.getModelPool().clear();
|
|
815
|
+
this.globalQueryEngine?.getModelPool().clear();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
760
818
|
if (params.reloadModels) {
|
|
761
819
|
try {
|
|
762
|
-
const seen = new Set();
|
|
763
|
-
const reload = (target) => {
|
|
764
|
-
if (!target || seen.has(target))
|
|
765
|
-
return;
|
|
766
|
-
seen.add(target);
|
|
767
|
-
target.reloadModelPool();
|
|
768
|
-
};
|
|
769
|
-
reload(this.legacyEngine);
|
|
770
|
-
reload(this.globalQueryEngine);
|
|
771
820
|
if (this.chatManager) {
|
|
772
|
-
this.chatManager.
|
|
821
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const seen = new Set();
|
|
825
|
+
const reload = (target) => {
|
|
826
|
+
if (!target || seen.has(target))
|
|
827
|
+
return;
|
|
828
|
+
seen.add(target);
|
|
829
|
+
target.reloadModelPool();
|
|
830
|
+
};
|
|
831
|
+
reload(this.legacyEngine);
|
|
832
|
+
reload(this.globalQueryEngine);
|
|
833
|
+
if (seen.size === 0)
|
|
834
|
+
reload(engine);
|
|
773
835
|
}
|
|
774
|
-
if (seen.size === 0)
|
|
775
|
-
reload(engine);
|
|
776
836
|
}
|
|
777
837
|
catch (err) {
|
|
778
838
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -782,7 +842,12 @@ export class AgentServer {
|
|
|
782
842
|
if (params.model !== undefined && engine) {
|
|
783
843
|
try {
|
|
784
844
|
const entry = engine.switchModel(params.model);
|
|
785
|
-
this.transport.send(createResponse(req.id, {
|
|
845
|
+
this.transport.send(createResponse(req.id, {
|
|
846
|
+
ok: true,
|
|
847
|
+
model: entry.model,
|
|
848
|
+
key: entry.key,
|
|
849
|
+
maxContextTokens: entry.maxContextTokens,
|
|
850
|
+
}));
|
|
786
851
|
return;
|
|
787
852
|
}
|
|
788
853
|
catch (err) {
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ export interface RunParams {
|
|
|
61
61
|
* When omitted, the engine keeps its configured default.
|
|
62
62
|
*/
|
|
63
63
|
permissionMode?: PermissionMode;
|
|
64
|
+
/**
|
|
65
|
+
* Per-run model pool key. Applied after the session exists and before the
|
|
66
|
+
* turn starts, so cold desktop runs don't need a separate pre-run configure
|
|
67
|
+
* request to a worker that has not been spawned yet.
|
|
68
|
+
*/
|
|
69
|
+
model?: string;
|
|
64
70
|
/**
|
|
65
71
|
* Workspace trust for this run's project (`cwd`), asserted by the host
|
|
66
72
|
* (desktop main from its trust-store) — never by the renderer. When false,
|
|
@@ -149,6 +155,8 @@ export interface ConfigureParams {
|
|
|
149
155
|
* running engine picks them up without a process restart.
|
|
150
156
|
*/
|
|
151
157
|
reloadModels?: boolean;
|
|
158
|
+
/** Clear the live model pool, used after logout removes saved credentials. */
|
|
159
|
+
clearModels?: boolean;
|
|
152
160
|
/**
|
|
153
161
|
* Re-read disk settings and hot-push the disk-default config fields (preset /
|
|
154
162
|
* customSystemPrompt / appendSystemPrompt / personalization / mcpServers) +
|
|
@@ -85,8 +85,13 @@ export declare function resolveShellInvocation(command: string, shell?: string):
|
|
|
85
85
|
file: string;
|
|
86
86
|
args: string[];
|
|
87
87
|
};
|
|
88
|
+
export declare function resolveGitBash(): string | undefined;
|
|
89
|
+
/** Reset the Git Bash probe cache. Test-only (platform is stubbed per test). */
|
|
90
|
+
export declare function _resetGitBashCache(): void;
|
|
88
91
|
/** The platform's default interactive shell binary, for spawning a bare shell
|
|
89
|
-
* (no `-c`/`/c` command). Windows → ComSpec/cmd.exe;
|
|
92
|
+
* (no `-c`/`/c` command). Windows → Git Bash if present, else ComSpec/cmd.exe;
|
|
93
|
+
* POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
|
|
94
|
+
* commands actually run (cmd.exe can't). */
|
|
90
95
|
export declare function defaultShellBinary(shell?: string): string;
|
|
91
96
|
/**
|
|
92
97
|
* Resolve the actual (file, args) for a shell `command` under an optional
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* spawn detached, so there's no separate process group to reap); it shares
|
|
21
21
|
* (1) and (2) here. The background manager uses all three.
|
|
22
22
|
*/
|
|
23
|
-
import { spawn } from "node:child_process";
|
|
23
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
24
|
+
import { existsSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
24
26
|
/**
|
|
25
27
|
* Env vars that are always safe to forward into a sandboxed shell. Mirrors
|
|
26
28
|
* the allowlist that previously lived in bash.ts — kept here so foreground
|
|
@@ -115,20 +117,81 @@ export function mergeShellEnv(base, projectEnv) {
|
|
|
115
117
|
export function resolveShellInvocation(command, shell) {
|
|
116
118
|
if (process.platform === "win32") {
|
|
117
119
|
const file = shell ?? process.env.ComSpec ?? "cmd.exe";
|
|
118
|
-
// PowerShell
|
|
120
|
+
// Flag form depends on the shell: PowerShell → -Command; a POSIX shell such
|
|
121
|
+
// as Git Bash's bash.exe / sh → -c (it does NOT understand cmd's /c); cmd.exe
|
|
122
|
+
// (and cmd-like) → /c. Detecting bash/sh matters now that defaultShellBinary
|
|
123
|
+
// prefers Git Bash on Windows — feeding it /c would break every command.
|
|
119
124
|
const isPwsh = /(^|[\\/])(pwsh|powershell)(\.exe)?$/i.test(file);
|
|
120
|
-
|
|
125
|
+
if (isPwsh)
|
|
126
|
+
return { file, args: ["-Command", command] };
|
|
127
|
+
const isPosixShell = /(^|[\\/])(bash|sh|zsh|dash)(\.exe)?$/i.test(file);
|
|
128
|
+
return { file, args: [isPosixShell ? "-c" : "/c", command] };
|
|
121
129
|
}
|
|
122
130
|
const file = shell ?? process.env.SHELL ?? "/bin/bash";
|
|
123
131
|
return { file, args: ["-c", command] };
|
|
124
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Best-effort locate Git Bash's `bash.exe` on Windows. Returns undefined on
|
|
135
|
+
* non-Windows, when git isn't installed, or when no bash.exe is found.
|
|
136
|
+
*
|
|
137
|
+
* WHY: The Bash tool and background shells feed the model's *bash* commands
|
|
138
|
+
* (`ls`, `&&`, `$(…)`, pipes, quoting) to the shell. On Windows the historical
|
|
139
|
+
* default is `cmd.exe`, which can't run any of that — so "Bash" was effectively
|
|
140
|
+
* broken on Windows. Git for Windows (which most devs already have — we detect
|
|
141
|
+
* it for repo ops anyway) ships a full bash at `<git>\bin\bash.exe`, so prefer
|
|
142
|
+
* it. Only falls back to cmd.exe when Git Bash truly isn't present.
|
|
143
|
+
*
|
|
144
|
+
* Resolution order:
|
|
145
|
+
* 1. CODE_SHELL_GIT_BASH_PATH env override (explicit user config wins).
|
|
146
|
+
* 2. Reverse-engineer from the git binary's location: `where git` gives e.g.
|
|
147
|
+
* `C:\Program Files\Git\cmd\git.exe` → `…\Git\bin\bash.exe`.
|
|
148
|
+
* 3. The two default install locations (Program Files / Program Files (x86)).
|
|
149
|
+
* Cached after first probe (a spawn per command would be wasteful).
|
|
150
|
+
*/
|
|
151
|
+
let gitBashCache;
|
|
152
|
+
export function resolveGitBash() {
|
|
153
|
+
if (process.platform !== "win32")
|
|
154
|
+
return undefined;
|
|
155
|
+
if (gitBashCache !== undefined)
|
|
156
|
+
return gitBashCache ?? undefined;
|
|
157
|
+
const override = process.env.CODE_SHELL_GIT_BASH_PATH;
|
|
158
|
+
if (override && existsSync(override))
|
|
159
|
+
return (gitBashCache = override);
|
|
160
|
+
const candidates = [];
|
|
161
|
+
// (2) derive from `where git`. Git installs git.exe under either \cmd\ or
|
|
162
|
+
// \bin\; bash.exe lives under the sibling \bin\. Walk up to the Git root.
|
|
163
|
+
try {
|
|
164
|
+
const out = execFileSync("where", ["git"], { encoding: "utf-8", timeout: 3000 });
|
|
165
|
+
const gitExe = out.split(/\r?\n/).find((l) => l.trim().toLowerCase().endsWith("git.exe"));
|
|
166
|
+
if (gitExe) {
|
|
167
|
+
const gitRoot = dirname(dirname(gitExe.trim())); // …\Git\cmd\git.exe → …\Git
|
|
168
|
+
candidates.push(join(gitRoot, "bin", "bash.exe"));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// git not on PATH — fall through to the well-known locations.
|
|
173
|
+
}
|
|
174
|
+
// (3) default install locations.
|
|
175
|
+
const pf = process.env["ProgramFiles"] ?? "C:\\Program Files";
|
|
176
|
+
const pf86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
177
|
+
candidates.push(join(pf, "Git", "bin", "bash.exe"));
|
|
178
|
+
candidates.push(join(pf86, "Git", "bin", "bash.exe"));
|
|
179
|
+
const found = candidates.find((p) => existsSync(p));
|
|
180
|
+
return (gitBashCache = found ?? null) ?? undefined;
|
|
181
|
+
}
|
|
182
|
+
/** Reset the Git Bash probe cache. Test-only (platform is stubbed per test). */
|
|
183
|
+
export function _resetGitBashCache() {
|
|
184
|
+
gitBashCache = undefined;
|
|
185
|
+
}
|
|
125
186
|
/** The platform's default interactive shell binary, for spawning a bare shell
|
|
126
|
-
* (no `-c`/`/c` command). Windows → ComSpec/cmd.exe;
|
|
187
|
+
* (no `-c`/`/c` command). Windows → Git Bash if present, else ComSpec/cmd.exe;
|
|
188
|
+
* POSIX → $SHELL/bin/bash. Windows prefers Git Bash so the model's bash-syntax
|
|
189
|
+
* commands actually run (cmd.exe can't). */
|
|
127
190
|
export function defaultShellBinary(shell) {
|
|
128
191
|
if (shell)
|
|
129
192
|
return shell;
|
|
130
193
|
if (process.platform === "win32")
|
|
131
|
-
return process.env.ComSpec ?? "cmd.exe";
|
|
194
|
+
return resolveGitBash() ?? process.env.ComSpec ?? "cmd.exe";
|
|
132
195
|
return process.env.SHELL ?? "/bin/bash";
|
|
133
196
|
}
|
|
134
197
|
/**
|
|
@@ -29,8 +29,10 @@ function sandboxMark(backend) {
|
|
|
29
29
|
export const bashToolDef = {
|
|
30
30
|
name: "Bash",
|
|
31
31
|
description: "Execute a shell command and return its output. " +
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"Use for ordinary shell commands, system operations, git commands, " +
|
|
33
|
+
"package-manager commands, running tests, installing packages, etc. " +
|
|
34
|
+
"On Windows this tool prefers Git Bash when available, so prefer Bash over " +
|
|
35
|
+
"PowerShell unless the task needs PowerShell-specific syntax or APIs.",
|
|
34
36
|
inputSchema: {
|
|
35
37
|
type: "object",
|
|
36
38
|
properties: {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Built-in tool registration.
|
|
3
3
|
*/
|
|
4
4
|
import type { RegisteredTool } from "../../types.js";
|
|
5
|
+
import type { ToolVisibilityContext } from "../context.js";
|
|
5
6
|
/**
|
|
6
7
|
* Tool executor signature.
|
|
7
8
|
*
|
|
@@ -27,6 +28,7 @@ export type BuiltinToolResult = string | {
|
|
|
27
28
|
sandbox: import("../../types.js").ToolResult["sandbox"];
|
|
28
29
|
};
|
|
29
30
|
export type BuiltinToolFn = (args: Record<string, unknown>, ctx?: import("../context.js").ToolContext) => Promise<BuiltinToolResult>;
|
|
31
|
+
export type BuiltinToolGuard = (ctx: ToolVisibilityContext) => boolean;
|
|
30
32
|
export interface BuiltinTool {
|
|
31
33
|
definition: RegisteredTool;
|
|
32
34
|
execute: BuiltinToolFn;
|
|
@@ -38,6 +40,6 @@ export declare const BUILTIN_TOOLS: BuiltinTool[];
|
|
|
38
40
|
* engine.ts toolDefs assembly). Tools NOT listed here are always visible.
|
|
39
41
|
* Keyed by the tool's `name` (must match the toolDef name).
|
|
40
42
|
*/
|
|
41
|
-
export declare const BUILTIN_TOOL_GUARDS: Map<string,
|
|
43
|
+
export declare const BUILTIN_TOOL_GUARDS: Map<string, BuiltinToolGuard>;
|
|
42
44
|
/** UseCredential is available when the cwd's CredentialStore has ≥1 credential. */
|
|
43
45
|
export declare function isUseCredentialAvailable(cwd: string): boolean;
|
|
@@ -711,17 +711,17 @@ export const BUILTIN_TOOLS = [
|
|
|
711
711
|
* Keyed by the tool's `name` (must match the toolDef name).
|
|
712
712
|
*/
|
|
713
713
|
export const BUILTIN_TOOL_GUARDS = new Map([
|
|
714
|
-
[webSearchToolDef.name, isWebSearchAvailable],
|
|
715
|
-
[generateImageToolDef.name, isGenerateImageAvailable],
|
|
716
|
-
[generateVideoToolDef.name, isGenerateVideoAvailable],
|
|
714
|
+
[webSearchToolDef.name, (ctx) => isWebSearchAvailable(ctx.cwd)],
|
|
715
|
+
[generateImageToolDef.name, (ctx) => isGenerateImageAvailable(ctx.cwd)],
|
|
716
|
+
[generateVideoToolDef.name, (ctx) => isGenerateVideoAvailable(ctx.cwd)],
|
|
717
717
|
// UseCredential is hidden until at least one credential exists — keeps it out
|
|
718
718
|
// of the tool list (and the context) for the common no-credentials case,
|
|
719
719
|
// matching the spec's "quiet when empty" intent (true ToolSearch-deferral for
|
|
720
720
|
// builtins isn't wired in the engine).
|
|
721
|
-
[useCredentialToolDef.name, isUseCredentialAvailable],
|
|
721
|
+
[useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd)],
|
|
722
722
|
// InjectCredential hidden until ≥1 cookie credential exists (browser injection
|
|
723
723
|
// is cookie-only). Also degrades at call time if no browser bridge is wired.
|
|
724
|
-
[injectCredentialToolDef.name, isInjectCredentialAvailable],
|
|
724
|
+
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd)],
|
|
725
725
|
]);
|
|
726
726
|
/** UseCredential is available when the cwd's CredentialStore has ≥1 credential. */
|
|
727
727
|
export function isUseCredentialAvailable(cwd) {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import { safeSpawn } from "../../runtime/safe-spawn.js";
|
|
9
9
|
export const powershellToolDef = {
|
|
10
10
|
name: "PowerShell",
|
|
11
|
-
description: "Execute PowerShell commands.
|
|
11
|
+
description: "Execute PowerShell commands. Use only when the user explicitly asks for PowerShell " +
|
|
12
|
+
"or the task requires PowerShell-specific cmdlets, Windows APIs, registry access, " +
|
|
13
|
+
"or .ps1 behavior. For ordinary shell/git/package/test commands, prefer Bash " +
|
|
14
|
+
"(Git Bash on Windows when available).",
|
|
12
15
|
inputSchema: {
|
|
13
16
|
type: "object",
|
|
14
17
|
properties: {
|
|
@@ -169,6 +169,10 @@ export interface SubAgentSpawner {
|
|
|
169
169
|
* Optional fields are filled in by Engine.run(); some headless paths may
|
|
170
170
|
* leave them undefined (e.g. running without UI → no askUser).
|
|
171
171
|
*/
|
|
172
|
+
export interface ToolVisibilityContext {
|
|
173
|
+
cwd: string;
|
|
174
|
+
hasGoal: boolean;
|
|
175
|
+
}
|
|
172
176
|
export interface ToolContext {
|
|
173
177
|
/** Active working directory for this Engine. */
|
|
174
178
|
cwd: string;
|
|
@@ -290,6 +294,12 @@ export interface ToolContext {
|
|
|
290
294
|
* sub-agents and no-cwd contexts (same as readBuiltinOverride).
|
|
291
295
|
*/
|
|
292
296
|
disabledBuiltins?: Set<string>;
|
|
297
|
+
/**
|
|
298
|
+
* Per-turn context used by builtin availability guards. Engine.run() uses the
|
|
299
|
+
* same object to hide tools from the model; ToolExecutor reuses it to reject
|
|
300
|
+
* direct calls to tools that are not available in the current runtime state.
|
|
301
|
+
*/
|
|
302
|
+
toolVisibility?: ToolVisibilityContext;
|
|
293
303
|
/**
|
|
294
304
|
* MCP servers THIS session's merged config enables (keys of
|
|
295
305
|
* config.mcpServers, enabled!==false). The pool + registry are
|
|
@@ -11,6 +11,9 @@ import { validateToolArgs } from "./validation.js";
|
|
|
11
11
|
import { PLAN_MODE_ALLOWED_TOOLS } from "./plan-mode-allowlist.js";
|
|
12
12
|
import { enforcePathPolicyWithApproval } from "./path-policy.js";
|
|
13
13
|
import { parsePatch } from "./builtin/apply-patch/parser.js";
|
|
14
|
+
import { BUILTIN_TOOL_GUARDS } from "./builtin/index.js";
|
|
15
|
+
import { COMPLETE_GOAL_TOOL_NAME } from "./builtin/complete-goal.js";
|
|
16
|
+
import { CANCEL_GOAL_TOOL_NAME } from "./builtin/cancel-goal.js";
|
|
14
17
|
// A1 hardening: hooks must never promote a non-`allow` classifier
|
|
15
18
|
// decision to `allow`. They may otherwise adjust the decision freely
|
|
16
19
|
// (e.g. tighten `allow` to `deny`/`ask`, or relax `deny` to `ask` to
|
|
@@ -119,6 +122,24 @@ export class ToolExecutor {
|
|
|
119
122
|
isError: true,
|
|
120
123
|
};
|
|
121
124
|
}
|
|
125
|
+
if ((call.toolName === COMPLETE_GOAL_TOOL_NAME || call.toolName === CANCEL_GOAL_TOOL_NAME) &&
|
|
126
|
+
this.toolCtx?.toolVisibility?.hasGoal !== true) {
|
|
127
|
+
return {
|
|
128
|
+
id: call.id,
|
|
129
|
+
toolName: call.toolName,
|
|
130
|
+
error: `Tool ${call.toolName} is only available while an active goal is present. Do NOT retry this tool call unless a goal is active.`,
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const visibilityGuard = BUILTIN_TOOL_GUARDS.get(call.toolName);
|
|
135
|
+
if (visibilityGuard && this.toolCtx?.toolVisibility && !visibilityGuard(this.toolCtx.toolVisibility)) {
|
|
136
|
+
return {
|
|
137
|
+
id: call.id,
|
|
138
|
+
toolName: call.toolName,
|
|
139
|
+
error: `Tool ${call.toolName} is not available in the current session context. Do NOT retry this tool call unless the relevant context changes.`,
|
|
140
|
+
isError: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
122
143
|
// Same gate for MCP tools: the registry is worker-shared, so it can hold
|
|
123
144
|
// tools from servers OTHER sessions enabled. Visibility filtering hides
|
|
124
145
|
// them from this session's tool list; this rejects a direct call anyway.
|
|
@@ -3,9 +3,11 @@ export function createOffBackend() {
|
|
|
3
3
|
return {
|
|
4
4
|
name: "off",
|
|
5
5
|
wrap(command, opts) {
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// "off" = no sandboxing; just run the command through the shell. Delegate
|
|
7
|
+
// to resolveShellInvocation for the PLATFORM-CORRECT flag instead of a
|
|
8
|
+
// hardcoded POSIX `-c`: on Windows cmd.exe needs `/c` (a bare `-c` is taken
|
|
9
|
+
// as a filename and cmd hangs in interactive mode until timeout — the
|
|
10
|
+
// "Bash never runs on Windows" beta regression). POSIX still gets `-c`.
|
|
9
11
|
return resolveShellInvocation(command, opts.shell);
|
|
10
12
|
},
|
|
11
13
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -466,6 +466,8 @@ export interface LLMConfig {
|
|
|
466
466
|
apiKey?: string;
|
|
467
467
|
baseUrl?: string;
|
|
468
468
|
maxTokens?: number;
|
|
469
|
+
/** Per-model context window size used by hosts to seed Engine.maxContextTokens. */
|
|
470
|
+
maxContextTokens?: number;
|
|
469
471
|
/**
|
|
470
472
|
* Shell command whose stdout is the auth token (TODO 7.2). Resolved at
|
|
471
473
|
* client-build time when `apiKey` is absent. The trimmed first line of
|
package/dist/utils/exec.d.ts
CHANGED
|
@@ -38,3 +38,11 @@ export declare function setGitPathOverride(path: string | null | undefined): voi
|
|
|
38
38
|
export declare function resolveGit(env?: NodeJS.ProcessEnv): string;
|
|
39
39
|
/** Is a usable git binary available (override path, or git on PATH)? */
|
|
40
40
|
export declare function isGitAvailable(env?: NodeJS.ProcessEnv): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The RESOLVED absolute path of the usable git binary (override path, or git
|
|
43
|
+
* found on PATH), or null if none. Lets the settings UI auto-fill the git.path
|
|
44
|
+
* field after a successful detection instead of only reporting available:true
|
|
45
|
+
* with no path (the "检测到了但没回填 path" complaint). Returns the real path so
|
|
46
|
+
* the user can see/keep exactly what was found.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveGitPath(env?: NodeJS.ProcessEnv): string | null;
|
package/dist/utils/exec.js
CHANGED
|
@@ -120,6 +120,16 @@ export function resolveGit(env = process.env) {
|
|
|
120
120
|
export function isGitAvailable(env = process.env) {
|
|
121
121
|
return findExecutable(gitPathOverride ?? "git", env) !== null;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The RESOLVED absolute path of the usable git binary (override path, or git
|
|
125
|
+
* found on PATH), or null if none. Lets the settings UI auto-fill the git.path
|
|
126
|
+
* field after a successful detection instead of only reporting available:true
|
|
127
|
+
* with no path (the "检测到了但没回填 path" complaint). Returns the real path so
|
|
128
|
+
* the user can see/keep exactly what was found.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveGitPath(env = process.env) {
|
|
131
|
+
return findExecutable(gitPathOverride ?? "git", env);
|
|
132
|
+
}
|
|
123
133
|
function resolveExecutableUncached(trimmed, env) {
|
|
124
134
|
const command = trimmed;
|
|
125
135
|
// Already a path (absolute or relative with a separator): resolve extension.
|
package/package.json
CHANGED