@bermudi/pi-delegate 0.1.15 → 0.1.17
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/agents.ts +2 -2
- package/constants.ts +10 -0
- package/delegate.ts +1 -0
- package/dispatch.ts +21 -7
- package/extension.ts +55 -1
- package/lifecycle.ts +17 -6
- package/manual.ts +5 -3
- package/package.json +1 -1
- package/render-branches.ts +2 -1
- package/schema.ts +163 -79
- package/task-resolution.ts +39 -19
- package/telemetry.ts +86 -11
- package/test-preload.ts +26 -0
- package/ticket-format.ts +6 -2
- package/types.ts +22 -4
- package/workspace.ts +183 -58
package/agents.ts
CHANGED
|
@@ -120,7 +120,7 @@ export const BUILTIN_AGENT_CONFIGS: Readonly<Record<string, AgentConfig>> = {
|
|
|
120
120
|
[DEFAULT_AGENT_NAME]: {
|
|
121
121
|
name: DEFAULT_AGENT_NAME,
|
|
122
122
|
description:
|
|
123
|
-
"
|
|
123
|
+
"General-purpose subagent mirroring the live parent model, thinking level, native tools, and base prompt. Prefer it for general work; pick a specialist only when its role, tool limits, or scratch workspace genuinely fit.",
|
|
124
124
|
tools: DEFAULT_TOOLS,
|
|
125
125
|
systemPrompt: "",
|
|
126
126
|
builtin: true,
|
|
@@ -151,7 +151,7 @@ export const BUILTIN_AGENT_CONFIGS: Readonly<Record<string, AgentConfig>> = {
|
|
|
151
151
|
systemPrompt:
|
|
152
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
153
|
builtin: true,
|
|
154
|
-
workspace: "
|
|
154
|
+
workspace: "shared",
|
|
155
155
|
},
|
|
156
156
|
};
|
|
157
157
|
|
package/constants.ts
CHANGED
|
@@ -59,3 +59,13 @@ export const VALID_THINKING_LEVELS = [
|
|
|
59
59
|
* `Set<VALID_THINKING_LEVELS[number]>`) so the `.has(string)` call sites in
|
|
60
60
|
* task-resolution.ts and agents.ts stay ergonomic under strict typing. */
|
|
61
61
|
export const VALID_THINKING: Set<string> = new Set(VALID_THINKING_LEVELS);
|
|
62
|
+
|
|
63
|
+
/** Session-control actions: RPC against the session pool, not work. Callers
|
|
64
|
+
* spell them at the TOP level (`sessionAction`, one action per call — #32).
|
|
65
|
+
* This predicate remains the one shared spelling of the control set for the
|
|
66
|
+
* session-mode intent check and validator in schema.ts and the pipeline
|
|
67
|
+
* guards over internally bridged tasks (task-resolution.ts). Per-action
|
|
68
|
+
* behavior lives at the branch sites (e.g. lifecycle applySessionAction). */
|
|
69
|
+
export function isSessionControlAction(action: unknown): boolean {
|
|
70
|
+
return action === "close" || action === "list";
|
|
71
|
+
}
|
package/delegate.ts
CHANGED
package/dispatch.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
DelegateToolResult,
|
|
49
49
|
ParentAgentDefaults,
|
|
50
50
|
ResolvedTask,
|
|
51
|
+
DispatchableTask,
|
|
51
52
|
TaskDef,
|
|
52
53
|
TaskProgress,
|
|
53
54
|
TaskResult,
|
|
@@ -58,7 +59,7 @@ const UNSAFE_SHARED_WRITES_WARNING =
|
|
|
58
59
|
"UNSAFE SHARED WRITES ENABLED: shared-write admission is bypassed. Delegate provides no isolation or rollback.";
|
|
59
60
|
|
|
60
61
|
interface ActiveSyncDispatch {
|
|
61
|
-
tasks:
|
|
62
|
+
tasks: DispatchableTask[];
|
|
62
63
|
resolved: ResolvedTask[];
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -157,7 +158,7 @@ export function makeFireUpdater(
|
|
|
157
158
|
export interface AsyncDispatchInput {
|
|
158
159
|
pi: ExtensionAPI;
|
|
159
160
|
ctx: DelegateToolCtx;
|
|
160
|
-
tasks:
|
|
161
|
+
tasks: DispatchableTask[];
|
|
161
162
|
resolved: ResolvedTask[];
|
|
162
163
|
progress: TaskProgress[];
|
|
163
164
|
parentModelId: string | undefined;
|
|
@@ -169,7 +170,7 @@ export interface AsyncDispatchInput {
|
|
|
169
170
|
/** Inputs needed by the sync (blocking) dispatch path. */
|
|
170
171
|
export interface SyncDispatchInput {
|
|
171
172
|
ctx: DelegateToolCtx;
|
|
172
|
-
tasks:
|
|
173
|
+
tasks: DispatchableTask[];
|
|
173
174
|
resolved: ResolvedTask[];
|
|
174
175
|
progress: TaskProgress[];
|
|
175
176
|
parentModelId: string | undefined;
|
|
@@ -193,7 +194,7 @@ export interface DelegateDispatchInput {
|
|
|
193
194
|
callSpan?: CallSpan;
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
function taskReference(task:
|
|
197
|
+
function taskReference(task: DispatchableTask, index: number): string {
|
|
197
198
|
return `Task ${index + 1}${task.id ? `#${task.id}` : ""}`;
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -207,7 +208,7 @@ function asAdmissionWriter(task: ResolvedTask): ResolvedTask {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
function sharedWriteRejection(
|
|
210
|
-
tasks:
|
|
211
|
+
tasks: DispatchableTask[],
|
|
211
212
|
parentModelId: string | undefined,
|
|
212
213
|
conflicts: SharedWriteConflict[],
|
|
213
214
|
references: readonly string[] = tasks.map(taskReference),
|
|
@@ -236,7 +237,7 @@ function sharedWriteRejection(
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
function sharedWriteSafetyFailure(
|
|
239
|
-
tasks:
|
|
240
|
+
tasks: DispatchableTask[],
|
|
240
241
|
parentModelId: string | undefined,
|
|
241
242
|
error: unknown,
|
|
242
243
|
): DelegateToolResult {
|
|
@@ -289,13 +290,26 @@ export async function dispatchDelegate(
|
|
|
289
290
|
return validationError;
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
const
|
|
293
|
+
const resolveResult = resolveTasks(
|
|
293
294
|
tasks,
|
|
294
295
|
ctx,
|
|
295
296
|
agents,
|
|
296
297
|
parentDefaults,
|
|
297
298
|
dispatchConfig,
|
|
298
299
|
);
|
|
300
|
+
if (resolveResult.error !== undefined) {
|
|
301
|
+
callSpan?.finish({
|
|
302
|
+
status: "failed",
|
|
303
|
+
totalTokens: 0,
|
|
304
|
+
totalCost: 0,
|
|
305
|
+
wallMs: Date.now() - callSpan.startedAt,
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: "text", text: resolveResult.error }],
|
|
309
|
+
details: { tasks, results: [], progress: [], parentModel: parentModelId },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const resolved = resolveResult.tasks;
|
|
299
313
|
if (params.async && resolved.some((task) => task.workspace === "isolated")) {
|
|
300
314
|
callSpan?.finish({
|
|
301
315
|
status: "failed",
|
package/extension.ts
CHANGED
|
@@ -44,7 +44,11 @@ import {
|
|
|
44
44
|
prepareTelemetryForSession,
|
|
45
45
|
sealTelemetryWrites,
|
|
46
46
|
} from "./telemetry.ts";
|
|
47
|
-
import type {
|
|
47
|
+
import type {
|
|
48
|
+
DelegateArguments,
|
|
49
|
+
DelegateDetails,
|
|
50
|
+
DispatchableTask,
|
|
51
|
+
} from "./types.ts";
|
|
48
52
|
|
|
49
53
|
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 10_000;
|
|
50
54
|
let shutdownDrainTimeoutMs = DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS;
|
|
@@ -52,6 +56,21 @@ let shutdownDrainTimeoutMs = DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS;
|
|
|
52
56
|
type ShutdownDrainResult =
|
|
53
57
|
{ drained: true; failures: unknown[] } | { drained: false; failures: [] };
|
|
54
58
|
|
|
59
|
+
/** Bridge the promoted top-level session RPC (`sessionAction` + `sessionId`)
|
|
60
|
+
* to the internal single-entry batch the runner executes. Internal only — the
|
|
61
|
+
* public task schema has no `sessionAction`; validation guarantees close
|
|
62
|
+
* carries a sessionId by this point. `list` never carries a sessionId: it does
|
|
63
|
+
* not target a specific session, and attaching one would make validateTasks
|
|
64
|
+
* run busy/quarantine checks that can fail the list call. */
|
|
65
|
+
function bridgeSessionControlTask(params: DelegateArguments): DispatchableTask {
|
|
66
|
+
return {
|
|
67
|
+
...(params.sessionAction === "close" && params.sessionId
|
|
68
|
+
? { sessionId: params.sessionId }
|
|
69
|
+
: {}),
|
|
70
|
+
sessionAction: params.sessionAction,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
/** Wait for shutdown workers without allowing a stuck provider/tool to hold
|
|
56
75
|
* Pi's reload or exit hostage. The allSettled promise is intentionally left
|
|
57
76
|
* attached after timeout so a late rejection cannot become unhandled. */
|
|
@@ -160,6 +179,11 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
160
179
|
let mode: string;
|
|
161
180
|
if (params.ticketAction) {
|
|
162
181
|
mode = params.ticketAction;
|
|
182
|
+
} else if (params.sessionAction) {
|
|
183
|
+
// Session RPC is its own mode, not a task shape — label it before the
|
|
184
|
+
// tasks-based branches so close/list spans stop misreporting as
|
|
185
|
+
// "sync" with a zero task count.
|
|
186
|
+
mode = "session";
|
|
163
187
|
} else if (tasks.length === 0) {
|
|
164
188
|
mode = "manual";
|
|
165
189
|
} else if (params.async) {
|
|
@@ -173,6 +197,7 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
173
197
|
mode,
|
|
174
198
|
taskCount,
|
|
175
199
|
parentSessionFile,
|
|
200
|
+
parentCwd: ctx.cwd,
|
|
176
201
|
});
|
|
177
202
|
|
|
178
203
|
function failCall(): void {
|
|
@@ -246,6 +271,35 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
246
271
|
// silently swap the agent roster.
|
|
247
272
|
const agents = discoverAgents(ctx.cwd);
|
|
248
273
|
|
|
274
|
+
// ── Session RPC (top-level sessionAction, #32 promotion) ─────────
|
|
275
|
+
// Session control is no longer spelled as a task, but it still runs
|
|
276
|
+
// through the one runner: bridge it to an internal single-entry batch so
|
|
277
|
+
// busy-index validation, per-session locking (lifecycle's literal
|
|
278
|
+
// per-action branches), progress rows, and result formatting all keep
|
|
279
|
+
// their existing behavior. Never async — validation rejects that.
|
|
280
|
+
// Must precede the help short-circuit: session RPC carries no tasks and
|
|
281
|
+
// must not be mistaken for an empty help request.
|
|
282
|
+
if (params.sessionAction) {
|
|
283
|
+
invalidateHostDepsCache();
|
|
284
|
+
// Keep the footer-status pipeline in step exactly as a normal
|
|
285
|
+
// dispatch would (deduped no-op when nothing is running).
|
|
286
|
+
syncDelegateStatus(ctx);
|
|
287
|
+
return await dispatchDelegate({
|
|
288
|
+
pi,
|
|
289
|
+
params: { ...params, tasks: [bridgeSessionControlTask(params)] },
|
|
290
|
+
ctx,
|
|
291
|
+
agents,
|
|
292
|
+
parentModelId,
|
|
293
|
+
parentDefaults: {
|
|
294
|
+
thinking: pi.getThinkingLevel(),
|
|
295
|
+
tools: pi.getActiveTools(),
|
|
296
|
+
},
|
|
297
|
+
signal,
|
|
298
|
+
onUpdate,
|
|
299
|
+
callSpan,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
249
303
|
// ── Help mode ─────────────────────────────────────────────────
|
|
250
304
|
if (!tasks.length) {
|
|
251
305
|
succeedCall();
|
package/lifecycle.ts
CHANGED
|
@@ -142,9 +142,18 @@ function scratchSetupFailureResult(
|
|
|
142
142
|
if (signalAborted) {
|
|
143
143
|
message = "Aborted";
|
|
144
144
|
failureKind = "cancelled";
|
|
145
|
-
} else
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
} else {
|
|
146
|
+
if (error instanceof ScratchDeadlineError) {
|
|
147
|
+
message = formatDeadlineExceededError(task.deadlineMs ?? 0);
|
|
148
|
+
failureKind = "deadline_exceeded";
|
|
149
|
+
}
|
|
150
|
+
// Every non-aborted setup failure names its remedy in one place, so the
|
|
151
|
+
// paths (platform guard, pre-check, reflink, worktree, deadline) cannot
|
|
152
|
+
// drift. The symlink message already carries remedy text naming
|
|
153
|
+
// workspace "shared" — skip the append there to avoid saying it twice.
|
|
154
|
+
if (!message.includes('workspace "shared"')) {
|
|
155
|
+
message = `${message} — to retry without scratch containment, use workspace: "shared".`;
|
|
156
|
+
}
|
|
148
157
|
}
|
|
149
158
|
return {
|
|
150
159
|
...failTask(task, message, undefined, failureKind),
|
|
@@ -433,9 +442,10 @@ function recordTaskOutcome(
|
|
|
433
442
|
* that model, so same-model retry is pointless. Distinguished from a bare
|
|
434
443
|
* transient 429 (per-minute rate limit) by the *account-level* wording:
|
|
435
444
|
* "usage limit", "quota", "upgrade for higher limits", "exceeded your
|
|
436
|
-
* … quota",
|
|
437
|
-
* port like 4019 doesn't false-positive)
|
|
438
|
-
*
|
|
445
|
+
* … quota", an auth/credential failure (401/403 with word boundaries so a
|
|
446
|
+
* port like 4019 doesn't false-positive), or an invalidated OAuth token
|
|
447
|
+
* ("invalid"/"invalidated" anywhere alongside "oauth token"). The parent
|
|
448
|
+
* should resume with a different `model` (see `resumeFrom` + `model`). */
|
|
439
449
|
export function isModelAttributableError(error: string | undefined): boolean {
|
|
440
450
|
if (!error) return false;
|
|
441
451
|
const e = error.toLowerCase();
|
|
@@ -454,6 +464,7 @@ export function isModelAttributableError(error: string | undefined): boolean {
|
|
|
454
464
|
e.includes("authentication") ||
|
|
455
465
|
e.includes("invalid api key") ||
|
|
456
466
|
(e.includes("api key") && e.includes("invalid")) ||
|
|
467
|
+
(e.includes("oauth token") && e.includes("invalid")) ||
|
|
457
468
|
/\b401\b/.test(e) ||
|
|
458
469
|
/\b403\b/.test(e)
|
|
459
470
|
);
|
package/manual.ts
CHANGED
|
@@ -138,6 +138,8 @@ export function getSubagentManualMarkdown(
|
|
|
138
138
|
"",
|
|
139
139
|
...builtinLines,
|
|
140
140
|
"",
|
|
141
|
+
"Prefer `default` for general work: it is the only built-in guaranteed to run the parent's exact model and thinking. `scout`/`coder`/`reviewer` apply any configured delegate.json tiers (`agentOverrides`, `agentOverridesByParentModel`) and may run a different model or thinking level than the parent. `scout` is read-only, and `reviewer` runs in a disposable scratch workspace — its writes are discarded by design. Choose a specialist for its role, not its name.",
|
|
142
|
+
"",
|
|
141
143
|
"Fresh built-ins inherit the parent's exact model object and thinking level. A same-named Markdown file can override any built-in (first definition wins); an explicit `model` or `thinking` in that file replaces parent inheritance. Task-level `model`/`thinking`/`tools` always win. For `scout`/`coder`/`reviewer`, delegate.json overrides (`agentOverrides`, `agentOverridesByParentModel`) win over the Markdown file; `default` ignores overrides and uses only an explicit Markdown `model`/`thinking` when present. A prompt-only Markdown override keeps the built-in's tools and workspace, so `scout` stays read-only and `reviewer` stays scratch unless the file explicitly changes them. Parent extension/MCP tools are not copied. Parent-global `AGENTS.md` instructions are also excluded. Project-local context and skills are rebuilt for the task's `cwd`; per-task fields remain explicit overrides.",
|
|
142
144
|
"",
|
|
143
145
|
"## Available Custom Agents",
|
|
@@ -177,10 +179,10 @@ export function getSubagentManualMarkdown(
|
|
|
177
179
|
'delegate({ tasks: [{ prompt: "Now check the tests for that module", sessionId: "auth-research" }] })',
|
|
178
180
|
"",
|
|
179
181
|
"// Clean up when done",
|
|
180
|
-
'delegate({
|
|
182
|
+
'delegate({ sessionAction: "close", sessionId: "auth-research" })',
|
|
181
183
|
"```",
|
|
182
184
|
"",
|
|
183
|
-
'Pooled sessions remain live until `sessionAction: "close"` or parent Pi session shutdown.',
|
|
185
|
+
'Pooled sessions remain live until a top-level `sessionAction: "close"` or parent Pi session shutdown.',
|
|
184
186
|
"",
|
|
185
187
|
"## Resuming Previous Sessions",
|
|
186
188
|
"",
|
|
@@ -239,7 +241,7 @@ export function getSubagentManualMarkdown(
|
|
|
239
241
|
'- Shared writers that overlap tasks in the same call or a running sync/async dispatch are rejected. Unknown tools count as mutating. Run them sequentially, use `workspace: "isolated"` for Git-backed ordered reconciliation, or use `workspace: "scratch"` when changes may be discarded.',
|
|
240
242
|
"- `*` means read/write/edit/bash, not every tool. `grep`, `find`, and `ls` are valid explicit tools and are the `ro` preset.",
|
|
241
243
|
'- `tasks` is an array. The tool recovers common stringified calls for compatibility, but canonical calls use `{ tasks: [{ prompt: "..." }] }`.',
|
|
242
|
-
'-
|
|
244
|
+
'- Prefer `agent: "default"` — the parent\'s live model/thinking/native tools/base prompt — for general tasks. Pick `scout`/`coder`/`reviewer` only when the role, tool limits, or scratch workspace fit; they follow delegate.json tiers and may not run the parent model. Omitting `agent` creates an ad-hoc task (parent model, full tools).',
|
|
243
245
|
"- Omit `thinking` for named agents unless the user asks or the task clearly needs escalation — task-level `thinking` overrides every configured tier (`agentOverrides`, `agentOverridesByParentModel`, Markdown frontmatter, `:level` model suffix), so a casual value silently defeats the configured budget. Ad-hoc tasks default to the parent's thinking.",
|
|
244
246
|
"- An ad-hoc task with no `tools` uses `*`; a named custom task uses its profile; a profile with no tools uses `*`.",
|
|
245
247
|
"- Subagents inherit all skills discovered in their `cwd` (via AgentSession's resource loader). Per-task skill filtering is not supported — curate the cwd's skill set instead.",
|
package/package.json
CHANGED
package/render-branches.ts
CHANGED
|
@@ -470,7 +470,8 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
470
470
|
);
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
// Warnings (e.g.
|
|
473
|
+
// Warnings (e.g. scratch-workspace notices, ignored model suffix) — muted
|
|
474
|
+
// line under the task.
|
|
474
475
|
pushWarnings(p, ind);
|
|
475
476
|
|
|
476
477
|
// Tool activities: compact summary only in expanded mode, terminal tasks only.
|