@falkenslab/agent-kit 0.16.0 → 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 +6 -4
- package/assets/agent-help/SKILL.md +57 -0
- package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +1 -1
- 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/fileKnowledgeStore.js +21 -4
- package/dist/core/knowledge.js +1 -1
- package/dist/core/knowledgeStore.d.ts +5 -0
- package/dist/core/messages/de.js +4 -4
- package/dist/core/messages/en.d.ts +3 -2
- package/dist/core/messages/en.js +4 -4
- package/dist/core/messages/es.js +4 -4
- package/dist/core/messages/fr.js +4 -4
- package/dist/core/runner.d.ts +5 -0
- package/dist/core/runner.js +1 -0
- package/dist/core/session.js +43 -12
- package/dist/core/tools/knowledgeTools.js +8 -1
- package/dist/core/tools/saveToSources.js +27 -13
- package/dist/core/version.d.ts +5 -0
- package/dist/core/version.js +12 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- 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
|
@@ -34,13 +34,15 @@ A foundation for building AI agents on top of the [Claude Agent SDK](https://www
|
|
|
34
34
|
|
|
35
35
|
[Captain Whiskers](./examples/captain-whiskers) is a toy agent to see the kit in action: a retired pirate cat who tells jokes in the terminal, in your language. It starts full screen with its logo and has a small crew of subagents: one looks for new jokes on the web, another rates them before they're told, and a third tells the time with a tool of the captain's own. It lets you try the different levels of oversight and is the best starting point for an agent of your own. Its README explains how to run it.
|
|
36
36
|
|
|
37
|
-

|
|
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
|
+
|
|
45
|
+
Oversight, memory, safety and the chat come from the kit; miyagi brings what it knows about Moodle and driving the browser.
|
|
44
46
|
|
|
45
47
|
## Installation
|
|
46
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`).
|
|
@@ -13,7 +13,7 @@ A source is ingested once, and from then on the knowledge base holds what it tau
|
|
|
13
13
|
|
|
14
14
|
## 2. Read it for real
|
|
15
15
|
|
|
16
|
-
Read the whole source, not its first page (a long PDF in parts: `list_sources` gives its pages). Its path is
|
|
16
|
+
Read the whole source, not its first page (a long PDF in parts: `list_sources` gives its pages). Its path is the sources folder's (named in the system prompt) plus the path `list_sources` gives, from the project folder, not from this skill's folder. `Read` reads PDFs and images; `extract_text` reads DOCX, PPTX (with its speaker notes) and XLSX. If it exists only outside the sources folder, keep the original first: `download_to_sources` for a URL (a web page is kept as markdown too: read that one), `save_to_sources` for a file in the run's folder, `request_file` to ask the person for one.
|
|
17
17
|
|
|
18
18
|
## 3. Write the summary page
|
|
19
19
|
|
|
@@ -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
|
/**
|
|
@@ -163,7 +163,12 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
163
163
|
async function storedField(key, value, file) {
|
|
164
164
|
if (key !== "file" || !options.sourcesDir || /^[a-z]+:\/\//i.test(value))
|
|
165
165
|
return value;
|
|
166
|
-
|
|
166
|
+
// Relative to the sources folder; a leading "sources/" or the folder's own name is dropped.
|
|
167
|
+
let relative = value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
168
|
+
for (const prefix of ["sources/", `${path.basename(options.sourcesDir)}/`])
|
|
169
|
+
if (relative.startsWith(prefix))
|
|
170
|
+
relative = relative.slice(prefix.length);
|
|
171
|
+
const original = path.resolve(options.sourcesDir, relative);
|
|
167
172
|
if (!(await stat(original).catch(() => null))?.isFile())
|
|
168
173
|
throw new Error(`"${value}" isn't an original in sources/ (list_sources lists them).`);
|
|
169
174
|
return posix(path.relative(path.dirname(file), original));
|
|
@@ -321,6 +326,12 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
321
326
|
.map(({ id, title, snippet }) => ({ id, title, snippet }));
|
|
322
327
|
},
|
|
323
328
|
index: indexText,
|
|
329
|
+
async recentLog(limit = 10) {
|
|
330
|
+
const text = await readFile(path.join(knowledgeDir, "log.md"), "utf8").catch(() => "");
|
|
331
|
+
// Entries start with "## [date] operation | what"; the newest are at the bottom.
|
|
332
|
+
const entries = text.split(/\r?\n(?=## )/).filter((entry) => entry.startsWith("## ")).map((entry) => entry.trim());
|
|
333
|
+
return entries.slice(-limit).reverse();
|
|
334
|
+
},
|
|
324
335
|
async log(operation, what, pages = []) {
|
|
325
336
|
await mkdir(knowledgeDir, { recursive: true });
|
|
326
337
|
const file = path.join(knowledgeDir, "log.md");
|
|
@@ -331,6 +342,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
331
342
|
async check() {
|
|
332
343
|
await writeIndex(); // the one mechanical fix there is: an index.md out of date
|
|
333
344
|
const { pages, byFile } = await pagesByFile();
|
|
345
|
+
const reservedFiles = new Set([...RESERVED].map((name) => path.normalize(path.join(knowledgeDir, `${name}.md`))));
|
|
334
346
|
const known = new Map(pages.map((p) => [p.id, p]));
|
|
335
347
|
const inbound = new Map();
|
|
336
348
|
const report = { brokenLinks: [], orphans: [], linksToRetired: [], missingOriginals: [] };
|
|
@@ -338,11 +350,16 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
338
350
|
if (statusOf(page) === "retired")
|
|
339
351
|
continue;
|
|
340
352
|
const body = fromFile(page.body, page.file, byFile);
|
|
341
|
-
// Relative links to .md files that resolve to nothing are broken too.
|
|
353
|
+
// Relative links to .md files that resolve to nothing are broken too. The overview, the
|
|
354
|
+
// index and the log aren't pages but are files: a link to one that exists is fine (a
|
|
355
|
+
// knowledge base written with the file tools links the overview, #22).
|
|
342
356
|
for (const m of page.body.matchAll(/\]\(([^)\s:]+\.md)(?:#[^)]*)?\)/g)) {
|
|
343
357
|
const target = path.normalize(path.resolve(path.dirname(page.file), decodeURI(m[1])));
|
|
344
|
-
if (
|
|
345
|
-
|
|
358
|
+
if (byFile.has(target) || !target.startsWith(path.normalize(knowledgeDir)))
|
|
359
|
+
continue;
|
|
360
|
+
if (reservedFiles.has(target) && (await stat(target).catch(() => null)))
|
|
361
|
+
continue;
|
|
362
|
+
report.brokenLinks.push({ page: page.id, target: posix(path.relative(knowledgeDir, target)) });
|
|
346
363
|
}
|
|
347
364
|
for (const id of new Set(linkedIds(body))) {
|
|
348
365
|
const target = known.get(id);
|
package/dist/core/knowledge.js
CHANGED
|
@@ -30,7 +30,7 @@ function toolsSection(originals, pageTypes) {
|
|
|
30
30
|
Your memory across sessions is an interlinked knowledge base of pages that you write and maintain yourself — a wiki, not a pile of notes. A future session only knows what is written there, so anything worth remembering must end up in a page, not just in this turn's reply. You reach it only through the \`knowledge_*\` tools. Write page content in the language you are using with the human.
|
|
31
31
|
|
|
32
32
|
### Layers
|
|
33
|
-
${originals ? originalsLine(originals) : ""}- **The knowledge base**: pages identified by type and slug (\`concept/spring-tides\`), plus the \`overview\`, a living synthesis of the whole. Page types: ${pageTypes.map((type) => `\`${type.type}\` (${type.description})`).join("; ")}.
|
|
33
|
+
${originals ? originalsLine(originals) : ""}- **The knowledge base**: pages identified by type and slug (\`concept/spring-tides\`), plus the \`overview\`, a living synthesis of the whole, and the \`log\` of what was done to it (\`knowledge_read\` reads both). Page types: ${pageTypes.map((type) => `\`${type.type}\` (${type.description})`).join("; ")}.
|
|
34
34
|
- **The schema**: these rules plus the \`knowledge-ingest\`, \`knowledge-query\` and \`knowledge-lint\` skills. \`knowledge_create\` without content gives a type's template.
|
|
35
35
|
|
|
36
36
|
### Working rules
|
|
@@ -114,6 +114,11 @@ export interface KnowledgeStore {
|
|
|
114
114
|
index(): Promise<string>;
|
|
115
115
|
/** Adds an entry to the operation log, dated today. */
|
|
116
116
|
log(operation: string, what: string, pages?: readonly string[]): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* The latest entries of the operation log, newest first, as markdown (e.g. "## [2026-10-04]
|
|
119
|
+
* ingest | …"). Optional: a store without it can't show the log to the agent.
|
|
120
|
+
*/
|
|
121
|
+
recentLog?(limit?: number): Promise<string[]>;
|
|
117
122
|
/** The mechanical problems. */
|
|
118
123
|
check(): Promise<CheckReport>;
|
|
119
124
|
/** The overview: a living synthesis of the whole knowledge base ("" if none yet). */
|
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",
|
|
@@ -70,12 +70,12 @@ export const de = {
|
|
|
70
70
|
keepPlanning: "Weiter planen",
|
|
71
71
|
cancelPlan: "Abbrechen",
|
|
72
72
|
planCommentQuestion: "Was soll sich ändern? (Enter zum Überspringen): ",
|
|
73
|
-
retireLines: (source, why, reason, replacedBy) => [
|
|
73
|
+
retireLines: (source, why, reason, replacedBy, folder) => [
|
|
74
74
|
`Quelle: ${source}`,
|
|
75
75
|
why === "wrong" ? "Warum: sie ist falsch; ihre Zusammenfassung wird zurückgezogen" : "Warum: sie wird ersetzt; ihre Zusammenfassung wird als ersetzt markiert",
|
|
76
76
|
`Grund: ${reason}`,
|
|
77
77
|
...(replacedBy ? [`Ersetzt durch: ${replacedBy}`] : []),
|
|
78
|
-
|
|
78
|
+
`Sie wird nach ${folder}.agent-kit/retired/ verschoben, nicht gelöscht.`,
|
|
79
79
|
],
|
|
80
80
|
toolPhrases: {
|
|
81
81
|
Read: ["hat {n} Datei gelesen", "hat {n} Dateien gelesen"],
|
|
@@ -120,7 +120,7 @@ export const de = {
|
|
|
120
120
|
labels: {
|
|
121
121
|
askingApproval: (summary) => `Bittet um menschliche Genehmigung: ${summary}`,
|
|
122
122
|
waitingManual: "Wartet auf einen manuellen Eingriff",
|
|
123
|
-
savingToSources: (destination) => `Speichert
|
|
123
|
+
savingToSources: (destination) => `Speichert ${destination} in den Quellen`,
|
|
124
124
|
checkingTime: "Datum und Uhrzeit abfragen",
|
|
125
125
|
calculatingDates: "Daten berechnen",
|
|
126
126
|
reading: (file) => `Liest ${file}`,
|
|
@@ -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;
|
|
@@ -67,7 +68,7 @@ export interface Messages {
|
|
|
67
68
|
keepPlanning: string;
|
|
68
69
|
cancelPlan: string;
|
|
69
70
|
planCommentQuestion: string;
|
|
70
|
-
retireLines(source: string, why: "wrong" | "replaced", reason: string, replacedBy
|
|
71
|
+
retireLines(source: string, why: "wrong" | "replaced", reason: string, replacedBy: string | undefined, folder: string): string[];
|
|
71
72
|
toolPhrases: Record<string, ToolPhrase>;
|
|
72
73
|
otherTools: ToolPhrase;
|
|
73
74
|
noOutput: 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",
|
|
@@ -69,12 +69,12 @@ export const en = {
|
|
|
69
69
|
keepPlanning: "Keep planning",
|
|
70
70
|
cancelPlan: "Cancel",
|
|
71
71
|
planCommentQuestion: "What should change? (Enter to skip): ",
|
|
72
|
-
retireLines: (source, why, reason, replacedBy) => [
|
|
72
|
+
retireLines: (source, why, reason, replacedBy, folder) => [
|
|
73
73
|
`Source: ${source}`,
|
|
74
74
|
why === "wrong" ? "Why: it's wrong; its summary will be retired" : "Why: it's replaced; its summary will be marked superseded",
|
|
75
75
|
`Reason: ${reason}`,
|
|
76
76
|
...(replacedBy ? [`Replaced by: ${replacedBy}`] : []),
|
|
77
|
-
|
|
77
|
+
`It's moved to ${folder}.agent-kit/retired/, not deleted.`,
|
|
78
78
|
],
|
|
79
79
|
toolPhrases: {
|
|
80
80
|
Read: ["read {n} file", "read {n} files"],
|
|
@@ -119,7 +119,7 @@ export const en = {
|
|
|
119
119
|
labels: {
|
|
120
120
|
askingApproval: (summary) => `Asking for human approval: ${summary}`,
|
|
121
121
|
waitingManual: "Waiting for a human to intervene manually",
|
|
122
|
-
savingToSources: (destination) => `Saving
|
|
122
|
+
savingToSources: (destination) => `Saving ${destination} to the sources`,
|
|
123
123
|
checkingTime: "Checking the date and time",
|
|
124
124
|
calculatingDates: "Calculating dates",
|
|
125
125
|
reading: (file) => `Reading ${file}`,
|
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",
|
|
@@ -70,12 +70,12 @@ export const es = {
|
|
|
70
70
|
keepPlanning: "Seguir planificando",
|
|
71
71
|
cancelPlan: "Cancelar",
|
|
72
72
|
planCommentQuestion: "¿Qué debería cambiar? (Enter para omitir): ",
|
|
73
|
-
retireLines: (source, why, reason, replacedBy) => [
|
|
73
|
+
retireLines: (source, why, reason, replacedBy, folder) => [
|
|
74
74
|
`Fuente: ${source}`,
|
|
75
75
|
why === "wrong" ? "Motivo: es errónea; su resumen se retirará" : "Motivo: se sustituye; su resumen se marcará como sustituido",
|
|
76
76
|
`Razón: ${reason}`,
|
|
77
77
|
...(replacedBy ? [`Sustituida por: ${replacedBy}`] : []),
|
|
78
|
-
|
|
78
|
+
`Se mueve a ${folder}.agent-kit/retired/, no se borra.`,
|
|
79
79
|
],
|
|
80
80
|
toolPhrases: {
|
|
81
81
|
Read: ["leyó {n} fichero", "leyó {n} ficheros"],
|
|
@@ -120,7 +120,7 @@ export const es = {
|
|
|
120
120
|
labels: {
|
|
121
121
|
askingApproval: (summary) => `Pidiendo aprobación humana: ${summary}`,
|
|
122
122
|
waitingManual: "Esperando a que una persona intervenga a mano",
|
|
123
|
-
savingToSources: (destination) => `Guardando
|
|
123
|
+
savingToSources: (destination) => `Guardando ${destination} en las fuentes`,
|
|
124
124
|
checkingTime: "Consultando la fecha y la hora",
|
|
125
125
|
calculatingDates: "Calculando fechas",
|
|
126
126
|
reading: (file) => `Leyendo ${file}`,
|
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",
|
|
@@ -70,12 +70,12 @@ export const fr = {
|
|
|
70
70
|
keepPlanning: "Continuer à planifier",
|
|
71
71
|
cancelPlan: "Annuler",
|
|
72
72
|
planCommentQuestion: "Que faut-il changer ? (Entrée pour passer) : ",
|
|
73
|
-
retireLines: (source, why, reason, replacedBy) => [
|
|
73
|
+
retireLines: (source, why, reason, replacedBy, folder) => [
|
|
74
74
|
`Source : ${source}`,
|
|
75
75
|
why === "wrong" ? "Motif : elle est erronée ; son résumé sera retiré" : "Motif : elle est remplacée ; son résumé sera marqué comme remplacé",
|
|
76
76
|
`Raison : ${reason}`,
|
|
77
77
|
...(replacedBy ? [`Remplacée par : ${replacedBy}`] : []),
|
|
78
|
-
|
|
78
|
+
`Elle est déplacée dans ${folder}.agent-kit/retired/, pas supprimée.`,
|
|
79
79
|
],
|
|
80
80
|
toolPhrases: {
|
|
81
81
|
Read: ["a lu {n} fichier", "a lu {n} fichiers"],
|
|
@@ -120,7 +120,7 @@ export const fr = {
|
|
|
120
120
|
labels: {
|
|
121
121
|
askingApproval: (summary) => `Demande d'approbation humaine : ${summary}`,
|
|
122
122
|
waitingManual: "En attente d'une intervention manuelle",
|
|
123
|
-
savingToSources: (destination) => `Enregistrement
|
|
123
|
+
savingToSources: (destination) => `Enregistrement de ${destination} dans les sources`,
|
|
124
124
|
checkingTime: "Consultation de la date et de l'heure",
|
|
125
125
|
calculatingDates: "Calcul de dates",
|
|
126
126
|
reading: (file) => `Lecture de ${file}`,
|
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
|
|
@@ -148,6 +174,11 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
148
174
|
// name isn't known ahead of time.
|
|
149
175
|
canUseTool: allowAnyMcpTool,
|
|
150
176
|
disallowedTools: spec.disallowedTools ?? [],
|
|
177
|
+
// TodoWrite (todos.ts) only exists with CLAUDE_CODE_ENABLE_TASKS=0: by default the CLI
|
|
178
|
+
// offers its TaskCreate/TaskUpdate/TaskList/TaskGet tools instead, and drops TodoWrite from
|
|
179
|
+
// the session even when `tools` names it (confirmed empirically). `env` replaces the
|
|
180
|
+
// subprocess environment, so the rest is passed on as it is.
|
|
181
|
+
env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" },
|
|
151
182
|
// Real text streaming instead of silently waiting for the turn's complete message —
|
|
152
183
|
// see the caller's message loop.
|
|
153
184
|
includePartialMessages: true,
|
|
@@ -165,8 +196,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
165
196
|
// "skills" acts as a name whitelist, not an addition: "all" enables both the
|
|
166
197
|
// SDK's own official skills (pdf/docx), the project's own custom ones, and the
|
|
167
198
|
// plugin-provided built-ins below. A spec's own list gets the knowledge base's
|
|
168
|
-
// skills added, so it only names its own.
|
|
169
|
-
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)),
|
|
170
201
|
plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
|
|
171
202
|
}
|
|
172
203
|
: {}),
|
|
@@ -254,10 +285,10 @@ const KNOWLEDGE_SKILLS = {
|
|
|
254
285
|
tools: ["knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
|
|
255
286
|
files: ["knowledge-pages", "knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
|
|
256
287
|
};
|
|
257
|
-
function skillList(skills, knowledgeVariant) {
|
|
288
|
+
function skillList(skills, knowledgeVariant, agentHelp) {
|
|
258
289
|
if (skills === undefined || skills === "all")
|
|
259
290
|
return "all";
|
|
260
|
-
return
|
|
291
|
+
return [...new Set([...skills, ...(knowledgeVariant ? KNOWLEDGE_SKILLS[knowledgeVariant] : []), ...(agentHelp ? [AGENT_HELP_SKILL] : [])])];
|
|
261
292
|
}
|
|
262
293
|
/**
|
|
263
294
|
* User message queue backed by a single long-lived generator, for any multi-turn
|
|
@@ -22,6 +22,7 @@ async function attempt(work) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
const OVERVIEW = "overview";
|
|
25
|
+
const LOG = "log";
|
|
25
26
|
/** The `knowledge` MCP server over `store`. */
|
|
26
27
|
export function createKnowledgeServer(store, options) {
|
|
27
28
|
const typeNames = store.types().map((t) => t.type);
|
|
@@ -49,9 +50,15 @@ export function createKnowledgeServer(store, options) {
|
|
|
49
50
|
const hits = await store.search(args.query, args.limit ?? 10);
|
|
50
51
|
return hits.length ? hits.map((hit) => `- ${hit.id} — ${hit.title}: ${hit.snippet}`).join("\n") : "No page matches.";
|
|
51
52
|
}), { annotations: { readOnlyHint: true } });
|
|
52
|
-
const read = tool("knowledge_read", 'A page by its id ("concept/bowline")
|
|
53
|
+
const read = tool("knowledge_read", 'A page by its id ("concept/bowline"): its fields, its content (links to other pages as ids) and the pages that link to it. Or "overview" (the living synthesis of the whole knowledge base), or "log" (the latest entries of the operation log, newest first: what was done to the knowledge base, and when).', { page: z.string().describe('The page id, "overview" or "log"') }, async (args) => attempt(async () => {
|
|
53
54
|
if (args.page === OVERVIEW)
|
|
54
55
|
return (await store.overview()) || "There's no overview yet: write it with knowledge_rewrite (page \"overview\").";
|
|
56
|
+
if (args.page === LOG) {
|
|
57
|
+
if (!store.recentLog)
|
|
58
|
+
return "This knowledge base's store can't show its log.";
|
|
59
|
+
const entries = await store.recentLog(10);
|
|
60
|
+
return entries.length ? entries.join("\n\n") : "The log is empty: nothing has been logged yet.";
|
|
61
|
+
}
|
|
55
62
|
const page = await store.read(args.page);
|
|
56
63
|
if (!page)
|
|
57
64
|
throw new Error(`There's no page "${args.page}". knowledge_search or knowledge_index finds the right one.`);
|
|
@@ -46,8 +46,6 @@ const DEFAULT_DESCRIPTION = "Copy a file from this run's own folder (e.g. someth
|
|
|
46
46
|
"downloading a document you're about to study in depth, so later sessions have it too.";
|
|
47
47
|
/** The most `download_to_sources` and `request_file` copy in. */
|
|
48
48
|
export const MAX_SOURCE_BYTES = 50 * 1024 * 1024;
|
|
49
|
-
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
50
|
-
const fail = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
51
49
|
const message = (error) => (error instanceof Error ? error.message : String(error));
|
|
52
50
|
function added(result, extra = "") {
|
|
53
51
|
return result.duplicateOf
|
|
@@ -116,6 +114,20 @@ export async function downloadToFile(url, file, maxBytes) {
|
|
|
116
114
|
export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFAULT_DESCRIPTION, options = {}) {
|
|
117
115
|
const projectDir = options.projectDir ?? path.dirname(sourcesDir);
|
|
118
116
|
const maxBytes = options.maxBytes ?? MAX_SOURCE_BYTES;
|
|
117
|
+
// The texts say "sources/"; the model reads the folder's real name (e.g. "treasure/"),
|
|
118
|
+
// since it reads and searches the originals by that path. Paths it gives may start with
|
|
119
|
+
// either.
|
|
120
|
+
const folder = `${path.relative(projectDir, sourcesDir).split(path.sep).join("/") || "."}/`;
|
|
121
|
+
const named = (text) => text.replaceAll("sources/", folder);
|
|
122
|
+
const inFolder = (relative) => {
|
|
123
|
+
const clean = relative.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
124
|
+
for (const prefix of [folder, "sources/"])
|
|
125
|
+
if (clean.startsWith(prefix))
|
|
126
|
+
return clean.slice(prefix.length);
|
|
127
|
+
return clean;
|
|
128
|
+
};
|
|
129
|
+
const ok = (text) => ({ content: [{ type: "text", text: named(text) }] });
|
|
130
|
+
const fail = (text) => ({ content: [{ type: "text", text: named(text) }], isError: true });
|
|
119
131
|
const replaces = z.string().optional().describe("For a new version: the original it replaces, relative to sources/ (kept, never overwritten)");
|
|
120
132
|
const saveToSources = tool("save_to_sources", description, {
|
|
121
133
|
source: z.string().describe("Path to the file, relative to this run's own folder - not a URL"),
|
|
@@ -131,7 +143,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
131
143
|
}
|
|
132
144
|
try {
|
|
133
145
|
const relative = path.relative(runDir, resolvedSource).split(path.sep).join("/");
|
|
134
|
-
return ok(added(await addSource(sourcesDir, resolvedSource, args.destination, { kind: "run", from: relative }, { replaces: args.replaces })));
|
|
146
|
+
return ok(added(await addSource(sourcesDir, resolvedSource, inFolder(args.destination), { kind: "run", from: relative }, { replaces: args.replaces && inFolder(args.replaces) })));
|
|
135
147
|
}
|
|
136
148
|
catch (error) {
|
|
137
149
|
return fail(message(error));
|
|
@@ -150,7 +162,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
150
162
|
from: z.number().int().positive().optional().describe("First slide (PPTX)"),
|
|
151
163
|
to: z.number().int().positive().optional().describe("Last slide (PPTX)"),
|
|
152
164
|
}, async (args) => {
|
|
153
|
-
const file = resolveWithin(sourcesDir, args.source);
|
|
165
|
+
const file = resolveWithin(sourcesDir, inFolder(args.source));
|
|
154
166
|
if (!file || !(await stat(file).catch(() => null))?.isFile())
|
|
155
167
|
return fail(`"${args.source}" isn't an original in sources/ (list_sources lists them).`);
|
|
156
168
|
if (!EXTRACTABLE.has(path.extname(file).slice(1).toLowerCase()))
|
|
@@ -171,7 +183,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
171
183
|
try {
|
|
172
184
|
const type = await downloadToFile(args.url, temp, maxBytes);
|
|
173
185
|
const name = decodeURIComponent(new URL(args.url).pathname.split("/").filter(Boolean).pop() ?? "download");
|
|
174
|
-
const result = await addSource(sourcesDir, temp, args.destination, { kind: "url", from: args.url }, { replaces: args.replaces, name });
|
|
186
|
+
const result = await addSource(sourcesDir, temp, inFolder(args.destination), { kind: "url", from: args.url }, { replaces: args.replaces && inFolder(args.replaces), name });
|
|
175
187
|
let extra = "";
|
|
176
188
|
if (!result.duplicateOf && /html/i.test(type)) {
|
|
177
189
|
const html = await readFile(temp, "utf8");
|
|
@@ -217,7 +229,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
217
229
|
if (info.size > maxBytes)
|
|
218
230
|
return fail(`"${answer}" is ${info.size} bytes, over the ${maxBytes}-byte limit.`);
|
|
219
231
|
try {
|
|
220
|
-
return ok(added(await addSource(sourcesDir, file, args.destination
|
|
232
|
+
return ok(added(await addSource(sourcesDir, file, args.destination ? inFolder(args.destination) : path.basename(file), { kind: "person", from: path.basename(file) }, { replaces: args.replaces && inFolder(args.replaces) })));
|
|
221
233
|
}
|
|
222
234
|
catch (error) {
|
|
223
235
|
return fail(message(error));
|
|
@@ -229,11 +241,11 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
229
241
|
reason: z.string().describe("Why, in one sentence, in the person's language"),
|
|
230
242
|
replacedBy: z.string().optional().describe('For "replaced": the original that replaces it, relative to sources/'),
|
|
231
243
|
}, async (args) => {
|
|
232
|
-
const answer = await askForDecision(runDir, { title: t().retireTitle, lines: t().retireLines(args.source, args.why, args.reason, args.replacedBy) });
|
|
244
|
+
const answer = await askForDecision(runDir, { title: t().retireTitle, lines: t().retireLines(inFolder(args.source), args.why, args.reason, args.replacedBy && inFolder(args.replacedBy), folder) });
|
|
233
245
|
if (answer !== "" && answer !== "y" && answer !== "yes")
|
|
234
246
|
return ok(answer === "q" ? "The person stopped: don't retire it, and stop what you were doing." : "The person said no: the original stays.");
|
|
235
247
|
try {
|
|
236
|
-
const result = await retireSource(sourcesDir, options.knowledgeDir, projectDir, args.source, args.why, args.reason, args.replacedBy);
|
|
248
|
+
const result = await retireSource(sourcesDir, options.knowledgeDir, projectDir, inFolder(args.source), args.why, args.reason, args.replacedBy && inFolder(args.replacedBy));
|
|
237
249
|
const pages = result.summaries.map((s) => `${s.page} marked ${s.marked}${s.supersededBy ? ` (see ${s.supersededBy})` : ""}`);
|
|
238
250
|
return ok([
|
|
239
251
|
`Retired: moved to sources/${result.movedTo}.`,
|
|
@@ -245,9 +257,11 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
245
257
|
return fail(message(error));
|
|
246
258
|
}
|
|
247
259
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
260
|
+
// The descriptions (the tool's and its parameters') with the folder's real name too.
|
|
261
|
+
const tools = [saveToSources, listSourcesTool, extractTextTool, downloadToSources, ...(options.interactive ? [requestFile, retire] : [])].map((definition) => ({
|
|
262
|
+
...definition,
|
|
263
|
+
description: named(definition.description),
|
|
264
|
+
inputSchema: Object.fromEntries(Object.entries(definition.inputSchema).map(([key, schema]) => [key, schema.description ? schema.describe(named(schema.description)) : schema])),
|
|
265
|
+
}));
|
|
266
|
+
return createSdkMcpServer({ name: "sourceFiles", version: "1.0.0", tools: tools });
|
|
253
267
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
let cached;
|
|
5
|
+
/**
|
|
6
|
+
* The version of agent-kit in use, read from its own `package.json` (next to `dist/`, or
|
|
7
|
+
* `src/` under tsx), e.g. to show it in an agent's header or log.
|
|
8
|
+
*/
|
|
9
|
+
export function agentKitVersion() {
|
|
10
|
+
cached ??= JSON.parse(readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8")).version;
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,10 +6,11 @@ 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";
|
|
13
|
+
export { agentKitVersion } from "./core/version.js";
|
|
13
14
|
export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, type KnowledgeStore, type PageType, type KnowledgePage, type PageInfo, type SearchHit, type CheckReport, type FieldChanges, type NewPage, } from "./core/knowledgeStore.js";
|
|
14
15
|
export { createFileKnowledgeStore, type FileKnowledgeStoreOptions } from "./core/fileKnowledgeStore.js";
|
|
15
16
|
export { createKnowledgeServer, type KnowledgeToolsOptions } from "./core/tools/knowledgeTools.js";
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { createFriendlyToolLabel, truncate, truncatePath, } from "./core/toolLab
|
|
|
11
11
|
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode } from "./core/session.js";
|
|
12
12
|
export { runQuery } from "./core/runner.js";
|
|
13
13
|
export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
|
|
14
|
+
export { agentKitVersion } from "./core/version.js";
|
|
14
15
|
export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, } from "./core/knowledgeStore.js";
|
|
15
16
|
export { createFileKnowledgeStore } from "./core/fileKnowledgeStore.js";
|
|
16
17
|
export { createKnowledgeServer } from "./core/tools/knowledgeTools.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)
|