@cad0p/pi-subagents-tintinweb 0.14.2 → 0.22.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/CHANGELOG.md +20 -1
- package/CONTRIBUTING.md +8 -0
- package/README.md +98 -58
- package/docs/fork-differences.md +51 -0
- package/docs/verification-checklist.md +132 -0
- package/examples/agent-tool-description.md +3 -9
- package/package.json +13 -11
- package/pnpm-workspace.yaml +9 -0
- package/src/abortable.ts +43 -0
- package/src/agent-manager.ts +404 -140
- package/src/agent-runner.ts +462 -89
- package/src/agent-types.ts +269 -23
- package/src/checkpoint.ts +49 -0
- package/src/child-context.ts +30 -0
- package/src/cross-extension-rpc.ts +65 -5
- package/src/custom-agents.ts +161 -14
- package/src/default-agents.ts +1 -1
- package/src/index.ts +1014 -980
- package/src/invocation-config.ts +81 -12
- package/src/model-resolver.ts +18 -0
- package/src/model-scope.ts +62 -0
- package/src/nested-tools.ts +406 -0
- package/src/output-file.ts +18 -1
- package/src/prompts.ts +71 -0
- package/src/result-report.ts +170 -0
- package/src/schedule-store.ts +300 -13
- package/src/schedule.ts +222 -77
- package/src/settings.ts +206 -32
- package/src/status-note.ts +18 -1
- package/src/text-safety.ts +69 -0
- package/src/types.ts +101 -58
- package/src/ui/agent-widget.ts +104 -36
- package/src/ui/conversation-viewer.ts +36 -18
- package/src/ui/fleet-list.ts +19 -8
- package/src/ui/schedule-menu.ts +63 -19
- package/src/usage.ts +128 -8
- package/src/worktree.ts +19 -0
- package/src/group-join.ts +0 -141
package/src/custom-agents.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
9
|
-
import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
|
|
9
|
+
import type { AgentConfig, IsolationMode, MemoryScope, ThinkingLevel } from "./types.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Scan for custom agent .md files from multiple locations.
|
|
@@ -20,20 +20,23 @@ import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
|
|
|
20
20
|
* authority; .agents/agents is an additional read location.
|
|
21
21
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
22
22
|
*/
|
|
23
|
-
export function loadCustomAgents(cwd: string): Map<string, AgentConfig> {
|
|
23
|
+
export function loadCustomAgents(cwd: string, strict = false): Map<string, AgentConfig> {
|
|
24
24
|
const globalDir = join(getAgentDir(), "agents");
|
|
25
25
|
const workspaceProjectDir = join(cwd, ".agents", "agents");
|
|
26
26
|
const projectDir = join(cwd, ".pi", "agents");
|
|
27
27
|
|
|
28
28
|
const agents = new Map<string, AgentConfig>();
|
|
29
|
-
loadFromDir(globalDir, agents, "global"); // lowest priority
|
|
30
|
-
loadFromDir(workspaceProjectDir, agents, "project"); // shared workspace
|
|
31
|
-
loadFromDir(projectDir, agents, "project"); // highest priority (overwrites)
|
|
29
|
+
loadFromDir(globalDir, agents, "global", strict); // lowest priority
|
|
30
|
+
loadFromDir(workspaceProjectDir, agents, "project", strict); // shared workspace
|
|
31
|
+
loadFromDir(projectDir, agents, "project", strict); // highest priority (overwrites)
|
|
32
|
+
|
|
33
|
+
warnedLastLoad = warnedThisLoad;
|
|
34
|
+
warnedThisLoad = new Set();
|
|
32
35
|
return agents;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** Load agent configs from a directory into the map. */
|
|
36
|
-
function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "project" | "global"): void {
|
|
39
|
+
function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "project" | "global", strict: boolean): void {
|
|
37
40
|
if (!existsSync(dir)) return;
|
|
38
41
|
|
|
39
42
|
let files: string[];
|
|
@@ -45,15 +48,15 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
45
48
|
|
|
46
49
|
for (const file of files) {
|
|
47
50
|
const name = basename(file, ".md");
|
|
51
|
+
const path = join(dir, file);
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} catch {
|
|
53
|
+
const parsed = readAgentFile(path, strict);
|
|
54
|
+
if (!parsed) {
|
|
55
|
+
warnSkippedOverride(name, agents);
|
|
53
56
|
continue;
|
|
54
57
|
}
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const { frontmatter: fm, body } = parsed;
|
|
59
|
+
warnUnknownKeys(path, fm);
|
|
57
60
|
|
|
58
61
|
const { builtinToolNames, extSelectors } = parseToolsField(fm.tools);
|
|
59
62
|
|
|
@@ -67,25 +70,121 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
67
70
|
extensions: inheritField(fm.extensions ?? fm.inherit_extensions),
|
|
68
71
|
excludeExtensions: csvListOptional(fm.exclude_extensions),
|
|
69
72
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
73
|
+
scopedModels: scopedModelsField(fm.scoped_models ?? fm.inherit_scoped_models),
|
|
70
74
|
model: str(fm.model),
|
|
71
75
|
thinking: str(fm.thinking) as ThinkingLevel | undefined,
|
|
72
76
|
maxTurns: nonNegativeInt(fm.max_turns),
|
|
73
77
|
persistSession: fm.persist_session != null ? fm.persist_session === true : undefined,
|
|
74
78
|
outputTranscript: fm.output_transcript != null ? fm.output_transcript !== false : undefined,
|
|
75
79
|
sessionDir: str(fm.session_dir),
|
|
80
|
+
allowedSubagents: parseAllowedSubagents(fm.allowed_subagents),
|
|
81
|
+
maxSubagentDepth: nonNegativeInt(fm.max_subagent_depth),
|
|
76
82
|
systemPrompt: body.trim(),
|
|
77
83
|
promptMode: fm.prompt_mode === "append" ? "append" : "replace",
|
|
78
84
|
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
79
|
-
runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
|
|
80
85
|
isolated: fm.isolated != null ? fm.isolated === true : undefined,
|
|
81
86
|
memory: parseMemory(fm.memory),
|
|
82
|
-
isolation: fm.isolation
|
|
87
|
+
isolation: parseIsolation(fm.isolation),
|
|
83
88
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
84
89
|
source,
|
|
90
|
+
sourcePath: path,
|
|
85
91
|
});
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Read and parse one agent file, or warn and return undefined for the caller to
|
|
97
|
+
* skip. One bad file must not take the whole extension down with it — an
|
|
98
|
+
* unparseable `.md` used to abort activation, so pi exited before the TUI.
|
|
99
|
+
*
|
|
100
|
+
* The path is as much of the fix as the recovery: a bare YAML error ("line 2,
|
|
101
|
+
* column 14") is unactionable when agents come from three directories at once,
|
|
102
|
+
* and the only other symptom is `Unknown agent type`, which reads like a typo.
|
|
103
|
+
*
|
|
104
|
+
* Under `strict` the same failure rethrows, still naming the path, so callers
|
|
105
|
+
* that opted into failing closed stop rather than run a substituted agent.
|
|
106
|
+
*/
|
|
107
|
+
function readAgentFile(path: string, strict: boolean): { frontmatter: Record<string, unknown>; body: string } | undefined {
|
|
108
|
+
try {
|
|
109
|
+
return parseFrontmatter<Record<string, unknown>>(readFileSync(path, "utf-8"));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
112
|
+
if (strict) throw new Error(`${path}: ${reason}`);
|
|
113
|
+
warnIfNew(`Skipping agent file ${path}: ${reason}`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A skipped file that was overriding an already-loaded agent leaves the name
|
|
120
|
+
* pointing at a *different* file — its own prompt, model and tools. Nothing
|
|
121
|
+
* downstream can flag that: unlike an unknown type, the `Agent` call succeeds.
|
|
122
|
+
*/
|
|
123
|
+
function warnSkippedOverride(name: string, agents: Map<string, AgentConfig>): void {
|
|
124
|
+
const surviving = agents.get(name);
|
|
125
|
+
// Nothing shadowed, or what it shadowed is disabled: dispatch refuses the type
|
|
126
|
+
// either way (see resolveEnabledTypeIn), so there is no substitution to report.
|
|
127
|
+
if (!surviving?.sourcePath || surviving.enabled === false) return;
|
|
128
|
+
warnIfNew(`Agent "${name}" now loads from ${surviving.sourcePath} instead`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Frontmatter keys this loader reads. The parser below is field-by-field, so a
|
|
133
|
+
* key outside this set is a silent no-op: a typo, or a field from another tool
|
|
134
|
+
* (`run_in_background`, `join_mode`, `name:`) that reads as configuration and
|
|
135
|
+
* does nothing. Reported once per file per key at load — the same channel as a
|
|
136
|
+
* malformed file — rather than per spawn, where it would be noise.
|
|
137
|
+
*/
|
|
138
|
+
const KNOWN_FRONTMATTER_KEYS: ReadonlySet<string> = new Set([
|
|
139
|
+
"description",
|
|
140
|
+
"display_name",
|
|
141
|
+
"tools",
|
|
142
|
+
"disallowed_tools",
|
|
143
|
+
"extensions",
|
|
144
|
+
"inherit_extensions",
|
|
145
|
+
"exclude_extensions",
|
|
146
|
+
"skills",
|
|
147
|
+
"inherit_skills",
|
|
148
|
+
"scoped_models",
|
|
149
|
+
"inherit_scoped_models",
|
|
150
|
+
"model",
|
|
151
|
+
"thinking",
|
|
152
|
+
"max_turns",
|
|
153
|
+
"persist_session",
|
|
154
|
+
"output_transcript",
|
|
155
|
+
"session_dir",
|
|
156
|
+
"allowed_subagents",
|
|
157
|
+
"max_subagent_depth",
|
|
158
|
+
"prompt_mode",
|
|
159
|
+
"inherit_context",
|
|
160
|
+
"isolated",
|
|
161
|
+
"memory",
|
|
162
|
+
"isolation",
|
|
163
|
+
"enabled",
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
function warnUnknownKeys(path: string, frontmatter: Record<string, unknown>): void {
|
|
167
|
+
for (const key of Object.keys(frontmatter)) {
|
|
168
|
+
if (KNOWN_FRONTMATTER_KEYS.has(key)) continue;
|
|
169
|
+
warnIfNew(`Ignoring unknown frontmatter key "${key}" in ${path}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let warnedLastLoad = new Set<string>();
|
|
174
|
+
let warnedThisLoad = new Set<string>();
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Agents reload on activation and again on every `Agent` call, so an unchanged
|
|
178
|
+
* problem would re-warn all session — over a painted TUI, since pi does not
|
|
179
|
+
* redirect console output. Compare against the previous load rather than every
|
|
180
|
+
* load ever, so a file that is fixed and then broken again still reports.
|
|
181
|
+
*/
|
|
182
|
+
function warnIfNew(message: string): void {
|
|
183
|
+
warnedThisLoad.add(message);
|
|
184
|
+
if (warnedLastLoad.has(message)) return;
|
|
185
|
+
console.warn(`[pi-subagents] ${message}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
89
188
|
// ---- Field parsers ----
|
|
90
189
|
// All follow the same convention: omitted → default, "none"/empty → nothing, value → exact.
|
|
91
190
|
|
|
@@ -110,6 +209,22 @@ function parseCsvField(val: unknown): string[] | undefined {
|
|
|
110
209
|
return items.length > 0 ? items : undefined;
|
|
111
210
|
}
|
|
112
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Parse the nested-delegation allowlist. Single field, default-off:
|
|
214
|
+
* omitted/empty/"none"/`false` → undefined (no nested tools); "all"/"*"/`true`
|
|
215
|
+
* → "all" (any enabled agent); csv → only the listed types.
|
|
216
|
+
*
|
|
217
|
+
* Booleans are accepted because `extensions:`/`skills:` take them and users
|
|
218
|
+
* generalize: without this, YAML's `true` stringifies into an agent type
|
|
219
|
+
* literally named "true", so the tools appear and every spawn is refused.
|
|
220
|
+
*/
|
|
221
|
+
function parseAllowedSubagents(val: unknown): "all" | string[] | undefined {
|
|
222
|
+
if (typeof val === "boolean") return val ? "all" : undefined;
|
|
223
|
+
const items = parseCsvField(val);
|
|
224
|
+
if (!items) return undefined;
|
|
225
|
+
return items.some(i => i === "*" || i.toLowerCase() === "all") ? "all" : items;
|
|
226
|
+
}
|
|
227
|
+
|
|
113
228
|
/**
|
|
114
229
|
* Parse a comma-separated list field with defaults.
|
|
115
230
|
* omitted → defaults; "none"/empty → []; csv → listed items.
|
|
@@ -155,6 +270,24 @@ function parseMemory(val: unknown): MemoryScope | undefined {
|
|
|
155
270
|
return undefined;
|
|
156
271
|
}
|
|
157
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Parse the `isolation` frontmatter field.
|
|
275
|
+
*
|
|
276
|
+
* `off` is kept as a value rather than folded into `undefined` because the two
|
|
277
|
+
* do not mean the same thing here: agent config outranks tool-call params, so
|
|
278
|
+
* `off` vetoes a caller's `worktree` while an absent field lets it through.
|
|
279
|
+
*
|
|
280
|
+
* pi's frontmatter parser is not YAML 1.1 — bare `off` and `no` arrive as
|
|
281
|
+
* strings and only `false` becomes a boolean — so all three spellings are
|
|
282
|
+
* accepted rather than leaving an author's intent silently dropped. Anything
|
|
283
|
+
* else stays `undefined`, as before.
|
|
284
|
+
*/
|
|
285
|
+
function parseIsolation(val: unknown): IsolationMode | undefined {
|
|
286
|
+
if (val === "worktree") return "worktree";
|
|
287
|
+
if (val === "off" || val === "none" || val === "no" || val === false) return "off";
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
158
291
|
/**
|
|
159
292
|
* Parse an inherit field (extensions, skills).
|
|
160
293
|
* omitted/true → true (inherit all); false/"none"/empty → false; csv → listed names.
|
|
@@ -165,3 +298,17 @@ function inheritField(val: unknown): true | string[] | false {
|
|
|
165
298
|
const items = csvList(val, []);
|
|
166
299
|
return items.length > 0 ? items : false;
|
|
167
300
|
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Parse scoped_models (alias inherit_scoped_models), the child session's
|
|
304
|
+
* model scope. omitted/true/"inherit" → true (inherit the parent session's
|
|
305
|
+
* scope); false/"none"/empty → false; csv → explicit provider/modelId list.
|
|
306
|
+
* Unlike inheritField, the literal "inherit" is accepted as a readable alias,
|
|
307
|
+
* but only as the sole value — mixed `inherit, foo` stays an explicit list.
|
|
308
|
+
*/
|
|
309
|
+
function scopedModelsField(val: unknown): true | string[] | false {
|
|
310
|
+
// `inherit` must be the sole value: a scalar or a single-item YAML list.
|
|
311
|
+
const sole = Array.isArray(val) ? (val.length === 1 ? val[0] : undefined) : val;
|
|
312
|
+
if (typeof sole === "string" && sole.trim().toLowerCase() === "inherit") return true;
|
|
313
|
+
return inheritField(val);
|
|
314
|
+
}
|
package/src/default-agents.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
16
16
|
displayName: "Agent",
|
|
17
17
|
description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
|
|
18
18
|
// builtinToolNames omitted — means "all available tools" (resolved at lookup time)
|
|
19
|
-
// inheritContext /
|
|
19
|
+
// inheritContext / isolated omitted — strategy fields, callers decide per-call.
|
|
20
20
|
// Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
|
|
21
21
|
extensions: true,
|
|
22
22
|
skills: true,
|