@cjhyy/code-shell-core 0.6.0-rc.2 → 0.6.0-rc.4
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/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/server.js +72 -17
- package/dist/protocol/types.d.ts +4 -0
- package/dist/runtime/spawn-common.d.ts +6 -1
- package/dist/runtime/spawn-common.js +72 -7
- package/dist/tool-system/builtin/cron.js +10 -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/sleep.js +5 -0
- package/dist/tool-system/context.d.ts +10 -0
- package/dist/tool-system/executor.js +25 -2
- package/dist/tool-system/permission.d.ts +3 -1
- package/dist/tool-system/permission.js +2 -1
- package/dist/tool-system/sandbox/off.js +7 -1
- package/dist/types.d.ts +4 -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
|
+
export declare const VERSION = "0.6.0-rc.4";
|
|
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
|
+
export const VERSION = "0.6.0-rc.4";
|
|
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) {
|
|
@@ -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;
|
package/dist/protocol/server.js
CHANGED
|
@@ -461,7 +461,18 @@ export class AgentServer {
|
|
|
461
461
|
}
|
|
462
462
|
const resolve = s.pendingApprovals.get(params.requestId);
|
|
463
463
|
if (!resolve) {
|
|
464
|
-
|
|
464
|
+
// Tool approvals still resolve through the interactive backend's
|
|
465
|
+
// legacy pending map; the sessionId on their envelope is UI routing
|
|
466
|
+
// metadata. Accept a session-tagged response for those requests too.
|
|
467
|
+
const legacyResolve = this.pendingApprovals.get(params.requestId);
|
|
468
|
+
if (!legacyResolve) {
|
|
469
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval: ${params.requestId}`));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.pendingApprovals.delete(params.requestId);
|
|
473
|
+
this.clearApprovalTimer(params.requestId);
|
|
474
|
+
legacyResolve(params.decision);
|
|
475
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
465
476
|
return;
|
|
466
477
|
}
|
|
467
478
|
s.pendingApprovals.delete(params.requestId);
|
|
@@ -709,7 +720,11 @@ export class AgentServer {
|
|
|
709
720
|
const sid = params.sessionId;
|
|
710
721
|
const s = this.chatManager.get(sid);
|
|
711
722
|
if (!s) {
|
|
712
|
-
|
|
723
|
+
// Session not found (already cleaned by idle sweeper, or never created).
|
|
724
|
+
// Don't create one just for configure — let the subsequent run() do it
|
|
725
|
+
// with proper per-session config. Return OK since there's nothing to
|
|
726
|
+
// configure on a non-existent session.
|
|
727
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
713
728
|
return;
|
|
714
729
|
}
|
|
715
730
|
if (typeof params.planMode === "boolean")
|
|
@@ -717,13 +732,32 @@ export class AgentServer {
|
|
|
717
732
|
if (typeof params.permissionMode === "string") {
|
|
718
733
|
s.engine.setPermissionMode(params.permissionMode);
|
|
719
734
|
}
|
|
735
|
+
if (params.clearModels) {
|
|
736
|
+
this.chatManager.runtime.clearModels();
|
|
737
|
+
}
|
|
738
|
+
if (params.reloadModels) {
|
|
739
|
+
try {
|
|
740
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
720
747
|
// Per-session model switch — the missing piece that made model changes
|
|
721
748
|
// worker-global (session-isolation research §3). requestModelSwitch
|
|
722
749
|
// applies immediately when idle, defers to the run boundary when busy
|
|
723
750
|
// so it never swaps the model under a running LLM client.
|
|
724
751
|
if (typeof params.model === "string") {
|
|
725
752
|
try {
|
|
726
|
-
s.requestModelSwitch(params.model);
|
|
753
|
+
const entry = s.requestModelSwitch(params.model);
|
|
754
|
+
this.transport.send(createResponse(req.id, {
|
|
755
|
+
ok: true,
|
|
756
|
+
model: entry.model,
|
|
757
|
+
key: entry.key,
|
|
758
|
+
maxContextTokens: entry.maxContextTokens,
|
|
759
|
+
}));
|
|
760
|
+
return;
|
|
727
761
|
}
|
|
728
762
|
catch (err) {
|
|
729
763
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
@@ -746,22 +780,33 @@ export class AgentServer {
|
|
|
746
780
|
// Global configure — delegate to legacyEngine if available,
|
|
747
781
|
// or to any session's engine from chatManager for settings ops
|
|
748
782
|
const engine = this.legacyEngine ?? this.anyEngine();
|
|
783
|
+
if (params.clearModels) {
|
|
784
|
+
if (this.chatManager) {
|
|
785
|
+
this.chatManager.runtime.clearModels();
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
this.legacyEngine?.getModelPool().clear();
|
|
789
|
+
this.globalQueryEngine?.getModelPool().clear();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
749
792
|
if (params.reloadModels) {
|
|
750
793
|
try {
|
|
751
|
-
const seen = new Set();
|
|
752
|
-
const reload = (target) => {
|
|
753
|
-
if (!target || seen.has(target))
|
|
754
|
-
return;
|
|
755
|
-
seen.add(target);
|
|
756
|
-
target.reloadModelPool();
|
|
757
|
-
};
|
|
758
|
-
reload(this.legacyEngine);
|
|
759
|
-
reload(this.globalQueryEngine);
|
|
760
794
|
if (this.chatManager) {
|
|
761
|
-
this.chatManager.
|
|
795
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
const seen = new Set();
|
|
799
|
+
const reload = (target) => {
|
|
800
|
+
if (!target || seen.has(target))
|
|
801
|
+
return;
|
|
802
|
+
seen.add(target);
|
|
803
|
+
target.reloadModelPool();
|
|
804
|
+
};
|
|
805
|
+
reload(this.legacyEngine);
|
|
806
|
+
reload(this.globalQueryEngine);
|
|
807
|
+
if (seen.size === 0)
|
|
808
|
+
reload(engine);
|
|
762
809
|
}
|
|
763
|
-
if (seen.size === 0)
|
|
764
|
-
reload(engine);
|
|
765
810
|
}
|
|
766
811
|
catch (err) {
|
|
767
812
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -771,7 +816,12 @@ export class AgentServer {
|
|
|
771
816
|
if (params.model !== undefined && engine) {
|
|
772
817
|
try {
|
|
773
818
|
const entry = engine.switchModel(params.model);
|
|
774
|
-
this.transport.send(createResponse(req.id, {
|
|
819
|
+
this.transport.send(createResponse(req.id, {
|
|
820
|
+
ok: true,
|
|
821
|
+
model: entry.model,
|
|
822
|
+
key: entry.key,
|
|
823
|
+
maxContextTokens: entry.maxContextTokens,
|
|
824
|
+
}));
|
|
775
825
|
return;
|
|
776
826
|
}
|
|
777
827
|
catch (err) {
|
|
@@ -1291,6 +1341,7 @@ export class AgentServer {
|
|
|
1291
1341
|
requestApprovalFromClient(request) {
|
|
1292
1342
|
return new Promise((resolve) => {
|
|
1293
1343
|
const requestId = nanoid(12);
|
|
1344
|
+
const sessionId = typeof request.sessionId === "string" ? request.sessionId : undefined;
|
|
1294
1345
|
this.pendingApprovals.set(requestId, resolve);
|
|
1295
1346
|
const timer = setTimeout(() => {
|
|
1296
1347
|
if (this.pendingApprovals.has(requestId)) {
|
|
@@ -1300,7 +1351,11 @@ export class AgentServer {
|
|
|
1300
1351
|
}
|
|
1301
1352
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1302
1353
|
this.approvalTimers.set(requestId, timer);
|
|
1303
|
-
this.notify(Methods.ApprovalRequest, {
|
|
1354
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1355
|
+
...(sessionId ? { sessionId } : {}),
|
|
1356
|
+
requestId,
|
|
1357
|
+
request,
|
|
1358
|
+
});
|
|
1304
1359
|
});
|
|
1305
1360
|
}
|
|
1306
1361
|
/**
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -149,6 +149,8 @@ export interface ConfigureParams {
|
|
|
149
149
|
* running engine picks them up without a process restart.
|
|
150
150
|
*/
|
|
151
151
|
reloadModels?: boolean;
|
|
152
|
+
/** Clear the live model pool, used after logout removes saved credentials. */
|
|
153
|
+
clearModels?: boolean;
|
|
152
154
|
/**
|
|
153
155
|
* Re-read disk settings and hot-push the disk-default config fields (preset /
|
|
154
156
|
* customSystemPrompt / appendSystemPrompt / personalization / mcpServers) +
|
|
@@ -243,6 +245,8 @@ export interface AgentStreamEventNotification {
|
|
|
243
245
|
}
|
|
244
246
|
/** Server requests approval from the client (UI). */
|
|
245
247
|
export interface ApprovalRequestNotification {
|
|
248
|
+
/** Originating engine session when known. */
|
|
249
|
+
sessionId?: string;
|
|
246
250
|
requestId: string;
|
|
247
251
|
request: ApprovalRequest;
|
|
248
252
|
}
|
|
@@ -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
|
/**
|
|
@@ -143,8 +206,10 @@ export function resolveSpawnTarget(command, opts) {
|
|
|
143
206
|
const wrapped = opts.sandbox.wrap(command, { cwd: opts.cwd, shell: opts.shell });
|
|
144
207
|
return { file: wrapped.file, args: wrapped.args, cleanup: wrapped.cleanup };
|
|
145
208
|
}
|
|
146
|
-
// No sandbox
|
|
147
|
-
//
|
|
209
|
+
// No sandbox configured: pick the shell + command-flag form for the
|
|
210
|
+
// platform instead of assuming POSIX `-c`. Note Bash always passes a
|
|
211
|
+
// backend (off at minimum), so it never reaches this line — the off
|
|
212
|
+
// backend's wrap() delegates to resolveShellInvocation itself.
|
|
148
213
|
return resolveShellInvocation(command, opts.shell);
|
|
149
214
|
}
|
|
150
215
|
/**
|
|
@@ -27,7 +27,14 @@ export const cronCreateToolDef = {
|
|
|
27
27
|
"'30 8 * * 1' = 8:30am every Monday. Day-of-week: 0=Sunday..6=Saturday.\n\n" +
|
|
28
28
|
"For calendar schedules, set `timezone` to the user's IANA zone (e.g. 'Asia/Shanghai', " +
|
|
29
29
|
"'America/New_York'); ask the user if unknown. Set `cwd` to the project the job operates on. " +
|
|
30
|
-
"Leave `permissionLevel` as 'read-only' unless the user explicitly wants the job to modify code
|
|
30
|
+
"Leave `permissionLevel` as 'read-only' unless the user explicitly wants the job to modify code.\n\n" +
|
|
31
|
+
"SELF-WAKEUP / background-task safety net: you can also schedule THIS job for yourself, with no user " +
|
|
32
|
+
"request, to re-check on a long-running background task (a download, a build, a background shell/agent) " +
|
|
33
|
+
"that might hang and never signal completion. Use a short interval + `once: true` + `continueInSession: true` " +
|
|
34
|
+
"and a `prompt` that reminds you what to check, e.g. schedule '5m', once true, continueInSession true, " +
|
|
35
|
+
"prompt 'check whether the yt-dlp download finished (BashOutput/ListShells); if still running, wait again'. " +
|
|
36
|
+
"You wake back in THIS conversation with full context, inspect the task, and either finish or reschedule. " +
|
|
37
|
+
"This is the right pattern for a simple poll-until-done loop or a hang safety net — prefer it over looping Sleep.",
|
|
31
38
|
inputSchema: {
|
|
32
39
|
type: "object",
|
|
33
40
|
properties: {
|
|
@@ -52,7 +59,8 @@ export const cronCreateToolDef = {
|
|
|
52
59
|
once: {
|
|
53
60
|
type: "boolean",
|
|
54
61
|
description: "true = one-shot: run once at the scheduled time, then auto-delete (for 'in N minutes / " +
|
|
55
|
-
"at <time>, do X once' reminders or
|
|
62
|
+
"at <time>, do X once' reminders, tasks, or a self-wakeup to re-check a background task). " +
|
|
63
|
+
"Default false = recurring per `schedule`. " +
|
|
56
64
|
"A one-shot still uses `schedule` for its time: interval '10m' = 10 minutes from now; " +
|
|
57
65
|
"cron '0 7 25 6 *' = once at 07:00 on June 25.",
|
|
58
66
|
},
|
|
@@ -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) {
|
|
@@ -6,6 +6,11 @@ export const sleepToolDef = {
|
|
|
6
6
|
description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
|
|
7
7
|
"Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
|
|
8
8
|
"the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
|
|
9
|
+
"If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
|
|
10
|
+
"instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
|
|
11
|
+
"({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
|
|
12
|
+
"prompt: 'check whether <that task> finished; if still running, wait again' }). " +
|
|
13
|
+
"That returns control to you at the interval without burning a turn spinning. " +
|
|
9
14
|
"Maximum duration is 300 seconds (5 minutes).",
|
|
10
15
|
inputSchema: {
|
|
11
16
|
type: "object",
|
|
@@ -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.
|
|
@@ -274,7 +295,7 @@ export class ToolExecutor {
|
|
|
274
295
|
// the hook and the user together have decided.
|
|
275
296
|
if (hookResult.decision === "ask") {
|
|
276
297
|
const reason = hookResult.messages?.join("\n") ?? undefined;
|
|
277
|
-
const approved = await this.permission.handleAsk(call.toolName, call.args, reason);
|
|
298
|
+
const approved = await this.permission.handleAsk(call.toolName, call.args, reason, { sessionId: this.toolCtx?.sessionId });
|
|
278
299
|
if (!approved) {
|
|
279
300
|
return {
|
|
280
301
|
id: call.id,
|
|
@@ -347,7 +368,9 @@ export class ToolExecutor {
|
|
|
347
368
|
}
|
|
348
369
|
if (decision === "ask") {
|
|
349
370
|
const reason = permHook.messages?.join("\n");
|
|
350
|
-
const approved = await this.permission.handleAsk(call.toolName, call.args, reason
|
|
371
|
+
const approved = await this.permission.handleAsk(call.toolName, call.args, reason, {
|
|
372
|
+
sessionId: this.toolCtx?.sessionId,
|
|
373
|
+
});
|
|
351
374
|
if (!approved) {
|
|
352
375
|
return {
|
|
353
376
|
id: call.id,
|
|
@@ -130,7 +130,9 @@ export declare class PermissionClassifier {
|
|
|
130
130
|
reconfigure(mode: PermissionMode, approvalBackend: ApprovalBackend, rules?: PermissionRule[]): void;
|
|
131
131
|
getMode(): PermissionMode;
|
|
132
132
|
classify(toolName: string, args: Record<string, unknown>): PermissionDecision;
|
|
133
|
-
handleAsk(toolName: string, args: Record<string, unknown>, reason?: string
|
|
133
|
+
handleAsk(toolName: string, args: Record<string, unknown>, reason?: string, opts?: {
|
|
134
|
+
sessionId?: string;
|
|
135
|
+
}): Promise<boolean>;
|
|
134
136
|
/** Get denial warning message if the model keeps getting denied. */
|
|
135
137
|
getDenialWarning(toolName: string): string | undefined;
|
|
136
138
|
private matchesRule;
|
|
@@ -861,7 +861,7 @@ export class PermissionClassifier {
|
|
|
861
861
|
return "ask";
|
|
862
862
|
}
|
|
863
863
|
}
|
|
864
|
-
async handleAsk(toolName, args, reason) {
|
|
864
|
+
async handleAsk(toolName, args, reason, opts) {
|
|
865
865
|
if (this.defaultMode === "dontAsk") {
|
|
866
866
|
this.log.info("permission.auto_deny", {
|
|
867
867
|
cat: "permission",
|
|
@@ -900,6 +900,7 @@ export class PermissionClassifier {
|
|
|
900
900
|
? `${baseDescription}\n\nReason (from pre_tool_use hook): ${reason}`
|
|
901
901
|
: baseDescription;
|
|
902
902
|
result = await this.approvalBackend.requestApproval({
|
|
903
|
+
...(opts?.sessionId ? { sessionId: opts.sessionId } : {}),
|
|
903
904
|
toolName,
|
|
904
905
|
args,
|
|
905
906
|
description,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { resolveShellInvocation } from "../../runtime/spawn-common.js";
|
|
1
2
|
export function createOffBackend() {
|
|
2
3
|
return {
|
|
3
4
|
name: "off",
|
|
4
5
|
wrap(command, opts) {
|
|
5
|
-
|
|
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`.
|
|
11
|
+
return resolveShellInvocation(command, opts.shell);
|
|
6
12
|
},
|
|
7
13
|
};
|
|
8
14
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -229,6 +229,8 @@ export interface PermissionRule {
|
|
|
229
229
|
reason?: string;
|
|
230
230
|
}
|
|
231
231
|
export interface ApprovalRequest {
|
|
232
|
+
/** Originating engine session. Hosts use this only to route the prompt UI. */
|
|
233
|
+
sessionId?: string;
|
|
232
234
|
toolName: string;
|
|
233
235
|
args: Record<string, unknown>;
|
|
234
236
|
description: string;
|
|
@@ -464,6 +466,8 @@ export interface LLMConfig {
|
|
|
464
466
|
apiKey?: string;
|
|
465
467
|
baseUrl?: string;
|
|
466
468
|
maxTokens?: number;
|
|
469
|
+
/** Per-model context window size used by hosts to seed Engine.maxContextTokens. */
|
|
470
|
+
maxContextTokens?: number;
|
|
467
471
|
/**
|
|
468
472
|
* Shell command whose stdout is the auth token (TODO 7.2). Resolved at
|
|
469
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