@cjhyy/code-shell-core 0.5.0-rc.0 → 0.5.0-rc.1
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/agent/agent-definition-registry.d.ts +14 -0
- package/dist/agent/agent-definition-registry.js +45 -0
- package/dist/agent/agent-definition.d.ts +20 -0
- package/dist/agent/agent-definition.js +38 -0
- package/dist/cli/agent-server-stdio.js +6 -1
- package/dist/engine/engine.d.ts +51 -5
- package/dist/engine/engine.js +113 -42
- package/dist/index.d.ts +1 -1
- package/dist/prompt/composer.d.ts +6 -0
- package/dist/prompt/composer.js +1 -0
- package/dist/settings/manager.d.ts +12 -1
- package/dist/settings/manager.js +27 -16
- package/dist/settings/manager.test.d.ts +1 -0
- package/dist/settings/manager.test.js +73 -0
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +7 -0
- package/dist/skills/scanner.d.ts +13 -6
- package/dist/skills/scanner.js +23 -4
- package/dist/tool-system/builtin/agent-registry.d.ts +4 -0
- package/dist/tool-system/builtin/agent-registry.js +6 -0
- package/dist/tool-system/builtin/agent.d.ts +36 -0
- package/dist/tool-system/builtin/agent.js +97 -5
- package/dist/tool-system/builtin/skill.js +23 -5
- package/dist/tool-system/context.d.ts +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AgentDefinition } from "./agent-definition.js";
|
|
2
|
+
/**
|
|
3
|
+
* Loads reusable sub-agent role definitions from a directory of `*.md` files.
|
|
4
|
+
* Non-recursive. Malformed files are skipped with a warning rather than
|
|
5
|
+
* failing the whole load — one bad role file must not break the agent system.
|
|
6
|
+
*/
|
|
7
|
+
export declare class AgentDefinitionRegistry {
|
|
8
|
+
private defs;
|
|
9
|
+
readonly warnings: string[];
|
|
10
|
+
static loadFromDir(dir: string): AgentDefinitionRegistry;
|
|
11
|
+
has(name: string): boolean;
|
|
12
|
+
get(name: string): AgentDefinition | undefined;
|
|
13
|
+
list(): AgentDefinition[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseAgentDefinition } from "./agent-definition.js";
|
|
4
|
+
/**
|
|
5
|
+
* Loads reusable sub-agent role definitions from a directory of `*.md` files.
|
|
6
|
+
* Non-recursive. Malformed files are skipped with a warning rather than
|
|
7
|
+
* failing the whole load — one bad role file must not break the agent system.
|
|
8
|
+
*/
|
|
9
|
+
export class AgentDefinitionRegistry {
|
|
10
|
+
defs = new Map();
|
|
11
|
+
warnings = [];
|
|
12
|
+
static loadFromDir(dir) {
|
|
13
|
+
const reg = new AgentDefinitionRegistry();
|
|
14
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
15
|
+
return reg;
|
|
16
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
17
|
+
if (!entry.endsWith(".md"))
|
|
18
|
+
continue;
|
|
19
|
+
const full = join(dir, entry);
|
|
20
|
+
try {
|
|
21
|
+
if (!statSync(full).isFile())
|
|
22
|
+
continue;
|
|
23
|
+
const def = parseAgentDefinition(readFileSync(full, "utf8"), entry);
|
|
24
|
+
if (reg.defs.has(def.name)) {
|
|
25
|
+
reg.warnings.push(`${entry}: duplicate agent name '${def.name}' ignored (first definition wins)`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
reg.defs.set(def.name, def);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
reg.warnings.push(`${entry}: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return reg;
|
|
35
|
+
}
|
|
36
|
+
has(name) {
|
|
37
|
+
return this.defs.has(name);
|
|
38
|
+
}
|
|
39
|
+
get(name) {
|
|
40
|
+
return this.defs.get(name);
|
|
41
|
+
}
|
|
42
|
+
list() {
|
|
43
|
+
return [...this.defs.values()];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A reusable sub-agent role, loaded from a Markdown file. */
|
|
2
|
+
export interface AgentDefinition {
|
|
3
|
+
/** Unique role key, e.g. "researcher". Matched against Agent({ agent_type }). */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Human-facing summary of when to use this role. */
|
|
6
|
+
description: string;
|
|
7
|
+
/** Optional ModelPool key (e.g. "flash"). Undefined → inherit parent model. */
|
|
8
|
+
model?: string;
|
|
9
|
+
/** Optional turn cap for this role. Undefined → caller/default decides. */
|
|
10
|
+
maxTurns?: number;
|
|
11
|
+
/** Optional tool allowlist. Undefined → inherit parent's full tool set. */
|
|
12
|
+
tools?: string[];
|
|
13
|
+
/** Markdown body — becomes the child Engine's appendSystemPrompt. */
|
|
14
|
+
systemPrompt: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse a Markdown agent-definition file (YAML frontmatter + body).
|
|
18
|
+
* Pure: no filesystem access. `sourceName` is only used in error messages.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseAgentDefinition(raw: string, sourceName: string): AgentDefinition;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a Markdown agent-definition file (YAML frontmatter + body).
|
|
4
|
+
* Pure: no filesystem access. `sourceName` is only used in error messages.
|
|
5
|
+
*/
|
|
6
|
+
export function parseAgentDefinition(raw, sourceName) {
|
|
7
|
+
const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw.trim());
|
|
8
|
+
if (!match) {
|
|
9
|
+
throw new Error(`${sourceName}: missing YAML frontmatter (expected leading '---' block)`);
|
|
10
|
+
}
|
|
11
|
+
const [, yamlSrc, body] = match;
|
|
12
|
+
let fm;
|
|
13
|
+
try {
|
|
14
|
+
fm = (parseYaml(yamlSrc) ?? {});
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
throw new Error(`${sourceName}: invalid YAML frontmatter — ${err.message}`, { cause: err });
|
|
18
|
+
}
|
|
19
|
+
if (typeof fm.name !== "string" || fm.name.trim().length === 0) {
|
|
20
|
+
throw new Error(`${sourceName}: frontmatter must include a non-empty 'name'`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof fm.description !== "string" || fm.description.trim().length === 0) {
|
|
23
|
+
throw new Error(`${sourceName}: frontmatter must include a non-empty 'description'`);
|
|
24
|
+
}
|
|
25
|
+
const def = {
|
|
26
|
+
name: fm.name.trim(),
|
|
27
|
+
description: fm.description.trim(),
|
|
28
|
+
systemPrompt: body.trim(),
|
|
29
|
+
};
|
|
30
|
+
if (typeof fm.model === "string" && fm.model.trim())
|
|
31
|
+
def.model = fm.model.trim();
|
|
32
|
+
if (typeof fm.maxTurns === "number")
|
|
33
|
+
def.maxTurns = fm.maxTurns;
|
|
34
|
+
if (Array.isArray(fm.tools)) {
|
|
35
|
+
def.tools = fm.tools.filter((t) => typeof t === "string");
|
|
36
|
+
}
|
|
37
|
+
return def;
|
|
38
|
+
}
|
|
@@ -40,7 +40,9 @@ import { CostTracker } from "../cost-tracker.js";
|
|
|
40
40
|
// ─── Read base config from environment / settings ─────────────────
|
|
41
41
|
const cwd = process.env.AGENT_CWD ?? process.cwd();
|
|
42
42
|
// Load settings once to derive llm config for the seed engine.
|
|
43
|
-
|
|
43
|
+
// Desktop is a host application: read the full disk hierarchy (incl. the
|
|
44
|
+
// user's ~/.code-shell). The SDK default 'project' would skip user config.
|
|
45
|
+
const settingsManager = new SettingsManager(cwd, "full");
|
|
44
46
|
const settings = settingsManager.get();
|
|
45
47
|
const llmConfig = {
|
|
46
48
|
provider: settings.model.provider,
|
|
@@ -54,6 +56,7 @@ const llmConfig = {
|
|
|
54
56
|
const seedEngine = new Engine({
|
|
55
57
|
llm: llmConfig,
|
|
56
58
|
cwd,
|
|
59
|
+
settingsScope: "full",
|
|
57
60
|
// No runtime — Engine.populateModelPoolFromSettings() runs in ctor.
|
|
58
61
|
});
|
|
59
62
|
// ─── Step 2: extract shared resources ────────────────────────────
|
|
@@ -93,6 +96,8 @@ const chatManager = new ChatSessionManager({
|
|
|
93
96
|
llm: resolvedLlmConfig,
|
|
94
97
|
cwd,
|
|
95
98
|
runtime,
|
|
99
|
+
// Inherit full scope so spawned subagents read user config too.
|
|
100
|
+
settingsScope: "full",
|
|
96
101
|
// Per-session overrides from the protocol request
|
|
97
102
|
permissionMode: slice.permissionMode,
|
|
98
103
|
preset: slice.preset,
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -10,10 +10,12 @@ import type { HookHandler } from "../hooks/registry.js";
|
|
|
10
10
|
import { SessionManager } from "../session/session-manager.js";
|
|
11
11
|
import type { CostStateStore } from "./cost-store.js";
|
|
12
12
|
import type { AskUserFn } from "../tool-system/builtin/ask-user.js";
|
|
13
|
+
import { type SettingsScope } from "../settings/manager.js";
|
|
13
14
|
import type { ToolContext } from "../tool-system/context.js";
|
|
14
15
|
import { type SandboxConfig } from "../tool-system/sandbox/index.js";
|
|
15
16
|
import { type AgentPresetName } from "../preset/index.js";
|
|
16
17
|
import { ModelPool, type ModelEntry } from "../llm/model-pool.js";
|
|
18
|
+
import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
|
|
17
19
|
import { EngineRuntime } from "./runtime.js";
|
|
18
20
|
export interface EngineConfig {
|
|
19
21
|
llm: LLMConfig;
|
|
@@ -68,6 +70,16 @@ export interface EngineConfig {
|
|
|
68
70
|
* work unchanged — T11 will migrate them.
|
|
69
71
|
*/
|
|
70
72
|
runtime?: EngineRuntime;
|
|
73
|
+
/**
|
|
74
|
+
* Which disk config layers this Engine may read. Defaults to 'project' —
|
|
75
|
+
* the safe default: a library/SDK embedding never silently inherits the
|
|
76
|
+
* host user's personal ~/.code-shell config (keys, models, MCP, hooks); it
|
|
77
|
+
* only reads the project-level ${cwd}/.code-shell that travels with a repo.
|
|
78
|
+
* Host-terminal entrypoints (TUI/desktop/CLI) pass 'full' to restore the
|
|
79
|
+
* managed+user+project+local behavior. 'isolated' reads no disk at all.
|
|
80
|
+
* Subagents inherit the parent Engine's scope. See SettingsScope.
|
|
81
|
+
*/
|
|
82
|
+
settingsScope?: SettingsScope;
|
|
71
83
|
}
|
|
72
84
|
export interface EngineHookConfig {
|
|
73
85
|
event: HookEventName;
|
|
@@ -82,6 +94,27 @@ export interface EngineResult {
|
|
|
82
94
|
turnCount: number;
|
|
83
95
|
usage: TokenUsage;
|
|
84
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the LLM config for a spawned child Engine.
|
|
99
|
+
* - `modelKey` set + present in pool → that model's config (over parent base).
|
|
100
|
+
* - otherwise (no key, no pool, or key miss) → the parent's llm unchanged.
|
|
101
|
+
* Key miss is a soft fallback, NOT an error: a stale agent definition must not
|
|
102
|
+
* crash the spawn.
|
|
103
|
+
*/
|
|
104
|
+
export declare function resolveChildLlm(modelKey: string | undefined, pool: ModelPool | undefined, parentLlm: LLMConfig): LLMConfig;
|
|
105
|
+
/** Load reusable sub-agent role definitions from <cwd>/.code-shell/agents. */
|
|
106
|
+
export declare function loadAgentDefinitionsForCwd(cwd: string): AgentDefinitionRegistry;
|
|
107
|
+
/**
|
|
108
|
+
* Compute a child Engine's tool scope.
|
|
109
|
+
* - `allowlist` set → child enabled = allowlist minus nested-agent tools
|
|
110
|
+
* (a per-role tool whitelist, e.g. a read-only researcher).
|
|
111
|
+
* - `allowlist` undefined → inherit parent enabled/disabled, always with the
|
|
112
|
+
* nested-agent tools forced into `disabled` (no grandchildren).
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolveChildToolScope(allowlist: string[] | undefined, parentDisabled: string[] | undefined, parentEnabled: string[] | undefined): {
|
|
115
|
+
enabled?: string[];
|
|
116
|
+
disabled: string[];
|
|
117
|
+
};
|
|
85
118
|
export declare class Engine {
|
|
86
119
|
private config;
|
|
87
120
|
private readonly preset;
|
|
@@ -90,6 +123,8 @@ export declare class Engine {
|
|
|
90
123
|
private sessionManager;
|
|
91
124
|
private mcpManager;
|
|
92
125
|
private modelPool;
|
|
126
|
+
/** Memoized sub-agent role registry, keyed by the cwd it was loaded from. */
|
|
127
|
+
private agentDefsCache?;
|
|
93
128
|
/** Shared resources supplied at construction (adapter pattern — null when self-constructed). */
|
|
94
129
|
readonly runtime: EngineRuntime | null;
|
|
95
130
|
/** Active permission mode for this Engine instance. */
|
|
@@ -279,6 +314,12 @@ export declare class Engine {
|
|
|
279
314
|
* Also syncs permissionMode to keep both fields consistent.
|
|
280
315
|
*/
|
|
281
316
|
setPlanMode(value: boolean): void;
|
|
317
|
+
/**
|
|
318
|
+
* Sub-agent role registry for the given cwd, memoized per-cwd so the
|
|
319
|
+
* directory is read once rather than every turn. A new cwd (e.g. via
|
|
320
|
+
* run({ cwd })) reloads.
|
|
321
|
+
*/
|
|
322
|
+
private getAgentDefinitions;
|
|
282
323
|
/**
|
|
283
324
|
* Build a base ToolContext for this Engine. Used by run() (which then
|
|
284
325
|
* overlays turn-specific fields like sandbox and subAgentSpawner) and
|
|
@@ -286,10 +327,15 @@ export declare class Engine {
|
|
|
286
327
|
*/
|
|
287
328
|
buildToolContext(): ToolContext;
|
|
288
329
|
/**
|
|
289
|
-
* Read settings.disabledSkills
|
|
290
|
-
*
|
|
291
|
-
* at ~line 237):
|
|
292
|
-
*
|
|
330
|
+
* Read settings.disabledSkills + settings.disabledPlugins in a single
|
|
331
|
+
* pass. Sub-agents skip both for the same reason they skip
|
|
332
|
+
* settings.hooks / plugin hooks (registerSettingsHooks at ~line 237):
|
|
333
|
+
* they run with a minimal surface area. Defaults to [] for both
|
|
334
|
+
* fields so callers don't have to null-check.
|
|
335
|
+
*
|
|
336
|
+
* Combined read avoids drift if settings change between two separate
|
|
337
|
+
* reads — the prompt composer and the tool context will always see
|
|
338
|
+
* the same snapshot.
|
|
293
339
|
*/
|
|
294
|
-
private
|
|
340
|
+
private readDisabledLists;
|
|
295
341
|
}
|
package/dist/engine/engine.js
CHANGED
|
@@ -27,6 +27,7 @@ import { FileHistory } from "../session/file-history.js";
|
|
|
27
27
|
import { defaultSandboxConfig, resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
28
28
|
import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js";
|
|
29
29
|
import { ModelPool } from "../llm/model-pool.js";
|
|
30
|
+
import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
|
|
30
31
|
import { ProviderCatalog } from "../llm/provider-catalog.js";
|
|
31
32
|
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
32
33
|
import { detectProviderFromApiKey, buildModelPool, } from "../onboarding.js";
|
|
@@ -37,6 +38,44 @@ import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
|
|
|
37
38
|
import { join } from "node:path";
|
|
38
39
|
import { homedir } from "node:os";
|
|
39
40
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the LLM config for a spawned child Engine.
|
|
43
|
+
* - `modelKey` set + present in pool → that model's config (over parent base).
|
|
44
|
+
* - otherwise (no key, no pool, or key miss) → the parent's llm unchanged.
|
|
45
|
+
* Key miss is a soft fallback, NOT an error: a stale agent definition must not
|
|
46
|
+
* crash the spawn.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveChildLlm(modelKey, pool, parentLlm) {
|
|
49
|
+
if (modelKey && pool?.has(modelKey)) {
|
|
50
|
+
const resolved = pool.resolveLLMConfig(modelKey, parentLlm);
|
|
51
|
+
if (resolved)
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
return parentLlm;
|
|
55
|
+
}
|
|
56
|
+
/** Load reusable sub-agent role definitions from <cwd>/.code-shell/agents. */
|
|
57
|
+
export function loadAgentDefinitionsForCwd(cwd) {
|
|
58
|
+
return AgentDefinitionRegistry.loadFromDir(`${cwd}/.code-shell/agents`);
|
|
59
|
+
}
|
|
60
|
+
const NESTED_AGENT_TOOLS = ["Agent", "AgentStatus", "AgentCancel"];
|
|
61
|
+
/**
|
|
62
|
+
* Compute a child Engine's tool scope.
|
|
63
|
+
* - `allowlist` set → child enabled = allowlist minus nested-agent tools
|
|
64
|
+
* (a per-role tool whitelist, e.g. a read-only researcher).
|
|
65
|
+
* - `allowlist` undefined → inherit parent enabled/disabled, always with the
|
|
66
|
+
* nested-agent tools forced into `disabled` (no grandchildren).
|
|
67
|
+
*/
|
|
68
|
+
export function resolveChildToolScope(allowlist, parentDisabled, parentEnabled) {
|
|
69
|
+
if (allowlist) {
|
|
70
|
+
return {
|
|
71
|
+
enabled: allowlist.filter((t) => !NESTED_AGENT_TOOLS.includes(t)),
|
|
72
|
+
disabled: [...NESTED_AGENT_TOOLS],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const disabled = Array.from(new Set([...(parentDisabled ?? []), ...NESTED_AGENT_TOOLS]));
|
|
76
|
+
const enabled = parentEnabled?.filter((t) => !NESTED_AGENT_TOOLS.includes(t));
|
|
77
|
+
return { enabled, disabled };
|
|
78
|
+
}
|
|
40
79
|
export class Engine {
|
|
41
80
|
config;
|
|
42
81
|
preset;
|
|
@@ -45,6 +84,8 @@ export class Engine {
|
|
|
45
84
|
sessionManager;
|
|
46
85
|
mcpManager;
|
|
47
86
|
modelPool;
|
|
87
|
+
/** Memoized sub-agent role registry, keyed by the cwd it was loaded from. */
|
|
88
|
+
agentDefsCache;
|
|
48
89
|
/** Shared resources supplied at construction (adapter pattern — null when self-constructed). */
|
|
49
90
|
runtime;
|
|
50
91
|
/** Active permission mode for this Engine instance. */
|
|
@@ -201,24 +242,31 @@ export class Engine {
|
|
|
201
242
|
// We then switch the pool and write the resolved entry's credentials
|
|
202
243
|
// into config.llm, so the first run() uses the right endpoint instead
|
|
203
244
|
// of whatever env-derived fallback repl.ts seeded earlier.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
245
|
+
// Sub-agents skip the activeKey resync: their llm is chosen by the
|
|
246
|
+
// parent's resolveChildLlm (per-role model routing). activeKey is the
|
|
247
|
+
// *user's* current UI model selection and must not clobber a child's
|
|
248
|
+
// routed model — without this guard a role's `model: flash` is silently
|
|
249
|
+
// overridden back to whatever the user has active in the foreground.
|
|
250
|
+
if (this.config.isSubAgent !== true) {
|
|
251
|
+
const activeKey = settings.activeKey;
|
|
252
|
+
let match;
|
|
253
|
+
if (activeKey) {
|
|
254
|
+
match = settings.models.find((m) => m.key === activeKey);
|
|
255
|
+
}
|
|
256
|
+
if (!match) {
|
|
257
|
+
const currentModel = this.config.llm.model;
|
|
258
|
+
// OpenRouter stores entries as "provider/model-name"; the top-level
|
|
259
|
+
// settings.model.name is just "model-name". Match either form.
|
|
260
|
+
match = settings.models.find((m) => m.model === currentModel ||
|
|
261
|
+
(currentModel && m.model?.endsWith(`/${currentModel}`)));
|
|
262
|
+
}
|
|
263
|
+
if (match) {
|
|
264
|
+
const entry = this.modelPool.switch(match.key);
|
|
265
|
+
this.config = {
|
|
266
|
+
...this.config,
|
|
267
|
+
llm: this.modelPool.toLLMConfig(entry, this.config.llm),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
222
270
|
}
|
|
223
271
|
}
|
|
224
272
|
else if (this.config.llm.apiKey) {
|
|
@@ -388,29 +436,27 @@ export class Engine {
|
|
|
388
436
|
// background process explosion), and the sid / approval / dock
|
|
389
437
|
// model assumes a flat parent→children hierarchy. Layered with a
|
|
390
438
|
// runtime check in agent.ts as defense-in-depth.
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
...(this.config.disabledBuiltinTools ?? []),
|
|
394
|
-
...NESTED_AGENT_TOOLS,
|
|
395
|
-
]));
|
|
396
|
-
// If enabledBuiltinTools is set (explicit allow-list mode), strip
|
|
397
|
-
// the nested-agent tools from it too so the disable above isn't
|
|
398
|
-
// contradicted by an explicit allow.
|
|
399
|
-
const childEnabled = this.config.enabledBuiltinTools?.filter((t) => !NESTED_AGENT_TOOLS.includes(t));
|
|
439
|
+
const { enabled: childEnabled, disabled: childDisabled } = resolveChildToolScope(req.toolAllowlist, this.config.disabledBuiltinTools, this.config.enabledBuiltinTools);
|
|
440
|
+
const childLlm = resolveChildLlm(req.model, this.modelPool, this.config.llm);
|
|
400
441
|
const child = new Engine({
|
|
401
|
-
llm: { ...
|
|
442
|
+
llm: { ...childLlm, retryMaxAttempts: 2 },
|
|
402
443
|
cwd,
|
|
403
444
|
permissionMode: this.config.permissionMode,
|
|
404
445
|
preset: this.preset.name,
|
|
405
446
|
enabledBuiltinTools: childEnabled,
|
|
406
447
|
disabledBuiltinTools: childDisabled,
|
|
407
448
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
408
|
-
appendSystemPrompt: this.config.appendSystemPrompt,
|
|
449
|
+
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt]
|
|
450
|
+
.filter(Boolean)
|
|
451
|
+
.join("\n\n") || undefined,
|
|
409
452
|
maxTurns: req.maxTurns,
|
|
410
453
|
maxContextTokens: this.config.maxContextTokens ?? 200_000,
|
|
411
454
|
sessionStorageDir: this.config.sessionStorageDir,
|
|
412
455
|
headless: this.config.headless,
|
|
413
456
|
sandbox: this.config.sandbox,
|
|
457
|
+
// Subagents inherit the parent's scope: a child runs in the same
|
|
458
|
+
// cwd/session, so it should see the same config layers the parent did.
|
|
459
|
+
settingsScope: this.config.settingsScope ?? "project",
|
|
414
460
|
isSubAgent: true,
|
|
415
461
|
});
|
|
416
462
|
// Where the spawned child Engine's stream events go. AgentTool's
|
|
@@ -470,6 +516,7 @@ export class Engine {
|
|
|
470
516
|
const toolCtx = {
|
|
471
517
|
...this.buildToolContext(),
|
|
472
518
|
subAgentSpawner,
|
|
519
|
+
agentDefinitions: this.getAgentDefinitions(cwd),
|
|
473
520
|
sandbox: sandboxBackend,
|
|
474
521
|
cwd,
|
|
475
522
|
// TodoWrite reads this to push task_update events independently
|
|
@@ -715,13 +762,15 @@ export class Engine {
|
|
|
715
762
|
maxTokens: this.resolveMaxContextTokens(),
|
|
716
763
|
});
|
|
717
764
|
this.lastContextManager = contextManager;
|
|
765
|
+
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
718
766
|
const promptComposer = new PromptComposer({
|
|
719
767
|
cwd,
|
|
720
768
|
model: this.config.llm.model,
|
|
721
769
|
preset: this.preset,
|
|
722
770
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
723
771
|
appendSystemPrompt: this.config.appendSystemPrompt,
|
|
724
|
-
disabledSkills
|
|
772
|
+
disabledSkills,
|
|
773
|
+
disabledPlugins,
|
|
725
774
|
});
|
|
726
775
|
// Connect MCP servers (if configured and not already connected).
|
|
727
776
|
// B1: prefer the Runtime-owned MCPManager so all sessions in a
|
|
@@ -1305,7 +1354,7 @@ export class Engine {
|
|
|
1305
1354
|
}
|
|
1306
1355
|
getSettingsManager() {
|
|
1307
1356
|
if (!this.settingsManager) {
|
|
1308
|
-
this.settingsManager = new SettingsManager(this.config.cwd);
|
|
1357
|
+
this.settingsManager = new SettingsManager(this.config.cwd, this.config.settingsScope ?? "project");
|
|
1309
1358
|
}
|
|
1310
1359
|
return this.settingsManager;
|
|
1311
1360
|
}
|
|
@@ -1357,7 +1406,7 @@ export class Engine {
|
|
|
1357
1406
|
rules.push({ tool: "Bash", decision: "allow" });
|
|
1358
1407
|
}
|
|
1359
1408
|
try {
|
|
1360
|
-
const settingsManager = new SettingsManager(cwd);
|
|
1409
|
+
const settingsManager = new SettingsManager(cwd, this.config.settingsScope ?? "project");
|
|
1361
1410
|
const settings = settingsManager.get();
|
|
1362
1411
|
if (settings.permissions?.rules?.length) {
|
|
1363
1412
|
rules.unshift(...settings.permissions.rules);
|
|
@@ -1428,12 +1477,24 @@ export class Engine {
|
|
|
1428
1477
|
this.planMode = value;
|
|
1429
1478
|
}
|
|
1430
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Sub-agent role registry for the given cwd, memoized per-cwd so the
|
|
1482
|
+
* directory is read once rather than every turn. A new cwd (e.g. via
|
|
1483
|
+
* run({ cwd })) reloads.
|
|
1484
|
+
*/
|
|
1485
|
+
getAgentDefinitions(cwd) {
|
|
1486
|
+
if (this.agentDefsCache?.cwd !== cwd) {
|
|
1487
|
+
this.agentDefsCache = { cwd, reg: loadAgentDefinitionsForCwd(cwd) };
|
|
1488
|
+
}
|
|
1489
|
+
return this.agentDefsCache.reg;
|
|
1490
|
+
}
|
|
1431
1491
|
/**
|
|
1432
1492
|
* Build a base ToolContext for this Engine. Used by run() (which then
|
|
1433
1493
|
* overlays turn-specific fields like sandbox and subAgentSpawner) and
|
|
1434
1494
|
* by tests that want a ToolContext without a full run() cycle.
|
|
1435
1495
|
*/
|
|
1436
1496
|
buildToolContext() {
|
|
1497
|
+
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
1437
1498
|
return {
|
|
1438
1499
|
cwd: this.config.cwd ?? process.cwd(),
|
|
1439
1500
|
llmConfig: this.config.llm,
|
|
@@ -1444,24 +1505,34 @@ export class Engine {
|
|
|
1444
1505
|
hooks: this.hooks,
|
|
1445
1506
|
planMode: this.planMode,
|
|
1446
1507
|
engine: this,
|
|
1447
|
-
disabledSkills
|
|
1508
|
+
disabledSkills,
|
|
1509
|
+
disabledPlugins,
|
|
1448
1510
|
};
|
|
1449
1511
|
}
|
|
1450
1512
|
/**
|
|
1451
|
-
* Read settings.disabledSkills
|
|
1452
|
-
*
|
|
1453
|
-
* at ~line 237):
|
|
1454
|
-
*
|
|
1513
|
+
* Read settings.disabledSkills + settings.disabledPlugins in a single
|
|
1514
|
+
* pass. Sub-agents skip both for the same reason they skip
|
|
1515
|
+
* settings.hooks / plugin hooks (registerSettingsHooks at ~line 237):
|
|
1516
|
+
* they run with a minimal surface area. Defaults to [] for both
|
|
1517
|
+
* fields so callers don't have to null-check.
|
|
1518
|
+
*
|
|
1519
|
+
* Combined read avoids drift if settings change between two separate
|
|
1520
|
+
* reads — the prompt composer and the tool context will always see
|
|
1521
|
+
* the same snapshot.
|
|
1455
1522
|
*/
|
|
1456
|
-
|
|
1457
|
-
if (this.config.isSubAgent === true)
|
|
1458
|
-
return [];
|
|
1523
|
+
readDisabledLists() {
|
|
1524
|
+
if (this.config.isSubAgent === true) {
|
|
1525
|
+
return { disabledSkills: [], disabledPlugins: [] };
|
|
1526
|
+
}
|
|
1459
1527
|
try {
|
|
1460
1528
|
const settings = this.getSettingsManager().get();
|
|
1461
|
-
return
|
|
1529
|
+
return {
|
|
1530
|
+
disabledSkills: settings.disabledSkills ?? [],
|
|
1531
|
+
disabledPlugins: settings.disabledPlugins ?? [],
|
|
1532
|
+
};
|
|
1462
1533
|
}
|
|
1463
1534
|
catch {
|
|
1464
|
-
return [];
|
|
1535
|
+
return { disabledSkills: [], disabledPlugins: [] };
|
|
1465
1536
|
}
|
|
1466
1537
|
}
|
|
1467
1538
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export type { IterateConfig, IterateResult, IterateSubject, IterateFormat, Itera
|
|
|
72
72
|
export { RunManager, type RunManagerConfig, type RunStore, FileRunStore, RunQueue, EngineRunner, type EngineRunnerConfig, type RunExecutionHandle, type RunExecutor, type CustomToolEntry, RunApprovalBackend, createRunAskUserFn, type RunLifecycleHooks, CheckpointWriter, ArtifactTracker, RunLock, Heartbeat, NoopEvaluator, CompositeEvaluator, type Evaluator, type EvaluatorResult, type EvaluatorContext, type RunStatus, type RunSnapshot, type RunEvent, type RunCheckpoint, type RunApproval, type RunArtifactRef, type SubmitRunInput, type ResumeRunInput, type ListRunsQuery, type RunStreamEvent, type RunStreamCallback, type DetachFn, VALID_TRANSITIONS, createRunManager, type CreateRunManagerOptions, } from "./run/index.js";
|
|
73
73
|
export { defineProduct, type ProductDefinition, type ProductPreset, type ProductAdapter, type ProductContract, type CustomTool, type ProductRuntimeOptions, type ProductInstance, } from "./product/index.js";
|
|
74
74
|
export { logger } from "./logging/logger.js";
|
|
75
|
-
export { SettingsManager } from "./settings/manager.js";
|
|
75
|
+
export { SettingsManager, type SettingsScope } from "./settings/manager.js";
|
|
76
76
|
export { SettingsSchema, validateSettings } from "./settings/schema.js";
|
|
77
77
|
export { getSessionId, switchSession, getOriginalCwd, setOriginalCwd, getProjectRoot, setProjectRoot, getCwdState, getIsInteractive, updateLastInteractionTime, flushInteractionTime, markScrollActivity, type AttributedCounter, type ChannelEntry, } from "./state.js";
|
|
78
78
|
export { getGraphemeSegmenter, firstGrapheme, lastGrapheme, getWordSegmenter, getRelativeTimeFormat, getTimeZone, getSystemLocaleLanguage, } from "./utils/intl.js";
|
|
@@ -23,6 +23,12 @@ export interface ComposerOptions {
|
|
|
23
23
|
* dispatch — see scanSkills(opts.disabledSkills) and skillTool.
|
|
24
24
|
*/
|
|
25
25
|
disabledSkills?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Plugin names the user has totally disabled. Coarser knob than
|
|
28
|
+
* disabledSkills — every skill whose namespaced name starts with
|
|
29
|
+
* `${pluginName}:` is filtered. See scanSkills(opts.disabledPlugins).
|
|
30
|
+
*/
|
|
31
|
+
disabledPlugins?: string[];
|
|
26
32
|
}
|
|
27
33
|
export declare class PromptComposer {
|
|
28
34
|
private readonly options;
|
package/dist/prompt/composer.js
CHANGED
|
@@ -5,11 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type ValidatedSettings } from "./schema.js";
|
|
7
7
|
export type SettingsSourceName = "managed" | "user" | "project" | "local" | "flag";
|
|
8
|
+
/**
|
|
9
|
+
* Which disk layers a SettingsManager is allowed to read.
|
|
10
|
+
* 'full' — managed + user (~/.code-shell) + project + local (host terminal entrypoints)
|
|
11
|
+
* 'project' — project + local only (${cwd}/.code-shell); never the host user dir. [default]
|
|
12
|
+
* 'isolated' — no disk layers at all; only explicit flag overrides.
|
|
13
|
+
* Flag overrides always apply regardless of scope. Default is 'project' so a
|
|
14
|
+
* codeshell library/SDK embedding never silently inherits the host user's
|
|
15
|
+
* personal ~/.code-shell config (keys, models, MCP servers, hooks).
|
|
16
|
+
*/
|
|
17
|
+
export type SettingsScope = "isolated" | "project" | "full";
|
|
8
18
|
export declare class SettingsManager {
|
|
9
19
|
private readonly cwd;
|
|
20
|
+
private readonly scope;
|
|
10
21
|
private sources;
|
|
11
22
|
private merged;
|
|
12
|
-
constructor(cwd?: string);
|
|
23
|
+
constructor(cwd?: string, scope?: SettingsScope);
|
|
13
24
|
/**
|
|
14
25
|
* Load settings from all sources.
|
|
15
26
|
*/
|
package/dist/settings/manager.js
CHANGED
|
@@ -20,29 +20,39 @@ function userHome() {
|
|
|
20
20
|
}
|
|
21
21
|
export class SettingsManager {
|
|
22
22
|
cwd;
|
|
23
|
+
scope;
|
|
23
24
|
sources = [];
|
|
24
25
|
merged = null;
|
|
25
|
-
constructor(cwd = process.cwd()) {
|
|
26
|
+
constructor(cwd = process.cwd(), scope = "project") {
|
|
26
27
|
this.cwd = cwd;
|
|
28
|
+
this.scope = scope;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* Load settings from all sources.
|
|
30
32
|
*/
|
|
31
33
|
load(flagOverrides) {
|
|
32
34
|
this.sources = [];
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
// Scope gates which disk layers we read. 'full' reads the host user dir
|
|
36
|
+
// (~/.code-shell); 'project' and 'isolated' never do. See SettingsScope.
|
|
37
|
+
const readUser = this.scope === "full";
|
|
38
|
+
const readProject = this.scope !== "isolated";
|
|
39
|
+
if (readUser) {
|
|
40
|
+
// 1. Managed (lowest priority)
|
|
41
|
+
this.loadJsonFile(join(userHome(), ".code-shell", "settings.managed.json"), "managed", 0);
|
|
42
|
+
// 2. User — only ~/.code-shell/. We used to also read ~/.claude/settings.json
|
|
43
|
+
// for "zero-migration from Claude Code", but Claude Code's schema diverges
|
|
44
|
+
// (e.g. `model` is a string there, an object here). Merging caused boot
|
|
45
|
+
// crashes on machines that had Claude Code installed but never ran us.
|
|
46
|
+
// File-level compat (CLAUDE.md, .claude/skills/) is kept elsewhere — only
|
|
47
|
+
// the settings.json read is dropped.
|
|
48
|
+
this.loadJsonFile(join(userHome(), ".code-shell", "settings.json"), "user", 1);
|
|
49
|
+
}
|
|
50
|
+
if (readProject) {
|
|
51
|
+
// 3. Project
|
|
52
|
+
this.loadJsonFile(join(this.cwd, ".code-shell", "settings.json"), "project", 2);
|
|
53
|
+
// 4. Local
|
|
54
|
+
this.loadJsonFile(join(this.cwd, ".code-shell", "settings.local.json"), "local", 3);
|
|
55
|
+
}
|
|
46
56
|
// 5. CLI flags (highest priority)
|
|
47
57
|
if (flagOverrides && Object.keys(flagOverrides).length > 0) {
|
|
48
58
|
this.sources.push({ name: "flag", priority: 4, data: flagOverrides });
|
|
@@ -54,9 +64,10 @@ export class SettingsManager {
|
|
|
54
64
|
// Auto-migrate legacy models[] in the user settings file. Runs directly
|
|
55
65
|
// on the user-scope file (not the merged result), because the merge
|
|
56
66
|
// collapses provenance and the migration needs to write back to a
|
|
57
|
-
// single physical file.
|
|
67
|
+
// single physical file. Gated on readUser: under non-full scope we must
|
|
68
|
+
// not read — let alone rewrite — the host's ~/.code-shell/settings.json.
|
|
58
69
|
const userPath = join(userHome(), ".code-shell", "settings.json");
|
|
59
|
-
if (existsSync(userPath)) {
|
|
70
|
+
if (readUser && existsSync(userPath)) {
|
|
60
71
|
try {
|
|
61
72
|
const userRaw = JSON.parse(readFileSync(userPath, "utf-8"));
|
|
62
73
|
const result = migrateModels({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { SettingsManager } from "./manager.js";
|
|
6
|
+
/**
|
|
7
|
+
* Scope isolation tests. We point HOME at a temp dir (userHome() reads
|
|
8
|
+
* process.env.HOME first) and use a separate temp cwd, then seed each of the
|
|
9
|
+
* four disk layers with a uniquely-named mcpServers entry. mcpServers is a
|
|
10
|
+
* record merged key-by-key, so every layer that was actually read leaves its
|
|
11
|
+
* own key in the merged result — letting us detect which layers were read
|
|
12
|
+
* (unlike arrays, which deep-merge replaces wholesale).
|
|
13
|
+
*/
|
|
14
|
+
describe("SettingsManager scope", () => {
|
|
15
|
+
let home;
|
|
16
|
+
let cwd;
|
|
17
|
+
let prevHome;
|
|
18
|
+
function serverFor(tag) {
|
|
19
|
+
return { [tag]: { name: tag, command: "echo", transport: "stdio" } };
|
|
20
|
+
}
|
|
21
|
+
function writeSettings(dir, file, tag) {
|
|
22
|
+
mkdirSync(join(dir, ".code-shell"), { recursive: true });
|
|
23
|
+
writeFileSync(join(dir, ".code-shell", file), JSON.stringify({ mcpServers: serverFor(tag) }), "utf-8");
|
|
24
|
+
}
|
|
25
|
+
function layersIn(sm) {
|
|
26
|
+
return Object.keys(sm.get().mcpServers ?? {}).sort();
|
|
27
|
+
}
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
prevHome = process.env.HOME;
|
|
30
|
+
home = mkdtempSync(join(tmpdir(), "cs-home-"));
|
|
31
|
+
cwd = mkdtempSync(join(tmpdir(), "cs-cwd-"));
|
|
32
|
+
process.env.HOME = home;
|
|
33
|
+
writeSettings(home, "settings.managed.json", "MANAGED");
|
|
34
|
+
writeSettings(home, "settings.json", "USER");
|
|
35
|
+
writeSettings(cwd, "settings.json", "PROJECT");
|
|
36
|
+
writeSettings(cwd, "settings.local.json", "LOCAL");
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.env.HOME = prevHome;
|
|
40
|
+
rmSync(home, { recursive: true, force: true });
|
|
41
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
test("full reads all four disk layers", () => {
|
|
44
|
+
expect(layersIn(new SettingsManager(cwd, "full"))).toEqual([
|
|
45
|
+
"LOCAL",
|
|
46
|
+
"MANAGED",
|
|
47
|
+
"PROJECT",
|
|
48
|
+
"USER",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
test("project (default) reads project+local, never user/managed", () => {
|
|
52
|
+
expect(layersIn(new SettingsManager(cwd, "project"))).toEqual(["LOCAL", "PROJECT"]);
|
|
53
|
+
});
|
|
54
|
+
test("default scope is project", () => {
|
|
55
|
+
expect(layersIn(new SettingsManager(cwd))).toEqual(["LOCAL", "PROJECT"]);
|
|
56
|
+
});
|
|
57
|
+
test("isolated reads no disk layers", () => {
|
|
58
|
+
expect(layersIn(new SettingsManager(cwd, "isolated"))).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
test("flag overrides apply even under isolated", () => {
|
|
61
|
+
const sm = new SettingsManager(cwd, "isolated");
|
|
62
|
+
sm.load({ mcpServers: serverFor("FLAG") });
|
|
63
|
+
expect(layersIn(sm)).toEqual(["FLAG"]);
|
|
64
|
+
});
|
|
65
|
+
test("non-full scope never triggers user-file model migration write-back", () => {
|
|
66
|
+
// Legacy models[] in the user file would normally be migrated (and the
|
|
67
|
+
// file rewritten with a .bak). Under project/isolated we must not touch
|
|
68
|
+
// the user file at all.
|
|
69
|
+
writeFileSync(join(home, ".code-shell", "settings.json"), JSON.stringify({ models: [{ id: "legacy", provider: "x" }] }), "utf-8");
|
|
70
|
+
new SettingsManager(cwd, "project").get();
|
|
71
|
+
expect(existsSync(join(home, ".code-shell", "settings.json.bak"))).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -257,6 +257,13 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
257
257
|
* tool both see the filtered set — see scanSkills(opts).
|
|
258
258
|
*/
|
|
259
259
|
disabledSkills: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
260
|
+
/**
|
|
261
|
+
* Plugin-level total switch: every skill whose namespaced name
|
|
262
|
+
* starts with `${pluginName}:` is filtered. Coarser knob than
|
|
263
|
+
* disabledSkills; both are honored simultaneously. Bare plugin
|
|
264
|
+
* names (no colon suffix). See scanSkills(opts.disabledPlugins).
|
|
265
|
+
*/
|
|
266
|
+
disabledPlugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
260
267
|
instructions: z.ZodDefault<z.ZodObject<{
|
|
261
268
|
fileName: z.ZodDefault<z.ZodString>;
|
|
262
269
|
scanDirs: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -661,6 +668,13 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
661
668
|
* tool both see the filtered set — see scanSkills(opts).
|
|
662
669
|
*/
|
|
663
670
|
disabledSkills: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
671
|
+
/**
|
|
672
|
+
* Plugin-level total switch: every skill whose namespaced name
|
|
673
|
+
* starts with `${pluginName}:` is filtered. Coarser knob than
|
|
674
|
+
* disabledSkills; both are honored simultaneously. Bare plugin
|
|
675
|
+
* names (no colon suffix). See scanSkills(opts.disabledPlugins).
|
|
676
|
+
*/
|
|
677
|
+
disabledPlugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
664
678
|
instructions: z.ZodDefault<z.ZodObject<{
|
|
665
679
|
fileName: z.ZodDefault<z.ZodString>;
|
|
666
680
|
scanDirs: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -1065,6 +1079,13 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
1065
1079
|
* tool both see the filtered set — see scanSkills(opts).
|
|
1066
1080
|
*/
|
|
1067
1081
|
disabledSkills: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
1082
|
+
/**
|
|
1083
|
+
* Plugin-level total switch: every skill whose namespaced name
|
|
1084
|
+
* starts with `${pluginName}:` is filtered. Coarser knob than
|
|
1085
|
+
* disabledSkills; both are honored simultaneously. Bare plugin
|
|
1086
|
+
* names (no colon suffix). See scanSkills(opts.disabledPlugins).
|
|
1087
|
+
*/
|
|
1088
|
+
disabledPlugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
1068
1089
|
instructions: z.ZodDefault<z.ZodObject<{
|
|
1069
1090
|
fileName: z.ZodDefault<z.ZodString>;
|
|
1070
1091
|
scanDirs: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
package/dist/settings/schema.js
CHANGED
|
@@ -155,6 +155,13 @@ export const SettingsSchema = z
|
|
|
155
155
|
* tool both see the filtered set — see scanSkills(opts).
|
|
156
156
|
*/
|
|
157
157
|
disabledSkills: z.array(z.string()).default([]),
|
|
158
|
+
/**
|
|
159
|
+
* Plugin-level total switch: every skill whose namespaced name
|
|
160
|
+
* starts with `${pluginName}:` is filtered. Coarser knob than
|
|
161
|
+
* disabledSkills; both are honored simultaneously. Bare plugin
|
|
162
|
+
* names (no colon suffix). See scanSkills(opts.disabledPlugins).
|
|
163
|
+
*/
|
|
164
|
+
disabledPlugins: z.array(z.string()).default([]),
|
|
158
165
|
instructions: z
|
|
159
166
|
.object({
|
|
160
167
|
fileName: z.string().default("CODESHELL.md"),
|
package/dist/skills/scanner.d.ts
CHANGED
|
@@ -17,15 +17,22 @@ export interface SkillDefinition {
|
|
|
17
17
|
source: "project" | "user" | "plugin";
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Options accepted by scanSkills.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* never force a re-scan.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* Options accepted by scanSkills. Both filters are applied after the
|
|
21
|
+
* memoized scan returns so the cache stays warm across different filter
|
|
22
|
+
* values — changing `settings.disabledSkills` or `settings.disabledPlugins`
|
|
23
|
+
* should never force a re-scan.
|
|
24
|
+
*
|
|
25
|
+
* - `disabledSkills` names must match the SkillDefinition.name exactly,
|
|
26
|
+
* including any "<plugin>:" prefix — see scanInstalledPlugins() at
|
|
27
|
+
* line ~168 for namespace construction.
|
|
28
|
+
* - `disabledPlugins` names are bare plugin names (no colon suffix).
|
|
29
|
+
* Every skill whose name starts with `${pluginName}:` is filtered.
|
|
30
|
+
* This is the coarse "plugin total switch" knob; `disabledSkills` is
|
|
31
|
+
* the per-skill knob.
|
|
26
32
|
*/
|
|
27
33
|
export interface ScanSkillsOptions {
|
|
28
34
|
disabledSkills?: string[];
|
|
35
|
+
disabledPlugins?: string[];
|
|
29
36
|
}
|
|
30
37
|
export declare function scanSkills(cwd: string, opts?: ScanSkillsOptions): SkillDefinition[];
|
|
31
38
|
export declare function invalidateSkillCache(): void;
|
package/dist/skills/scanner.js
CHANGED
|
@@ -160,11 +160,30 @@ function installedPluginsMtime() {
|
|
|
160
160
|
const memoized = memoize(scanOnce, (cwd) => `${cwd}\0${userHome()}\0${installedPluginsMtime()}`);
|
|
161
161
|
export function scanSkills(cwd, opts) {
|
|
162
162
|
const all = memoized(cwd);
|
|
163
|
-
const
|
|
164
|
-
|
|
163
|
+
const disabledSkills = opts?.disabledSkills;
|
|
164
|
+
const disabledPlugins = opts?.disabledPlugins;
|
|
165
|
+
const hasSkillFilter = disabledSkills && disabledSkills.length > 0;
|
|
166
|
+
const hasPluginFilter = disabledPlugins && disabledPlugins.length > 0;
|
|
167
|
+
if (!hasSkillFilter && !hasPluginFilter)
|
|
165
168
|
return all;
|
|
166
|
-
const
|
|
167
|
-
|
|
169
|
+
const skillSet = hasSkillFilter ? new Set(disabledSkills) : null;
|
|
170
|
+
const pluginSet = hasPluginFilter ? new Set(disabledPlugins) : null;
|
|
171
|
+
return all.filter((s) => {
|
|
172
|
+
if (skillSet && skillSet.has(s.name))
|
|
173
|
+
return false;
|
|
174
|
+
if (pluginSet) {
|
|
175
|
+
// Use indexOf, not split — skill names may theoretically contain
|
|
176
|
+
// more colons after the first; the namespace boundary is the
|
|
177
|
+
// first ":" only.
|
|
178
|
+
const colon = s.name.indexOf(":");
|
|
179
|
+
if (colon > 0) {
|
|
180
|
+
const prefix = s.name.slice(0, colon);
|
|
181
|
+
if (pluginSet.has(prefix))
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
168
187
|
}
|
|
169
188
|
export function invalidateSkillCache() {
|
|
170
189
|
memoized.cache.clear?.();
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* RunManager, not here.
|
|
13
13
|
*/
|
|
14
14
|
export type AsyncAgentStatus = "running" | "completed" | "failed" | "cancelled";
|
|
15
|
+
/** Process-wide cap on concurrent background sub-agents (aligns with Codex max_threads=6). */
|
|
16
|
+
export declare const MAX_BACKGROUND_AGENTS = 6;
|
|
15
17
|
/**
|
|
16
18
|
* Minimal structural shape for an entry in an agent's transcript. We avoid
|
|
17
19
|
* importing the UI's `ChatEntry` here to prevent a tool-system → ui import
|
|
@@ -44,6 +46,8 @@ declare class AsyncAgentRegistry {
|
|
|
44
46
|
subscribe: (cb: () => void) => (() => void);
|
|
45
47
|
getSnapshot: () => AsyncAgentEntry[];
|
|
46
48
|
hasRunning: () => boolean;
|
|
49
|
+
/** Count of agents currently in the "running" state (cap enforcement). */
|
|
50
|
+
runningCount(): number;
|
|
47
51
|
private notify;
|
|
48
52
|
register(entry: AsyncAgentEntry): void;
|
|
49
53
|
appendToTranscript(agentId: string, entry: AgentTranscriptEntry): void;
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* the right boundary: long-running cross-process work belongs to
|
|
12
12
|
* RunManager, not here.
|
|
13
13
|
*/
|
|
14
|
+
/** Process-wide cap on concurrent background sub-agents (aligns with Codex max_threads=6). */
|
|
15
|
+
export const MAX_BACKGROUND_AGENTS = 6;
|
|
14
16
|
class AsyncAgentRegistry {
|
|
15
17
|
agents = new Map();
|
|
16
18
|
listeners = new Set();
|
|
@@ -28,6 +30,10 @@ class AsyncAgentRegistry {
|
|
|
28
30
|
hasRunning = () => {
|
|
29
31
|
return this.snapshot.some((e) => e.status === "running");
|
|
30
32
|
};
|
|
33
|
+
/** Count of agents currently in the "running" state (cap enforcement). */
|
|
34
|
+
runningCount() {
|
|
35
|
+
return this.snapshot.filter((e) => e.status === "running").length;
|
|
36
|
+
}
|
|
31
37
|
notify() {
|
|
32
38
|
this.snapshot = [...this.agents.values()];
|
|
33
39
|
for (const cb of this.listeners) {
|
|
@@ -8,9 +8,45 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ToolDefinition } from "../../types.js";
|
|
10
10
|
import type { ToolContext } from "../context.js";
|
|
11
|
+
import type { AgentDefinitionRegistry } from "../../agent/agent-definition-registry.js";
|
|
12
|
+
import type { HookRegistry } from "../../hooks/registry.js";
|
|
13
|
+
export interface AgentTypeOverrides {
|
|
14
|
+
model?: string;
|
|
15
|
+
maxTurns?: number;
|
|
16
|
+
toolAllowlist?: string[];
|
|
17
|
+
appendSystemPrompt?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve an `agent_type` against the role registry into spawn overrides.
|
|
21
|
+
* Omitted type → empty overrides (ephemeral mode). Unknown type → throw, so
|
|
22
|
+
* the LLM gets a clear correction instead of silently running a generic agent.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveAgentTypeOverrides(agentType: string | undefined, registry: AgentDefinitionRegistry | undefined): AgentTypeOverrides;
|
|
25
|
+
type SubAgentLifecycle = "subagent_start" | "subagent_finish" | "subagent_error";
|
|
26
|
+
/**
|
|
27
|
+
* Emit a sub-agent lifecycle event via the existing `notification` hook,
|
|
28
|
+
* tagged with a `kind`. No-op when hooks are absent. Fire-and-forget: emit is
|
|
29
|
+
* async, we deliberately `void` it so bookkeeping never blocks on a handler
|
|
30
|
+
* (mirrors the background-completion notification below).
|
|
31
|
+
*/
|
|
32
|
+
export declare function emitSubAgentHook(hooks: HookRegistry | undefined, kind: SubAgentLifecycle, payload: {
|
|
33
|
+
agentId: string;
|
|
34
|
+
description: string;
|
|
35
|
+
text?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}): void;
|
|
38
|
+
/** Default per-sub-agent wall-clock timeout (5 minutes). */
|
|
39
|
+
export declare const DEFAULT_SUBAGENT_TIMEOUT_MS: number;
|
|
40
|
+
/**
|
|
41
|
+
* Run `work()` with a timeout. On expiry, calls `onTimeout` (to abort the
|
|
42
|
+
* child) and rejects with a timeout error. The child's own abort handling
|
|
43
|
+
* unwinds its resources.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runWithTimeout<T>(work: () => Promise<T>, timeoutMs: number, onTimeout: () => void): Promise<T>;
|
|
11
46
|
export declare const agentToolDef: ToolDefinition;
|
|
12
47
|
export declare function agentTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
13
48
|
export declare const agentStatusToolDef: ToolDefinition;
|
|
14
49
|
export declare function agentStatusTool(args: Record<string, unknown>): Promise<string>;
|
|
15
50
|
export declare const agentCancelToolDef: ToolDefinition;
|
|
16
51
|
export declare function agentCancelTool(args: Record<string, unknown>): Promise<string>;
|
|
52
|
+
export {};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Supports AbortSignal for cascading cancellation.
|
|
7
7
|
* Supports onStream for real-time output passthrough.
|
|
8
8
|
*/
|
|
9
|
-
import { asyncAgentRegistry } from "./agent-registry.js";
|
|
9
|
+
import { asyncAgentRegistry, MAX_BACKGROUND_AGENTS } from "./agent-registry.js";
|
|
10
10
|
import { createTranscriptTranslator } from "./agent-transcript-translator.js";
|
|
11
11
|
import { notificationQueue } from "./agent-notifications.js";
|
|
12
12
|
import { nanoid } from "nanoid";
|
|
@@ -32,6 +32,58 @@ function safeEmit(sink, event) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve an `agent_type` against the role registry into spawn overrides.
|
|
37
|
+
* Omitted type → empty overrides (ephemeral mode). Unknown type → throw, so
|
|
38
|
+
* the LLM gets a clear correction instead of silently running a generic agent.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveAgentTypeOverrides(agentType, registry) {
|
|
41
|
+
if (!agentType)
|
|
42
|
+
return {};
|
|
43
|
+
const def = registry?.get(agentType);
|
|
44
|
+
if (!def) {
|
|
45
|
+
const available = registry?.list().map((d) => d.name).join(", ") || "(none defined)";
|
|
46
|
+
throw new Error(`unknown agent_type '${agentType}'. Available: ${available}`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
model: def.model,
|
|
50
|
+
maxTurns: def.maxTurns,
|
|
51
|
+
toolAllowlist: def.tools,
|
|
52
|
+
appendSystemPrompt: def.systemPrompt,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Emit a sub-agent lifecycle event via the existing `notification` hook,
|
|
57
|
+
* tagged with a `kind`. No-op when hooks are absent. Fire-and-forget: emit is
|
|
58
|
+
* async, we deliberately `void` it so bookkeeping never blocks on a handler
|
|
59
|
+
* (mirrors the background-completion notification below).
|
|
60
|
+
*/
|
|
61
|
+
export function emitSubAgentHook(hooks, kind, payload) {
|
|
62
|
+
void hooks?.emit("notification", { kind, ...payload });
|
|
63
|
+
}
|
|
64
|
+
/** Default per-sub-agent wall-clock timeout (5 minutes). */
|
|
65
|
+
export const DEFAULT_SUBAGENT_TIMEOUT_MS = 5 * 60_000;
|
|
66
|
+
/**
|
|
67
|
+
* Run `work()` with a timeout. On expiry, calls `onTimeout` (to abort the
|
|
68
|
+
* child) and rejects with a timeout error. The child's own abort handling
|
|
69
|
+
* unwinds its resources.
|
|
70
|
+
*/
|
|
71
|
+
export async function runWithTimeout(work, timeoutMs, onTimeout) {
|
|
72
|
+
let timer;
|
|
73
|
+
const timeout = new Promise((_, reject) => {
|
|
74
|
+
timer = setTimeout(() => {
|
|
75
|
+
onTimeout();
|
|
76
|
+
reject(new Error(`Sub-agent timed out after ${timeoutMs}ms`));
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
return await Promise.race([work(), timeout]);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (timer)
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
35
87
|
export const agentToolDef = {
|
|
36
88
|
name: "Agent",
|
|
37
89
|
description: "Launch a sub-agent to handle a complex task autonomously. " +
|
|
@@ -53,6 +105,12 @@ export const agentToolDef = {
|
|
|
53
105
|
"Shown in the agent dock to identify what kind of work this sub-agent is doing. " +
|
|
54
106
|
"Keep it 1-2 words. Defaults to 'Agent' if omitted.",
|
|
55
107
|
},
|
|
108
|
+
agent_type: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "Optional reusable role defined in .code-shell/agents/*.md (e.g. 'researcher'). " +
|
|
111
|
+
"Loads that role's model, tool allowlist, turn cap, and system prompt. " +
|
|
112
|
+
"Omit to run an ad-hoc agent described entirely by 'prompt'.",
|
|
113
|
+
},
|
|
56
114
|
description: {
|
|
57
115
|
type: "string",
|
|
58
116
|
description: "A short (3-5 word) description of the task",
|
|
@@ -100,6 +158,7 @@ streamOverride) {
|
|
|
100
158
|
const { agentId, name, description } = opts;
|
|
101
159
|
const startEndSink = uiStream ?? spawner.parentStream;
|
|
102
160
|
safeEmit(startEndSink, { type: "agent_start", agentId, name, description });
|
|
161
|
+
emitSubAgentHook(opts.hooks, "subagent_start", { agentId, description });
|
|
103
162
|
// `resetPlanMode` / `restorePlanMode` operated on a module-level singleton
|
|
104
163
|
// that no longer exists. The child Engine is a fresh instance; plan-mode
|
|
105
164
|
// isolation between parent and child is enforced via separate Engine
|
|
@@ -107,6 +166,7 @@ streamOverride) {
|
|
|
107
166
|
const text = await spawner.spawn({ ...opts, streamOverride });
|
|
108
167
|
const finalText = text || `Agent completed but produced no text output.`;
|
|
109
168
|
safeEmit(startEndSink, { type: "agent_end", agentId, name, description, text: finalText });
|
|
169
|
+
emitSubAgentHook(opts.hooks, "subagent_finish", { agentId, description, text: finalText });
|
|
110
170
|
return finalText;
|
|
111
171
|
}
|
|
112
172
|
export async function agentTool(args, ctx) {
|
|
@@ -132,7 +192,15 @@ export async function agentTool(args, ctx) {
|
|
|
132
192
|
if (parentSignal?.aborted) {
|
|
133
193
|
return "Agent aborted before starting.";
|
|
134
194
|
}
|
|
135
|
-
const
|
|
195
|
+
const agentType = args.agent_type?.trim() || undefined;
|
|
196
|
+
let overrides;
|
|
197
|
+
try {
|
|
198
|
+
overrides = resolveAgentTypeOverrides(agentType, ctx?.agentDefinitions);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
return `Error: ${err.message}`;
|
|
202
|
+
}
|
|
203
|
+
const maxTurns = args.max_turns || overrides.maxTurns || 15;
|
|
136
204
|
const runInBackground = args.run_in_background === true;
|
|
137
205
|
const agentId = nanoid(8);
|
|
138
206
|
const parentStream = spawner.parentStream;
|
|
@@ -142,6 +210,10 @@ export async function agentTool(args, ctx) {
|
|
|
142
210
|
// (background agents survive the spawning turn). Cancellation goes
|
|
143
211
|
// through AgentCancel(agent_id).
|
|
144
212
|
if (runInBackground) {
|
|
213
|
+
if (asyncAgentRegistry.runningCount() >= MAX_BACKGROUND_AGENTS) {
|
|
214
|
+
return `Error: too many background agents running (limit ${MAX_BACKGROUND_AGENTS}). ` +
|
|
215
|
+
`Wait for some to finish or cancel one with AgentCancel(agent_id) before launching more.`;
|
|
216
|
+
}
|
|
145
217
|
const controller = new AbortController();
|
|
146
218
|
asyncAgentRegistry.register({
|
|
147
219
|
agentId,
|
|
@@ -171,6 +243,10 @@ export async function agentTool(args, ctx) {
|
|
|
171
243
|
description,
|
|
172
244
|
prompt,
|
|
173
245
|
maxTurns,
|
|
246
|
+
model: overrides.model,
|
|
247
|
+
toolAllowlist: overrides.toolAllowlist,
|
|
248
|
+
appendSystemPrompt: overrides.appendSystemPrompt,
|
|
249
|
+
hooks: ctx?.hooks,
|
|
174
250
|
signal: controller.signal,
|
|
175
251
|
}, parentStream, // uiStream: agent_start/end → main feed
|
|
176
252
|
transcriptSink)
|
|
@@ -262,22 +338,38 @@ export async function agentTool(args, ctx) {
|
|
|
262
338
|
].join("\n");
|
|
263
339
|
}
|
|
264
340
|
// ─── Synchronous path ──────────────────────────────────────────
|
|
341
|
+
// A timeout-capable controller: the timeout callback aborts the child, and
|
|
342
|
+
// a parent abort is forwarded to it too. Keeping the timeout OUTSIDE
|
|
343
|
+
// runSubAgent (and off the background path) avoids the background path's
|
|
344
|
+
// "abort means user-cancel → drop silently" semantics; here a timeout is a
|
|
345
|
+
// genuine error surfaced to the parent.
|
|
346
|
+
const syncController = new AbortController();
|
|
347
|
+
const onParentAbort = () => syncController.abort();
|
|
348
|
+
parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
265
349
|
try {
|
|
266
|
-
return await runSubAgent(spawner, {
|
|
350
|
+
return await runWithTimeout(() => runSubAgent(spawner, {
|
|
267
351
|
agentId,
|
|
268
352
|
name,
|
|
269
353
|
description,
|
|
270
354
|
prompt,
|
|
271
355
|
maxTurns,
|
|
272
|
-
|
|
273
|
-
|
|
356
|
+
model: overrides.model,
|
|
357
|
+
toolAllowlist: overrides.toolAllowlist,
|
|
358
|
+
appendSystemPrompt: overrides.appendSystemPrompt,
|
|
359
|
+
hooks: ctx?.hooks,
|
|
360
|
+
signal: syncController.signal,
|
|
361
|
+
}), DEFAULT_SUBAGENT_TIMEOUT_MS, () => syncController.abort());
|
|
274
362
|
}
|
|
275
363
|
catch (err) {
|
|
364
|
+
emitSubAgentHook(ctx?.hooks, "subagent_error", { agentId, description, error: err.message });
|
|
276
365
|
safeEmit(parentStream, { type: "agent_end", agentId, name, description, error: err.message });
|
|
277
366
|
if (parentSignal?.aborted)
|
|
278
367
|
return "Agent was aborted.";
|
|
279
368
|
return `Agent error: ${err.message}`;
|
|
280
369
|
}
|
|
370
|
+
finally {
|
|
371
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
372
|
+
}
|
|
281
373
|
}
|
|
282
374
|
// ─── AgentStatus / AgentCancel — companions to run_in_background ─
|
|
283
375
|
export const agentStatusToolDef = {
|
|
@@ -31,16 +31,34 @@ export async function skillTool(args, ctx) {
|
|
|
31
31
|
if (!skillName) {
|
|
32
32
|
return "Error: skill name is required.";
|
|
33
33
|
}
|
|
34
|
-
// Reject disabled skills before scanning so the user gets a
|
|
35
|
-
// message that distinguishes "disabled" from "not found" —
|
|
36
|
-
// the UI's toggle semantics. The scanner would also filter
|
|
37
|
-
// out, which alone would produce a misleading "not
|
|
34
|
+
// Reject disabled skills/plugins before scanning so the user gets a
|
|
35
|
+
// clear message that distinguishes "disabled" from "not found" —
|
|
36
|
+
// matches the UI's toggle semantics. The scanner would also filter
|
|
37
|
+
// these entries out, which alone would produce a misleading "not
|
|
38
|
+
// found" reply.
|
|
39
|
+
//
|
|
40
|
+
// Ordering: per-skill check fires BEFORE plugin-level check so the
|
|
41
|
+
// more-specific match wins. Both produce distinct error strings so
|
|
42
|
+
// ordering is observable but not load-bearing.
|
|
38
43
|
const disabledSkills = ctx?.disabledSkills;
|
|
44
|
+
const disabledPlugins = ctx?.disabledPlugins;
|
|
39
45
|
if (disabledSkills && disabledSkills.includes(skillName)) {
|
|
40
46
|
return `Skill "${skillName}" is disabled. Enable it in Customize or remove it from settings.disabledSkills.`;
|
|
41
47
|
}
|
|
48
|
+
if (disabledPlugins && disabledPlugins.length > 0) {
|
|
49
|
+
const colon = skillName.indexOf(":");
|
|
50
|
+
if (colon > 0) {
|
|
51
|
+
const pluginName = skillName.slice(0, colon);
|
|
52
|
+
if (disabledPlugins.includes(pluginName)) {
|
|
53
|
+
return `Skill "${skillName}" is in disabled plugin "${pluginName}". Enable the plugin in Customize or remove "${pluginName}" from settings.disabledPlugins.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
42
57
|
// A4: scan skills from the Engine's cwd, not the host process cwd.
|
|
43
|
-
const skills = scanSkills(ctx?.cwd ?? process.cwd(), {
|
|
58
|
+
const skills = scanSkills(ctx?.cwd ?? process.cwd(), {
|
|
59
|
+
disabledSkills,
|
|
60
|
+
disabledPlugins,
|
|
61
|
+
});
|
|
44
62
|
const found = skills.find((s) => s.name === skillName);
|
|
45
63
|
if (!found) {
|
|
46
64
|
return `Skill "${skillName}" not found. Run /skills to list available skills.`;
|
|
@@ -65,6 +65,23 @@ export interface SubAgentSpawnRequest {
|
|
|
65
65
|
* gets `agent_start` / `agent_end` markers via `spawner.parentStream`.
|
|
66
66
|
*/
|
|
67
67
|
streamOverride?: StreamCallback;
|
|
68
|
+
/**
|
|
69
|
+
* Optional ModelPool key for the child Engine's LLM (e.g. "flash").
|
|
70
|
+
* Undefined → child inherits the parent's model (current behavior).
|
|
71
|
+
*/
|
|
72
|
+
model?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Optional tool-name allowlist for the child. When set, the child's tool
|
|
75
|
+
* pool is restricted to these names (still minus the nested-agent tools).
|
|
76
|
+
* Undefined → child inherits the parent's full tool set (current behavior).
|
|
77
|
+
*/
|
|
78
|
+
toolAllowlist?: string[];
|
|
79
|
+
/**
|
|
80
|
+
* Optional per-call system prompt appended to the child Engine's prompt
|
|
81
|
+
* (the role definition's Markdown body). Undefined → child inherits only
|
|
82
|
+
* the parent's appendSystemPrompt (current behavior).
|
|
83
|
+
*/
|
|
84
|
+
appendSystemPrompt?: string;
|
|
68
85
|
}
|
|
69
86
|
export interface SubAgentSpawner {
|
|
70
87
|
/** Run a sub-agent synchronously and return its text output. */
|
|
@@ -97,6 +114,12 @@ export interface ToolContext {
|
|
|
97
114
|
askUser?: AskUserFn;
|
|
98
115
|
/** Sub-agent spawner (Agent tool). Undefined → Agent tool unavailable. */
|
|
99
116
|
subAgentSpawner?: SubAgentSpawner;
|
|
117
|
+
/**
|
|
118
|
+
* Reusable sub-agent role definitions (loaded from .code-shell/agents/*.md).
|
|
119
|
+
* The Agent tool reads this to resolve `agent_type`. Undefined → only the
|
|
120
|
+
* ephemeral (inline prompt) mode is available.
|
|
121
|
+
*/
|
|
122
|
+
agentDefinitions?: import("../agent/agent-definition-registry.js").AgentDefinitionRegistry;
|
|
100
123
|
/**
|
|
101
124
|
* True when this Engine is itself a sub-agent. Set from EngineConfig.
|
|
102
125
|
* The Agent tool refuses to spawn when this is true — runtime check
|
|
@@ -150,6 +173,14 @@ export interface ToolContext {
|
|
|
150
173
|
* Populated from `settings.disabledSkills` by Engine.run().
|
|
151
174
|
*/
|
|
152
175
|
disabledSkills?: string[];
|
|
176
|
+
/**
|
|
177
|
+
* Plugin names the user has totally disabled. Every skill whose
|
|
178
|
+
* namespaced name starts with `${pluginName}:` is hidden from the
|
|
179
|
+
* LLM and rejected at dispatch by the skill builtin tool with a
|
|
180
|
+
* distinct "disabled plugin" message. Populated from
|
|
181
|
+
* `settings.disabledPlugins` by Engine.run().
|
|
182
|
+
*/
|
|
183
|
+
disabledPlugins?: string[];
|
|
153
184
|
}
|
|
154
185
|
/**
|
|
155
186
|
* Per-Engine container that produces a fresh ToolContext on demand.
|
package/package.json
CHANGED