@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 +105 -0
- package/README.md +238 -0
- package/dist/adapter.d.ts +99 -0
- package/dist/adapter.js +29 -0
- package/dist/author.d.ts +40 -0
- package/dist/author.js +244 -0
- package/dist/authoring-schema.d.ts +17 -0
- package/dist/authoring-schema.js +53 -0
- package/dist/authorizer.d.ts +25 -0
- package/dist/authorizer.js +29 -0
- package/dist/catalog.d.ts +30 -0
- package/dist/catalog.js +22 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +36 -0
- package/dist/contract.d.ts +126 -0
- package/dist/contract.js +14 -0
- package/dist/emit-tool.d.ts +16 -0
- package/dist/emit-tool.js +48 -0
- package/dist/guardrail.d.ts +27 -0
- package/dist/guardrail.js +18 -0
- package/dist/guards.d.ts +3 -0
- package/dist/guards.js +5 -0
- package/dist/hooks.d.ts +42 -0
- package/dist/hooks.js +28 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +45 -0
- package/dist/mock-adapter.d.ts +23 -0
- package/dist/mock-adapter.js +48 -0
- package/dist/openai-adapter.d.ts +62 -0
- package/dist/openai-adapter.js +176 -0
- package/dist/prompt.d.ts +47 -0
- package/dist/prompt.js +112 -0
- package/dist/ssrf.d.ts +33 -0
- package/dist/ssrf.js +116 -0
- package/dist/toolset.d.ts +57 -0
- package/dist/toolset.js +76 -0
- package/dist/validator.d.ts +54 -0
- package/dist/validator.js +196 -0
- package/package.json +66 -0
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
|
+
}
|
package/dist/adapter.js
ADDED
|
@@ -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 {};
|
package/dist/author.d.ts
ADDED
|
@@ -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>;
|