@bermudi/pi-delegate 0.1.7 → 0.1.9
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 +14 -1
- package/agents.ts +54 -3
- package/config.ts +84 -9
- package/constants.ts +17 -0
- package/delegate.ts +12 -1
- package/dispatch.ts +4 -0
- package/extension.ts +11 -1
- package/host.ts +243 -78
- package/manual.ts +9 -6
- package/package.json +1 -1
- package/schema.ts +4 -4
- package/settings.ts +235 -13
- package/task-resolution.ts +109 -52
- package/types.ts +7 -1
package/README.md
CHANGED
|
@@ -38,6 +38,19 @@ Parent extension/MCP tools are not copied, and project instructions are rebuilt
|
|
|
38
38
|
for the task's `cwd`. Omit `agent` when you want an ad-hoc task using delegate's
|
|
39
39
|
normal inline defaults instead.
|
|
40
40
|
|
|
41
|
+
The other built-ins are:
|
|
42
|
+
|
|
43
|
+
- `scout` — read-only investigation with `read`, `grep`, `find`, and `ls`.
|
|
44
|
+
- `coder` — implementation and verification with `read`, `write`, `edit`, and
|
|
45
|
+
`bash` in the shared workspace.
|
|
46
|
+
- `reviewer` — review with `read` and `bash`, using a disposable scratch copy by
|
|
47
|
+
default. Set `workspace: "shared"` when a reviewer needs a persistent
|
|
48
|
+
`sessionId`.
|
|
49
|
+
|
|
50
|
+
Fresh built-ins inherit the parent's exact model object and thinking level.
|
|
51
|
+
Task-level overrides win; settings can provide unconditional overrides or exact
|
|
52
|
+
parent-model overrides under `delegate.agentOverridesByParentModel`.
|
|
53
|
+
|
|
41
54
|
### Disposable scratch workspace
|
|
42
55
|
|
|
43
56
|
For review, tests, or other commands whose project changes should be thrown
|
|
@@ -136,7 +149,7 @@ over an installed extension.
|
|
|
136
149
|
rare override. Markdown agents are examples of custom agents.
|
|
137
150
|
- **Named agent** / **Markdown agent** — A reusable custom agent persisted as a
|
|
138
151
|
Markdown file in `.pi/agents/*.md` or `~/.pi/agent/agents/*.md`. The frontmatter
|
|
139
|
-
defines its name, description, model, tools, thinking level
|
|
152
|
+
defines its name, description, model, tools, and thinking level; the
|
|
140
153
|
Markdown body is its system prompt.
|
|
141
154
|
- **Ad-hoc subagent** — A subagent created from inline task fields instead of a
|
|
142
155
|
named Markdown agent profile. In current output this is labeled `ad-hoc`.
|
package/agents.ts
CHANGED
|
@@ -7,8 +7,13 @@ import type {
|
|
|
7
7
|
} from "@earendil-works/pi-agent-core";
|
|
8
8
|
import { parseFrontmatter as parsePiFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import {
|
|
10
|
+
BUILTIN_AGENT_NAMES,
|
|
11
|
+
CODER_AGENT_NAME,
|
|
10
12
|
DEFAULT_AGENT_NAME,
|
|
11
13
|
DEFAULT_TOOLS,
|
|
14
|
+
READONLY_TOOLS,
|
|
15
|
+
REVIEWER_AGENT_NAME,
|
|
16
|
+
SCOUT_AGENT_NAME,
|
|
12
17
|
VALID_THINKING,
|
|
13
18
|
} from "./constants.ts";
|
|
14
19
|
import { resolveToolGroups } from "./tools.ts";
|
|
@@ -110,6 +115,50 @@ export function parseFrontmatter(
|
|
|
110
115
|
|
|
111
116
|
// ── Agent Discovery ───────────────────────────────────────────────────────
|
|
112
117
|
|
|
118
|
+
/** Built-in profiles are always available and cannot vary with Markdown files. */
|
|
119
|
+
export const BUILTIN_AGENT_CONFIGS: Readonly<Record<string, AgentConfig>> = {
|
|
120
|
+
[DEFAULT_AGENT_NAME]: {
|
|
121
|
+
name: DEFAULT_AGENT_NAME,
|
|
122
|
+
description:
|
|
123
|
+
"Mirror the live parent model, thinking level, native tools, and base prompt.",
|
|
124
|
+
tools: DEFAULT_TOOLS,
|
|
125
|
+
systemPrompt: "",
|
|
126
|
+
builtin: true,
|
|
127
|
+
workspace: "shared",
|
|
128
|
+
},
|
|
129
|
+
[SCOUT_AGENT_NAME]: {
|
|
130
|
+
name: SCOUT_AGENT_NAME,
|
|
131
|
+
description: "Investigate without modifying the source project.",
|
|
132
|
+
tools: READONLY_TOOLS,
|
|
133
|
+
systemPrompt:
|
|
134
|
+
"Explore the codebase to answer the assigned question. Do not modify files. Trace relevant code, tests, documentation, and history when useful. Return concise findings with concrete paths, symbols, and any uncertainty. Prefer evidence over speculation.",
|
|
135
|
+
builtin: true,
|
|
136
|
+
workspace: "shared",
|
|
137
|
+
},
|
|
138
|
+
[CODER_AGENT_NAME]: {
|
|
139
|
+
name: CODER_AGENT_NAME,
|
|
140
|
+
description: "Implement and verify changes in the shared source tree.",
|
|
141
|
+
tools: DEFAULT_TOOLS,
|
|
142
|
+
systemPrompt:
|
|
143
|
+
"Implement the assigned change. Read the existing code and project instructions first. Prefer the smallest maintainable solution that follows existing conventions. Surface failures clearly. Run focused tests or checks and report what changed, what passed, and any remaining risk.",
|
|
144
|
+
builtin: true,
|
|
145
|
+
workspace: "shared",
|
|
146
|
+
},
|
|
147
|
+
[REVIEWER_AGENT_NAME]: {
|
|
148
|
+
name: REVIEWER_AGENT_NAME,
|
|
149
|
+
description: "Inspect the current snapshot and report actionable findings.",
|
|
150
|
+
tools: ["read", "bash"],
|
|
151
|
+
systemPrompt:
|
|
152
|
+
"Review the current snapshot for correctness, regressions, security problems, and missing tests. Do not modify the source project. Run focused checks when useful. Report actionable findings ordered by severity, with concrete paths and locations. If there are no material findings, say so plainly; do not invent issues or merely summarize the implementation.",
|
|
153
|
+
builtin: true,
|
|
154
|
+
workspace: "scratch",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export function isBuiltinAgentName(name: string): boolean {
|
|
159
|
+
return (BUILTIN_AGENT_NAMES as readonly string[]).includes(name);
|
|
160
|
+
}
|
|
161
|
+
|
|
113
162
|
/** Find the nearest ancestor containing project-scoped agent files. */
|
|
114
163
|
export function findProjectRoot(cwd: string): string | null {
|
|
115
164
|
let dir = cwd;
|
|
@@ -330,7 +379,9 @@ export function discoverAgents(cwd: string): Map<string, AgentConfig> {
|
|
|
330
379
|
scope: "claude",
|
|
331
380
|
});
|
|
332
381
|
|
|
333
|
-
const agents = new Map<string, AgentConfig>(
|
|
382
|
+
const agents = new Map<string, AgentConfig>(
|
|
383
|
+
Object.entries(BUILTIN_AGENT_CONFIGS),
|
|
384
|
+
);
|
|
334
385
|
const loadDir = (
|
|
335
386
|
{ dir, scope }: { dir: string; scope: AgentConfig["scope"] },
|
|
336
387
|
loader: (fp: string) => AgentConfig | null,
|
|
@@ -345,9 +396,9 @@ export function discoverAgents(cwd: string): Map<string, AgentConfig> {
|
|
|
345
396
|
if (!e.name.endsWith(".md") || e.name.endsWith(".chain.md")) continue;
|
|
346
397
|
const filePath = path.join(dir, e.name);
|
|
347
398
|
const cfg = loader(filePath);
|
|
348
|
-
if (cfg
|
|
399
|
+
if (cfg && isBuiltinAgentName(cfg.name)) {
|
|
349
400
|
console.warn(
|
|
350
|
-
`[delegate] ignoring agent profile '${
|
|
401
|
+
`[delegate] ignoring agent profile '${cfg.name}' from ${filePath}: the name is reserved for a built-in delegate profile.`,
|
|
351
402
|
);
|
|
352
403
|
continue;
|
|
353
404
|
}
|
package/config.ts
CHANGED
|
@@ -129,10 +129,17 @@ function normalizeProviderExtensions(
|
|
|
129
129
|
// Provider-scoped opt-in extension map for subagents. Keep this aligned with
|
|
130
130
|
// the currently shipped codex remote-compaction integration. `delegate.json`
|
|
131
131
|
// `providerExtensions` replaces a provider's entries (it does not append); an
|
|
132
|
-
// empty array is ignored so the default persists.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
// empty array is ignored so the default persists. Provenance is classification
|
|
133
|
+
// by config presence: every source the user lists is required and fails
|
|
134
|
+
// closed when missing, unverifiable, or broken — including an exact re-listing
|
|
135
|
+
// of a shipped default. Shipped defaults (providers the user never mentioned)
|
|
136
|
+
// are best-effort: they degrade silently to extension-free subagents on Pi's
|
|
137
|
+
// native compaction, because absence of an optional integration is not a
|
|
138
|
+
// warning condition — that is Pi's normal operation.
|
|
139
|
+
const DEFAULT_PROVIDER_EXTENSIONS: Record<string, readonly string[]> =
|
|
140
|
+
Object.assign(Object.create(null) as Record<string, readonly string[]>, {
|
|
141
|
+
"openai-codex": ["npm:@bermudi/pi-codex"],
|
|
142
|
+
});
|
|
136
143
|
|
|
137
144
|
function resolveProviderExtensions(
|
|
138
145
|
raw: unknown,
|
|
@@ -155,7 +162,7 @@ const DEFAULT_DELEGATE_CONFIG: DelegateConfig = {
|
|
|
155
162
|
wholeTaskMaxRetries: 3,
|
|
156
163
|
wholeTaskBaseDelayMs: 1_000,
|
|
157
164
|
},
|
|
158
|
-
providerExtensions:
|
|
165
|
+
providerExtensions: {},
|
|
159
166
|
telemetry: {
|
|
160
167
|
enabled: true,
|
|
161
168
|
},
|
|
@@ -174,7 +181,15 @@ let __delegateConfig: DelegateConfig = {
|
|
|
174
181
|
};
|
|
175
182
|
let stallTimeoutOverrideForTesting: number | undefined;
|
|
176
183
|
|
|
177
|
-
/** Read delegate config from disk. Returns defaults if file missing or corrupt.
|
|
184
|
+
/** Read delegate config from disk. Returns defaults if file missing or corrupt.
|
|
185
|
+
*
|
|
186
|
+
* The returned `providerExtensions` is the *user-only* view — exactly what the
|
|
187
|
+
* file said, defaults excluded. `getSubagentProviderExtensionMap()` is the
|
|
188
|
+
* merged (defaults + user) view, and
|
|
189
|
+
* `getSubagentProviderExtensionSourcesForProvider()` is the provenance-tagged
|
|
190
|
+
* view. Keeping the raw user map here is what lets the sources getter
|
|
191
|
+
* distinguish "the user listed this" from "this is a shipped default" by
|
|
192
|
+
* config presence rather than string identity. */
|
|
178
193
|
export function loadDelegateConfig(): DelegateConfig {
|
|
179
194
|
try {
|
|
180
195
|
const raw = fs.readFileSync(DELEGATE_CONFIG_PATH, "utf-8");
|
|
@@ -191,7 +206,9 @@ export function loadDelegateConfig(): DelegateConfig {
|
|
|
191
206
|
...(parsed.concurrency ?? {}),
|
|
192
207
|
},
|
|
193
208
|
retry: { ...DEFAULT_DELEGATE_CONFIG.retry, ...(parsed.retry ?? {}) },
|
|
194
|
-
providerExtensions:
|
|
209
|
+
providerExtensions: normalizeProviderExtensions(
|
|
210
|
+
parsed.providerExtensions,
|
|
211
|
+
),
|
|
195
212
|
telemetry: normalizeTelemetryConfig(parsed.telemetry),
|
|
196
213
|
output: { ...DEFAULT_DELEGATE_CONFIG.output, ...(parsed.output ?? {}) },
|
|
197
214
|
} as DelegateConfig;
|
|
@@ -255,7 +272,7 @@ export function _setDelegateConfigForTesting(
|
|
|
255
272
|
...DEFAULT_DELEGATE_CONFIG.retry,
|
|
256
273
|
...(config.retry ?? {}),
|
|
257
274
|
},
|
|
258
|
-
providerExtensions:
|
|
275
|
+
providerExtensions: normalizeProviderExtensions(config.providerExtensions),
|
|
259
276
|
telemetry: normalizeTelemetryConfig(config.telemetry),
|
|
260
277
|
output: {
|
|
261
278
|
...DEFAULT_DELEGATE_CONFIG.output,
|
|
@@ -266,7 +283,11 @@ export function _setDelegateConfigForTesting(
|
|
|
266
283
|
}
|
|
267
284
|
|
|
268
285
|
/**
|
|
269
|
-
* Get the configured provider-scoped extension allowlist for subagents
|
|
286
|
+
* Get the configured provider-scoped extension allowlist for subagents:
|
|
287
|
+
* the merged view (shipped defaults + user config, user entries replacing a
|
|
288
|
+
* provider's defaults). The stored `config.providerExtensions` itself is the
|
|
289
|
+
* user-only view; the merge happens here so provenance survives until a
|
|
290
|
+
* consumer asks for it (`getSubagentProviderExtensionSourcesForProvider`).
|
|
270
291
|
* Explicit configs are normalized here too, so callers using the injected
|
|
271
292
|
* config form get the same case-insensitive and replace-per-provider
|
|
272
293
|
* semantics as the file-backed singleton.
|
|
@@ -295,6 +316,60 @@ export function getSubagentProviderExtensionsForProvider(
|
|
|
295
316
|
: [];
|
|
296
317
|
}
|
|
297
318
|
|
|
319
|
+
/** A provider-extension source together with how it entered the config. */
|
|
320
|
+
export interface ProviderExtensionSource {
|
|
321
|
+
/** The normalized source string, as the package manager consumes it. */
|
|
322
|
+
readonly source: string;
|
|
323
|
+
/**
|
|
324
|
+
* Whether the user configured this source themselves (required) or it is a
|
|
325
|
+
* shipped default for a provider the user never mentioned (best-effort).
|
|
326
|
+
* Required sources fail closed when missing, unverifiable, or broken;
|
|
327
|
+
* best-effort defaults degrade silently to extension-free subagents on
|
|
328
|
+
* Pi's native compaction.
|
|
329
|
+
*/
|
|
330
|
+
readonly required: boolean;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get the provenance-tagged extension sources for a provider's subagents.
|
|
335
|
+
* Classification is by config presence, never by string identity: everything
|
|
336
|
+
* the user lists in `providerExtensions` is `required: true` — including an
|
|
337
|
+
* exact re-listing of a shipped default, because typing it into the config
|
|
338
|
+
* expresses intent. Providers the user never configured fall back to the
|
|
339
|
+
* shipped defaults, tagged `required: false` (best-effort).
|
|
340
|
+
*/
|
|
341
|
+
export function getSubagentProviderExtensionSourcesForProvider(
|
|
342
|
+
provider: string | undefined,
|
|
343
|
+
config: DelegateConfig = __delegateConfig,
|
|
344
|
+
): readonly ProviderExtensionSource[] {
|
|
345
|
+
const normalized = provider?.trim().toLowerCase();
|
|
346
|
+
if (!normalized) return [];
|
|
347
|
+
// Normalize the injected config map so an unnormalized key like " Custom-Provider "
|
|
348
|
+
// is handled, matching getSubagentProviderExtensionMap / getSubagentProviderExtensionsForProvider.
|
|
349
|
+
const rawUserMap = config.providerExtensions;
|
|
350
|
+
const userMap = rawUserMap
|
|
351
|
+
? normalizeProviderExtensions(rawUserMap)
|
|
352
|
+
: (Object.create(null) as Record<string, readonly string[]>);
|
|
353
|
+
if (Object.prototype.hasOwnProperty.call(userMap, normalized)) {
|
|
354
|
+
return (userMap[normalized] ?? []).map((source) => ({
|
|
355
|
+
source,
|
|
356
|
+
required: true,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
if (
|
|
360
|
+
!Object.prototype.hasOwnProperty.call(
|
|
361
|
+
DEFAULT_PROVIDER_EXTENSIONS,
|
|
362
|
+
normalized,
|
|
363
|
+
)
|
|
364
|
+
) {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
return (DEFAULT_PROVIDER_EXTENSIONS[normalized] ?? []).map((source) => ({
|
|
368
|
+
source,
|
|
369
|
+
required: false,
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
298
373
|
// ── Config Getters ───────────────────────────────────────────────────────
|
|
299
374
|
|
|
300
375
|
/**
|
package/constants.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
/** Reserved built-in profile that mirrors the live parent configuration. */
|
|
2
2
|
export const DEFAULT_AGENT_NAME = "default";
|
|
3
3
|
|
|
4
|
+
/** Reserved built-in profile for read-only investigation. */
|
|
5
|
+
export const SCOUT_AGENT_NAME = "scout";
|
|
6
|
+
|
|
7
|
+
/** Reserved built-in profile for shared-workspace implementation work. */
|
|
8
|
+
export const CODER_AGENT_NAME = "coder";
|
|
9
|
+
|
|
10
|
+
/** Reserved built-in profile for isolated review work. */
|
|
11
|
+
export const REVIEWER_AGENT_NAME = "reviewer";
|
|
12
|
+
|
|
13
|
+
/** All names reserved by delegate's built-in profiles. */
|
|
14
|
+
export const BUILTIN_AGENT_NAMES = [
|
|
15
|
+
DEFAULT_AGENT_NAME,
|
|
16
|
+
SCOUT_AGENT_NAME,
|
|
17
|
+
CODER_AGENT_NAME,
|
|
18
|
+
REVIEWER_AGENT_NAME,
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
4
21
|
/** Full-capability agent set. Inline-task default and the `*` shorthand.
|
|
5
22
|
* Bash subsumes search, so the dedicated grep/find/ls tools are excluded. */
|
|
6
23
|
export const DEFAULT_TOOLS = ["read", "write", "edit", "bash"];
|
package/delegate.ts
CHANGED
|
@@ -24,6 +24,10 @@ export type { DelegateConfig } from "./config.ts";
|
|
|
24
24
|
|
|
25
25
|
export {
|
|
26
26
|
DEFAULT_AGENT_NAME,
|
|
27
|
+
SCOUT_AGENT_NAME,
|
|
28
|
+
CODER_AGENT_NAME,
|
|
29
|
+
REVIEWER_AGENT_NAME,
|
|
30
|
+
BUILTIN_AGENT_NAMES,
|
|
27
31
|
DEFAULT_TOOLS,
|
|
28
32
|
READONLY_TOOLS,
|
|
29
33
|
MAX_CONCURRENCY,
|
|
@@ -123,6 +127,8 @@ export {
|
|
|
123
127
|
loadAgentFile,
|
|
124
128
|
loadClaudeAgentFile,
|
|
125
129
|
discoverAgents,
|
|
130
|
+
BUILTIN_AGENT_CONFIGS,
|
|
131
|
+
isBuiltinAgentName,
|
|
126
132
|
buildSubagentSystemPrompt,
|
|
127
133
|
DEFAULT_SUBAGENT_SYSTEM_PROMPT,
|
|
128
134
|
} from "./agents.ts";
|
|
@@ -133,7 +139,12 @@ export {
|
|
|
133
139
|
resolveModelRequest,
|
|
134
140
|
findAvailableAlternative,
|
|
135
141
|
} from "./model.ts";
|
|
136
|
-
export {
|
|
142
|
+
export {
|
|
143
|
+
readDelegateSettingsFile,
|
|
144
|
+
loadDelegateSettings,
|
|
145
|
+
clearDelegateSettingsCache,
|
|
146
|
+
} from "./settings.ts";
|
|
147
|
+
export type { AgentOverride } from "./settings.ts";
|
|
137
148
|
export { resolveCwd, extractOutput, extractUsage } from "./utils.ts";
|
|
138
149
|
export {
|
|
139
150
|
decideSpill,
|
package/dispatch.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { validateDelegateOperation } from "./schema.ts";
|
|
25
25
|
import { notifyCrossLeafDelivery, syncDelegateStatus } from "./status.ts";
|
|
26
26
|
import { validateTasks, resolveTasks } from "./task-resolution.ts";
|
|
27
|
+
import { clearDelegateSettingsCache } from "./settings.ts";
|
|
27
28
|
import type { CallSpan } from "./telemetry.ts";
|
|
28
29
|
import type {
|
|
29
30
|
AgentConfig,
|
|
@@ -146,6 +147,9 @@ export interface DelegateDispatchInput {
|
|
|
146
147
|
export async function dispatchDelegate(
|
|
147
148
|
input: DelegateDispatchInput,
|
|
148
149
|
): Promise<DelegateToolResult> {
|
|
150
|
+
// Settings are user-editable. Clear once at the dispatch boundary so every
|
|
151
|
+
// task in this batch observes one consistent settings snapshot.
|
|
152
|
+
clearDelegateSettingsCache();
|
|
149
153
|
const {
|
|
150
154
|
pi,
|
|
151
155
|
params,
|
package/extension.ts
CHANGED
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
} from "./dispatch.ts";
|
|
19
19
|
import { renderDelegateCall, renderDelegateResult } from "./render-result.ts";
|
|
20
20
|
import { hostCompatError } from "./host-compat.ts";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
invalidateHostDepsCache,
|
|
23
|
+
registerProviderExtensionNotifier,
|
|
24
|
+
} from "./host.ts";
|
|
22
25
|
import { recordTreeNavigation, resetLeafTracking } from "./leaf.ts";
|
|
23
26
|
import { closeAllPooledAgents } from "./pool.ts";
|
|
24
27
|
import {
|
|
@@ -122,6 +125,12 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
122
125
|
prepareArguments: normalizeDelegateArguments,
|
|
123
126
|
|
|
124
127
|
async execute(_id, params: DelegateArguments, signal, onUpdate, ctx) {
|
|
128
|
+
// Prime the UI notice for best-effort provider extensions that load for
|
|
129
|
+
// subagents (host.ts consumes it where the fact is discovered). Every
|
|
130
|
+
// execute re-primes so a stale ctx never sticks.
|
|
131
|
+
registerProviderExtensionNotifier((message) =>
|
|
132
|
+
ctx.ui.notify(message, "info"),
|
|
133
|
+
);
|
|
125
134
|
const parentModelId = ctx.model?.id;
|
|
126
135
|
const tasks = params.tasks ?? [];
|
|
127
136
|
const parentSessionFile = (
|
|
@@ -347,6 +356,7 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
347
356
|
// captured pi) to touch. The cancelled completion path still writes one
|
|
348
357
|
// final aggregate after late task results arrive, but never delivers UI.
|
|
349
358
|
clearDelegateStatusContext();
|
|
359
|
+
registerProviderExtensionNotifier(undefined);
|
|
350
360
|
// A replacement session starts on its own leaf; stale tracking would make
|
|
351
361
|
// every ticket look cross-leaf (or, worse, look same-leaf by accident).
|
|
352
362
|
resetLeafTracking();
|