@falkenslab/agent-kit 0.16.1 → 0.17.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/README.md CHANGED
@@ -38,11 +38,11 @@ A foundation for building AI agents on top of the [Claude Agent SDK](https://www
38
38
 
39
39
  See [everything he can do](https://falkenslab.github.io/agent-kit/docs/next/examples/captain-whiskers#what-he-can-do), with screenshots.
40
40
 
41
- ### teacher-agent
41
+ ### miyagi
42
42
 
43
- [teacher-agent](https://github.com/falkenslab/teacher-agent) is a real assistant for Moodle teachers. It signs in with your account in a Chrome window and works as you would: it grades submissions, answers in the forum, creates or reviews content and sums up how the class is going. It asks for your permission before publishing anything students will see, and takes notes about the course to remember everything in the next session.
43
+ [miyagi](https://github.com/falkenslab/miyagi) is a real assistant for Moodle teachers. It signs in with your account in a Chrome window and works as you would: it grades submissions, answers in the forum, creates or reviews content and sums up how the class is going. It asks for your permission before publishing anything students will see, and takes notes about the course to remember everything in the next session.
44
44
 
45
- Oversight, memory, safety and the chat come from the kit; teacher-agent brings what it knows about Moodle and driving the browser.
45
+ Oversight, memory, safety and the chat come from the kit; miyagi brings what it knows about Moodle and driving the browser.
46
46
 
47
47
  ## Installation
48
48
 
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: agent-help
3
+ description: How to use this agent - its chat's modes, keys and slash commands, resuming an earlier conversation, approvals and questions, the command line, and the agent's own commands and configuration. Load it whenever the person asks how to do something with you, what a key or command does, or what you can do.
4
+ ---
5
+
6
+ # Helping the person use you
7
+
8
+ Answer from this page only: the chat's part below is the same for every agent built on agent-kit, and the "This session" and "This agent" sections at the end are about you. If something isn't covered here, say you don't know rather than inventing a key, command or option. Answer in the person's language, briefly, with the exact key or command; keys and commands are typed as written here, whatever the language.
9
+
10
+ ## Modes
11
+
12
+ How much you ask before acting. The current one is in the status bar, below the prompt.
13
+
14
+ - **interactive**: asks before every single action (each tool call), with the tool and its parameters.
15
+ - **guided**: asks only before something hard to undo or visible to others (publishing, sending); everything else runs on its own.
16
+ - **autonomous**: never asks; there's nobody to ask. A session that starts autonomous stays autonomous.
17
+ - **plan**: only reads and plans, changing nothing, until the plan is approved. The plan comes as a panel: **Run it** (leaves plan mode and carries it out), **Keep planning** (with a comment on what should change) or **Cancel**.
18
+
19
+ `Shift+Tab` switches to the next mode the session allows (guided, interactive, plan, in a loop). `/plan` turns plan mode on, and off again back to the mode it came from.
20
+
21
+ ## Approvals and questions
22
+
23
+ - **An approval** shows what is about to happen: **Yes** to go on, **No** to refuse that one action (the agent tries something else), **Stop** to stop it altogether.
24
+ - **A question** with options: `↑`/`↓` and `Enter` to choose, `Space` to mark when several can be chosen, or **Other** to type your own answer. A free question: type the answer and `Enter`, or `Esc` for no answer.
25
+ - **A request for a file**: drag the file onto the terminal (or type its path) and `Enter`, or just `Enter` if you don't have it.
26
+ - **A manual step** (signing in by hand, for an agent that drives a window): do it, then **Done, continue**.
27
+ - In interactive mode every action asks; switching to guided with `Shift+Tab` stops that.
28
+
29
+ ## Keys
30
+
31
+ - `Enter` sends the message; `\` then `Enter`, or `Ctrl+J`, starts a new line.
32
+ - `Tab` completes a `/command`, or takes the grey suggestion.
33
+ - `↑`/`↓` go through earlier messages; `Ctrl+R` searches them. `↑` on an empty prompt edits the last message still waiting to be sent.
34
+ - `Ctrl+W` deletes a word, `Ctrl+K` to the end of the line, `Ctrl+U` the whole line; `Ctrl+←`/`Ctrl+→` move by word.
35
+ - `@` mentions a file, with completion.
36
+ - `?` on an empty prompt shows the shortcuts.
37
+ - `Esc` interrupts the agent's turn; `Ctrl+C` interrupts it, or leaves at an empty prompt.
38
+ - `Ctrl+O` unfolds (and folds again) the tool calls; the task list of a long job stays on screen while it runs.
39
+ - `PgUp`/`PgDn` or the mouse wheel scroll; `Ctrl+End` goes back to the bottom. In full screen, drag to select and right-click to copy.
40
+
41
+ Messages typed while the agent works are queued and sent when it finishes.
42
+
43
+ ## Commands
44
+
45
+ - `/exit` or `/quit`: leave the chat (an agent can name its own exit commands instead).
46
+ - `/copy`: copy the last reply to the clipboard.
47
+ - `/resume`: pick an earlier conversation to go on with (`↑`/`↓`, `Enter`, `Esc` to cancel), where the agent keeps its conversations.
48
+ - `/plan`: plan mode on or off (see Modes).
49
+
50
+ Typing `/` and `Tab` lists every command, the agent's own included (named `/plugin:command`). An unknown `/command` isn't sent.
51
+
52
+ ## Command line
53
+
54
+ - `--continue`: start with the latest conversation instead of a new one.
55
+ - `--language=<code>`: the language of the chat and of the replies: `en`, `es`, `fr` or `de`. Without it, the agent's configured one, or the system's.
56
+
57
+ Some agents use a simpler line-by-line chat that has only part of this (no full screen, no `/copy`).
@@ -0,0 +1,30 @@
1
+ import type { AgentIdentity, Mode } from "./agentSpec.js";
2
+ /**
3
+ * The agent's self-knowledge: who it is (a section of the system prompt) and how it is used
4
+ * (the `agent-help` skill). The chat's part of the skill is the kit's, the same for every
5
+ * agent and kept next to the chat's code (`assets/agent-help/SKILL.md`, checked against the
6
+ * chat's commands and keys by a test); the agent's part is its own guide (`AgentSpec.helpGuide`).
7
+ */
8
+ /** The plugin the skill is loaded from, as the SDK names its skills ("agent-kit:agent-help"). */
9
+ export declare const AGENT_HELP_SKILL = "agent-kit:agent-help";
10
+ /** Absolute path of the kit's part of the skill, next to `dist/` (or `src/` under tsx). */
11
+ export declare function agentHelpSkillPath(): string;
12
+ /** The "Who you are" section appended to the system prompt: name, version, what it is, the kit's version. */
13
+ export declare function identityPromptSection(identity: AgentIdentity): string;
14
+ /** What the generated skill says about this session. */
15
+ export interface AgentHelpSession {
16
+ mode: Mode;
17
+ /** The modes Shift+Tab goes through (`ModeControl.switchable`). */
18
+ switchable: readonly Mode[];
19
+ /** The built-in knowledge base is on: its slash commands exist. */
20
+ knowledgeBase: boolean;
21
+ /** The sources folder, as shown to the model (e.g. "sources/"). */
22
+ sourcesFolder?: string;
23
+ }
24
+ /**
25
+ * Writes the `agent-help` plugin into `dir` (a folder of this run's), with the kit's part of
26
+ * the skill, this session's facts and the agent's own guide, and returns its root. Generated
27
+ * rather than shipped as it is: the guide goes inside the skill, so the agent needs no file
28
+ * tools to read it, and nothing about it reaches the context until the skill is loaded.
29
+ */
30
+ export declare function writeAgentHelpPlugin(dir: string, session: AgentHelpSession, helpGuide?: string): Promise<string>;
@@ -0,0 +1,55 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { agentKitVersion } from "./version.js";
5
+ /**
6
+ * The agent's self-knowledge: who it is (a section of the system prompt) and how it is used
7
+ * (the `agent-help` skill). The chat's part of the skill is the kit's, the same for every
8
+ * agent and kept next to the chat's code (`assets/agent-help/SKILL.md`, checked against the
9
+ * chat's commands and keys by a test); the agent's part is its own guide (`AgentSpec.helpGuide`).
10
+ */
11
+ /** The plugin the skill is loaded from, as the SDK names its skills ("agent-kit:agent-help"). */
12
+ export const AGENT_HELP_SKILL = "agent-kit:agent-help";
13
+ /** Absolute path of the kit's part of the skill, next to `dist/` (or `src/` under tsx). */
14
+ export function agentHelpSkillPath() {
15
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "agent-help", "SKILL.md");
16
+ }
17
+ /** The "Who you are" section appended to the system prompt: name, version, what it is, the kit's version. */
18
+ export function identityPromptSection(identity) {
19
+ const who = `${identity.name}${identity.version ? ` ${identity.version}` : ""}${identity.description ? `: ${identity.description}` : ""}`;
20
+ return `## Who you are
21
+ You are ${who}. You are built on agent-kit ${agentKitVersion()}, which provides your chat (its modes, keys and slash commands) and your built-in tools.
22
+ When the person asks how to use you (your modes, keys or commands, resuming an earlier conversation, your configuration, what you can do), load the \`agent-help\` skill and answer from it; don't invent options it doesn't list.`;
23
+ }
24
+ /** The skill's "This session" section. */
25
+ function sessionSection(session) {
26
+ const lines = [
27
+ `- This session started in **${session.mode}** mode.`,
28
+ session.switchable.length > 1
29
+ ? `- \`Shift+Tab\` goes through: ${session.switchable.join(", ")}.`
30
+ : "- The mode can't be switched during this session.",
31
+ ];
32
+ if (session.knowledgeBase)
33
+ lines.push("- You keep a knowledge base (your memory across sessions). Its commands: `/knowledge:ingest` (learn the new or changed files of the sources folder), `/knowledge:query <question>` (answer from it), `/knowledge:lint` (check it and fix what's mechanical).");
34
+ if (session.sourcesFolder)
35
+ lines.push(`- Originals go in \`${session.sourcesFolder}\`: the person drops files there (or you ask for one, or download it), and you never change them.`);
36
+ return `## This session\n\n${lines.join("\n")}`;
37
+ }
38
+ /**
39
+ * Writes the `agent-help` plugin into `dir` (a folder of this run's), with the kit's part of
40
+ * the skill, this session's facts and the agent's own guide, and returns its root. Generated
41
+ * rather than shipped as it is: the guide goes inside the skill, so the agent needs no file
42
+ * tools to read it, and nothing about it reaches the context until the skill is loaded.
43
+ */
44
+ export async function writeAgentHelpPlugin(dir, session, helpGuide) {
45
+ const kitPart = (await readFile(agentHelpSkillPath(), "utf8")).trimEnd();
46
+ const guide = helpGuide ? (await readFile(helpGuide, "utf8")).trim() : "";
47
+ const agentPart = guide
48
+ ? `## This agent\n\nThe agent's own guide, for everything about its domain (its commands, configuration, folders):\n\n${guide}`
49
+ : "## This agent\n\nThis agent has no guide of its own: for its domain, say only what your instructions tell you, and that the rest isn't documented.";
50
+ await mkdir(path.join(dir, ".claude-plugin"), { recursive: true });
51
+ await mkdir(path.join(dir, "skills", "agent-help"), { recursive: true });
52
+ await writeFile(path.join(dir, ".claude-plugin", "plugin.json"), `${JSON.stringify({ name: "agent-kit", description: "How to use this agent: the kit's chat and the agent's own guide." }, null, 2)}\n`);
53
+ await writeFile(path.join(dir, "skills", "agent-help", "SKILL.md"), `${kitPart}\n\n${sessionSection(session)}\n\n${agentPart}\n`);
54
+ return dir;
55
+ }
@@ -70,6 +70,18 @@ export interface BaseSessionConfig {
70
70
  export interface AgentSpec<TConfig extends BaseSessionConfig> {
71
71
  /** The system prompt for this run — everything role/mode/domain-specific lives behind this call. */
72
72
  buildSystemPrompt(config: TConfig): string;
73
+ /**
74
+ * Who the agent is. With it, the kit tells the model its name, version and what it is, plus
75
+ * agent-kit's version, and offers the `agent-help` skill, which answers how to use the
76
+ * agent: the kit's chat (modes, keys, slash commands, resuming) and `helpGuide`. Without
77
+ * it, neither.
78
+ */
79
+ identity?: AgentIdentity;
80
+ /**
81
+ * Absolute path of a markdown guide to the agent's own domain (its commands, configuration,
82
+ * folders), which the `agent-help` skill includes. Only with `identity`.
83
+ */
84
+ helpGuide?: string;
73
85
  /**
74
86
  * MCP servers this agent always registers (e.g. Playwright for a browser-driving
75
87
  * agent), on top of the generic ones `buildSessionOptions()` wires up itself when
@@ -192,6 +204,15 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
192
204
  */
193
205
  planMode?: PlanModeSpec<TConfig>;
194
206
  }
207
+ /** Who an agent is, as told to its model (`AgentSpec.identity`). */
208
+ export interface AgentIdentity {
209
+ /** The agent's name, e.g. "padawan". */
210
+ name: string;
211
+ /** Its version, e.g. its package's. */
212
+ version?: string;
213
+ /** What it is, in one line, e.g. "an agent that takes a Moodle course as a student". */
214
+ description?: string;
215
+ }
195
216
  /** The domain's part of "plan" mode: which files hold the plan and which of its own tools only read. */
196
217
  export interface PlanModeSpec<TConfig extends BaseSessionConfig = BaseSessionConfig> {
197
218
  /**
@@ -5,7 +5,7 @@ export const de = {
5
5
  escToInterrupt: "Esc zum Unterbrechen",
6
6
  workedFor: (seconds) => `${seconds}s gearbeitet`,
7
7
  turns: (count) => `${count} ${plural(count, "Runde", "Runden")}`,
8
- tokens: (input, output) => `${input} ein / ${output} aus`,
8
+ tokens: (input, output, cached) => `${input} ein${cached ? ` (+${cached} Cache)` : ""} / ${output} aus`,
9
9
  context: (percent) => `Kontext ${percent}%`,
10
10
  mode: (mode) => MODES[mode],
11
11
  switchModeKey: "⇧Tab",
@@ -11,7 +11,8 @@ export interface Messages {
11
11
  escToInterrupt: string;
12
12
  workedFor(seconds: number): string;
13
13
  turns(count: number): string;
14
- tokens(input: string, output: string): string;
14
+ /** The session's input tokens: new ones, and those read again from the cache (if any). */
15
+ tokens(input: string, output: string, cached?: string): string;
15
16
  context(percent: number): string;
16
17
  mode(mode: Mode): string;
17
18
  switchModeKey: string;
@@ -4,7 +4,7 @@ export const en = {
4
4
  escToInterrupt: "esc to interrupt",
5
5
  workedFor: (seconds) => `Worked for ${seconds}s`,
6
6
  turns: (count) => `${count} ${plural(count, "turn", "turns")}`,
7
- tokens: (input, output) => `${input} in / ${output} out`,
7
+ tokens: (input, output, cached) => `${input} in${cached ? ` (+${cached} cached)` : ""} / ${output} out`,
8
8
  context: (percent) => `context ${percent}%`,
9
9
  mode: (mode) => mode,
10
10
  switchModeKey: "shift+tab",
@@ -5,7 +5,7 @@ export const es = {
5
5
  escToInterrupt: "esc para interrumpir",
6
6
  workedFor: (seconds) => `Trabajó ${seconds}s`,
7
7
  turns: (count) => `${count} ${plural(count, "turno", "turnos")}`,
8
- tokens: (input, output) => `${input} ent. / ${output} sal.`,
8
+ tokens: (input, output, cached) => `${input} ent.${cached ? ` (+${cached} caché)` : ""} / ${output} sal.`,
9
9
  context: (percent) => `contexto ${percent}%`,
10
10
  mode: (mode) => MODES[mode],
11
11
  switchModeKey: "⇧tab",
@@ -5,7 +5,7 @@ export const fr = {
5
5
  escToInterrupt: "échap pour interrompre",
6
6
  workedFor: (seconds) => `A travaillé ${seconds}s`,
7
7
  turns: (count) => `${count} ${plural(count, "tour", "tours")}`,
8
- tokens: (input, output) => `${input} entr. / ${output} sort.`,
8
+ tokens: (input, output, cached) => `${input} entr.${cached ? ` (+${cached} cache)` : ""} / ${output} sort.`,
9
9
  context: (percent) => `contexte ${percent}%`,
10
10
  mode: (mode) => MODES[mode],
11
11
  switchModeKey: "⇧tab",
@@ -97,6 +97,11 @@ export type AgentEvent =
97
97
  export interface SessionUsage {
98
98
  /** Input tokens, cache reads and writes included, across every model the session used. */
99
99
  inputTokens: number;
100
+ /**
101
+ * The part of `inputTokens` read from the cache: the context sent again on every call. It
102
+ * grows with each call while the context barely does, so the chat shows it apart.
103
+ */
104
+ cacheReadTokens?: number;
100
105
  outputTokens: number;
101
106
  /** An estimate, not a billing statement. */
102
107
  costUsd: number;
@@ -90,6 +90,7 @@ export function runQuery(prompt, options) {
90
90
  const models = Object.values(message.modelUsage ?? {});
91
91
  const usage = {
92
92
  inputTokens: models.reduce((sum, m) => sum + m.inputTokens + m.cacheReadInputTokens + m.cacheCreationInputTokens, 0),
93
+ cacheReadTokens: models.reduce((sum, m) => sum + m.cacheReadInputTokens, 0),
93
94
  outputTokens: models.reduce((sum, m) => sum + m.outputTokens, 0),
94
95
  costUsd: message.total_cost_usd,
95
96
  };
@@ -16,6 +16,7 @@ import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
16
16
  import { createKnowledgeServer } from "./tools/knowledgeTools.js";
17
17
  import { allowAnyMcpTool } from "./mcpPermissions.js";
18
18
  import { knowledgePluginRoot, knowledgePromptSection } from "./knowledge.js";
19
+ import { AGENT_HELP_SKILL, identityPromptSection, writeAgentHelpPlugin } from "./agentHelp.js";
19
20
  import { replyLanguageInstruction } from "./language.js";
20
21
  import { chooseLanguage } from "./messages/index.js";
21
22
  import { createRunStore } from "./runs.js";
@@ -86,15 +87,29 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
86
87
  // to invent one just to get `cwd`/`skills: "all"`/`plugins` wired up. Gated on either
87
88
  // signal, not on `pluginRoots` alone, so an agent with file tools keeps getting the SDK's
88
89
  // own project-level `.claude/skills`/`.claude/commands` discovery.
89
- const pluginRoots = [...spec.pluginRoots(config), ...(includeKnowledgeBase ? [knowledgePluginRoot(knowledgeViaStore ? "tools" : "files")] : [])];
90
+ // With an identity, the agent-help skill (agentHelp.ts): written into the run's folder, since
91
+ // it carries this session's facts and the agent's own guide.
92
+ const helpPlugin = spec.identity
93
+ ? await writeAgentHelpPlugin(path.join(runDir, "agent-help"), {
94
+ mode,
95
+ switchable: modeControl.switchable,
96
+ knowledgeBase: includeKnowledgeBase,
97
+ ...(config.sourcesDir ? { sourcesFolder: `${path.relative(config.projectDir, config.sourcesDir).split(path.sep).join("/")}/` } : {}),
98
+ }, spec.helpGuide)
99
+ : undefined;
100
+ const pluginRoots = [
101
+ ...spec.pluginRoots(config),
102
+ ...(includeKnowledgeBase ? [knowledgePluginRoot(knowledgeViaStore ? "tools" : "files")] : []),
103
+ ...(helpPlugin ? [helpPlugin] : []),
104
+ ];
90
105
  const knowledgeStore = knowledgeViaStore && config.knowledgeDir
91
106
  ? (spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(config.knowledgeDir, { pageTypes: spec.knowledgePageTypes, sourcesDir: config.sourcesDir }))
92
107
  : undefined;
93
108
  const includeSkillsAndPlugins = includeFileTools || pluginRoots.length > 0;
94
109
  const skillTools = includeSkillsAndPlugins ? ["Skill"] : [];
95
110
  // Whatever opt-in subagents `spec` wants for this config, or undefined if none apply.
96
- // Either opt-in feature needs the Agent tool (to delegate to a subagent) and Bash (used
97
- // only by the subagent itself — see createSubagentBashGate() below, which denies Bash
111
+ // Subagents need the Agent tool (to delegate to them), and Bash when one of them uses it
112
+ // (used only by the subagent itself — see createSubagentBashGate() below, which denies Bash
98
113
  // to the main agent regardless of which one put it in `tools`) — Bash has to be present
99
114
  // in this session's own tools for any subagent to be allowed to use it at all
100
115
  // (confirmed empirically the SDK refuses to spawn a subagent whose own tools list names
@@ -106,6 +121,12 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
106
121
  ? Object.fromEntries(Object.entries(subagents.agents).map(([name, agent]) => [name, { ...agent, prompt: withReplyLine(agent.prompt) }]))
107
122
  : undefined;
108
123
  const includeSubagentTools = subagents !== undefined;
124
+ // Bash only when a subagent can use it: one that lists it, or one without `tools`, which
125
+ // inherits every session tool. Its definition costs about 1.9k input tokens on every call
126
+ // (measured), and the main agent can't use it anyway.
127
+ const subagentTools = includeSubagentTools
128
+ ? ["Agent", ...(Object.values(subagents.agents).some((agent) => !agent.tools || agent.tools.includes("Bash")) ? ["Bash"] : [])]
129
+ : [];
109
130
  // The exact set of subagent_type values the Agent tool may spawn in this session — see
110
131
  // createSubagentTypeGate() below for why this can't just be "whatever's in `agents`
111
132
  // below": the SDK's own built-in "general-purpose" type is spawnable regardless of that
@@ -114,10 +135,15 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
114
135
  const allowedSubagentTypes = subagents?.allowedSubagentTypes ?? [];
115
136
  const transcriptPath = path.join(runDir, "transcript.jsonl");
116
137
  const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
138
+ const promptSections = [
139
+ spec.buildSystemPrompt(config),
140
+ ...(spec.identity ? [identityPromptSection(spec.identity)] : []),
141
+ ...(includeKnowledgeBase && config.knowledgeDir
142
+ ? [knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir, { tools: knowledgeViaStore, pageTypes: knowledgeStore?.types() })]
143
+ : []),
144
+ ];
117
145
  const sdkOptions = {
118
- systemPrompt: withReplyLine(includeKnowledgeBase && config.knowledgeDir
119
- ? `${spec.buildSystemPrompt(config)}\n\n${knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir, { tools: knowledgeViaStore, pageTypes: knowledgeStore?.types() })}`
120
- : spec.buildSystemPrompt(config)),
146
+ systemPrompt: withReplyLine(promptSections.join("\n\n")),
121
147
  // Not the SDK's default (every source): "user" hands the agent the runner's own Claude
122
148
  // Code configuration, and its `language` setting outranked the agent's prompt.
123
149
  settingSources: spec.settingSources ?? ["project"],
@@ -139,8 +165,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
139
165
  // so the model ends up looking for SKILL.md files by hand (confirmed empirically).
140
166
  // TodoWrite: the SDK's task list for long jobs, in every session (the chats show the list
141
167
  // instead of the calls; see todos.ts). Accepted in an explicit list (confirmed empirically).
142
- tools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
143
- allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
168
+ tools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...subagentTools],
169
+ allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...subagentTools],
144
170
  // Every mcp__* tool (whatever spec.buildMcpServers() registers, approvals/manualLogin
145
171
  // when enabled below, sourceFiles, and any server a project's own .mcp.json
146
172
  // declares) is approved generically here rather than listed one by one — see
@@ -170,8 +196,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
170
196
  // "skills" acts as a name whitelist, not an addition: "all" enables both the
171
197
  // SDK's own official skills (pdf/docx), the project's own custom ones, and the
172
198
  // plugin-provided built-ins below. A spec's own list gets the knowledge base's
173
- // skills added, so it only names its own.
174
- skills: skillList(spec.skills, includeKnowledgeBase ? (knowledgeViaStore ? "tools" : "files") : null),
199
+ // skills and agent-help added, so it only names its own.
200
+ skills: skillList(spec.skills, includeKnowledgeBase ? (knowledgeViaStore ? "tools" : "files") : null, Boolean(helpPlugin)),
175
201
  plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
176
202
  }
177
203
  : {}),
@@ -259,10 +285,10 @@ const KNOWLEDGE_SKILLS = {
259
285
  tools: ["knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
260
286
  files: ["knowledge-pages", "knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
261
287
  };
262
- function skillList(skills, knowledgeVariant) {
288
+ function skillList(skills, knowledgeVariant, agentHelp) {
263
289
  if (skills === undefined || skills === "all")
264
290
  return "all";
265
- return knowledgeVariant ? [...new Set([...skills, ...KNOWLEDGE_SKILLS[knowledgeVariant]])] : skills;
291
+ return [...new Set([...skills, ...(knowledgeVariant ? KNOWLEDGE_SKILLS[knowledgeVariant] : []), ...(agentHelp ? [AGENT_HELP_SKILL] : [])])];
266
292
  }
267
293
  /**
268
294
  * User message queue backed by a single long-lived generator, for any multi-turn
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { createPromptLoader } from "./core/promptTemplate.js";
6
6
  export { resolveClaudeAuth, type ClaudeAuthConfig } from "./core/claudeAuth.js";
7
7
  export { ensureClaudeAuth } from "./tui/claudeAuth.js";
8
8
  export { createFriendlyToolLabel, truncate, truncatePath, type ToolDescriber, } from "./core/toolLabels.js";
9
- export type { Mode, BaseSessionConfig, AgentSpec, PlanModeSpec } from "./core/agentSpec.js";
9
+ export type { Mode, BaseSessionConfig, AgentSpec, AgentIdentity, PlanModeSpec } from "./core/agentSpec.js";
10
10
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode, type ModeControl } from "./core/session.js";
11
11
  export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
12
12
  export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
@@ -19,6 +19,7 @@ export declare function previewLines(lines: readonly string[], terminalRows: num
19
19
  export declare function statusText(mode: Mode | undefined, turns: number, usage: SessionUsage | null, extra?: {
20
20
  contextPercent?: number | null;
21
21
  modeSwitchable?: boolean;
22
+ width?: number;
22
23
  }): string;
23
24
  export interface SessionViewProps {
24
25
  model: SessionModel;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import wrapAnsi from "wrap-ansi";
3
+ import stringWidth from "string-width";
3
4
  import { useEffect, useReducer, useRef, useState, useSyncExternalStore } from "react";
4
5
  import { Box, measureElement, Static, Text, useInput, useStdout } from "ink";
5
6
  import { MultiSelect, Select, TextInput } from "@inkjs/ui";
@@ -86,13 +87,24 @@ function formatTokens(count) {
86
87
  return count >= 1000 ? `${(count / 1000).toFixed(1)}k` : String(count);
87
88
  }
88
89
  export function statusText(mode, turns, usage, extra = {}) {
89
- const parts = [
90
+ const text = (withCached) => statusParts(mode, turns, usage, extra, withCached).join(" · ");
91
+ const full = text(true);
92
+ // The cached part goes first when the terminal is too narrow for it.
93
+ return extra.width !== undefined && stringWidth(full) > extra.width ? text(false) : full;
94
+ }
95
+ function statusParts(mode, turns, usage, extra, withCached) {
96
+ return [
90
97
  ...(mode ? [`⏵⏵ ${t().mode(mode)}${extra.modeSwitchable ? ` (${t().switchModeKey})` : ""}`] : []),
91
98
  t().turns(turns),
92
- ...(usage ? [t().tokens(formatTokens(usage.inputTokens), formatTokens(usage.outputTokens))] : []),
99
+ // The new input apart from what was read again from the cache: the total grows with every
100
+ // call (each one sends the whole context again), which looked like the context growing.
101
+ ...(usage
102
+ ? [
103
+ t().tokens(formatTokens(usage.inputTokens - (usage.cacheReadTokens ?? 0)), formatTokens(usage.outputTokens), withCached && usage.cacheReadTokens ? formatTokens(usage.cacheReadTokens) : undefined),
104
+ ]
105
+ : []),
93
106
  ...(extra.contextPercent != null ? [t().context(Math.round(extra.contextPercent))] : []),
94
107
  ];
95
- return parts.join(" · ");
96
108
  }
97
109
  // No emoji-capable characters: Windows Terminal draws ✳ (U+2733) as a green emoji.
98
110
  const WORKING_GLYPHS = ["·", "✢", "✱", "✶", "✻", "✽", "✻", "✶", "✱", "✢"];
@@ -110,7 +122,8 @@ function Working({ label, startedAt, width }) {
110
122
  /** Line in progress, checkpoint panel or spinner, the input, and the status bar. */
111
123
  function LiveArea({ session, checkpoint, renderApproval, mode, width, statusExtra, modeSwitchable, reservedRows, children }) {
112
124
  // The mode in the spinner's color; the rest dim.
113
- const plain = statusText(mode, session.turns, session.usage, { contextPercent: session.contextPercent, modeSwitchable }) + (statusExtra ? ` · ${statusExtra}` : "");
125
+ const extraText = statusExtra ? ` · ${statusExtra}` : "";
126
+ const plain = statusText(mode, session.turns, session.usage, { contextPercent: session.contextPercent, modeSwitchable, width: width - stringWidth(extraText) }) + extraText;
114
127
  const cut = mode ? plain.indexOf(" · ") : -1;
115
128
  const status = cut > 0 ? ui.working(plain.slice(0, cut)) + ui.dim(plain.slice(cut)) : ui.dim(plain);
116
129
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, marginTop: session.liveGap ? 1 : 0, children: [stripAnsi(session.live) ? _jsx(Text, { children: session.live }) : null, checkpoint ? (_jsx(CheckpointPanel, { checkpoint: checkpoint, renderApproval: renderApproval, reservedRows: reservedRows, width: width })) : session.busy ? (
@@ -74,6 +74,13 @@ export interface InkChatOptions extends ChatTuiOptions {
74
74
  */
75
75
  firstPromptSuggestion?: string;
76
76
  }
77
+ /** The chat's own commands, never sent to the model (the agent-help skill lists them; a test keeps both in step). */
78
+ export declare const DEFAULT_EXIT_COMMANDS: readonly string[];
79
+ export declare const LOCAL_COMMANDS: {
80
+ readonly copy: "/copy";
81
+ readonly resume: "/resume";
82
+ readonly plan: "/plan";
83
+ };
77
84
  /** A choice asked in place of the prompt (the /resume list). */
78
85
  export interface ChatPicker {
79
86
  title: string;
@@ -27,7 +27,9 @@ import { createTerminalStatus, focusFromReport } from "./terminalStatus.js";
27
27
  import { listProjectFiles } from "./fileMentions.js";
28
28
  import { headerLines } from "./header.js";
29
29
  const DEFAULT_PROMPT_LABEL = "\n> ";
30
- const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
30
+ /** The chat's own commands, never sent to the model (the agent-help skill lists them; a test keeps both in step). */
31
+ export const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
32
+ export const LOCAL_COMMANDS = { copy: "/copy", resume: "/resume", plan: "/plan" };
31
33
  const DEFAULT_HISTORY_LIMIT = 100;
32
34
  // The prompt's frame: a border and one column of padding on each side.
33
35
  const PROMPT_FRAME_COLUMNS = 4;
@@ -382,19 +384,19 @@ export async function runChatInk(options, tuiOptions = {}) {
382
384
  if (exitCommands.has(line.toLowerCase()))
383
385
  break;
384
386
  // A local command, never sent to the model: the last reply to the clipboard (OSC 52).
385
- if (status && line.toLowerCase() === "/copy") {
387
+ if (status && line.toLowerCase() === LOCAL_COMMANDS.copy) {
386
388
  const reply = model.lastReply();
387
389
  if (reply)
388
390
  stdout.write(clipboardSequence(reply));
389
391
  model.writeLine(ui.dim(reply ? t().copiedReply(reply.length) : t().nothingToCopy));
390
392
  continue;
391
393
  }
392
- if (runsDir && line.toLowerCase() === "/resume") {
394
+ if (runsDir && line.toLowerCase() === LOCAL_COMMANDS.resume) {
393
395
  await resume();
394
396
  continue;
395
397
  }
396
398
  // A local command too: into plan mode, or back to the mode it was entered from.
397
- if (line.toLowerCase() === "/plan") {
399
+ if (line.toLowerCase() === LOCAL_COMMANDS.plan) {
398
400
  const control = modeControl();
399
401
  const next = control ? togglePlanMode(control) : null;
400
402
  if (next)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",