@zocomputer/agent-sdk 0.5.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 +21 -0
- package/README.md +673 -0
- package/dist/attachments.js +52 -0
- package/dist/gateway-fetch.js +67 -0
- package/dist/index.js +4169 -0
- package/dist/initiator-auth.js +49 -0
- package/dist/platform/agent-sandbox/index.js +691 -0
- package/dist/platform/cloud-tools/image.js +498 -0
- package/dist/platform/cloud-tools/index.js +507 -0
- package/dist/platform/cloud-tools/web-search.js +87 -0
- package/dist/platform/runtime-ai/gateway.js +87 -0
- package/dist/platform/runtime-ai/index.js +91 -0
- package/dist/platform/runtime-ai/register.js +88 -0
- package/dist/platform/runtime-ai/session-fetch.js +54 -0
- package/dist/platform/runtime-auth/index.js +141 -0
- package/dist/state-files.js +287 -0
- package/dist/state-sandbox.js +383 -0
- package/dist/state.js +29 -0
- package/dist/steer-inbox.js +84 -0
- package/dist/steer.js +83 -0
- package/package.json +143 -0
- package/platform/agent-sandbox/api-client.ts +196 -0
- package/platform/agent-sandbox/index.ts +27 -0
- package/platform/agent-sandbox/pure.ts +83 -0
- package/platform/agent-sandbox/sftp.ts +141 -0
- package/platform/agent-sandbox/ssh-connection.ts +165 -0
- package/platform/agent-sandbox/ssh-exec.ts +98 -0
- package/platform/agent-sandbox/ssh-session.ts +487 -0
- package/platform/agent-sandbox/zo-backend.ts +88 -0
- package/platform/agent-sandbox/zo-sandbox.ts +39 -0
- package/platform/cloud-tools/image-path.ts +44 -0
- package/platform/cloud-tools/image.ts +225 -0
- package/platform/cloud-tools/index.ts +22 -0
- package/platform/cloud-tools/state-files.ts +368 -0
- package/platform/cloud-tools/tool-meta.ts +32 -0
- package/platform/cloud-tools/web-search.ts +15 -0
- package/platform/runtime-ai/gateway.ts +76 -0
- package/platform/runtime-ai/index.ts +20 -0
- package/platform/runtime-ai/register.ts +26 -0
- package/platform/runtime-ai/session-fetch.ts +124 -0
- package/platform/runtime-auth/index.ts +331 -0
- package/src/async-tasks.ts +273 -0
- package/src/attachments.ts +109 -0
- package/src/backgroundable.ts +88 -0
- package/src/bounded-output.ts +159 -0
- package/src/dir-conventions.ts +238 -0
- package/src/extract/cache.ts +40 -0
- package/src/extract/docx.ts +18 -0
- package/src/extract/pdf.ts +54 -0
- package/src/extract/sheet.ts +56 -0
- package/src/file-kind.ts +258 -0
- package/src/file-view.ts +80 -0
- package/src/gateway-fetch.ts +115 -0
- package/src/glob-match.ts +13 -0
- package/src/hooks.ts +213 -0
- package/src/index.ts +419 -0
- package/src/initiator-auth.ts +81 -0
- package/src/instructions.ts +224 -0
- package/src/list-files.ts +41 -0
- package/src/mock-model.ts +572 -0
- package/src/park-delivery.ts +247 -0
- package/src/path-locks.ts +52 -0
- package/src/read-file-content.ts +142 -0
- package/src/read-text.ts +40 -0
- package/src/redeliver.ts +155 -0
- package/src/run.ts +159 -0
- package/src/sandbox-io.ts +414 -0
- package/src/state-files.ts +460 -0
- package/src/state-sandbox.ts +710 -0
- package/src/state.ts +96 -0
- package/src/steer-inbox.ts +105 -0
- package/src/steer-tool.ts +83 -0
- package/src/steer.ts +146 -0
- package/src/task.ts +320 -0
- package/src/tools/bash.ts +143 -0
- package/src/tools/edit.ts +58 -0
- package/src/tools/glob.ts +56 -0
- package/src/tools/grep.ts +188 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/tasks.ts +241 -0
- package/src/tools/webfetch.ts +368 -0
- package/src/tools/write.ts +42 -0
- package/src/walk.ts +112 -0
- package/src/watch-output.ts +104 -0
- package/src/web-fetch.ts +324 -0
- package/src/web-page.ts +179 -0
- package/src/workspace-io.ts +225 -0
- package/src/workspace.ts +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
# @zocomputer/agent-sdk
|
|
2
|
+
|
|
3
|
+
A standard library for [eve](https://eve.dev) agents that work on a real
|
|
4
|
+
filesystem: the workspace toolset (`read`, `edit`, `write`, `glob`, `grep`,
|
|
5
|
+
`bash`, `webfetch`), background-task orchestration, and rich-filetype reads
|
|
6
|
+
(PDF, DOCX, spreadsheets), wired in one call.
|
|
7
|
+
|
|
8
|
+
We build [Zo](https://zo.computer), where published cloud agents run on eve —
|
|
9
|
+
this SDK is the toolset we give them, extracted from the coding agent we use
|
|
10
|
+
on our own repo. It's deliberately generic: nothing in it assumes Zo, and every
|
|
11
|
+
tool factory and helper module is exported à la carte for eve projects that
|
|
12
|
+
want a subset.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bun add @zocomputer/agent-sdk@github:zocomputer/agent-sdk#v0.4.0
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Each release is a `v<version>` tag on this repo; pin one. (The npm publish
|
|
21
|
+
isn't bootstrapped yet — until it is, this repo is the registry.)
|
|
22
|
+
|
|
23
|
+
`eve`, `zod`, and `ai` are peer dependencies. Runtime imports load built JS
|
|
24
|
+
from `dist/` (Node won't load raw TS out of `node_modules`); types resolve
|
|
25
|
+
straight from the TypeScript source shipped alongside it.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
eve auto-loads `agent/tools/*.ts` and `agent/instructions/*.ts` by filename —
|
|
30
|
+
the tool file's **name is the wire name** the model sees. So you build the
|
|
31
|
+
stdlib once, then add one tiny re-export file per tool. Steps 1–5 below are the
|
|
32
|
+
full prescription; copy it verbatim and you have the complete toolset.
|
|
33
|
+
|
|
34
|
+
### 1. Build the stdlib once
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// agent/lib/stdlib.ts
|
|
38
|
+
import { createStdlib } from "@zocomputer/agent-sdk";
|
|
39
|
+
|
|
40
|
+
export const stdlib = createStdlib({
|
|
41
|
+
workspaceRoot: process.env.MY_WORKDIR ?? process.cwd(),
|
|
42
|
+
stateDir: ".agent", // tasks.json + spilled tool output — gitignore it
|
|
43
|
+
workspaceNoun: "repo", // what tool descriptions call the workspace
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Re-export each tool as its own file
|
|
48
|
+
|
|
49
|
+
One file per tool; the filename is the name the model calls. Create all of
|
|
50
|
+
these under `agent/tools/`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// agent/tools/read.ts (repeat for edit, write, glob, grep, bash, webfetch)
|
|
54
|
+
import { stdlib } from "../lib/stdlib";
|
|
55
|
+
export default stdlib.tools.read;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| file | export | model sees |
|
|
59
|
+
| ------------- | --------------------- | --------------------- |
|
|
60
|
+
| `read.ts` | `stdlib.tools.read` | `read` |
|
|
61
|
+
| `edit.ts` | `stdlib.tools.edit` | `edit` |
|
|
62
|
+
| `write.ts` | `stdlib.tools.write` | `write` |
|
|
63
|
+
| `glob.ts` | `stdlib.tools.glob` | `glob` |
|
|
64
|
+
| `grep.ts` | `stdlib.tools.grep` | `grep` |
|
|
65
|
+
| `bash.ts` | `stdlib.tools.bash` | `bash` |
|
|
66
|
+
| `webfetch.ts` | `stdlib.tools.webfetch` | `webfetch` |
|
|
67
|
+
| `tasks.ts`\* | `stdlib.tools.tasks` | `run_async`, `check_tasks`, `await_task` |
|
|
68
|
+
|
|
69
|
+
\* The task tools are a **bundle** — one file exports all three, so its own
|
|
70
|
+
filename is free (rib calls it `parallel.ts`):
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// agent/tools/tasks.ts
|
|
74
|
+
import { stdlib } from "../lib/stdlib";
|
|
75
|
+
export default stdlib.tools.tasks; // run_async + check_tasks + await_task
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Vacate the eve built-ins you're replacing
|
|
79
|
+
|
|
80
|
+
eve injects every built-in tool whose name you don't override or disable. The
|
|
81
|
+
rule:
|
|
82
|
+
|
|
83
|
+
- **Same name → automatic override.** `bash.ts` above already replaces eve's
|
|
84
|
+
built-in `bash`; nothing else to do.
|
|
85
|
+
- **Different name → disable the built-in** so the model doesn't see two file
|
|
86
|
+
readers/writers. The stdlib uses the Claude Code / opencode names (`read`,
|
|
87
|
+
`write`), so shim out eve's `read_file` and `write_file`:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// agent/tools/read_file.ts (and agent/tools/write_file.ts)
|
|
91
|
+
import { disableTool } from "eve/tools";
|
|
92
|
+
export default disableTool();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Register the instructions
|
|
96
|
+
|
|
97
|
+
The stdlib ships the operational prose alongside the tools — the workflow,
|
|
98
|
+
communication, and HITL contracts that make a coding agent behave well, not
|
|
99
|
+
just the file operations. One re-export file per instruction under
|
|
100
|
+
`agent/instructions/`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// agent/instructions/workflow.ts — explore→read→edit→verify + the end-of-turn check
|
|
104
|
+
import { stdlib } from "../lib/stdlib";
|
|
105
|
+
export default stdlib.instructions.workflow;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| file | export | teaches |
|
|
109
|
+
| --------------------- | --------------------------------- | --------------------------------------------------- |
|
|
110
|
+
| `workflow.ts` | `stdlib.instructions.workflow` | explore before edit, read before edit, verify, todo tracking, finish before ending the turn |
|
|
111
|
+
| `communication.ts` | `stdlib.instructions.communication` | lead with the outcome, readable over brief, report-don't-fix, act without permission-seeking |
|
|
112
|
+
| `hitl.ts` | `stdlib.instructions.hitl` | the `ask_question` playbook — options, `style: "primary"`, `allowFreeform`, ask independent questions together |
|
|
113
|
+
| `parallel-tools.ts` | `stdlib.instructions.parallelTools` | background tasks, `notify` watchers, await-before-ending |
|
|
114
|
+
| `repo-conventions.ts` | `stdlib.instructions.repoConventions` | injects the workspace's root `AGENTS.md` |
|
|
115
|
+
| `subagents.ts` | `stdlib.instructions.subagents` | delegation with eve's built-in `agent` tool |
|
|
116
|
+
|
|
117
|
+
Persona stays yours: the stdlib ships operational contracts, not voice — write
|
|
118
|
+
your agent's identity as your own instruction file (see the example's
|
|
119
|
+
`coder.ts`).
|
|
120
|
+
|
|
121
|
+
### 5. Register the park-delivery hook
|
|
122
|
+
|
|
123
|
+
One hook file makes `read` media actually reach the model (see
|
|
124
|
+
[Media reads](#media-reads-images-video-audio)) and delivers background-task
|
|
125
|
+
notifications (see [Tool behavior](#tool-behavior)):
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// agent/hooks/park-delivery.ts
|
|
129
|
+
import { createParkDeliveryHook } from "@zocomputer/agent-sdk";
|
|
130
|
+
export default createParkDeliveryHook();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
(If you enable [Steering](#steering-mid-turn-messages), pass the same inbox dir
|
|
134
|
+
here: `createParkDeliveryHook({ steer: { dir } })`.)
|
|
135
|
+
|
|
136
|
+
That's the whole setup. Everything is also exported à la carte
|
|
137
|
+
(`createReadTool`, `createCommandRunner`, …) if you'd rather compose a subset.
|
|
138
|
+
|
|
139
|
+
### 6. Optional: declare model-tier task subagents
|
|
140
|
+
|
|
141
|
+
A generic delegation worker: a full-capability copy of your agent pinned to a
|
|
142
|
+
model the *caller* chooses. Eve has no per-call model parameter — a subagent
|
|
143
|
+
tool's input is fixed at `{ message, outputSchema? }` and its model compiles
|
|
144
|
+
from its `agent.ts` — so the model knob is **one declared subagent per tier**
|
|
145
|
+
(`task_fast`, `task_deep`, …): the parent picks a model by picking a tool, and
|
|
146
|
+
each tool's description carries that model's identity and routing guidance.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// agent/subagents/task_fast/agent.ts — the tier's identity + pinned model
|
|
150
|
+
import { createTaskAgent } from "@zocomputer/agent-sdk";
|
|
151
|
+
export default createTaskAgent({
|
|
152
|
+
model: "anthropic/claude-sonnet-5",
|
|
153
|
+
modelName: "Claude Sonnet 5",
|
|
154
|
+
modelBlurb: "…", // the model's catalog description, checked in (see below)
|
|
155
|
+
use: "Prefer it for quick, well-scoped subtasks — exploration, focused questions, mechanical edits — where a fast, cheap model is enough.",
|
|
156
|
+
workspaceNoun: "repo",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// agent/subagents/task_fast/instructions/task.ts — the child's operating contract
|
|
160
|
+
import { createTaskInstruction } from "@zocomputer/agent-sdk";
|
|
161
|
+
export default createTaskInstruction({ workspaceNoun: "repo" });
|
|
162
|
+
|
|
163
|
+
// agent/subagents/task_fast/tools/bash.ts — one re-export per PARENT tool
|
|
164
|
+
export { default } from "../../../tools/bash";
|
|
165
|
+
|
|
166
|
+
// agent/subagents/task_fast/tools/read.ts — EXCEPT read/webfetch, which use
|
|
167
|
+
// attach-disabled child instances: no park-delivery hook runs in a child, so
|
|
168
|
+
// the parent's attachment-enabled tools would promise media that never arrives
|
|
169
|
+
import { taskChildTools } from "../lib/child-tools"; // your createTaskChildTools(...) instance
|
|
170
|
+
export default taskChildTools.read;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**The critical part: a declared subagent inherits nothing from the root.** An
|
|
174
|
+
absent `tools/` slot falls back to eve's *framework defaults*, not your
|
|
175
|
+
authored tools — so "same tools as the parent" must be constructed: one
|
|
176
|
+
re-export file per parent tool (parent disable shims included), minus any
|
|
177
|
+
parent-session-coupled tools you exclude, plus a `disableTool()` shim per
|
|
178
|
+
`TASK_DISABLED_BUILTINS` entry (just `ask_question`: a parked child parks the
|
|
179
|
+
parent's turn, so the task contract is decide-and-report). Do **not** shim the
|
|
180
|
+
`agent` clone tool: eve injects it at the harness layer rather than as a
|
|
181
|
+
framework tool, so a shim for it fails runtime agent-graph resolution and
|
|
182
|
+
breaks every session; the task instruction bounds onward delegation instead
|
|
183
|
+
(see the maintainers notes below):
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// agent/subagents/task_fast/tools/ask_question.ts
|
|
187
|
+
import { disableTool } from "eve/tools";
|
|
188
|
+
export default disableTool();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Add a test that diffs each tier's `tools/` directory against
|
|
192
|
+
`expectedTaskToolNames({ parentToolNames, excludedParentTools })`, so a parent
|
|
193
|
+
tool added without a re-export (or a forgotten shim) fails CI instead of
|
|
194
|
+
shipping a child whose tool surface differs from what its description says.
|
|
195
|
+
|
|
196
|
+
Model blurbs come from the AI Gateway model catalog — the same public catalog
|
|
197
|
+
the AI SDK's `gateway.getAvailableModels()` reads — via
|
|
198
|
+
`fetchGatewayModelCatalog()` in a **one-shot refresh script**, and are checked
|
|
199
|
+
in. Never fetch them at agent build time: tool descriptions are part of the
|
|
200
|
+
cached prompt prefix and must be static and offline-safe.
|
|
201
|
+
|
|
202
|
+
Finally, tell the parent when to route to each tier — pass a roster to the
|
|
203
|
+
stdlib and the `subagents` instruction grows a "Choosing a subagent" section:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const stdlib = createStdlib({
|
|
207
|
+
// …
|
|
208
|
+
subagentRoster: [
|
|
209
|
+
{ name: "task_fast", when: "quick, well-scoped subtasks on a fast, cheap model" },
|
|
210
|
+
{ name: "task_deep", when: "reasoning-heavy subtasks worth frontier-model cost" },
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Instructions aren't inherited either — re-export the stdlib instructions the
|
|
216
|
+
child needs (`repoConventions`, `workflow`, `parallelTools`) beside the task
|
|
217
|
+
contract. Same for hooks: if your agent logs sessions via a hook, re-export it
|
|
218
|
+
under `agent/subagents/task_fast/hooks/` or child sessions won't be recorded.
|
|
219
|
+
|
|
220
|
+
## Example
|
|
221
|
+
|
|
222
|
+
[`examples/coder`](./examples/coder) is a complete, minimal eve coding agent
|
|
223
|
+
built on this stdlib — the six steps above as real files: the full toolset,
|
|
224
|
+
the `read_file`/`write_file` shims, the six instructions, the park-delivery
|
|
225
|
+
hook, a `task_fast` model-tier subagent, and a one-file coder persona. Point
|
|
226
|
+
it at a project and run it:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
cd examples/coder
|
|
230
|
+
bun install
|
|
231
|
+
CODER_WORKDIR=/path/to/project AI_GATEWAY_API_KEY=… bun dev
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The coder is also this package's **end-to-end test agent**: `bun run eval`
|
|
235
|
+
(in `examples/coder`) runs [`evals-mock/`](./examples/coder/evals-mock) — ten
|
|
236
|
+
deterministic evals that drive the prescribed wiring through a real eve server
|
|
237
|
+
on the mock model (see [Mock model](#mock-model-credential-free-testing)),
|
|
238
|
+
with zero credentials. Park/resume on `ask_question`, two parallel questions
|
|
239
|
+
pending on one park, the todo write/update order, real task_fast delegation, a
|
|
240
|
+
visible `turn.failed` on an injected stream error, and the stream-shape
|
|
241
|
+
scenarios. Copy the pattern (`scripts/eval.ts` + `evals-mock/`) to give your
|
|
242
|
+
own agent the same CI-friendly suite.
|
|
243
|
+
|
|
244
|
+
## Mock model (credential-free testing)
|
|
245
|
+
|
|
246
|
+
`createMockStoryModel()` is a scripted `LanguageModelV4` that turns the whole
|
|
247
|
+
eve stack into a deterministic test rig: session routes, the harness, framework
|
|
248
|
+
tools (`ask_question`, `todo`), declared subagents, and durable streams all run
|
|
249
|
+
REAL — only inference is canned. Gate it behind an env flag in `agent.ts` and
|
|
250
|
+
never set that flag in a normal run:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// agent/agent.ts
|
|
254
|
+
import { defineAgent } from "eve";
|
|
255
|
+
import { createMockStoryModel } from "@zocomputer/agent-sdk";
|
|
256
|
+
|
|
257
|
+
export default function agent() {
|
|
258
|
+
if (process.env.MY_AGENT_MOCK_MODEL === "1") {
|
|
259
|
+
return defineAgent({ model: createMockStoryModel() });
|
|
260
|
+
}
|
|
261
|
+
return defineAgent({ model: "anthropic/claude-opus-4.8" });
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
(The coder example wires this as `CODER_MOCK_MODEL=1`; rib as
|
|
266
|
+
`RIB_MOCK_MODEL=1`.)
|
|
267
|
+
|
|
268
|
+
A turn with no directive streams a long, paced deterministic story — a turn
|
|
269
|
+
that stays in-flight exactly as long as your test needs (`chunkCount` ×
|
|
270
|
+
`chunkDelayMs`), with the asking prompt echoed into the output so parallel
|
|
271
|
+
chats stay distinguishable. A `[mock:<scenario>]` directive in the user
|
|
272
|
+
message scripts the turn instead:
|
|
273
|
+
|
|
274
|
+
| Directive | What it drives |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| `[mock:hitl]` | One `ask_question` call (styled options + freeform) → park → answer → wrap-up. |
|
|
277
|
+
| `[mock:parallel]` | TWO `ask_question` calls in one response — both pend on a single park; one respond resumes. |
|
|
278
|
+
| `[mock:todo]` | Writes a 4-item todo list, then updates it (completed/cancelled), then wraps up. |
|
|
279
|
+
| `[mock:delegate]` | Delegates to a declared subagent (default tool name `task_fast` — requires one; see step 6). |
|
|
280
|
+
| `[mock:fail]` | A few deltas, then a terminal stream error — the deterministic failed-turn trigger. |
|
|
281
|
+
| `[mock:burst]` | `burstChunks` unpaced deltas — the renderer-throughput probe. |
|
|
282
|
+
| `[mock:markdown]` | Structure-heavy markdown split across deltas (fences, tables, unicode) — streaming-renderer stability. |
|
|
283
|
+
| `[mock:interleave]` | Alternating reasoning and text blocks in one message, like extended-thinking models stream. |
|
|
284
|
+
| `[mock:empty]` | A completion with zero content parts. |
|
|
285
|
+
|
|
286
|
+
Scripted tool inputs stream as fragmented `tool-input-delta` parts (like a
|
|
287
|
+
real model), each scripted step opens with a reasoning burst so "Thinking…"
|
|
288
|
+
renders, and every stream — including aborted ones — is grammatical (blocks
|
|
289
|
+
close, a terminal part ends the stream; pinned by the package's conformance
|
|
290
|
+
tests, which also validate the scripted `ask_question`/`todo` inputs against
|
|
291
|
+
the installed eve's own framework-tool schemas). Inject `now` for
|
|
292
|
+
byte-deterministic streams. Because the mock is credential-free, `eve eval`
|
|
293
|
+
suites built on it can run end-to-end in CI — the coder example's
|
|
294
|
+
[`evals-mock/`](./examples/coder/evals-mock) suite (run via its
|
|
295
|
+
`scripts/eval.ts`) is the reference setup.
|
|
296
|
+
|
|
297
|
+
## Tool behavior
|
|
298
|
+
|
|
299
|
+
The names are deliberately boring; the behavior behind them is the point:
|
|
300
|
+
|
|
301
|
+
- **`read`** is multi-format — line-numbered text windows plus content-sniffed
|
|
302
|
+
PDF (PDFium via `clawpdf`), DOCX (`mammoth`), and spreadsheet (`.xlsx`/
|
|
303
|
+
`.xlsm`/`.xls`/`.ods` via SheetJS, TSV per sheet) → text, and UTF-16 BOM
|
|
304
|
+
decode. Reading an **image** returns metadata and queues the pixels to appear
|
|
305
|
+
as a viewable attachment on the next turn; **video/audio** reads return
|
|
306
|
+
metadata (format, MIME type, bytes) and can queue the same way where the
|
|
307
|
+
model supports it (see [Media reads](#media-reads-images-video-audio)).
|
|
308
|
+
No-extractor formats fail with a named, actionable error; extraction is cached
|
|
309
|
+
by path + stat. The first read under a directory with its own `AGENTS.md`
|
|
310
|
+
attaches that file to the result (`directory_conventions`), **once per
|
|
311
|
+
directory per session** — nested conventions arrive exactly when the model
|
|
312
|
+
enters the directory, instead of hoping it remembers to read them. The root
|
|
313
|
+
file is excluded (`instructions.repoConventions` already injects it); riders
|
|
314
|
+
are result content, so the prompt prefix stays byte-stable. Opt out with
|
|
315
|
+
`injectDirConventions: false`; rename the file with `conventionsFileName`.
|
|
316
|
+
- **`glob` / `grep`** prefer git-tracked candidates (`git ls-files`), falling
|
|
317
|
+
back to a filesystem walk outside a repo, with bounded result counts.
|
|
318
|
+
- **`bash`** waits briefly, then auto-backgrounds a still-running command
|
|
319
|
+
(returns a `task_id`); oversized output spills to `stateDir` instead of
|
|
320
|
+
flooding the context window.
|
|
321
|
+
- **`webfetch`** returns a page as markdown (default), plain text, or raw
|
|
322
|
+
HTML. HTML is reduced to its **main content** under a title/byline header
|
|
323
|
+
(`defuddle` extraction, with a guard that falls back to the full page when
|
|
324
|
+
extraction over-prunes), and the result is honest about failure: a page that
|
|
325
|
+
yields almost no text gets a note saying so (with a hint for known
|
|
326
|
+
client-rendered/login-walled domains like X or Reddit), and a conversion
|
|
327
|
+
that leaves raw HTML flags itself. Fetched PDFs/DOCX/spreadsheets route
|
|
328
|
+
through the same extractors as `read` (`.pdf` URLs get a longer default
|
|
329
|
+
timeout); images return metadata and attach to the chat like `read`;
|
|
330
|
+
oversized bodies spill to `stateDir`.
|
|
331
|
+
- **`run_async` / `check_tasks` / `await_task`** persist the task registry
|
|
332
|
+
across restarts (tasks running across a restart report as `lost`); any
|
|
333
|
+
`defineOp` op becomes `run_async`-able via `extraBackgroundables`.
|
|
334
|
+
- **Background notifications**: `bash` and `run_async` take an optional
|
|
335
|
+
`notify` watcher (`{ pattern, reason }`) — output lines matching the regex
|
|
336
|
+
(debounced, capped) are delivered to the model as a message while the
|
|
337
|
+
session is idle, instead of it polling `check_tasks`; `run_async` also takes
|
|
338
|
+
`notify_on_complete` for a settle notice. Delivery rides the park-delivery
|
|
339
|
+
hook (Quick start step 5): notifications queue until the session parks and
|
|
340
|
+
then start its next turn, exactly like a user message.
|
|
341
|
+
|
|
342
|
+
## Sandbox-backed file tools (split topologies)
|
|
343
|
+
|
|
344
|
+
`createStdlib`'s file tools do `node:fs` against the process's own disk —
|
|
345
|
+
right when the eve process and the workspace share a machine (a local coding
|
|
346
|
+
agent, the coder example). On a **split topology** — eve on a serverless
|
|
347
|
+
function, the workspace in a remote sandbox (`ctx.getSandbox()`) — that would
|
|
348
|
+
read the harness's filesystem, not the workspace. For that case the same
|
|
349
|
+
tools run over the sandbox session:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// agent/lib/file-tools.ts
|
|
353
|
+
import { createSandboxFileTools } from "@zocomputer/agent-sdk";
|
|
354
|
+
|
|
355
|
+
export const fileTools = createSandboxFileTools({
|
|
356
|
+
workspaceRoot: "/workspace", // absolute path INSIDE the sandbox
|
|
357
|
+
spillDir: "/workspace/.agent/tool-outputs", // grep overflow, readable by `read`
|
|
358
|
+
});
|
|
359
|
+
// then re-export fileTools.tools.read / edit / write / glob / grep per file,
|
|
360
|
+
// with disableTool() shims for eve's read_file/write_file (glob/grep shadow
|
|
361
|
+
// the built-ins by name), exactly like the Quick start.
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Every effect routes through the session sandbox, resolved per tool call:
|
|
365
|
+
bytes over `readBinaryFile`/`writeBinaryFile`, stat/list/search executed
|
|
366
|
+
remotely via `run` (ripgrep when present, POSIX grep fallback) so a search
|
|
367
|
+
never pulls file contents over the wire. The rich-read pipeline (extraction,
|
|
368
|
+
media detection, attachments, `AGENTS.md` riders) is byte-identical to the
|
|
369
|
+
local backend — a shared conformance suite pins the two together. `bash` and
|
|
370
|
+
the task machinery stay host-side by design: on a sandboxed runtime, keep
|
|
371
|
+
eve's built-in `bash` (already sandbox-native).
|
|
372
|
+
|
|
373
|
+
Under the hood this is one seam: every file tool takes an `io:
|
|
374
|
+
WorkspaceIoProvider` (default local `node:fs`), and `createSandboxIo` /
|
|
375
|
+
`sandboxIoProvider` implement it over a structural `SandboxSessionLike` that
|
|
376
|
+
eve's `SandboxSession` satisfies. A custom backend (e.g. a bootstrap step
|
|
377
|
+
before first use) plugs in via `resolveSession`.
|
|
378
|
+
|
|
379
|
+
One default flips versus `createStdlib`: `attachImagesToChat` is **false**
|
|
380
|
+
here. The attachment path needs the park-delivery hook and a runtime that can
|
|
381
|
+
send itself the next-turn message over loopback — unvalidated on hosted
|
|
382
|
+
serverless runtimes — so until a consumer wires and verifies that leg, image
|
|
383
|
+
reads return the honest metadata-only note instead of a "queued" promise that
|
|
384
|
+
never delivers.
|
|
385
|
+
|
|
386
|
+
## Design rules
|
|
387
|
+
|
|
388
|
+
The full rationale — each foundational decision, why we made it, and the
|
|
389
|
+
prior art it came from (Claude Code, opencode, openclaw, hermes, pi, Cursor,
|
|
390
|
+
and Zo v1's hostagent) — lives in
|
|
391
|
+
[`design/foundation/`](./design/foundation/00-overview.md). The short
|
|
392
|
+
version:
|
|
393
|
+
|
|
394
|
+
- **Prompt-cache stability.** Tool descriptions and dynamic instructions are
|
|
395
|
+
built once per session (`"session.started"`) and stay byte-identical
|
|
396
|
+
thereafter; live state rides tool results, never a description. Options like
|
|
397
|
+
`workspaceNoun` interpolate at build time.
|
|
398
|
+
- **Prior-aligned naming.** Tool names and parameters follow what models
|
|
399
|
+
already know from Claude Code and opencode: lowercase `read`/`edit`/`write`/
|
|
400
|
+
`glob`/`grep`/`bash`, snake_case params, `path` not `file_path`. Echo-back
|
|
401
|
+
keys mirror the params that consume them (`task_id`).
|
|
402
|
+
- **Workspace-scoped.** Every file tool resolves paths inside
|
|
403
|
+
`workspaceRoot` and refuses escapes.
|
|
404
|
+
- **No house types.** The package imports nothing repo-specific — plain
|
|
405
|
+
discriminated unions, `eve` + `zod` as peers, WASM/pure-JS extraction deps
|
|
406
|
+
(no native postinstalls).
|
|
407
|
+
|
|
408
|
+
## Media reads (images, video, audio)
|
|
409
|
+
|
|
410
|
+
eve tool results are text/json only, so `read` can't hand the model an image
|
|
411
|
+
directly. The workaround: for media under the inline cap, `read` embeds the
|
|
412
|
+
bytes as a `data:` URL on its **raw** result under a model-hidden field, and
|
|
413
|
+
its `toModelOutput` strips that field. The model sees only metadata + a note;
|
|
414
|
+
the **park-delivery hook** (`createParkDeliveryHook`, one file in
|
|
415
|
+
`agent/hooks/` — Quick start step 5) watches the runtime stream from inside
|
|
416
|
+
the agent's own server process and, when the session parks, sends the media
|
|
417
|
+
back into the session as a real user turn over loopback. The model sees the
|
|
418
|
+
pixels on its next turn with no browser, cockpit, or user action involved.
|
|
419
|
+
(The same hook delivers background-task notifications — see
|
|
420
|
+
[Tool behavior](#tool-behavior).)
|
|
421
|
+
|
|
422
|
+
- eve hooks are observe-only for model context, so the hook doesn't mutate the
|
|
423
|
+
current turn — it starts the next one, exactly like a user hitting send.
|
|
424
|
+
Delivery is deduped per tool call, retried briefly on a racing send, and
|
|
425
|
+
re-queued for the next park if it still fails.
|
|
426
|
+
- The contract + a dependency-free reader live at
|
|
427
|
+
**`@zocomputer/agent-sdk/attachments`** (`readChatAttachment(output)` →
|
|
428
|
+
`ChatAttachment | null`, kinds `image`/`video`/`audio`), so UI clients that
|
|
429
|
+
want to render or track the attachments import it without the extraction
|
|
430
|
+
deps. The pure decision core (`redeliveryFromEvent`, `createRedeliveryState`,
|
|
431
|
+
`buildRedeliveryMessage`) is exported for hosts that would rather run
|
|
432
|
+
delivery elsewhere.
|
|
433
|
+
- **Images** attach by default: `attachImagesToChat` (default `true`) and
|
|
434
|
+
`maxInlineImageBytes` (default 3 MiB — eve's attachment staging inlines
|
|
435
|
+
images up to that size at model-call time and text-stubs bigger ones, so the
|
|
436
|
+
cap keeps the "queued" promise truthful; larger images fall back to the
|
|
437
|
+
metadata-only "ask the user" note).
|
|
438
|
+
- **Video/audio are opt-in**: `attachVideoToChat` / `attachAudioToChat`
|
|
439
|
+
(default `false`) and `maxInlineMediaBytes` (default 10 MB, read's stat
|
|
440
|
+
guard). Two gates must hold before enabling them: your **model** takes that
|
|
441
|
+
medium (Gemini accepts video/audio file parts; Claude and most others don't
|
|
442
|
+
— an unsupported part fails the delivery turn), and your **runtime** passes
|
|
443
|
+
them through (eve's attachment staging currently hydrates only images ≤3 MiB
|
|
444
|
+
and PDFs ≤20 MiB back into the model call; anything else becomes an
|
|
445
|
+
"Attached file …" text stub — see the eve-maintainer notes below). Until
|
|
446
|
+
both hold, video/audio reads return honest metadata + a note steering to
|
|
447
|
+
bash extraction (e.g. ffmpeg frames read back as images).
|
|
448
|
+
- `createParkDeliveryHook`'s `serverUrl` (defaults to loopback on `$PORT`, eve
|
|
449
|
+
dev's 2000 otherwise) and `log`. An agent that skips the hook simply gets
|
|
450
|
+
the metadata note (the bytes ride the stream unused — turn inlining off with
|
|
451
|
+
`attachImagesToChat: false`).
|
|
452
|
+
|
|
453
|
+
## Steering (mid-turn messages)
|
|
454
|
+
|
|
455
|
+
eve queues a message sent to a busy session until the turn ends — hooks are
|
|
456
|
+
observe-only and a mid-turn `send()` is rejected, so there's no framework
|
|
457
|
+
channel into a running turn. The SDK's channel rides the tool results:
|
|
458
|
+
|
|
459
|
+
- **Enable it with `createStdlib({ steer: { dir } })`.** The stdlib builds a
|
|
460
|
+
steer **inbox** — one NDJSON file per session under `dir` (exposed as
|
|
461
|
+
`stdlib.steerInbox`) — and wraps every stdlib tool so a completing call
|
|
462
|
+
drains the inbox and attaches the queued messages to its result under
|
|
463
|
+
`user_steer`, with a note telling the model to adjust course now. On a long
|
|
464
|
+
turn, `await_task` is the highest-value delivery window.
|
|
465
|
+
- **A UI queues a steer by appending to the inbox**:
|
|
466
|
+
`createSteerInbox({ dir }).append(sessionId, text)`
|
|
467
|
+
(`@zocomputer/agent-sdk/steer-inbox`), typically behind a small HTTP route.
|
|
468
|
+
Drain-vs-append races are safe (rename-first drain).
|
|
469
|
+
- **Wrap your own tools** with `createSteerWrapper(stdlib.steerInbox)` (or
|
|
470
|
+
`withSteerDelivery(tool, inbox)`) so they deliver steers too.
|
|
471
|
+
- **Messages that miss every tool window drain on park**: with
|
|
472
|
+
`createParkDeliveryHook({ steer: { dir } })` (Quick start step 5), leftovers
|
|
473
|
+
start the session's next turn — delivered first, verbatim.
|
|
474
|
+
- **The wire contract is dependency-free at `@zocomputer/agent-sdk/steer`**
|
|
475
|
+
(`STEER_FIELD`, `SteerMessage`, `readSteerMessages`, …), so UI clients can
|
|
476
|
+
project delivered steers into user-message bubbles without pulling in the
|
|
477
|
+
extraction deps.
|
|
478
|
+
|
|
479
|
+
## Gateway stream guards (surviving a dead connection)
|
|
480
|
+
|
|
481
|
+
Neither eve's `defineAgent` nor the AI SDK's gateway provider exposes
|
|
482
|
+
per-attempt timeouts, so a model call that hangs — response headers never
|
|
483
|
+
arrive, or the SSE body goes quiet mid-stream on a dropped connection —
|
|
484
|
+
hangs the turn forever. The one seam the provider does expose is `fetch`;
|
|
485
|
+
`withStreamGuards` (`@zocomputer/agent-sdk/gateway-fetch`) wraps it with the
|
|
486
|
+
two guards a streaming call needs:
|
|
487
|
+
|
|
488
|
+
- **first byte** — abort when response headers don't arrive in time;
|
|
489
|
+
- **idle** — abort when the response body goes quiet between chunks (a dead
|
|
490
|
+
connection the TCP stack never surfaces).
|
|
491
|
+
|
|
492
|
+
A guard firing rejects like any network failure, so the AI SDK's normal
|
|
493
|
+
retry-with-backoff takes over instead of waiting on a dead socket:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
import { createGateway } from "ai";
|
|
497
|
+
import { withStreamGuards } from "@zocomputer/agent-sdk/gateway-fetch";
|
|
498
|
+
|
|
499
|
+
const gateway = createGateway({ fetch: withStreamGuards(fetch) });
|
|
500
|
+
export default defineAgent({ model: gateway("anthropic/claude-sonnet-5") });
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The defaults (60s to first byte, 180s idle) are deliberately generous: the
|
|
504
|
+
point is converting a *dead* connection into a retryable error, not racing a
|
|
505
|
+
slow-but-alive reasoning model. Override via the second argument
|
|
506
|
+
(`{ firstByteMs, idleMs }`).
|
|
507
|
+
|
|
508
|
+
## Zo platform modules (`platform/`)
|
|
509
|
+
|
|
510
|
+
Everything above is the generic stdlib — nothing in it assumes Zo. The
|
|
511
|
+
published artifact additionally vendors the Zo platform packages under
|
|
512
|
+
`platform/`, exposed as subpath exports, so an agent deployed on Zo installs
|
|
513
|
+
its whole harness from this one dependency:
|
|
514
|
+
|
|
515
|
+
| Import | What it is |
|
|
516
|
+
| --- | --- |
|
|
517
|
+
| `@zocomputer/agent-sdk/sandbox` | `zoSandbox()` — the Zo sandbox backend for eve's `agent/sandbox.ts` slot. The runtime holds no provider key; it asks the Zo control plane (`ZO_API_URL`, authenticated by `ZO_AGENT_TOKEN`) for a scoped, short-lived SSH session. |
|
|
518
|
+
| `@zocomputer/agent-sdk/ai` (+ `/ai/gateway`, `/ai/register`, `/ai/session-fetch`) | The Zo AI provider layer. `import "@zocomputer/agent-sdk/ai/register"` (first in `agent.ts`) points the AI SDK's default provider at Zo's metering gateway so bare catalog model slugs work. |
|
|
519
|
+
| `@zocomputer/agent-sdk/cloud-tools` (+ `/image`, `/web-search`) | The default Zo cloud tools: `generate_image` and the Exa web-search factory, built on the gateway. |
|
|
520
|
+
| `@zocomputer/agent-sdk/runtime-auth` | The agent-token contract (header/env names, mint/verify) shared with the Zo control plane. |
|
|
521
|
+
|
|
522
|
+
These modules assume Zo's control plane and are inert elsewhere; `ai` joins
|
|
523
|
+
`eve`/`zod` as a peer dependency (the `/ai/register` side effect must mutate
|
|
524
|
+
*your* `ai` instance), and `platform/agent-sandbox` brings `ssh2` (its native
|
|
525
|
+
addon is optional — the pure-JS fallback is fine).
|
|
526
|
+
|
|
527
|
+
In the [Zo monorepo](https://github.com/zocomputer/zov2-code) these are
|
|
528
|
+
separate workspace packages; the mirror sync composes them into this one
|
|
529
|
+
package so a deployed agent's `package.json` needs a single
|
|
530
|
+
`github:zocomputer/agent-sdk#<ref>` (or npm) dependency.
|
|
531
|
+
|
|
532
|
+
## Notes for the eve maintainers
|
|
533
|
+
|
|
534
|
+
Gaps we hit building this — each is something we'd rather see upstream than
|
|
535
|
+
keep working around:
|
|
536
|
+
|
|
537
|
+
- **Multimodal tool results.** `ToolModelOutput` is `text | json` only, so
|
|
538
|
+
`read` can't return an image directly — we work around it by smuggling the
|
|
539
|
+
bytes past on the raw result and having a hook send them back as the next
|
|
540
|
+
user turn (see [Media reads](#media-reads-images-video-audio)), which costs
|
|
541
|
+
an extra turn and an extra copy of the bytes in the durable stream.
|
|
542
|
+
`@workflow/ai`'s DurableAgent already merged multimodal tool-result
|
|
543
|
+
pass-through (`type: "content"`, vercel/workflow#848 → #1385); exposing that
|
|
544
|
+
through eve's tool surface would let `read` return real image blocks and
|
|
545
|
+
delete the whole workaround. We've worked the design to change-list
|
|
546
|
+
precision — including the storage-independent persistence policy (stub-first
|
|
547
|
+
history + an in-process byte cache, so the model sees media the turn it read
|
|
548
|
+
them and degrades gracefully to a text stub across process boundaries) and
|
|
549
|
+
the per-provider degrade table it needs (Anthropic's tool-result converter
|
|
550
|
+
warn-drops non-image/PDF media; OpenAI chat completions would stringify
|
|
551
|
+
base64 — the opencode blowup) — in
|
|
552
|
+
[`design/proposals/eve-content-tool-results.md`](./design/proposals/eve-content-tool-results.md).
|
|
553
|
+
- **Attachment hydration is image/PDF-only.** The staging pipeline
|
|
554
|
+
(`attachment-staging.ts`) stages every inbound file part to the sandbox, but
|
|
555
|
+
`shouldInlineSandboxRefAsBytes` re-inlines only `image/*` ≤3 MiB and
|
|
556
|
+
`application/pdf` ≤20 MiB at model-call time — every other media type
|
|
557
|
+
(video, audio) hydrates as an `Attached file …` text stub even when the
|
|
558
|
+
session's model accepts it. That silently blocks video/audio delivery on any
|
|
559
|
+
sandboxed runtime (the user-turn workaround included), while the AI SDK
|
|
560
|
+
underneath is ready: `@ai-sdk/google` converts any file part to `inlineData`
|
|
561
|
+
and Gemini natively takes video/audio (Anthropic's converter throws
|
|
562
|
+
`UnsupportedFunctionalityError` on them, so a *blind* widening would turn
|
|
563
|
+
today's stub into a failed model call). The fix that threads that needle is
|
|
564
|
+
model-aware hydration — detect the provider family from the already-resolved
|
|
565
|
+
model (the `detectPromptCachePath` idiom) and inline video/audio ≤20 MiB for
|
|
566
|
+
the google family only, keeping the text-stub fallback for everyone else. We
|
|
567
|
+
built and tested exactly that patch against `vercel/eve` (all suites green):
|
|
568
|
+
the PR-grade writeup is
|
|
569
|
+
[`design/proposals/eve-hydrate-model-aware-media.md`](./design/proposals/eve-hydrate-model-aware-media.md)
|
|
570
|
+
and the DCO-signed patch sits beside it
|
|
571
|
+
(`eve-hydrate-media-support.patch`). Filed upstream as
|
|
572
|
+
[vercel/eve#543](https://github.com/vercel/eve/issues/543); the PR follows
|
|
573
|
+
on maintainer go-ahead. It would make `read`'s opt-in video/audio
|
|
574
|
+
attachments (`attachVideoToChat`/`attachAudioToChat`) work end-to-end.
|
|
575
|
+
- **HITL replay.** eve persists `input.requested` but not the client's
|
|
576
|
+
`input.responded`, so a replayed session reopens answered prompts as
|
|
577
|
+
pending. We append synthetic responded-events from client-side storage;
|
|
578
|
+
persisting the response (or accepting it into the durable stream) would fix
|
|
579
|
+
every client at once.
|
|
580
|
+
- **`ask_question` multi-select.** The input-request contract carries rich
|
|
581
|
+
options (`id`/`label`/`description`/`style`) but the response carries a
|
|
582
|
+
single `optionId` — there's no way to ask "pick all that apply."
|
|
583
|
+
`allowMultiple` on the request plus `optionIds` on the response would
|
|
584
|
+
complete the surface; clients render checkboxes instead of buttons when
|
|
585
|
+
it's set.
|
|
586
|
+
- **Tool naming + config-level disable.** The built-ins ship off-prior names
|
|
587
|
+
(`read_file`, `write_file`), and vacating one requires a `disableTool()`
|
|
588
|
+
shim file per name. Prior-aligned defaults — or a config switch to disable
|
|
589
|
+
built-ins wholesale — would remove the shims.
|
|
590
|
+
- **No per-tool `strict` / `providerOptions` passthrough.** Anthropic's
|
|
591
|
+
`strict: true` (grammar-constrained sampling — the documented fix for
|
|
592
|
+
newer Claude models garbling off-prior tool schemas) is a per-tool flag
|
|
593
|
+
the AI SDK already forwards to providers, but `buildToolSet` constructs
|
|
594
|
+
`tool()` without it, so no eve agent can enable it. Accepting `strict`
|
|
595
|
+
(and per-tool `providerOptions`) on `defineTool` and passing them through
|
|
596
|
+
would make it a one-line opt-in; the change-list design is
|
|
597
|
+
[`design/proposals/eve-strict-tool-passthrough.md`](./design/proposals/eve-strict-tool-passthrough.md).
|
|
598
|
+
Same seam: `experimental_repairToolCall` is unwired.
|
|
599
|
+
- **Invalid tool calls never reach the event stream.** When a call fails
|
|
600
|
+
schema validation the AI SDK marks it `invalid` and feeds the error back
|
|
601
|
+
to the model, but `emitStreamContent` and `emitStepActions` both skip
|
|
602
|
+
invalid calls — no event is emitted, so clients can't render the retry
|
|
603
|
+
and harnesses can't measure schema-misuse rates (the regression newer
|
|
604
|
+
Anthropic models show on off-prior schemas). An `action.invalid` event
|
|
605
|
+
with the tool name and error class closes the gap. We built and tested
|
|
606
|
+
exactly that patch against `vercel/eve` (all suites green): the PR-grade
|
|
607
|
+
writeup is
|
|
608
|
+
[`design/proposals/eve-invalid-tool-call-events.md`](./design/proposals/eve-invalid-tool-call-events.md)
|
|
609
|
+
and the DCO-signed patch sits beside it
|
|
610
|
+
(`eve-invalid-tool-call-events.patch`). Filed upstream as
|
|
611
|
+
[vercel/eve#542](https://github.com/vercel/eve/issues/542); the PR follows
|
|
612
|
+
on maintainer go-ahead.
|
|
613
|
+
- **`ask_question`'s options are the off-prior nested shape.** Its `options`
|
|
614
|
+
array of `.strict()` option objects is exactly the shape newer Claude
|
|
615
|
+
models garble (invented trailing keys after long strings), and it has no
|
|
616
|
+
Claude Code analog to ride. Flattening it — or at least dropping
|
|
617
|
+
`.strict()` so the advertised contract matches the (unvalidated) lenient
|
|
618
|
+
runtime — would cut the schema-slop surface every HITL agent presents.
|
|
619
|
+
- **The `agent` clone tool can't be disabled.** It's injected at the harness
|
|
620
|
+
layer (`createNodeHarnessTools`), not as a framework tool, so a
|
|
621
|
+
`disableTool()` shim for it fails runtime agent-graph resolution — every
|
|
622
|
+
session create 500s. A read-only child that should answer one question and
|
|
623
|
+
return has no way to vacate recursive delegation; we fall back to
|
|
624
|
+
instruction text. Either registering `agent` as a disableable framework
|
|
625
|
+
tool or honoring the shim at the harness layer would close the gap.
|
|
626
|
+
- **Concurrent tool calls race shared resources.** The AI SDK loop runs a
|
|
627
|
+
step's tool calls with `Promise.all`, so two mutating calls against the same
|
|
628
|
+
file in one step race their read-modify-write sections — both report
|
|
629
|
+
success and the second write silently drops the first edit. Models batch
|
|
630
|
+
same-file edits because batching independent work is exactly what we teach
|
|
631
|
+
them, and the lost update is invisible at the tool boundary (a live sweep
|
|
632
|
+
burned ~8 steps rediscovering one through a lint error). Our `edit`/`write`
|
|
633
|
+
serialize per resolved path with a `globalThis`-anchored FIFO lock
|
|
634
|
+
([`src/path-locks.ts`](./src/path-locks.ts)), but every toolset author has
|
|
635
|
+
to rediscover this; a declarative "serialize calls sharing this key" seam
|
|
636
|
+
on the tool contract — eve already knows the set of calls it's about to run
|
|
637
|
+
concurrently — would queue same-key calls FIFO while keeping distinct keys
|
|
638
|
+
parallel (zocomputer/zov2-code#337).
|
|
639
|
+
- **Streaming events are quadratic.** `message.appended`/`reasoning.appended`
|
|
640
|
+
carry the full text-so-far alongside each delta, so one turn's events sum
|
|
641
|
+
to O(n²) bytes — a 3,000-delta turn measures ~330 MB of payloads, and
|
|
642
|
+
late deltas re-send ~36 KB of prefix each. Clients that store or replay
|
|
643
|
+
streams must compact/thin app-side (chat-core's `stream-thinning`);
|
|
644
|
+
delta-only stream events would fix storage, replay, and live-wire
|
|
645
|
+
throughput at the source.
|
|
646
|
+
- **An aborted stream resets the client session.** `advanceSession` carries
|
|
647
|
+
the session forward only when the consumed stream ends on
|
|
648
|
+
`session.waiting`; an abort (Stop/Esc) therefore erases the sessionId, and
|
|
649
|
+
the next `send` silently creates a fresh session — forking the
|
|
650
|
+
conversation. Preserving the session state across aborts (the id was
|
|
651
|
+
known!) would remove a whole class of client-side recovery code.
|
|
652
|
+
- **No turn cancellation.** Aborting the client stream leaves the server-side
|
|
653
|
+
turn running to completion; there's no API to actually stop it. Stop
|
|
654
|
+
buttons today can only detach and re-attach.
|
|
655
|
+
- **Continuation-token scoping.** A hook's `ctx.channel.continuationToken` is
|
|
656
|
+
the runtime-namespaced form (`eve:eve:<uuid>`), but `ClientSession.send`
|
|
657
|
+
needs the client-facing token (`eve:<uuid>`) — and posting the namespaced
|
|
658
|
+
form doesn't error, it **silently creates a new session** (the continue
|
|
659
|
+
route's get-or-create). We strip the namespace (`clientContinuationToken`)
|
|
660
|
+
and assert the echoed session id; either surfacing the client token on
|
|
661
|
+
`HookContext` or rejecting unknown tokens on continue would remove the trap.
|
|
662
|
+
- **AGENTS.md ingestion.** eve injects no repo conventions; every other
|
|
663
|
+
harness (Claude Code, Cursor, Codex) reads `AGENTS.md` natively. Our
|
|
664
|
+
`repoConventions` instruction covers the root file, but first-class support
|
|
665
|
+
belongs in the framework.
|
|
666
|
+
- **In-history tool-output pruning.** Old tool results stay in the model
|
|
667
|
+
prompt verbatim for a session's lifetime. We bound tool output at the source
|
|
668
|
+
(spill files, result caps); a framework-level pruning/compaction hook would
|
|
669
|
+
do better.
|
|
670
|
+
|
|
671
|
+
## License
|
|
672
|
+
|
|
673
|
+
[MIT](./LICENSE)
|