@flowget/ai 0.1.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/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Flowget
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # @flowget/ai
2
+
3
+ **Request-first, stateless AI workflow-authoring primitives** for [Flowget](https://flowget.io).
4
+
5
+ Send a natural-language command → get a schema-valid workflow graph back. `author(config, request)` owns the catalog, the LLM, toolsets, guardrail, authorizers, and validation. This package ships **primitives, not an HTTP server** — build a config with `resolveConfig` and call `author`, then wrap it in whatever transport you need (a customer who wants an API writes it themselves).
6
+
7
+ **Stateless by design.** The workflow graph IS the state, so there is no session store — every request is self-contained. Optional continuity is carried by the _client_ via `context`. A chat is a thin, optional layer on the same primitive.
8
+
9
+ ```
10
+ command + currentGraph? + actor? → { kind: "proposal", graph, summary } | { kind: "message", text }
11
+ ```
12
+
13
+ - `proposal` — a workflow graph to preview / apply. Node positions are optional (the builder lays it out).
14
+ - `message` — a text reply, including a **clarifying question** when the command is ambiguous.
15
+
16
+ > ⚠️ **A proposal is model-authored — untrusted until approved.** Validation gates *runnability* (schema, known node types, resolvable refs), NOT field *values*: a malicious or mistaken value (e.g. an exfiltrating URL in an `http_request` node) passes. Never auto-apply a proposal without human review and/or a downstream field-value policy check. Treat `summary` (and any `message.text`) as untrusted model output — output-encode before rendering.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @flowget/ai
22
+ # or: pnpm add @flowget/ai · yarn add @flowget/ai · bun add @flowget/ai
23
+ ```
24
+
25
+ Developed with Bun internally; consumers can use any package manager and any Node-compatible runtime.
26
+
27
+ ## Quickstart
28
+
29
+ ```ts
30
+ import { resolveConfig, author, openaiAdapter } from "@flowget/ai";
31
+
32
+ const config = await resolveConfig({
33
+ adapter: openaiAdapter({ apiKey: process.env.OPENROUTER_API_KEY! }),
34
+ catalog: { registryDir: process.env.REGISTRY_DIR! }, // or a pre-loaded NodeMetadata[]
35
+ });
36
+
37
+ const event = await author(config, {
38
+ command: "When a webhook fires, email ops with the order payload",
39
+ });
40
+
41
+ if (event.kind === "proposal") {
42
+ console.log(event.summary, event.graph); // a workflow graph authored from YOUR registry
43
+ } else {
44
+ console.log(event.text); // a clarifying question or a plain reply
45
+ }
46
+ ```
47
+
48
+ A `proposal` graph looks like:
49
+
50
+ ```json
51
+ {
52
+ "kind": "proposal",
53
+ "summary": "Emails ops whenever the order webhook fires.",
54
+ "graph": {
55
+ "nodes": [
56
+ { "id": "trigger_1", "type": "webhook_trigger", "data": { "path": "/orders" } },
57
+ { "id": "email_1", "type": "send_email",
58
+ "data": { "to": "ops@example.com", "subject": "New order",
59
+ "body": "{{ steps.trigger_1.output.payload }}" } }
60
+ ],
61
+ "edges": [{ "id": "e1", "source": "trigger_1", "target": "email_1" }]
62
+ }
63
+ }
64
+ ```
65
+
66
+ Point `catalog` at your own directory of node `.json` files (`{ registryDir }`) to author against your real catalog. `resolveConfig` loads the catalog; `finalizeConfig` is the sync variant when you already hold a `NodeMetadata[]`.
67
+
68
+ ## Configuration
69
+
70
+ Every piece is a named, typed, swappable hook with a batteries-included default. Only `adapter` and `catalog` are required.
71
+
72
+ | Option | Default | What it does |
73
+ | ------------------- | -------------------------------------- | ----------------------------------------------------------------------- |
74
+ | `adapter` | — (required) | The LLM. `openaiAdapter({ apiKey })` → OpenRouter, or BYO. |
75
+ | `catalog` | — (required) | `NodeMetadata[]`, `{ registryDir }`, or `{ paths }`. |
76
+ | `guardrail` | none | Pre-LLM policy gate → `allow` / `block(message)`. |
77
+ | `authorizers` | `[]` | Verify the trusted actor before guardrail/toolsets. |
78
+ | `toolsets` | `[]` | Actor-scoped, model-invoked tools. |
79
+ | `hooks` | no-op | `onProposal` / `onBlocked` audit sinks. |
80
+ | `validator` | AJV + catalog cross-ref + template-ref | Bounds what can become a proposal. |
81
+ | `persona` | workflow-only persona | System-prompt override. |
82
+ | `model` | adapter default | Model slug forwarded to the adapter. |
83
+ | `maxToolIterations` | `8` | Max adapter turns per request. |
84
+ | `maxRepairAttempts` | `2` | Max validate/repair retries before giving up with a `message`. |
85
+
86
+ ### BYO LLM adapter
87
+
88
+ The adapter is the only LLM-specific seam — one method:
89
+
90
+ ```ts
91
+ import type { AiAdapter } from "@flowget/ai";
92
+
93
+ const myAdapter: AiAdapter = {
94
+ async generate({ messages, tools }) {
95
+ // Call any LLM/gateway with the messages + tool definitions.
96
+ // Return the assistant's turn: free text and/or parsed tool calls.
97
+ return { text: null, toolCalls: [] };
98
+ },
99
+ };
100
+ ```
101
+
102
+ ### Actor-scoped toolset
103
+
104
+ Model-invoked tools run against your data, scoped by the `actor`. A domain miss is a **return value**; an infra fault **throws**. See [`examples/db-toolset.ts`](https://github.com/flowgethq/ai/blob/main/examples/db-toolset.ts) (in the repo — the `examples/` dir is not in the published tarball):
105
+
106
+ ```ts
107
+ import type { Toolset } from "@flowget/ai";
108
+
109
+ const dbToolset: Toolset = {
110
+ namespace: "db",
111
+ tools: [
112
+ {
113
+ name: "find_user",
114
+ description: "Look up a user by email within the caller's tenant.",
115
+ parameters: { type: "object", required: ["email"],
116
+ properties: { email: { type: "string" } } },
117
+ run: async (input, { actor }) => {
118
+ const user = await db.findUser({ email: String(input.email), tenantId: actor?.tenantId });
119
+ return user ? { found: true, user } : { found: false };
120
+ },
121
+ },
122
+ ],
123
+ };
124
+ ```
125
+
126
+ > ⚠️ **`actor` is client-supplied JSON — not trusted by default.** A toolset that scopes data by `actor` (like `tenantId` above) is only safe when an **`authorizer` verifies the actor first**; otherwise a client can spoof `tenantId` and read another tenant's data. Always wire an authorizer alongside an actor-scoped toolset:
127
+ >
128
+ > ```ts
129
+ > await resolveConfig({ adapter, catalog, authorizers: [verifyActor], toolsets: [dbToolset] });
130
+ > ```
131
+ >
132
+ > The package is auth-agnostic — it never forces authentication — so enforcing this is your call. See `requireVerifiedTenant` in `examples/db-toolset.ts`.
133
+
134
+ ### Guardrail + audit hooks
135
+
136
+ > ⚠️ **Bounding request inputs is a hard integration requirement.** `command`, `currentGraph`, **and** `context` are all `JSON.stringify`'d into the prompt, and `currentGraph` / `context` can be re-sent up to `maxToolIterations` (default 8) times per request. The host MUST cap the size of **all three** before calling `author` — an unbounded `currentGraph` or `context` is as much a token-DoS as a huge `command`. A guardrail is the natural place:
137
+
138
+ ```ts
139
+ import { resolveConfig, author, block, inMemoryAuditSink } from "@flowget/ai";
140
+
141
+ const audit = inMemoryAuditSink(); // default append-only sink
142
+
143
+ // Cap EVERY prompt input, not just the command.
144
+ const tooBig = (value: unknown, max: number) =>
145
+ value !== undefined && JSON.stringify(value).length > max;
146
+
147
+ const config = await resolveConfig({
148
+ adapter, catalog,
149
+ guardrail: (req) =>
150
+ req.command.length > 2_000 ||
151
+ tooBig(req.currentGraph, 50_000) ||
152
+ tooBig(req.context, 10_000)
153
+ ? block("Request too large.")
154
+ : { action: "allow" },
155
+ hooks: audit, // audit.entries collects every proposal / block
156
+ });
157
+ const event = await author(config, { command });
158
+ ```
159
+
160
+ ## Testing with a mock adapter (no LLM, no network)
161
+
162
+ Drive the `author` primitive with a deterministic mock and assert the `message | proposal`:
163
+
164
+ ```ts
165
+ import { resolveConfig, author, mockAdapter, mockProposeWorkflow } from "@flowget/ai";
166
+
167
+ const config = await resolveConfig({
168
+ catalog,
169
+ adapter: mockAdapter([
170
+ mockProposeWorkflow(
171
+ { nodes: [{ id: "t1", type: "webhook_trigger", data: {} }], edges: [] },
172
+ "A one-node workflow.",
173
+ ),
174
+ ]),
175
+ });
176
+
177
+ const event = await author(config, { command: "start on a webhook" });
178
+ // event: { kind: "proposal", graph, summary }
179
+ ```
180
+
181
+ ## Wrapping it in a transport
182
+
183
+ `author` is transport-agnostic — the graph is the state, so a request is self-contained. Wrap it in whatever you already run (an HTTP route, a queue consumer, a CLI). A minimal Fetch handler:
184
+
185
+ ```ts
186
+ import { resolveConfig, author, openaiAdapter } from "@flowget/ai";
187
+
188
+ const config = await resolveConfig({
189
+ adapter: openaiAdapter({ apiKey: process.env.OPENROUTER_API_KEY! }),
190
+ catalog: { registryDir: process.env.REGISTRY_DIR! },
191
+ });
192
+
193
+ export default {
194
+ async fetch(req: Request): Promise<Response> {
195
+ const { command, currentGraph, actor, context } = await req.json();
196
+ const event = await author(config, { command, currentGraph, actor, context });
197
+ return Response.json(event);
198
+ },
199
+ };
200
+ ```
201
+
202
+ Self-hosted or Flowget-hosted, the primitive is the same stateless call — any infra, any runtime, any transport.
203
+
204
+ ### Streaming (`authorStream`)
205
+
206
+ For a chat "typing" feel, `authorStream` yields `text-delta`s as the model writes, then exactly one terminal `proposal` / `message` / `error`. Discriminate on `event.kind`. Frame the events over whatever streaming transport you run — **example wiring you build; the library pins no wire format.** A minimal SSE handler:
207
+
208
+ ```ts
209
+ import { resolveConfig, authorStream, openaiAdapter } from "@flowget/ai";
210
+
211
+ const config = await resolveConfig({
212
+ adapter: openaiAdapter({ apiKey: process.env.OPENROUTER_API_KEY! }),
213
+ catalog: { registryDir: process.env.REGISTRY_DIR! },
214
+ });
215
+
216
+ export default {
217
+ async fetch(req: Request): Promise<Response> {
218
+ const { command, currentGraph, actor, context } = await req.json();
219
+ const encoder = new TextEncoder();
220
+ const stream = new ReadableStream({
221
+ async start(controller) {
222
+ for await (const event of authorStream(config, { command, currentGraph, actor, context })) {
223
+ // event.kind: "text-delta" | "proposal" | "message" | "error"
224
+ controller.enqueue(encoder.encode(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`));
225
+ }
226
+ controller.close();
227
+ },
228
+ });
229
+ return new Response(stream, { headers: { "content-type": "text/event-stream" } });
230
+ },
231
+ };
232
+ ```
233
+
234
+ An adapter without `generateStream` still works — `authorStream` emits no deltas, just the terminal event. A mid-stream failure arrives as a terminal `{ kind: "error", error }` rather than a throw, so your `case "error"` is reachable.
235
+
236
+ ## License
237
+
238
+ [FSL-1.1-ALv2](./LICENSE).
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The LLM seam. Everything model-specific lives behind `AiAdapter`;
3
+ * the rest of the library is provider-agnostic.
4
+ *
5
+ * The adapter is a thin transport: given a running conversation plus a
6
+ * set of callable tool definitions, produce the assistant's next turn
7
+ * (free text and/or tool calls). `author` owns prompt assembly, the
8
+ * tool-calling loop, validation, and repair — the adapter owns only
9
+ * "talk to the model."
10
+ *
11
+ * **BYO is one method.** Implement `generate` against any LLM or
12
+ * gateway and pass it as `config.adapter`. The default `openaiAdapter`
13
+ * maps this onto the `openai` SDK pointed at OpenRouter. The adapter is
14
+ * server-side config, held in closure, and is never serialized.
15
+ *
16
+ * **Two seams, one required.** `generate` (required) returns a full
17
+ * turn. `generateStream` (optional) emits token deltas for the chat
18
+ * "typing" feel: `author` consumes `generate`; `authorStream` consumes
19
+ * `generateStream` when present and transparently falls back to
20
+ * `generate` otherwise. Both feed the SAME validate/repair/tool logic.
21
+ *
22
+ * **Security.** An adapter MUST NOT leak credentials into thrown errors
23
+ * or returned text — scrub provider errors before rethrowing so the
24
+ * `apiKey` never reaches a caller-visible message. And treat the
25
+ * assistant's `text` and a proposal's `summary` as UNTRUSTED model
26
+ * output: a caller MUST output-encode them before rendering (they can
27
+ * carry markup or prompt-injection payloads).
28
+ */
29
+ /** A tool the model may call, described by a JSON-Schema parameter shape. */
30
+ export type AiToolDefinition = {
31
+ name: string;
32
+ description: string;
33
+ /** JSON Schema for the tool's arguments object. */
34
+ parameters: Record<string, unknown>;
35
+ };
36
+ /** A tool invocation the model requested, with already-parsed arguments. */
37
+ export type AiToolCall = {
38
+ /** Provider-assigned call id — echoed back on the matching tool result. */
39
+ id: string;
40
+ name: string;
41
+ /** Parsed JSON arguments. Adapters parse the raw string before returning. */
42
+ arguments: Record<string, unknown>;
43
+ };
44
+ /** One message in the conversation handed to the adapter. */
45
+ export type AiMessage = {
46
+ role: "system";
47
+ content: string;
48
+ } | {
49
+ role: "user";
50
+ content: string;
51
+ } | {
52
+ role: "assistant";
53
+ content: string | null;
54
+ toolCalls?: AiToolCall[];
55
+ } | {
56
+ role: "tool";
57
+ toolCallId: string;
58
+ content: string;
59
+ };
60
+ export type AiGenerateRequest = {
61
+ messages: AiMessage[];
62
+ /** Tools the model may call this turn. May be empty. */
63
+ tools: AiToolDefinition[];
64
+ /** Optional per-call model override; adapters fall back to their own default. */
65
+ model?: string;
66
+ /** Cooperative cancellation for the underlying HTTP call. */
67
+ signal?: AbortSignal;
68
+ };
69
+ export type AiGenerateResult = {
70
+ /** Assistant free-text for this turn, or `null` when it only called tools. */
71
+ text: string | null;
72
+ /** Tool calls the model requested this turn. Empty when it replied with text. */
73
+ toolCalls: AiToolCall[];
74
+ };
75
+ /**
76
+ * A chunk of a streamed adapter turn. Text arrives incrementally as
77
+ * `text-delta`s; the turn ends with exactly one `result` carrying the full
78
+ * text plus the resolved tool calls (args accumulated across chunks). This is
79
+ * the forward-compatible streaming shape referenced in this file's header —
80
+ * BYO adapters opt in by implementing {@link AiAdapter.generateStream}; the
81
+ * ones that don't keep working through `generate`.
82
+ */
83
+ export type AiStreamPart = {
84
+ type: "text-delta";
85
+ delta: string;
86
+ } | {
87
+ type: "result";
88
+ result: AiGenerateResult;
89
+ };
90
+ export interface AiAdapter {
91
+ generate(request: AiGenerateRequest): Promise<AiGenerateResult>;
92
+ /**
93
+ * Optional token-level streaming. When present, `authorStream` forwards the
94
+ * `text-delta`s to the caller (chat "typing" feel) and uses the terminal
95
+ * `result` to run the same validate/repair/tool logic as `generate`. Absent
96
+ * ⇒ `authorStream` transparently falls back to `generate` (no deltas).
97
+ */
98
+ generateStream?(request: AiGenerateRequest): AsyncIterable<AiStreamPart>;
99
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The LLM seam. Everything model-specific lives behind `AiAdapter`;
3
+ * the rest of the library is provider-agnostic.
4
+ *
5
+ * The adapter is a thin transport: given a running conversation plus a
6
+ * set of callable tool definitions, produce the assistant's next turn
7
+ * (free text and/or tool calls). `author` owns prompt assembly, the
8
+ * tool-calling loop, validation, and repair — the adapter owns only
9
+ * "talk to the model."
10
+ *
11
+ * **BYO is one method.** Implement `generate` against any LLM or
12
+ * gateway and pass it as `config.adapter`. The default `openaiAdapter`
13
+ * maps this onto the `openai` SDK pointed at OpenRouter. The adapter is
14
+ * server-side config, held in closure, and is never serialized.
15
+ *
16
+ * **Two seams, one required.** `generate` (required) returns a full
17
+ * turn. `generateStream` (optional) emits token deltas for the chat
18
+ * "typing" feel: `author` consumes `generate`; `authorStream` consumes
19
+ * `generateStream` when present and transparently falls back to
20
+ * `generate` otherwise. Both feed the SAME validate/repair/tool logic.
21
+ *
22
+ * **Security.** An adapter MUST NOT leak credentials into thrown errors
23
+ * or returned text — scrub provider errors before rethrowing so the
24
+ * `apiKey` never reaches a caller-visible message. And treat the
25
+ * assistant's `text` and a proposal's `summary` as UNTRUSTED model
26
+ * output: a caller MUST output-encode them before rendering (they can
27
+ * carry markup or prompt-injection payloads).
28
+ */
29
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The command → proposal core.
3
+ *
4
+ * Orchestration (stateless — nothing survives the call):
5
+ * 1. authorizers verify the trusted actor (throw ⇒ request rejected);
6
+ * 2. the guardrail runs pre-LLM (block ⇒ a `message`, no adapter call);
7
+ * 3. the tool-calling loop assembles the prompt, calls the adapter,
8
+ * dispatches actor-scoped toolset calls, and validates/repairs the
9
+ * emitted graph until it is valid — or the model asks a clarifying
10
+ * question, or the bounds are hit;
11
+ * 4. audit hooks fire on the outcome.
12
+ *
13
+ * A `proposal` is only ever returned once the graph passes the
14
+ * validator (schema + catalog + structural + trigger-presence), so only
15
+ * runnable-shaped graphs reach the caller.
16
+ *
17
+ * {@link author} and {@link authorStream} share ONE per-turn decision
18
+ * ({@link stepTurn}); the only difference is how each fetches a turn —
19
+ * `adapter.generate` (buffered) vs {@link streamOneTurn} (token deltas).
20
+ */
21
+ import type { ResolvedConfig } from "./config.js";
22
+ import type { AuthorEvent, AuthorRequest, AuthorStreamEvent } from "./contract.js";
23
+ export type AuthorOptions = {
24
+ signal?: AbortSignal;
25
+ };
26
+ export declare function author(config: ResolvedConfig, request: AuthorRequest, options?: AuthorOptions): Promise<AuthorEvent>;
27
+ /**
28
+ * Streaming sibling of {@link author}: identical orchestration, but the
29
+ * assistant's text is forwarded as `text-delta`s as it is generated (the chat
30
+ * "typing" feel), ending in exactly one terminal `proposal`, `message`, or
31
+ * `error`.
32
+ *
33
+ * Adapters that don't implement `generateStream` degrade cleanly — no deltas,
34
+ * just the terminal event. A failure DURING the stream is yielded as a
35
+ * terminal `{ kind: "error" }` (per {@link AuthorStreamEvent}) rather than
36
+ * thrown, so a consumer's `case "error"` is reachable. Pre-stream rejections
37
+ * (a throwing authorizer / guardrail) still throw, mirroring {@link author}
38
+ * so a transport can reject before opening the stream.
39
+ */
40
+ export declare function authorStream(config: ResolvedConfig, request: AuthorRequest, options?: AuthorOptions): AsyncGenerator<AuthorStreamEvent>;