@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 +3 -3
- package/assets/agent-help/SKILL.md +57 -0
- package/dist/core/agentHelp.d.ts +30 -0
- package/dist/core/agentHelp.js +55 -0
- package/dist/core/agentSpec.d.ts +21 -0
- package/dist/core/messages/de.js +1 -1
- package/dist/core/messages/en.d.ts +2 -1
- package/dist/core/messages/en.js +1 -1
- package/dist/core/messages/es.js +1 -1
- package/dist/core/messages/fr.js +1 -1
- package/dist/core/runner.d.ts +5 -0
- package/dist/core/runner.js +1 -0
- package/dist/core/session.js +38 -12
- package/dist/index.d.ts +1 -1
- package/dist/tui/ink/SessionView.d.ts +1 -0
- package/dist/tui/ink/SessionView.js +17 -4
- package/dist/tui/ink/runChatInk.d.ts +7 -0
- package/dist/tui/ink/runChatInk.js +6 -4
- package/package.json +1 -1
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
|
-
###
|
|
41
|
+
### miyagi
|
|
42
42
|
|
|
43
|
-
[
|
|
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;
|
|
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
|
+
}
|
package/dist/core/agentSpec.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/core/messages/de.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -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",
|
package/dist/core/messages/es.js
CHANGED
|
@@ -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
|
|
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",
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -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
|
|
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",
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/runner.js
CHANGED
|
@@ -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
|
};
|
package/dist/core/session.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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, ...
|
|
143
|
-
allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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() ===
|
|
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() ===
|
|
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() ===
|
|
399
|
+
if (line.toLowerCase() === LOCAL_COMMANDS.plan) {
|
|
398
400
|
const control = modeControl();
|
|
399
401
|
const next = control ? togglePlanMode(control) : null;
|
|
400
402
|
if (next)
|