@tranhoangnguyen0310/pi-flow-external 2.1.0-external.0 → 2.1.0-external.2
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 +14 -0
- package/CONTEXT.md +2 -0
- package/README.md +15 -3
- package/package.json +1 -1
- package/src/core/agy.ts +5 -3
- package/src/core/claude.ts +4 -2
- package/src/core/codex.ts +4 -2
- package/src/core/progress.ts +7 -1
- package/src/core/run-inspection.ts +11 -2
- package/src/core/spawn.ts +8 -5
- package/src/external-help.ts +5 -2
- package/src/pi-subagent.ts +1 -1
- package/src/prompts.ts +2 -2
- package/src/workflow/script-worker.ts +4 -0
- package/src/workflow/tool.ts +7 -1
- package/src/workflow/types.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to pi-flow external are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [2.1.0-external.2] - 2026-09-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Preserved and exposed available partial assistant output on failure, cancellation, and timeout across all backend adapters (`codex`, `claude`, `agy`, and in-process `pi`). Unsuccessful terminal receipts retain the failure status and error diagnostic while appending a bounded `Interrupted output:` preview.
|
|
12
|
+
- Recovered interrupted assistant output in run evidence inspection (`external_runs(action: "inspect", view: "output")`) from completed non-done summary documents, including Pi SDK child runs.
|
|
13
|
+
- `ChildRunError` preserves `partialOutput` and `assistantOutput` for workflow scripts to inspect, and uncaught child errors surface the interrupted output preview in workflow failure receipts.
|
|
14
|
+
- Hardened CI release workflow polling loop against npm registry replication latency (increased timeout to 150s).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Coordinator guidance now names all four harnesses (`Agent` description, `buildCoordinatorPrompt` body): registered Pi harnesses are first-class alongside Claude Code/Codex CLI/Antigravity, not "external CLIs only" leftovers of the pre-2.1.0 copy. `test/agent-contract`/`agent-rendering` pin the copy.
|
|
19
|
+
- Parent guidance states the blocking default explicitly (`background:true` is opt-in, use `external_runs` on the returned handle) and how same-harness parallel work actually shares the one `maxConcurrentSubagents` limiter: workflow `parallel([() => agent(...), ...])` or separate background `Agent` calls in one turn; sequential awaits stay serial. Includes a three-`agy` workflow example plus fore/background meta examples in `external_help`. Pinned by a new `test/external-help` regression.
|
|
20
|
+
|
|
7
21
|
## [2.1.0-external.0] - 2026-09-15
|
|
8
22
|
|
|
9
23
|
Named Pi harness configurations (`pi-*`): a fourth delegation surface running in-process through Pi's own SDK, selectable exactly like `agy`/`claude`/`codex`. Design: `docs/plans/pi-named-configurations-design.md`; plan: `docs/plans/pi-named-configurations-implementation-plan.md`.
|
package/CONTEXT.md
CHANGED
|
@@ -32,6 +32,8 @@ The split is global and intentional to avoid tool ambiguity across projects. The
|
|
|
32
32
|
|
|
33
33
|
User operations are namespaced under `/external`: status, Doctor, settings, profiles, profile creation, workflows, interactive run navigation, durable run summary/pruning, and help. `/external runs` uses the same registry/readers as `external_runs`, including every cursor-paged workflow child and output/diagnostic page. Extension-owned concurrency, timeout, and global default-harness settings live in `$PI_CODING_AGENT_DIR/pi-flow-external/settings.json`; profiles remain authoritative for downstream backend/model/thinking metadata. Named Pi harness configurations live in the sibling `pi-flow-external/harnesses.json` registry. A trusted project may override only `defaultHarness`, which may itself name a registered `pi-*` harness.
|
|
34
34
|
|
|
35
|
+
> Next agent: full implementation map is `docs/ARCHITECTURE_SNAPSHOT.md` (verified 2026-09-16). Keep this file and `AGENTS.md` as the persistent breadcrumbs.
|
|
36
|
+
|
|
35
37
|
## Design stance
|
|
36
38
|
|
|
37
39
|
Guardrails exist to keep lanes from bleeding into each other (a reviewer that edits, a worker that fixes), not to constrain how a model works. Role bodies stay short: define the job, state the boundary, then get out of the way and trust the model's judgment on approach, depth, and method. The generic `worker` role covers non-coding tasks with no method constraints at all. The shipped default roster is five code-oriented roles (explorer, planner, implementer, reviewer, qa) plus `worker`; other roles are user-created through `/external profile create` and still receive the name-based execution permission floor. External agents must not be arbitrarily handcuffed just to satisfy a theoretical safety checklist: what transpires on the ground is what matters. When an external agent needs heightened permissions to inspect, run tests, and execute tools, the system defaults to providing that authority rather than blocking execution.
|
package/README.md
CHANGED
|
@@ -253,7 +253,7 @@ Workflows show access once at the workflow level, retain done/active/queued/fail
|
|
|
253
253
|
|
|
254
254
|
## Background runs and supervision
|
|
255
255
|
|
|
256
|
-
`Agent` and `workflow` are blocking by default. Add `background: true`
|
|
256
|
+
`Agent` and `workflow` are blocking by default — an ordinary call waits for its child and returns the result, run everything this way unless the parent has other work to do first. Add `background: true` only when the parent can productively proceed before completion; it returns a stable `run_...` or `wf_...` handle after validation and registration while the originating Pi session continues to own the work:
|
|
257
257
|
|
|
258
258
|
```ts
|
|
259
259
|
Agent({
|
|
@@ -275,7 +275,19 @@ Interrupting a blocking `Agent`/`workflow` call cancels its work. Interrupting `
|
|
|
275
275
|
|
|
276
276
|
## Workflow usage
|
|
277
277
|
|
|
278
|
-
The `workflow` tool runs trusted JavaScript that calls one or more external roles and returns a JSON-serializable result.
|
|
278
|
+
The `workflow` tool runs trusted JavaScript that calls one or more external roles and returns a JSON-serializable result. Workflows are for explicit fan-out — and for the case this extension was built to make explicit: running several children on the **same** harness at once. Example:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
export const meta = { apiVersion: 1, name: "triage", description: "Three parallel agy reviewers" };
|
|
282
|
+
const [a, b, c] = await parallel([
|
|
283
|
+
() => agent("Review slice A read-only: /absolute/repo", { label: "rev-a", role: "reviewer", harness: "agy" }),
|
|
284
|
+
() => agent("Review slice B read-only: /absolute/repo", { label: "rev-b", role: "reviewer", harness: "agy" }),
|
|
285
|
+
() => agent("Review slice C read-only: /absolute/repo", { label: "rev-c", role: "reviewer", harness: "agy" }),
|
|
286
|
+
]);
|
|
287
|
+
return { a, b, c };
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Sequential `await agent(...)` stays serial by construction; `parallel([...])` is how parallel children share the single `maxConcurrentSubagents` limiter at once. Top-level direct `Agent` calls issued together also run together (`executionMode: "parallel"`, default under one limiter). `external_runs wait` and `external_runs inspect` cover both surfaces. Its first statement must be the current declaration `export const meta = { apiVersion: 1, name, description }`; missing or unsupported versions fail before any child launches. Every `agent()` child uses the same `role`/optional `harness` resolution as direct `Agent` calls, with legacy exact `subagent_type` also supported.
|
|
279
291
|
|
|
280
292
|
Saved workflows are discovered on demand through `external_help({ topic: "workflow" })`; project `.pi/workflows` entries are included only when Pi reports the project trusted.
|
|
281
293
|
|
|
@@ -285,7 +297,7 @@ Example request:
|
|
|
285
297
|
Use the workflow tool to ask role "explorer" on harness "claude" for an architecture map and role "reviewer" on harness "codex" for a risk review, then synthesize their findings.
|
|
286
298
|
```
|
|
287
299
|
|
|
288
|
-
Direct `Agent` calls and workflow children share
|
|
300
|
+
Direct `Agent` calls and workflow children share one concurrency limiter (`maxConcurrentSubagents`, default 12) and the same timeout controls. There is no per-harness cap: three `agy` children run together when started together — `parallel([() => agent(..., { harness: "agy" }), ...])` in a workflow, or three `background: true` `Agent` calls in one turn followed by `external_runs wait`. Sequentially `await`ed children stay serial by construction. Direct calls default `executionMode: "parallel"` so top-level `Agent` calls issued together run together. Workflow `agent(prompt, { role, context: { mode: "recent", turns: 5 } })` accepts the same context modes. Every current-version `agent()` returns its value or throws a catchable `ChildRunError` with `runId`, `outcome` (`failed`, `cancelled`, or `timed_out`), `message`, and output/diagnostic references. Catch an optional failure explicitly; an uncaught child error fails the workflow and drains active siblings. `parallel` and `pipeline` preserve this contract and never convert failure to `null`.
|
|
289
301
|
|
|
290
302
|
Every child selects from one parent snapshot and effective settings frozen at workflow invocation; earlier child results must still be passed explicitly. `resumeFromRunId` is explicit replay for a persisted `scriptPath`: it reuses only the longest unchanged prefix of successful child calls. The API version, transferred context, prompt, selection, and relevant options participate in fingerprints. The first changed, failed, cancelled, or timed-out call and everything after it executes again. Recomposition is cheap, but rerun children may cost money or repeat side effects; the runtime never retries or replays a repaired script automatically.
|
|
291
303
|
|
package/package.json
CHANGED
package/src/core/agy.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { assistantOutput, createProgressEmitter, textResult, type AgentToolResult } from "./progress.ts";
|
|
3
|
+
import { assistantOutput, createProgressEmitter, formatInterruptedOutputPreview, textResult, type AgentToolResult } from "./progress.ts";
|
|
4
4
|
import {
|
|
5
5
|
createBoundedBuffer,
|
|
6
6
|
MAX_STDERR_CHARS,
|
|
@@ -486,7 +486,8 @@ export async function spawnAgySubagent(params: {
|
|
|
486
486
|
if (child) abortChildTree(child);
|
|
487
487
|
const message = error instanceof Error ? error.message : String(error);
|
|
488
488
|
const status = params.signal?.aborted ? "aborted" : "error";
|
|
489
|
-
const
|
|
489
|
+
const candidate = terminalResult ? textFromAgyResult(terminalResult)?.trim() : undefined;
|
|
490
|
+
const output = assistantOutput(assistantMessages, "interrupted", candidate);
|
|
490
491
|
params.onUsage(latestUsage);
|
|
491
492
|
if (progress) {
|
|
492
493
|
progress.status = status;
|
|
@@ -495,7 +496,8 @@ export async function spawnAgySubagent(params: {
|
|
|
495
496
|
progress.assistantOutput = output;
|
|
496
497
|
progress.endedAt = Date.now();
|
|
497
498
|
}
|
|
498
|
-
|
|
499
|
+
const preview = formatInterruptedOutputPreview(output);
|
|
500
|
+
return textResult(`Subagent "${params.description}" (${subagentType}) ${status === "aborted" ? "aborted" : "failed"}: ${message}${preview}`, {
|
|
499
501
|
description: params.description,
|
|
500
502
|
subagentType,
|
|
501
503
|
backend: params.profile.backend,
|
package/src/core/claude.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import {
|
|
4
4
|
assistantOutput,
|
|
5
5
|
createProgressEmitter,
|
|
6
|
+
formatInterruptedOutputPreview,
|
|
6
7
|
textResult,
|
|
7
8
|
type AgentToolResult,
|
|
8
9
|
} from "./progress.ts";
|
|
@@ -570,7 +571,7 @@ export async function spawnClaudeSubagent(params: {
|
|
|
570
571
|
if (child) abortChildTree(child);
|
|
571
572
|
const message = error instanceof Error ? error.message : String(error);
|
|
572
573
|
const status = params.signal?.aborted ? "aborted" : "error";
|
|
573
|
-
const output = assistantOutput(assistantMessages, "interrupted");
|
|
574
|
+
const output = assistantOutput(assistantMessages, "interrupted", resultText);
|
|
574
575
|
params.onUsage(latestUsage);
|
|
575
576
|
if (progress) {
|
|
576
577
|
progress.status = status;
|
|
@@ -580,7 +581,8 @@ export async function spawnClaudeSubagent(params: {
|
|
|
580
581
|
progress.endedAt = Date.now();
|
|
581
582
|
}
|
|
582
583
|
const verb = status === "aborted" ? "aborted" : "failed";
|
|
583
|
-
|
|
584
|
+
const preview = formatInterruptedOutputPreview(output);
|
|
585
|
+
return textResult(`Subagent "${params.description}" (${subagentType}) ${verb}: ${message}${preview}`, {
|
|
584
586
|
description: params.description,
|
|
585
587
|
subagentType,
|
|
586
588
|
backend: params.profile.backend,
|
package/src/core/codex.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import {
|
|
7
7
|
assistantOutput,
|
|
8
8
|
createProgressEmitter,
|
|
9
|
+
formatInterruptedOutputPreview,
|
|
9
10
|
textResult,
|
|
10
11
|
type AgentToolResult,
|
|
11
12
|
} from "./progress.ts";
|
|
@@ -533,7 +534,7 @@ export async function spawnCodexSubagent(params: {
|
|
|
533
534
|
if (child) abortChildTree(child);
|
|
534
535
|
const message = error instanceof Error ? error.message : String(error);
|
|
535
536
|
const status = params.signal?.aborted ? "aborted" : "error";
|
|
536
|
-
const output = assistantOutput(assistantMessages, "interrupted");
|
|
537
|
+
const output = assistantOutput(assistantMessages, "interrupted", resultText);
|
|
537
538
|
params.onUsage(latestUsage);
|
|
538
539
|
if (progress) {
|
|
539
540
|
progress.status = status;
|
|
@@ -543,7 +544,8 @@ export async function spawnCodexSubagent(params: {
|
|
|
543
544
|
progress.endedAt = Date.now();
|
|
544
545
|
}
|
|
545
546
|
const verb = status === "aborted" ? "aborted" : "failed";
|
|
546
|
-
|
|
547
|
+
const preview = formatInterruptedOutputPreview(output);
|
|
548
|
+
return textResult(`Subagent "${params.description}" (${subagentType}) ${verb}: ${message}${preview}`, {
|
|
547
549
|
description: params.description,
|
|
548
550
|
subagentType,
|
|
549
551
|
backend: params.profile.backend,
|
package/src/core/progress.ts
CHANGED
|
@@ -51,6 +51,12 @@ export function assistantOutput(
|
|
|
51
51
|
return output.length ? { status, messages: output } : undefined;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export function formatInterruptedOutputPreview(output: SubagentAssistantOutput | undefined): string {
|
|
55
|
+
if (!output?.messages?.length) return "";
|
|
56
|
+
const text = output.messages.map((m) => m.text.trim()).filter(Boolean).join("\n\n");
|
|
57
|
+
return text ? `\n\nInterrupted output:\n${text.slice(-4_000)}` : "";
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
export function extractAssistantMessages(messages: readonly unknown[]): Array<{ id?: string; text: string }> {
|
|
55
61
|
return messages.flatMap((value) => {
|
|
56
62
|
const message = value as { id?: unknown; role?: unknown; content?: unknown };
|
|
@@ -88,7 +94,7 @@ function getFirstTextLine(text: string): string {
|
|
|
88
94
|
return text.split("\n").find((line) => line.trim()) ?? text;
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
function extractTextContent(content: unknown): string {
|
|
97
|
+
export function extractTextContent(content: unknown): string {
|
|
92
98
|
if (typeof content === "string") {
|
|
93
99
|
return content;
|
|
94
100
|
}
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { agyActivityFromEvent } from "./agy.ts";
|
|
6
6
|
import { claudeActivityFromEvent, extractClaudeFinalText } from "./claude.ts";
|
|
7
7
|
import { codexActivityFromEvent, extractCodexFinalText } from "./codex.ts";
|
|
8
|
+
import { extractTextContent } from "./progress.ts";
|
|
8
9
|
|
|
9
10
|
const CURSOR_VERSION = 1;
|
|
10
11
|
const DEFAULT_PAGE_BYTES = 32 * 1024;
|
|
@@ -117,7 +118,7 @@ export async function inspectRun({
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
if (decoded?.kind === "inspect" && decoded.source === "summary") throw new Error("Cursor source does not match inspection view");
|
|
120
|
-
if (view === "output" && summary.document && terminalStatus === "done") {
|
|
121
|
+
if (view === "output" && summary.document && (terminalStatus === "done" || assistantItems(terminal).length > 0)) {
|
|
121
122
|
if (decoded?.kind === "inspect" && decoded.source === "events") throw staleCursorError();
|
|
122
123
|
const observation = await readObservation(eventsPath, runId);
|
|
123
124
|
const items = terminalOutputItems(terminal);
|
|
@@ -347,7 +348,7 @@ function summaryProjection(runId: string, summary: SummaryState, observation: Ru
|
|
|
347
348
|
};
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
function outputFromEvent(event: EvidenceEvent): ({ kind: "claude" | "codex" | "agy"; id?: string; text: string }) | undefined {
|
|
351
|
+
function outputFromEvent(event: EvidenceEvent): ({ kind: "claude" | "codex" | "agy" | "pi"; id?: string; text: string }) | undefined {
|
|
351
352
|
if (event.type !== "backend_event") return undefined;
|
|
352
353
|
const envelope = asRecord(event.data);
|
|
353
354
|
const backendEvent = asRecord(envelope?.event);
|
|
@@ -368,6 +369,14 @@ function outputFromEvent(event: EvidenceEvent): ({ kind: "claude" | "codex" | "a
|
|
|
368
369
|
const id = asString(update?.step_id);
|
|
369
370
|
return text ? { kind: "agy", ...(id ? { id } : {}), text } : undefined;
|
|
370
371
|
}
|
|
372
|
+
if (envelope?.backend === "pi" && backendEvent.type === "message_end") {
|
|
373
|
+
const message = asRecord(backendEvent.message);
|
|
374
|
+
if (message?.role === "assistant") {
|
|
375
|
+
const text = extractTextContent(message.content);
|
|
376
|
+
const id = asString(message.id);
|
|
377
|
+
return text ? { kind: "pi", ...(id ? { id } : {}), text } : undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
371
380
|
return undefined;
|
|
372
381
|
}
|
|
373
382
|
|
package/src/core/spawn.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
extractFinalAssistantText,
|
|
14
14
|
extractAssistantMessages,
|
|
15
15
|
assistantOutput,
|
|
16
|
+
formatInterruptedOutputPreview,
|
|
16
17
|
getFinalAssistantFailure,
|
|
17
18
|
getSubagentUsage,
|
|
18
19
|
textResult,
|
|
@@ -223,8 +224,7 @@ function rewriteTimeoutResult(
|
|
|
223
224
|
): AgentToolResult {
|
|
224
225
|
const details = markSubagentTimedOut(result.details as SubagentToolDetails, params.timeoutMs);
|
|
225
226
|
const message = details.error;
|
|
226
|
-
const
|
|
227
|
-
const preview = partial ? `\n\nInterrupted output:\n${partial.slice(-4_000)}` : "";
|
|
227
|
+
const preview = formatInterruptedOutputPreview(details.assistantOutput);
|
|
228
228
|
return textResult(`Subagent "${params.description}" (${params.profile.name}) aborted: ${message}${preview}`, {
|
|
229
229
|
...details,
|
|
230
230
|
description: params.description,
|
|
@@ -345,11 +345,13 @@ export async function spawnSubagent(params: SpawnSubagentParams): Promise<AgentT
|
|
|
345
345
|
const details = result.details as SubagentToolDetails;
|
|
346
346
|
if (details.status === "aborted") {
|
|
347
347
|
const reason = params.signal.reason instanceof Error ? params.signal.reason.message : String(params.signal.reason);
|
|
348
|
-
const previous = details.error;
|
|
349
348
|
details.error = reason;
|
|
350
349
|
if (details.progress) details.progress.error = reason;
|
|
351
350
|
const first = result.content[0];
|
|
352
|
-
if (first?.type === "text")
|
|
351
|
+
if (first?.type === "text") {
|
|
352
|
+
const preview = formatInterruptedOutputPreview(details.assistantOutput);
|
|
353
|
+
first.text = `Subagent "${params.description}" (${params.profile.name}) aborted: ${reason}${preview}`;
|
|
354
|
+
}
|
|
353
355
|
}
|
|
354
356
|
}
|
|
355
357
|
if (params.context) {
|
|
@@ -829,7 +831,8 @@ async function spawnSubagentRuntime(params: SpawnSubagentRuntimeParams): Promise
|
|
|
829
831
|
if (thinkingClamped) progress.thinkingClamped = thinkingClamped;
|
|
830
832
|
}
|
|
831
833
|
const verb = status === "aborted" ? "aborted" : "failed";
|
|
832
|
-
|
|
834
|
+
const preview = formatInterruptedOutputPreview(output);
|
|
835
|
+
return textResult(`Subagent "${description}" (${subagentType}) ${verb}: ${message}${preview}`, {
|
|
833
836
|
description,
|
|
834
837
|
subagentType,
|
|
835
838
|
backend: profile.backend,
|
package/src/external-help.ts
CHANGED
|
@@ -94,11 +94,14 @@ Provide exactly one workflow source: name, scriptPath, or script. Every script s
|
|
|
94
94
|
|
|
95
95
|
APIs: agent(prompt, { label, role, harness, subagent_type, permission, max_budget_usd, resume, context, schema, phase }); parallel(thunks); pipeline(items, ...stages); phase(title); log(message). Globals: args and cwd. Agent options other than the profile selector are optional. Choose role/harness or legacy exact subagent_type, never both. Use unique labels and clear task prompts. An agent() succeeds with its value or throws ChildRunError { runId, outcome, message, outputRef, diagnosticsRef }; catch optional failures explicitly. Uncaught failures terminate the workflow and drain siblings. Helpers never convert failures to null. Context options: {mode:"none"} (default), {mode:"recent",turns:N} (positive integer, includes current user turn), or {mode:"full"} (available context after compaction). All children share invocation-time context/settings; earlier child results must still be passed explicitly. Context excludes thinking/system instructions and pending calls; images and snapshots over 1 MiB fail without truncation. Context sharing cannot be combined with resume. Use schema for results that control branching or aggregation. Imports, filesystem globals, Date APIs, and Math.random() are unavailable.
|
|
96
96
|
|
|
97
|
-
Background and supervision:
|
|
97
|
+
Background and supervision: blocking by default — an ordinary call waits for its child and returns the result, run everything this way unless the parent has other work to do first. Set background:true only when the parent can proceed before completion, then use external_runs on the returned stable run ID while session-owned work continues. Parallel same-harness work (e.g. three agy workers) runs concurrently under the shared maxConcurrentSubagents cap: call parallel([() => agent(...), ...]) in a workflow, or issue separate Agent calls with background:true in one turn, then external_runs wait/inspect/cancel. Awaiting agent() calls sequentially stays serial by construction. external_runs actions are list (optional workflowRunId/cursor/workflowCursor/limit), inspect (runId, view summary|output|diagnostics, optional opaque cursor/limitBytes), wait (runId or runIds, mode any|all), and cancel (runId, optional reason). Follow nextCursor/nextWorkflowCursor for complete results. Wait returns selected terminal outcomes and pending IDs, never cancels pending work, and returns an unsuccessful workflow early; interrupting wait stops only the wait. Cancelling a workflow stops active children, while cancelling one child is a catchable workflow error. Blocking-call interruption cancels that call; background work survives tool return but is cancelled on orderly owning-session shutdown. It is not a daemon: crashes leave unfinished evidence interrupted/uncertain, restart does not adopt work, and live steering is unavailable.
|
|
98
98
|
|
|
99
99
|
Replay: resumeFromRunId works only with persisted scriptPath and starts an explicit new attempt. It reuses the longest unchanged prefix of successful child calls; the first changed/failed/cancelled/timed-out call and its suffix run again. Script recomposition is cheap, but child reruns can cost money or repeat side effects. There is no automatic repaired-script replay.
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
Background example:
|
|
102
|
+
export const meta = { apiVersion: 1, name: "review", description: "Map and review a repository", background: true };
|
|
103
|
+
|
|
104
|
+
Foreground example:
|
|
102
105
|
export const meta = { apiVersion: 1, name: "review", description: "Map and review a repository" };
|
|
103
106
|
const [map, review] = await parallel([
|
|
104
107
|
() => agent("Map /absolute/repo read-only.", { label: "map", role: "explorer" }),
|
package/src/pi-subagent.ts
CHANGED
|
@@ -352,7 +352,7 @@ function createAgentTool(
|
|
|
352
352
|
return defineTool({
|
|
353
353
|
name: "Agent",
|
|
354
354
|
label: "Agent",
|
|
355
|
-
description: "Delegate one task to an external Claude Code, Codex CLI, or
|
|
355
|
+
description: "Delegate one task to an external Claude Code, Codex CLI, Antigravity, or registered Pi harness role.",
|
|
356
356
|
promptSnippet: AGENT_PROMPT_SNIPPET,
|
|
357
357
|
parameters: agentToolParameters,
|
|
358
358
|
executionMode: "parallel",
|
package/src/prompts.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { EXTERNAL_HARNESSES, type SubagentProfile } from "./types.ts";
|
|
|
3
3
|
import { externalProfileRole, externalRoleAvailability } from "./profiles.ts";
|
|
4
4
|
|
|
5
5
|
export const AGENT_PROMPT_SNIPPET =
|
|
6
|
-
"Delegate one task to an external Claude Code, Codex CLI, or
|
|
6
|
+
"Delegate one task to an external Claude Code, Codex CLI, Antigravity, or registered Pi harness role.";
|
|
7
7
|
|
|
8
8
|
export const WORKFLOW_PROMPT_SNIPPET =
|
|
9
9
|
"Run requested multi-agent orchestration with external roles; use external_help for syntax and saved workflows.";
|
|
@@ -92,5 +92,5 @@ export function buildCoordinatorPrompt(
|
|
|
92
92
|
|
|
93
93
|
${formatExternalRoleCatalog(profiles, defaultHarness, configuredHarnessNames)}
|
|
94
94
|
|
|
95
|
-
Catalog availability reflects configured profiles, not CLI installation or authentication. External delegation tools use external CLIs
|
|
95
|
+
Catalog availability reflects configured profiles, not CLI installation or authentication. External delegation tools use external CLIs and registered Pi harnesses; use native Pi subagents for Pi-backed work. Give each child a clear task, absolute paths, and read-only/edit intent; nested agents may start elsewhere. Share parent context explicitly when a child needs it: context: {mode:"recent", turns:N} for the last N user turns (including the current turn), {mode:"full"} for available post-compaction conversation, or omit it for independent tasks. Prefer the smallest sufficient snapshot; snapshots exclude thinking/system instructions and pending calls, fail on images or over 1 MiB, and may contain sensitive data sent to the external harness. Use resume to follow up on an existing child; never combine it with sharing. This is text transfer, not a native session clone or guaranteed cache reuse. Blocking by default; set background:true only when the parent can proceed before completion, then use external_runs on the returned run ID. Parallel same-harness work shares one maxConcurrentSubagents cap: use parallel([() => agent(...), ...]) or separate background Agent calls in one turn; sequential awaits stay serial. External CLIs have host access, and agy always runs unsandboxed (readonly/edit are advisory). Role selection never falls back. Do not retry failed runs: agy alone may make one disclosed infrastructure retry. Use external_help for role descriptions, permission details, workflow syntax, supervision syntax, and saved workflows.`;
|
|
96
96
|
}
|
|
@@ -63,6 +63,8 @@ class ChildRunError extends Error {
|
|
|
63
63
|
this.outcome = error.outcome;
|
|
64
64
|
this.outputRef = error.outputRef;
|
|
65
65
|
this.diagnosticsRef = error.diagnosticsRef;
|
|
66
|
+
this.assistantOutput = error.assistantOutput;
|
|
67
|
+
this.partialOutput = error.partialOutput;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -116,6 +118,8 @@ function postError(error) {
|
|
|
116
118
|
message: error.message,
|
|
117
119
|
...(error.outputRef ? { outputRef: error.outputRef } : {}),
|
|
118
120
|
...(error.diagnosticsRef ? { diagnosticsRef: error.diagnosticsRef } : {}),
|
|
121
|
+
...(error.assistantOutput ? { assistantOutput: error.assistantOutput } : {}),
|
|
122
|
+
...(error.partialOutput ? { partialOutput: error.partialOutput } : {}),
|
|
119
123
|
} } : {}),
|
|
120
124
|
});
|
|
121
125
|
} finally {
|
package/src/workflow/tool.ts
CHANGED
|
@@ -306,12 +306,15 @@ export function createWorkflowTool(
|
|
|
306
306
|
}
|
|
307
307
|
if (resultDetails.status !== "done") {
|
|
308
308
|
const runId = resultDetails.runId ?? call.runRecord?.runId ?? childId;
|
|
309
|
+
const partialOutput = resultDetails.assistantOutput?.messages.map((m) => m.text.trim()).filter(Boolean).join("\n\n");
|
|
309
310
|
throw new ChildRunError({
|
|
310
311
|
runId,
|
|
311
312
|
outcome: childOutcome(resultDetails),
|
|
312
313
|
message: resultDetails.error ?? "subagent failed",
|
|
313
314
|
outputRef: { runId, view: "output" },
|
|
314
315
|
diagnosticsRef: { runId, view: "diagnostics" },
|
|
316
|
+
...(resultDetails.assistantOutput ? { assistantOutput: resultDetails.assistantOutput } : {}),
|
|
317
|
+
...(partialOutput ? { partialOutput } : {}),
|
|
315
318
|
});
|
|
316
319
|
}
|
|
317
320
|
if (externalOutputSchema) {
|
|
@@ -528,8 +531,11 @@ export function createWorkflowTool(
|
|
|
528
531
|
agent.endedAt = Date.now();
|
|
529
532
|
}
|
|
530
533
|
}
|
|
534
|
+
const partial = error instanceof ChildRunError && error.partialOutput
|
|
535
|
+
? `\n\nInterrupted child output (${error.runId}):\n${error.partialOutput.slice(-4_000)}`
|
|
536
|
+
: "";
|
|
531
537
|
return workflowResult(
|
|
532
|
-
`Workflow "${metaName}" ${aborted ? "aborted" : "failed"}: ${message}${formatRecentLogs(snapshot.logs)}`,
|
|
538
|
+
`Workflow "${metaName}" ${aborted ? "aborted" : "failed"}: ${message}${partial}${formatRecentLogs(snapshot.logs)}`,
|
|
533
539
|
cloneSnapshot(snapshot),
|
|
534
540
|
);
|
|
535
541
|
} finally {
|
package/src/workflow/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ParentContextMessages, ParentContextReceipt } from "../core/parent-context.ts";
|
|
2
2
|
import type { ConcurrencyLimiter } from "../core/concurrency.ts";
|
|
3
3
|
import type { RunRecord } from "../core/run-record.ts";
|
|
4
|
+
import type { SubagentAssistantOutput } from "../types.ts";
|
|
4
5
|
|
|
5
6
|
export type ChildRunOutcome = "failed" | "cancelled" | "timed_out";
|
|
6
7
|
|
|
@@ -15,6 +16,8 @@ export interface SerializedChildRunError {
|
|
|
15
16
|
message: string;
|
|
16
17
|
outputRef?: ChildRunReference;
|
|
17
18
|
diagnosticsRef?: ChildRunReference;
|
|
19
|
+
assistantOutput?: SubagentAssistantOutput;
|
|
20
|
+
partialOutput?: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export class ChildRunError extends Error {
|
|
@@ -22,6 +25,8 @@ export class ChildRunError extends Error {
|
|
|
22
25
|
readonly outcome: ChildRunOutcome;
|
|
23
26
|
readonly outputRef?: ChildRunReference;
|
|
24
27
|
readonly diagnosticsRef?: ChildRunReference;
|
|
28
|
+
readonly assistantOutput?: SubagentAssistantOutput;
|
|
29
|
+
readonly partialOutput?: string;
|
|
25
30
|
|
|
26
31
|
constructor(error: SerializedChildRunError) {
|
|
27
32
|
super(error.message);
|
|
@@ -30,6 +35,8 @@ export class ChildRunError extends Error {
|
|
|
30
35
|
this.outcome = error.outcome;
|
|
31
36
|
this.outputRef = error.outputRef;
|
|
32
37
|
this.diagnosticsRef = error.diagnosticsRef;
|
|
38
|
+
this.assistantOutput = error.assistantOutput;
|
|
39
|
+
this.partialOutput = error.partialOutput;
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -40,6 +47,8 @@ export function childRunErrorData(error: ChildRunError): SerializedChildRunError
|
|
|
40
47
|
message: error.message,
|
|
41
48
|
...(error.outputRef ? { outputRef: error.outputRef } : {}),
|
|
42
49
|
...(error.diagnosticsRef ? { diagnosticsRef: error.diagnosticsRef } : {}),
|
|
50
|
+
...(error.assistantOutput ? { assistantOutput: error.assistantOutput } : {}),
|
|
51
|
+
...(error.partialOutput ? { partialOutput: error.partialOutput } : {}),
|
|
43
52
|
};
|
|
44
53
|
}
|
|
45
54
|
|