@theokit/sdk 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*stream*"
5
+ - "**/*Stream*"
6
+ - "**/*SDKMessage*"
7
+ description: TheoKit SDK streaming reference — Run.stream(), SDKMessage union, streamObject, generateObject
8
+ ---
9
+
10
+ # TheoKit Streaming
11
+
12
+ ## `Run.stream()` — SDKMessage events
13
+
14
+ ```typescript
15
+ const run = await agent.send("Find the bug in src/auth.ts");
16
+ for await (const event of run.stream()) {
17
+ switch (event.type) {
18
+ case "assistant":
19
+ for (const block of event.message.content) {
20
+ if (block.type === "text") process.stdout.write(block.text);
21
+ }
22
+ break;
23
+ case "thinking":
24
+ process.stdout.write(event.text);
25
+ break;
26
+ case "tool_call":
27
+ console.log(`[tool] ${event.name}: ${event.status}`);
28
+ break;
29
+ case "status":
30
+ console.log(`[status] ${event.status}`);
31
+ break;
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## SDKMessage discriminated union
37
+
38
+ ```typescript
39
+ type SDKMessage =
40
+ | SDKSystemMessage // "system" — init metadata, emitted once
41
+ | SDKUserMessageEvent // "user" — echo of user prompt
42
+ | SDKAssistantMessage // "assistant" — model text output
43
+ | SDKThinkingMessage // "thinking" — reasoning content
44
+ | SDKToolUseMessage // "tool_call" — tool invocation lifecycle
45
+ | SDKStatusMessage // "status" — cloud run transitions
46
+ | SDKTaskMessage // "task" — task milestones/summaries
47
+ | SDKRequestMessage; // "request" — awaiting user input
48
+ ```
49
+
50
+ All events include `agent_id` and `run_id`.
51
+
52
+ ### Key message types
53
+
54
+ | Type | Key fields |
55
+ |---|---|
56
+ | `"system"` | `subtype?: "init"`, `model?`, `tools?` |
57
+ | `"assistant"` | `message.content: (TextBlock \| ToolUseBlock)[]` |
58
+ | `"thinking"` | `text`, `thinking_duration_ms?` |
59
+ | `"tool_call"` | `call_id`, `name`, `status`, `args?`, `result?`, `truncated?` |
60
+ | `"status"` | `status: "CREATING" \| "RUNNING" \| "FINISHED" \| ...` |
61
+
62
+ `tool_call` is emitted twice: once with `status: "running"` + `args`, then
63
+ again on completion with `status: "completed"` or `"error"` + `result`.
64
+
65
+ ## Raw deltas — `onDelta` callback
66
+
67
+ For per-token streaming, pass `onDelta` to `agent.send()`:
68
+
69
+ ```typescript
70
+ const run = await agent.send("Refactor the utils module", {
71
+ onDelta: ({ update }) => {
72
+ if (update.type === "text-delta") process.stdout.write(update.text);
73
+ if (update.type === "thinking-delta") process.stdout.write(update.text);
74
+ },
75
+ onStep: ({ step }) => {
76
+ console.log(`[step] ${step.type}`);
77
+ },
78
+ });
79
+ ```
80
+
81
+ ### InteractionUpdate types
82
+
83
+ `text-delta`, `thinking-delta`, `thinking-completed`, `tool-call-started`,
84
+ `tool-call-completed`, `partial-tool-call`, `token-delta`, `step-started`,
85
+ `step-completed`, `turn-ended`, `summary`, `shell-output-delta`.
86
+
87
+ `turn-ended` includes token usage:
88
+ ```typescript
89
+ { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }
90
+ ```
91
+
92
+ ## `Agent.generateObject()` — structured output
93
+
94
+ ```typescript
95
+ import { z } from "zod";
96
+ import { Agent } from "@theokit/sdk";
97
+
98
+ const { object, raw, usage, finishReason } = await Agent.generateObject({
99
+ schema: z.object({
100
+ title: z.string().min(1),
101
+ summary: z.string(),
102
+ year: z.number().nullable(),
103
+ }),
104
+ prompt: "Produce a fact card about: Brazilian samba.",
105
+ model: { id: "google/gemini-2.0-flash-001" },
106
+ local: { cwd: process.cwd(), sandboxOptions: { enabled: false } },
107
+ apiKey: process.env.THEOKIT_API_KEY,
108
+ maxRetries: 1,
109
+ });
110
+ // object is fully typed: z.infer<typeof schema>
111
+ ```
112
+
113
+ Throws `GenerateObjectError` with `code: "no_tool_call" | "parse_failed"`.
114
+
115
+ ## `Agent.streamObject()` — streaming structured output (v1.2+)
116
+
117
+ ```typescript
118
+ for await (const evt of Agent.streamObject({
119
+ schema: FactCard,
120
+ prompt: "Produce a fact card about: jazz music.",
121
+ model: { id: "google/gemini-2.0-flash-001" },
122
+ apiKey: process.env.THEOKIT_API_KEY,
123
+ local: { cwd: process.cwd() },
124
+ })) {
125
+ if (evt.type === "partial") render(evt.partial);
126
+ if (evt.type === "complete") finalize(evt.object);
127
+ }
128
+ ```
129
+
130
+ ### StreamObjectEvent
131
+
132
+ ```typescript
133
+ type StreamObjectEvent<T> =
134
+ | { type: "partial"; partial: DeepPartial<T>; attempt: number }
135
+ | { type: "complete"; object: T; raw: unknown; usage; finishReason };
136
+ ```
137
+
138
+ The `complete` event always fires (or the iterator throws `StreamObjectError`).
139
+ Partials are best-effort.
140
+
141
+ ## Waiting without streaming
142
+
143
+ ```typescript
144
+ const result = await run.wait();
145
+ console.log(result.status); // "finished" | "error" | "cancelled"
146
+ console.log(result.result); // final assistant text
147
+ console.log(result.durationMs);
148
+ console.log(result.git); // cloud: { branches: [{ repoUrl, branch?, prUrl? }] }
149
+ ```
150
+
151
+ ## Cancelling
152
+
153
+ ```typescript
154
+ await run.cancel();
155
+ // status moves to "cancelled", partial output preserved
156
+ ```
@@ -0,0 +1,148 @@
1
+ ---
2
+ user-invocable: false
3
+ paths:
4
+ - "**/*subscri*"
5
+ - "**/*sse*"
6
+ - "**/*websocket*"
7
+ - "**/*ws.*"
8
+ description: TheoKit SDK Subscriptions API — defineSubscription, SSE/WebSocket transport, subscribe, tracked, resume tokens
9
+ ---
10
+
11
+ # TheoKit Subscriptions
12
+
13
+ Typed WebSocket + W3C SSE subscriptions with opaque resume tokens. Available
14
+ via the `@theokit/sdk/subscription` sub-path import (not on the main barrel).
15
+
16
+ ## Server side — `defineSubscription`
17
+
18
+ ```typescript
19
+ import { defineSubscription } from "@theokit/sdk/subscription";
20
+ import { z } from "zod";
21
+
22
+ export default defineSubscription({
23
+ input: z.object({
24
+ room: z.string(),
25
+ lastEventId: z.string().optional(),
26
+ }),
27
+ output: z.object({
28
+ id: z.string(),
29
+ text: z.string(),
30
+ sender: z.string(),
31
+ ts: z.number(),
32
+ }),
33
+ async *handler(input, ctx) {
34
+ let cursor = input.lastEventId ?? "0";
35
+ while (!ctx.signal.aborted) {
36
+ const msgs = await fetchNewMessages(input.room, cursor);
37
+ for (const m of msgs) {
38
+ cursor = m.id;
39
+ yield ctx.tracked(m.id, {
40
+ id: m.id,
41
+ text: m.text,
42
+ sender: m.sender,
43
+ ts: m.ts,
44
+ });
45
+ }
46
+ await sleep(1000);
47
+ }
48
+ },
49
+ });
50
+ ```
51
+
52
+ ### `ctx.tracked(id, payload)`
53
+
54
+ Advertises a resume token alongside the payload. The client receives the token
55
+ and echoes it back on reconnect via `lastEventId`. The token is **opaque to
56
+ the SDK** — the server handler decides its semantics:
57
+
58
+ - Monotonic int: `"42"` — resume after event 42
59
+ - ULID: `"01H9X..."` — resume after that ULID
60
+ - Encrypted cursor: consumer decrypts + decodes
61
+ - Timestamp: `"2026-06-04T15:00:00Z"` — resume after that moment
62
+
63
+ ## Client side — `subscribe`
64
+
65
+ ```typescript
66
+ import { subscribe } from "@theokit/sdk/subscription";
67
+
68
+ for await (const msg of subscribe<
69
+ { room: string },
70
+ { id: string; text: string; sender: string; ts: number }
71
+ >(
72
+ "chat",
73
+ { room: "lobby" },
74
+ {
75
+ baseUrl: "http://localhost:3000",
76
+ transport: "auto", // 'ws' | 'sse' | 'auto' (default)
77
+ maxReconnectAttempts: 10, // 0 disables reconnect
78
+ },
79
+ )) {
80
+ console.log(`[${msg.sender}] ${msg.text}`);
81
+ }
82
+ ```
83
+
84
+ ## Transport selection
85
+
86
+ | Mode | When to use |
87
+ |---|---|
88
+ | `'auto'` (default) | Prefer WS, fall back to SSE — works everywhere |
89
+ | `'ws'` | Strict bidirectional — error if WS unavailable |
90
+ | `'sse'` | Browser-native EventSource, no upgrade required |
91
+
92
+ ## Composing with LLM streaming
93
+
94
+ `Agent.streamObject` and `defineSubscription` are independent surfaces. Call
95
+ `Agent.streamObject` inside a subscription handler:
96
+
97
+ ```typescript
98
+ export default defineSubscription({
99
+ input: z.object({ topic: z.string() }),
100
+ output: z.object({ kind: z.enum(["partial", "complete"]), text: z.string() }),
101
+ async *handler(input, ctx) {
102
+ let counter = 0;
103
+ const iter = Agent.streamObject({
104
+ schema: z.object({ text: z.string() }),
105
+ prompt: `Write a haiku about ${input.topic}`,
106
+ model: { id: "openrouter/openai/gpt-4o-mini" },
107
+ apiKey: process.env.OPENROUTER_API_KEY,
108
+ local: { settingSources: [] },
109
+ });
110
+ for await (const evt of iter) {
111
+ if (evt.type === "partial") {
112
+ yield ctx.tracked(String(++counter), {
113
+ kind: "partial",
114
+ text: JSON.stringify(evt.partial),
115
+ });
116
+ } else if (evt.type === "complete") {
117
+ yield ctx.tracked(String(++counter), {
118
+ kind: "complete",
119
+ text: evt.object.text,
120
+ });
121
+ }
122
+ }
123
+ },
124
+ });
125
+ ```
126
+
127
+ ## Multi-runtime support
128
+
129
+ | Runtime | v1.7.0 | v1.8.x (planned) |
130
+ |---|---|---|
131
+ | Node 22+ | Yes (`ws` optional peer) | Yes |
132
+ | Cloudflare Workers | Consumer-supplied `WsAdapter` only | `@theokit/sdk-ws-cloudflare` |
133
+ | Bun | Consumer-supplied `WsAdapter` only | `@theokit/sdk-ws-bun` |
134
+ | Deno | Consumer-supplied `WsAdapter` only | `@theokit/sdk-ws-deno` |
135
+
136
+ ## Security checklist
137
+
138
+ 1. Authenticate WS upgrade via the request object — auth runs BEFORE upgrade
139
+ 2. Validate input via Zod schema (done by SDK automatically)
140
+ 3. Bind resume tokens to session when token leakage allows replay
141
+ 4. Force-close on session revocation via `ctx.disconnect(code, reason)`
142
+ 5. Never log payloads — telemetry captures `{subscriptionName, lastEventId}` only
143
+
144
+ ## Why sub-path import?
145
+
146
+ `@theokit/sdk/subscription` is a dedicated entry point to isolate the
147
+ subscription module from the main `index.ts` DTS bundle. Once the internal
148
+ rollup-dts cycle is resolved, `Theokit.subscribe` can be promoted additively.
@@ -0,0 +1,170 @@
1
+ ---
2
+ user-invocable: false
3
+ description: Custom tools, defineTool with Zod schemas, and built-in coding tools for @theokit/sdk.
4
+ paths:
5
+ - "**/*tool*"
6
+ - "**/*Tool*"
7
+ ---
8
+
9
+ # TheoKit SDK -- Tools
10
+
11
+ Quick reference for custom inline tools and built-in coding tools.
12
+
13
+ ## defineTool (type-safe builder)
14
+
15
+ ```typescript
16
+ import { z } from "zod";
17
+ import { defineTool } from "@theokit/sdk";
18
+
19
+ const rollTool = defineTool({
20
+ name: "roll",
21
+ description: "Roll N dice with S sides each.",
22
+ inputSchema: z.object({
23
+ count: z.number().int().min(1).max(100),
24
+ sides: z.number().int().min(2).max(1000),
25
+ }),
26
+ handler: ({ count, sides }) => {
27
+ const rolls = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * sides));
28
+ return JSON.stringify({ rolls, total: rolls.reduce((a, b) => a + b, 0) });
29
+ },
30
+ });
31
+ ```
32
+
33
+ Requires `zod` as a peer dependency. Converts Zod schema to JSON Schema for the LLM. Runtime `schema.parse` validates input before the handler runs. Invalid input becomes `tool_result(isError)` with a Zod message.
34
+
35
+ ### DefineToolSpec
36
+
37
+ ```typescript
38
+ interface DefineToolSpec<T extends ZodType> {
39
+ name: string;
40
+ description: string;
41
+ inputSchema: T;
42
+ handler: (input: z.infer<T>) => string | Promise<string>;
43
+ }
44
+ ```
45
+
46
+ ## CustomTool (raw interface)
47
+
48
+ ```typescript
49
+ interface CustomTool {
50
+ name: string; // /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/
51
+ description: string;
52
+ inputSchema: Record<string, unknown>; // JSON Schema, type: "object"
53
+ handler: (input: Record<string, unknown>) => string | Promise<string>;
54
+ }
55
+ ```
56
+
57
+ ### Reserved names (rejected at create time)
58
+
59
+ `shell`, `memory_search`, `memory_get`, anything prefixed with `mcp_`.
60
+
61
+ ### Constraints
62
+
63
+ - **Local runtime only.** Cloud agents throw `ConfigurationError(code: "cloud_custom_tools_rejected")` when `tools.length > 0`.
64
+ - **Not persisted.** Handlers are in-memory closures. Re-pass tools on `Agent.resume`.
65
+
66
+ ## Registering tools with agents
67
+
68
+ ```typescript
69
+ const agent = await Agent.create({
70
+ apiKey: process.env.THEOKIT_API_KEY!,
71
+ model: { id: "google/gemini-2.0-flash-001" },
72
+ local: { cwd: process.cwd() },
73
+ tools: [rollTool, lookupTool],
74
+ });
75
+ ```
76
+
77
+ ### Per-send tool override
78
+
79
+ ```typescript
80
+ await agent.send("Use only the calculator.", {
81
+ tools: [calculatorTool], // fully replaces agent-level tools for this run
82
+ });
83
+ // tools: undefined -> fall back to agent tools
84
+ // tools: [] -> no custom tools for this run
85
+ ```
86
+
87
+ ## Built-in coding tools (`@theokit/sdk/tools`)
88
+
89
+ Drop-in toolkit for coding agents. All tools are project-scoped and refuse sensitive files.
90
+
91
+ ```typescript
92
+ import { createAgentFactory } from "@theokit/sdk";
93
+ import {
94
+ createReadFileTool,
95
+ createListDirTool,
96
+ createSearchTextTool,
97
+ createGitDiffTool,
98
+ createRunVitestTool,
99
+ } from "@theokit/sdk/tools";
100
+
101
+ const projectRoot = process.cwd();
102
+ const factory = createAgentFactory({
103
+ apiKey: process.env.ANTHROPIC_API_KEY!,
104
+ model: { id: "claude-sonnet-4-6" },
105
+ tools: [
106
+ createReadFileTool({ projectRoot }),
107
+ createListDirTool({ projectRoot }),
108
+ createSearchTextTool({ projectRoot, maxMatches: 100 }),
109
+ createGitDiffTool({ projectRoot, timeoutMs: 30_000 }),
110
+ createRunVitestTool({ projectRoot, timeoutMs: 120_000 }),
111
+ ],
112
+ });
113
+ ```
114
+
115
+ ### Tool reference
116
+
117
+ | Tool | Returns on success | Error codes |
118
+ |---|---|---|
119
+ | `read_file` | `{ ok, content, size }` | `path_traversal`, `forbidden_path`, `binary_file`, `not_found`, `too_large` |
120
+ | `list_dir` | `{ ok, entries: [{ name, type }], truncated, totalCount }` | `path_traversal`, `forbidden_path` |
121
+ | `search_text` | `{ ok, matches: [{ file, line, preview }], truncated }` | `path_traversal`, `forbidden_path` |
122
+ | `git_diff` | `{ ok, diff, truncated }` | `not_a_repo`, `timeout`, `git_failed`, `path_traversal` |
123
+ | `run_vitest` | `{ ok, summary: { numTotalTests, numPassedTests, numFailedTests, success } }` | `no_vitest`, `timeout`, `unparseable_output`, `path_traversal` |
124
+
125
+ ### Safety rules (shared across all 5 tools)
126
+
127
+ 1. Every I/O call passes through `safePathJoin` + `assertNoSymlinkEscape`.
128
+ 2. Sensitive files refused: `.env*` (except `.env.example`), `.git/`, `node_modules/`, `.theo/`, lock files.
129
+ 3. Handlers return JSON strings; never throw on user mistakes.
130
+
131
+ ### Hardening
132
+
133
+ - `read_file`: rejects binary files via null-byte detection in first 8 KB. Caps at 5 MB.
134
+ - `list_dir`: caps at 500 entries by default (override via `max`).
135
+ - `search_text`: skips binary files and files > 1 MB.
136
+ - `git_diff` / `run_vitest`: spawn detached process groups; on timeout kill the whole group.
137
+
138
+ ## Tool lifecycle hooks
139
+
140
+ ```typescript
141
+ const agent = await Agent.create({
142
+ apiKey, model,
143
+ onToolStart: ({ toolName, callId, args, conversationId }) => { /* ... */ },
144
+ onToolEnd: ({ toolName, callId, durationMs, result }) => { /* ... */ },
145
+ onToolError: ({ toolName, callId, error, durationMs, attempt }) => { /* ... */ },
146
+ });
147
+ ```
148
+
149
+ - `callId` correlates start/end pairs.
150
+ - Hook errors are swallowed -- listener bugs do NOT crash the agent run.
151
+
152
+ ## Tool stream events
153
+
154
+ Tool calls emit `SDKToolUseMessage` (type `"tool_call"`) twice: once with `status: "running"` and `args`, then with `status: "completed"` (or `"error"`) and `result`. The `args` and `result` payloads are unstable -- parse defensively.
155
+
156
+ ## Path safety utilities (`@theokit/sdk/path-safety`)
157
+
158
+ ```typescript
159
+ import {
160
+ safePathJoin,
161
+ assertNoSymlinkEscape,
162
+ isForbiddenPath,
163
+ PathTraversalError,
164
+ ForbiddenPathError,
165
+ } from "@theokit/sdk/path-safety";
166
+
167
+ const safe = safePathJoin(projectRoot, userPath);
168
+ assertNoSymlinkEscape(safe, projectRoot);
169
+ if (isForbiddenPath(userPath)) throw new ForbiddenPathError(userPath);
170
+ ```
@@ -0,0 +1,218 @@
1
+ ---
2
+ user-invocable: false
3
+ description: Workflow orchestration -- Workflow.create, steps, branching, retry, suspend/resume.
4
+ paths:
5
+ - "**/*workflow*"
6
+ - "**/*Workflow*"
7
+ - "**/*step*"
8
+ ---
9
+
10
+ # TheoKit SDK -- Workflows
11
+
12
+ Quick reference for declarative multi-step orchestration (v1.17+).
13
+
14
+ ## Workflow.create / .run
15
+
16
+ ```typescript
17
+ import { Agent, Workflow, fn, agentStep } from "@theokit/sdk";
18
+
19
+ const classifier = await Agent.create({ /* ... */ });
20
+ const billingExpert = await Agent.create({ /* ... */ });
21
+
22
+ const wf = Workflow.create<{ claim: string }, string>({ name: "refund-pipeline" })
23
+ .then(fn("validate", (input) => {
24
+ if (!input.claim) throw new Error("missing claim");
25
+ return input;
26
+ }))
27
+ .then(agentStep("classify", classifier, (i) => `Classify: ${JSON.stringify(i)}`))
28
+ .branch([
29
+ [(out) => String(out).includes("BILLING"), [agentStep("resolve", billingExpert, "Handle it")]],
30
+ ], { fallback: [fn("escalate", () => "escalated")] })
31
+ .commit();
32
+
33
+ const run = await wf.run({ claim: "I was charged twice" });
34
+ console.log(run.status, run.output);
35
+ ```
36
+
37
+ ## Step types
38
+
39
+ ### fn step (pure function)
40
+
41
+ ```typescript
42
+ import { fn } from "@theokit/sdk";
43
+
44
+ fn("validate", (input, ctx) => {
45
+ // input: previous step's output
46
+ // ctx: { signal, suspend(), stepId }
47
+ return transformedInput;
48
+ }, {
49
+ inputSchema: z.object({ /* ... */ }), // optional Zod validation
50
+ outputSchema: z.object({ /* ... */ }), // optional Zod validation
51
+ retry: {
52
+ maxAttempts: 3,
53
+ initialBackoffMs: 1000,
54
+ backoffCoefficient: 2.0,
55
+ maximumBackoffMs: 30_000,
56
+ nonRetryableErrors: ["ConfigurationError"],
57
+ },
58
+ });
59
+ ```
60
+
61
+ ### agentStep (agent.send wrapper)
62
+
63
+ ```typescript
64
+ import { agentStep } from "@theokit/sdk";
65
+
66
+ agentStep("classify", agent, (input) => `Classify this: ${JSON.stringify(input)}`, {
67
+ retry: { maxAttempts: 2, initialBackoffMs: 2000 },
68
+ });
69
+ // Third arg is a prompt renderer: (stepInput) => string
70
+ ```
71
+
72
+ ## Control-flow primitives
73
+
74
+ ### .then (sequential)
75
+
76
+ ```typescript
77
+ wf.then(fn("a", ...)).then(fn("b", ...))
78
+ ```
79
+
80
+ ### .parallel (fan-out)
81
+
82
+ ```typescript
83
+ wf.parallel([
84
+ [fn("branch-a", ...)],
85
+ [fn("branch-b", ...)],
86
+ ], {
87
+ concurrency: 4,
88
+ errorPolicy: "fail-fast", // or "collect"
89
+ })
90
+ ```
91
+
92
+ ### .branch (first-match routing)
93
+
94
+ ```typescript
95
+ wf.branch([
96
+ [(output) => output.category === "billing", [agentStep("billing", billingAgent, "Handle")]],
97
+ [(output) => output.category === "support", [agentStep("support", supportAgent, "Handle")]],
98
+ ], {
99
+ fallback: [fn("unknown", () => "escalated")],
100
+ })
101
+ ```
102
+
103
+ ### .foreach (map over array)
104
+
105
+ ```typescript
106
+ wf.foreach("source-step-id", fn("process", (item) => transform(item)), {
107
+ concurrency: 4,
108
+ })
109
+ ```
110
+
111
+ ### .dowhile (loop)
112
+
113
+ ```typescript
114
+ wf.dowhile(
115
+ fn("iterate", (input) => { /* ... */ return { done: false, data: input }; }),
116
+ (output) => !output.done,
117
+ { maxIterations: 100 },
118
+ )
119
+ ```
120
+
121
+ ### .sleep
122
+
123
+ ```typescript
124
+ wf.sleep(5000, "wait-for-api") // abortable via signal
125
+ ```
126
+
127
+ ### .suspend (human-in-the-loop)
128
+
129
+ ```typescript
130
+ wf.then(fn("wait_approval", async (input, ctx) => {
131
+ await ctx.suspend({ awaiting: "human-approval", draft: input });
132
+ return "sentinel"; // never reached
133
+ }))
134
+ ```
135
+
136
+ ## Suspend / resume
137
+
138
+ ```typescript
139
+ const first = await wf.run(undefined);
140
+ // first.status === "suspended"
141
+
142
+ // Later, after human approves:
143
+ const resumed = await Workflow.resume({
144
+ runId: first.id,
145
+ workflow: wf,
146
+ payload: { approved: true, by: "manager" },
147
+ });
148
+ // resumed.status === "completed"
149
+ ```
150
+
151
+ ## Persistence
152
+
153
+ | Backend | When | How |
154
+ |---|---|---|
155
+ | `memory` (default) | Same-process suspend/resume | `Workflow.create({ name })` |
156
+ | `json` | Survive process restart | `Workflow.create({ name, persistence: { backend: "json", dir: ".theokit/workflows" } })` |
157
+
158
+ ## Retry policy
159
+
160
+ ```typescript
161
+ retry: {
162
+ maxAttempts: 3, // 1..20, required
163
+ initialBackoffMs: 1000, // default 1000
164
+ backoffCoefficient: 2.0, // default 2.0
165
+ maximumBackoffMs: 30_000, // default 30s
166
+ nonRetryableErrors: ["ConfigurationError", "AbortError"],
167
+ }
168
+ ```
169
+
170
+ Retry sleeps are abortable via `AbortSignal`. Non-retryable errors skip the retry loop.
171
+
172
+ ## Cancellation
173
+
174
+ ```typescript
175
+ const ctrl = new AbortController();
176
+ const promise = wf.run(input, { signal: ctrl.signal });
177
+
178
+ ctrl.abort("user cancelled");
179
+ const run = await promise;
180
+ // run.status === "cancelled"
181
+ ```
182
+
183
+ `AbortSignal` is checked at step boundaries AND mid-backoff sleep. `ctx.signal` is passed to step functions so `fetch` / `agent.send` can be cancelled too.
184
+
185
+ ## WorkflowRun result
186
+
187
+ ```typescript
188
+ interface WorkflowRun<O> {
189
+ id: string;
190
+ status: "completed" | "suspended" | "cancelled" | "failed";
191
+ output?: O;
192
+ error?: Error;
193
+ steps: WorkflowStepResult[];
194
+ }
195
+ ```
196
+
197
+ ## Telemetry
198
+
199
+ When OTel is installed, each `wf.run` emits a `workflow.run` root span and per-step `workflow.step.<id>` child spans with attributes: `workflow.name`, `workflow.run_id`, `step.kind`, `step.attempts`, `step.status`.
200
+
201
+ ## Errors
202
+
203
+ | Error | Cause |
204
+ |---|---|
205
+ | `WorkflowDuplicateStepIdError` | Two steps with same id at `.commit()` |
206
+ | `WorkflowAlreadyRunningError` | Concurrent `.run()` with same `(workflowId, runId)` |
207
+ | `WorkflowSnapshotNotFoundError` | `Workflow.resume(runId)` with unknown runId |
208
+ | `WorkflowMaxIterationsExceededError` | `.dowhile` over `maxIterations` |
209
+ | `WorkflowNotSerializableError` | `ctx.suspend(payload)` with non-JSON value |
210
+ | `WorkflowResumeStepNotFoundError` | Resume against a diverged workflow definition |
211
+ | `WorkflowParallelError` | Aggregate of branch failures in fail-fast mode |
212
+ | `WorkflowCompensateNotImplementedError` | `compensate` field set (saga deferred to v1.2) |
213
+
214
+ ## Limitations (v1)
215
+
216
+ - **LocalAgent only** -- cloud agent steps throw `UnsupportedRunOperationError`.
217
+ - **Saga compensation deferred** -- `compensate?` field reserved but throws if set.
218
+ - **No cron-trigger integration** -- wire via `Cron.create` calling `wf.run` directly.