@multiagency/sdk 0.3.1

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.
Files changed (78) hide show
  1. package/CHANGES.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +142 -0
  4. package/THIRD-PARTY-NOTICES +127 -0
  5. package/bin/release-server.d.mts +29 -0
  6. package/bin/release-server.mjs +143 -0
  7. package/dist/compensation-contract.d.ts +370 -0
  8. package/dist/compensation-contract.js +51 -0
  9. package/dist/contract/agent-connection.d.ts +86 -0
  10. package/dist/contract/agent-connection.js +36 -0
  11. package/dist/contract/app-access.d.ts +141 -0
  12. package/dist/contract/app-access.js +57 -0
  13. package/dist/contract/app-client.d.ts +140 -0
  14. package/dist/contract/app-client.js +61 -0
  15. package/dist/contract/app-consent.d.ts +68 -0
  16. package/dist/contract/app-consent.js +91 -0
  17. package/dist/contract/assets.d.ts +6 -0
  18. package/dist/contract/assets.js +6 -0
  19. package/dist/contract/client.d.ts +347 -0
  20. package/dist/contract/client.js +81 -0
  21. package/dist/contract/gateway.d.ts +74 -0
  22. package/dist/contract/gateway.js +47 -0
  23. package/dist/contract/index.d.ts +26 -0
  24. package/dist/contract/index.js +25 -0
  25. package/dist/contract/inputs.d.ts +230 -0
  26. package/dist/contract/inputs.js +99 -0
  27. package/dist/contract/intents-account.d.ts +5 -0
  28. package/dist/contract/intents-account.js +8 -0
  29. package/dist/contract/orpc-client.js +2037 -0
  30. package/dist/contract/owner-capabilities.d.ts +81 -0
  31. package/dist/contract/owner-capabilities.js +33 -0
  32. package/dist/contract/owner-client.d.ts +99 -0
  33. package/dist/contract/owner-client.js +146 -0
  34. package/dist/contract/owner-contract.d.ts +1156 -0
  35. package/dist/contract/owner-contract.js +37 -0
  36. package/dist/contract/owner-inputs.d.ts +166 -0
  37. package/dist/contract/owner-inputs.js +1 -0
  38. package/dist/contract/owner-outputs.d.ts +5749 -0
  39. package/dist/contract/owner-outputs.js +751 -0
  40. package/dist/contract/package-version.d.ts +5 -0
  41. package/dist/contract/package-version.js +2 -0
  42. package/dist/contract/payee-entry.d.ts +10 -0
  43. package/dist/contract/payee-entry.js +10 -0
  44. package/dist/contract/payee.d.ts +326 -0
  45. package/dist/contract/payee.js +32 -0
  46. package/dist/contract/permissions.d.ts +20 -0
  47. package/dist/contract/permissions.js +64 -0
  48. package/dist/contract/proposals.d.ts +178 -0
  49. package/dist/contract/proposals.js +27 -0
  50. package/dist/contract/run-log-schema.d.ts +888 -0
  51. package/dist/contract/run-log-schema.js +150 -0
  52. package/dist/contract/run-progress-schema.d.ts +104 -0
  53. package/dist/contract/run-progress-schema.js +20 -0
  54. package/dist/payee-contract.d.ts +9 -0
  55. package/dist/payee-contract.js +15 -0
  56. package/package.json +58 -0
  57. package/provenance.json +61 -0
  58. package/schemas/agentConnection.json +76 -0
  59. package/schemas/agentConnectionIssue.json +22 -0
  60. package/schemas/agentConnectionList.json +88 -0
  61. package/schemas/agentConnectionStarted.json +90 -0
  62. package/schemas/compensationSend.json +40 -0
  63. package/schemas/errorBody.json +16 -0
  64. package/schemas/fileResult.json +67 -0
  65. package/schemas/invoicesView.json +130 -0
  66. package/schemas/ownerCompensationView.json +556 -0
  67. package/schemas/payeeCompensationView.json +546 -0
  68. package/schemas/payeeWorkOrderRequest.json +31 -0
  69. package/schemas/publicWorkOrder.json +94 -0
  70. package/schemas/reportCommand.json +82 -0
  71. package/schemas/requestRecord.json +149 -0
  72. package/schemas/session.json +20 -0
  73. package/schemas/workOrderList.json +43 -0
  74. package/schemas/workOrderRequestResult.json +20 -0
  75. package/skills/app-server/SKILL.md +190 -0
  76. package/skills/getting-started/SKILL.md +90 -0
  77. package/skills/owner-workflows/SKILL.md +157 -0
  78. package/skills/payee-workflows/SKILL.md +200 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Owner-app capabilities (P2 manifest v1): what an owner frontend may declare in `contract.json`.
3
+ * A capability names existing routes so the gateway can refuse a release this platform cannot serve.
4
+ * It grants nothing: every call is still authorized per session, agent and procedure.
5
+ */
6
+ export declare const ownerCapabilities: {
7
+ readonly "gateway-session-v1": {
8
+ readonly routes: readonly ["/gw/challenge", "/gw/session", "/gw/logout", "/gw/me", "/gw/agents", "/a/<agent>/access"];
9
+ readonly procedures: readonly [];
10
+ };
11
+ readonly "agent-observe-v1": {
12
+ readonly routes: readonly [];
13
+ readonly procedures: readonly ["setup", "instance", "overview", "policyHistory", "activity", "unresolved", "held", "autopilot/get", "inbox/list", "inbox/dismiss"];
14
+ };
15
+ /** A safety control: stopping a running schedule stays reachable on every host that can run one. */
16
+ readonly "agent-schedule-stop-v1": {
17
+ readonly routes: readonly [];
18
+ readonly procedures: readonly ["autopilot/stop"];
19
+ };
20
+ readonly "agent-funding-v1": {
21
+ readonly routes: readonly [];
22
+ readonly procedures: readonly ["deposit"];
23
+ };
24
+ /** The owning DAO link, funding from it, shielding and sweep: any agent, not only Treasury strategy. */
25
+ readonly "owner-treasury-v1": {
26
+ readonly routes: readonly [];
27
+ readonly procedures: readonly ["treasury/get", "treasury/link", "treasury/unlink", "treasury/setConfidential", "treasury/fund", "sweep"];
28
+ };
29
+ readonly "owner-intents-v1": {
30
+ readonly routes: readonly ["/gw/proposals", "/gw/interventions"];
31
+ readonly procedures: readonly ["owner/prepare", "owner/submit", "owner/status", "owner/pending", "owner/actions"];
32
+ };
33
+ readonly "treasury-observe-v1": {
34
+ readonly routes: readonly [];
35
+ readonly procedures: readonly ["strategy/get", "autopilot/evidence", "autopilot/log", "ledger", "ledgerCsv", "reconcileBooks"];
36
+ };
37
+ readonly "treasury-strategy-v1": {
38
+ readonly routes: readonly [];
39
+ readonly procedures: readonly ["strategy/save", "autopilot/set", "autopilot/enable", "autopilot/tick"];
40
+ };
41
+ readonly "treasury-plans-v1": {
42
+ readonly routes: readonly [];
43
+ readonly procedures: readonly ["rebalance", "externalPlan", "executePlan", "steward/propose"];
44
+ };
45
+ readonly "treasury-recovery-v1": {
46
+ readonly routes: readonly [];
47
+ readonly procedures: readonly ["recover"];
48
+ };
49
+ readonly "payables-payments-v1": {
50
+ readonly routes: readonly [];
51
+ readonly procedures: readonly ["pay", "paymentHistory", "paymentStatus", "invoices", "cancelPreparedInvoice", "ledger", "ledgerCsv", "reconcileBooks"];
52
+ };
53
+ readonly "payables-payout-requests-v1": {
54
+ readonly routes: readonly [];
55
+ readonly procedures: readonly ["payouts/request", "payouts/status"];
56
+ };
57
+ readonly "payables-agreements-v1": {
58
+ readonly routes: readonly [];
59
+ readonly procedures: readonly ["agreements/list", "agreements/propose", "agreements/offer", "agreements/decline"];
60
+ };
61
+ readonly "payables-work-orders-v1": {
62
+ readonly routes: readonly [];
63
+ readonly procedures: readonly ["workOrders/list", "workOrders/get", "workOrders/status", "workOrders/create", "workOrders/prepare", "workOrders/submit", "workOrders/deposit", "workOrders/file"];
64
+ };
65
+ readonly "payables-compensation-v1": {
66
+ readonly routes: readonly [];
67
+ readonly procedures: readonly ["compensation/list", "compensation/agreement", "compensation/review", "compensation/prepare", "compensation/send"];
68
+ };
69
+ readonly "payables-recovery-v1": {
70
+ readonly routes: readonly [];
71
+ readonly procedures: readonly ["recover"];
72
+ };
73
+ /** Temporary: guide inference stays in the platform until its extraction (recorded migration gap). */
74
+ readonly "payables-guide-v1": {
75
+ readonly routes: readonly [];
76
+ readonly procedures: readonly ["demoGuide/compose"];
77
+ };
78
+ };
79
+ export type OwnerCapability = keyof typeof ownerCapabilities;
80
+ /** What a host provides, from the areas of the agents assigned to it. `showcase`-only hosts get the neutral set. */
81
+ export declare function providedCapabilities(areas: readonly ("treasury" | "payables")[]): OwnerCapability[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Owner-app capabilities (P2 manifest v1): what an owner frontend may declare in `contract.json`.
3
+ * A capability names existing routes so the gateway can refuse a release this platform cannot serve.
4
+ * It grants nothing: every call is still authorized per session, agent and procedure.
5
+ */
6
+ export const ownerCapabilities = {
7
+ "gateway-session-v1": { routes: ["/gw/challenge", "/gw/session", "/gw/logout", "/gw/me", "/gw/agents", "/a/<agent>/access"], procedures: [] },
8
+ "agent-observe-v1": { routes: [], procedures: ["setup", "instance", "overview", "policyHistory", "activity", "unresolved", "held", "autopilot/get", "inbox/list", "inbox/dismiss"] },
9
+ /** A safety control: stopping a running schedule stays reachable on every host that can run one. */
10
+ "agent-schedule-stop-v1": { routes: [], procedures: ["autopilot/stop"] },
11
+ "agent-funding-v1": { routes: [], procedures: ["deposit"] },
12
+ /** The owning DAO link, funding from it, shielding and sweep: any agent, not only Treasury strategy. */
13
+ "owner-treasury-v1": { routes: [], procedures: ["treasury/get", "treasury/link", "treasury/unlink", "treasury/setConfidential", "treasury/fund", "sweep"] },
14
+ "owner-intents-v1": { routes: ["/gw/proposals", "/gw/interventions"], procedures: ["owner/prepare", "owner/submit", "owner/status", "owner/pending", "owner/actions"] },
15
+ "treasury-observe-v1": { routes: [], procedures: ["strategy/get", "autopilot/evidence", "autopilot/log", "ledger", "ledgerCsv", "reconcileBooks"] },
16
+ "treasury-strategy-v1": { routes: [], procedures: ["strategy/save", "autopilot/set", "autopilot/enable", "autopilot/tick"] },
17
+ "treasury-plans-v1": { routes: [], procedures: ["rebalance", "externalPlan", "executePlan", "steward/propose"] },
18
+ "treasury-recovery-v1": { routes: [], procedures: ["recover"] },
19
+ "payables-payments-v1": { routes: [], procedures: ["pay", "paymentHistory", "paymentStatus", "invoices", "cancelPreparedInvoice", "ledger", "ledgerCsv", "reconcileBooks"] },
20
+ "payables-payout-requests-v1": { routes: [], procedures: ["payouts/request", "payouts/status"] },
21
+ "payables-agreements-v1": { routes: [], procedures: ["agreements/list", "agreements/propose", "agreements/offer", "agreements/decline"] },
22
+ "payables-work-orders-v1": { routes: [], procedures: ["workOrders/list", "workOrders/get", "workOrders/status", "workOrders/create", "workOrders/prepare", "workOrders/submit", "workOrders/deposit", "workOrders/file"] },
23
+ "payables-compensation-v1": { routes: [], procedures: ["compensation/list", "compensation/agreement", "compensation/review", "compensation/prepare", "compensation/send"] },
24
+ "payables-recovery-v1": { routes: [], procedures: ["recover"] },
25
+ /** Temporary: guide inference stays in the platform until its extraction (recorded migration gap). */
26
+ "payables-guide-v1": { routes: [], procedures: ["demoGuide/compose"] },
27
+ };
28
+ const neutral = ["gateway-session-v1", "agent-observe-v1", "agent-schedule-stop-v1", "agent-funding-v1", "owner-treasury-v1", "owner-intents-v1"];
29
+ /** What a host provides, from the areas of the agents assigned to it. `showcase`-only hosts get the neutral set. */
30
+ export function providedCapabilities(areas) {
31
+ const names = Object.keys(ownerCapabilities);
32
+ return [...neutral, ...names.filter(name => areas.some(area => name.startsWith(`${area}-`)))];
33
+ }
@@ -0,0 +1,99 @@
1
+ import type { z } from "zod";
2
+ import { ownerOutputs, ownerStreams } from "./owner-outputs.js";
3
+ import type { OwnerInputs } from "./owner-inputs.js";
4
+ import { type GatewayDenialScope } from "./gateway.js";
5
+ export type OwnerProcedure = keyof typeof ownerOutputs & keyof OwnerInputs;
6
+ type Stream = (typeof ownerStreams)[number];
7
+ export type OwnerOutput<P extends OwnerProcedure> = z.output<(typeof ownerOutputs)[P]>;
8
+ type Identity = Record<string, unknown>;
9
+ /** Supplementary provider facts on a `provider` failure (not retry flags or authorization decisions). */
10
+ export type RefusalDetails = {
11
+ availableAt?: string;
12
+ budgetRemainingUsd?: string;
13
+ };
14
+ /** Per-call options: an aborted read is `unavailable`; an aborted mutation or stream is `unknown` (it may have been accepted). */
15
+ export type OwnerCallOptions = {
16
+ signal?: AbortSignal;
17
+ };
18
+ /**
19
+ * Recovery is decided by `kind`, never by message text.
20
+ * - `session` (401) / `forbidden` (403): from the status alone, with the `source` that refused. A gateway denial adds the `scope` it declares (no
21
+ * session, no access to this agent, or no permission for this procedure); without one, the caller picks its default.
22
+ * - `unresolved` / `busy`: refused before anything was sent; follow existing operations, then try again.
23
+ * - `refused`: the platform refused before sending (with `code` when it names a reason).
24
+ * - `provider`: the provider refused or failed; check the recorded outcome before retrying.
25
+ * - `unavailable`: a read failed or returned an invalid answer; reading again is safe.
26
+ * - `unknown`: a mutation or stream may have taken effect. Keep `identity`, read the existing records, never resend.
27
+ * `refused` keeps the HTTP `status` and oRPC `rpcCode` of the encoded refusal. A plain answer that isn't an encoded
28
+ * oRPC error (a proxy page, or a 404 for a procedure this host lacks) adds its `httpStatus` to `unavailable` or
29
+ * `unknown`, for diagnosis only: nothing in it proves the handler didn't run, so it never becomes `refused`.
30
+ */
31
+ export type OwnerFailure = {
32
+ ok: false;
33
+ kind: "session";
34
+ status: 401;
35
+ error: string;
36
+ source?: DenialSource;
37
+ scope?: GatewayDenialScope;
38
+ } | {
39
+ ok: false;
40
+ kind: "forbidden";
41
+ status: 403;
42
+ error: string;
43
+ source?: DenialSource;
44
+ scope?: GatewayDenialScope;
45
+ } | {
46
+ ok: false;
47
+ kind: "unresolved" | "busy";
48
+ error: string;
49
+ } | {
50
+ ok: false;
51
+ kind: "refused";
52
+ error: string;
53
+ code?: string;
54
+ status?: number;
55
+ rpcCode?: string;
56
+ } | {
57
+ ok: false;
58
+ kind: "provider";
59
+ error: string;
60
+ code?: string;
61
+ details: RefusalDetails;
62
+ } | {
63
+ ok: false;
64
+ kind: "unavailable";
65
+ httpStatus?: number;
66
+ } | {
67
+ ok: false;
68
+ kind: "unknown";
69
+ procedure: OwnerProcedure;
70
+ identity: Identity;
71
+ httpStatus?: number;
72
+ };
73
+ /** Who refused: the gateway (a plain answer, before forwarding) or the agent server (an encoded oRPC error). */
74
+ export type DenialSource = "gateway" | "agent";
75
+ export type OwnerResult<T> = {
76
+ ok: true;
77
+ value: T;
78
+ } | OwnerFailure;
79
+ type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
80
+ /**
81
+ * Where owner procedures live: `""` or `/a/<agent>` on the page's own host (the gateway session cookie goes same-origin),
82
+ * or an absolute URL for a caller outside a browser, such as Treasury's scoped machine route
83
+ * `https://<host>/machine/v1/a/<agent>`. The client adds no credential: a non-browser caller's `fetch` adds the one its
84
+ * route accepts. A Treasury machine credential is accepted only on that machine route, never on `/a/<agent>/rpc`, and
85
+ * there is no Payables or DAO machine delegation.
86
+ */
87
+ export type OwnerBase = "" | `/${string}` | `https://${string}` | `http://${string}`;
88
+ /** Typed owner procedures under an agent base (see OwnerBase), same wire as the app's oRPC client. Never retries. */
89
+ export declare function ownerClient({ base, fetch: send }?: {
90
+ base?: OwnerBase;
91
+ fetch?: Fetch;
92
+ }): {
93
+ /** One read or non-stream mutation; the answer is validated against the published output schema. */
94
+ call<P extends Exclude<OwnerProcedure, Stream>>(path: P, ...[input, options]: undefined extends OwnerInputs[P] ? [input?: OwnerInputs[P], options?: OwnerCallOptions] : [input: OwnerInputs[P], options?: OwnerCallOptions]): Promise<OwnerResult<OwnerOutput<P>>>;
95
+ /** A stream's events, each validated; a lost stream or invalid event ends it as `unknown` with the input identity. */
96
+ stream<P extends Stream>(path: P, input: OwnerInputs[P], options?: OwnerCallOptions): AsyncGenerator<OwnerOutput<P>, OwnerFailure | undefined>;
97
+ };
98
+ export type OwnerClient = ReturnType<typeof ownerClient>;
99
+ export {};
@@ -0,0 +1,146 @@
1
+ import { createORPCClient, ORPCError, RPCLink } from "./orpc-client.js";
2
+ import { ownerOutputs } from "./owner-outputs.js";
3
+ import { gatewayDenialScope } from "./gateway.js";
4
+ import { READS } from "./permissions.js";
5
+ /** oRPC codes that mean the server refused before acting. TIMEOUT and CLIENT_CLOSED_REQUEST are not: the outcome is unknown. */
6
+ const REFUSALS = new Set(["BAD_REQUEST", "NOT_FOUND", "METHOD_NOT_SUPPORTED", "NOT_ACCEPTABLE", "CONFLICT", "PRECONDITION_FAILED", "PAYLOAD_TOO_LARGE", "UNSUPPORTED_MEDIA_TYPE", "UNPROCESSABLE_CONTENT", "TOO_MANY_REQUESTS"]);
7
+ /**
8
+ * An encoded oRPC error from any copy of `@orpc/client`. A bundler can load the link's copy apart from this module's
9
+ * (Module Federation shares `@orpc/client` but not `@orpc/client/fetch`; seen in the dashboard's built API), and
10
+ * `instanceof ORPCError` then fails across them, turning a proven refusal into `unknown`. Every version names its error
11
+ * `ORPCError` and carries a string `code`.
12
+ */
13
+ const isRpcError = (error) => error instanceof ORPCError || (error instanceof Error && error.name === "ORPCError" && typeof error.code === "string");
14
+ /** A non-2xx answer that isn't an encoded oRPC error. */
15
+ class PlainAnswer extends Error {
16
+ status;
17
+ constructor(status) {
18
+ super(`HTTP ${status}`);
19
+ this.status = status;
20
+ }
21
+ }
22
+ class GatewayDenial extends Error {
23
+ status;
24
+ scope;
25
+ constructor(status, message, scope) {
26
+ super(message);
27
+ this.status = status;
28
+ this.scope = scope;
29
+ }
30
+ }
31
+ /** Typed owner procedures under an agent base (see OwnerBase), same wire as the app's oRPC client. Never retries. */
32
+ export function ownerClient({ base = "", fetch: send = globalThis.fetch } = {}) {
33
+ /** One call's client: it records that call's HTTP status, which oRPC's errors don't carry. */
34
+ const procedure = (path, answered) => {
35
+ // oRPC's URL type omits `http://`; the link only concatenates it and hands it to `fetch` below.
36
+ const client = createORPCClient(new RPCLink({ url: `${base}/rpc`, fetch: (url, init) => answer(url, init, answered) }));
37
+ return path.split("/").reduce((node, key) => node[key], client);
38
+ };
39
+ async function answer(url, init, answered) {
40
+ // Same origin in a browser; outside one, relative paths resolve against localhost (tests and tools pass their own fetch).
41
+ const response = await send(new URL(url, globalThis.location?.href ?? "http://localhost"), init);
42
+ answered(response.status);
43
+ if (response.ok)
44
+ return response;
45
+ // Gateway denials are plain JSON, not encoded RPC errors; the status alone decides. Any other plain answer keeps
46
+ // only its status: it is classified as lost below.
47
+ const body = await response.clone().json().catch(() => null);
48
+ if (typeof body?.json?.code === "string")
49
+ return response;
50
+ if (response.status === 401 || response.status === 403)
51
+ throw new GatewayDenial(response.status, typeof body?.error === "string" ? body.error : response.status === 401 ? "Sign in again." : "This account may not do that.", gatewayDenialScope(body));
52
+ throw new PlainAnswer(response.status);
53
+ }
54
+ function failure(error, path, identity, status) {
55
+ const mutation = !READS.has(path);
56
+ const lost = mutation ? { ok: false, kind: "unknown", procedure: path, identity } : { ok: false, kind: "unavailable" };
57
+ if (error instanceof GatewayDenial) {
58
+ const scope = error.scope && { scope: error.scope };
59
+ return error.status === 401 ? { ok: false, kind: "session", status: 401, error: error.message, source: "gateway", ...scope }
60
+ : { ok: false, kind: "forbidden", status: 403, error: error.message, source: "gateway", ...scope };
61
+ }
62
+ if (error instanceof PlainAnswer)
63
+ return error.status > 0 ? { ...lost, httpStatus: error.status } : lost;
64
+ if (!isRpcError(error))
65
+ return lost;
66
+ if (error.code === "UNAUTHORIZED")
67
+ return { ok: false, kind: "session", status: 401, error: error.message, source: "agent" };
68
+ if (error.code === "FORBIDDEN")
69
+ return { ok: false, kind: "forbidden", status: 403, error: error.message, source: "agent" };
70
+ const data = (error.data ?? {});
71
+ const code = typeof data.code === "string" ? data.code : undefined;
72
+ if (data.kind === "unresolved" || data.kind === "busy")
73
+ return { ok: false, kind: data.kind, error: error.message };
74
+ const refusal = { ...(status !== undefined && { status }), rpcCode: error.code };
75
+ if (data.kind === "refused")
76
+ return { ok: false, kind: "refused", error: error.message, ...(code && { code }), ...refusal };
77
+ if (data.kind === "provider")
78
+ return { ok: false, kind: "provider", error: error.message, ...(code && { code }), details: {
79
+ ...(typeof data.availableAt === "string" && { availableAt: data.availableAt }),
80
+ ...(typeof data.budgetRemainingUsd === "string" && { budgetRemainingUsd: data.budgetRemainingUsd })
81
+ } };
82
+ // Untyped refusal codes come before the handler acted (input validation, not found); anything else may follow an effect.
83
+ return REFUSALS.has(error.code) ? { ok: false, kind: "refused", error: error.message, ...(code && { code }), ...refusal } : lost;
84
+ }
85
+ return {
86
+ /** One read or non-stream mutation; the answer is validated against the published output schema. */
87
+ async call(path, ...[input, options]) {
88
+ const identity = (input ?? {});
89
+ let output;
90
+ let status;
91
+ try {
92
+ output = await procedure(path, answered => { status = answered; })(input, { signal: options?.signal });
93
+ }
94
+ catch (error) {
95
+ return failure(error, path, identity, status);
96
+ }
97
+ const parsed = ownerOutputs[path].safeParse(output);
98
+ if (parsed.success)
99
+ return { ok: true, value: parsed.data };
100
+ return READS.has(path) ? { ok: false, kind: "unavailable" } : { ok: false, kind: "unknown", procedure: path, identity };
101
+ },
102
+ /** A stream's events, each validated; a lost stream or invalid event ends it as `unknown` with the input identity. */
103
+ async *stream(path, input, options) {
104
+ const identity = (input ?? {});
105
+ let events;
106
+ let status;
107
+ try {
108
+ events = await procedure(path, answered => { status = answered; })(input, { signal: options?.signal });
109
+ }
110
+ catch (error) {
111
+ return failure(error, path, identity, status);
112
+ }
113
+ let started = false;
114
+ const iterator = events[Symbol.asyncIterator]();
115
+ // An open stream must end on abort even if the transport keeps it open: race each read with the signal.
116
+ const aborted = new Promise(resolve => {
117
+ if (options?.signal?.aborted)
118
+ resolve("aborted");
119
+ options?.signal?.addEventListener("abort", () => resolve("aborted"), { once: true });
120
+ });
121
+ try {
122
+ for (;;) {
123
+ const next = await Promise.race([iterator.next(), aborted]);
124
+ if (next === "aborted") {
125
+ void iterator.return?.();
126
+ return { ok: false, kind: "unknown", procedure: path, identity };
127
+ }
128
+ if (next.done)
129
+ break;
130
+ started = true;
131
+ const parsed = ownerOutputs[path].safeParse(next.value);
132
+ if (!parsed.success) {
133
+ void iterator.return?.();
134
+ return { ok: false, kind: "unknown", procedure: path, identity };
135
+ }
136
+ yield parsed.data;
137
+ }
138
+ }
139
+ catch (error) {
140
+ // After any event, part of the stream may have taken effect: only a failure before the first event keeps its kind.
141
+ return started ? { ok: false, kind: "unknown", procedure: path, identity } : failure(error, path, identity, status);
142
+ }
143
+ return undefined;
144
+ },
145
+ };
146
+ }