@tranhoangnguyen0310/pi-flow-external 2.3.0-external.0 → 2.3.1-external.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/AGENTS.md +3 -1
- package/CHANGELOG.md +29 -0
- package/README.md +22 -2
- package/package.json +1 -1
- package/src/core/agent-snapshot.ts +76 -0
- package/src/core/agy.ts +4 -0
- package/src/core/claude.ts +4 -0
- package/src/core/codex.ts +4 -0
- package/src/core/permissions.ts +4 -1
- package/src/core/progress.ts +6 -2
- package/src/core/run-inspection.ts +33 -1
- package/src/core/run-projection.ts +233 -0
- package/src/core/run-render.ts +77 -0
- package/src/core/spawn.ts +9 -0
- package/src/core/subagent-render.ts +43 -3
- package/src/external-command.ts +1 -53
- package/src/external-help.ts +10 -3
- package/src/external-runs.ts +354 -152
- package/src/pi-subagent.ts +7 -2
- package/src/profiles.ts +8 -2
- package/src/types.ts +20 -0
- package/src/workflow/runtime-values.ts +12 -1
- package/src/workflow/tool.ts +29 -53
package/AGENTS.md
CHANGED
|
@@ -13,6 +13,7 @@ This fork changes the original pi-flow contract: `Agent` is not a generic Pi sub
|
|
|
13
13
|
- A `backend: pi` profile is external-delegation-eligible only when it also declares `harness: <name>` for a name present in the live `harnesses.json` registry; a bare `backend: pi` profile (no `harness`, or one naming an unregistered config) is filtered out and rejected exactly as before — it belongs to Pi's native subagent system and this extension never modifies it.
|
|
14
14
|
- **Named Pi harness configuration:** a `pi-<label>` entry in `harnesses.json` pinning a `provider/model` id (resolved through Pi's own model registry) and a thinking level (`off|minimal|low|medium|high|xhigh`, always persisted explicitly). It runs in-process via Pi's own SDK, not as a spawned CLI. A pi child loads no project/user extensions, skills, prompt templates, or themes — its entire tool surface is the SDK's own builtins (`read`/`bash`/`edit`/`write`, plus `grep`/`find`/`ls` where a tier's default allow-list adds them), curated but never claimed to be an OS sandbox: `danger` tier `bash` is exactly as exposed as on any external CLI. Retry is disabled per child (in-memory, call-scoped, never touching the user's real settings) to honor this extension's no-auto-retry contract, since the underlying SDK otherwise retries transient provider errors on its own. Pi children cannot resume (no persisted session) and have no enforced budget cap — both are deliberate v1 limitations, not oversights. Custom (non-canonical) roles are still authored per harness via `/external profile create`, not shared across harnesses automatically. Follow-up capability expansion (trusted extensions/MCP/skills, shared custom roles, resumable sessions, real budget controls) is tracked in issue #43.
|
|
15
15
|
- Use Pi's native subagent system for Pi-backed scout/reviewer/planner/worker/oracle work.
|
|
16
|
+
- Permission resolution takes the least restrictive of the profile tier (or default when absent) and the caller's request: `readonly < edit < danger`. Profile permissions are a floor, not an overridable default; a parent cannot handcuff a `danger` worker by requesting `readonly`. Existing backend-specific execution floors still apply. Disclosure and receipts show the resolved tier.
|
|
16
17
|
- External CLI backends use their own tools and permission mechanisms. Codex tiers map to its `--sandbox` axis. Grok tiers also map to its `--sandbox` axis (`read-only`/`workspace`/`off`), always paired with `--permission-mode bypassPermissions`; bypass only skips the interactive prompt, and the kernel sandbox remains the enforced boundary at every tier. Grok's readonly network-blocking guarantee is Linux-only (a no-op on macOS), and sandbox startup can fail closed rather than silently downgrading on some macOS hosts (for example when `/var/run/docker.sock` resolves to a symlink). Claude falls back to `--permission-mode auto` when its effective UID is 0 because Claude refuses bypass mode under root. Execution lanes (implementer, qa, worker) require shell command authority to inspect repositories and run tests; on Claude and on pi harnesses (where `edit` tier excludes `bash`), execution lanes maintain a danger floor so agents are not artificially handcuffed. Grok needs no such floor: its `edit` tier (`--sandbox workspace`) already permits shell execution, sandboxed to writes within the workspace. Antigravity (`agy`) has no granular headless permission mode — its default sandbox denies even read-only tools — so every agy run is unsandboxed (`--dangerously-skip-permissions`) and `readonly`/`edit` on agy are advisory profile-body instructions, not a boundary. Run them only in trusted repositories and state whether the task is read-only or may edit files.
|
|
17
18
|
- Grok supports `resume` via its own `--resume <sessionId>` flag and reports native cost (`total_cost_usd`) rather than an estimate. It has no budget-enforcement mechanism, so `max_budget_usd` is recorded but unenforceable, and — like codex — it is never automatically retried (agy's one infra-failure retry exception does not apply to Grok).
|
|
18
19
|
- Children receive no parent history by default. `Agent` and workflow `agent()` can opt into `context: {mode: "recent", turns: N}` (last N user turns, including the current one) or `{mode: "full"}` (available post-compaction conversation). Snapshots exclude system instructions, thinking, tool-result metadata, and pending calls; unsupported content/images and more than 1 MiB fail explicitly. Use the smallest sufficient snapshot plus a clear task, absolute paths, and read-only/edit intent. Shared context goes to the external harness and private local evidence; avoid unnecessary sensitive history. `resume` continues an existing child and cannot be combined with sharing. Workflow children select from one frozen parent snapshot, and replay fingerprints include the transferred context.
|
|
@@ -37,7 +38,8 @@ This fork changes the original pi-flow contract: `Agent` is not a generic Pi sub
|
|
|
37
38
|
- Settings retention automatically prunes eligible completed records beyond `maxRunRecords`; active, interrupted, incomplete, and damaged records are not eligible.
|
|
38
39
|
- Structured nested-agent activity may extend the wall-clock deadline once, by one fresh base timeout, capped at twice the original deadline.
|
|
39
40
|
- Do not automatically retry failed or aborted external runs. Preserve the receipt and retry only when the user asks. Exception: the agy backend retries once on infrastructure-classified failures (auth, eligibility, network); the retry is disclosed in the receipt details (`retries`, `retryOf`) and never applies to agent-level failures, aborts, or timeouts.
|
|
40
|
-
- `external_runs` is session/project scoped. `list` pages runs and workflow roots, each carrying a shared timing projection (`queuedAt`/`executionStartedAt`/`processStartedAt`/activity timestamps plus derived `queueDelayMs`/`elapsedMs`/`activityAgeMs`/`processDurationMs`); `activityAgeMs` and a running `elapsedMs` are computed only for a target independently confirmed live by the registry, never inferred from a durable record's status field alone (an unowned "running"-looking record may be an orphaned/crashed run, not a live one). `inspect` pages `summary`/`output`/`diagnostics`/`final` for one `runId`; `final` returns only the same canonical terminal result every other view already reads (no separate backend parser, no narration promoted to final) and is empty with `finalAvailable: false` until a verified successful terminal boundary exists — including for workflows, where an intentional JSON `null` result still counts as available. `inspect` also accepts a bounded batch `runIds` (up to 20, deduplicated, order preserved, summary-only, mutually exclusive with `runId`): every requested ID's ownership is validated before any page returns, entries share one consistent nested shape regardless of live/durable/agent/workflow origin (a workflow entry omits its unbounded children array in favor of `outputRef`/`diagnosticsRef`), reuses the existing per-inspect `limitBytes` cap with no second budget, and fails with an actionable error naming the run rather than ever silently dropping a target or serving a page over the caller's own byte budget. `wait` returns outcomes only for the selected one/any/all target set and never cancels pending work; `cancel` targets one stable ID and preserves its reason. An unresolvable `wf_...` ID fails as unknown/unavailable across `inspect`/`cancel`/`wait`/batch `inspect`, never falling through to the agent-only durable reader. No routine run event sends a parent message or notification.
|
|
41
|
+
- `external_runs` is session/project scoped. `list` pages runs and workflow roots, each carrying a shared timing projection (`queuedAt`/`executionStartedAt`/`processStartedAt`/activity timestamps plus derived `queueDelayMs`/`elapsedMs`/`activityAgeMs`/`processDurationMs`); `activityAgeMs` and a running `elapsedMs` are computed only for a target independently confirmed live by the registry, never inferred from a durable record's status field alone (an unowned "running"-looking record may be an orphaned/crashed run, not a live one). `inspect` pages `summary`/`output`/`diagnostics`/`final` for one `runId`; `final` returns only the same canonical terminal result every other view already reads (no separate backend parser, no narration promoted to final) and is empty with `finalAvailable: false` until a verified successful terminal boundary exists — including for workflows, where an intentional JSON `null` result still counts as available. `inspect` also accepts a bounded batch `runIds` (up to 20, deduplicated, order preserved, summary-only, mutually exclusive with `runId`): every requested ID's ownership is validated before any page returns, entries share one consistent nested shape regardless of live/durable/agent/workflow origin (a workflow entry omits its unbounded children array in favor of `outputRef`/`diagnosticsRef`), reuses the existing per-inspect `limitBytes` cap with no second budget, and fails with an actionable error naming the run rather than ever silently dropping a target or serving a page over the caller's own byte budget. `wait` returns outcomes only for the selected one/any/all target set and never cancels pending work; while waiting it reports bounded live progress (watched targets, completed/pending counts, recent activity) through the tool's own update channel on a fixed heartbeat, stopped on settlement/error/interruption, that never mutates run state or affects cancellation; each settled outcome's `result` is spent from one shared byte budget (`limitBytes`, default 32768) across the whole response in the caller's requested order, never settlement race order — complete when it fits, `resultTruncated: true` plus the existing `outputRef`/`diagnosticsRef` when it does not, never a fixed-length teaser regardless of size; a target's evidence is never read from disk once that shared budget is already exhausted. `cancel` targets one stable ID and preserves its reason. An unresolvable `wf_...` ID fails as unknown/unavailable across `inspect`/`cancel`/`wait`/batch `inspect`, never falling through to the agent-only durable reader. No routine run event sends a parent message or notification.
|
|
42
|
+
- `list`, `inspect`, and `wait` share one run projection (`src/core/run-projection.ts`: `projectLiveAgent`/`projectDurableAgent` for agents, `projectWorkflowRun` for workflows) instead of independent per-surface field extraction. Task identity includes the resolved profile and an explicit `harness` (registered pi-* config, or equal to backend for agy/claude/codex), persisted at the source (run-record metadata, live progress nodes, workflow child snapshots) by every backend — never reparsed or guessed from a profile/subagentType name.
|
|
41
43
|
|
|
42
44
|
## Workflow contract
|
|
43
45
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,37 @@ All notable changes to pi-flow external are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [2.3.1-external.0] - 2026-09-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Resolve permission requests against the profile's minimum authority (`readonly < edit < danger`) across all harnesses. Parent requests can increase permissions but cannot reduce a worker's calibrated permission floor. Agent, workflow, and disclosures share the same resolver; backend-specific floors remain in effect.
|
|
11
|
+
|
|
7
12
|
## [2.3.0-external.0] - 2026-09-21
|
|
8
13
|
|
|
14
|
+
Unifies the run experience across direct `Agent`, `workflow`, and `external_runs` per issue #52: a shared projection, shared rendering, and a corrected model/human contract, rather than isolated renderer patches.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `Agent`, `workflow`, and `external_runs` now share one normalized run projection (`src/core/run-projection.ts`) for identity/state/timing/output, covering both agents (`projectLiveAgent`/`projectDurableAgent`) and workflows (`projectWorkflowRun`). Every `external_runs` surface — `list`, single `inspect`, batch `inspect`, and `wait` — routes through it instead of independently hand-rolling its own shape; a durable `list`/`inspect` row now also carries the same nested `task`/`state`/`output` shape a live one already had, alongside its existing flat fields for compatibility.
|
|
19
|
+
- Run identity now includes the resolved profile and harness end-to-end: `Agent`, workflow children, and every backend (`claude`/`codex`/`agy`/pi) persist an explicit `harness` (registered pi-* config, or equal to backend for external CLIs) on `SubagentProgressNode`/`SubagentToolDetails`/`WorkflowAgentSnapshot` and in run-record metadata — never reparsed or guessed from a profile/subagentType name. `RunTaskProjection.profile`/`.harness` are populated from this real persisted data (`src/profiles.ts`'s `selectorHarness` is the one shared resolution).
|
|
20
|
+
- `external_runs` registers its own `renderCall`/`renderResult` for the first time, covering `list`, single/batch `inspect`, `wait`, and `cancel` with the same reusable header/row presentation `Agent`/`workflow` already use (`src/core/run-render.ts`, shared with `/external runs`' own text formatting).
|
|
21
|
+
- `external_runs wait` reports bounded live progress through its update channel while waiting — watched targets, completed/pending counts, and recent activity — on a fixed heartbeat independent of any single target settling; it stops on settlement, error, or interruption without ever cancelling watched work.
|
|
22
|
+
- Text answers now render as Markdown where a host theme is available (`renderOutputText` in `src/core/subagent-render.ts`, shared by `Agent`'s expanded output, `workflow`'s final text-answer output, and `external_runs`'s `output`/`final` views), falling back to plain text — never throwing — when no theme has been initialized (headless/RPC callers, tests). A workflow's structured (non-string) result stays formatted data, never Markdown-interpreted, per this issue's "no invented prose answer" contract.
|
|
23
|
+
- Workflow `agent()` accepts `description` as a compatible alias for `label`, matching direct `Agent`'s common task-name option; setting both to different values is rejected.
|
|
24
|
+
- `WorkflowToolDetails` gained an additive `lifecycleStatus` field normalizing the legacy internal `"completed"` state word to the same vocabulary `Agent`'s `details.status` already uses (`"done"`), for a coordinating model reading either tool's details. The internal `status` field and its literal `"completed"` value are unchanged for existing readers (journal replay, rendering internals).
|
|
25
|
+
- Documented ccstyle (`pi-cc-extensions`) host-renderer integration: the `excludeRenderers` setting is the supported way to preserve `Agent`/`workflow`/`external_runs`'s own rendering under that host (README "Host renderer integration").
|
|
26
|
+
- Added an authoritative cross-surface contract test (`test/run-contract.test.ts`) asserting the same run reports the same identity/status/output facts across `list`, single `inspect`, batch `inspect`, and `wait`, and that a workflow child's identity matches what `external_runs` independently reports for that same child runId.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `external_runs wait` no longer clips every settled outcome to a fixed 512-character `preview`. Each response now spends one shared byte budget (`limitBytes`, default 32768) across all settled outcomes in the caller's REQUESTED order (never settlement race order — two calls with the same targets and final states now spend the budget identically regardless of which target happened to settle first): a result that fits is returned complete under the new `result` field; one that does not is truncated to what remains with `resultTruncated: true`, alongside the existing `outputRef`/`diagnosticsRef` to read the rest. A disk read of a target's evidence is now skipped entirely once the shared budget is already exhausted, rather than reading and then discarding it. The old `preview` field is removed.
|
|
31
|
+
- `liveSummary`/`journalSummary`/batch `resolveRunSummaryEntry`'s workflow branch now build their `task`/`state`/`output` through the single shared `projectWorkflowRun`, fixing a real drift: batch inspect previously omitted `output.status` entirely (present on the other two), and `liveSummary` never stated a top-level `live` flag.
|
|
32
|
+
- `WorkflowAgentSnapshot` field copying (`workflow/tool.ts`) now goes through shared helpers (`src/core/agent-snapshot.ts`) instead of an inline per-field copy. Fixes a real disclosure/evidence gap: a workflow child's `retries`, `retryOf`, and `permissionRequested` were never copied onto its snapshot at all (silently dropped the moment a retried/elevated child ran inside a workflow instead of as a direct `Agent` call), and `thinkingClamped` was dropped on the running-progress copy.
|
|
33
|
+
- Corrected the workflow help's background example: `background` is a `workflow` tool call parameter, never a `meta` field. The previous example's `meta.background: true` was silently ignored and did not run the workflow in the background.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- `preview`'s hidden double-truncation: a full in-memory `outcome.result` under 512 characters was fine, but the exact bug reported in issue #52 (`external_runs wait` returning `kind: "agent"` with a clipped result) is now gone for any result that fits the shared budget.
|
|
9
38
|
Adds `grok` as a fourth external CLI backend, delegating to the official Grok Build CLI alongside Claude Code, Codex CLI, and Antigravity. Investigation notes: `docs/plans/grok-backend-prep.md`; implementation plan: `docs/plans/2026-05-21-grok-cli-implementation.md`; ex-ante design issue: [#51](https://github.com/tranhoangnguyen03/pi-flow-external/issues/51).
|
|
10
39
|
|
|
11
40
|
### Added
|
package/README.md
CHANGED
|
@@ -259,6 +259,24 @@ Completed rows show a short evidence identifier and result preview. Press **Ctrl
|
|
|
259
259
|
|
|
260
260
|
Workflows show access once at the workflow level, retain done/active/queued/failed counts, and expose bounded final output plus the workflow journal when expanded. Hidden child rows remain reachable through `/external runs`. Active rows report last-activity freshness independently of the spinner. Raw backend events remain in local records rather than flooding the default terminal view.
|
|
261
261
|
|
|
262
|
+
`Agent`, `workflow`, and `external_runs` all register their own `renderCall`/`renderResult` — the same reusable header, status/timing/output presentation, and disclosure rules across all three, so a workflow's card is more hierarchy over the same visual language rather than a different one, and `external_runs` (list/inspect/wait) reads like the tools it observes rather than raw JSON. A text answer's final output — `Agent`'s own expanded result, a workflow's string result, and `external_runs`' `output`/`final` views — renders as Markdown where a host theme is available, falling back to plain text (never a crash) otherwise; a workflow's structured (object/array) result stays formatted data rather than Markdown-interpreted prose.
|
|
263
|
+
|
|
264
|
+
### Host renderer integration
|
|
265
|
+
|
|
266
|
+
Stock Pi renders every tool's own `renderCall`/`renderResult` directly, so the cards above work with no configuration.
|
|
267
|
+
|
|
268
|
+
**Compatibility caveat — pi-cc / ccstyle:** our live progress renderers are not compatible with the default rendering overrides in [`pi-cc-extensions`](https://www.npmjs.com/package/pi-cc-extensions). Manual testing showed workflow and wait progress replaced by a generic `Pending…` display while Agent retained its dedicated renderer. Expanded generic output can also duplicate raw JSON. This is a display limitation, not evidence that the underlying run has stopped.
|
|
269
|
+
|
|
270
|
+
To preserve this extension's renderers, add its three tool names to pi-cc's `excludeRenderers` setting in `~/.pi/agent/pi-cc-extensions.json`. Merge these entries into your existing configuration; do not replace other settings or exclusions:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"excludeRenderers": ["Agent", "workflow", "external_runs"]
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Restart Pi after changing the configuration. Exclusions use **tool names**, not the extension's package name. All three tools above provide custom renderers. This is pi-cc's supported renderer-preservation mechanism; do not edit its installed package files. End-to-end progress rendering with these exclusions still needs confirmation in your installed pi-cc version. If it continues to show only `Pending…`, disable pi-cc when you need our live progress display. Full compatibility with pi-cc's default overrides is not claimed.
|
|
279
|
+
|
|
262
280
|
## Background runs and supervision
|
|
263
281
|
|
|
264
282
|
`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:
|
|
@@ -277,7 +295,7 @@ Use `external_runs` with these actions:
|
|
|
277
295
|
- `list`: current session/project runs; use `cursor` for run pages and `workflowCursor` for workflow pages. `workflowRunId` filters children of one workflow. Rows carry a `timing` projection (`queueDelayMs`, `elapsedMs`, `activityAgeMs` when live, `processDurationMs`) plus `outputAvailable`/`finalAvailable`.
|
|
278
296
|
- `inspect`: single `runId` with `view: "summary" | "output" | "diagnostics" | "final"`, and optional opaque `cursor`/`limitBytes` (max 64 KiB, same cap for every view). Follow `nextCursor` to avoid truncation. `summary` includes the same `timing` projection as `list`, plus `output.finalAvailable`. `final` returns only the verified canonical terminal answer — empty with `finalAvailable: false` until a successful terminal boundary exists; it never promotes partial/narration text. `output` stays the combined stream (assistant messages plus canonical result) and is unchanged.
|
|
279
297
|
- `inspect` with `runIds` instead of `runId` (up to 20, deduplicated, order preserved): a single bounded batch of `summary`-only projections — one cheap request to see whether several selected background children are queued, running, or terminal, each with `outputRef`/`diagnosticsRef` for follow-up detail. Ownership of every requested ID is validated before any page is returned. Reuses the same `limitBytes` cap as single-run inspection; pages contain whole target entries and continue through `nextCursor`, without invalidation from ordinary live progress. If one compact entry cannot fit, an actionable error asks you to increase `limitBytes` or inspect that run individually; no target is silently dropped. `runId` and `runIds` are mutually exclusive, and batch `view` must stay `summary`.
|
|
280
|
-
- `wait`: one `runId` or selected `runIds`, with `mode: "any" | "all"`. It returns terminal outcomes plus still-pending IDs; an unsuccessful workflow returns early even in `all` mode. It never chooses a winner or cancels pending work.
|
|
298
|
+
- `wait`: one `runId` or selected `runIds`, with `mode: "any" | "all"`. It returns terminal outcomes plus still-pending IDs; an unsuccessful workflow returns early even in `all` mode. It never chooses a winner or cancels pending work. While waiting, a bounded heartbeat (independent of any single target settling) reports live progress — watched targets, completed/pending counts, and recent activity — through the tool's update channel; it stops automatically on settlement, error, or interruption. Each settled outcome's `result` is spent from one shared byte budget (`limitBytes`, default 32768) across the whole response, in the requested `runId`/`runIds` order — never settlement race order, so the same targets and final states spend the budget identically regardless of which one happened to settle first: a result that fits is returned complete, one that does not is truncated with `resultTruncated: true` and the existing `outputRef`/`diagnosticsRef` to continue reading it — not a fixed-length teaser regardless of size. A target's evidence is never read from disk once the shared budget is already exhausted.
|
|
281
299
|
- `cancel`: one `runId` and optional reason. Whole-workflow cancellation stops active children; targeted child cancellation remains a catchable workflow outcome. Cancellation does not roll back edits or other side effects.
|
|
282
300
|
|
|
283
301
|
Interrupting a blocking `Agent`/`workflow` call cancels its work. Interrupting `external_runs wait` stops only that wait. Background work survives its launching tool return and ordinary parent turns, but not the owning session: orderly session shutdown requests cancellation and waits for bounded cleanup. This is not a daemon. After a host crash or unconfirmed shutdown, unfinished evidence is `interrupted_or_uncertain`; restart restores evidence access, never live ownership or guaranteed retrospective process termination. No routine activity wakes the parent, and live steering is not supported.
|
|
@@ -321,7 +339,9 @@ const [a, b, c] = await parallel([
|
|
|
321
339
|
return { a, b, c };
|
|
322
340
|
```
|
|
323
341
|
|
|
324
|
-
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.
|
|
342
|
+
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. `agent()`'s second argument accepts `description` as the common task-name option shared with direct `Agent` calls; `label` remains a compatible alias for existing scripts. Setting both to different values is rejected rather than silently preferring one.
|
|
343
|
+
|
|
344
|
+
`background: true` belongs to the `workflow` tool call itself (`workflow({ script, background: true })`), never to `meta` — `meta` fields other than `apiVersion`/`name`/`description`/`phases` are ignored, so `export const meta = { ..., background: true }` does not run the workflow in the background.
|
|
325
345
|
|
|
326
346
|
Saved workflows are discovered on demand through `external_help({ topic: "workflow" })`; project `.pi/workflows` entries are included only when Pi reports the project trusted.
|
|
327
347
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SubagentProgressNode, SubagentToolDetails, WorkflowAgentSnapshot } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single source of truth for copying a running child's live progress onto its
|
|
5
|
+
* workflow snapshot row. Previously inlined per-field in `workflow/tool.ts`'s
|
|
6
|
+
* `onProgress` handler; kept here so the running and terminal copies
|
|
7
|
+
* (`applySubagentResultToWorkflowAgent` below) cannot silently drift apart on
|
|
8
|
+
* which fields they carry.
|
|
9
|
+
*/
|
|
10
|
+
export function applySubagentProgressToWorkflowAgent(agent: WorkflowAgentSnapshot, progress: SubagentProgressNode): void {
|
|
11
|
+
agent.startedAt = progress.startedAt;
|
|
12
|
+
agent.endedAt = progress.endedAt;
|
|
13
|
+
agent.activity = [...progress.activity];
|
|
14
|
+
agent.activityCount = progress.activityCount;
|
|
15
|
+
agent.result = progress.result;
|
|
16
|
+
agent.error = progress.error;
|
|
17
|
+
agent.assistantOutput = progress.assistantOutput;
|
|
18
|
+
agent.processStartedAt = progress.processStartedAt;
|
|
19
|
+
agent.firstActivityAt = progress.firstActivityAt;
|
|
20
|
+
agent.lastActivityAt = progress.lastActivityAt;
|
|
21
|
+
agent.timedOut = progress.timedOut;
|
|
22
|
+
agent.usage = progress.usage;
|
|
23
|
+
agent.status = progress.status;
|
|
24
|
+
agent.sessionId = progress.sessionId;
|
|
25
|
+
agent.resumedFrom = progress.resumedFrom;
|
|
26
|
+
agent.context = progress.context;
|
|
27
|
+
if (progress.thinkingClamped) agent.thinkingClamped = progress.thinkingClamped;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Single source of truth for copying a finished child's full result details
|
|
32
|
+
* onto its workflow snapshot row. Fixes a real disclosure/evidence gap: the
|
|
33
|
+
* previous inline copy in `workflow/tool.ts` never carried `retries`,
|
|
34
|
+
* `retryOf`, `permissionRequested`, or `thinkingClamped` onto
|
|
35
|
+
* `WorkflowAgentSnapshot`, so a workflow child that retried an agy
|
|
36
|
+
* infrastructure failure (or had its thinking level clamped, or its
|
|
37
|
+
* requested permission tier elevated) silently lost that disclosure the
|
|
38
|
+
* moment it became part of a workflow — a direct `Agent` call never lost it.
|
|
39
|
+
*/
|
|
40
|
+
export function applySubagentResultToWorkflowAgent(agent: WorkflowAgentSnapshot, resultDetails: SubagentToolDetails): void {
|
|
41
|
+
const progress = resultDetails.progress;
|
|
42
|
+
agent.status = resultDetails.status;
|
|
43
|
+
agent.result = resultDetails.result;
|
|
44
|
+
agent.error = resultDetails.error;
|
|
45
|
+
agent.assistantOutput = resultDetails.assistantOutput;
|
|
46
|
+
agent.processStartedAt = progress?.processStartedAt;
|
|
47
|
+
agent.firstActivityAt = progress?.firstActivityAt;
|
|
48
|
+
agent.lastActivityAt = progress?.lastActivityAt;
|
|
49
|
+
agent.timedOut = resultDetails.timedOut;
|
|
50
|
+
agent.usage = resultDetails.usage;
|
|
51
|
+
agent.externalRunId = resultDetails.runId;
|
|
52
|
+
agent.recordPath = resultDetails.recordPath;
|
|
53
|
+
agent.backendEventCount = resultDetails.backendEventCount;
|
|
54
|
+
agent.nestedActivitySeen = resultDetails.nestedActivitySeen;
|
|
55
|
+
agent.nestedTimeoutExtended = resultDetails.nestedTimeoutExtended;
|
|
56
|
+
agent.effectiveTimeoutMs = resultDetails.effectiveTimeoutMs;
|
|
57
|
+
agent.recordingError = resultDetails.recordingError;
|
|
58
|
+
agent.permission = resultDetails.permission;
|
|
59
|
+
agent.permissionEnforced = resultDetails.permissionEnforced;
|
|
60
|
+
agent.permissionDenials = resultDetails.permissionDenials;
|
|
61
|
+
agent.maxBudgetUsd = resultDetails.maxBudgetUsd;
|
|
62
|
+
agent.sessionId = resultDetails.sessionId;
|
|
63
|
+
agent.resumedFrom = resultDetails.resumedFrom;
|
|
64
|
+
agent.context = resultDetails.context;
|
|
65
|
+
if (resultDetails.permissionRequested !== undefined) agent.permissionRequested = resultDetails.permissionRequested;
|
|
66
|
+
if (resultDetails.retries !== undefined) agent.retries = resultDetails.retries;
|
|
67
|
+
if (resultDetails.retryOf !== undefined) agent.retryOf = resultDetails.retryOf;
|
|
68
|
+
const thinkingClamped = resultDetails.thinkingClamped ?? progress?.thinkingClamped;
|
|
69
|
+
if (thinkingClamped) agent.thinkingClamped = thinkingClamped;
|
|
70
|
+
if (progress) {
|
|
71
|
+
agent.startedAt = progress.startedAt;
|
|
72
|
+
agent.endedAt = progress.endedAt;
|
|
73
|
+
agent.activity = [...progress.activity];
|
|
74
|
+
agent.activityCount = progress.activityCount;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/core/agy.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MAX_STDOUT_LINE_CHARS,
|
|
8
8
|
} from "./stream.ts";
|
|
9
9
|
import type { PermissionTier, SubagentProfile, SubagentUsage, ThinkingLevel } from "../types.ts";
|
|
10
|
+
import { selectorHarness } from "../profiles.ts";
|
|
10
11
|
import { buildPermissionArgs } from "./permissions.ts";
|
|
11
12
|
import { abortChildTree } from "./process-tree.ts";
|
|
12
13
|
|
|
@@ -277,6 +278,7 @@ export async function spawnAgySubagent(params: {
|
|
|
277
278
|
description: params.description,
|
|
278
279
|
subagentType,
|
|
279
280
|
backend: params.profile.backend,
|
|
281
|
+
harness: selectorHarness(params.profile),
|
|
280
282
|
enabled: params.progressEnabled,
|
|
281
283
|
onProgress: params.onProgress,
|
|
282
284
|
executionStartedAt: params.executionStartedAt,
|
|
@@ -477,6 +479,7 @@ export async function spawnAgySubagent(params: {
|
|
|
477
479
|
description: params.description,
|
|
478
480
|
subagentType,
|
|
479
481
|
backend: params.profile.backend,
|
|
482
|
+
harness: selectorHarness(params.profile),
|
|
480
483
|
status: "done",
|
|
481
484
|
result,
|
|
482
485
|
usage: latestUsage,
|
|
@@ -503,6 +506,7 @@ export async function spawnAgySubagent(params: {
|
|
|
503
506
|
description: params.description,
|
|
504
507
|
subagentType,
|
|
505
508
|
backend: params.profile.backend,
|
|
509
|
+
harness: selectorHarness(params.profile),
|
|
506
510
|
status,
|
|
507
511
|
error: message,
|
|
508
512
|
usage: latestUsage,
|
package/src/core/claude.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
MAX_STDOUT_LINE_CHARS,
|
|
14
14
|
} from "./stream.ts";
|
|
15
15
|
import type { PermissionTier, SubagentProfile, SubagentUsage, ThinkingLevel } from "../types.ts";
|
|
16
|
+
import { selectorHarness } from "../profiles.ts";
|
|
16
17
|
import { buildPermissionArgs } from "./permissions.ts";
|
|
17
18
|
import { abortChildTree } from "./process-tree.ts";
|
|
18
19
|
|
|
@@ -349,6 +350,7 @@ export async function spawnClaudeSubagent(params: {
|
|
|
349
350
|
description: params.description,
|
|
350
351
|
subagentType,
|
|
351
352
|
backend: params.profile.backend,
|
|
353
|
+
harness: selectorHarness(params.profile),
|
|
352
354
|
enabled: params.progressEnabled,
|
|
353
355
|
onProgress: params.onProgress,
|
|
354
356
|
executionStartedAt: params.executionStartedAt,
|
|
@@ -564,6 +566,7 @@ export async function spawnClaudeSubagent(params: {
|
|
|
564
566
|
description: params.description,
|
|
565
567
|
subagentType,
|
|
566
568
|
backend: params.profile.backend,
|
|
569
|
+
harness: selectorHarness(params.profile),
|
|
567
570
|
status: "done",
|
|
568
571
|
result,
|
|
569
572
|
usage: latestUsage,
|
|
@@ -591,6 +594,7 @@ export async function spawnClaudeSubagent(params: {
|
|
|
591
594
|
description: params.description,
|
|
592
595
|
subagentType,
|
|
593
596
|
backend: params.profile.backend,
|
|
597
|
+
harness: selectorHarness(params.profile),
|
|
594
598
|
status,
|
|
595
599
|
error: message,
|
|
596
600
|
usage: latestUsage,
|
package/src/core/codex.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
MAX_STDOUT_LINE_CHARS,
|
|
17
17
|
} from "./stream.ts";
|
|
18
18
|
import type { PermissionTier, SubagentProfile, SubagentUsage, ThinkingLevel } from "../types.ts";
|
|
19
|
+
import { selectorHarness } from "../profiles.ts";
|
|
19
20
|
import { buildPermissionArgs } from "./permissions.ts";
|
|
20
21
|
import { abortChildTree } from "./process-tree.ts";
|
|
21
22
|
|
|
@@ -320,6 +321,7 @@ export async function spawnCodexSubagent(params: {
|
|
|
320
321
|
description: params.description,
|
|
321
322
|
subagentType,
|
|
322
323
|
backend: params.profile.backend,
|
|
324
|
+
harness: selectorHarness(params.profile),
|
|
323
325
|
enabled: params.progressEnabled,
|
|
324
326
|
onProgress: params.onProgress,
|
|
325
327
|
executionStartedAt: params.executionStartedAt,
|
|
@@ -525,6 +527,7 @@ export async function spawnCodexSubagent(params: {
|
|
|
525
527
|
description: params.description,
|
|
526
528
|
subagentType,
|
|
527
529
|
backend: params.profile.backend,
|
|
530
|
+
harness: selectorHarness(params.profile),
|
|
528
531
|
status: "done",
|
|
529
532
|
result,
|
|
530
533
|
usage: latestUsage,
|
|
@@ -551,6 +554,7 @@ export async function spawnCodexSubagent(params: {
|
|
|
551
554
|
description: params.description,
|
|
552
555
|
subagentType,
|
|
553
556
|
backend: params.profile.backend,
|
|
557
|
+
harness: selectorHarness(params.profile),
|
|
554
558
|
status,
|
|
555
559
|
error: message,
|
|
556
560
|
usage: latestUsage,
|
package/src/core/permissions.ts
CHANGED
|
@@ -55,7 +55,10 @@ export function resolveEffectivePermissionTier(
|
|
|
55
55
|
profile: PermissionProfileRef | undefined,
|
|
56
56
|
defaultTier: PermissionTier = "danger",
|
|
57
57
|
): PermissionTier {
|
|
58
|
-
|
|
58
|
+
// Profile permissions are a floor: parent requests can grant more, never less.
|
|
59
|
+
const floor = profile?.permission ?? defaultTier;
|
|
60
|
+
const tiers: readonly PermissionTier[] = ["readonly", "edit", "danger"];
|
|
61
|
+
const baseTier = tiers[Math.max(tiers.indexOf(floor), tiers.indexOf(requestedTier ?? floor))]!;
|
|
59
62
|
if (profile?.backend === "agy") {
|
|
60
63
|
// Get out of the way: agy's only unsandboxed headless mode is
|
|
61
64
|
// --dangerously-skip-permissions, and its default sandbox denies even
|
package/src/core/progress.ts
CHANGED
|
@@ -27,12 +27,14 @@ export function createProgressNode(
|
|
|
27
27
|
subagentType: SubagentType,
|
|
28
28
|
status: SubagentProgressNode["status"] = "running",
|
|
29
29
|
backend?: SubagentBackend,
|
|
30
|
+
harness?: string,
|
|
30
31
|
): SubagentProgressNode {
|
|
31
32
|
return {
|
|
32
33
|
id,
|
|
33
34
|
description,
|
|
34
35
|
subagentType,
|
|
35
36
|
...(backend ? { backend } : {}),
|
|
37
|
+
...(harness ? { harness } : {}),
|
|
36
38
|
status,
|
|
37
39
|
startedAt: Date.now(),
|
|
38
40
|
activity: [],
|
|
@@ -184,6 +186,7 @@ export interface ProgressEmitterOptions {
|
|
|
184
186
|
description: string;
|
|
185
187
|
subagentType: SubagentType;
|
|
186
188
|
backend?: SubagentBackend;
|
|
189
|
+
harness?: string;
|
|
187
190
|
enabled: boolean;
|
|
188
191
|
onProgress: ((result: AgentToolResult) => void) | undefined;
|
|
189
192
|
/** Execution-start boundary captured by the caller right after the shared concurrency limiter granted a slot. */
|
|
@@ -214,8 +217,8 @@ export interface ProgressEmitter {
|
|
|
214
217
|
* hand-synchronized copies that silently drift.
|
|
215
218
|
*/
|
|
216
219
|
export function createProgressEmitter(options: ProgressEmitterOptions): ProgressEmitter {
|
|
217
|
-
const { toolCallId, description, subagentType, backend, enabled, onProgress } = options;
|
|
218
|
-
const progress = createProgressNode(toolCallId, description, subagentType, "running", backend);
|
|
220
|
+
const { toolCallId, description, subagentType, backend, harness, enabled, onProgress } = options;
|
|
221
|
+
const progress = createProgressNode(toolCallId, description, subagentType, "running", backend, harness);
|
|
219
222
|
if (options.executionStartedAt !== undefined) {
|
|
220
223
|
progress.executionStartedAt = options.executionStartedAt;
|
|
221
224
|
}
|
|
@@ -239,6 +242,7 @@ export function createProgressEmitter(options: ProgressEmitterOptions): Progress
|
|
|
239
242
|
description,
|
|
240
243
|
subagentType,
|
|
241
244
|
...(backend ? { backend } : {}),
|
|
245
|
+
...(harness ? { harness } : {}),
|
|
242
246
|
status: progress.status,
|
|
243
247
|
result: progress.result,
|
|
244
248
|
error: progress.error,
|
|
@@ -70,6 +70,10 @@ export interface RunRecordListItem {
|
|
|
70
70
|
error?: string;
|
|
71
71
|
description?: string;
|
|
72
72
|
backend?: string;
|
|
73
|
+
/** Resolved profile/subagentType name, when persisted in run-record metadata. Never reparsed from other fields. */
|
|
74
|
+
profile?: string;
|
|
75
|
+
/** Resolved harness (registered pi-* config, or equal to backend for agy/claude/codex), when persisted in run-record metadata. */
|
|
76
|
+
harness?: string;
|
|
73
77
|
project?: string;
|
|
74
78
|
parentSessionId?: string;
|
|
75
79
|
workflowRunId?: string;
|
|
@@ -341,6 +345,8 @@ async function readListItem(runsDirectory: string, runId: string): Promise<RunRe
|
|
|
341
345
|
...(boundedString(terminal?.error) ? { error: boundedString(terminal?.error) } : {}),
|
|
342
346
|
...(boundedString(metadata?.description) ? { description: boundedString(metadata?.description) } : {}),
|
|
343
347
|
...(asString(metadata?.backend) ? { backend: asString(metadata?.backend) } : {}),
|
|
348
|
+
...(metadataProfileName(metadata) ? { profile: metadataProfileName(metadata) } : {}),
|
|
349
|
+
...(metadataHarness(metadata) ? { harness: metadataHarness(metadata) } : {}),
|
|
344
350
|
...(asString(metadata?.project) ? { project: asString(metadata?.project) } : {}),
|
|
345
351
|
...(asString(metadata?.parentSessionId) ? { parentSessionId: asString(metadata?.parentSessionId) } : {}),
|
|
346
352
|
...(asString(metadata?.workflowRunId) ? { workflowRunId: asString(metadata?.workflowRunId) } : {}),
|
|
@@ -423,7 +429,8 @@ function summaryProjection(runId: string, summary: SummaryState, observation: Ru
|
|
|
423
429
|
task: compactObject({
|
|
424
430
|
description: boundedString(metadata?.description),
|
|
425
431
|
backend: metadata?.backend,
|
|
426
|
-
profile:
|
|
432
|
+
profile: metadataProfileName(metadata),
|
|
433
|
+
harness: metadataHarness(metadata),
|
|
427
434
|
project: boundedString(metadata?.project),
|
|
428
435
|
parentSessionId: metadata?.parentSessionId,
|
|
429
436
|
workflowRunId: metadata?.workflowRunId,
|
|
@@ -824,6 +831,31 @@ function asString(value: unknown): string | undefined {
|
|
|
824
831
|
return typeof value === "string" ? value : undefined;
|
|
825
832
|
}
|
|
826
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Resolved profile/subagentType name from persisted run-record metadata.
|
|
836
|
+
* `metadata.profile` has shipped in two shapes historically: a plain string
|
|
837
|
+
* (the direct `Agent`/workflow-child metadata this package writes today) and
|
|
838
|
+
* a full profile object (spawn.ts's own fallback record creation, used when
|
|
839
|
+
* a caller does not pre-create its own run record). Both are read here, in
|
|
840
|
+
* ONE place, so `readListItem` and `summaryProjection` cannot independently
|
|
841
|
+
* drift on how they unpack it.
|
|
842
|
+
*/
|
|
843
|
+
function metadataProfileName(metadata: Record<string, unknown> | undefined): string | undefined {
|
|
844
|
+
return typeof metadata?.profile === "string" ? metadata.profile : asString(asRecord(metadata?.profile)?.name);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Resolved harness (registered pi-* config, or equal to backend for
|
|
849
|
+
* agy/claude/codex) from persisted run-record metadata: an explicit
|
|
850
|
+
* `metadata.harness` field when present (every current call site writes
|
|
851
|
+
* one), falling back to the embedded profile object's own `harness` field
|
|
852
|
+
* for the historical spawn.ts fallback shape. Never reparsed/guessed from
|
|
853
|
+
* `profile`/`subagentType`'s name string.
|
|
854
|
+
*/
|
|
855
|
+
function metadataHarness(metadata: Record<string, unknown> | undefined): string | undefined {
|
|
856
|
+
return asString(metadata?.harness) ?? asString(asRecord(metadata?.profile)?.harness);
|
|
857
|
+
}
|
|
858
|
+
|
|
827
859
|
function isNotFound(error: unknown): boolean {
|
|
828
860
|
return asRecord(error)?.code === "ENOENT";
|
|
829
861
|
}
|