@falkenslab/agent-kit 0.16.1 → 0.18.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/assets/knowledge-plugin/commands/ingest.md +1 -1
- package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +8 -3
- package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +12 -5
- package/dist/core/agentHelp.d.ts +30 -0
- package/dist/core/agentHelp.js +55 -0
- package/dist/core/agentSpec.d.ts +37 -16
- package/dist/core/fileKnowledgeStore.d.ts +1 -3
- package/dist/core/fileKnowledgeStore.js +23 -29
- package/dist/core/hooks/fileScopeGate.d.ts +17 -2
- package/dist/core/hooks/fileScopeGate.js +45 -10
- package/dist/core/knowledge.d.ts +20 -17
- package/dist/core/knowledge.js +32 -60
- package/dist/core/knowledgeStore.d.ts +4 -9
- package/dist/core/knowledgeStore.js +16 -2
- package/dist/core/language.js +1 -1
- package/dist/core/messages/de.js +2 -2
- package/dist/core/messages/en.d.ts +2 -1
- package/dist/core/messages/en.js +2 -2
- package/dist/core/messages/es.js +2 -2
- package/dist/core/messages/fr.js +2 -2
- package/dist/core/runner.d.ts +5 -0
- package/dist/core/runner.js +1 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +121 -40
- package/dist/core/sources.d.ts +24 -31
- package/dist/core/sources.js +45 -108
- package/dist/core/tools/humanApproval.js +3 -3
- package/dist/core/tools/knowledgeTools.d.ts +6 -6
- package/dist/core/tools/knowledgeTools.js +18 -53
- package/dist/core/tools/saveToSources.d.ts +6 -2
- package/dist/core/tools/saveToSources.js +17 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +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/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
- package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
- package/assets/knowledge-plugin-files/commands/lint.md +0 -5
- package/assets/knowledge-plugin-files/commands/query.md +0 -5
- package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
- package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
- package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
- package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
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 language you reply in, 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`).
|
|
@@ -4,4 +4,4 @@ description: Ingest material into the knowledge base right now - the files or to
|
|
|
4
4
|
|
|
5
5
|
Apply the `knowledge-ingest` skill right now to: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
If nothing is given above, ingest every original `list_sources`
|
|
7
|
+
If nothing is given above, ingest every original in `list_sources` that no summary is about, and update the summaries whose original's `changedAt` is after their `ingested` (both ISO 8601 in UTC: the later one sorts after as text). When you're done, summarize which pages you created and updated, and anything you couldn't read.
|
|
@@ -9,7 +9,12 @@ A source is ingested once, and from then on the knowledge base holds what it tau
|
|
|
9
9
|
|
|
10
10
|
## 1. Check it isn't already ingested
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
The sources and the knowledge base each keep their own data; you match them:
|
|
13
|
+
|
|
14
|
+
- `list_sources` gives each original with `changedAt`, when its content last changed.
|
|
15
|
+
- `knowledge_index` gives each summary with `file`, the original it's about, and `ingested`, when it was written.
|
|
16
|
+
|
|
17
|
+
An original with no summary needs ingesting. One whose `changedAt` is after its summary's `ingested` changed since: update its summary (`knowledge_rewrite`, which sets `ingested` again). Both dates are ISO 8601 in UTC, so the later one sorts after as text; use `date_math` if in doubt. For a document from elsewhere, `knowledge_search` its title or URL.
|
|
13
18
|
|
|
14
19
|
## 2. Read it for real
|
|
15
20
|
|
|
@@ -17,7 +22,7 @@ Read the whole source, not its first page (a long PDF in parts: `list_sources` g
|
|
|
17
22
|
|
|
18
23
|
## 3. Write the summary page
|
|
19
24
|
|
|
20
|
-
`knowledge_create` with type `summary` (call it once without content to get the template), and the field `file` (the original,
|
|
25
|
+
`knowledge_create` with type `summary` (call it once without content to get the template), and the field `file` (the original, exactly as `list_sources` names it) or `url`; `ingested` is set for you. Key points quote literally where exactness matters. Create the new concept and entity pages it feeds in the same call (`also`): they can link to each other.
|
|
21
26
|
|
|
22
27
|
## 4. Update concepts and entities
|
|
23
28
|
|
|
@@ -33,4 +38,4 @@ If the source contradicts a page, record both versions with attribution in both
|
|
|
33
38
|
|
|
34
39
|
## An original that was wrong or replaced
|
|
35
40
|
|
|
36
|
-
`retire_source` (the person
|
|
41
|
+
`retire_source` (it asks the person itself) moves it aside; `list_sources` shows it as retired, with why and what replaced it. The knowledge base is then yours to update: find its summaries (`knowledge_index`, by `file`) and the passages that cite it (`knowledge_search` its file name). After a wrong one, `knowledge_retire` its summary (it asks the person itself: don't ask them first) and correct or remove what it taught (`knowledge_edit`, or `knowledge_retire` for a page that was only about it). After a replaced one, ingest the new original, `knowledge_supersede` the old summary by the new one, and re-ground those passages on it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: knowledge-lint
|
|
3
|
-
description: Health-check the knowledge base - broken links, orphan pages, links to retired pages,
|
|
3
|
+
description: Health-check the knowledge base - broken links, orphan pages, links to retired pages, summaries behind their originals, duplicated concepts, mentioned-but-missing concepts, contradictions, gaps - fix what's mechanical and report the rest.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Linting the knowledge base
|
|
@@ -9,14 +9,21 @@ A knowledge base maintained across sessions drifts. Lint it when asked and after
|
|
|
9
9
|
|
|
10
10
|
## 1. The mechanical checks, in one call
|
|
11
11
|
|
|
12
|
-
`knowledge_check` returns broken links, orphan pages
|
|
12
|
+
`knowledge_check` returns broken links, orphan pages and links to retired pages. The index and backlinks are kept by the store: there's nothing to fix there.
|
|
13
13
|
|
|
14
14
|
- **Broken links**: fix them with `knowledge_edit` when the right target is obvious (a typo, a page under a close name: `knowledge_search`); otherwise report them.
|
|
15
15
|
- **Orphans**: link them from the pages where they belong, or report why they don't belong anywhere.
|
|
16
|
-
- **Links to retired pages
|
|
17
|
-
- **New or changed originals**: report them as what to ingest or update next (or do it, if asked).
|
|
16
|
+
- **Links to retired pages**: correct or re-ground them.
|
|
18
17
|
|
|
19
|
-
## 2. The
|
|
18
|
+
## 2. The knowledge base against its originals
|
|
19
|
+
|
|
20
|
+
When there's a sources folder, match `list_sources` with `knowledge_index`:
|
|
21
|
+
|
|
22
|
+
- **Not ingested**: present originals no summary is about. Report them as what to ingest next (or do it, if asked).
|
|
23
|
+
- **Behind**: summaries whose original's `changedAt` is after their `ingested`, (ISO 8601 in UTC: the later one sorts after as text). Report them as what to update.
|
|
24
|
+
- **Retired or missing originals**: summaries still active whose original is retired (`knowledge_retire` them if it was wrong, which asks the person itself, or `knowledge_supersede` them if it was replaced) or missing (report them); and the passages that cite them (`knowledge_search` the file name).
|
|
25
|
+
|
|
26
|
+
## 3. The checks that need judgement
|
|
20
27
|
|
|
21
28
|
Use `knowledge_index` and `knowledge_search`; read only the pages a check needs.
|
|
22
29
|
|
|
@@ -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
|
@@ -40,6 +40,12 @@ export interface BaseSessionConfig {
|
|
|
40
40
|
sourcesDir?: string;
|
|
41
41
|
/** Directories besides `knowledgeDir`/`sourcesDir` where Write/Edit are allowed (see hooks/fileScopeGate.ts). */
|
|
42
42
|
extraWritableDirs?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Directories besides `sourcesDir`/`extraWritableDirs` the agent may read and search (Read,
|
|
45
|
+
* Glob, Grep), never write. Read and Glob reach nothing else on the disk but what the kit
|
|
46
|
+
* knows the agent needs (see hooks/fileScopeGate.ts).
|
|
47
|
+
*/
|
|
48
|
+
extraReadableDirs?: string[];
|
|
43
49
|
/** Files or directories the agent must never read, search or write, e.g. a config file holding a password (see hooks/fileScopeGate.ts). */
|
|
44
50
|
deniedPaths?: string[];
|
|
45
51
|
/** Extra values (e.g. a password) to scrub out of the transcript log — see hooks/transcriptLogger.ts. */
|
|
@@ -70,6 +76,18 @@ export interface BaseSessionConfig {
|
|
|
70
76
|
export interface AgentSpec<TConfig extends BaseSessionConfig> {
|
|
71
77
|
/** The system prompt for this run — everything role/mode/domain-specific lives behind this call. */
|
|
72
78
|
buildSystemPrompt(config: TConfig): string;
|
|
79
|
+
/**
|
|
80
|
+
* Who the agent is. With it, the kit tells the model its name, version and what it is, plus
|
|
81
|
+
* agent-kit's version, and offers the `agent-help` skill, which answers how to use the
|
|
82
|
+
* agent: the kit's chat (modes, keys, slash commands, resuming) and `helpGuide`. Without
|
|
83
|
+
* it, neither.
|
|
84
|
+
*/
|
|
85
|
+
identity?: AgentIdentity;
|
|
86
|
+
/**
|
|
87
|
+
* Absolute path of a markdown guide to the agent's own domain (its commands, configuration,
|
|
88
|
+
* folders), which the `agent-help` skill includes. Only with `identity`.
|
|
89
|
+
*/
|
|
90
|
+
helpGuide?: string;
|
|
73
91
|
/**
|
|
74
92
|
* MCP servers this agent always registers (e.g. Playwright for a browser-driving
|
|
75
93
|
* agent), on top of the generic ones `buildSessionOptions()` wires up itself when
|
|
@@ -109,23 +127,15 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
|
|
|
109
127
|
saveToSourcesDescription?: string;
|
|
110
128
|
/**
|
|
111
129
|
* The built-in knowledge base (see knowledge.ts): when `config.knowledgeDir` is set, the kit
|
|
112
|
-
* appends its "Knowledge base" rules to the system prompt
|
|
130
|
+
* appends its "Knowledge base" rules to the system prompt, loads its plugin (skills
|
|
113
131
|
* knowledge-ingest/knowledge-query/knowledge-lint, commands /knowledge:ingest, /knowledge:query,
|
|
114
|
-
* /knowledge:lint)
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* notes) — `knowledgePromptSection()`/`knowledgePluginRoot()` are exported to reuse the pieces.
|
|
132
|
+
* /knowledge:lint) and its `knowledge_*` tools, the only way into the knowledge base (ADR-024).
|
|
133
|
+
* On by default; `false` makes `knowledgeDir` a folder of the agent's own notes, kept with the
|
|
134
|
+
* file tools under rules it writes itself.
|
|
118
135
|
*/
|
|
119
136
|
knowledgeBase?: boolean;
|
|
120
137
|
/**
|
|
121
|
-
*
|
|
122
|
-
* the kit's `knowledge_*` tools over a `KnowledgeStore`, never with the file tools, so the
|
|
123
|
-
* storage can change and the wiki's rules are kept by code (ADR-024); `"files"`, with the
|
|
124
|
-
* file tools on `knowledgeDir` and the rules in the prompt, as before.
|
|
125
|
-
*/
|
|
126
|
-
knowledgeTools?: "store" | "files";
|
|
127
|
-
/**
|
|
128
|
-
* The agent's own page types for the built-in knowledge base, besides the kit's four
|
|
138
|
+
* The agent's own page types for the built-in knowledge base, besides the kit's
|
|
129
139
|
* (summary, concept, entity, synthesis): each with its folder (`""` for the root), index
|
|
130
140
|
* section, description (told to the model) and template.
|
|
131
141
|
*/
|
|
@@ -154,11 +164,13 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
|
|
|
154
164
|
settingSources?: SettingSource[];
|
|
155
165
|
/**
|
|
156
166
|
* The skills the agent offers (the SDK's `skills`): names, or `plugin:skill` for a
|
|
157
|
-
* plugin's. Default `"all"`, every skill found
|
|
158
|
-
*
|
|
167
|
+
* plugin's. Default `"all"`, every skill found: the SDK's own (about twenty, whatever
|
|
168
|
+
* `settingSources` says), the project's, the plugins'. `"plugins"` offers only those of the
|
|
169
|
+
* plugins the session loads (`pluginRoots`, the knowledge base's, agent-help's), without
|
|
170
|
+
* naming them. A list keeps the rest out of each turn's context; the knowledge base's own
|
|
159
171
|
* skills are added to it when the knowledge base is on. A context filter, not a sandbox.
|
|
160
172
|
*/
|
|
161
|
-
skills?: string[] | "all";
|
|
173
|
+
skills?: string[] | "all" | "plugins";
|
|
162
174
|
/**
|
|
163
175
|
* Text for the generic human-approval checkpoint (see tools/humanApproval.ts) — what
|
|
164
176
|
* counts as "publishing something visible to others" is entirely domain-specific.
|
|
@@ -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
|
/**
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { type KnowledgeStore, type PageType } from "./knowledgeStore.js";
|
|
2
2
|
/** Options of `createFileKnowledgeStore()`. */
|
|
3
3
|
export interface FileKnowledgeStoreOptions {
|
|
4
|
-
/** The agent's own page types, besides the kit's
|
|
4
|
+
/** The agent's own page types, besides the kit's. */
|
|
5
5
|
pageTypes?: readonly PageType[];
|
|
6
|
-
/** The sources folder: a summary's `file` field is given relative to it. */
|
|
7
|
-
sourcesDir?: string;
|
|
8
6
|
}
|
|
9
7
|
/** A `KnowledgeStore` over the markdown files of `knowledgeDir`. */
|
|
10
8
|
export declare function createFileKnowledgeStore(knowledgeDir: string, options?: FileKnowledgeStoreOptions): KnowledgeStore;
|
|
@@ -27,6 +27,8 @@ function serialize(fields, body) {
|
|
|
27
27
|
const lines = [...fields].map(([key, value]) => `${key}: ${/[:#]/.test(value) && !/^\[.*\]$/.test(value) ? JSON.stringify(value) : value}`);
|
|
28
28
|
return `---\n${lines.join("\n")}\n---\n\n${body.replace(/^\s+/, "").replace(/\s*$/, "")}\n`;
|
|
29
29
|
}
|
|
30
|
+
/** When a summary was written from its original (ISO 8601, as `list_sources`' `changedAt`). */
|
|
31
|
+
const stampNow = () => new Date().toISOString();
|
|
30
32
|
/** A `KnowledgeStore` over the markdown files of `knowledgeDir`. */
|
|
31
33
|
export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
32
34
|
const declared = options.pageTypes ?? [];
|
|
@@ -134,9 +136,9 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
134
136
|
for (const [i, page] of ready.entries()) {
|
|
135
137
|
const fields = newPages[i].fields ?? {};
|
|
136
138
|
page.body = toFile(page.body, page.file);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
await applyFields(page, fields);
|
|
140
|
+
if (page.type.type === "summary")
|
|
141
|
+
page.fields.set("ingested", stampNow());
|
|
140
142
|
}
|
|
141
143
|
for (const page of ready) {
|
|
142
144
|
await mkdir(path.dirname(page.file), { recursive: true });
|
|
@@ -159,28 +161,20 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
159
161
|
if (missing.length)
|
|
160
162
|
throw new Error(`These links point to pages that don't exist: ${missing.join(", ")}. Create them first, or link existing ones (knowledge_search).`);
|
|
161
163
|
}
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
|
172
|
-
if (!(await stat(original).catch(() => null))?.isFile())
|
|
173
|
-
throw new Error(`"${value}" isn't an original in sources/ (list_sources lists them).`);
|
|
174
|
-
return posix(path.relative(path.dirname(file), original));
|
|
175
|
-
}
|
|
164
|
+
/**
|
|
165
|
+
* Sets or removes the fields given. `type` is the page's, and a summary's `ingested` is the
|
|
166
|
+
* store's (when it was written from its original), so neither is taken from the caller.
|
|
167
|
+
* `file` is kept as given: the original's path relative to the sources folder, which the
|
|
168
|
+
* store doesn't check (the sources are another extension's data, #30).
|
|
169
|
+
*/
|
|
176
170
|
async function applyFields(page, changes = {}) {
|
|
177
171
|
for (const [key, value] of Object.entries(changes)) {
|
|
178
|
-
if (key === "type")
|
|
172
|
+
if (key === "type" || key === "ingested")
|
|
179
173
|
continue;
|
|
180
174
|
if (value === null)
|
|
181
175
|
page.fields.delete(key);
|
|
182
176
|
else
|
|
183
|
-
page.fields.set(key,
|
|
177
|
+
page.fields.set(key, value);
|
|
184
178
|
}
|
|
185
179
|
page.fields.set("updated", today());
|
|
186
180
|
}
|
|
@@ -191,7 +185,8 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
191
185
|
}
|
|
192
186
|
/** The notice under the frontmatter of a superseded or retired page. */
|
|
193
187
|
const withNotice = (body, notice) => `> ${notice}\n\n${body.replace(/^> (Superseded|Retired) on [^\n]*\n\n/, "")}`;
|
|
194
|
-
|
|
188
|
+
/** The catalog: with links as ids (`summary/kitchen`) for the tools, or as files for `index.md`. */
|
|
189
|
+
async function indexText(links = "ids") {
|
|
195
190
|
const pages = (await allPages()).filter((p) => statusOf(p) !== "retired");
|
|
196
191
|
const lines = ["# Index", ""];
|
|
197
192
|
for (const type of types) {
|
|
@@ -202,7 +197,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
202
197
|
for (const page of own) {
|
|
203
198
|
const extras = (type.indexFields ?? []).filter((f) => page.fields.get(f)).map((f) => `${f}: ${page.fields.get(f)}`);
|
|
204
199
|
const status = statusOf(page) === "superseded" ? " (superseded)" : "";
|
|
205
|
-
lines.push(`- [${titleOf(page)}](${posix(path.relative(knowledgeDir, page.file))}) — ${descriptionOf(page)}${extras.length ? ` (${extras.join(", ")})` : ""}${status}`);
|
|
200
|
+
lines.push(`- [${titleOf(page)}](${links === "ids" ? page.id : posix(path.relative(knowledgeDir, page.file))}) — ${descriptionOf(page)}${extras.length ? ` (${extras.join(", ")})` : ""}${status}`);
|
|
206
201
|
}
|
|
207
202
|
lines.push("");
|
|
208
203
|
}
|
|
@@ -210,7 +205,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
210
205
|
}
|
|
211
206
|
async function writeIndex() {
|
|
212
207
|
await mkdir(knowledgeDir, { recursive: true });
|
|
213
|
-
await writeFile(path.join(knowledgeDir, "index.md"), await indexText());
|
|
208
|
+
await writeFile(path.join(knowledgeDir, "index.md"), await indexText("files"));
|
|
214
209
|
}
|
|
215
210
|
async function pagesByFile() {
|
|
216
211
|
const pages = await allPages();
|
|
@@ -265,6 +260,9 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
265
260
|
await linksResolve(content, id);
|
|
266
261
|
page.body = toFile(/^#\s/.test(content.trimStart()) ? content : `# ${titleOf(page)}\n\n${content}`, page.file);
|
|
267
262
|
await applyFields(page, fields);
|
|
263
|
+
// A summary redone whole is redone from its original: written now.
|
|
264
|
+
if (page.type.type === "summary")
|
|
265
|
+
page.fields.set("ingested", stampNow());
|
|
268
266
|
await save(page);
|
|
269
267
|
},
|
|
270
268
|
async supersede(id, by, reason) {
|
|
@@ -325,7 +323,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
325
323
|
.slice(0, limit)
|
|
326
324
|
.map(({ id, title, snippet }) => ({ id, title, snippet }));
|
|
327
325
|
},
|
|
328
|
-
index: indexText,
|
|
326
|
+
index: () => indexText("ids"),
|
|
329
327
|
async recentLog(limit = 10) {
|
|
330
328
|
const text = await readFile(path.join(knowledgeDir, "log.md"), "utf8").catch(() => "");
|
|
331
329
|
// Entries start with "## [date] operation | what"; the newest are at the bottom.
|
|
@@ -345,7 +343,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
345
343
|
const reservedFiles = new Set([...RESERVED].map((name) => path.normalize(path.join(knowledgeDir, `${name}.md`))));
|
|
346
344
|
const known = new Map(pages.map((p) => [p.id, p]));
|
|
347
345
|
const inbound = new Map();
|
|
348
|
-
const report = { brokenLinks: [], orphans: [], linksToRetired: []
|
|
346
|
+
const report = { brokenLinks: [], orphans: [], linksToRetired: [] };
|
|
349
347
|
for (const page of pages) {
|
|
350
348
|
if (statusOf(page) === "retired")
|
|
351
349
|
continue;
|
|
@@ -370,13 +368,9 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
|
|
|
370
368
|
if (id !== page.id)
|
|
371
369
|
inbound.set(id, (inbound.get(id) ?? 0) + 1);
|
|
372
370
|
}
|
|
373
|
-
const file = page.fields.get("file");
|
|
374
|
-
if (file && !/^[a-z]+:\/\//i.test(file) && !(await stat(path.resolve(path.dirname(page.file), file)).catch(() => null))) {
|
|
375
|
-
report.missingOriginals.push({ page: page.id, file });
|
|
376
|
-
}
|
|
377
371
|
}
|
|
378
372
|
report.orphans = pages
|
|
379
|
-
.filter((p) => statusOf(p) !== "retired" &&
|
|
373
|
+
.filter((p) => statusOf(p) !== "retired" && !["summary", "synthesis", "preference"].includes(p.type.type) && !inbound.get(p.id))
|
|
380
374
|
.map((p) => p.id);
|
|
381
375
|
return report;
|
|
382
376
|
},
|
|
@@ -24,17 +24,32 @@ export interface FileScope {
|
|
|
24
24
|
dir: string;
|
|
25
25
|
instead: string;
|
|
26
26
|
}[];
|
|
27
|
+
/**
|
|
28
|
+
* With it, `Read` and `Glob` are only allowed inside these (an allow-list, like
|
|
29
|
+
* `writableDirs` and `searchableDirs`), plus `alsoReadable` and the tool results under
|
|
30
|
+
* `toolResultsRoot`; `deniedPaths` still wins. Without it, they reach any path but the
|
|
31
|
+
* denied ones: any file the user can read, `~/.ssh` included.
|
|
32
|
+
*/
|
|
33
|
+
readableDirs?: string[];
|
|
34
|
+
/** Readable too with `readableDirs`, but not named in a denial: the run folder, the plugin roots. */
|
|
35
|
+
alsoReadable?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* The CLI's folder for this project (`~/.claude/projects/<project>`): with `readableDirs`,
|
|
38
|
+
* its sessions' `tool-results/` folders, where the SDK leaves large tool outputs for the
|
|
39
|
+
* agent to `Read`, are readable; nothing else in it.
|
|
40
|
+
*/
|
|
41
|
+
toolResultsRoot?: string;
|
|
27
42
|
}
|
|
28
43
|
/** Whether `candidate` is `base` itself or somewhere inside it (case-insensitive on Windows, like path.relative). */
|
|
29
44
|
export declare function isWithin(base: string, candidate: string): boolean;
|
|
30
45
|
/**
|
|
31
46
|
* The reason to deny `toolName` with `input` under `scope`, or `undefined` to let it
|
|
32
|
-
* through. Tools other than Read/Write/Edit/Grep are never judged here, and a call
|
|
47
|
+
* through. Tools other than Read/Write/Edit/Grep/Glob are never judged here, and a call
|
|
33
48
|
* without a path is left for the tool itself to reject.
|
|
34
49
|
*/
|
|
35
50
|
export declare function checkFileScope(scope: FileScope, toolName: string, input: Record<string, unknown>): string | undefined;
|
|
36
51
|
/**
|
|
37
|
-
* PreToolUse hook enforcing `scope` on Read/Write/Edit/Grep, for the main agent and its
|
|
52
|
+
* PreToolUse hook enforcing `scope` on Read/Write/Edit/Grep/Glob, for the main agent and its
|
|
38
53
|
* subagents alike. Bash, when a subagent has it, is not covered: it can reach any path,
|
|
39
54
|
* which is why Bash-granting features stay opt-in.
|
|
40
55
|
*/
|
|
@@ -5,11 +5,33 @@ export function isWithin(base, candidate) {
|
|
|
5
5
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
6
6
|
}
|
|
7
7
|
function describeDirs(scope, dirs) {
|
|
8
|
-
return dirs.map((dir) => `${path.relative(scope.projectDir, dir) || "."}/`).join(", ");
|
|
8
|
+
return dirs.map((dir) => (isWithin(scope.projectDir, dir) ? `${path.relative(scope.projectDir, dir) || "."}/` : `${dir}/`)).join(", ");
|
|
9
|
+
}
|
|
10
|
+
/** Where a `Glob` really starts: its path plus the pattern's fixed folders ("sources/**" starts in sources/). */
|
|
11
|
+
function globStart(scope, input) {
|
|
12
|
+
const target = typeof input.path === "string" && input.path !== "" ? path.resolve(scope.projectDir, input.path) : scope.projectDir;
|
|
13
|
+
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
14
|
+
return path.resolve(target, pattern.replace(/[^/\\]*[*?[{].*$/, ""));
|
|
15
|
+
}
|
|
16
|
+
/** Whether `target` is readable under a scope with `readableDirs`. */
|
|
17
|
+
function isReadable(scope, target) {
|
|
18
|
+
const dirs = [...(scope.readableDirs ?? []), ...(scope.alsoReadable ?? [])].map((dir) => path.resolve(scope.projectDir, dir));
|
|
19
|
+
if (dirs.some((dir) => isWithin(dir, target)))
|
|
20
|
+
return true;
|
|
21
|
+
if (!scope.toolResultsRoot || !isWithin(scope.toolResultsRoot, target))
|
|
22
|
+
return false;
|
|
23
|
+
// <root>/<session>/tool-results/…, and nothing else of the CLI's (transcripts, other files).
|
|
24
|
+
return path.relative(scope.toolResultsRoot, target).split(path.sep)[1] === "tool-results";
|
|
25
|
+
}
|
|
26
|
+
/** The denial for reading or listing outside `readableDirs`. */
|
|
27
|
+
function notReadable(scope, shown, tool) {
|
|
28
|
+
const dirs = scope.readableDirs ?? [];
|
|
29
|
+
const what = tool === "Glob" ? `Glob can't list "${String(shown)}": listing` : `"${String(shown)}" can't be read: reading`;
|
|
30
|
+
return `${what} is only allowed inside ${dirs.length ? `${describeDirs(scope, dirs)} and ` : ""}your own plugins and run folder.`;
|
|
9
31
|
}
|
|
10
32
|
/**
|
|
11
33
|
* The reason to deny `toolName` with `input` under `scope`, or `undefined` to let it
|
|
12
|
-
* through. Tools other than Read/Write/Edit/Grep are never judged here, and a call
|
|
34
|
+
* through. Tools other than Read/Write/Edit/Grep/Glob are never judged here, and a call
|
|
13
35
|
* without a path is left for the tool itself to reject.
|
|
14
36
|
*/
|
|
15
37
|
export function checkFileScope(scope, toolName, input) {
|
|
@@ -26,19 +48,32 @@ export function checkFileScope(scope, toolName, input) {
|
|
|
26
48
|
return viaTools(input.file_path, only);
|
|
27
49
|
}
|
|
28
50
|
if (toolName === "Grep" || toolName === "Glob") {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// ("sources/**" starts in sources/). From above the folder it would reach into it.
|
|
33
|
-
const start = toolName === "Glob" ? path.resolve(target, pattern.replace(/[^/\\]*[*?[{].*$/, "")) : target;
|
|
51
|
+
// Where a search really starts: Grep's path, Glob's path plus the pattern's fixed folders.
|
|
52
|
+
// From above the folder it would reach into it.
|
|
53
|
+
const start = toolName === "Glob" ? globStart(scope, input) : (resolve(input.path) ?? scope.projectDir);
|
|
34
54
|
const only = toolOnly(start) ?? toolOnlyDirs.find(({ dir }) => isWithin(start, dir));
|
|
35
55
|
if (only)
|
|
36
|
-
return viaTools(input.path ?? pattern, only);
|
|
56
|
+
return viaTools(input.path ?? input.pattern ?? "", only);
|
|
37
57
|
}
|
|
38
58
|
switch (toolName) {
|
|
39
59
|
case "Read": {
|
|
40
60
|
const target = resolve(input.file_path);
|
|
41
|
-
|
|
61
|
+
if (!target)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (isDenied(target))
|
|
64
|
+
return `"${input.file_path}" is off limits: it can't be read.`;
|
|
65
|
+
return scope.readableDirs && !isReadable(scope, target) ? notReadable(scope, input.file_path, "Read") : undefined;
|
|
66
|
+
}
|
|
67
|
+
case "Glob": {
|
|
68
|
+
const start = globStart(scope, input);
|
|
69
|
+
const shown = input.path ?? input.pattern;
|
|
70
|
+
if (isDenied(start))
|
|
71
|
+
return `"${String(shown)}" is off limits: it can't be listed.`;
|
|
72
|
+
if (!scope.readableDirs)
|
|
73
|
+
return undefined;
|
|
74
|
+
// ".." in the pattern ("**/../../x") would climb out of where the search starts.
|
|
75
|
+
const climbs = typeof input.pattern === "string" && input.pattern.split(/[/\\]/).includes("..");
|
|
76
|
+
return climbs || !isReadable(scope, start) ? notReadable(scope, shown, "Glob") : undefined;
|
|
42
77
|
}
|
|
43
78
|
case "Write":
|
|
44
79
|
case "Edit": {
|
|
@@ -66,7 +101,7 @@ export function checkFileScope(scope, toolName, input) {
|
|
|
66
101
|
}
|
|
67
102
|
}
|
|
68
103
|
/**
|
|
69
|
-
* PreToolUse hook enforcing `scope` on Read/Write/Edit/Grep, for the main agent and its
|
|
104
|
+
* PreToolUse hook enforcing `scope` on Read/Write/Edit/Grep/Glob, for the main agent and its
|
|
70
105
|
* subagents alike. Bash, when a subagent has it, is not covered: it can reach any path,
|
|
71
106
|
* which is why Bash-granting features stay opt-in.
|
|
72
107
|
*/
|