@theokit/sdk 1.7.0 → 1.8.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/CHANGELOG.md +6 -0
- package/bin/init-claude.mjs +34 -0
- package/claude-template/AGENTS.md +139 -0
- package/claude-template/CLAUDE.md +51 -0
- package/claude-template/dot-claude/rules/theokit-conventions.md +33 -0
- package/claude-template/dot-claude/settings.json +16 -0
- package/claude-template/dot-claude/skills/theokit-agent-core/SKILL.md +209 -0
- package/claude-template/dot-claude/skills/theokit-budget/SKILL.md +176 -0
- package/claude-template/dot-claude/skills/theokit-config/SKILL.md +139 -0
- package/claude-template/dot-claude/skills/theokit-cron/SKILL.md +148 -0
- package/claude-template/dot-claude/skills/theokit-di/SKILL.md +233 -0
- package/claude-template/dot-claude/skills/theokit-di-agent/SKILL.md +294 -0
- package/claude-template/dot-claude/skills/theokit-errors/SKILL.md +172 -0
- package/claude-template/dot-claude/skills/theokit-eval/SKILL.md +144 -0
- package/claude-template/dot-claude/skills/theokit-gateways/SKILL.md +209 -0
- package/claude-template/dot-claude/skills/theokit-memory/SKILL.md +176 -0
- package/claude-template/dot-claude/skills/theokit-rag/SKILL.md +226 -0
- package/claude-template/dot-claude/skills/theokit-streaming/SKILL.md +156 -0
- package/claude-template/dot-claude/skills/theokit-subscriptions/SKILL.md +148 -0
- package/claude-template/dot-claude/skills/theokit-tools/SKILL.md +170 -0
- package/claude-template/dot-claude/skills/theokit-workflows/SKILL.md +218 -0
- package/package.json +3 -1
|
@@ -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.
|