@aeondave/pi-persona 1.17.1 → 1.17.4
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 +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
package/src/bus/contact.ts
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* `customTool` by the engine, bound to the shared {@link InProcessBus} + the child's
|
|
6
6
|
* handle.
|
|
7
7
|
*
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
10
|
-
* - `need_decision` / `interview_request` (blocking): `bus.ask` — the child waits for
|
|
8
|
+
* - `progress` (one-way): post and return immediately.
|
|
9
|
+
* - `decision` / `interview` (blocking): `bus.ask` — the child waits for
|
|
11
10
|
* the supervisor's reply. This only makes sense in **async** runs where the supervisor
|
|
12
11
|
* is free to answer; the tool aborts cleanly on the run's signal so it never hangs a
|
|
13
12
|
* sync run with no one listening.
|
|
@@ -18,27 +17,68 @@
|
|
|
18
17
|
import { type Static, Type } from "typebox";
|
|
19
18
|
|
|
20
19
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
21
21
|
|
|
22
|
+
import { compactInlineText } from "../ui/presentation.ts";
|
|
23
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
22
24
|
import type { InProcessBus, MsgKind } from "./inproc.ts";
|
|
23
25
|
|
|
24
26
|
const ContactParams = Type.Object({
|
|
25
27
|
kind: Type.Union([Type.Literal("progress"), Type.Literal("decision"), Type.Literal("interview")], {
|
|
26
28
|
description:
|
|
27
|
-
"progress = one-way status update (don't wait); decision = ask the supervisor to choose; interview = ask a clarifying question.
|
|
29
|
+
"progress = one-way status update (don't wait); decision = ask the supervisor to choose; interview = ask a clarifying question. Async decision/interview requests wait for a reply; synchronous requests post one-way with no possible answer, so the request remains unresolved.",
|
|
28
30
|
}),
|
|
29
31
|
message: Type.String({ description: "What to tell or ask the supervisor — specific and self-contained." }),
|
|
30
32
|
});
|
|
31
33
|
|
|
32
|
-
type
|
|
34
|
+
type ContactOutcome = "reported" | "answered" | "unavailable" | "timed-out" | "cancelled" | "failed";
|
|
35
|
+
|
|
36
|
+
type ContactDetails = {
|
|
37
|
+
kind: MsgKind;
|
|
38
|
+
/** Delivery evidence only, never evidence of a reply or approval. False means delivery was not confirmed. */
|
|
39
|
+
delivered: boolean;
|
|
40
|
+
outcome: ContactOutcome;
|
|
41
|
+
};
|
|
33
42
|
|
|
34
43
|
type AskFailure = "aborted" | "timeout" | "inbox-full" | "unavailable" | "generic";
|
|
35
44
|
|
|
45
|
+
const UNRESOLVED_NEXT_STEP = "Continue only with independent work already authorized; otherwise return a blocked/partial result.";
|
|
46
|
+
|
|
47
|
+
function unresolvedGuidance(kind: "decision" | "interview"): string {
|
|
48
|
+
return `The ${kind} remains unresolved. ${UNRESOLVED_NEXT_STEP}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function outcomeForAskFailure(failure: AskFailure): ContactOutcome {
|
|
52
|
+
switch (failure) {
|
|
53
|
+
case "aborted": return "cancelled";
|
|
54
|
+
case "timeout": return "timed-out";
|
|
55
|
+
case "inbox-full":
|
|
56
|
+
case "unavailable": return "unavailable";
|
|
57
|
+
case "generic": return "failed";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function askFailureText(failure: AskFailure): string {
|
|
62
|
+
switch (failure) {
|
|
63
|
+
case "aborted": return "The request was cancelled before the supervisor replied.";
|
|
64
|
+
case "timeout": return "The supervisor did not reply in time.";
|
|
65
|
+
case "inbox-full": return "The supervisor inbox is full; this request was not delivered. Retry later or consolidate the question.";
|
|
66
|
+
case "unavailable": return "The supervisor is unavailable; delivery could not be confirmed.";
|
|
67
|
+
case "generic": return "The supervisor request failed; delivery could not be confirmed.";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
function classifyAskFailure(error: unknown, signal: AbortSignal | undefined): AskFailure {
|
|
37
72
|
if (signal?.aborted) return "aborted";
|
|
38
73
|
const message = error instanceof Error ? error.message : "";
|
|
39
74
|
if (/^ask timeout(?:\s|$)/i.test(message)) return "timeout";
|
|
40
75
|
if (/^inbox full(?:\s|:|$)/i.test(message)) return "inbox-full";
|
|
41
|
-
if (
|
|
76
|
+
if (
|
|
77
|
+
/^ask unregistered(?:\s|$)/i.test(message) ||
|
|
78
|
+
/^unknown peer(?:\s|:|$)/i.test(message) ||
|
|
79
|
+
/^bridge: not connected(?:\s|$)/i.test(message) ||
|
|
80
|
+
/^broker (?:connection|client) closed(?:\s|$)/i.test(message)
|
|
81
|
+
) return "unavailable";
|
|
42
82
|
return "generic";
|
|
43
83
|
}
|
|
44
84
|
|
|
@@ -52,7 +92,7 @@ function result(t: string, details: ContactDetails): { content: [{ type: "text";
|
|
|
52
92
|
*
|
|
53
93
|
* `allowBlocking` (async runs only): when false, a `decision`/`interview` does NOT block —
|
|
54
94
|
* the supervisor is busy (a sync run holds the turn), so the question is posted one-way and
|
|
55
|
-
*
|
|
95
|
+
* returns unresolved. This prevents a sync delegate/council from deadlocking on a child ask. */
|
|
56
96
|
export function makeContactSupervisorTool(
|
|
57
97
|
bus: InProcessBus,
|
|
58
98
|
fromHandle: string,
|
|
@@ -68,32 +108,49 @@ export function makeContactSupervisorTool(
|
|
|
68
108
|
"Use `progress` to report a milestone (one-way, don't wait).",
|
|
69
109
|
"Use `decision` when you hit a choice only the supervisor should make, or `interview` to ask a clarifying question.",
|
|
70
110
|
allowBlocking
|
|
71
|
-
? "This async run waits for the supervisor's reply before continuing."
|
|
72
|
-
: "This synchronous run cannot block: decision/interview are posted one-way and return immediately;
|
|
111
|
+
? "This async run waits for the supervisor's reply before continuing. If there is no reply, the request remains unresolved: continue only with independent work already authorized, otherwise return a blocked/partial result."
|
|
112
|
+
: "This synchronous run cannot block: decision/interview are posted one-way and return immediately; no answer is possible in this path, so the request remains unresolved. Continue only with independent work already authorized, otherwise return a blocked/partial result.",
|
|
73
113
|
`Messages longer than ${MAX_CONTACT_MESSAGE_CHARS} characters are shortened before delivery; use smaller messages to preserve full detail.`,
|
|
74
114
|
].join(" "),
|
|
75
115
|
promptSnippet: "contact_supervisor — report progress or ask the supervisor a question",
|
|
76
116
|
parameters: ContactParams,
|
|
77
117
|
async execute(_toolCallId, params: Static<typeof ContactParams>, signal, _onUpdate, _ctx) {
|
|
78
118
|
const kind = params.kind as MsgKind;
|
|
119
|
+
if (signal?.aborted) {
|
|
120
|
+
const text = kind === "progress"
|
|
121
|
+
? "The progress update was cancelled before delivery."
|
|
122
|
+
: `The request was cancelled before the supervisor was contacted. ${unresolvedGuidance(kind)}`;
|
|
123
|
+
return result(text, { kind, delivered: false, outcome: "cancelled" });
|
|
124
|
+
}
|
|
79
125
|
const message = boundContactMessage(params.message);
|
|
80
126
|
if (kind === "progress") {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
try {
|
|
128
|
+
const delivered = bus.send(fromHandle, supervisorHandle, message, "progress");
|
|
129
|
+
return result(
|
|
130
|
+
delivered ? "Progress reported to the supervisor." : "(no supervisor listening or progress delivery could not be confirmed; progress dropped)",
|
|
131
|
+
{ kind, delivered, outcome: delivered ? "reported" : "unavailable" },
|
|
132
|
+
);
|
|
133
|
+
} catch {
|
|
134
|
+
return result("(progress could not be sent; delivery could not be confirmed)", { kind, delivered: false, outcome: "failed" });
|
|
135
|
+
}
|
|
86
136
|
}
|
|
87
137
|
|
|
88
138
|
// In a sync run the supervisor is blocked holding the turn → it cannot answer. Post the
|
|
89
|
-
// question one-way
|
|
139
|
+
// question one-way, but return unresolved; the send is not authorization to proceed on it.
|
|
90
140
|
if (!allowBlocking) {
|
|
91
|
-
|
|
141
|
+
let delivered: boolean;
|
|
142
|
+
try {
|
|
143
|
+
delivered = bus.send(fromHandle, supervisorHandle, message, kind);
|
|
144
|
+
} catch {
|
|
145
|
+
return result(
|
|
146
|
+
`The question could not be sent; no answer is possible in this path. ${unresolvedGuidance(kind)}`,
|
|
147
|
+
{ kind, delivered: false, outcome: "failed" },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const delivery = delivered ? "question sent one-way" : "question delivery could not be confirmed";
|
|
92
151
|
return result(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
: "(no supervisor reachable — proceed using your best judgement)",
|
|
96
|
-
{ kind, delivered },
|
|
152
|
+
`The supervisor is busy right now; ${delivery}, and no answer is possible in this path. ${unresolvedGuidance(kind)}`,
|
|
153
|
+
{ kind, delivered, outcome: "unavailable" },
|
|
97
154
|
);
|
|
98
155
|
}
|
|
99
156
|
|
|
@@ -103,30 +160,48 @@ export function makeContactSupervisorTool(
|
|
|
103
160
|
let answer: Promise<string>;
|
|
104
161
|
try {
|
|
105
162
|
answer = bus.ask(fromHandle, supervisorHandle, message, signal ? { kind, signal } : { kind });
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const failure = classifyAskFailure(error, signal);
|
|
165
|
+
return result(`${askFailureText(failure)} ${unresolvedGuidance(kind)}`, {
|
|
166
|
+
kind,
|
|
167
|
+
delivered: false,
|
|
168
|
+
outcome: outcomeForAskFailure(failure),
|
|
169
|
+
});
|
|
108
170
|
}
|
|
109
171
|
try {
|
|
110
172
|
const reply = await answer;
|
|
111
|
-
|
|
173
|
+
// A response that settles concurrently with cancellation is stale for this call:
|
|
174
|
+
// never report it as an answer once the caller's signal has aborted.
|
|
175
|
+
if (signal?.aborted) {
|
|
176
|
+
return result(`${askFailureText("aborted")} ${unresolvedGuidance(kind)}`, {
|
|
177
|
+
kind,
|
|
178
|
+
delivered: false,
|
|
179
|
+
outcome: "cancelled",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return result(`Supervisor: ${reply}`, { kind, delivered: true, outcome: "answered" });
|
|
112
183
|
} catch (error) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
case "inbox-full":
|
|
120
|
-
return "(the supervisor inbox is full — this request was not delivered; retry later or consolidate the question)";
|
|
121
|
-
case "unavailable":
|
|
122
|
-
return "(the supervisor is no longer available — this request was not delivered; proceed using your best judgement)";
|
|
123
|
-
default:
|
|
124
|
-
return "(the supervisor request could not be completed — proceed using your best judgement)";
|
|
125
|
-
}
|
|
126
|
-
})();
|
|
127
|
-
return result(msg, { kind, delivered: false });
|
|
184
|
+
const failure = classifyAskFailure(error, signal);
|
|
185
|
+
return result(`${askFailureText(failure)} ${unresolvedGuidance(kind)}`, {
|
|
186
|
+
kind,
|
|
187
|
+
delivered: false,
|
|
188
|
+
outcome: outcomeForAskFailure(failure),
|
|
189
|
+
});
|
|
128
190
|
}
|
|
129
191
|
},
|
|
192
|
+
// Compact purpose line only: the message is the child's own prose, and echoing it in the
|
|
193
|
+
// call card would overflow the transcript the way the host fallback's raw-args echo does.
|
|
194
|
+
renderCall(args, theme) {
|
|
195
|
+
const kind = compactInlineText(args.kind ?? "", { maxChars: 12 });
|
|
196
|
+
return new Text(theme.fg("toolTitle", theme.bold(kind ? `contact ${kind}` : "contact")), 0, 0);
|
|
197
|
+
},
|
|
198
|
+
// One shared literal receipt. An answered ask is bounded collapsed and expands to the full
|
|
199
|
+
// reply; progress receipts are naturally one compact line. Unresolved outcomes
|
|
200
|
+
// (unavailable/timed-out/cancelled/failed) are SUCCESSFUL tool results carrying an
|
|
201
|
+
// unresolved outcome — the error heading appears only when the host context says isError.
|
|
202
|
+
renderResult(result, { expanded }, theme, context) {
|
|
203
|
+
return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
204
|
+
},
|
|
130
205
|
});
|
|
131
206
|
}
|
|
132
207
|
/** Maximum body accepted by contact_supervisor's message action. Full reports should use the
|
package/src/bus/peers.ts
CHANGED
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
import { type Static, Type } from "typebox";
|
|
21
21
|
|
|
22
22
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
23
24
|
|
|
25
|
+
import { compactInlineText } from "../ui/presentation.ts";
|
|
26
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
24
27
|
import type { InProcessBus } from "./inproc.ts";
|
|
25
28
|
|
|
26
29
|
/** Anti ping-pong budget: after this many sends the tool tells the member to finalize. */
|
|
@@ -110,5 +113,17 @@ export function makeContactPeerTool(bus: InProcessBus, selfHandle: string, deps:
|
|
|
110
113
|
{ action: "send", delivered },
|
|
111
114
|
);
|
|
112
115
|
},
|
|
116
|
+
// Compact purpose line only — same rule as contact_supervisor: no note body in the call
|
|
117
|
+
// card (the host fallback would echo raw args).
|
|
118
|
+
renderCall(args, theme) {
|
|
119
|
+
const action = compactInlineText(args.action ?? "", { maxChars: 12 });
|
|
120
|
+
return new Text(theme.fg("toolTitle", theme.bold(action ? `peer ${action}` : "peer")), 0, 0);
|
|
121
|
+
},
|
|
122
|
+
// One shared literal receipt: a send receipt is one compact line; a long peer list stays
|
|
123
|
+
// bounded collapsed (expansion hint) and shows every peer expanded. The error heading comes
|
|
124
|
+
// only from the host's isError flag, never from the semantic content.
|
|
125
|
+
renderResult(result, { expanded }, theme, context) {
|
|
126
|
+
return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
127
|
+
},
|
|
113
128
|
});
|
|
114
129
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Narrow result-read boundary. No archive ownership, discovery, paths or session identity.
|
|
2
|
+
* The parent supplies a reader that checks CURRENT capabilities/owner and its explicit ref
|
|
3
|
+
* grant on every call. The broker owns attachment, cancellation and wire validation only. */
|
|
4
|
+
import { isResultRef } from "../results/record.ts";
|
|
5
|
+
import { RESULT_READ_DEFAULT, RESULT_READ_MAX, type ResultReadRequest, type ResultProvenance,
|
|
6
|
+
type TerminalStatus } from "../results/types.ts";
|
|
7
|
+
|
|
8
|
+
export type { ResultReadRequest } from "../results/types.ts";
|
|
9
|
+
export const RESULT_READ_TIMEOUT_MS = 10_000;
|
|
10
|
+
export const RESULT_READ_MAX_PENDING = 8;
|
|
11
|
+
export const RESULT_READ_MAX_FRAME_BYTES = 128 * 1024;
|
|
12
|
+
/** Bound replay tracking without ever evicting an id and accepting its replay. Exhaustion
|
|
13
|
+
* ends the reader connection; it does not reconnect or retry. */
|
|
14
|
+
export const RESULT_READ_MAX_REQUESTS = 4096;
|
|
15
|
+
|
|
16
|
+
const ERROR_MESSAGES = {
|
|
17
|
+
invalid: "Invalid result read request.",
|
|
18
|
+
denied: "Result read is not authorized.",
|
|
19
|
+
revoked: "Result reader was revoked.",
|
|
20
|
+
unavailable: "Result reader is unavailable.",
|
|
21
|
+
cancelled: "Result read was cancelled.",
|
|
22
|
+
timeout: "Result read deadline exceeded.",
|
|
23
|
+
busy: "Too many pending result reads.",
|
|
24
|
+
protocol: "Invalid result reader protocol.",
|
|
25
|
+
} as const;
|
|
26
|
+
export type ReadErrorCode = keyof typeof ERROR_MESSAGES;
|
|
27
|
+
export type ReadReply = {
|
|
28
|
+
ok: true; ref: string; body: string; offset: number; nextOffset?: number;
|
|
29
|
+
storedChars: number; originalChars: number; truncated: boolean; endOfStoredBody: boolean;
|
|
30
|
+
status: TerminalStatus; provenance: ResultProvenance;
|
|
31
|
+
} | { ok: false; code: ReadErrorCode; message: string };
|
|
32
|
+
|
|
33
|
+
export interface ScopedReader {
|
|
34
|
+
read(request: ResultReadRequest, signal?: AbortSignal): Promise<ReadReply>;
|
|
35
|
+
revoke(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PreparedResultReader {
|
|
39
|
+
/** Private spawn environment only. Never put in tasks, receipts or diagnostics. */
|
|
40
|
+
token: string;
|
|
41
|
+
revoke(): void;
|
|
42
|
+
/** Historical attachment evidence, not a promise of continued connectivity. */
|
|
43
|
+
wasBound(): boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readFailure(code: ReadErrorCode): ReadReply & { ok: false } {
|
|
47
|
+
return { ok: false, code, message: ERROR_MESSAGES[code] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Reject hidden/accessor fields too: this validator is also used on parent callback data. */
|
|
51
|
+
export function closedRecord(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
52
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
53
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
54
|
+
return (proto === Object.prototype || proto === null) && Reflect.ownKeys(value).every((key) => {
|
|
55
|
+
const d = Object.getOwnPropertyDescriptor(value, key);
|
|
56
|
+
return typeof key === "string" && keys.includes(key) && d?.enumerable === true && "value" in d;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const counter = (n: unknown): n is number => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
60
|
+
|
|
61
|
+
export function isReadRequest(value: unknown): value is ResultReadRequest {
|
|
62
|
+
return closedRecord(value, ["ref", "offset", "limit"]) && isResultRef(value.ref)
|
|
63
|
+
&& (!("offset" in value) || counter(value.offset))
|
|
64
|
+
&& (!("limit" in value) || (counter(value.limit) && value.limit >= 2 && value.limit <= RESULT_READ_MAX));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isReadReply(value: unknown): value is ReadReply {
|
|
68
|
+
if (!closedRecord(value, ["ok", "ref", "body", "offset", "nextOffset", "storedChars", "originalChars",
|
|
69
|
+
"truncated", "endOfStoredBody", "status", "provenance", "code", "message"])) return false;
|
|
70
|
+
if (value.ok === false) return closedRecord(value, ["ok", "code", "message"])
|
|
71
|
+
&& typeof value.code === "string" && Object.hasOwn(ERROR_MESSAGES, value.code)
|
|
72
|
+
&& value.message === ERROR_MESSAGES[value.code as ReadErrorCode];
|
|
73
|
+
if (value.ok !== true || "code" in value || "message" in value || !isResultRef(value.ref)
|
|
74
|
+
|| typeof value.body !== "string" || value.body.length > RESULT_READ_MAX || !counter(value.offset)
|
|
75
|
+
|| !counter(value.storedChars) || !counter(value.originalChars) || value.originalChars < value.storedChars
|
|
76
|
+
|| value.truncated !== (value.originalChars > value.storedChars)
|
|
77
|
+
|| (value.status !== "done" && value.status !== "failed" && value.status !== "stopped")
|
|
78
|
+
|| (value.provenance !== "result-output" && value.provenance !== "partial-output" && value.provenance !== "error")) return false;
|
|
79
|
+
const end = value.offset + value.body.length;
|
|
80
|
+
if (!Number.isSafeInteger(end) || end > value.storedChars || value.endOfStoredBody !== (end === value.storedChars)) return false;
|
|
81
|
+
return value.endOfStoredBody ? !("nextOffset" in value) : value.body.length > 0 && value.nextOffset === end;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Page offsets are UTF-16; the archive, not transport, owns surrogate-boundary slicing. */
|
|
85
|
+
export function isReadReplyFor(value: unknown, request: ResultReadRequest): value is ReadReply {
|
|
86
|
+
return isReadReply(value) && (!value.ok || (value.ref === request.ref && value.offset === (request.offset ?? 0)
|
|
87
|
+
&& value.body.length <= (request.limit ?? RESULT_READ_DEFAULT)));
|
|
88
|
+
}
|