@boardwalk-labs/workflow 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 +18 -0
- package/README.md +51 -0
- package/dist/events.d.ts +191 -0
- package/dist/events.js +213 -0
- package/dist/extract.d.ts +27 -0
- package/dist/extract.js +192 -0
- package/dist/host.d.ts +62 -0
- package/dist/host.js +70 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +106 -0
- package/dist/manifest.d.ts +143 -0
- package/dist/manifest.js +227 -0
- package/dist/meta.d.ts +164 -0
- package/dist/meta.js +8 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +6 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.js +2 -0
- package/package.json +50 -0
package/dist/extract.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @boardwalk-labs/workflow/extract — static extraction of a workflow program's `meta` → manifest.
|
|
2
|
+
//
|
|
3
|
+
// A workflow is a TS/JS program file whose `export const meta = { … }` is a PURE LITERAL.
|
|
4
|
+
// Engines and tooling must derive the manifest WITHOUT executing the program, so this module
|
|
5
|
+
// parses the file's AST and reads the literal statically:
|
|
6
|
+
//
|
|
7
|
+
// source text → TS AST → unwrap `satisfies`/`as` → evaluate the pure object literal
|
|
8
|
+
// → validate against workflowManifestSchema → WorkflowManifest
|
|
9
|
+
//
|
|
10
|
+
// "Pure literal" means: object/array literals, string/number/boolean/null literals, and
|
|
11
|
+
// no-substitution template strings ONLY. Any variable reference, function call (incl.
|
|
12
|
+
// `defineMeta(...)`), spread, shorthand, computed key, or template interpolation is
|
|
13
|
+
// rejected — those would require executing the file to know the value. (`defineMeta` is
|
|
14
|
+
// deliberately unsupported; type the literal with `satisfies WorkflowMeta` instead.)
|
|
15
|
+
//
|
|
16
|
+
// This module executes none of the program. It is pure logic — tested exhaustively. It is a
|
|
17
|
+
// subpath export (`@boardwalk-labs/workflow/extract`) consumed by engines and the CLI; author
|
|
18
|
+
// programs never import it.
|
|
19
|
+
import ts from "typescript";
|
|
20
|
+
import { validateMeta } from "./manifest.js";
|
|
21
|
+
/** Thrown when a program's `meta` cannot be statically extracted as a pure literal. */
|
|
22
|
+
export class MetaExtractionError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "MetaExtractionError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const DEFAULT_FILE_NAME = "index.ts";
|
|
29
|
+
/**
|
|
30
|
+
* Statically extract the raw `meta` pure-literal object from a workflow program's source.
|
|
31
|
+
*
|
|
32
|
+
* Returns the plain JS object exactly as written (no schema defaults applied). Throws
|
|
33
|
+
* {@link MetaExtractionError} when there is no `meta` declaration, when it is not an object
|
|
34
|
+
* literal, or when any part of it is not a pure literal.
|
|
35
|
+
*/
|
|
36
|
+
export function extractMetaLiteral(source, options = {}) {
|
|
37
|
+
const fileName = options.fileName ?? DEFAULT_FILE_NAME;
|
|
38
|
+
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /*setParentNodes*/ true);
|
|
39
|
+
const initializer = findMetaInitializer(sf);
|
|
40
|
+
if (initializer === null) {
|
|
41
|
+
throw fail("No `meta` declaration found — a workflow program must export a pure-literal " +
|
|
42
|
+
"`export const meta = { … }`");
|
|
43
|
+
}
|
|
44
|
+
const unwrapped = unwrapTypeOnly(initializer);
|
|
45
|
+
// Targeted message for the one mistake the design explicitly forbids.
|
|
46
|
+
if (ts.isCallExpression(unwrapped)) {
|
|
47
|
+
throw fail("`meta` must be a plain object literal, not a function call — `defineMeta(...)` is " +
|
|
48
|
+
"not supported; type the literal with `satisfies WorkflowMeta` instead", unwrapped, sf);
|
|
49
|
+
}
|
|
50
|
+
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
51
|
+
throw fail("`meta` must be an object literal (`export const meta = { … }`)", unwrapped, sf);
|
|
52
|
+
}
|
|
53
|
+
return evalObjectLiteral(unwrapped, sf);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Statically extract `meta` and validate it against the manifest schema, returning the
|
|
57
|
+
* fully-defaulted, validated manifest (the contract every engine consumes). Throws
|
|
58
|
+
* {@link MetaExtractionError} on an unextractable literal, or `MetaValidationError` (from
|
|
59
|
+
* `validateMeta`) on a schema violation.
|
|
60
|
+
*/
|
|
61
|
+
export function extractManifest(source, options = {}) {
|
|
62
|
+
return validateMeta(extractMetaLiteral(source, options));
|
|
63
|
+
}
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// AST walking
|
|
66
|
+
// ============================================================================
|
|
67
|
+
/** Find the initializer of the first top-level `const meta = …` (exported or not). */
|
|
68
|
+
function findMetaInitializer(sf) {
|
|
69
|
+
for (const stmt of sf.statements) {
|
|
70
|
+
if (!ts.isVariableStatement(stmt))
|
|
71
|
+
continue;
|
|
72
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
73
|
+
if (ts.isIdentifier(decl.name) &&
|
|
74
|
+
decl.name.text === "meta" &&
|
|
75
|
+
decl.initializer !== undefined) {
|
|
76
|
+
return decl.initializer;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Strip type-only wrappers that are erased at compile time and don't affect the value:
|
|
84
|
+
* `expr satisfies T`, `expr as T`, `<T>expr`, and `(expr)`. Applied repeatedly so
|
|
85
|
+
* `({ … } satisfies WorkflowMeta)` unwraps fully.
|
|
86
|
+
*/
|
|
87
|
+
function unwrapTypeOnly(node) {
|
|
88
|
+
let current = node;
|
|
89
|
+
for (;;) {
|
|
90
|
+
if (ts.isSatisfiesExpression(current) || ts.isAsExpression(current)) {
|
|
91
|
+
current = current.expression;
|
|
92
|
+
}
|
|
93
|
+
else if (ts.isParenthesizedExpression(current)) {
|
|
94
|
+
current = current.expression;
|
|
95
|
+
}
|
|
96
|
+
else if (ts.isTypeAssertionExpression(current)) {
|
|
97
|
+
current = current.expression;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return current;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Pure-literal evaluation
|
|
106
|
+
// ============================================================================
|
|
107
|
+
function evalObjectLiteral(node, sf) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const prop of node.properties) {
|
|
110
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
111
|
+
throw fail("shorthand properties reference a variable, which is not a pure literal — " +
|
|
112
|
+
"write `key: value` with a literal value", prop, sf);
|
|
113
|
+
}
|
|
114
|
+
if (ts.isSpreadAssignment(prop)) {
|
|
115
|
+
throw fail("spread (`...`) is not allowed in a `meta` literal", prop, sf);
|
|
116
|
+
}
|
|
117
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
118
|
+
// Method / get / set declarations.
|
|
119
|
+
throw fail("methods and accessors are not allowed in a `meta` literal", prop, sf);
|
|
120
|
+
}
|
|
121
|
+
const key = propertyKey(prop.name, sf);
|
|
122
|
+
out[key] = evalLiteral(prop.initializer, sf);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** Resolve a property name to a string, allowing identifier / string / numeric / `["x"]`. */
|
|
127
|
+
function propertyKey(name, sf) {
|
|
128
|
+
if (ts.isIdentifier(name))
|
|
129
|
+
return name.text;
|
|
130
|
+
if (ts.isStringLiteral(name))
|
|
131
|
+
return name.text;
|
|
132
|
+
if (ts.isNumericLiteral(name))
|
|
133
|
+
return name.text;
|
|
134
|
+
if (ts.isComputedPropertyName(name) && ts.isStringLiteral(name.expression)) {
|
|
135
|
+
return name.expression.text;
|
|
136
|
+
}
|
|
137
|
+
throw fail("property keys must be plain identifiers or string/number literals (no computed keys)", name, sf);
|
|
138
|
+
}
|
|
139
|
+
/** Evaluate any value node, rejecting anything that isn't a pure literal. */
|
|
140
|
+
function evalLiteral(node, sf) {
|
|
141
|
+
const n = unwrapTypeOnly(node);
|
|
142
|
+
if (ts.isObjectLiteralExpression(n))
|
|
143
|
+
return evalObjectLiteral(n, sf);
|
|
144
|
+
if (ts.isArrayLiteralExpression(n))
|
|
145
|
+
return evalArrayLiteral(n, sf);
|
|
146
|
+
if (ts.isStringLiteral(n))
|
|
147
|
+
return n.text;
|
|
148
|
+
if (ts.isNoSubstitutionTemplateLiteral(n))
|
|
149
|
+
return n.text;
|
|
150
|
+
if (ts.isNumericLiteral(n))
|
|
151
|
+
return parseNumber(n.text);
|
|
152
|
+
if (n.kind === ts.SyntaxKind.TrueKeyword)
|
|
153
|
+
return true;
|
|
154
|
+
if (n.kind === ts.SyntaxKind.FalseKeyword)
|
|
155
|
+
return false;
|
|
156
|
+
if (n.kind === ts.SyntaxKind.NullKeyword)
|
|
157
|
+
return null;
|
|
158
|
+
// Negative numbers parse as `-` applied to a numeric literal.
|
|
159
|
+
if (ts.isPrefixUnaryExpression(n) &&
|
|
160
|
+
n.operator === ts.SyntaxKind.MinusToken &&
|
|
161
|
+
ts.isNumericLiteral(n.operand)) {
|
|
162
|
+
return -parseNumber(n.operand.text);
|
|
163
|
+
}
|
|
164
|
+
throw fail("unsupported expression in `meta` — only pure literals are allowed", n, sf);
|
|
165
|
+
}
|
|
166
|
+
function evalArrayLiteral(node, sf) {
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const el of node.elements) {
|
|
169
|
+
if (ts.isSpreadElement(el)) {
|
|
170
|
+
throw fail("spread (`...`) is not allowed in a `meta` literal array", el, sf);
|
|
171
|
+
}
|
|
172
|
+
if (el.kind === ts.SyntaxKind.OmittedExpression) {
|
|
173
|
+
throw fail("array holes (`[1, , 3]`) are not allowed in a `meta` literal", el, sf);
|
|
174
|
+
}
|
|
175
|
+
out.push(evalLiteral(el, sf));
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/** Parse a numeric-literal source token (handles separators, hex/oct/bin, floats, exponents). */
|
|
180
|
+
function parseNumber(text) {
|
|
181
|
+
return Number(text.replace(/_/g, ""));
|
|
182
|
+
}
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Errors
|
|
185
|
+
// ============================================================================
|
|
186
|
+
function fail(message, node, sf) {
|
|
187
|
+
if (node !== undefined && sf !== undefined) {
|
|
188
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
189
|
+
return new MetaExtractionError(`${sf.fileName}:${String(line + 1)}:${String(character + 1)} — ${message}`);
|
|
190
|
+
}
|
|
191
|
+
return new MetaExtractionError(message);
|
|
192
|
+
}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AgentOptions, ArtifactBody, ArtifactRef, CallOptions, JsonValue, PhaseOptions, SleepArg } from "./types.js";
|
|
2
|
+
/** The engine contract a host supplies. The author-facing hooks delegate to this. */
|
|
3
|
+
export interface WorkflowHost {
|
|
4
|
+
/**
|
|
5
|
+
* Mark the current run phase. Everything after this marker belongs to the phase until the next
|
|
6
|
+
* marker or run end. Observability-only: this is not a checkpoint/resume boundary.
|
|
7
|
+
*/
|
|
8
|
+
setPhase?(name: string, opts: PhaseOptions | undefined): void;
|
|
9
|
+
/**
|
|
10
|
+
* Run an agent leaf to completion; resolve to its text (or schema-validated object).
|
|
11
|
+
* `opts.model` may be omitted — the provider routes automatically (default provider =
|
|
12
|
+
* `boardwalk` on every engine; BYO keys only when a provider is explicitly named).
|
|
13
|
+
*/
|
|
14
|
+
agent(prompt: string, opts: AgentOptions | undefined): Promise<unknown>;
|
|
15
|
+
/** Dispatch a durable child run and resolve to its output (parent holds while it runs). */
|
|
16
|
+
callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<unknown>;
|
|
17
|
+
/** Hold the run for the requested duration (the run stays held while it waits; locals survive). */
|
|
18
|
+
sleep(arg: SleepArg): Promise<void>;
|
|
19
|
+
/** Resolve a granted secret to its plaintext value (fail-closed against `meta.secrets`). */
|
|
20
|
+
getSecret(name: string): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Fire-and-forget trigger of another workflow; resolve to the new run's id WITHOUT holding for
|
|
23
|
+
* its result (the sibling of {@link callWorkflow}). Optional — an engine that doesn't support it
|
|
24
|
+
* makes the `workflows.run` hook throw a clear error.
|
|
25
|
+
*/
|
|
26
|
+
runWorkflow?(slug: string, input: unknown, opts: CallOptions | undefined): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Store a file under the run's artifact prefix and resolve to its id + download URL.
|
|
29
|
+
* Optional — an engine that doesn't support it makes the `artifacts.write` hook throw a clear error.
|
|
30
|
+
*/
|
|
31
|
+
writeArtifact?(name: string, contentType: string, body: ArtifactBody, metadata: Record<string, unknown> | undefined): Promise<ArtifactRef>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The trigger payload for the current run, exposed to the program as
|
|
35
|
+
* `import { input } from "@boardwalk-labs/workflow"`. It is an ES live binding: the engine assigns
|
|
36
|
+
* it via {@link installInput} before the program evaluates, so the import reflects the value.
|
|
37
|
+
* `unknown` by contract — narrow it (e.g. with a schema) in your program.
|
|
38
|
+
*/
|
|
39
|
+
export declare let input: unknown;
|
|
40
|
+
/**
|
|
41
|
+
* The run's deploy-time configuration, exposed as `import { config } from "@boardwalk-labs/workflow"`.
|
|
42
|
+
* `{}` unless the deployment supplies one. Read it to vary behavior WITHOUT editing code — e.g.
|
|
43
|
+
* `agent(prompt, { model: config.model ?? undefined })`. Like {@link input}, an ES live binding
|
|
44
|
+
* the engine installs before the program evaluates. Frozen so a program can't mutate it.
|
|
45
|
+
*/
|
|
46
|
+
export declare let config: Readonly<Record<string, JsonValue>>;
|
|
47
|
+
/** Record the run's output (last write wins). Author-facing as `output(...)` (re-exported by index). */
|
|
48
|
+
export declare function recordOutput(value: JsonValue): void;
|
|
49
|
+
/** The engine reads the program's declared output after the body finishes; null ⇒ never declared. */
|
|
50
|
+
export declare function takeDeclaredOutput(): {
|
|
51
|
+
value: JsonValue;
|
|
52
|
+
} | null;
|
|
53
|
+
/** Install the host adapter. Called by the engine (hosted or local) before the program evaluates. */
|
|
54
|
+
export declare function installHost(host: WorkflowHost): void;
|
|
55
|
+
/** Set the run's trigger payload (the value `import { input }` resolves to). */
|
|
56
|
+
export declare function installInput(value: unknown): void;
|
|
57
|
+
/** Set the run's deploy-time config (the value `import { config }` resolves to). Frozen on install. */
|
|
58
|
+
export declare function installConfig(value: Record<string, JsonValue>): void;
|
|
59
|
+
/** Clear all installed runtime state. Primarily for tests and reused local-dev processes. */
|
|
60
|
+
export declare function resetRuntime(): void;
|
|
61
|
+
/** The installed host, or a clear error if a hook was called outside a Boardwalk engine. */
|
|
62
|
+
export declare function requireHost(): WorkflowHost;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The host seam — the engine-implementation boundary of the SDK.
|
|
2
|
+
//
|
|
3
|
+
// `@boardwalk-labs/workflow` is a host-backed package: the hooks authors import (agent, sleep,
|
|
4
|
+
// workflows.call, secrets.get, input) are thin facades over a `WorkflowHost` the *engine*
|
|
5
|
+
// installs at runtime. hosted Boardwalk installs its hosted adapter; the local engine installs
|
|
6
|
+
// one backed by the developer's environment. The author's program is identical either way —
|
|
7
|
+
// explicit hooks instead of injected globals.
|
|
8
|
+
//
|
|
9
|
+
// State is a module-level singleton. Node ESM caches a module by resolved path, so the
|
|
10
|
+
// program (which imports the hooks) and the engine (which installs the host) share ONE
|
|
11
|
+
// instance of this module and therefore one host + one `input`.
|
|
12
|
+
let currentHost = null;
|
|
13
|
+
/**
|
|
14
|
+
* The trigger payload for the current run, exposed to the program as
|
|
15
|
+
* `import { input } from "@boardwalk-labs/workflow"`. It is an ES live binding: the engine assigns
|
|
16
|
+
* it via {@link installInput} before the program evaluates, so the import reflects the value.
|
|
17
|
+
* `unknown` by contract — narrow it (e.g. with a schema) in your program.
|
|
18
|
+
*/
|
|
19
|
+
export let input = undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The run's deploy-time configuration, exposed as `import { config } from "@boardwalk-labs/workflow"`.
|
|
22
|
+
* `{}` unless the deployment supplies one. Read it to vary behavior WITHOUT editing code — e.g.
|
|
23
|
+
* `agent(prompt, { model: config.model ?? undefined })`. Like {@link input}, an ES live binding
|
|
24
|
+
* the engine installs before the program evaluates. Frozen so a program can't mutate it.
|
|
25
|
+
*/
|
|
26
|
+
export let config = {};
|
|
27
|
+
/**
|
|
28
|
+
* The run's declared output — what `output(value)` recorded, or null if the program never called
|
|
29
|
+
* it. Wrapped in a `{ value }` box so an explicit `output(null)` is distinguishable from "never
|
|
30
|
+
* set". The engine reads this AFTER the program body finishes (see {@link takeDeclaredOutput});
|
|
31
|
+
* it's the value persisted as the run's output, returned to a `workflows.call` parent, and the
|
|
32
|
+
* `output` event in the run's stream.
|
|
33
|
+
*/
|
|
34
|
+
let declaredOutput = null;
|
|
35
|
+
/** Record the run's output (last write wins). Author-facing as `output(...)` (re-exported by index). */
|
|
36
|
+
export function recordOutput(value) {
|
|
37
|
+
declaredOutput = { value };
|
|
38
|
+
}
|
|
39
|
+
/** The engine reads the program's declared output after the body finishes; null ⇒ never declared. */
|
|
40
|
+
export function takeDeclaredOutput() {
|
|
41
|
+
return declaredOutput;
|
|
42
|
+
}
|
|
43
|
+
/** Install the host adapter. Called by the engine (hosted or local) before the program evaluates. */
|
|
44
|
+
export function installHost(host) {
|
|
45
|
+
currentHost = host;
|
|
46
|
+
}
|
|
47
|
+
/** Set the run's trigger payload (the value `import { input }` resolves to). */
|
|
48
|
+
export function installInput(value) {
|
|
49
|
+
input = value;
|
|
50
|
+
}
|
|
51
|
+
/** Set the run's deploy-time config (the value `import { config }` resolves to). Frozen on install. */
|
|
52
|
+
export function installConfig(value) {
|
|
53
|
+
config = Object.freeze({ ...value });
|
|
54
|
+
}
|
|
55
|
+
/** Clear all installed runtime state. Primarily for tests and reused local-dev processes. */
|
|
56
|
+
export function resetRuntime() {
|
|
57
|
+
currentHost = null;
|
|
58
|
+
input = undefined;
|
|
59
|
+
config = {};
|
|
60
|
+
declaredOutput = null;
|
|
61
|
+
}
|
|
62
|
+
/** The installed host, or a clear error if a hook was called outside a Boardwalk engine. */
|
|
63
|
+
export function requireHost() {
|
|
64
|
+
if (currentHost === null) {
|
|
65
|
+
throw new Error("@boardwalk-labs/workflow hooks were called with no host installed. Under a Boardwalk engine " +
|
|
66
|
+
"the host is installed automatically; in tests call installHost(...) " +
|
|
67
|
+
'from "@boardwalk-labs/workflow/runtime" first.');
|
|
68
|
+
}
|
|
69
|
+
return currentHost;
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AgentOptions, ArtifactBody, ArtifactRef, CallOptions, JsonValue, PhaseOptions, SleepArg } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Mark the current run phase for live-tail and run-log grouping. Everything after this call
|
|
4
|
+
* belongs to the named phase until the next `Phase(...)` marker or the run ends. This is
|
|
5
|
+
* observability-only; it does not checkpoint or skip code on restart.
|
|
6
|
+
*/
|
|
7
|
+
export declare function Phase(name: string, opts?: PhaseOptions): void;
|
|
8
|
+
/**
|
|
9
|
+
* Run an agent leaf to completion. Without `opts.schema`, resolves to the leaf's final text
|
|
10
|
+
* (`T` defaults to `string`); with a schema, resolves to the validated object — pass the
|
|
11
|
+
* expected type, e.g. `await agent<Groups>(prompt, { schema })`. Omit `opts.model` to let the
|
|
12
|
+
* provider route automatically (the default `boardwalk` provider on every engine; your own
|
|
13
|
+
* keys only via an explicit provider). Capabilities (`tools`, `mcp`, `skills`, `memory`) are
|
|
14
|
+
* PER-AGENT — each call brings its own; the manifest declares none of them.
|
|
15
|
+
*/
|
|
16
|
+
export declare function agent<T = string>(prompt: string, opts?: AgentOptions): Promise<T>;
|
|
17
|
+
/** Cross-workflow composition: `call` (await the result) and `run` (fire-and-forget). */
|
|
18
|
+
export declare const workflows: {
|
|
19
|
+
/**
|
|
20
|
+
* Call another workflow as a durable child run. The parent HOLDS while the child runs and
|
|
21
|
+
* resolves to the child's output; the call is idempotent, so a restarted parent re-attaches
|
|
22
|
+
* instead of re-spawning. Use when you need the child's result to continue.
|
|
23
|
+
*/
|
|
24
|
+
readonly call: (slug: string, input: unknown, opts?: CallOptions) => Promise<unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Trigger another workflow as a fire-and-forget run. Returns the new run's id WITHOUT
|
|
27
|
+
* holding for its result. Idempotent on (parent, target, input) like {@link call}, so a
|
|
28
|
+
* restarted parent doesn't double-fire. Use when you want to kick something off and move on.
|
|
29
|
+
*/
|
|
30
|
+
readonly run: (slug: string, input: unknown, opts?: CallOptions) => Promise<string>;
|
|
31
|
+
};
|
|
32
|
+
/** Hold the run for a duration or until a timestamp (the run stays held while it waits; locals survive). */
|
|
33
|
+
export declare function sleep(arg: SleepArg): Promise<void>;
|
|
34
|
+
/** Granted secrets, resolved lazily and fail-closed against `meta.secrets`. */
|
|
35
|
+
export declare const secrets: {
|
|
36
|
+
/** Resolve a granted secret to its plaintext value. */
|
|
37
|
+
readonly get: (name: string) => Promise<string>;
|
|
38
|
+
};
|
|
39
|
+
/** Files (artifacts) that persist with the run. */
|
|
40
|
+
export declare const artifacts: {
|
|
41
|
+
/**
|
|
42
|
+
* Store a file under the run's artifact prefix. `body` is UTF-8 text or raw bytes; pass the
|
|
43
|
+
* MIME `contentType` (e.g. "text/plain", "application/json"). Resolves to the artifact's id,
|
|
44
|
+
* name, and a download URL.
|
|
45
|
+
*/
|
|
46
|
+
readonly write: (name: string, contentType: string, body: ArtifactBody, metadata?: Record<string, unknown>) => Promise<ArtifactRef>;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Run thunks concurrently and resolve to their results in order. A barrier: awaits all of
|
|
50
|
+
* them. Rejects on the first thunk that throws (standard `Promise.all` semantics) — wrap a
|
|
51
|
+
* thunk in your own try/catch if you want failures tolerated.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parallel<T>(thunks: readonly (() => Promise<T>)[]): Promise<T[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Declare the run's output — its final result. Since a workflow program is top-level module
|
|
56
|
+
* code (it can't `return`), this is how you set what the run produced: the value a
|
|
57
|
+
* `workflows.call` parent receives, what the run log shows as the result, and the `output`
|
|
58
|
+
* event in the run's stream. Last call wins; never calling it leaves the output null. The
|
|
59
|
+
* value must be JSON-serializable and is validated against `meta.output_schema` when declared.
|
|
60
|
+
*/
|
|
61
|
+
export declare function output(value: JsonValue): void;
|
|
62
|
+
export { input, config } from "./host.js";
|
|
63
|
+
export type { WorkflowMeta, Trigger, CronTrigger, WebhookTrigger, ManualTrigger, ToolGrant, McpServerRef, Concurrency, CallableBy, OrgRole, RunsOn, HostedRunsOn, HostedRunsOnObject, HostedRunnerSize, SelfHostedRunsOn, Container, SecretRef, EnvVars, EgressPolicy, RunPermissions, RunPermissionAccess, Budget, Notification, Workspace, } from "./meta.js";
|
|
64
|
+
export type { AgentOptions, ToolDef, ArtifactBody, ArtifactRef, CallOptions, PhaseOptions, SleepArg, JsonSchema, JsonValue, } from "./types.js";
|
|
65
|
+
export { workflowManifestSchema, validateMeta, MetaValidationError, type WorkflowManifest, } from "./manifest.js";
|
|
66
|
+
export { type RunEvent, type RunEventKind, type RunStatus, type Channel, type EventEnvelope, type TokenUsage, type ToolReturn, runEventSchema, CHANNELS, DEFAULT_CHANNELS, channelOf, matchesChannels, makeCursor, TURN_CURSOR_STRIDE, } from "./events.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @boardwalk-labs/workflow — the author-facing API a workflow program imports.
|
|
2
|
+
//
|
|
3
|
+
// import { agent, workflows, sleep, secrets, input, type WorkflowMeta } from "@boardwalk-labs/workflow"
|
|
4
|
+
//
|
|
5
|
+
// export const meta = { name: "x", triggers: [{ kind: "manual" }] } satisfies WorkflowMeta
|
|
6
|
+
//
|
|
7
|
+
// const groups = await agent("group failures", { schema: GROUPS })
|
|
8
|
+
// await parallel(groups.map((g) => () => workflows.call("file-issue", g)))
|
|
9
|
+
// await sleep({ until: "2026-07-01T00:00:00Z" })
|
|
10
|
+
//
|
|
11
|
+
// These hooks are facades over the installed host (see host.ts). Author programs never
|
|
12
|
+
// install a host — the engine running the program does.
|
|
13
|
+
import { requireHost, recordOutput } from "./host.js";
|
|
14
|
+
/**
|
|
15
|
+
* Mark the current run phase for live-tail and run-log grouping. Everything after this call
|
|
16
|
+
* belongs to the named phase until the next `Phase(...)` marker or the run ends. This is
|
|
17
|
+
* observability-only; it does not checkpoint or skip code on restart.
|
|
18
|
+
*/
|
|
19
|
+
export function Phase(name, opts) {
|
|
20
|
+
const host = requireHost();
|
|
21
|
+
if (host.setPhase === undefined) {
|
|
22
|
+
throw new Error("Phase is not supported by the installed engine");
|
|
23
|
+
}
|
|
24
|
+
host.setPhase(name, opts);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run an agent leaf to completion. Without `opts.schema`, resolves to the leaf's final text
|
|
28
|
+
* (`T` defaults to `string`); with a schema, resolves to the validated object — pass the
|
|
29
|
+
* expected type, e.g. `await agent<Groups>(prompt, { schema })`. Omit `opts.model` to let the
|
|
30
|
+
* provider route automatically (the default `boardwalk` provider on every engine; your own
|
|
31
|
+
* keys only via an explicit provider). Capabilities (`tools`, `mcp`, `skills`, `memory`) are
|
|
32
|
+
* PER-AGENT — each call brings its own; the manifest declares none of them.
|
|
33
|
+
*/
|
|
34
|
+
export async function agent(prompt, opts) {
|
|
35
|
+
return (await requireHost().agent(prompt, opts));
|
|
36
|
+
}
|
|
37
|
+
/** Cross-workflow composition: `call` (await the result) and `run` (fire-and-forget). */
|
|
38
|
+
export const workflows = {
|
|
39
|
+
/**
|
|
40
|
+
* Call another workflow as a durable child run. The parent HOLDS while the child runs and
|
|
41
|
+
* resolves to the child's output; the call is idempotent, so a restarted parent re-attaches
|
|
42
|
+
* instead of re-spawning. Use when you need the child's result to continue.
|
|
43
|
+
*/
|
|
44
|
+
async call(slug, input, opts) {
|
|
45
|
+
return await requireHost().callWorkflow(slug, input, opts);
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Trigger another workflow as a fire-and-forget run. Returns the new run's id WITHOUT
|
|
49
|
+
* holding for its result. Idempotent on (parent, target, input) like {@link call}, so a
|
|
50
|
+
* restarted parent doesn't double-fire. Use when you want to kick something off and move on.
|
|
51
|
+
*/
|
|
52
|
+
async run(slug, input, opts) {
|
|
53
|
+
const host = requireHost();
|
|
54
|
+
if (host.runWorkflow === undefined) {
|
|
55
|
+
throw new Error("workflows.run is not supported by the installed engine");
|
|
56
|
+
}
|
|
57
|
+
return await host.runWorkflow(slug, input, opts);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
/** Hold the run for a duration or until a timestamp (the run stays held while it waits; locals survive). */
|
|
61
|
+
export async function sleep(arg) {
|
|
62
|
+
await requireHost().sleep(arg);
|
|
63
|
+
}
|
|
64
|
+
/** Granted secrets, resolved lazily and fail-closed against `meta.secrets`. */
|
|
65
|
+
export const secrets = {
|
|
66
|
+
/** Resolve a granted secret to its plaintext value. */
|
|
67
|
+
async get(name) {
|
|
68
|
+
return await requireHost().getSecret(name);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
/** Files (artifacts) that persist with the run. */
|
|
72
|
+
export const artifacts = {
|
|
73
|
+
/**
|
|
74
|
+
* Store a file under the run's artifact prefix. `body` is UTF-8 text or raw bytes; pass the
|
|
75
|
+
* MIME `contentType` (e.g. "text/plain", "application/json"). Resolves to the artifact's id,
|
|
76
|
+
* name, and a download URL.
|
|
77
|
+
*/
|
|
78
|
+
async write(name, contentType, body, metadata) {
|
|
79
|
+
const host = requireHost();
|
|
80
|
+
if (host.writeArtifact === undefined) {
|
|
81
|
+
throw new Error("artifacts.write is not supported by the installed engine");
|
|
82
|
+
}
|
|
83
|
+
return await host.writeArtifact(name, contentType, body, metadata);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Run thunks concurrently and resolve to their results in order. A barrier: awaits all of
|
|
88
|
+
* them. Rejects on the first thunk that throws (standard `Promise.all` semantics) — wrap a
|
|
89
|
+
* thunk in your own try/catch if you want failures tolerated.
|
|
90
|
+
*/
|
|
91
|
+
export async function parallel(thunks) {
|
|
92
|
+
return Promise.all(thunks.map((thunk) => thunk()));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Declare the run's output — its final result. Since a workflow program is top-level module
|
|
96
|
+
* code (it can't `return`), this is how you set what the run produced: the value a
|
|
97
|
+
* `workflows.call` parent receives, what the run log shows as the result, and the `output`
|
|
98
|
+
* event in the run's stream. Last call wins; never calling it leaves the output null. The
|
|
99
|
+
* value must be JSON-serializable and is validated against `meta.output_schema` when declared.
|
|
100
|
+
*/
|
|
101
|
+
export function output(value) {
|
|
102
|
+
recordOutput(value);
|
|
103
|
+
}
|
|
104
|
+
export { input, config } from "./host.js";
|
|
105
|
+
export { workflowManifestSchema, validateMeta, MetaValidationError, } from "./manifest.js";
|
|
106
|
+
export { runEventSchema, CHANNELS, DEFAULT_CHANNELS, channelOf, matchesChannels, makeCursor, TURN_CURSOR_STRIDE, } from "./events.js";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const workflowManifestSchema: z.ZodObject<{
|
|
3
|
+
name: z.ZodString;
|
|
4
|
+
description: z.ZodOptional<z.ZodString>;
|
|
5
|
+
triggers: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
6
|
+
kind: z.ZodLiteral<"cron">;
|
|
7
|
+
expr: z.ZodString;
|
|
8
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
10
|
+
kind: z.ZodLiteral<"webhook">;
|
|
11
|
+
auth: z.ZodEnum<{
|
|
12
|
+
token: "token";
|
|
13
|
+
signature: "signature";
|
|
14
|
+
}>;
|
|
15
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
16
|
+
kind: z.ZodLiteral<"manual">;
|
|
17
|
+
}, z.core.$strict>], "kind">>;
|
|
18
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
19
|
+
name: z.ZodString;
|
|
20
|
+
}, z.core.$strict>>>;
|
|
21
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
22
|
+
input_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
23
|
+
output_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
24
|
+
workspace: z.ZodOptional<z.ZodObject<{
|
|
25
|
+
persist: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodArray<z.ZodString>]>>;
|
|
26
|
+
}, z.core.$strict>>;
|
|
27
|
+
budget: z.ZodOptional<z.ZodObject<{
|
|
28
|
+
max_tokens: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
max_usd: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
max_duration_seconds: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
}, z.core.$strict>>;
|
|
32
|
+
concurrency: z.ZodDefault<z.ZodUnion<readonly [z.ZodObject<{
|
|
33
|
+
mode: z.ZodLiteral<"serial_by_key">;
|
|
34
|
+
key: z.ZodString;
|
|
35
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
36
|
+
mode: z.ZodLiteral<"serial">;
|
|
37
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
38
|
+
mode: z.ZodLiteral<"unlimited">;
|
|
39
|
+
}, z.core.$strict>]>>;
|
|
40
|
+
runs_on: z.ZodDefault<z.ZodUnion<readonly [z.ZodObject<{
|
|
41
|
+
kind: z.ZodLiteral<"self-hosted">;
|
|
42
|
+
pool: z.ZodString;
|
|
43
|
+
labels: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
44
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
45
|
+
label: z.ZodEnum<{
|
|
46
|
+
"boardwalk/linux": "boardwalk/linux";
|
|
47
|
+
"boardwalk/linux-node": "boardwalk/linux-node";
|
|
48
|
+
"boardwalk/linux-python": "boardwalk/linux-python";
|
|
49
|
+
"boardwalk/linux-large": "boardwalk/linux-large";
|
|
50
|
+
}>;
|
|
51
|
+
size: z.ZodOptional<z.ZodEnum<{
|
|
52
|
+
small: "small";
|
|
53
|
+
medium: "medium";
|
|
54
|
+
large: "large";
|
|
55
|
+
xlarge: "xlarge";
|
|
56
|
+
}>>;
|
|
57
|
+
}, z.core.$strict>, z.ZodEnum<{
|
|
58
|
+
"boardwalk/linux": "boardwalk/linux";
|
|
59
|
+
"boardwalk/linux-node": "boardwalk/linux-node";
|
|
60
|
+
"boardwalk/linux-python": "boardwalk/linux-python";
|
|
61
|
+
"boardwalk/linux-large": "boardwalk/linux-large";
|
|
62
|
+
}>]>>;
|
|
63
|
+
container: z.ZodOptional<z.ZodObject<{
|
|
64
|
+
image: z.ZodString;
|
|
65
|
+
}, z.core.$strict>>;
|
|
66
|
+
permissions: z.ZodOptional<z.ZodObject<{
|
|
67
|
+
id_token: z.ZodOptional<z.ZodEnum<{
|
|
68
|
+
none: "none";
|
|
69
|
+
write: "write";
|
|
70
|
+
}>>;
|
|
71
|
+
artifacts: z.ZodOptional<z.ZodEnum<{
|
|
72
|
+
none: "none";
|
|
73
|
+
read: "read";
|
|
74
|
+
write: "write";
|
|
75
|
+
}>>;
|
|
76
|
+
contents: z.ZodOptional<z.ZodEnum<{
|
|
77
|
+
none: "none";
|
|
78
|
+
read: "read";
|
|
79
|
+
write: "write";
|
|
80
|
+
}>>;
|
|
81
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
82
|
+
name: z.ZodString;
|
|
83
|
+
}, z.core.$strict>>>;
|
|
84
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
85
|
+
name: z.ZodString;
|
|
86
|
+
config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
87
|
+
scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
88
|
+
}, z.core.$strict>>>;
|
|
89
|
+
}, z.core.$strict>>;
|
|
90
|
+
callable_by: z.ZodDefault<z.ZodUnion<readonly [z.ZodObject<{
|
|
91
|
+
roles: z.ZodArray<z.ZodEnum<{
|
|
92
|
+
owner: "owner";
|
|
93
|
+
admin: "admin";
|
|
94
|
+
member: "member";
|
|
95
|
+
viewer: "viewer";
|
|
96
|
+
}>>;
|
|
97
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
98
|
+
workflows: z.ZodArray<z.ZodString>;
|
|
99
|
+
}, z.core.$strict>, z.ZodEnum<{
|
|
100
|
+
anyone_in_org: "anyone_in_org";
|
|
101
|
+
users_only: "users_only";
|
|
102
|
+
workflows_only: "workflows_only";
|
|
103
|
+
}>]>>;
|
|
104
|
+
egress: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
105
|
+
level: z.ZodLiteral<"custom">;
|
|
106
|
+
allow: z.ZodArray<z.ZodString>;
|
|
107
|
+
include_defaults: z.ZodOptional<z.ZodBoolean>;
|
|
108
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
109
|
+
level: z.ZodEnum<{
|
|
110
|
+
none: "none";
|
|
111
|
+
trusted: "trusted";
|
|
112
|
+
full: "full";
|
|
113
|
+
}>;
|
|
114
|
+
}, z.core.$strict>]>>;
|
|
115
|
+
notifications: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
116
|
+
on: z.ZodEnum<{
|
|
117
|
+
cancelled: "cancelled";
|
|
118
|
+
completion: "completion";
|
|
119
|
+
failure: "failure";
|
|
120
|
+
}>;
|
|
121
|
+
channel: z.ZodEnum<{
|
|
122
|
+
webhook: "webhook";
|
|
123
|
+
email: "email";
|
|
124
|
+
}>;
|
|
125
|
+
target: z.ZodString;
|
|
126
|
+
template: z.ZodOptional<z.ZodString>;
|
|
127
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
128
|
+
on: z.ZodLiteral<"budget_exceeded">;
|
|
129
|
+
channel: z.ZodLiteral<"email">;
|
|
130
|
+
target: z.ZodString;
|
|
131
|
+
}, z.core.$strict>]>>>;
|
|
132
|
+
}, z.core.$strict>;
|
|
133
|
+
/** The fully-defaulted, validated manifest — the contract every engine consumes. */
|
|
134
|
+
export type WorkflowManifest = z.infer<typeof workflowManifestSchema>;
|
|
135
|
+
/**
|
|
136
|
+
* Validate an already-extracted `meta` object (e.g. from `extract.ts` or a test fixture) and
|
|
137
|
+
* return the manifest, or throw a `MetaValidationError` listing every issue with its path.
|
|
138
|
+
*/
|
|
139
|
+
export declare function validateMeta(meta: unknown): WorkflowManifest;
|
|
140
|
+
/** Thrown by {@link validateMeta} when a `meta` object violates the manifest schema. */
|
|
141
|
+
export declare class MetaValidationError extends Error {
|
|
142
|
+
constructor(message: string);
|
|
143
|
+
}
|