@falkenslab/agent-kit 0.18.0 → 0.20.0
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/assets/extension-launcher.mjs +24 -0
- package/dist/chat/chatController.d.ts +267 -0
- package/dist/chat/chatController.js +536 -0
- package/dist/chat/extensions.d.ts +18 -0
- package/dist/chat/extensions.js +44 -0
- package/dist/chat/history.d.ts +17 -0
- package/dist/chat/history.js +39 -0
- package/dist/{tui → chat}/runs.d.ts +11 -1
- package/dist/{tui → chat}/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +28 -50
- package/dist/core/extensions.d.ts +128 -0
- package/dist/core/extensions.js +87 -0
- package/dist/core/externalExtensions.d.ts +143 -0
- package/dist/core/externalExtensions.js +368 -0
- package/dist/core/hooks/planGate.d.ts +2 -0
- package/dist/core/hooks/planGate.js +1 -10
- package/dist/core/hooks/stepGate.d.ts +1 -1
- package/dist/core/hooks/stepGate.js +3 -3
- package/dist/core/liveExtensions.d.ts +51 -0
- package/dist/core/liveExtensions.js +70 -0
- package/dist/core/marketplaces.d.ts +118 -0
- package/dist/core/marketplaces.js +242 -0
- package/dist/core/messages/de.js +12 -32
- package/dist/core/messages/en.d.ts +15 -16
- package/dist/core/messages/en.js +12 -32
- package/dist/core/messages/es.js +12 -32
- package/dist/core/messages/fr.js +12 -32
- package/dist/core/messages/index.d.ts +6 -0
- package/dist/core/messages/index.js +12 -1
- package/dist/core/packaged.d.ts +10 -0
- package/dist/core/packaged.js +38 -0
- package/dist/core/pluginAgents.d.ts +20 -0
- package/dist/core/pluginAgents.js +76 -0
- package/dist/core/pluginDownloads.d.ts +14 -0
- package/dist/core/pluginDownloads.js +141 -0
- package/dist/core/runner.js +21 -9
- package/dist/core/session.d.ts +38 -6
- package/dist/core/session.js +200 -99
- package/dist/core/sessionFacts.d.ts +85 -0
- package/dist/core/sessionFacts.js +49 -0
- package/dist/core/toolLabels.d.ts +24 -6
- package/dist/core/toolLabels.js +24 -39
- package/dist/extensions/awareness/index.d.ts +22 -0
- package/dist/extensions/awareness/index.js +42 -0
- package/dist/extensions/awareness/labels.d.ts +3 -0
- package/dist/extensions/awareness/labels.js +12 -0
- package/dist/extensions/awareness/tools.d.ts +5 -0
- package/dist/extensions/awareness/tools.js +54 -0
- package/dist/extensions/knowledge/index.d.ts +22 -0
- package/dist/extensions/knowledge/index.js +38 -0
- package/dist/{core → extensions/knowledge}/knowledgeStore.d.ts +2 -1
- package/dist/extensions/knowledge/labels.d.ts +3 -0
- package/dist/extensions/knowledge/labels.js +119 -0
- package/dist/{core/knowledge.d.ts → extensions/knowledge/prompt.d.ts} +4 -4
- package/dist/{core/knowledge.js → extensions/knowledge/prompt.js} +10 -7
- package/dist/{core/tools/knowledgeTools.d.ts → extensions/knowledge/tools.d.ts} +1 -1
- package/dist/{core/tools/knowledgeTools.js → extensions/knowledge/tools.js} +2 -2
- package/dist/extensions/memory/index.d.ts +21 -0
- package/dist/extensions/memory/index.js +59 -0
- package/dist/extensions/memory/labels.d.ts +3 -0
- package/dist/extensions/memory/labels.js +58 -0
- package/dist/extensions/memory/memoryStore.d.ts +53 -0
- package/dist/extensions/memory/memoryStore.js +88 -0
- package/dist/extensions/memory/tools.d.ts +9 -0
- package/dist/extensions/memory/tools.js +82 -0
- package/dist/extensions/sources/index.d.ts +21 -0
- package/dist/extensions/sources/index.js +42 -0
- package/dist/extensions/sources/labels.d.ts +3 -0
- package/dist/extensions/sources/labels.js +82 -0
- package/dist/{core/tools/saveToSources.d.ts → extensions/sources/tools.d.ts} +1 -1
- package/dist/{core/tools/saveToSources.js → extensions/sources/tools.js} +5 -5
- package/dist/index.d.ts +19 -9
- package/dist/index.js +16 -7
- package/dist/tui/chatTui.d.ts +11 -20
- package/dist/tui/chatTui.js +75 -230
- package/dist/tui/extensionCommand.d.ts +30 -0
- package/dist/tui/extensionCommand.js +257 -0
- package/dist/tui/ink/runChatInk.d.ts +4 -3
- package/dist/tui/ink/runChatInk.js +101 -217
- package/extensions/awareness/.claude-plugin/plugin.json +10 -0
- package/{assets/agent-help → extensions/awareness/skills/help}/SKILL.md +4 -3
- package/{assets/knowledge-plugin → extensions/knowledge}/.claude-plugin/plugin.json +7 -1
- package/extensions/memory/.claude-plugin/plugin.json +10 -0
- package/extensions/memory/commands/forget.md +8 -0
- package/extensions/memory/commands/list.md +5 -0
- package/extensions/sources/.claude-plugin/plugin.json +10 -0
- package/package.json +3 -2
- package/dist/core/agentHelp.d.ts +0 -30
- package/dist/core/agentHelp.js +0 -55
- /package/dist/{core → extensions/knowledge}/fileKnowledgeStore.d.ts +0 -0
- /package/dist/{core → extensions/knowledge}/fileKnowledgeStore.js +0 -0
- /package/dist/{core → extensions/knowledge}/knowledgeStore.js +0 -0
- /package/dist/{core → extensions/sources}/extractText.d.ts +0 -0
- /package/dist/{core → extensions/sources}/extractText.js +0 -0
- /package/dist/{core → extensions/sources}/sources.d.ts +0 -0
- /package/dist/{core → extensions/sources}/sources.js +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/commands/ingest.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/commands/lint.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/commands/query.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-ingest/SKILL.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-lint/SKILL.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-query/SKILL.md +0 -0
package/dist/core/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
1
|
+
import { cp, readdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createTranscriptLogger } from "./hooks/transcriptLogger.js";
|
|
@@ -10,18 +10,20 @@ import { createFileScopeGate } from "./hooks/fileScopeGate.js";
|
|
|
10
10
|
import { createPlanGate } from "./hooks/planGate.js";
|
|
11
11
|
import { createHumanApprovalServer } from "./tools/humanApproval.js";
|
|
12
12
|
import { createManualLoginServer } from "./tools/manualLogin.js";
|
|
13
|
-
import { createSaveToSourcesServer, sourcesPromptSection } from "./tools/saveToSources.js";
|
|
14
13
|
import { createTimeServer } from "./tools/time.js";
|
|
15
14
|
import { TODO_TOOL } from "./todos.js";
|
|
16
15
|
import { createModeControl } from "./modeControl.js";
|
|
17
|
-
import {
|
|
18
|
-
import { createKnowledgeServer } from "./tools/knowledgeTools.js";
|
|
16
|
+
import { extensionsPromptSection, resolveExtensions, skillsMissingCapabilities } from "./extensions.js";
|
|
19
17
|
import { allowAnyMcpTool } from "./mcpPermissions.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
18
|
+
import { pluginAgents } from "./pluginAgents.js";
|
|
19
|
+
import { outsideArchive, unpackedClaudeExecutable } from "./packaged.js";
|
|
20
|
+
import { listInstalled, loadExternalExtensions } from "./externalExtensions.js";
|
|
22
21
|
import { replyLanguageInstruction } from "./language.js";
|
|
23
22
|
import { chooseLanguage } from "./messages/index.js";
|
|
24
23
|
import { createRunStore } from "./runs.js";
|
|
24
|
+
import { createSessionView, registerSessionView } from "./sessionFacts.js";
|
|
25
|
+
import { createExtensionSwitch } from "./liveExtensions.js";
|
|
26
|
+
import { agentKitVersion } from "./version.js";
|
|
25
27
|
/**
|
|
26
28
|
* Builds the `options` object passed to the Agent SDK's `query()` — everything about
|
|
27
29
|
* *how* the agent runs a session (system prompt, tools, MCP servers, hooks), independent
|
|
@@ -29,9 +31,10 @@ import { createRunStore } from "./runs.js";
|
|
|
29
31
|
* one-shot run or an `AsyncIterable` for a multi-turn chat).
|
|
30
32
|
*
|
|
31
33
|
* Everything actually *about the domain* (which system prompt, which MCP servers besides
|
|
32
|
-
* the generic human-in-the-loop
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* the generic human-in-the-loop ones below, which plugin roots, which subagents) is behind
|
|
35
|
+
* `spec` (see agentSpec.ts), and the kit's optional parts (the knowledge base, the sources
|
|
36
|
+
* folder) are extensions whose contributions this function puts together (extensions.ts,
|
|
37
|
+
* ADR-025): it names none of them.
|
|
35
38
|
*
|
|
36
39
|
* The per-mode behavioral differences (whether the approval/manual-intervention tools
|
|
37
40
|
* exist, whether the interactive step gate or the plan gate decides) all fall out of
|
|
@@ -57,60 +60,78 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
57
60
|
// domain's own opt-in signal (see its doc comment in agentSpec.ts), not a generic
|
|
58
61
|
// boolean this kit could infer on its own.
|
|
59
62
|
const manualInterventionTexts = mode !== "autonomous" ? spec.manualInterventionTexts : undefined;
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
// What the enabled extensions bring (ADR-025): their tools, prompt sections, plugins and
|
|
64
|
+
// folders, put together here.
|
|
65
|
+
// `session()` reads the view built below, once the options are: an extension's tools call it later.
|
|
66
|
+
const late = {};
|
|
67
|
+
const extensionContext = {
|
|
68
|
+
config,
|
|
69
|
+
spec,
|
|
70
|
+
runDir,
|
|
71
|
+
mode,
|
|
72
|
+
interactive: mode !== "autonomous",
|
|
73
|
+
// What the active ones provide, once they're resolved below.
|
|
74
|
+
capabilities: new Set(),
|
|
75
|
+
session: () => (late.view ? late.view.facts() : Promise.reject(new Error("The session isn't built yet: call session() from a tool, not from contribute()."))),
|
|
76
|
+
};
|
|
77
|
+
// The installed ones (#37), the project's over the agent's: those that can't load are off with
|
|
78
|
+
// why, and so is one named like an extension the spec enables.
|
|
79
|
+
const extensionDirs = config.extensionDirs ?? {};
|
|
80
|
+
const external = await loadExternalExtensions(extensionDirs);
|
|
81
|
+
const enabledNames = new Set((spec.extensions ?? []).map((extension) => extension.name));
|
|
82
|
+
const resolved = resolveExtensions([...(spec.extensions ?? []), ...external.extensions.filter((extension) => !enabledNames.has(extension.name))], extensionContext);
|
|
83
|
+
resolved.inactive.push(...external.off, ...external.extensions.filter((extension) => enabledNames.has(extension.name)).map((extension) => ({ name: extension.name, reason: "has the name of one this agent already has" })));
|
|
84
|
+
// Each installed one's plugin, copied into the run's folder: the running session empties and
|
|
85
|
+
// fills the copy to turn the extension off and on (liveExtensions.ts), never what's installed.
|
|
86
|
+
const installedDirs = new Map();
|
|
87
|
+
for (const { extension } of resolved.active.filter(({ extension }) => extension.external)) {
|
|
88
|
+
const copy = path.join(runDir, "extensions", extension.name);
|
|
89
|
+
await rm(copy, { recursive: true, force: true });
|
|
90
|
+
await cp(extension.plugin, copy, { recursive: true });
|
|
91
|
+
installedDirs.set(extension.name, extension.plugin);
|
|
92
|
+
extension.plugin = copy;
|
|
93
|
+
}
|
|
94
|
+
const externalPlugins = new Set(resolved.active.filter(({ extension }) => extension.external).map(({ extension }) => path.resolve(extension.plugin)));
|
|
95
|
+
extensionContext.capabilities = resolved.capabilities;
|
|
96
|
+
const added = await Promise.all(resolved.active.map(({ extension }) => extension.contribute(extensionContext)));
|
|
97
|
+
const extensionPlugins = resolved.active.map(({ extension }) => extension.plugin);
|
|
98
|
+
const fromExtensions = (pick) => added.flatMap((contribution) => pick(contribution) ?? []);
|
|
71
99
|
const extraDirs = config.extraWritableDirs ?? [];
|
|
72
100
|
const readableExtras = config.extraReadableDirs ?? [];
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
:
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
// The file tools the session needs: all five for the extra writable folders, the reading ones
|
|
102
|
+
// for the extra readable folders, and whatever the extensions ask for (the sources' reading
|
|
103
|
+
// ones); without any of them, none.
|
|
104
|
+
const wantedFileTools = new Set([
|
|
105
|
+
...(extraDirs.length ? FILE_TOOLS : []),
|
|
106
|
+
...(readableExtras.length ? ["Read", "Glob", "Grep"] : []),
|
|
107
|
+
...fromExtensions((contribution) => contribution.fileTools),
|
|
108
|
+
]);
|
|
109
|
+
const fileTools = FILE_TOOLS.filter((tool) => wantedFileTools.has(tool));
|
|
80
110
|
// Where Write/Edit may act and Grep may search (see hooks/fileScopeGate.ts): the
|
|
81
111
|
// project's own folders, never the whole cwd — which would include whatever else the
|
|
82
|
-
// project directory holds (the user's config...).
|
|
83
|
-
// deliberately NOT writable:
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
112
|
+
// project directory holds (the user's config...). An extension's folders (the sources) are
|
|
113
|
+
// searchable but deliberately NOT writable: the only way to add to them is its tools.
|
|
114
|
+
const readOnlyDirs = fromExtensions((contribution) => contribution.readOnlyDirs);
|
|
115
|
+
const toolOnlyDirs = fromExtensions((contribution) => contribution.toolOnlyDirs);
|
|
116
|
+
const writableDirs = extraDirs;
|
|
117
|
+
const searchableDirs = [...readOnlyDirs, ...extraDirs, ...readableExtras];
|
|
88
118
|
// Skills/commands/plugins are a separate concern from the file tools: an agent that
|
|
89
|
-
// wants a plugin-provided skill/command but has no
|
|
119
|
+
// wants a plugin-provided skill/command but has no folder of its own shouldn't have
|
|
90
120
|
// to invent one just to get `cwd`/`skills: "all"`/`plugins` wired up. Gated on either
|
|
91
121
|
// signal, not on `pluginRoots` alone, so an agent with file tools keeps getting the SDK's
|
|
92
122
|
// own project-level `.claude/skills`/`.claude/commands` discovery.
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
...(includeKnowledgeBase ? [knowledgePluginRoot()] : []),
|
|
106
|
-
...(helpPlugin ? [helpPlugin] : []),
|
|
107
|
-
];
|
|
108
|
-
const knowledgeStore = includeKnowledgeBase && config.knowledgeDir
|
|
109
|
-
? (spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(config.knowledgeDir, { pageTypes: spec.knowledgePageTypes }))
|
|
110
|
-
: undefined;
|
|
111
|
-
const includeSkillsAndPlugins = includeFileTools || pluginRoots.length > 0;
|
|
112
|
-
// "plugins": the skills of the plugins loaded, named as the SDK names them.
|
|
113
|
-
const offeredSkills = spec.skills === "plugins" ? await pluginSkills(pluginRoots) : spec.skills;
|
|
123
|
+
// Outside the archive when the agent runs packaged (Electron's app.asar): the CLI reads them (#42).
|
|
124
|
+
const pluginRoots = [...spec.pluginRoots(config), ...extensionPlugins].map(outsideArchive);
|
|
125
|
+
const includeSkillsAndPlugins = fileTools.length > 0 || pluginRoots.length > 0;
|
|
126
|
+
// "plugins": the skills of the plugins loaded, named as the SDK names them. A skill that
|
|
127
|
+
// `requires` a capability no active extension provides is left out; since the SDK's
|
|
128
|
+
// `skillOverrides` doesn't reach plugin skills (confirmed empirically), leaving one out takes
|
|
129
|
+
// a list: the plugins' skills and the project's, without it.
|
|
130
|
+
const missingCapabilities = await skillsMissingCapabilities(pluginRoots, resolved.capabilities);
|
|
131
|
+
const requested = spec.skills === "plugins" ? await pluginSkills(pluginRoots) : spec.skills;
|
|
132
|
+
const offeredSkills = missingCapabilities.length && (requested === undefined || requested === "all")
|
|
133
|
+
? [...(await pluginSkills(pluginRoots)), ...((spec.settingSources ?? ["project"]).includes("project") ? await projectSkills(config.projectDir) : [])]
|
|
134
|
+
: requested;
|
|
114
135
|
const skillTools = includeSkillsAndPlugins ? ["Skill"] : [];
|
|
115
136
|
// Whatever opt-in subagents `spec` wants for this config, or undefined if none apply.
|
|
116
137
|
// Subagents need the Agent tool (to delegate to them), and Bash when one of them uses it
|
|
@@ -121,39 +142,49 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
121
142
|
// anything not already present here — which is why the hook, not just omitting "Bash"
|
|
122
143
|
// from this list, is what actually confines it to subagent use).
|
|
123
144
|
const subagents = spec.buildSubagents(config);
|
|
145
|
+
// The plugins' subagents too (agents/*.md, the agent's and its extensions'), registered as the
|
|
146
|
+
// spec's are: allowed to be spawned, counted in the Agent and Bash decision, and given the
|
|
147
|
+
// reply line (pluginAgents.ts; ADR-025).
|
|
148
|
+
// An installed extension's get no Bash (ADR-025): one without `tools` gets none.
|
|
149
|
+
const fromPlugins = {
|
|
150
|
+
...(await pluginAgents(pluginRoots.filter((root) => !externalPlugins.has(path.resolve(root))))),
|
|
151
|
+
...Object.fromEntries(Object.entries(await pluginAgents([...externalPlugins])).map(([name, agent]) => [name, { ...agent, tools: (agent.tools ?? []).filter((tool) => tool !== "Bash") }])),
|
|
152
|
+
};
|
|
153
|
+
const allAgents = { ...fromPlugins, ...(subagents?.agents ?? {}) };
|
|
124
154
|
// Each subagent gets the reply line too: its prompt doesn't include the main one's.
|
|
125
|
-
const subagentDefinitions =
|
|
126
|
-
? Object.fromEntries(Object.entries(
|
|
155
|
+
const subagentDefinitions = Object.keys(allAgents).length
|
|
156
|
+
? Object.fromEntries(Object.entries(allAgents).map(([name, agent]) => [name, { ...agent, prompt: withReplyLine(agent.prompt) }]))
|
|
127
157
|
: undefined;
|
|
128
|
-
const includeSubagentTools =
|
|
158
|
+
const includeSubagentTools = subagentDefinitions !== undefined;
|
|
129
159
|
// Bash only when a subagent can use it: one that lists it, or one without `tools`, which
|
|
130
160
|
// inherits every session tool. Its definition costs about 1.9k input tokens on every call
|
|
131
161
|
// (measured), and the main agent can't use it anyway.
|
|
132
162
|
const subagentTools = includeSubagentTools
|
|
133
|
-
? ["Agent", ...(Object.values(
|
|
163
|
+
? ["Agent", ...(Object.values(allAgents).some((agent) => !agent.tools || agent.tools.includes("Bash")) ? ["Bash"] : [])]
|
|
134
164
|
: [];
|
|
135
165
|
// The exact set of subagent_type values the Agent tool may spawn in this session — see
|
|
136
166
|
// createSubagentTypeGate() below for why this can't just be "whatever's in `agents`
|
|
137
167
|
// below": the SDK's own built-in "general-purpose" type is spawnable regardless of that
|
|
138
168
|
// map, and inherits the full session tools (Bash included) rather than the narrow list
|
|
139
169
|
// each declared subagent actually declares.
|
|
140
|
-
const allowedSubagentTypes = subagents?.allowedSubagentTypes ?? [];
|
|
170
|
+
const allowedSubagentTypes = [...new Set([...(subagents?.allowedSubagentTypes ?? []), ...Object.keys(fromPlugins)])];
|
|
171
|
+
// The extensions' read-only tools, for the plan gate: a list the live switch changes (liveExtensions.ts).
|
|
172
|
+
const readOnlyTools = fromExtensions((contribution) => contribution.readOnlyTools);
|
|
173
|
+
// What no file tool may touch: the agent's denied paths and the SDK's own credentials.
|
|
174
|
+
const deniedPaths = [...(config.deniedPaths ?? []), path.join(claudeConfigDir(), ".credentials.json")];
|
|
141
175
|
const transcriptPath = path.join(runDir, "transcript.jsonl");
|
|
142
176
|
const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
|
|
143
|
-
// The person's preferences, by title, for the knowledge base's section (#33).
|
|
144
|
-
const preferences = knowledgeStore
|
|
145
|
-
? (await knowledgeStore.list()).filter((page) => page.type === "preference" && page.status === "active").map(({ id, title }) => ({ id, title }))
|
|
146
|
-
: [];
|
|
147
177
|
const promptSections = [
|
|
148
178
|
spec.buildSystemPrompt(config),
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
...(
|
|
153
|
-
? [knowledgePromptSection({ withSources: Boolean(config.sourcesDir), pageTypes: knowledgeStore?.types(), preferences })]
|
|
154
|
-
: []),
|
|
179
|
+
// What's on and what isn't, then the extensions' own sections.
|
|
180
|
+
...(resolved.active.length || resolved.inactive.length ? [extensionsPromptSection(resolved)] : []),
|
|
181
|
+
// The extensions' own sections, in their order (the sources' before the knowledge base's).
|
|
182
|
+
...fromExtensions((contribution) => (contribution.promptSection ? [contribution.promptSection] : [])),
|
|
155
183
|
];
|
|
184
|
+
// Packaged, the SDK would look for its CLI binary inside the archive, and hang (#42).
|
|
185
|
+
const claudeExecutable = unpackedClaudeExecutable();
|
|
156
186
|
const sdkOptions = {
|
|
187
|
+
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
157
188
|
systemPrompt: withReplyLine(promptSections.join("\n\n")),
|
|
158
189
|
// Not the SDK's default (every source): "user" hands the agent the runner's own Claude
|
|
159
190
|
// Code configuration, and its `language` setting outranked the agent's prompt.
|
|
@@ -173,6 +204,12 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
173
204
|
autoMemoryEnabled: false,
|
|
174
205
|
includeGitInstructions: false,
|
|
175
206
|
disableClaudeAiConnectors: true,
|
|
207
|
+
// An installed extension's plugin may carry hooks: none run (ADR-025). The kit's own,
|
|
208
|
+
// passed in `hooks`, still do (confirmed empirically).
|
|
209
|
+
...(externalPlugins.size ? { disableAllHooks: true } : {}),
|
|
210
|
+
// The denied paths again, as the SDK's own rules: a second layer under the file scope gate,
|
|
211
|
+
// which a tool the gate doesn't know of can't pass either (#49).
|
|
212
|
+
permissions: { deny: denyRules(deniedPaths) },
|
|
176
213
|
},
|
|
177
214
|
// No built-in tools except, if applicable, Read/Write/Edit/Glob/Grep scoped to the
|
|
178
215
|
// project's own folders (fileScopeGate below), and WebFetch/WebSearch
|
|
@@ -208,15 +245,15 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
208
245
|
// skills, on top of the plugin-provided built-ins from spec.pluginRoots()
|
|
209
246
|
// below. skipMcpDiscovery: true on those plugins is the caller's choice.
|
|
210
247
|
cwd: config.projectDir,
|
|
211
|
-
// Empty when there's no
|
|
248
|
+
// Empty when there's no folder of the agent's or its extensions' (the plugin-only case) — an
|
|
212
249
|
// empty array is a valid, no-op value for this SDK option, not omitted, since
|
|
213
250
|
// this whole block is already conditioned on includeSkillsAndPlugins.
|
|
214
251
|
additionalDirectories: searchableDirs,
|
|
215
252
|
// "skills" acts as a name whitelist, not an addition: "all" enables both the
|
|
216
253
|
// SDK's own official skills (pdf/docx), the project's own custom ones, and the
|
|
217
|
-
// plugin-provided built-ins below. A spec's own list gets the
|
|
218
|
-
//
|
|
219
|
-
skills: skillList(offeredSkills,
|
|
254
|
+
// plugin-provided built-ins below. A spec's own list gets the extensions' skills
|
|
255
|
+
// added, so it only names its own.
|
|
256
|
+
skills: skillList(offeredSkills, await pluginSkills(extensionPlugins), missingCapabilities),
|
|
220
257
|
plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
|
|
221
258
|
}
|
|
222
259
|
: {}),
|
|
@@ -227,36 +264,22 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
227
264
|
...spec.buildMcpServers(config, runDir),
|
|
228
265
|
...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts, { modeControl }) } : {}),
|
|
229
266
|
...(manualInterventionTexts ? { manualLogin: createManualLoginServer(runDir, manualInterventionTexts) } : {}),
|
|
230
|
-
// The
|
|
231
|
-
|
|
232
|
-
...(config.sourcesDir
|
|
233
|
-
? {
|
|
234
|
-
sourceFiles: createSaveToSourcesServer(runDir, config.sourcesDir, spec.saveToSourcesDescription, {
|
|
235
|
-
projectDir: config.projectDir,
|
|
236
|
-
interactive: mode !== "autonomous",
|
|
237
|
-
}),
|
|
238
|
-
}
|
|
239
|
-
: {}),
|
|
240
|
-
// The knowledge base's own tools, over its store (tools/knowledgeTools.ts, ADR-024).
|
|
241
|
-
...(knowledgeStore
|
|
242
|
-
? {
|
|
243
|
-
knowledge: createKnowledgeServer(knowledgeStore, { runDir, interactive: mode !== "autonomous" }),
|
|
244
|
-
}
|
|
245
|
-
: {}),
|
|
267
|
+
// The extensions' tools (the knowledge base's, the sources').
|
|
268
|
+
...Object.assign({}, ...added.map((contribution) => contribution.mcpServers ?? {})),
|
|
246
269
|
// The date and date arithmetic, in every session and mode: only reads (tools/time.ts).
|
|
247
270
|
time: createTimeServer(config.timeZone),
|
|
248
271
|
},
|
|
249
272
|
hooks: {
|
|
250
273
|
PreToolUse: [
|
|
251
274
|
{ hooks: [transcriptLogger.preToolUse] },
|
|
252
|
-
...(
|
|
275
|
+
...(fileTools.length || toolOnlyDirs.length
|
|
253
276
|
? [{ hooks: [createFileScopeGate({
|
|
254
277
|
projectDir: config.projectDir,
|
|
255
278
|
writableDirs,
|
|
256
279
|
searchableDirs,
|
|
257
280
|
readOnlyDirs,
|
|
258
281
|
// The SDK's own credentials, whatever the agent denies.
|
|
259
|
-
deniedPaths
|
|
282
|
+
deniedPaths,
|
|
260
283
|
// Read and Glob only in the agent's folders, plus what only the kit knows it
|
|
261
284
|
// needs: the run folder, the plugins, the project's .claude/ (when loaded) and
|
|
262
285
|
// the SDK's large tool results. Never the rest of the disk (~/.ssh, other
|
|
@@ -264,13 +287,13 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
264
287
|
readableDirs: searchableDirs,
|
|
265
288
|
alsoReadable: [runDir, ...pluginRoots, ...((spec.settingSources ?? ["project"]).includes("project") ? [path.join(config.projectDir, ".claude")] : [])],
|
|
266
289
|
toolResultsRoot: claudeProjectDir(config.projectDir),
|
|
267
|
-
|
|
290
|
+
toolOnlyDirs,
|
|
268
291
|
}),
|
|
269
292
|
] }]
|
|
270
293
|
: []),
|
|
271
294
|
// Registered wherever the approval tool is, and asking only while the current mode is
|
|
272
295
|
// "interactive": a guided session can switch to interactive and back (ModeControl).
|
|
273
|
-
...(includeApprovalTool ? [{ hooks: [createStepGate(runDir, () => modeControl.mode === "interactive")] }] : []),
|
|
296
|
+
...(includeApprovalTool ? [{ hooks: [createStepGate(runDir, () => modeControl.mode === "interactive", fromExtensions((contribution) => contribution.selfAskingTools))] }] : []),
|
|
274
297
|
// Likewise for "plan", which any non-autonomous session can switch into: the agent
|
|
275
298
|
// only reads and plans while it's on (hooks/planGate.ts).
|
|
276
299
|
...(includeApprovalTool
|
|
@@ -279,6 +302,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
279
302
|
hooks: [
|
|
280
303
|
createPlanGate({
|
|
281
304
|
projectDir: config.projectDir,
|
|
305
|
+
readOnlyTools,
|
|
282
306
|
...(spec.planMode?.isPlanFile ? { isPlanFile: (filePath) => spec.planMode.isPlanFile(filePath, config) } : {}),
|
|
283
307
|
...(spec.planMode?.isReadOnlyTool ? { isReadOnlyTool: spec.planMode.isReadOnlyTool } : {}),
|
|
284
308
|
}, () => modeControl.mode === "plan"),
|
|
@@ -297,7 +321,66 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
297
321
|
PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
|
|
298
322
|
},
|
|
299
323
|
};
|
|
300
|
-
|
|
324
|
+
// The extensions' own hooks (the memory hears the person's messages), after the kit's.
|
|
325
|
+
for (const contribution of added) {
|
|
326
|
+
for (const [event, matchers] of Object.entries(contribution.hooks ?? {})) {
|
|
327
|
+
sdkOptions.hooks[event] = [...(sdkOptions.hooks[event] ?? []), ...matchers];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const toolLabels = Object.assign({}, ...added.map((contribution) => contribution.toolLabels ?? {}));
|
|
331
|
+
const extensions = {
|
|
332
|
+
dirs: extensionDirs,
|
|
333
|
+
installed: await listInstalled(extensionDirs),
|
|
334
|
+
active: resolved.active.map(({ extension }) => extension.name),
|
|
335
|
+
about: Object.fromEntries(resolved.active.map(({ extension, manifest }) => [extension.name, { description: manifest.description, provides: manifest.provides }])),
|
|
336
|
+
inactive: resolved.inactive,
|
|
337
|
+
};
|
|
338
|
+
// What the session is, for the extensions that ask (sessionFacts.ts, #43).
|
|
339
|
+
const viewBase = {
|
|
340
|
+
...(spec.identity ? { identity: spec.identity } : {}),
|
|
341
|
+
kitVersion: agentKitVersion(),
|
|
342
|
+
language,
|
|
343
|
+
extensions: {
|
|
344
|
+
active: resolved.active.map(({ extension, manifest }, index) => ({
|
|
345
|
+
name: extension.name,
|
|
346
|
+
description: manifest.description,
|
|
347
|
+
provides: manifest.provides,
|
|
348
|
+
servers: Object.keys(added[index]?.mcpServers ?? {}),
|
|
349
|
+
help: added[index]?.helpLines ?? [],
|
|
350
|
+
})),
|
|
351
|
+
// Its own list: /extensions' status has another, and the live switch changes both.
|
|
352
|
+
inactive: [...resolved.inactive],
|
|
353
|
+
},
|
|
354
|
+
subagents: Object.entries(allAgents).map(([name, agent]) => ({ name, description: agent.description })),
|
|
355
|
+
skills: sdkOptions.skills === "all" ? await pluginSkills(pluginRoots) : [...(sdkOptions.skills ?? [])],
|
|
356
|
+
runDir,
|
|
357
|
+
};
|
|
358
|
+
late.view = createSessionView(viewBase, modeControl);
|
|
359
|
+
registerSessionView(sdkOptions, late.view);
|
|
360
|
+
// Turning an installed extension off and on while the session runs (#49).
|
|
361
|
+
const switchExtension = createExtensionSwitch(resolved.active.flatMap(({ extension, manifest }, index) => {
|
|
362
|
+
const installedDir = installedDirs.get(extension.name);
|
|
363
|
+
if (!installedDir)
|
|
364
|
+
return [];
|
|
365
|
+
const contribution = added[index];
|
|
366
|
+
return [
|
|
367
|
+
{
|
|
368
|
+
name: extension.name,
|
|
369
|
+
installedDir,
|
|
370
|
+
scopeDir: path.dirname(installedDir),
|
|
371
|
+
pluginCopy: extension.plugin,
|
|
372
|
+
servers: Object.keys(contribution?.mcpServers ?? {}),
|
|
373
|
+
readOnlyTools: contribution?.readOnlyTools ?? [],
|
|
374
|
+
toolLabels: contribution?.toolLabels ?? {},
|
|
375
|
+
agents: Object.keys(fromPlugins).filter((agent) => agent.startsWith(`${extension.name}:`)),
|
|
376
|
+
description: manifest.description,
|
|
377
|
+
provides: manifest.provides,
|
|
378
|
+
help: contribution?.helpLines ?? [],
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
}), { readOnlyTools, allowedSubagentTypes, toolLabels, status: extensions, facts: viewBase, controls: () => late.view?.controls() ?? {} });
|
|
382
|
+
const apis = Object.fromEntries(resolved.active.flatMap(({ extension }, index) => (added[index]?.api ? [[extension.name, added[index].api]] : [])));
|
|
383
|
+
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, switchExtension };
|
|
301
384
|
}
|
|
302
385
|
export { createModeControl, togglePlanMode } from "./modeControl.js";
|
|
303
386
|
/** The Claude Code configuration folder the SDK's CLI uses: `CLAUDE_CONFIG_DIR`, or `~/.claude`. */
|
|
@@ -312,8 +395,21 @@ function claudeConfigDir() {
|
|
|
312
395
|
export function claudeProjectDir(projectDir) {
|
|
313
396
|
return path.join(claudeConfigDir(), "projects", path.resolve(projectDir).replace(/[^a-zA-Z0-9]/g, "-"));
|
|
314
397
|
}
|
|
315
|
-
/**
|
|
316
|
-
|
|
398
|
+
/**
|
|
399
|
+
* The SDK's `permissions.deny` rules for paths no file tool may read or change, each one and
|
|
400
|
+
* whatever is inside it. An absolute path is written `//` and POSIX: `C:\Users\x` is
|
|
401
|
+
* `//c/Users/x` (confirmed empirically on Windows; on POSIX `/home/x` is `//home/x`). `Read`
|
|
402
|
+
* rules reach the tools that read, `Edit` rules those that write.
|
|
403
|
+
*/
|
|
404
|
+
export function denyRules(paths) {
|
|
405
|
+
return paths.flatMap((denied) => {
|
|
406
|
+
const posix = path.resolve(denied).replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`);
|
|
407
|
+
const rule = `/${posix}`;
|
|
408
|
+
return [`Read(${rule})`, `Read(${rule}/**)`, `Edit(${rule})`, `Edit(${rule}/**)`];
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/** The built-in file tools, in the order the session lists them. */
|
|
412
|
+
const FILE_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
|
|
317
413
|
/**
|
|
318
414
|
* The skills of the plugins in `roots`, as the SDK names them: `<plugin>:<folder>`, the
|
|
319
415
|
* plugin's name from its `.claude-plugin/plugin.json` and the skill's folder under `skills/`,
|
|
@@ -340,10 +436,15 @@ export async function pluginSkills(roots) {
|
|
|
340
436
|
}
|
|
341
437
|
return names;
|
|
342
438
|
}
|
|
343
|
-
function skillList(skills,
|
|
439
|
+
function skillList(skills, added, leftOut) {
|
|
344
440
|
if (skills === undefined || skills === "all")
|
|
345
441
|
return "all";
|
|
346
|
-
return [...new Set([...skills, ...
|
|
442
|
+
return [...new Set([...skills, ...added])].filter((skill) => !leftOut.includes(skill));
|
|
443
|
+
}
|
|
444
|
+
/** The project's own skills (`<projectDir>/.claude/skills/<name>/SKILL.md`), by name. */
|
|
445
|
+
async function projectSkills(projectDir) {
|
|
446
|
+
const folders = await readdir(path.join(projectDir, ".claude", "skills"), { withFileTypes: true }).catch(() => []);
|
|
447
|
+
return folders.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
347
448
|
}
|
|
348
449
|
/**
|
|
349
450
|
* User message queue backed by a single long-lived generator, for any multi-turn
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Options, SlashCommand } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import type { AgentIdentity, Mode } from "./agentSpec.js";
|
|
3
|
+
import type { Language } from "./language.js";
|
|
4
|
+
import type { ModeControl } from "./modeControl.js";
|
|
5
|
+
import type { ContextUsage } from "./runner.js";
|
|
6
|
+
/**
|
|
7
|
+
* What a session is, at the moment it's asked (`ExtensionContext.session()`, #43): the core's
|
|
8
|
+
* facts, read-only, never another extension's data. The mode follows Shift+Tab and plan mode;
|
|
9
|
+
* the tools, skills, commands and context come from the running session once it has started.
|
|
10
|
+
*/
|
|
11
|
+
export interface SessionFacts {
|
|
12
|
+
/** Who the agent is (`AgentSpec.identity`). */
|
|
13
|
+
identity?: AgentIdentity;
|
|
14
|
+
/** The agent-kit version it runs on. */
|
|
15
|
+
kitVersion: string;
|
|
16
|
+
/** The kit's language for this session. */
|
|
17
|
+
language: Language;
|
|
18
|
+
/** The mode now, and the ones the person can switch to (Shift+Tab). */
|
|
19
|
+
mode: Mode;
|
|
20
|
+
switchableModes: Mode[];
|
|
21
|
+
/** The extensions running (with their tools and what they say about themselves here), and the ones off, with why. */
|
|
22
|
+
extensions: {
|
|
23
|
+
active: {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
provides: string[];
|
|
27
|
+
tools: string[];
|
|
28
|
+
help: string[];
|
|
29
|
+
}[];
|
|
30
|
+
inactive: {
|
|
31
|
+
name: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
/** The subagents it can delegate to. */
|
|
36
|
+
subagents: {
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
}[];
|
|
40
|
+
/** Every tool of the session, as the model names it (empty until the session has started). */
|
|
41
|
+
tools: string[];
|
|
42
|
+
/** The skills offered (the running session's list once started; until then, the ones asked for). */
|
|
43
|
+
skills: string[];
|
|
44
|
+
/** The slash commands the person can type, with what each does (empty until the session has started). */
|
|
45
|
+
commands: {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
argumentHint: string;
|
|
49
|
+
}[];
|
|
50
|
+
/** How full the context window is, when the session can tell. */
|
|
51
|
+
context?: ContextUsage;
|
|
52
|
+
/** This run's folder. */
|
|
53
|
+
runDir: string;
|
|
54
|
+
}
|
|
55
|
+
/** What the running session tells the view: `runQuery()` attaches it on its own. */
|
|
56
|
+
export interface SessionLive {
|
|
57
|
+
tools?: string[];
|
|
58
|
+
skills?: string[];
|
|
59
|
+
supportedCommands?: () => Promise<SlashCommand[]>;
|
|
60
|
+
contextUsage?: () => Promise<ContextUsage | null>;
|
|
61
|
+
/** The running session's controls an installed extension is turned off and on with (liveExtensions.ts). */
|
|
62
|
+
toggleMcpServer?: (server: string, enabled: boolean) => Promise<void>;
|
|
63
|
+
reloadPlugins?: () => Promise<unknown>;
|
|
64
|
+
}
|
|
65
|
+
/** The facts known when the options are built: everything but the mode and what only the running session knows. */
|
|
66
|
+
export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" | "tools" | "commands" | "context" | "extensions"> & {
|
|
67
|
+
extensions: {
|
|
68
|
+
active: (Omit<SessionFacts["extensions"]["active"][number], "tools"> & {
|
|
69
|
+
servers: string[];
|
|
70
|
+
})[];
|
|
71
|
+
inactive: SessionFacts["extensions"]["inactive"];
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
export interface SessionView {
|
|
75
|
+
facts(): Promise<SessionFacts>;
|
|
76
|
+
attach(live: SessionLive): void;
|
|
77
|
+
/** What the running session has attached so far. */
|
|
78
|
+
controls(): SessionLive;
|
|
79
|
+
}
|
|
80
|
+
/** The view of a session: its static facts, the mode read from `modeControl` on every call, and what the running session attaches. */
|
|
81
|
+
export declare function createSessionView(base: StaticSessionFacts, modeControl: ModeControl): SessionView;
|
|
82
|
+
/** Ties a view to the options `buildSessionOptions()` returns. */
|
|
83
|
+
export declare function registerSessionView(options: Options, view: SessionView): void;
|
|
84
|
+
/** The view tied to these options (or a spread copy of them), if `buildSessionOptions()` built them. */
|
|
85
|
+
export declare function sessionViewOf(options: Options): SessionView | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A live call made from inside a tool, mid-turn: never let it hold the tool up.
|
|
2
|
+
const LIVE_TIMEOUT_MS = 3000;
|
|
3
|
+
const within = (promise, fallback) => Promise.race([promise.catch(() => fallback), new Promise((resolve) => setTimeout(() => resolve(fallback), LIVE_TIMEOUT_MS).unref())]);
|
|
4
|
+
/** The view of a session: its static facts, the mode read from `modeControl` on every call, and what the running session attaches. */
|
|
5
|
+
export function createSessionView(base, modeControl) {
|
|
6
|
+
let live = {};
|
|
7
|
+
return {
|
|
8
|
+
attach(more) {
|
|
9
|
+
live = { ...live, ...more };
|
|
10
|
+
},
|
|
11
|
+
controls: () => live,
|
|
12
|
+
async facts() {
|
|
13
|
+
const tools = live.tools ?? [];
|
|
14
|
+
const [commands, context] = await Promise.all([
|
|
15
|
+
live.supportedCommands ? within(live.supportedCommands(), []) : Promise.resolve([]),
|
|
16
|
+
live.contextUsage ? within(live.contextUsage(), null) : Promise.resolve(null),
|
|
17
|
+
]);
|
|
18
|
+
return {
|
|
19
|
+
...base,
|
|
20
|
+
mode: modeControl.mode,
|
|
21
|
+
switchableModes: [...modeControl.switchable],
|
|
22
|
+
extensions: {
|
|
23
|
+
active: base.extensions.active.map(({ servers, ...extension }) => ({
|
|
24
|
+
...extension,
|
|
25
|
+
// The running session's names when it has started; until then, each server's wildcard.
|
|
26
|
+
tools: tools.length ? tools.filter((tool) => servers.some((server) => tool.startsWith(`mcp__${server}__`))) : servers.map((server) => `mcp__${server}__*`),
|
|
27
|
+
})),
|
|
28
|
+
inactive: base.extensions.inactive,
|
|
29
|
+
},
|
|
30
|
+
tools,
|
|
31
|
+
skills: live.skills ?? base.skills,
|
|
32
|
+
commands: commands.map(({ name, description, argumentHint }) => ({ name, description, argumentHint })),
|
|
33
|
+
...(context ? { context } : {}),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// The view rides on the options under a symbol, for runQuery() to attach the running session to
|
|
39
|
+
// without the host passing anything: a spread (`{ ...options, more }`) keeps it, the SDK ignores
|
|
40
|
+
// it, and options built some other way just leave the live facts out.
|
|
41
|
+
const SESSION_VIEW = Symbol.for("agent-kit.sessionView");
|
|
42
|
+
/** Ties a view to the options `buildSessionOptions()` returns. */
|
|
43
|
+
export function registerSessionView(options, view) {
|
|
44
|
+
Object.defineProperty(options, SESSION_VIEW, { value: view, enumerable: true });
|
|
45
|
+
}
|
|
46
|
+
/** The view tied to these options (or a spread copy of them), if `buildSessionOptions()` built them. */
|
|
47
|
+
export function sessionViewOf(options) {
|
|
48
|
+
return options[SESSION_VIEW];
|
|
49
|
+
}
|