@workerdeck/core 0.6.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tobias Strebitzer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @workerdeck/core
2
+
3
+ The WorkerDeck engines, behind one `Runner` interface: `SessionRunner` wraps the Agent SDK's
4
+ `query()` with a push-based input queue, promotes `canUseTool` calls into pending approvals,
5
+ normalizes SDK messages into wire-protocol events, and keeps a seq-numbered event log for
6
+ attach/replay; `AiSdkRunner` does the same for any provider the AI SDK supports. Pure library, no
7
+ transport.
8
+
9
+ Part of [WorkerDeck](https://github.com/workerdeck/workerdeck). A `SessionRunner`
10
+ behaves like Claude Code launched in the session's directory — same skills, same `CLAUDE.md`, same
11
+ permission system — and both runners emit
12
+ [`@workerdeck/protocol`](https://www.npmjs.com/package/@workerdeck/protocol) events.
13
+ [`@workerdeck/server`](https://www.npmjs.com/package/@workerdeck/server) bridges runners to
14
+ HTTP + WebSocket; use core directly when you want sessions in-process with no server.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @workerdeck/core
20
+ ```
21
+
22
+ Depends on `@anthropic-ai/claude-agent-sdk`, which spawns the official Claude Code CLI. Needs
23
+ Node ≥ 22 and a real filesystem. WorkerDeck implements no Anthropic auth: the SDK/CLI resolves
24
+ credentials from the operator's environment (`ANTHROPIC_API_KEY`, Bedrock/Vertex, or a personal
25
+ `claude login`).
26
+
27
+ The provider engine additionally wants `ai` (AI SDK v7), your provider package, and — for
28
+ `eval_script` — [`@workerdeck/sandbox`](https://www.npmjs.com/package/@workerdeck/sandbox);
29
+ all optional, and unused if you only run Claude sessions.
30
+
31
+ ## Usage
32
+
33
+ `SessionRunnerConfig` is a protocol `CreateSessionRequest` plus host-side extras (`env`,
34
+ `extraOptions`, `defaultApprovalTimeoutMs`, injectable `queryFn`/`historyFn` for tests):
35
+
36
+ ```ts
37
+ import { SessionRunner } from '@workerdeck/core'
38
+
39
+ const runner = new SessionRunner({
40
+ cwd: '/srv/checkouts/my-repo',
41
+ prompt: 'Summarize the failing tests', // or a skill invocation like '/verify-content 42'
42
+ settingSources: ['user', 'project'], // pick up the repo's skills + CLAUDE.md
43
+ permissionMode: 'default',
44
+ })
45
+
46
+ const unsubscribe = runner.subscribe((event) => {
47
+ switch (event.type) {
48
+ case 'assistant_message':
49
+ console.log(event.message)
50
+ break
51
+ case 'permission_requested':
52
+ // Blocks the tool until resolved; denied on timeout (default 5 minutes).
53
+ runner.resolvePermission(event.request.id, { behavior: 'allow' })
54
+ break
55
+ }
56
+ })
57
+
58
+ const done = runner.start() // idempotent; resolves when the query ends
59
+ runner.sendMessage('Now fix the flakiest one') // queues the next turn
60
+ await done
61
+ ```
62
+
63
+ Other controls: `interrupt()`, `setPermissionMode(mode)`, `setModel(model?)`, `close(reason?)`,
64
+ `fail(message)` for host-enforced policy, and `info()` for a protocol `SessionInfo` snapshot
65
+ (status, cost, pending approval count, title). `runner.id` is the server-side id;
66
+ `runner.sdkSessionId` is the Agent SDK's — the one you pass back as `resume`.
67
+
68
+ ## Approvals, event log, resume
69
+
70
+ - **Pending approvals** — the runner's `canUseTool` hook turns each uncovered tool call into a
71
+ `permission_requested` event and a `PendingApproval` that blocks the tool until
72
+ `resolvePermission()` (or the timeout) settles it. Allowing echoes the tool input back as
73
+ `updatedInput` — the SDK requires a record even for an unmodified allow. `AskUserQuestion`
74
+ rides the same path; `questionBehavior: 'auto' | 'deny'` policy-resolves it for unattended runs.
75
+ - **Event log** — every event gets a monotonic `seq`; `subscribe(listener, afterSeq)` replays the
76
+ buffer past `afterSeq` before delivering live events, so late attachers always catch up.
77
+ - **Resume** — pass `resume: sdkSessionId` (optionally `forkSession`). The SDK only re-streams
78
+ user messages, so the runner backfills the full prior transcript from the SDK's on-disk store
79
+ as `replay: true` events before the query starts (`backfillHistory: false` to skip).
80
+ - **Capabilities + usage** — after init (and eagerly for promptless sessions) the runner fetches
81
+ supported models/slash commands and a context-window snapshot, emitting `capabilities` and
82
+ `context_usage` events; context usage is re-polled after every turn.
83
+
84
+ ## The second engine
85
+
86
+ `AiSdkRunner` runs the same protocol against any provider the [AI SDK](https://ai-sdk.dev)
87
+ supports — no CLI process, no config directory. `createEngineSession()` assembles one: the model,
88
+ the capability-scoped tool set, and the executor that runs tool calls.
89
+
90
+ ```ts
91
+ import { createEngineSession, QuickJsExecutor } from '@workerdeck/core'
92
+
93
+ const runner = createEngineSession({
94
+ config: { ...createSessionRequest, languageModel: anthropic('claude-sonnet-5') },
95
+ selectExecutor: () => new QuickJsExecutor({ timeoutMs: 15_000 }),
96
+ capabilities: { webFetch: {} }, // backends, not grants
97
+ })
98
+ ```
99
+
100
+ Two seams matter here:
101
+
102
+ - **Capabilities are grants, wired separately from backends.** `createToolContext` builds the tool
103
+ set from what a profile grants (`fs_*`, `eval_script`, `web_search`, `download`, `web_fetch`,
104
+ `deliver_file`) over what the host actually wired. There is no shell and no host filesystem: the
105
+ files a session sees are an in-memory scratch VFS. Every tool is typed `sandboxed` or
106
+ `authoritative`, and only sandboxed calls may leave the server.
107
+ - **`ToolExecutor` decides where code runs.** `QuickJsExecutor` runs it in-process in the
108
+ [QuickJS guest](https://www.npmjs.com/package/@workerdeck/sandbox); `BrowserBridgeExecutor`
109
+ ships it to the user's own tab, so client-held documents never reach the server; and
110
+ `DeferredExecutor` hands the call off to something that will answer later.
111
+
112
+ ## Work that outlives the runner
113
+
114
+ `DeferredExecutor` dispatches a call and doesn't wait. The runner then **parks**: `park()` returns
115
+ a `RunnerSnapshot`, the process can tear the runner down, and passing that snapshot back as
116
+ `restore` rebuilds the session as itself — same id, same event log, same seq numbering, mid-turn,
117
+ scratch filesystem included.
118
+
119
+ ```ts
120
+ selectExecutor: () => new DeferredExecutor({
121
+ timeoutMs: 86_400_000, // watchdog; a timeout reaches the agent as tool output
122
+ onDispatch: (call) => enqueueForYourWorkers(call), // call.executionId is the callback address
123
+ })
124
+ ```
125
+
126
+ [`@workerdeck/server`](https://www.npmjs.com/package/@workerdeck/server) drives both halves
127
+ for you — a `SessionStore` plus `POST /executions/:id/result` — but the mechanism is here, and works
128
+ with no server at all.
129
+
130
+ ## Also exported
131
+
132
+ `InputQueue` (the push-based `AsyncIterable` bridging `sendMessage()` into the SDK's streaming
133
+ prompt), `normalizeSdkMessage`/`toApiMessage` (SDKMessage → protocol event normalization),
134
+ `connectMcpTools` for live MCP over http/sse, and `createWebFetch` with its SSRF guard
135
+ (`isPrivateAddress`). Tests inject a fake `queryFn` — no real CLI spawn needed.
136
+
137
+ ## License
138
+
139
+ MIT © Tobias Strebitzer —
140
+ [LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE)