@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/dist/author.js ADDED
@@ -0,0 +1,244 @@
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 { runAuthorizers } from "./authorizer.js";
22
+ import { buildEmitTool, EMIT_TOOL_NAME } from "./emit-tool.js";
23
+ import { buildInitialMessages } from "./prompt.js";
24
+ /** Fallback text for a degenerate empty turn (no tool call, no text). */
25
+ const EMPTY_TURN_MESSAGE = "I didn't produce a response. Please rephrase or add detail.";
26
+ /** Given up after exhausting the per-request adapter-turn budget. */
27
+ const MAX_STEPS_MESSAGE = "I could not complete the request within the allowed number of steps. Please try a more specific command.";
28
+ export async function author(config, request, options = {}) {
29
+ const actor = request.actor;
30
+ // 1. Authorizers (throw ⇒ AuthorizationError bubbles to the handler).
31
+ await runAuthorizers(config.authorizers, actor, request);
32
+ // 2. Guardrail (pre-LLM). A block short-circuits with no token spend.
33
+ if (config.guardrail !== undefined) {
34
+ const decision = await config.guardrail(request, actor ? { actor } : {});
35
+ if (decision.action === "block") {
36
+ await config.hooks.onBlocked?.({
37
+ type: "blocked",
38
+ command: request.command,
39
+ ...(actor ? { actor } : {}),
40
+ reason: decision.message,
41
+ });
42
+ return { kind: "message", text: decision.message };
43
+ }
44
+ }
45
+ // 3. The tool-calling loop.
46
+ const event = await runToolLoop(config, request, options);
47
+ // 4. Audit the proposal.
48
+ if (event.kind === "proposal") {
49
+ await config.hooks.onProposal?.({
50
+ type: "proposal",
51
+ command: request.command,
52
+ ...(actor ? { actor } : {}),
53
+ summary: event.summary,
54
+ graph: event.graph,
55
+ });
56
+ }
57
+ return event;
58
+ }
59
+ /**
60
+ * Streaming sibling of {@link author}: identical orchestration, but the
61
+ * assistant's text is forwarded as `text-delta`s as it is generated (the chat
62
+ * "typing" feel), ending in exactly one terminal `proposal`, `message`, or
63
+ * `error`.
64
+ *
65
+ * Adapters that don't implement `generateStream` degrade cleanly — no deltas,
66
+ * just the terminal event. A failure DURING the stream is yielded as a
67
+ * terminal `{ kind: "error" }` (per {@link AuthorStreamEvent}) rather than
68
+ * thrown, so a consumer's `case "error"` is reachable. Pre-stream rejections
69
+ * (a throwing authorizer / guardrail) still throw, mirroring {@link author}
70
+ * so a transport can reject before opening the stream.
71
+ */
72
+ export async function* authorStream(config, request, options = {}) {
73
+ const actor = request.actor;
74
+ // 1. Authorizers (throw ⇒ bubbles to the handler — pre-stream).
75
+ await runAuthorizers(config.authorizers, actor, request);
76
+ // 2. Guardrail (pre-LLM). A block short-circuits with no token spend.
77
+ if (config.guardrail !== undefined) {
78
+ const decision = await config.guardrail(request, actor ? { actor } : {});
79
+ if (decision.action === "block") {
80
+ await config.hooks.onBlocked?.({
81
+ type: "blocked",
82
+ command: request.command,
83
+ ...(actor ? { actor } : {}),
84
+ reason: decision.message,
85
+ });
86
+ yield { kind: "message", text: decision.message };
87
+ return;
88
+ }
89
+ }
90
+ // 3. The streaming tool loop — deltas out, terminal event returned.
91
+ // Wrapped so a mid-stream failure becomes a terminal `error` event.
92
+ try {
93
+ const event = yield* runToolLoopStream(config, request, options);
94
+ // 4. Audit + emit the terminal event.
95
+ if (event.kind === "proposal") {
96
+ await config.hooks.onProposal?.({
97
+ type: "proposal",
98
+ command: request.command,
99
+ ...(actor ? { actor } : {}),
100
+ summary: event.summary,
101
+ graph: event.graph,
102
+ });
103
+ yield { kind: "proposal", graph: event.graph, summary: event.summary };
104
+ }
105
+ else {
106
+ yield { kind: "message", text: event.text };
107
+ }
108
+ }
109
+ catch (error) {
110
+ yield {
111
+ kind: "error",
112
+ error: error instanceof Error ? error.message : String(error),
113
+ };
114
+ }
115
+ }
116
+ function createTurnState(config, request) {
117
+ return {
118
+ tools: [buildEmitTool(), ...config.toolRegistry.definitions()],
119
+ messages: buildInitialMessages(request, {
120
+ catalog: config.catalog,
121
+ ...(config.persona !== undefined ? { persona: config.persona } : {}),
122
+ }),
123
+ toolContext: request.actor ? { actor: request.actor } : {},
124
+ repairs: 0,
125
+ };
126
+ }
127
+ function buildGenerateRequest(state, config, options) {
128
+ return {
129
+ messages: state.messages,
130
+ tools: state.tools,
131
+ ...(config.model !== undefined ? { model: config.model } : {}),
132
+ ...(options.signal ? { signal: options.signal } : {}),
133
+ };
134
+ }
135
+ /**
136
+ * The one per-turn decision, shared by the buffered and streaming loops.
137
+ * Reads the adapter's result, mutates `state` (records the assistant
138
+ * turn + tool results, bumps the repair counter), and returns either a
139
+ * terminal {@link AuthorEvent} or the signal to run another turn.
140
+ */
141
+ async function stepTurn(result, config, state) {
142
+ // No tool call ⇒ a plain-text message (a reply or a clarifying question).
143
+ if (result.toolCalls.length === 0) {
144
+ const text = result.text && result.text.length > 0 ? result.text : EMPTY_TURN_MESSAGE;
145
+ return { done: { kind: "message", text } };
146
+ }
147
+ // Record the assistant turn — the tool protocol requires a tool result
148
+ // for every tool call the assistant made.
149
+ state.messages.push({
150
+ role: "assistant",
151
+ content: result.text,
152
+ toolCalls: result.toolCalls,
153
+ });
154
+ for (const call of result.toolCalls) {
155
+ if (call.name === EMIT_TOOL_NAME) {
156
+ const graph = call.arguments["graph"];
157
+ const summary = typeof call.arguments["summary"] === "string"
158
+ ? call.arguments["summary"]
159
+ : "";
160
+ const verdict = config.validator(graph);
161
+ if (verdict.ok) {
162
+ // Return immediately — a later tool call in the same turn (e.g. a
163
+ // throwing toolset dispatch) must not discard a valid proposal.
164
+ return {
165
+ done: { kind: "proposal", graph: graph, summary },
166
+ };
167
+ }
168
+ state.repairs++;
169
+ state.messages.push({
170
+ role: "tool",
171
+ toolCallId: call.id,
172
+ content: "The workflow did not validate. Fix ALL of these and call propose_workflow again:\n- " +
173
+ verdict.errors.join("\n- "),
174
+ });
175
+ }
176
+ else if (config.toolRegistry.has(call.name)) {
177
+ const toolResult = await config.toolRegistry.dispatch(call, state.toolContext);
178
+ state.messages.push({
179
+ role: "tool",
180
+ toolCallId: call.id,
181
+ content: toolResult,
182
+ });
183
+ }
184
+ else {
185
+ // The model hallucinated a tool — tell it what actually exists.
186
+ state.messages.push({
187
+ role: "tool",
188
+ toolCallId: call.id,
189
+ content: `Unknown tool "${call.name}". Available tools: ${state.tools
190
+ .map((tool) => tool.name)
191
+ .join(", ")}.`,
192
+ });
193
+ }
194
+ }
195
+ if (state.repairs > config.maxRepairAttempts) {
196
+ return {
197
+ done: {
198
+ kind: "message",
199
+ text: `I could not produce a valid workflow after ${config.maxRepairAttempts} repair attempt(s). Please refine your request.`,
200
+ },
201
+ };
202
+ }
203
+ return { continue: true };
204
+ }
205
+ /** Buffered tool loop: fetch each turn via `adapter.generate`. */
206
+ async function runToolLoop(config, request, options) {
207
+ const state = createTurnState(config, request);
208
+ for (let iteration = 0; iteration < config.maxToolIterations; iteration++) {
209
+ const result = await config.adapter.generate(buildGenerateRequest(state, config, options));
210
+ const step = await stepTurn(result, config, state);
211
+ if ("done" in step)
212
+ return step.done;
213
+ }
214
+ return { kind: "message", text: MAX_STEPS_MESSAGE };
215
+ }
216
+ /** One adapter turn, streamed: yields text deltas, returns the full result.
217
+ * Falls back to `generate` when the adapter has no `generateStream`. */
218
+ async function* streamOneTurn(adapter, request) {
219
+ if (adapter.generateStream === undefined) {
220
+ return await adapter.generate(request);
221
+ }
222
+ let result;
223
+ for await (const part of adapter.generateStream(request)) {
224
+ if (part.type === "text-delta") {
225
+ yield { kind: "text-delta", delta: part.delta };
226
+ }
227
+ else {
228
+ result = part.result;
229
+ }
230
+ }
231
+ return result ?? { text: null, toolCalls: [] };
232
+ }
233
+ /** Streaming tool loop: fetch each turn via {@link streamOneTurn}, forwarding
234
+ * its text deltas, then feed the full result to the shared {@link stepTurn}. */
235
+ async function* runToolLoopStream(config, request, options) {
236
+ const state = createTurnState(config, request);
237
+ for (let iteration = 0; iteration < config.maxToolIterations; iteration++) {
238
+ const result = yield* streamOneTurn(config.adapter, buildGenerateRequest(state, config, options));
239
+ const step = await stepTurn(result, config, state);
240
+ if ("done" in step)
241
+ return step.done;
242
+ }
243
+ return { kind: "message", text: MAX_STEPS_MESSAGE };
244
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The **authoring schema** — the JSON Schema the AI emits against.
3
+ *
4
+ * It is the canonical `WorkflowGraph` schema that `@flowget/types`
5
+ * ships (`@flowget/types/schemas/workflow.schema.json`), with a single
6
+ * relaxation: `position` is no longer required on a node. The AI emits
7
+ * topology + config; the builder lays the graph out later. Deriving it
8
+ * from the shipped schema keeps a **single source of truth** — no
9
+ * hand-maintained copy to drift.
10
+ */
11
+ /** Load the canonical workflow-graph JSON schema shipped by `@flowget/types`. */
12
+ export declare function loadWorkflowSchema(): Record<string, unknown>;
13
+ /**
14
+ * The position-optional authoring schema, derived from the shipped
15
+ * workflow schema. Returns a fresh object each call (safe to mutate).
16
+ */
17
+ export declare function buildAuthoringSchema(): Record<string, unknown>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The **authoring schema** — the JSON Schema the AI emits against.
3
+ *
4
+ * It is the canonical `WorkflowGraph` schema that `@flowget/types`
5
+ * ships (`@flowget/types/schemas/workflow.schema.json`), with a single
6
+ * relaxation: `position` is no longer required on a node. The AI emits
7
+ * topology + config; the builder lays the graph out later. Deriving it
8
+ * from the shipped schema keeps a **single source of truth** — no
9
+ * hand-maintained copy to drift.
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { createRequire } from "node:module";
13
+ import { isRecord } from "./guards.js";
14
+ const require = createRequire(import.meta.url);
15
+ /**
16
+ * Memoized canonical schema. The file is stable for the process
17
+ * lifetime, and `loadWorkflowSchema` sits on the hot path
18
+ * (`buildSystemPrompt` + `buildEmitTool`, both per `author()` call), so
19
+ * the `readFileSync` + `JSON.parse` runs exactly once. Callers MUST
20
+ * treat the result as immutable — `buildAuthoringSchema` clones before
21
+ * mutating, and prompt assembly reads it non-destructively.
22
+ */
23
+ let cachedSchema;
24
+ /** Load the canonical workflow-graph JSON schema shipped by `@flowget/types`. */
25
+ export function loadWorkflowSchema() {
26
+ if (cachedSchema !== undefined)
27
+ return cachedSchema;
28
+ const schemaPath = require.resolve("@flowget/types/schemas/workflow.schema.json");
29
+ const parsed = JSON.parse(readFileSync(schemaPath, "utf8"));
30
+ if (!isRecord(parsed)) {
31
+ throw new Error("workflow.schema.json did not parse to an object — @flowget/types may be corrupt.");
32
+ }
33
+ cachedSchema = parsed;
34
+ return parsed;
35
+ }
36
+ /**
37
+ * The position-optional authoring schema, derived from the shipped
38
+ * workflow schema. Returns a fresh object each call (safe to mutate).
39
+ */
40
+ export function buildAuthoringSchema() {
41
+ const schema = structuredClone(loadWorkflowSchema());
42
+ const defs = schema["$defs"];
43
+ if (isRecord(defs)) {
44
+ const node = defs["WorkflowNode"];
45
+ if (isRecord(node) && Array.isArray(node["required"])) {
46
+ node["required"] = node["required"].filter((key) => key !== "position");
47
+ }
48
+ }
49
+ // Distinct $id so an AJV instance can hold both this and the
50
+ // canonical schema without a duplicate-id collision.
51
+ schema["$id"] = "https://schemas.flowget.com/workflow-authoring/v1.json";
52
+ return schema;
53
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Authorizers — verify the TRUSTED actor before the guardrail and
3
+ * toolsets run.
4
+ *
5
+ * The actor is stamped by the customer's BFF and handed to `author()`
6
+ * on the request; an authorizer is where you VERIFY it (signature
7
+ * check, tenant lookup, scope assertion). Authorizers run first, in
8
+ * order. Any authorizer that throws rejects the whole request — throw
9
+ * {@link AuthorizationError} to carry a status your transport can map
10
+ * to a response (401 by default).
11
+ *
12
+ * Auth-agnostic stance: the library never forces authentication. With
13
+ * no authorizers configured and no actor on the request, the request
14
+ * proceeds anonymously — identity enforcement is the customer's choice.
15
+ */
16
+ import type { Actor } from "@flowget/types";
17
+ import type { AuthorRequest } from "./contract.js";
18
+ export declare class AuthorizationError extends Error {
19
+ name: string;
20
+ readonly status: number;
21
+ constructor(message: string, status?: number);
22
+ }
23
+ export type Authorizer = (actor: Actor | undefined, request: AuthorRequest) => void | Promise<void>;
24
+ /** Run every authorizer in order; the first throw rejects the request. */
25
+ export declare function runAuthorizers(authorizers: readonly Authorizer[], actor: Actor | undefined, request: AuthorRequest): Promise<void>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Authorizers — verify the TRUSTED actor before the guardrail and
3
+ * toolsets run.
4
+ *
5
+ * The actor is stamped by the customer's BFF and handed to `author()`
6
+ * on the request; an authorizer is where you VERIFY it (signature
7
+ * check, tenant lookup, scope assertion). Authorizers run first, in
8
+ * order. Any authorizer that throws rejects the whole request — throw
9
+ * {@link AuthorizationError} to carry a status your transport can map
10
+ * to a response (401 by default).
11
+ *
12
+ * Auth-agnostic stance: the library never forces authentication. With
13
+ * no authorizers configured and no actor on the request, the request
14
+ * proceeds anonymously — identity enforcement is the customer's choice.
15
+ */
16
+ export class AuthorizationError extends Error {
17
+ name = "AuthorizationError";
18
+ status;
19
+ constructor(message, status = 401) {
20
+ super(message);
21
+ this.status = status;
22
+ }
23
+ }
24
+ /** Run every authorizer in order; the first throw rejects the request. */
25
+ export async function runAuthorizers(authorizers, actor, request) {
26
+ for (const authorizer of authorizers) {
27
+ await authorizer(actor, request);
28
+ }
29
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Catalog loading — the ONLY substrate the AI needs.
3
+ *
4
+ * The catalog is the customer's node metadata: the `.json` files under
5
+ * their `REGISTRY_DIR`, loaded via `@flowget/registry-loader`. We load
6
+ * ONLY the node `.json` (never `import` the executor `.ts`, never boot
7
+ * a worker, never touch Temporal), exactly the loader's documented
8
+ * web-side pattern. The library's dependency footprint stays tiny.
9
+ */
10
+ import type { BuilderHints, NodeMetadata } from "@flowget/types";
11
+ /** A single catalog entry — a node's projected metadata. */
12
+ export type CatalogNode = NodeMetadata<BuilderHints>;
13
+ /**
14
+ * Where the catalog comes from. Three accepted forms:
15
+ *
16
+ * - A pre-loaded `CatalogNode[]` — the customer already has metadata
17
+ * (e.g. from their own build step) and hands it in directly.
18
+ * - `{ registryDir }` — a `REGISTRY_DIR`-shaped specifier string
19
+ * (filesystem paths and/or npm subpaths, brace-expandable, comma-
20
+ * separated), resolved and walked for you.
21
+ * - `{ paths }` — already-resolved absolute base paths to walk.
22
+ */
23
+ export type CatalogSource = CatalogNode[] | {
24
+ registryDir: string;
25
+ cwd?: string;
26
+ } | {
27
+ paths: readonly string[];
28
+ };
29
+ /** Resolve any {@link CatalogSource} to a flat `CatalogNode[]`. */
30
+ export declare function resolveCatalog(source: CatalogSource): Promise<CatalogNode[]>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Catalog loading — the ONLY substrate the AI needs.
3
+ *
4
+ * The catalog is the customer's node metadata: the `.json` files under
5
+ * their `REGISTRY_DIR`, loaded via `@flowget/registry-loader`. We load
6
+ * ONLY the node `.json` (never `import` the executor `.ts`, never boot
7
+ * a worker, never touch Temporal), exactly the loader's documented
8
+ * web-side pattern. The library's dependency footprint stays tiny.
9
+ */
10
+ import { discoverRegistry, expandBraces, parseRegistryEnv, projectMetadata, resolveRegistrySpecifier, } from "@flowget/registry-loader";
11
+ /** Resolve any {@link CatalogSource} to a flat `CatalogNode[]`. */
12
+ export async function resolveCatalog(source) {
13
+ if (Array.isArray(source))
14
+ return [...source];
15
+ const paths = "registryDir" in source
16
+ ? parseRegistryEnv(source.registryDir)
17
+ .flatMap(expandBraces)
18
+ .map((entry) => resolveRegistrySpecifier(entry, source.cwd ? { cwd: source.cwd } : {}))
19
+ : [...source.paths];
20
+ const records = await discoverRegistry({ paths });
21
+ return records.map((record) => projectMetadata(record.json, record.jsonPath));
22
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Authoring configuration for `author()` — every piece a named, typed,
3
+ * swappable hook with a batteries-included default. `adapter` and
4
+ * `catalog` are the only required fields.
5
+ */
6
+ import type { AiAdapter } from "./adapter.js";
7
+ import type { Authorizer } from "./authorizer.js";
8
+ import type { CatalogNode, CatalogSource } from "./catalog.js";
9
+ import type { Guardrail } from "./guardrail.js";
10
+ import type { AuditHooks } from "./hooks.js";
11
+ import type { Toolset } from "./toolset.js";
12
+ import { ToolRegistry } from "./toolset.js";
13
+ import type { WorkflowValidator, WorkflowValidatorFactory } from "./validator.js";
14
+ export declare const DEFAULT_MAX_TOOL_ITERATIONS = 8;
15
+ export declare const DEFAULT_MAX_REPAIR_ATTEMPTS = 2;
16
+ export type AiConfig = {
17
+ /** The LLM. Default: `openaiAdapter({ apiKey })` → OpenRouter. */
18
+ adapter: AiAdapter;
19
+ /** The node catalog — the substrate the AI authors against. */
20
+ catalog: CatalogSource;
21
+ /** Pre-LLM policy gate. No default (policy is the customer's). */
22
+ guardrail?: Guardrail;
23
+ /** Verify the trusted actor before guardrail/toolsets. Default: none. */
24
+ authorizers?: Authorizer[];
25
+ /** Actor-scoped, model-invoked tools. Default: none. */
26
+ toolsets?: Toolset[];
27
+ /** Audit sinks for proposals / blocks. Default: no-op. */
28
+ hooks?: AuditHooks;
29
+ /** Persona override for the system prompt. Default: workflow-only persona. */
30
+ persona?: string;
31
+ /** Workflow-validator factory. Default: AJV + catalog + template-ref. */
32
+ validator?: WorkflowValidatorFactory;
33
+ /** Default model slug forwarded to the adapter. Adapter picks its own if absent. */
34
+ model?: string;
35
+ /** Max adapter turns per request. Default 8. */
36
+ maxToolIterations?: number;
37
+ /** Max emit re-validation (repair) attempts per request. Default 2. */
38
+ maxRepairAttempts?: number;
39
+ };
40
+ /** Fully-resolved runtime config — every default applied. */
41
+ export type ResolvedConfig = {
42
+ adapter: AiAdapter;
43
+ catalog: CatalogNode[];
44
+ validator: WorkflowValidator;
45
+ toolRegistry: ToolRegistry;
46
+ guardrail: Guardrail | undefined;
47
+ authorizers: Authorizer[];
48
+ hooks: AuditHooks;
49
+ persona: string | undefined;
50
+ model: string | undefined;
51
+ maxToolIterations: number;
52
+ maxRepairAttempts: number;
53
+ };
54
+ /** Build a {@link ResolvedConfig} from config + an already-loaded catalog. */
55
+ export declare function finalizeConfig(config: AiConfig, catalog: CatalogNode[]): ResolvedConfig;
56
+ /** Resolve config fully, loading the catalog from its source first. */
57
+ export declare function resolveConfig(config: AiConfig): Promise<ResolvedConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Authoring configuration for `author()` — every piece a named, typed,
3
+ * swappable hook with a batteries-included default. `adapter` and
4
+ * `catalog` are the only required fields.
5
+ */
6
+ import { resolveCatalog } from "./catalog.js";
7
+ import { EMIT_TOOL_NAME } from "./emit-tool.js";
8
+ import { ToolRegistry } from "./toolset.js";
9
+ import { createWorkflowValidator } from "./validator.js";
10
+ export const DEFAULT_MAX_TOOL_ITERATIONS = 8;
11
+ export const DEFAULT_MAX_REPAIR_ATTEMPTS = 2;
12
+ /** Build a {@link ResolvedConfig} from config + an already-loaded catalog. */
13
+ export function finalizeConfig(config, catalog) {
14
+ if (catalog.length === 0) {
15
+ throw new Error("resolveConfig: the resolved catalog is empty. Point `catalog` at a REGISTRY_DIR containing node `.json` files, or pass a pre-loaded NodeMetadata[]. The AI cannot author a workflow with no node types.");
16
+ }
17
+ const validatorFactory = config.validator ?? createWorkflowValidator;
18
+ return {
19
+ adapter: config.adapter,
20
+ catalog,
21
+ validator: validatorFactory(catalog),
22
+ toolRegistry: new ToolRegistry(config.toolsets ?? [], [EMIT_TOOL_NAME]),
23
+ guardrail: config.guardrail,
24
+ authorizers: config.authorizers ?? [],
25
+ hooks: config.hooks ?? {},
26
+ persona: config.persona,
27
+ model: config.model,
28
+ maxToolIterations: config.maxToolIterations ?? DEFAULT_MAX_TOOL_ITERATIONS,
29
+ maxRepairAttempts: config.maxRepairAttempts ?? DEFAULT_MAX_REPAIR_ATTEMPTS,
30
+ };
31
+ }
32
+ /** Resolve config fully, loading the catalog from its source first. */
33
+ export async function resolveConfig(config) {
34
+ const catalog = await resolveCatalog(config.catalog);
35
+ return finalizeConfig(config, catalog);
36
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The shapes `author()` / `authorStream()` return.
3
+ *
4
+ * Stateless: every request is self-contained. The workflow graph IS
5
+ * the state, so there is no session store. Optional continuity is
6
+ * carried by the CALLER via {@link AuthorRequest.context} — the library
7
+ * never persists anything across requests.
8
+ *
9
+ * These are plain data shapes, not a wire format. YOU choose the
10
+ * transport: return an {@link AuthorEvent} as a JSON response, push
11
+ * {@link AuthorStreamEvent}s over SSE / WebSocket, or consume them
12
+ * in-process. The library ships no HTTP server.
13
+ */
14
+ import type { Actor, WorkflowEdge, WorkflowNode } from "@flowget/types";
15
+ /**
16
+ * A node in an *authored* graph. Identical to `@flowget/types`'
17
+ * `WorkflowNode` except `position` is **optional** — the AI emits
18
+ * topology + config, and the builder lays the graph out (iteration 2).
19
+ * A downstream consumer that needs coordinates fills them in.
20
+ */
21
+ export type AuthoredNode = Omit<WorkflowNode, "position"> & {
22
+ position?: {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ };
27
+ /**
28
+ * The graph `author()` proposes. Position-optional sibling of
29
+ * `WorkflowGraph`; still validates against the workflow JSON schema
30
+ * with the `position` requirement relaxed (see `authoring-schema.ts`).
31
+ */
32
+ export type AuthoredGraph = {
33
+ nodes: AuthoredNode[];
34
+ edges: WorkflowEdge[];
35
+ };
36
+ /**
37
+ * The input to {@link author} / {@link authorStream}.
38
+ *
39
+ * - `command` — the natural-language instruction (required).
40
+ * - `currentGraph` — the graph being edited, for a targeted tweak.
41
+ * Absent for a from-scratch authoring.
42
+ * - `actor` — the caller identity, as **caller-supplied JSON**. It is
43
+ * threaded to `authorizers`, the guardrail, and actor-scoped
44
+ * toolsets. **It is NOT trusted or verified by default** — the
45
+ * library performs no signature or shape check. It becomes
46
+ * trustworthy ONLY once you configure an `authorizer` that verifies
47
+ * it (per the auth-agnostic stance). Any actor-scoped toolset that
48
+ * gates data access on `actor` (e.g. by `tenantId`) therefore
49
+ * REQUIRES an authorizer, or a caller can spoof the actor. Absent for
50
+ * an anonymous deployment.
51
+ * - `context` — opaque, caller-carried continuity payload. The library
52
+ * forwards it to the model as extra grounding but never inspects or
53
+ * persists it.
54
+ *
55
+ * **Bound the inputs (host responsibility).** `command`, `currentGraph`,
56
+ * and `context` are all serialized into the prompt, and `currentGraph` /
57
+ * `context` can be re-sent across repair turns (up to `maxToolIterations`).
58
+ * The host MUST cap the size of ALL THREE before calling — an unbounded
59
+ * `currentGraph` or `context` is as much a token-DoS as a huge `command`.
60
+ * See the README guardrail example.
61
+ */
62
+ export type AuthorRequest = {
63
+ command: string;
64
+ currentGraph?: AuthoredGraph;
65
+ actor?: Actor;
66
+ context?: unknown;
67
+ };
68
+ /**
69
+ * A workflow-graph proposal to preview / apply.
70
+ *
71
+ * **Model-authored, untrusted until approved.** The graph is a model
72
+ * suggestion, not a vetted artifact. Validation gates *runnability*
73
+ * (schema shape, known node types, resolvable references) — it does NOT
74
+ * vet field *values*: a malicious or mistaken value (e.g. an
75
+ * exfiltrating URL in an `http_request` node) passes validation. Never
76
+ * auto-apply a proposal without human review and/or a downstream
77
+ * field-value policy check. Treat `summary` as untrusted model output
78
+ * too (output-encode before rendering).
79
+ */
80
+ export type AuthorProposal = {
81
+ kind: "proposal";
82
+ graph: AuthoredGraph;
83
+ summary: string;
84
+ };
85
+ /**
86
+ * A text reply — a plain answer, a refusal, or a **clarifying
87
+ * question** when the command is ambiguous. Conversation emerges only
88
+ * when useful; the default is a one-shot proposal.
89
+ */
90
+ export type AuthorMessage = {
91
+ kind: "message";
92
+ text: string;
93
+ };
94
+ /**
95
+ * The discriminated result of {@link author}: exactly one
96
+ * {@link AuthorProposal} or {@link AuthorMessage}. Discriminate on
97
+ * `kind`. This is a plain value — serialize it over whatever transport
98
+ * you run (a JSON response, a queue message, an in-process return); the
99
+ * library pins no wire format.
100
+ */
101
+ export type AuthorEvent = AuthorProposal | AuthorMessage;
102
+ /**
103
+ * A streamed authoring event (see {@link authorStream}). Assistant text
104
+ * arrives incrementally as `text-delta`s; the stream ends with exactly one
105
+ * terminal `proposal`, `message`, or `error`. The terminal `message.text` is
106
+ * the full, authoritative text (a client that rendered the deltas can ignore
107
+ * it or use it to reconcile). A mid-stream failure is a terminal `error`.
108
+ *
109
+ * Discriminate on `kind` — the same key as {@link AuthorEvent}. These are
110
+ * plain values: frame them over SSE, a WebSocket, or an in-process async
111
+ * iterator as you see fit. The library pins no wire format.
112
+ */
113
+ export type AuthorStreamEvent = {
114
+ kind: "text-delta";
115
+ delta: string;
116
+ } | {
117
+ kind: "proposal";
118
+ graph: AuthoredGraph;
119
+ summary: string;
120
+ } | {
121
+ kind: "message";
122
+ text: string;
123
+ } | {
124
+ kind: "error";
125
+ error: string;
126
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The shapes `author()` / `authorStream()` return.
3
+ *
4
+ * Stateless: every request is self-contained. The workflow graph IS
5
+ * the state, so there is no session store. Optional continuity is
6
+ * carried by the CALLER via {@link AuthorRequest.context} — the library
7
+ * never persists anything across requests.
8
+ *
9
+ * These are plain data shapes, not a wire format. YOU choose the
10
+ * transport: return an {@link AuthorEvent} as a JSON response, push
11
+ * {@link AuthorStreamEvent}s over SSE / WebSocket, or consume them
12
+ * in-process. The library ships no HTTP server.
13
+ */
14
+ export {};