@tranhoangnguyen0310/pi-flow-external 3.0.0-external.0 → 3.1.0-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 +2 -0
- package/CHANGELOG.md +10 -0
- package/README.md +4 -2
- package/docs/agent-public-contract.md +21 -0
- package/package.json +10 -9
- package/src/core/errors.ts +10 -0
- package/src/core/parent-context.ts +10 -7
- package/src/core/run-registry.ts +2 -1
- package/src/core/spawn.ts +23 -1
- package/src/external-runs.ts +14 -5
- package/src/harnesses.ts +2 -16
- package/src/pi-subagent.ts +51 -17
- package/src/profiles.ts +13 -12
- package/src/public-contract.ts +73 -0
package/AGENTS.md
CHANGED
|
@@ -43,6 +43,8 @@ This fork changes the original pi-flow contract: `Agent` is not a generic Pi sub
|
|
|
43
43
|
|
|
44
44
|
## Receipt and evidence invariants
|
|
45
45
|
|
|
46
|
+
- Agent public contract v1 uses a discriminated `ok` envelope with `isError === !ok`; renderer details remain independent. Expected errors are coded at their source, not parsed from prose. Canonical output is inline only within 16 KiB UTF-8 JSON after redaction, with `output_redacted` disclosed when changed. Refs must be resolvable, liveness must be observed after evidence I/O, and unavailable evidence must not discard a settled result. Native codemode tests must assert script output and hook-captured flags outside hooks (the SDK catches hook exceptions).
|
|
47
|
+
|
|
46
48
|
- Success requires a recognized backend terminal-success event, a zero process exit, and a non-empty result.
|
|
47
49
|
- A backend failure with a complete local record is different from an incomplete or damaged record; preserve that distinction in reports.
|
|
48
50
|
- Normal runs write private best-effort evidence under `~/.pi/agent/pi-flow-external/runs/` or `PI_FLOW_EXTERNAL_RUNS_DIR`. Records may still contain sensitive prompts, excerpts, and tool output despite redaction.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-flow external are documented here.
|
|
4
4
|
|
|
5
|
+
## [3.1.0-external.0] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Versioned Agent receipts (`outputSchema`, `structuredContent`, and accurate `isError`) for native Pi codemode. Scripts must check `ok`; expected failures are data, while host dispatch failures and unexpected exceptions still throw.
|
|
9
|
+
- Registry-backed lifecycle/output/evidence projections, a 16 KiB UTF-8 JSON inline cap, inspect references, and explicit output-redaction warnings. Human renderer details remain separate. Workflow and supervision contracts remain follow-up work.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Require Pi `~0.99.2`, with development packages pinned to 0.99.2; older hosts are no longer claimed as supported. Pi children inherit registered providers and runtime API keys through ModelRuntime. Flow retains six concrete thinking levels (not SDK `max`); virtual models are rejected because their registrations cannot be inherited.
|
|
13
|
+
- Port isolated faux-provider and E2E harnesses to Pi 0.99.2. Native codemode regressions cover returned errors, host rejection/throws, redaction, declaration discovery, and background settlement during evidence reads.
|
|
14
|
+
|
|
5
15
|
## [3.0.0-external.0] - 2026-10-01
|
|
6
16
|
|
|
7
17
|
### Breaking: configuration v5 (issue #73)
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Six built-in roles are available in memory on every one of those harnesses. A fr
|
|
|
14
14
|
|
|
15
15
|
The ordinary driver has four tools (`workflow` can be disabled):
|
|
16
16
|
|
|
17
|
-
- `Agent` resolves and runs one external role.
|
|
17
|
+
- `Agent` resolves and runs one external role. Its [versioned public receipt](docs/agent-public-contract.md) is available to native Pi codemode; scripts must check `ok`.
|
|
18
18
|
- `workflow` orchestrates multiple external roles with trusted JavaScript.
|
|
19
19
|
- `external_help` returns the usage playbook, role details, permission behavior, or workflow guidance on demand.
|
|
20
20
|
- `external_runs` lists, inspects, waits for, and cancels session-owned runs.
|
|
@@ -23,6 +23,8 @@ The ordinary driver has four tools (`workflow` can be disabled):
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
+
Host baseline: **Pi 0.99.2**, with Pi peers restricted to `~0.99.2` (0.99.x patches from 0.99.2). The offline suite is pinned to exactly 0.99.2; older hosts and later minor versions are not claimed as supported.
|
|
27
|
+
|
|
26
28
|
Global installation:
|
|
27
29
|
|
|
28
30
|
```bash
|
|
@@ -370,7 +372,7 @@ Disable a role everywhere with `/external config role disable reviewer`, or add
|
|
|
370
372
|
|
|
371
373
|
### Named Pi harness configurations
|
|
372
374
|
|
|
373
|
-
A named Pi harness runs in-process through Pi's own SDK, for
|
|
375
|
+
A named Pi harness runs in-process through Pi's own SDK, for physical models Pi can already resolve (built-in, self-hosted, or a custom-registered provider). Children inherit in-memory provider registrations and the selected provider's runtime API key; stored authentication uses the same agent directory. Virtual model registrations cannot be transferred through the SDK registry and are rejected. Flow retains six concrete thinking levels; SDK `max` remains unsupported. Register it with **Add a Pi agent…** in `/external`, or with `/external config harness create pi-NAME --model provider/model`: a `pi-<label>` name, a `provider/model` id, a reasoning policy (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `parent`, stored explicitly on creation), and a resource preset (`minimal` or `skills`). `minimal` is the default and leaves skills unloaded. `skills` loads installed skills, and project skills only when the project is trusted. Registration is saved into the `harnesses` object of `settings.json` without sending a request. `/external config harness assist` is the model-assisted alternative; its smoke test uses the selected preset and runs before saving. A legacy entry that omits `preset` is `minimal`. The six CLI harnesses are built in and do not need entries there.
|
|
374
376
|
|
|
375
377
|
```json
|
|
376
378
|
{
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Agent public contract v1
|
|
2
|
+
|
|
3
|
+
`Agent` declares `outputSchema` and returns matching `structuredContent`. Human `content` and renderer `details` remain separate. Native Pi codemode receives the structured receipt, including failures: **check `ok`**, not just whether the call threw.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const receipt = await tools.Agent({ role: "worker", description: "Check build", prompt: "Run the build and report." });
|
|
7
|
+
if (!receipt.ok) return receipt.error;
|
|
8
|
+
return receipt.data.run;
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The envelope carries `contractVersion: 1`, `tool: "Agent"`, `action: "delegate"`, ISO `observedAt`, `ok`, `data: {run}`, and `warnings`. `error: {code,message}` exists only on failure. Unknown fields must be ignored by v1 consumers; removing fields or changing their meaning requires a new contract version. The host uses the schema for declarations, not runtime output validation; tests validate actual receipts.
|
|
12
|
+
|
|
13
|
+
Pre-registration failures have `run:null`. Run receipts expose task identity, registry-confirmed liveness, lifecycle state/outcome, timing, evidence integrity (`complete`, `incomplete`, `damaged`, or `unknown`), output availability/delivery, and inspect references when resolvable. Missing evidence does not turn a settled successful run into an uncertain execution. While a run is live, `incomplete` means evidence has not yet been finalized, not that execution failed. Unreadable evidence reports `unknown` without discarding the child result. Private evidence paths and launch/configuration are not included.
|
|
14
|
+
|
|
15
|
+
Only complete canonical results are inline, within **16 KiB of UTF-8 JSON after secret redaction**. Larger values use references. False, null, zero, and empty strings are valid results. Partial narration is never the canonical value. Both model-visible channels use the existing secret redactor; `warnings` includes `output_redacted` when it changes canonical output. this is best-effort pattern redaction, not a guarantee that arbitrary sensitive prose is identified.
|
|
16
|
+
|
|
17
|
+
Codes include `configuration_invalid`, `harness_unavailable`, `selection_invalid`, `model_unavailable`, `context_invalid`, `session_closed`, and terminal outcomes `failed`, `cancelled`, `timed_out`. Unexpected exceptions remain host errors, as do predispatch schema/permission failures. A content-replacing host redaction hook drops structured content unless it explicitly supplies a replacement.
|
|
18
|
+
|
|
19
|
+
Background `ok:true` means accepted, not completed. Await foreground calls; use `background:true` for work that must outlive a codemode script. Inspect/wait through the existing `external_runs` API; its versioned contract is separate work in #77.
|
|
20
|
+
|
|
21
|
+
Pi 0.99.2 generates a useful discriminated return type (native `describeTool` result: 2941 UTF-8 bytes). Its input declaration currently loses all input properties because Pi renders the existing root `anyOf` constraints before properties; input validation itself remains intact. This is a known input-discovery limitation, not a runtime validation bypass.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tranhoangnguyen0310/pi-flow-external",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0-external.0",
|
|
4
4
|
"description": "External Claude Code, Codex CLI, Antigravity, Grok Build CLI, Muse Code, and OpenCode delegation for pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"index.ts",
|
|
10
10
|
"src",
|
|
11
11
|
"scripts/field-report.mjs",
|
|
12
|
+
"docs/agent-public-contract.md",
|
|
12
13
|
"docs/ARCHITECTURE_SNAPSHOT.md",
|
|
13
14
|
"docs/field-testing.md",
|
|
14
15
|
"docs/releasing.md",
|
|
@@ -42,17 +43,17 @@
|
|
|
42
43
|
"test": "vitest run"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
|
-
"@earendil-works/pi-agent-core": "
|
|
46
|
-
"@earendil-works/pi-ai": "
|
|
47
|
-
"@earendil-works/pi-coding-agent": "
|
|
48
|
-
"@earendil-works/pi-tui": "
|
|
46
|
+
"@earendil-works/pi-agent-core": "~0.99.2",
|
|
47
|
+
"@earendil-works/pi-ai": "~0.99.2",
|
|
48
|
+
"@earendil-works/pi-coding-agent": "~0.99.2",
|
|
49
|
+
"@earendil-works/pi-tui": "~0.99.2",
|
|
49
50
|
"typebox": "*"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"@earendil-works/pi-agent-core": "0.
|
|
53
|
-
"@earendil-works/pi-ai": "0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
55
|
-
"@earendil-works/pi-tui": "0.
|
|
53
|
+
"@earendil-works/pi-agent-core": "0.99.2",
|
|
54
|
+
"@earendil-works/pi-ai": "0.99.2",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.99.2",
|
|
56
|
+
"@earendil-works/pi-tui": "0.99.2",
|
|
56
57
|
"@types/node": "^24.12.4",
|
|
57
58
|
"typebox": "1.1.38",
|
|
58
59
|
"typescript": "^5.9.3",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const FLOW_ERROR_CODES = ["configuration_invalid", "harness_unavailable", "selection_invalid", "model_unavailable", "context_invalid", "session_closed", "failed", "cancelled", "timed_out"] as const;
|
|
2
|
+
export type FlowErrorCode = (typeof FLOW_ERROR_CODES)[number];
|
|
3
|
+
|
|
4
|
+
/** Expected caller/configuration failure, distinct from a programming error. */
|
|
5
|
+
export class ExpectedFlowError extends Error {
|
|
6
|
+
constructor(readonly code: FlowErrorCode, message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ExpectedFlowError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExpectedFlowError } from "./errors.ts";
|
|
1
2
|
import { buildSessionContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type, type Static } from "typebox";
|
|
3
4
|
|
|
@@ -20,12 +21,12 @@ export interface ParentContextReceipt {
|
|
|
20
21
|
|
|
21
22
|
export function parseParentContext(value: unknown): ParentContext | undefined {
|
|
22
23
|
if (value === undefined) return undefined;
|
|
23
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new ExpectedFlowError("context_invalid", "context must be an object");
|
|
24
25
|
const { mode, turns } = value as Record<string, unknown>;
|
|
25
26
|
if (!["none", "recent", "full"].includes(mode as string) ||
|
|
26
27
|
Object.keys(value).some((key) => key !== "mode" && !(mode === "recent" && key === "turns")) ||
|
|
27
28
|
(mode === "recent" && (typeof turns !== "number" || !Number.isSafeInteger(turns) || turns < 1))) {
|
|
28
|
-
throw new
|
|
29
|
+
throw new ExpectedFlowError("context_invalid", "context must be {mode:'none'}, {mode:'recent',turns:positive integer}, or {mode:'full'}");
|
|
29
30
|
}
|
|
30
31
|
return mode === "recent" ? { mode, turns: turns as number } : { mode: mode as "none" | "full" };
|
|
31
32
|
}
|
|
@@ -46,8 +47,8 @@ export function prepareParentContext(
|
|
|
46
47
|
const context = parseParentContext(selection);
|
|
47
48
|
if (!context || context.mode === "none") return { prompt };
|
|
48
49
|
const resumeId = typeof resume === "string" && resume.trim() !== "" ? resume.trim() : undefined;
|
|
49
|
-
if (resumeId !== undefined) throw new
|
|
50
|
-
if (!messages) throw new
|
|
50
|
+
if (resumeId !== undefined) throw new ExpectedFlowError("context_invalid", "context sharing cannot be combined with resume; continue the child or start a new one");
|
|
51
|
+
if (!messages) throw new ExpectedFlowError("context_invalid", "Parent context is unavailable; use context:none and a self-contained prompt");
|
|
51
52
|
const compacted = messages.some((message) => message.role === "compactionSummary");
|
|
52
53
|
let start = 0;
|
|
53
54
|
if (context.mode === "recent") {
|
|
@@ -64,6 +65,8 @@ export function prepareParentContext(
|
|
|
64
65
|
const transcript: unknown[] = [];
|
|
65
66
|
let sharedTurns = 0;
|
|
66
67
|
for (const message of selected) {
|
|
68
|
+
// SDK transcripts include system messages; sharing excludes their content.
|
|
69
|
+
if (message.role === "system") continue;
|
|
67
70
|
if (message.role === "bashExecution") {
|
|
68
71
|
if (!message.excludeFromContext) transcript.push({ role: message.role, command: message.command, output: message.output, exitCode: message.exitCode, truncated: message.truncated });
|
|
69
72
|
continue;
|
|
@@ -73,7 +76,7 @@ export function prepareParentContext(
|
|
|
73
76
|
continue;
|
|
74
77
|
}
|
|
75
78
|
if (message.role !== "user" && message.role !== "assistant" && message.role !== "toolResult" && message.role !== "custom") {
|
|
76
|
-
throw new
|
|
79
|
+
throw new ExpectedFlowError("context_invalid", "Unsupported parent context message role");
|
|
77
80
|
}
|
|
78
81
|
if (message.role === "toolResult" && message.toolCallId === toolCallId) continue;
|
|
79
82
|
if (message.role === "toolResult") {
|
|
@@ -91,7 +94,7 @@ export function prepareParentContext(
|
|
|
91
94
|
if (block.type === "text") blocks.push({ type: "text", text: block.text });
|
|
92
95
|
else if (block.type === "toolCall") {
|
|
93
96
|
if (completed.has(block.id)) blocks.push({ type: "toolCall", id: block.id, name: block.name, arguments: block.arguments });
|
|
94
|
-
} else throw new
|
|
97
|
+
} else throw new ExpectedFlowError("context_invalid", `Unsupported parent context content: ${block.type}; share fewer turns or provide a text briefing`);
|
|
95
98
|
}
|
|
96
99
|
if (blocks.length) transcript.push({ role: message.role, content: blocks,
|
|
97
100
|
...(message.role === "toolResult" ? { toolCallId: message.toolCallId, toolName: message.toolName, isError: message.isError } : {}),
|
|
@@ -101,7 +104,7 @@ export function prepareParentContext(
|
|
|
101
104
|
const text = JSON.stringify(transcript);
|
|
102
105
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
103
106
|
// ponytail: fixed transport ceiling, backend-specific token budgeting if needed.
|
|
104
|
-
if (bytes > 1024 * 1024) throw new
|
|
107
|
+
if (bytes > 1024 * 1024) throw new ExpectedFlowError("context_invalid", "Parent context is too large (over 1 MiB); share fewer turns or provide a text briefing. Nothing was truncated.");
|
|
105
108
|
const receipt: ParentContextReceipt = { mode: context.mode, ...(context.mode === "recent" ? { requestedTurns: context.turns } : {}), sharedTurns, messages: transcript.length, bytes, compacted };
|
|
106
109
|
return {
|
|
107
110
|
context: receipt,
|
package/src/core/run-registry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExpectedFlowError } from "./errors.ts";
|
|
1
2
|
export type RegisteredRunKind = "agent" | "workflow";
|
|
2
3
|
export type RegisteredRunState = "running" | "terminal";
|
|
3
4
|
export type RegisteredRunStatus = "done" | "error" | "aborted";
|
|
@@ -82,7 +83,7 @@ export class RunRegistry {
|
|
|
82
83
|
|
|
83
84
|
start<T>(params: StartRegisteredRun<T>): RegisteredRunHandle<T> {
|
|
84
85
|
if (this.closedSessions.has(params.sessionId) || (params.sessionVersion !== undefined && params.sessionVersion !== this.sessionVersion(params.sessionId))) {
|
|
85
|
-
throw new
|
|
86
|
+
throw new ExpectedFlowError("session_closed", `Session ${params.sessionId} is closed; run was not started`);
|
|
86
87
|
}
|
|
87
88
|
if (this.entries.has(params.runId)) {
|
|
88
89
|
throw new Error(`Run is already registered: ${params.runId}`);
|
package/src/core/spawn.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
createAgentSession,
|
|
3
3
|
DefaultResourceLoader,
|
|
4
4
|
getAgentDir,
|
|
5
|
+
ModelRuntime,
|
|
5
6
|
SessionManager,
|
|
6
7
|
SettingsManager,
|
|
7
8
|
type ExtensionContext,
|
|
@@ -925,12 +926,33 @@ async function spawnSubagentRuntime(params: SpawnSubagentRuntimeParams): Promise
|
|
|
925
926
|
// markModified()/save(), so this still never reaches the settings file.
|
|
926
927
|
settingsManager.applyOverrides({ retry: { enabled: false } });
|
|
927
928
|
|
|
929
|
+
// The extension context exposes a registry, not its owning runtime. Rebuild
|
|
930
|
+
// from the same files and carry over in-memory provider/auth registrations.
|
|
931
|
+
if (model.api === "pi-virtual") {
|
|
932
|
+
throw new Error("Named Pi harnesses require a physical model; virtual model registrations cannot be inherited through the SDK registry.");
|
|
933
|
+
}
|
|
934
|
+
const modelRuntime = await ModelRuntime.create({
|
|
935
|
+
authPath: join(agentDir, "auth.json"),
|
|
936
|
+
modelsPath: join(agentDir, "models.json"),
|
|
937
|
+
});
|
|
938
|
+
for (const id of ctx.modelRegistry.getRegisteredProviderIds()) {
|
|
939
|
+
const native = ctx.modelRegistry.getRegisteredNativeProvider(id);
|
|
940
|
+
if (native) modelRuntime.registerNativeProvider(native);
|
|
941
|
+
const config = ctx.modelRegistry.getRegisteredProviderConfig(id);
|
|
942
|
+
if (config) modelRuntime.registerProvider(id, config);
|
|
943
|
+
}
|
|
944
|
+
if (ctx.modelRegistry.getProviderAuthStatus(model.provider).source === "runtime") {
|
|
945
|
+
const key = await ctx.modelRegistry.getApiKeyForProvider(model.provider);
|
|
946
|
+
if (key) await modelRuntime.setRuntimeApiKey(model.provider, key);
|
|
947
|
+
}
|
|
948
|
+
if (signal?.aborted) throw new Error("Subagent aborted before prompt start");
|
|
949
|
+
|
|
928
950
|
({ session } = await createAgentSession({
|
|
929
951
|
cwd,
|
|
930
952
|
agentDir,
|
|
931
953
|
model,
|
|
932
954
|
thinkingLevel: thinkingLevel as NonNullable<Parameters<typeof createAgentSession>[0]>["thinkingLevel"],
|
|
933
|
-
|
|
955
|
+
modelRuntime,
|
|
934
956
|
settingsManager,
|
|
935
957
|
sessionManager: SessionManager.inMemory(cwd),
|
|
936
958
|
resourceLoader,
|
package/src/external-runs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import { type ExtensionContext, type Theme, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Container, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type, type Static } from "typebox";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
@@ -655,16 +655,25 @@ function normalizeInspectSelectors(params: ExternalRunsParams): ExternalRunsPara
|
|
|
655
655
|
return { ...params, runId, runIds };
|
|
656
656
|
}
|
|
657
657
|
|
|
658
|
+
// Commands and tools share this executor; it needs no tool-only host capabilities.
|
|
659
|
+
type ExternalRunsTool = Omit<ToolDefinition<typeof externalRunsParameters, ExternalRunsDetails>, "execute"> & {
|
|
660
|
+
execute: (
|
|
661
|
+
toolCallId: string, params: ExternalRunsParams, signal: AbortSignal | undefined,
|
|
662
|
+
onUpdate: Parameters<ToolDefinition<typeof externalRunsParameters, ExternalRunsDetails>["execute"]>[3],
|
|
663
|
+
ctx: ExtensionContext,
|
|
664
|
+
) => ReturnType<ToolDefinition<typeof externalRunsParameters, ExternalRunsDetails>["execute"]>;
|
|
665
|
+
};
|
|
666
|
+
|
|
658
667
|
export function createExternalRunsTool(
|
|
659
668
|
options: CreateExternalRunsToolOptions,
|
|
660
|
-
):
|
|
661
|
-
return
|
|
669
|
+
): ExternalRunsTool {
|
|
670
|
+
return {
|
|
662
671
|
name: "external_runs",
|
|
663
672
|
label: "External Runs",
|
|
664
673
|
description: "List, inspect (single or batched summaries), wait for, or cancel session-owned external runs.",
|
|
665
674
|
promptSnippet: EXTERNAL_RUNS_PROMPT_SNIPPET,
|
|
666
675
|
parameters: externalRunsParameters,
|
|
667
|
-
async execute(_toolCallId, params: ExternalRunsParams, signal, onUpdate, ctx) {
|
|
676
|
+
async execute(_toolCallId, params: ExternalRunsParams, signal, onUpdate, ctx: ExtensionContext) {
|
|
668
677
|
if (params.action === "inspect") {
|
|
669
678
|
params = normalizeInspectSelectors(params);
|
|
670
679
|
if (params.runId === undefined && params.runIds !== undefined && params.runIds.length === 1 && params.view !== undefined && params.view !== "summary") {
|
|
@@ -981,5 +990,5 @@ export function createExternalRunsTool(
|
|
|
981
990
|
renderResult(toolResult, { expanded }, theme) {
|
|
982
991
|
return renderExternalRunsResult(toolResult, theme, expanded);
|
|
983
992
|
},
|
|
984
|
-
}
|
|
993
|
+
};
|
|
985
994
|
}
|
package/src/harnesses.ts
CHANGED
|
@@ -3,24 +3,10 @@ import { loadExternalSettings, saveExternalSettings, externalSettingsPath, DEFAU
|
|
|
3
3
|
import type { ThinkingLevel as SdkThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { PI_RESOURCE_PRESETS, type PiResourcePreset } from "./types.ts";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* The pinned SDK exposes its thinking-level union only as a TypeScript type,
|
|
8
|
-
* not a runtime-exported constant array, so this list is hand-maintained here.
|
|
9
|
-
* The `as const satisfies` + AssertNever guards below make drift a compile
|
|
10
|
-
* error rather than a procedural "re-check at upgrade time" reminder: if the
|
|
11
|
-
* SDK's ThinkingLevel union gains or loses a member, `npm run check` fails
|
|
12
|
-
* until this list is updated.
|
|
13
|
-
*/
|
|
6
|
+
/** Flow deliberately supports six levels; SDK additions do not expand settings. */
|
|
14
7
|
export const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const satisfies readonly SdkThinkingLevel[];
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
// Both directions must collapse to never: every SDK level is listed (no missing
|
|
18
|
-
// members) and every listed value is a real SDK level (no extras — also
|
|
19
|
-
// enforced by `satisfies`, but asserted symmetrically for clarity).
|
|
20
|
-
type _MissingThinkingLevels = AssertNever<Exclude<SdkThinkingLevel, (typeof VALID_THINKING_LEVELS)[number]>>;
|
|
21
|
-
type _ExtraThinkingLevels = AssertNever<Exclude<(typeof VALID_THINKING_LEVELS)[number], SdkThinkingLevel>>;
|
|
22
|
-
|
|
23
|
-
export function isValidThinkingLevel(value: unknown): value is SdkThinkingLevel {
|
|
9
|
+
export function isValidThinkingLevel(value: unknown): value is (typeof VALID_THINKING_LEVELS)[number] {
|
|
24
10
|
return typeof value === "string" && (VALID_THINKING_LEVELS as readonly string[]).includes(value);
|
|
25
11
|
}
|
|
26
12
|
|
package/src/pi-subagent.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ExpectedFlowError } from "./core/errors.ts";
|
|
2
|
+
import { agentOutputSchema, agentReceipt, type PublicError } from "./public-contract.ts";
|
|
3
|
+
import { getRunRecord } from "./core/run-inspection.ts";
|
|
4
|
+
import { redactSecrets } from "./core/run-record.ts";
|
|
1
5
|
import {
|
|
2
6
|
defineTool,
|
|
3
7
|
getAgentDir,
|
|
@@ -37,7 +41,7 @@ import { captureParentContext, parentContextSchema, prepareParentContext } from
|
|
|
37
41
|
import { resolvePermission, permissionLabel, resolveEffectivePermissionTier } from "./core/permissions.ts";
|
|
38
42
|
import { pruneRunRecords, runRecordsDirectory } from "./core/retention.ts";
|
|
39
43
|
import { createProgressNode, textResult, type AgentToolResult } from "./core/progress.ts";
|
|
40
|
-
import { RunRegistry } from "./core/run-registry.ts";
|
|
44
|
+
import { RunRegistry, type RegisteredRunEntry } from "./core/run-registry.ts";
|
|
41
45
|
import { formatUsage, renderSubagentNode } from "./core/subagent-render.ts";
|
|
42
46
|
import { SPINNER_INTERVAL_MS } from "./core/spinner.ts";
|
|
43
47
|
import { createWorkflowTool } from "./workflow/tool.ts";
|
|
@@ -359,19 +363,34 @@ function createAgentTool(
|
|
|
359
363
|
description: "Delegate one task to an external Claude Code, Codex CLI, Antigravity, Grok CLI, Muse Code, OpenCode, or registered Pi harness role.",
|
|
360
364
|
promptSnippet: AGENT_PROMPT_SNIPPET,
|
|
361
365
|
parameters: agentToolParameters,
|
|
366
|
+
outputSchema: agentOutputSchema,
|
|
362
367
|
executionMode: "parallel",
|
|
363
368
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
364
|
-
const resume = typeof params.resume === "string" && params.resume.trim() !== "" ? params.resume.trim() : undefined;
|
|
365
|
-
const briefing = prepareParentContext(params.prompt, params.context,
|
|
366
|
-
params.context && params.context.mode !== "none" ? captureParentContext(ctx.sessionManager) : undefined,
|
|
367
|
-
toolCallId, resume);
|
|
368
369
|
const state = getState();
|
|
370
|
+
const receipt = async (result: AgentToolResult, error?: PublicError, settled?: RegisteredRunEntry) => {
|
|
371
|
+
const details = result.details as SubagentToolDetails;
|
|
372
|
+
const durable = details.runId ? await getRunRecord(runRecordsDirectory(), details.runId).catch(() => undefined) : undefined;
|
|
373
|
+
// Evidence I/O may outlast the child: observe live state only after it.
|
|
374
|
+
const entry = settled ?? (details.runId ? state.registry.get(details.runId) : undefined);
|
|
375
|
+
const structuredContent = agentReceipt({ entry, integrity: durable?.integrity, error, inspectable: !!durable || !!(entry && state.registry.get(entry.runId)) });
|
|
376
|
+
return { ...result, content: redactSecrets(result.content) as typeof result.content, structuredContent, isError: !structuredContent.ok };
|
|
377
|
+
};
|
|
378
|
+
const resume = typeof params.resume === "string" && params.resume.trim() !== "" ? params.resume.trim() : undefined;
|
|
379
|
+
let briefing: ReturnType<typeof prepareParentContext>;
|
|
380
|
+
try {
|
|
381
|
+
briefing = prepareParentContext(params.prompt, params.context,
|
|
382
|
+
params.context && params.context.mode !== "none" ? captureParentContext(ctx.sessionManager) : undefined,
|
|
383
|
+
toolCallId, resume);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (!(error instanceof ExpectedFlowError)) throw error;
|
|
386
|
+
return receipt(textResult(error.message, { description: params.description, subagentType: "unknown", status: "error", error: error.message }), error);
|
|
387
|
+
}
|
|
369
388
|
const effectiveState: DelegationState = {
|
|
370
389
|
...state,
|
|
371
390
|
progressEnabled: state.progressEnabled || shouldEnableProgress(ctx),
|
|
372
391
|
};
|
|
373
392
|
const catalog = loadExternalCatalog(getAgentDir());
|
|
374
|
-
if (catalog.blocked) return textResult(catalog.diagnostics.join(" "), { description: params.description, subagentType: "unknown", status: "error", error: catalog.diagnostics.join(" ") });
|
|
393
|
+
if (catalog.blocked) return receipt(textResult(catalog.diagnostics.join(" "), { description: params.description, subagentType: "unknown", status: "error", error: catalog.diagnostics.join(" ") }), { code: "configuration_invalid", message: catalog.diagnostics.join(" ") });
|
|
375
394
|
const allProfiles = catalog.profiles;
|
|
376
395
|
const harnessConfigs = catalog.harnessConfigs;
|
|
377
396
|
const configuredHarnessNames: ReadonlySet<string> = new Set([...EXTERNAL_HARNESSES, ...harnessConfigs.keys()]);
|
|
@@ -388,12 +407,12 @@ function createAgentTool(
|
|
|
388
407
|
const defaultHarness = requestedDefault.harness;
|
|
389
408
|
if (params.role && !params.harness && !configuredHarnessNames.has(defaultHarness)) {
|
|
390
409
|
const error = `Default harness "${defaultHarness}" is not registered (missing from settings.json); pass harness explicitly or recreate it via /external config harness create.`;
|
|
391
|
-
return textResult(error, {
|
|
410
|
+
return receipt(textResult(error, {
|
|
392
411
|
description: params.description,
|
|
393
412
|
subagentType: "unknown",
|
|
394
413
|
status: "error",
|
|
395
414
|
error,
|
|
396
|
-
});
|
|
415
|
+
}), { code: "harness_unavailable", message: error });
|
|
397
416
|
}
|
|
398
417
|
let profile: SubagentProfile;
|
|
399
418
|
try {
|
|
@@ -403,8 +422,9 @@ function createAgentTool(
|
|
|
403
422
|
subagentType: params.subagent_type,
|
|
404
423
|
}, defaultHarness, { configuredHarnessNames, harnessConfigs, disabledHarnesses: catalog.disabledHarnesses });
|
|
405
424
|
} catch (error) {
|
|
406
|
-
|
|
407
|
-
|
|
425
|
+
if (!(error instanceof ExpectedFlowError)) throw error;
|
|
426
|
+
const message = error.message;
|
|
427
|
+
return receipt(textResult(
|
|
408
428
|
message,
|
|
409
429
|
{
|
|
410
430
|
description: params.description,
|
|
@@ -412,7 +432,7 @@ function createAgentTool(
|
|
|
412
432
|
status: "error",
|
|
413
433
|
error: message,
|
|
414
434
|
},
|
|
415
|
-
);
|
|
435
|
+
), { code: error.code, message });
|
|
416
436
|
}
|
|
417
437
|
const subagentType = profile.name;
|
|
418
438
|
profile = resolveExecutionProfile(profile, options.getThinkingLevel(), state.defaultMaxBudgetUsd);
|
|
@@ -420,14 +440,14 @@ function createAgentTool(
|
|
|
420
440
|
const model = resolveProfileModel(profile, ctx);
|
|
421
441
|
if (usesPiBackend(profile) && !model) {
|
|
422
442
|
const error = describeMissingModel(profile, ctx.modelRegistry);
|
|
423
|
-
return textResult(`Cannot launch subagent: ${error}.`, {
|
|
443
|
+
return receipt(textResult(`Cannot launch subagent: ${error}.`, {
|
|
424
444
|
description: params.description,
|
|
425
445
|
subagentType,
|
|
426
446
|
backend: profile.backend,
|
|
427
447
|
harness: selectorHarness(profile),
|
|
428
448
|
status: "error",
|
|
429
449
|
error,
|
|
430
|
-
});
|
|
450
|
+
}), { code: "model_unavailable", message: error });
|
|
431
451
|
}
|
|
432
452
|
|
|
433
453
|
const queuedAt = Date.now();
|
|
@@ -565,7 +585,9 @@ function createAgentTool(
|
|
|
565
585
|
}
|
|
566
586
|
};
|
|
567
587
|
|
|
568
|
-
|
|
588
|
+
let registered;
|
|
589
|
+
try {
|
|
590
|
+
registered = state.registry.start({
|
|
569
591
|
runId: runRecord.runId,
|
|
570
592
|
kind: "agent",
|
|
571
593
|
sessionId,
|
|
@@ -585,8 +607,20 @@ function createAgentTool(
|
|
|
585
607
|
};
|
|
586
608
|
},
|
|
587
609
|
});
|
|
588
|
-
|
|
589
|
-
|
|
610
|
+
} catch (error) {
|
|
611
|
+
if (!(error instanceof ExpectedFlowError)) throw error;
|
|
612
|
+
await runRecord.finish({ status: "error", error: error.message, backendStarted: false });
|
|
613
|
+
return receipt(textResult(error.message, { description: params.description, subagentType, status: "error", error: error.message }), error);
|
|
614
|
+
}
|
|
615
|
+
if (!background) {
|
|
616
|
+
const result = await registered.result;
|
|
617
|
+
const outcome = await registered.terminal;
|
|
618
|
+
return receipt(result, undefined, {
|
|
619
|
+
runId: runRecord.runId, kind: "agent", sessionId, project, state: "terminal",
|
|
620
|
+
observation: (result.details as SubagentToolDetails).progress ?? progress, outcome,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return receipt(textResult(
|
|
590
624
|
`Subagent "${params.description}" (${subagentType}) queued as ${runRecord.runId}. Use external_runs to inspect, wait, or cancel it.`,
|
|
591
625
|
{
|
|
592
626
|
description: params.description,
|
|
@@ -599,7 +633,7 @@ function createAgentTool(
|
|
|
599
633
|
progress,
|
|
600
634
|
backgroundReceipt: true,
|
|
601
635
|
},
|
|
602
|
-
);
|
|
636
|
+
));
|
|
603
637
|
},
|
|
604
638
|
renderCall(args, theme, context) {
|
|
605
639
|
const defaultHarness = options.getDefaultHarness(context.cwd);
|
package/src/profiles.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExpectedFlowError } from "./core/errors.ts";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync, lstatSync } from "node:fs";
|
|
2
3
|
import { basename, join } from "node:path";
|
|
3
4
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
@@ -431,7 +432,7 @@ export function reconcilePiProfileWithHarness(
|
|
|
431
432
|
harnessConfigs: ReadonlyMap<string, HarnessConfig>,
|
|
432
433
|
): SubagentProfile {
|
|
433
434
|
const { profile: reconciled, conflict } = computeReconciledPiProfile(profile, harnessConfigs);
|
|
434
|
-
if (conflict) throw new
|
|
435
|
+
if (conflict) throw new ExpectedFlowError("selection_invalid", conflict);
|
|
435
436
|
return reconciled;
|
|
436
437
|
}
|
|
437
438
|
|
|
@@ -522,36 +523,36 @@ export function resolveExternalProfile(
|
|
|
522
523
|
|
|
523
524
|
if (subagentType) {
|
|
524
525
|
if (role || harness) {
|
|
525
|
-
throw new
|
|
526
|
+
throw new ExpectedFlowError("selection_invalid", "Choose either role (with optional harness) or legacy subagent_type; do not combine them.");
|
|
526
527
|
}
|
|
527
528
|
const matches = [...profiles.values()].filter(p => p.configVersion === 5 && p.role && `${selectorHarness(p)}-${p.role}` === subagentType);
|
|
528
|
-
if (!profiles.has(subagentType) && matches.length > 1) throw new
|
|
529
|
+
if (!profiles.has(subagentType) && matches.length > 1) throw new ExpectedFlowError("selection_invalid", `Ambiguous legacy selector "${subagentType}"; use role and harness explicitly.`);
|
|
529
530
|
const profile = profiles.get(subagentType) ?? matches[0];
|
|
530
531
|
if (!profile) {
|
|
531
|
-
throw new
|
|
532
|
+
throw new ExpectedFlowError("selection_invalid", unknownExternalProfileMessage(subagentType, profiles.keys()));
|
|
532
533
|
}
|
|
533
|
-
if (profile.configurationError) throw new
|
|
534
|
+
if (profile.configurationError) throw new ExpectedFlowError("selection_invalid", profile.configurationError);
|
|
534
535
|
return reconcilePiProfileWithHarness(profile, harnessConfigs);
|
|
535
536
|
}
|
|
536
537
|
|
|
537
538
|
if (!role) {
|
|
538
|
-
throw new
|
|
539
|
+
throw new ExpectedFlowError("selection_invalid", harness
|
|
539
540
|
? "role is required when harness is provided; otherwise provide role or legacy subagent_type."
|
|
540
541
|
: "Either role or legacy subagent_type is required.");
|
|
541
542
|
}
|
|
542
543
|
const selectedHarness = harness || defaultHarness;
|
|
543
544
|
if (!configuredHarnessNames.has(selectedHarness)) {
|
|
544
|
-
throw new
|
|
545
|
+
throw new ExpectedFlowError("selection_invalid", `Unknown external harness "${selectedHarness}". Choose one of: ${[...configuredHarnessNames].join(", ") || "none"}.`);
|
|
545
546
|
}
|
|
546
547
|
if ((options.disabledHarnesses ?? DEFAULT_RESOLVE_OPTIONS.disabledHarnesses).has(selectedHarness)) {
|
|
547
|
-
throw new
|
|
548
|
+
throw new ExpectedFlowError("selection_invalid", disabledHarnessMessage(selectedHarness, !harness));
|
|
548
549
|
}
|
|
549
550
|
|
|
550
551
|
const legacy = profiles.get(`${selectedHarness}-${role}`);
|
|
551
552
|
const exact = profiles.get(bindingKey(selectedHarness, role)) ?? (legacy?.configVersion === 5 ? undefined : legacy);
|
|
552
553
|
if (exact) {
|
|
553
|
-
if (exact.configurationError) throw new
|
|
554
|
-
if (selectorHarness(exact) !== selectedHarness || externalProfileRole(exact) !== role) throw new
|
|
554
|
+
if (exact.configurationError) throw new ExpectedFlowError("selection_invalid", exact.configurationError);
|
|
555
|
+
if (selectorHarness(exact) !== selectedHarness || externalProfileRole(exact) !== role) throw new ExpectedFlowError("selection_invalid", `Override "${exact.name}" does not match selected harness "${selectedHarness}".`);
|
|
555
556
|
return reconcilePiProfileWithHarness(exact, harnessConfigs);
|
|
556
557
|
}
|
|
557
558
|
|
|
@@ -566,12 +567,12 @@ export function resolveExternalProfile(
|
|
|
566
567
|
const availability = externalRoleAvailability(profiles);
|
|
567
568
|
const supported = availability.get(role);
|
|
568
569
|
if (supported?.length) {
|
|
569
|
-
throw new
|
|
570
|
+
throw new ExpectedFlowError("selection_invalid",
|
|
570
571
|
`Role "${role}" is unavailable for harness "${selectedHarness}". Supported harnesses for this role: ${supported.join(", ")}. Choose one of those harnesses or add profile "${selectedHarness}-${role}".`,
|
|
571
572
|
);
|
|
572
573
|
}
|
|
573
574
|
const knownRoles = new Set([...availability.keys(), ...(EXTERNAL_HARNESSES.includes(selectedHarness as ExternalHarness) ? [] : defaultRoleNames())]);
|
|
574
|
-
throw new
|
|
575
|
+
throw new ExpectedFlowError("selection_invalid",
|
|
575
576
|
`Unknown external role "${role}". Available roles: ${[...knownRoles].join(", ") || "none"}. Nonstandard profile names must be selected with legacy subagent_type.`,
|
|
576
577
|
);
|
|
577
578
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { StringEnum, type JsonObject } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
import { projectLiveAgent } from "./core/run-projection.ts";
|
|
4
|
+
import type { RegisteredRunEntry } from "./core/run-registry.ts";
|
|
5
|
+
import { FLOW_ERROR_CODES } from "./core/errors.ts";
|
|
6
|
+
import { redactSecrets } from "./core/run-record.ts";
|
|
7
|
+
|
|
8
|
+
export const INLINE_RESULT_BYTES = 16 * 1024;
|
|
9
|
+
const strings = <T extends string[]>(...values: T) => StringEnum(values);
|
|
10
|
+
const errorSchema = Type.Object({ code: StringEnum(FLOW_ERROR_CODES), message: Type.String() });
|
|
11
|
+
const inspectRef = Type.Object({ tool: Type.Literal("external_runs"), action: Type.Literal("inspect"), runIds: Type.Array(Type.String()), view: strings("summary", "final", "diagnostics") });
|
|
12
|
+
const runSchema = Type.Object({
|
|
13
|
+
runId: Type.String(), kind: Type.Literal("agent"), live: Type.Boolean(),
|
|
14
|
+
task: Type.Object({ description: Type.Optional(Type.String()), profile: Type.Optional(Type.String()), backend: Type.Optional(Type.String()), harness: Type.Optional(Type.String()) }),
|
|
15
|
+
state: Type.Object({ status: strings("queued", "running", "done", "error", "aborted"), outcome: Type.Optional(strings("succeeded", "failed", "cancelled", "timed_out")) }),
|
|
16
|
+
timing: Type.Object({
|
|
17
|
+
queuedAt: Type.Optional(Type.String()), executionStartedAt: Type.Optional(Type.String()),
|
|
18
|
+
processStartedAt: Type.Optional(Type.String()), firstActivityAt: Type.Optional(Type.String()),
|
|
19
|
+
lastActivityAt: Type.Optional(Type.String()), finishedAt: Type.Optional(Type.String()),
|
|
20
|
+
queueDelayMs: Type.Optional(Type.Number()), elapsedMs: Type.Optional(Type.Number()),
|
|
21
|
+
activityAgeMs: Type.Optional(Type.Number()), processDurationMs: Type.Optional(Type.Number()),
|
|
22
|
+
}),
|
|
23
|
+
output: Type.Object({ available: Type.Boolean(), finalAvailable: Type.Boolean(), delivery: strings("inline", "reference", "none"), value: Type.Optional(Type.Unknown()) }),
|
|
24
|
+
evidence: Type.Object({ integrity: strings("complete", "incomplete", "damaged", "unknown") }),
|
|
25
|
+
refs: Type.Optional(Type.Object({ summary: inspectRef, final: inspectRef, diagnostics: inspectRef })),
|
|
26
|
+
});
|
|
27
|
+
const common = {
|
|
28
|
+
contractVersion: Type.Literal(1), tool: Type.Literal("Agent"), action: Type.Literal("delegate"),
|
|
29
|
+
observedAt: Type.String(), data: Type.Object({ run: Type.Union([Type.Null(), runSchema]) }),
|
|
30
|
+
warnings: Type.Array(Type.String()),
|
|
31
|
+
};
|
|
32
|
+
export const agentOutputSchema = Type.Union([
|
|
33
|
+
Type.Object({ ...common, ok: Type.Literal(true), error: Type.Optional(Type.Never()) }),
|
|
34
|
+
Type.Object({ ...common, ok: Type.Literal(false), error: errorSchema }),
|
|
35
|
+
]);
|
|
36
|
+
export type AgentReceipt = Static<typeof agentOutputSchema>;
|
|
37
|
+
export type PublicError = Static<typeof errorSchema>;
|
|
38
|
+
|
|
39
|
+
/** Only registry-confirmed entries may supply a public handle. Never infer liveness from disk. */
|
|
40
|
+
export function agentReceipt({ entry, integrity = "unknown", error, inspectable = true }: {
|
|
41
|
+
entry?: RegisteredRunEntry;
|
|
42
|
+
integrity?: "complete" | "incomplete" | "damaged" | "unknown";
|
|
43
|
+
error?: PublicError;
|
|
44
|
+
inspectable?: boolean;
|
|
45
|
+
}): AgentReceipt & JsonObject {
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
let run: AgentReceipt["data"]["run"] = null;
|
|
48
|
+
if (entry) {
|
|
49
|
+
const projected = projectLiveAgent(entry);
|
|
50
|
+
const value = entry.outcome?.result;
|
|
51
|
+
const finalAvailable = projected.output.finalAvailable;
|
|
52
|
+
let encoded: string | undefined;
|
|
53
|
+
try { encoded = finalAvailable ? JSON.stringify(value) : undefined; } catch { /* Non-JSON values are never presented as complete. */ }
|
|
54
|
+
const redacted = encoded === undefined ? undefined : redactSecrets(JSON.parse(encoded));
|
|
55
|
+
const original = encoded;
|
|
56
|
+
encoded = encoded === undefined ? undefined : JSON.stringify(redacted);
|
|
57
|
+
if (encoded !== original) warnings.push("output_redacted");
|
|
58
|
+
const inline = encoded !== undefined && Buffer.byteLength(encoded, "utf8") <= INLINE_RESULT_BYTES;
|
|
59
|
+
const ref = (view: "summary" | "final" | "diagnostics") => ({ tool: "external_runs" as const, action: "inspect" as const, runIds: [entry.runId], view });
|
|
60
|
+
run = {
|
|
61
|
+
runId: entry.runId, kind: "agent", live: projected.live, task: projected.task,
|
|
62
|
+
state: { status: projected.state.status as NonNullable<AgentReceipt["data"]["run"]>["state"]["status"], ...(entry.outcome ? { outcome: entry.outcome.outcome } : {}) },
|
|
63
|
+
timing: projected.timing,
|
|
64
|
+
output: { available: projected.output.available, finalAvailable, delivery: inline ? "inline" : finalAvailable && inspectable ? "reference" : "none", ...(inline ? { value: redacted } : {}) },
|
|
65
|
+
evidence: { integrity }, ...(inspectable ? { refs: { summary: ref("summary"), final: ref("final"), diagnostics: ref("diagnostics") } } : {}),
|
|
66
|
+
};
|
|
67
|
+
if (!error && entry.outcome && entry.outcome.status !== "done") {
|
|
68
|
+
error = { code: entry.outcome.outcome === "succeeded" ? "failed" : entry.outcome.outcome, message: entry.outcome.error ?? "Child execution failed" };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const base = { contractVersion: 1 as const, tool: "Agent" as const, action: "delegate" as const, observedAt: new Date().toISOString(), data: { run }, warnings };
|
|
72
|
+
return JSON.parse(JSON.stringify(redactSecrets(error ? { ...base, ok: false, error: { code: error.code, message: error.message } } : { ...base, ok: true }))) as AgentReceipt & JsonObject;
|
|
73
|
+
}
|