@falkenslab/agent-kit 0.15.0 → 0.16.1
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 -1
- package/assets/knowledge-plugin/.claude-plugin/plugin.json +1 -1
- package/assets/knowledge-plugin/commands/ingest.md +1 -1
- package/assets/knowledge-plugin/commands/lint.md +1 -1
- package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +13 -19
- package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +18 -14
- package/assets/knowledge-plugin/skills/knowledge-query/SKILL.md +5 -5
- package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +4 -0
- package/assets/knowledge-plugin-files/commands/ingest.md +7 -0
- package/assets/knowledge-plugin-files/commands/lint.md +5 -0
- package/assets/knowledge-plugin-files/commands/query.md +5 -0
- package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +46 -0
- package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +26 -0
- package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +14 -0
- package/dist/core/agentSpec.d.ts +27 -2
- package/dist/core/extractText.d.ts +13 -0
- package/dist/core/extractText.js +147 -0
- package/dist/core/fileKnowledgeStore.d.ts +10 -0
- package/dist/core/fileKnowledgeStore.js +395 -0
- package/dist/core/hooks/fileScopeGate.d.ts +8 -0
- package/dist/core/hooks/fileScopeGate.js +20 -0
- package/dist/core/hooks/humanInput.d.ts +18 -0
- package/dist/core/hooks/humanInput.js +30 -2
- package/dist/core/hooks/planGate.js +14 -5
- package/dist/core/hooks/stepGate.js +6 -0
- package/dist/core/interaction.d.ts +20 -0
- package/dist/core/knowledge.d.ts +14 -4
- package/dist/core/knowledge.js +36 -6
- package/dist/core/knowledgeStore.d.ts +139 -0
- package/dist/core/knowledgeStore.js +84 -0
- package/dist/core/messages/de.js +65 -1
- package/dist/core/messages/en.d.ts +39 -0
- package/dist/core/messages/en.js +65 -1
- package/dist/core/messages/es.js +65 -1
- package/dist/core/messages/fr.js +65 -1
- package/dist/core/modeControl.d.ts +36 -0
- package/dist/core/modeControl.js +61 -0
- package/dist/core/session.d.ts +6 -31
- package/dist/core/session.js +81 -71
- package/dist/core/sources.d.ts +122 -0
- package/dist/core/sources.js +312 -0
- package/dist/core/todos.d.ts +23 -0
- package/dist/core/todos.js +33 -0
- package/dist/core/toolLabels.d.ts +1 -1
- package/dist/core/toolLabels.js +42 -2
- package/dist/core/tools/humanApproval.d.ts +9 -5
- package/dist/core/tools/humanApproval.js +39 -7
- package/dist/core/tools/knowledgeTools.d.ts +16 -0
- package/dist/core/tools/knowledgeTools.js +168 -0
- package/dist/core/tools/saveToSources.d.ts +27 -8
- package/dist/core/tools/saveToSources.js +217 -55
- package/dist/core/tools/time.d.ts +29 -0
- package/dist/core/tools/time.js +129 -0
- package/dist/core/version.d.ts +5 -0
- package/dist/core/version.js +12 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -1
- package/dist/tui/chatTui.js +14 -1
- package/dist/tui/consoleRenderer.js +16 -2
- package/dist/tui/ink/SessionView.js +67 -17
- package/dist/tui/ink/inkInteraction.d.ts +4 -2
- package/dist/tui/ink/inkInteraction.js +20 -2
- package/dist/tui/ink/runChatInk.js +5 -0
- package/dist/tui/ink/sessionModel.d.ts +3 -0
- package/dist/tui/ink/sessionModel.js +14 -1
- package/dist/tui/terminalInteraction.js +2 -0
- package/package.json +31 -1
- /package/assets/{knowledge-plugin → knowledge-plugin-files}/skills/knowledge-pages/SKILL.md +0 -0
package/dist/core/messages/fr.js
CHANGED
|
@@ -54,6 +54,29 @@ export const fr = {
|
|
|
54
54
|
parametersLine: (parameters) => `Paramètres : ${parameters}`,
|
|
55
55
|
approvalTitle: "Confirmation humaine requise avant de publier",
|
|
56
56
|
summaryLine: (summary) => `Résumé : ${summary}`,
|
|
57
|
+
noAnswer: "(pas de réponse)",
|
|
58
|
+
textAnswerHint: "tapez la réponse",
|
|
59
|
+
textAnswerKeys: "Entrée pour envoyer · Échap pour ne pas répondre",
|
|
60
|
+
fileRequestTitle: "L'agent demande un fichier",
|
|
61
|
+
fileRequestQuestion: "Chemin du fichier (glissez-le ici), ou Entrée si vous ne l'avez pas : ",
|
|
62
|
+
retireTitle: "Retirer une source ?",
|
|
63
|
+
retirePageTitle: "Retirer une page de la base de connaissances ?",
|
|
64
|
+
choiceQuestion: (multiple) => (multiple ? "Numéros séparés par des virgules, ou votre propre réponse : " : "Numéro, ou votre propre réponse : "),
|
|
65
|
+
otherOption: "Autre (tapez votre réponse)",
|
|
66
|
+
choiceKeys: (multiple) => (multiple ? "Espace pour cocher · Entrée pour envoyer" : "↑/↓ choisir · Entrée pour envoyer"),
|
|
67
|
+
questionTitle: "L'agent demande",
|
|
68
|
+
planTitle: "Le plan, pour votre approbation",
|
|
69
|
+
runPlan: "L'exécuter",
|
|
70
|
+
keepPlanning: "Continuer à planifier",
|
|
71
|
+
cancelPlan: "Annuler",
|
|
72
|
+
planCommentQuestion: "Que faut-il changer ? (Entrée pour passer) : ",
|
|
73
|
+
retireLines: (source, why, reason, replacedBy, folder) => [
|
|
74
|
+
`Source : ${source}`,
|
|
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
|
+
`Raison : ${reason}`,
|
|
77
|
+
...(replacedBy ? [`Remplacée par : ${replacedBy}`] : []),
|
|
78
|
+
`Elle est déplacée dans ${folder}.agent-kit/retired/, pas supprimée.`,
|
|
79
|
+
],
|
|
57
80
|
toolPhrases: {
|
|
58
81
|
Read: ["a lu {n} fichier", "a lu {n} fichiers"],
|
|
59
82
|
Write: ["a écrit {n} fichier", "a écrit {n} fichiers"],
|
|
@@ -66,6 +89,26 @@ export const fr = {
|
|
|
66
89
|
Skill: ["a utilisé {n} skill", "a utilisé {n} skills"],
|
|
67
90
|
Agent: ["a lancé {n} sous-agent", "a lancé {n} sous-agents"],
|
|
68
91
|
Task: ["a lancé {n} sous-agent", "a lancé {n} sous-agents"],
|
|
92
|
+
mcp__time__current_time: ["a consulté l'heure", "a consulté l'heure {n} fois"],
|
|
93
|
+
mcp__time__date_math: ["a calculé des dates", "a calculé des dates {n} fois"],
|
|
94
|
+
mcp__sourceFiles__list_sources: ["a examiné les sources", "a examiné les sources {n} fois"],
|
|
95
|
+
mcp__sourceFiles__extract_text: ["a lu {n} document", "a lu {n} documents"],
|
|
96
|
+
mcp__sourceFiles__save_to_sources: ["a enregistré {n} source", "a enregistré {n} sources"],
|
|
97
|
+
mcp__sourceFiles__download_to_sources: ["a téléchargé {n} source", "a téléchargé {n} sources"],
|
|
98
|
+
mcp__sourceFiles__request_file: ["a demandé {n} fichier", "a demandé {n} fichiers"],
|
|
99
|
+
mcp__sourceFiles__retire_source: ["a retiré {n} source", "a retiré {n} sources"],
|
|
100
|
+
mcp__knowledge__knowledge_index: ["a lu l'index", "a lu l'index {n} fois"],
|
|
101
|
+
mcp__knowledge__knowledge_search: ["a cherché dans la base", "a cherché dans la base {n} fois"],
|
|
102
|
+
mcp__knowledge__knowledge_read: ["a lu {n} page", "a lu {n} pages"],
|
|
103
|
+
mcp__knowledge__knowledge_create: ["a créé {n} page", "a créé {n} pages"],
|
|
104
|
+
mcp__knowledge__knowledge_edit: ["a modifié {n} page", "a modifié {n} pages"],
|
|
105
|
+
mcp__knowledge__knowledge_rewrite: ["a réécrit {n} page", "a réécrit {n} pages"],
|
|
106
|
+
mcp__knowledge__knowledge_supersede: ["a remplacé {n} page", "a remplacé {n} pages"],
|
|
107
|
+
mcp__knowledge__knowledge_retire: ["a retiré {n} page", "a retiré {n} pages"],
|
|
108
|
+
mcp__knowledge__knowledge_log: ["a mis à jour le journal", "a mis à jour le journal {n} fois"],
|
|
109
|
+
mcp__knowledge__knowledge_check: ["a vérifié la base", "a vérifié la base {n} fois"],
|
|
110
|
+
mcp__approvals__ask_human: ["a posé {n} question", "a posé {n} questions"],
|
|
111
|
+
mcp__approvals__present_plan: ["a présenté le plan", "a présenté le plan {n} fois"],
|
|
69
112
|
},
|
|
70
113
|
otherTools: ["a utilisé {n} outil", "a utilisé {n} outils"],
|
|
71
114
|
noOutput: "(aucune sortie)",
|
|
@@ -73,10 +116,13 @@ export const fr = {
|
|
|
73
116
|
toolFailed: "Impossible de terminer",
|
|
74
117
|
moreResultLines: (count) => `(+${count} ${plural(count, "ligne", "lignes")})`,
|
|
75
118
|
earlierCalls: (count) => `… ${count} ${plural(count, "précédent", "précédents")}`,
|
|
119
|
+
moreTodos: (count) => `… ${count} ${plural(count, "tâche", "tâches")} de plus`,
|
|
76
120
|
labels: {
|
|
77
121
|
askingApproval: (summary) => `Demande d'approbation humaine : ${summary}`,
|
|
78
122
|
waitingManual: "En attente d'une intervention manuelle",
|
|
79
|
-
savingToSources: (destination) => `Enregistrement
|
|
123
|
+
savingToSources: (destination) => `Enregistrement de ${destination} dans les sources`,
|
|
124
|
+
checkingTime: "Consultation de la date et de l'heure",
|
|
125
|
+
calculatingDates: "Calcul de dates",
|
|
80
126
|
reading: (file) => `Lecture de ${file}`,
|
|
81
127
|
writing: (file) => `Écriture dans ${file}`,
|
|
82
128
|
editing: (file) => `Modification de ${file}`,
|
|
@@ -92,6 +138,24 @@ export const fr = {
|
|
|
92
138
|
searchingWeb: (query) => `Recherche sur le web de "${query}"`,
|
|
93
139
|
applyingSkill: (skill) => `Application de la skill "${skill}"`,
|
|
94
140
|
aSkill: "une skill",
|
|
141
|
+
updatingTasks: "Mise à jour de la liste des tâches",
|
|
142
|
+
listingSources: "Examen des sources",
|
|
143
|
+
extractingText: (source) => `Lecture de ${source}`,
|
|
144
|
+
downloading: (url) => `Téléchargement de ${url} dans les sources`,
|
|
145
|
+
requestingFile: (description) => `Demande d'un fichier : ${description}`,
|
|
146
|
+
retiringSource: (source) => `Retrait de la source ${source}`,
|
|
147
|
+
knowledgeIndex: "Lecture de l'index de la base de connaissances",
|
|
148
|
+
knowledgeSearch: (query) => `Recherche de "${query}" dans la base de connaissances`,
|
|
149
|
+
knowledgeRead: (page) => `Lecture de ${page}`,
|
|
150
|
+
knowledgeCreate: (page) => `Création de ${page}`,
|
|
151
|
+
knowledgeEdit: (page) => `Modification de ${page}`,
|
|
152
|
+
knowledgeRewrite: (page) => `Réécriture de ${page}`,
|
|
153
|
+
knowledgeSupersede: (page) => `${page} marquée comme remplacée`,
|
|
154
|
+
knowledgeRetire: (page) => `Retrait de ${page}`,
|
|
155
|
+
knowledgeLog: "Mise à jour du journal de la base de connaissances",
|
|
156
|
+
knowledgeCheck: "Vérification de la base de connaissances",
|
|
157
|
+
askingHuman: (question) => `Question : ${question}`,
|
|
158
|
+
presentingPlan: "Présentation du plan",
|
|
95
159
|
},
|
|
96
160
|
auth: {
|
|
97
161
|
noToken: "Aucun jeton d'authentification Claude n'a été trouvé",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Mode } from "./agentSpec.js";
|
|
2
|
+
/**
|
|
3
|
+
* The supervision mode of a running session. "guided", "interactive" and "plan" switch into
|
|
4
|
+
* one another: they share the same tools (the approval tool), and the step gate and the plan
|
|
5
|
+
* gate are registered in all three, deciding only in their own mode. "autonomous" has no
|
|
6
|
+
* approval tool at all, and a tool can't appear or vanish mid-session, so a session that
|
|
7
|
+
* starts autonomous stays autonomous, and one that doesn't can't become autonomous. The
|
|
8
|
+
* system prompt keeps the mode it was built with, so entering or leaving plan mode is told
|
|
9
|
+
* to the model with the next message instead (`takeNotice()`).
|
|
10
|
+
*/
|
|
11
|
+
export interface ModeControl {
|
|
12
|
+
readonly mode: Mode;
|
|
13
|
+
/** The modes this session can be in, in Shift+Tab's order; fewer than two means it can't switch. */
|
|
14
|
+
readonly switchable: readonly Mode[];
|
|
15
|
+
/** Switches to `next` if this session allows it; returns whether it did. */
|
|
16
|
+
set(next: Mode): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The note telling the model it entered or left plan mode, once per change it hasn't been
|
|
19
|
+
* told about yet; `undefined` otherwise. `createInputQueue({ modeControl })` puts it before
|
|
20
|
+
* the next message; a caller with its own queue prepends it itself.
|
|
21
|
+
*/
|
|
22
|
+
takeNotice?(): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Calls `listener` with the new mode whenever it changes (Shift+Tab, `/plan`, or the agent
|
|
25
|
+
* leaving plan mode through `present_plan`), so a UI can show it; returns the unsubscribe.
|
|
26
|
+
*/
|
|
27
|
+
subscribe?(listener: (mode: Mode) => void): () => void;
|
|
28
|
+
}
|
|
29
|
+
/** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
|
|
30
|
+
export declare function createModeControl(initial: Mode): ModeControl;
|
|
31
|
+
/**
|
|
32
|
+
* What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
|
|
33
|
+
* there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
|
|
34
|
+
* the new mode, or `null` when this session can't be in plan mode (an autonomous one).
|
|
35
|
+
*/
|
|
36
|
+
export declare function togglePlanMode(control: ModeControl): Mode | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
|
|
2
|
+
export function createModeControl(initial) {
|
|
3
|
+
let current = initial;
|
|
4
|
+
// The mode the model was last told about: a session starting in plan mode tells it with
|
|
5
|
+
// the first message, since the system prompt doesn't say.
|
|
6
|
+
let told = initial === "plan" ? undefined : initial;
|
|
7
|
+
const switchable = initial === "autonomous" ? ["autonomous"] : ["guided", "interactive", "plan"];
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
return {
|
|
10
|
+
get mode() {
|
|
11
|
+
return current;
|
|
12
|
+
},
|
|
13
|
+
switchable,
|
|
14
|
+
set(next) {
|
|
15
|
+
if (!switchable.includes(next))
|
|
16
|
+
return false;
|
|
17
|
+
const changed = next !== current;
|
|
18
|
+
current = next;
|
|
19
|
+
if (changed)
|
|
20
|
+
for (const listener of listeners)
|
|
21
|
+
listener(next);
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
takeNotice() {
|
|
25
|
+
const before = told;
|
|
26
|
+
told = current;
|
|
27
|
+
if (current === "plan" && before !== "plan")
|
|
28
|
+
return PLAN_MODE_ENTERED;
|
|
29
|
+
if (current !== "plan" && before === "plan")
|
|
30
|
+
return PLAN_MODE_LEFT;
|
|
31
|
+
return undefined;
|
|
32
|
+
},
|
|
33
|
+
subscribe(listener) {
|
|
34
|
+
listeners.add(listener);
|
|
35
|
+
return () => listeners.delete(listener);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// The mode each session was in when `/plan` took it into plan mode, to go back to.
|
|
40
|
+
const modeBeforePlan = new WeakMap();
|
|
41
|
+
/**
|
|
42
|
+
* What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
|
|
43
|
+
* there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
|
|
44
|
+
* the new mode, or `null` when this session can't be in plan mode (an autonomous one).
|
|
45
|
+
*/
|
|
46
|
+
export function togglePlanMode(control) {
|
|
47
|
+
if (!control.switchable.includes("plan"))
|
|
48
|
+
return null;
|
|
49
|
+
if (control.mode === "plan") {
|
|
50
|
+
const back = modeBeforePlan.get(control) ?? "guided";
|
|
51
|
+
control.set(back);
|
|
52
|
+
return back;
|
|
53
|
+
}
|
|
54
|
+
modeBeforePlan.set(control, control.mode);
|
|
55
|
+
control.set("plan");
|
|
56
|
+
return "plan";
|
|
57
|
+
}
|
|
58
|
+
// What the model reads on a plan-mode switch, in English like the rest of what it reads
|
|
59
|
+
// (ADR-019); tagged so a resumed conversation doesn't show it as the human's words (runs.ts).
|
|
60
|
+
const PLAN_MODE_ENTERED = "<system-reminder>The user switched to plan mode. Only read, research and plan: nothing can be changed (files, tools that modify something) until they switch out of it. When the plan is ready, present it and wait for them.</system-reminder>";
|
|
61
|
+
const PLAN_MODE_LEFT = "<system-reminder>The user left plan mode: you can carry out the plan now.</system-reminder>";
|
package/dist/core/session.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { type TranscriptLogger } from "./hooks/transcriptLogger.js";
|
|
3
|
-
import
|
|
3
|
+
import { type ModeControl } from "./modeControl.js";
|
|
4
|
+
import type { KnowledgeStore } from "./knowledgeStore.js";
|
|
5
|
+
import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
|
|
4
6
|
import { type Language } from "./language.js";
|
|
5
7
|
import { type RunFolder } from "./runs.js";
|
|
6
8
|
/**
|
|
@@ -31,37 +33,10 @@ export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(c
|
|
|
31
33
|
transcriptPath: string;
|
|
32
34
|
modeControl: ModeControl;
|
|
33
35
|
language: Language;
|
|
36
|
+
/** The knowledge base's store, when the agent reaches it through the `knowledge_*` tools. */
|
|
37
|
+
knowledgeStore?: KnowledgeStore;
|
|
34
38
|
}>;
|
|
35
|
-
|
|
36
|
-
* The supervision mode of a running session. "guided", "interactive" and "plan" switch into
|
|
37
|
-
* one another: they share the same tools (the approval tool), and the step gate and the plan
|
|
38
|
-
* gate are registered in all three, deciding only in their own mode. "autonomous" has no
|
|
39
|
-
* approval tool at all, and a tool can't appear or vanish mid-session, so a session that
|
|
40
|
-
* starts autonomous stays autonomous, and one that doesn't can't become autonomous. The
|
|
41
|
-
* system prompt keeps the mode it was built with, so entering or leaving plan mode is told
|
|
42
|
-
* to the model with the next message instead (`takeNotice()`).
|
|
43
|
-
*/
|
|
44
|
-
export interface ModeControl {
|
|
45
|
-
readonly mode: Mode;
|
|
46
|
-
/** The modes this session can be in, in Shift+Tab's order; fewer than two means it can't switch. */
|
|
47
|
-
readonly switchable: readonly Mode[];
|
|
48
|
-
/** Switches to `next` if this session allows it; returns whether it did. */
|
|
49
|
-
set(next: Mode): boolean;
|
|
50
|
-
/**
|
|
51
|
-
* The note telling the model it entered or left plan mode, once per change it hasn't been
|
|
52
|
-
* told about yet; `undefined` otherwise. `createInputQueue({ modeControl })` puts it before
|
|
53
|
-
* the next message; a caller with its own queue prepends it itself.
|
|
54
|
-
*/
|
|
55
|
-
takeNotice?(): string | undefined;
|
|
56
|
-
}
|
|
57
|
-
/** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
|
|
58
|
-
export declare function createModeControl(initial: Mode): ModeControl;
|
|
59
|
-
/**
|
|
60
|
-
* What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
|
|
61
|
-
* there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
|
|
62
|
-
* the new mode, or `null` when this session can't be in plan mode (an autonomous one).
|
|
63
|
-
*/
|
|
64
|
-
export declare function togglePlanMode(control: ModeControl): Mode | null;
|
|
39
|
+
export { createModeControl, togglePlanMode, type ModeControl } from "./modeControl.js";
|
|
65
40
|
/**
|
|
66
41
|
* User message queue backed by a single long-lived generator, for any multi-turn
|
|
67
42
|
* (chat-shaped) caller of `query()`.
|
package/dist/core/session.js
CHANGED
|
@@ -9,6 +9,11 @@ import { createPlanGate } from "./hooks/planGate.js";
|
|
|
9
9
|
import { createHumanApprovalServer } from "./tools/humanApproval.js";
|
|
10
10
|
import { createManualLoginServer } from "./tools/manualLogin.js";
|
|
11
11
|
import { createSaveToSourcesServer } from "./tools/saveToSources.js";
|
|
12
|
+
import { createTimeServer } from "./tools/time.js";
|
|
13
|
+
import { TODO_TOOL } from "./todos.js";
|
|
14
|
+
import { createModeControl } from "./modeControl.js";
|
|
15
|
+
import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
|
|
16
|
+
import { createKnowledgeServer } from "./tools/knowledgeTools.js";
|
|
12
17
|
import { allowAnyMcpTool } from "./mcpPermissions.js";
|
|
13
18
|
import { knowledgePluginRoot, knowledgePromptSection } from "./knowledge.js";
|
|
14
19
|
import { replyLanguageInstruction } from "./language.js";
|
|
@@ -53,24 +58,38 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
53
58
|
// (`knowledgeDir`) or for original files (`sourcesDir`) — there is no separate "context"
|
|
54
59
|
// folder: material the user provides is just more of `sourcesDir`.
|
|
55
60
|
const includeFileTools = Boolean(config.knowledgeDir || config.sourcesDir);
|
|
56
|
-
|
|
61
|
+
// The built-in knowledge base (rules in the system prompt + its skills/commands as a plugin) needs
|
|
62
|
+
// somewhere to keep the wiki, so it follows `knowledgeDir`; `spec.knowledgeBase: false` opts out.
|
|
63
|
+
const includeKnowledgeBase = Boolean(config.knowledgeDir) && spec.knowledgeBase !== false;
|
|
64
|
+
// By default the agent reaches it only through the `knowledge_*` tools, over a store, never
|
|
65
|
+
// with the file tools (ADR-024); `spec.knowledgeTools: "files"` keeps the file tools on it.
|
|
66
|
+
const knowledgeViaStore = includeKnowledgeBase && (spec.knowledgeTools ?? "store") === "store";
|
|
67
|
+
const notesDir = knowledgeViaStore ? undefined : config.knowledgeDir;
|
|
68
|
+
const extraDirs = config.extraWritableDirs ?? [];
|
|
69
|
+
// With the store, the file tools are only for the originals (reading) and the extra writable
|
|
70
|
+
// folders; without either, none.
|
|
71
|
+
const fileTools = !includeFileTools
|
|
72
|
+
? []
|
|
73
|
+
: !knowledgeViaStore
|
|
74
|
+
? ["Read", "Write", "Edit", "Glob", "Grep"]
|
|
75
|
+
: [...(config.sourcesDir || extraDirs.length ? ["Read", "Glob", "Grep"] : []), ...(extraDirs.length ? ["Write", "Edit"] : [])];
|
|
57
76
|
// Where Write/Edit may act and Grep may search (see hooks/fileScopeGate.ts): the
|
|
58
77
|
// project's own folders, never the whole cwd — which would include whatever else the
|
|
59
78
|
// project directory holds (the user's config...). `sourcesDir` is searchable but
|
|
60
79
|
// deliberately NOT writable: originals stay as obtained, and the only way to add to it is
|
|
61
80
|
// the `save_to_sources` tool (which never overwrites).
|
|
62
|
-
const writableDirs = [
|
|
63
|
-
const searchableDirs = [
|
|
81
|
+
const writableDirs = [notesDir, ...extraDirs].filter((d) => Boolean(d));
|
|
82
|
+
const searchableDirs = [notesDir, config.sourcesDir, ...extraDirs].filter((d) => Boolean(d));
|
|
64
83
|
const readOnlyDirs = config.sourcesDir ? [config.sourcesDir] : [];
|
|
65
84
|
// Skills/commands/plugins are a separate concern from the file tools: an agent that
|
|
66
85
|
// wants a plugin-provided skill/command but has no notes or sources folder shouldn't have
|
|
67
86
|
// to invent one just to get `cwd`/`skills: "all"`/`plugins` wired up. Gated on either
|
|
68
87
|
// signal, not on `pluginRoots` alone, so an agent with file tools keeps getting the SDK's
|
|
69
88
|
// own project-level `.claude/skills`/`.claude/commands` discovery.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
const pluginRoots = [...spec.pluginRoots(config), ...(includeKnowledgeBase ? [knowledgePluginRoot(knowledgeViaStore ? "tools" : "files")] : [])];
|
|
90
|
+
const knowledgeStore = knowledgeViaStore && config.knowledgeDir
|
|
91
|
+
? (spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(config.knowledgeDir, { pageTypes: spec.knowledgePageTypes, sourcesDir: config.sourcesDir }))
|
|
92
|
+
: undefined;
|
|
74
93
|
const includeSkillsAndPlugins = includeFileTools || pluginRoots.length > 0;
|
|
75
94
|
const skillTools = includeSkillsAndPlugins ? ["Skill"] : [];
|
|
76
95
|
// Whatever opt-in subagents `spec` wants for this config, or undefined if none apply.
|
|
@@ -97,7 +116,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
97
116
|
const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
|
|
98
117
|
const sdkOptions = {
|
|
99
118
|
systemPrompt: withReplyLine(includeKnowledgeBase && config.knowledgeDir
|
|
100
|
-
? `${spec.buildSystemPrompt(config)}\n\n${knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir)}`
|
|
119
|
+
? `${spec.buildSystemPrompt(config)}\n\n${knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir, { tools: knowledgeViaStore, pageTypes: knowledgeStore?.types() })}`
|
|
101
120
|
: spec.buildSystemPrompt(config)),
|
|
102
121
|
// Not the SDK's default (every source): "user" hands the agent the runner's own Claude
|
|
103
122
|
// Code configuration, and its `language` setting outranked the agent's prompt.
|
|
@@ -118,8 +137,10 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
118
137
|
// "Skill" has to be listed explicitly: with an explicit `tools` list the SDK loads the
|
|
119
138
|
// skills (they show up in the init message) but doesn't offer the tool to invoke them,
|
|
120
139
|
// so the model ends up looking for SKILL.md files by hand (confirmed empirically).
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
// TodoWrite: the SDK's task list for long jobs, in every session (the chats show the list
|
|
141
|
+
// instead of the calls; see todos.ts). Accepted in an explicit list (confirmed empirically).
|
|
142
|
+
tools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
|
|
143
|
+
allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
|
|
123
144
|
// Every mcp__* tool (whatever spec.buildMcpServers() registers, approvals/manualLogin
|
|
124
145
|
// when enabled below, sourceFiles, and any server a project's own .mcp.json
|
|
125
146
|
// declares) is approved generically here rather than listed one by one — see
|
|
@@ -127,6 +148,11 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
127
148
|
// name isn't known ahead of time.
|
|
128
149
|
canUseTool: allowAnyMcpTool,
|
|
129
150
|
disallowedTools: spec.disallowedTools ?? [],
|
|
151
|
+
// TodoWrite (todos.ts) only exists with CLAUDE_CODE_ENABLE_TASKS=0: by default the CLI
|
|
152
|
+
// offers its TaskCreate/TaskUpdate/TaskList/TaskGet tools instead, and drops TodoWrite from
|
|
153
|
+
// the session even when `tools` names it (confirmed empirically). `env` replaces the
|
|
154
|
+
// subprocess environment, so the rest is passed on as it is.
|
|
155
|
+
env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" },
|
|
130
156
|
// Real text streaming instead of silently waiting for the turn's complete message —
|
|
131
157
|
// see the caller's message loop.
|
|
132
158
|
includePartialMessages: true,
|
|
@@ -145,7 +171,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
145
171
|
// SDK's own official skills (pdf/docx), the project's own custom ones, and the
|
|
146
172
|
// plugin-provided built-ins below. A spec's own list gets the knowledge base's
|
|
147
173
|
// skills added, so it only names its own.
|
|
148
|
-
skills: skillList(spec.skills, includeKnowledgeBase),
|
|
174
|
+
skills: skillList(spec.skills, includeKnowledgeBase ? (knowledgeViaStore ? "tools" : "files") : null),
|
|
149
175
|
plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
|
|
150
176
|
}
|
|
151
177
|
: {}),
|
|
@@ -154,15 +180,47 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
154
180
|
...(subagentDefinitions ? { agents: subagentDefinitions } : {}),
|
|
155
181
|
mcpServers: {
|
|
156
182
|
...spec.buildMcpServers(config, runDir),
|
|
157
|
-
...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts) } : {}),
|
|
183
|
+
...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts, { modeControl }) } : {}),
|
|
158
184
|
...(manualInterventionTexts ? { manualLogin: createManualLoginServer(runDir, manualInterventionTexts) } : {}),
|
|
159
|
-
|
|
185
|
+
// The sources folder's tools (tools/saveToSources.ts); asking a person (request_file,
|
|
186
|
+
// retire_source) only where there is one.
|
|
187
|
+
...(config.sourcesDir
|
|
188
|
+
? {
|
|
189
|
+
sourceFiles: createSaveToSourcesServer(runDir, config.sourcesDir, spec.saveToSourcesDescription, {
|
|
190
|
+
knowledgeDir: config.knowledgeDir,
|
|
191
|
+
projectDir: config.projectDir,
|
|
192
|
+
interactive: mode !== "autonomous",
|
|
193
|
+
}),
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
// The knowledge base's own tools, over its store (tools/knowledgeTools.ts, ADR-024).
|
|
197
|
+
...(knowledgeStore
|
|
198
|
+
? {
|
|
199
|
+
knowledge: createKnowledgeServer(knowledgeStore, {
|
|
200
|
+
runDir,
|
|
201
|
+
interactive: mode !== "autonomous",
|
|
202
|
+
sourcesDir: config.sourcesDir,
|
|
203
|
+
projectDir: config.projectDir,
|
|
204
|
+
knowledgeDir: config.knowledgeDir,
|
|
205
|
+
}),
|
|
206
|
+
}
|
|
207
|
+
: {}),
|
|
208
|
+
// The date and date arithmetic, in every session and mode: only reads (tools/time.ts).
|
|
209
|
+
time: createTimeServer(config.timeZone),
|
|
160
210
|
},
|
|
161
211
|
hooks: {
|
|
162
212
|
PreToolUse: [
|
|
163
213
|
{ hooks: [transcriptLogger.preToolUse] },
|
|
164
214
|
...(includeFileTools
|
|
165
|
-
? [{ hooks: [createFileScopeGate({
|
|
215
|
+
? [{ hooks: [createFileScopeGate({
|
|
216
|
+
projectDir: config.projectDir,
|
|
217
|
+
writableDirs,
|
|
218
|
+
searchableDirs,
|
|
219
|
+
readOnlyDirs,
|
|
220
|
+
deniedPaths: config.deniedPaths ?? [],
|
|
221
|
+
...(knowledgeViaStore && config.knowledgeDir ? { toolOnlyDirs: [{ dir: config.knowledgeDir, instead: "the knowledge_* tools" }] } : {}),
|
|
222
|
+
}),
|
|
223
|
+
] }]
|
|
166
224
|
: []),
|
|
167
225
|
// Registered wherever the approval tool is, and asking only while the current mode is
|
|
168
226
|
// "interactive": a guided session can switch to interactive and back (ModeControl).
|
|
@@ -193,67 +251,19 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
193
251
|
PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
|
|
194
252
|
},
|
|
195
253
|
};
|
|
196
|
-
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language };
|
|
254
|
+
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, ...(knowledgeStore ? { knowledgeStore } : {}) };
|
|
197
255
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
256
|
+
export { createModeControl, togglePlanMode } from "./modeControl.js";
|
|
257
|
+
/** The knowledge plugin's skills, as the SDK names a plugin's skills ("plugin:skill"): the tools variant has no page templates skill (knowledge_create gives them). */
|
|
258
|
+
const KNOWLEDGE_SKILLS = {
|
|
259
|
+
tools: ["knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
|
|
260
|
+
files: ["knowledge-pages", "knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
|
|
261
|
+
};
|
|
262
|
+
function skillList(skills, knowledgeVariant) {
|
|
201
263
|
if (skills === undefined || skills === "all")
|
|
202
264
|
return "all";
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
/** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
|
|
206
|
-
export function createModeControl(initial) {
|
|
207
|
-
let current = initial;
|
|
208
|
-
// The mode the model was last told about: a session starting in plan mode tells it with
|
|
209
|
-
// the first message, since the system prompt doesn't say.
|
|
210
|
-
let told = initial === "plan" ? undefined : initial;
|
|
211
|
-
const switchable = initial === "autonomous" ? ["autonomous"] : ["guided", "interactive", "plan"];
|
|
212
|
-
return {
|
|
213
|
-
get mode() {
|
|
214
|
-
return current;
|
|
215
|
-
},
|
|
216
|
-
switchable,
|
|
217
|
-
set(next) {
|
|
218
|
-
if (!switchable.includes(next))
|
|
219
|
-
return false;
|
|
220
|
-
current = next;
|
|
221
|
-
return true;
|
|
222
|
-
},
|
|
223
|
-
takeNotice() {
|
|
224
|
-
const before = told;
|
|
225
|
-
told = current;
|
|
226
|
-
if (current === "plan" && before !== "plan")
|
|
227
|
-
return PLAN_MODE_ENTERED;
|
|
228
|
-
if (current !== "plan" && before === "plan")
|
|
229
|
-
return PLAN_MODE_LEFT;
|
|
230
|
-
return undefined;
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
// The mode each session was in when `/plan` took it into plan mode, to go back to.
|
|
235
|
-
const modeBeforePlan = new WeakMap();
|
|
236
|
-
/**
|
|
237
|
-
* What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
|
|
238
|
-
* there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
|
|
239
|
-
* the new mode, or `null` when this session can't be in plan mode (an autonomous one).
|
|
240
|
-
*/
|
|
241
|
-
export function togglePlanMode(control) {
|
|
242
|
-
if (!control.switchable.includes("plan"))
|
|
243
|
-
return null;
|
|
244
|
-
if (control.mode === "plan") {
|
|
245
|
-
const back = modeBeforePlan.get(control) ?? "guided";
|
|
246
|
-
control.set(back);
|
|
247
|
-
return back;
|
|
248
|
-
}
|
|
249
|
-
modeBeforePlan.set(control, control.mode);
|
|
250
|
-
control.set("plan");
|
|
251
|
-
return "plan";
|
|
265
|
+
return knowledgeVariant ? [...new Set([...skills, ...KNOWLEDGE_SKILLS[knowledgeVariant]])] : skills;
|
|
252
266
|
}
|
|
253
|
-
// What the model reads on a plan-mode switch, in English like the rest of what it reads
|
|
254
|
-
// (ADR-019); tagged so a resumed conversation doesn't show it as the human's words (runs.ts).
|
|
255
|
-
const PLAN_MODE_ENTERED = "<system-reminder>The user switched to plan mode. Only read, research and plan: nothing can be changed (files, tools that modify something) until they switch out of it. When the plan is ready, present it and wait for them.</system-reminder>";
|
|
256
|
-
const PLAN_MODE_LEFT = "<system-reminder>The user left plan mode: you can carry out the plan now.</system-reminder>";
|
|
257
267
|
/**
|
|
258
268
|
* User message queue backed by a single long-lived generator, for any multi-turn
|
|
259
269
|
* (chat-shaped) caller of `query()`.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sources folder's bookkeeping: originals stay files (ADR-024), and the kit keeps a
|
|
3
|
+
* manifest next to them (`.agent-kit/sources.json`, inside `sourcesDir`, where the agent's
|
|
4
|
+
* file tools can't write) with each original's hash, size and provenance, the version it
|
|
5
|
+
* replaces, and the hash it had when a summary page first pointed to it. From that it tells
|
|
6
|
+
* new, ingested, changed and missing originals apart, refuses duplicates, and retires an
|
|
7
|
+
* original into `.agent-kit/retired/` instead of deleting it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const KIT_DIR = ".agent-kit";
|
|
10
|
+
/** Where an original came from. */
|
|
11
|
+
export type SourceOrigin = {
|
|
12
|
+
kind: "run";
|
|
13
|
+
from: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "url";
|
|
16
|
+
from: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "person";
|
|
19
|
+
from: string;
|
|
20
|
+
}
|
|
21
|
+
/** Copied in by hand: first seen by `listSources()`. */
|
|
22
|
+
| {
|
|
23
|
+
kind: "manual";
|
|
24
|
+
};
|
|
25
|
+
export interface SourceEntry {
|
|
26
|
+
hash: string;
|
|
27
|
+
size: number;
|
|
28
|
+
/** For hashing a file again only when it changed. */
|
|
29
|
+
mtimeMs: number;
|
|
30
|
+
addedAt: string;
|
|
31
|
+
origin: SourceOrigin;
|
|
32
|
+
/** The original this one is a new version of (a path relative to `sourcesDir`). */
|
|
33
|
+
replaces?: string;
|
|
34
|
+
/** The page this one was generated from (a web page's markdown, from its HTML). */
|
|
35
|
+
derivedFrom?: string;
|
|
36
|
+
/** The hash when a summary page first pointed to it: a different hash now means it changed after its ingest. */
|
|
37
|
+
ingestedHash?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface RetiredSource {
|
|
40
|
+
path: string;
|
|
41
|
+
movedTo: string;
|
|
42
|
+
retiredAt: string;
|
|
43
|
+
why: "wrong" | "replaced";
|
|
44
|
+
reason: string;
|
|
45
|
+
replacedBy?: string;
|
|
46
|
+
entry?: SourceEntry;
|
|
47
|
+
}
|
|
48
|
+
export interface SourceManifest {
|
|
49
|
+
version: 1;
|
|
50
|
+
files: Record<string, SourceEntry>;
|
|
51
|
+
retired: RetiredSource[];
|
|
52
|
+
}
|
|
53
|
+
export type SourceStatus = "new" | "ingested" | "changed" | "missing" | "present";
|
|
54
|
+
export interface SourceListing {
|
|
55
|
+
path: string;
|
|
56
|
+
status: SourceStatus;
|
|
57
|
+
type: string;
|
|
58
|
+
size?: number;
|
|
59
|
+
pages?: number;
|
|
60
|
+
slides?: number;
|
|
61
|
+
origin?: SourceOrigin;
|
|
62
|
+
replaces?: string;
|
|
63
|
+
/** The summary pages that point to it (paths relative to the project). */
|
|
64
|
+
summaries: string[];
|
|
65
|
+
}
|
|
66
|
+
export declare function hashFile(file: string): Promise<string>;
|
|
67
|
+
/** Resolves `candidate` against `base`; the absolute path only if it stays inside it. */
|
|
68
|
+
export declare function resolveWithin(base: string, candidate: string): string | undefined;
|
|
69
|
+
export declare function loadManifest(sourcesDir: string): Promise<SourceManifest>;
|
|
70
|
+
export declare function saveManifest(sourcesDir: string, manifest: SourceManifest): Promise<void>;
|
|
71
|
+
/** Every original under `sourcesDir` (paths relative to it), leaving out hidden files and folders (the kit's own among them). */
|
|
72
|
+
export declare function scanSources(sourcesDir: string): Promise<string[]>;
|
|
73
|
+
/** The summary pages (knowledge base layout: `summaries/*.md`) by the absolute path of the original their frontmatter `file:` points to. */
|
|
74
|
+
export declare function summariesByOriginal(knowledgeDir: string): Promise<Map<string, string[]>>;
|
|
75
|
+
/** Pages of a PDF, read from its page tree without a parser; undefined when it can't tell (e.g. compressed object streams). */
|
|
76
|
+
export declare function pdfPageCount(bytes: Buffer): number | undefined;
|
|
77
|
+
/** Slides of a PPTX, from the file names in the zip (stored uncompressed). */
|
|
78
|
+
export declare function pptxSlideCount(bytes: Buffer): number | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Every original with its status, recording in the manifest the ones copied in by hand and,
|
|
81
|
+
* the first time a summary page points to one, its hash at ingest. Without a knowledge
|
|
82
|
+
* folder there's no ingest to compare with: originals are "present".
|
|
83
|
+
*/
|
|
84
|
+
export declare function listSources(sourcesDir: string, knowledgeDir: string | undefined, projectDir: string): Promise<SourceListing[]>;
|
|
85
|
+
export interface AddedSource {
|
|
86
|
+
/** Where it is, relative to `sourcesDir`. */
|
|
87
|
+
path: string;
|
|
88
|
+
/** An identical file was already there: nothing was copied. */
|
|
89
|
+
duplicateOf?: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Adds `file` (an absolute path) to `sourcesDir` at `destination` (a folder: under `extra.name`,
|
|
93
|
+
* or the file's own name): never overwriting, and
|
|
94
|
+
* not at all if an identical file is already there (its path is returned). `replaces`
|
|
95
|
+
* records it as a new version of an original already in the folder.
|
|
96
|
+
*/
|
|
97
|
+
export declare function addSource(sourcesDir: string, file: string, destination: string, origin: SourceOrigin, extra?: {
|
|
98
|
+
replaces?: string;
|
|
99
|
+
derivedFrom?: string;
|
|
100
|
+
name?: string;
|
|
101
|
+
}): Promise<AddedSource>;
|
|
102
|
+
/** What retiring an original did to its summary pages (paths relative to the project). */
|
|
103
|
+
export interface RetireResult {
|
|
104
|
+
movedTo: string;
|
|
105
|
+
summaries: {
|
|
106
|
+
page: string;
|
|
107
|
+
marked: "retired" | "superseded";
|
|
108
|
+
supersededBy?: string;
|
|
109
|
+
}[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Moves an original to `.agent-kit/retired/` (never deleted: the person empties that folder)
|
|
113
|
+
* and records why. Its summary pages are marked: retired if the original was wrong (what it
|
|
114
|
+
* taught was wrong), superseded if it was replaced, pointing to the replacement's summary
|
|
115
|
+
* when there is one.
|
|
116
|
+
*/
|
|
117
|
+
export declare function retireSource(sourcesDir: string, knowledgeDir: string | undefined, projectDir: string, source: string, why: "wrong" | "replaced", reason: string, replacedBy?: string): Promise<RetireResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Records `source` (relative to `sourcesDir`) as ingested now, with its current hash: a
|
|
120
|
+
* summary page was just written from it, so "changed" means changed after this.
|
|
121
|
+
*/
|
|
122
|
+
export declare function recordIngest(sourcesDir: string, source: string): Promise<void>;
|