toolnexus 0.18.0 → 0.19.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/README.md CHANGED
@@ -12,7 +12,7 @@ unified as one tool set, driving any LLM.
12
12
  > loop, hooks, streaming, retries, memory — and nothing it doesn't.
13
13
 
14
14
  The JS/TypeScript port of [toolnexus](https://github.com/muthuishere/toolnexus) — the same
15
- library, byte-identical, also in **Python, Go, Java, and C#**. Built on
15
+ library, byte-identical, also in **Python, Go, Java, C#, Elixir and Clojure**. Built on
16
16
  `@modelcontextprotocol/sdk` (the MCP SDK opencode uses).
17
17
 
18
18
  ## Install
@@ -39,420 +39,27 @@ const { text } = await agent.run("What files are in this folder?", { toolkit: tk
39
39
  console.log(text)
40
40
  ```
41
41
 
42
- `createClient` reads the key from `OPENROUTER_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY`
43
- (or pass `apiKey`). The loop runs call → execute tools → feed back → repeat (up to `maxTurns`,
44
- default 10), with hooks, streaming (`agent.stream(...)`), retries/backoff, timeout, and
45
- conversation memory available.
46
-
47
- ## With MCP + skills
48
-
49
- Point the toolkit at an `mcp.json` and a `skills/` folder — every MCP server tool and every
50
- `SKILL.md` becomes part of the same tool set. Skills are injected into the system prompt; a
51
- single `skill` tool loads each on demand (progressive disclosure).
52
-
53
- ```ts
54
- const tk = await createToolkit({
55
- mcpConfig: "./mcp.json", // path or a config object; local stdio + remote streamable-HTTP
56
- skillsDir: "./skills", // a folder of <name>/SKILL.md (string or string[])
57
- })
58
-
59
- const { text } = await agent.run("Refund order 1234 for the customer.", { toolkit: tk })
60
- console.log(text)
61
- await tk.close() // disconnect MCP servers
62
- ```
63
-
64
- ## Suspension — login / approval / input (`Pending` / `waitFor`)
65
-
66
- Some tools can't finish in one shot: they need an **out-of-band, async resolution** — a human
67
- logs in, approves a trade, types a value. That's one idea, not one feature: *login* is just the
68
- `kind:"authorization"` case. A tool returns **`Pending(request)`**; the client calls your
69
- **`waitFor(request) → answer`**, then **retries the tool** with `ctx.answer`, and the model gets
70
- the real answer. Everything crossing a boundary is plain data, so it works the same across
71
- processes, agents, and restarts — and identically in all five language ports.
72
-
73
- ```ts
74
- import { authRequired } from "toolnexus"
75
-
76
- // a tool that needs login the first time:
77
- defineTool({
78
- name: "get_balance", description: "…", inputSchema: { type: "object", properties: {} },
79
- run: (_args, ctx) =>
80
- authed ? `balance: ₹67,417` : authRequired("https://example.com/login", "Log in to continue"),
81
- })
82
-
83
- // the host supplies ONE function — how the link reaches the human is your call:
84
- const agent = createClient({
85
- baseUrl, style: "openai", model,
86
- waitFor: async (request) => { // interactive: open a browser + poll
87
- open(request.url); await pollUntilLoggedIn(); return { id: request.id, ok: true }
88
- },
89
- // headless desk: waitFor = send request.url to your channel + poll
90
- // another agent: waitFor = forward the request over A2A, await its answer
91
- })
92
- ```
93
-
94
- Omit `waitFor` for a **durable** host: `run()` doesn't hang — it returns
95
- `{ status: "pending", pending: request }`, so you can deliver the link out-of-band (file, HTTP,
96
- channel) and resume later, even in another process. `stream()` emits a `{ type: "pending", request }`
97
- event so a channel can push the link in real time. Runnable demo: `examples/pending.ts`.
98
-
99
- ## Conversations / memory
100
-
101
- `run()` is stateless — each call starts fresh. To keep a thread's history, use `ask()` with an
102
- `id`, or a `conversation()` object.
103
-
104
- **`ask(prompt, { toolkit, id? })`** — with an `id`, the client's `ConversationStore` remembers
105
- the thread: it *loads* that id's transcript, *runs*, *saves* the updated transcript, and returns
106
- the answer. The next `ask` with the same `id` continues it. Without an `id` it's a stateless
107
- one-shot (identical to `run`).
108
-
109
- ```ts
110
- const agent = createClient({
111
- baseUrl: "https://openrouter.ai/api/v1",
112
- style: "openai",
113
- model: "openai/gpt-4o-mini",
114
- // store defaults to InMemoryConversationStore (process lifetime).
115
- })
116
-
117
- await agent.ask("My name is Muthu.", { toolkit: tk, id: "user-42" })
118
- const { text } = await agent.ask("What's my name?", { toolkit: tk, id: "user-42" })
119
- console.log(text) // → "Your name is Muthu."
120
- ```
121
-
122
- **Persist across processes.** A `ConversationStore` is two methods — implement it for a file, DB,
123
- or Redis and pass it as `store`:
124
-
125
- ```ts
126
- import type { ConversationStore } from "toolnexus"
127
-
128
- interface ConversationStore {
129
- get(id: string): Promise<any[] | undefined> // stored transcript for id, or undefined
130
- save(id: string, messages: any[]): Promise<void>
131
- }
132
-
133
- const agent = createClient({ baseUrl, style: "openai", model, store: myFileStore })
134
- ```
135
-
136
- The default is `InMemoryConversationStore` (also exported). For a quick in-process thread without
137
- ids, `agent.conversation({ toolkit })` returns an object whose `.send(prompt)` retains history
138
- automatically (`.messages`, `.reset()`).
139
-
140
- The same `store` powers inbound A2A: `toolkit.serve` keys each peer's turns by the A2A
141
- `contextId` through `client.ask`, so a remote agent's conversation is remembered too (see below).
142
-
143
- **Streaming with memory.** The `id` also works while streaming. Pass `on_text` to `ask` to stream
144
- text deltas as they arrive — `ask` still returns the final `RunResult` — or iterate `stream()`
145
- directly. With an `id`, the thread is loaded before the stream and saved on the `done` event.
146
-
147
- ```ts
148
- // block-style: stream deltas to stdout, still get the RunResult back — remembered under `id`
149
- const res = await agent.ask("Draft a reply.", {
150
- toolkit: tk,
151
- id: "user-42",
152
- on_text: (delta) => process.stdout.write(delta),
153
- })
154
-
155
- // iterator: consume text + tool events; `id` makes it stateful (load before, save on `done`)
156
- for await (const ev of agent.stream("And summarise it.", { toolkit: tk, id: "user-42" })) {
157
- if (ev.type === "text") process.stdout.write(ev.delta)
158
- else if (ev.type === "done") console.log("\n", ev.result.usage)
159
- }
160
- ```
161
-
162
- ## Observability / metrics
163
-
164
- Zero-dependency, two outputs from one internal instrumentation — both opt-in, no cost when unused.
165
-
166
- **`onMetric` — a semantic event feed.** Pass it to `createClient` and it receives a readable
167
- `MetricEvent` at each significant point: `{ event: "llm", model, status, ms, promptTokens,
168
- completionTokens }` per model call, `{ event: "tool", tool, source, isError, ms }` per tool call,
169
- and a terminal `{ event: "run", model, turns, toolCalls, totalTokens, ms, error? }` per `run`/`ask`.
170
- Forward it anywhere (statsd, logs, OpenTelemetry) — the library holds no opinion.
171
-
172
- ```ts
173
- const agent = createClient({
174
- baseUrl, style: "openai", model,
175
- onMetric: (ev) => console.log("[metric]", ev.event, JSON.stringify(ev)),
176
- })
177
- ```
178
-
179
- **`client.metrics()` — built-in Prometheus text.** The same events feed a tiny in-memory registry
180
- that renders the Prometheus text exposition format (no third-party dep). Mount it at `GET /metrics`:
181
-
182
- ```ts
183
- import { createServer } from "node:http"
184
-
185
- createServer((req, res) => {
186
- if (req.url === "/metrics") {
187
- res.setHeader("Content-Type", "text/plain; version=0.0.4")
188
- res.end(agent.metrics())
189
- } else {
190
- res.statusCode = 404
191
- res.end()
192
- }
193
- }).listen(9090)
194
- ```
195
-
196
- Series: `toolnexus_llm_requests_total{model,status}`, `toolnexus_llm_tokens_total{type}`,
197
- `toolnexus_tool_calls_total{tool,source,is_error}`, `toolnexus_run_errors_total{model}`, plus the
198
- `toolnexus_llm_request_duration_seconds` and `toolnexus_tool_duration_seconds` histograms. The
199
- rendered text is byte-identical across all five ports; OTLP push is a planned future companion.
200
-
201
- ## Add your own tools
202
-
203
- ```ts
204
- import { defineTool, httpTool } from "toolnexus"
205
-
206
- tk.register(
207
- // a plain function → a tool (run returns a string or a full ToolResult)
208
- defineTool({
209
- name: "add",
210
- description: "Add two numbers",
211
- inputSchema: {
212
- type: "object",
213
- properties: { a: { type: "number" }, b: { type: "number" } },
214
- required: ["a", "b"],
215
- },
216
- run: (args) => `${(args.a as number) + (args.b as number)}`,
217
- }),
218
- // a REST endpoint → a tool
219
- httpTool({
220
- name: "create_ticket",
221
- description: "Create a ticket",
222
- method: "POST",
223
- url: "https://api.example.com/tickets",
224
- headers: { Authorization: "Bearer ${API_TOKEN}" }, // ${ENV} expands from process.env, never logged
225
- inputSchema: {
226
- type: "object",
227
- properties: { title: { type: "string" } },
228
- required: ["title"],
229
- },
230
- }),
231
- )
232
- ```
233
-
234
- For `httpTool`: URL `{placeholders}` are filled from args; `query` arg names go to the
235
- querystring (all args on GET); the rest become the JSON body (`body: "json" | "form" | "raw"`).
236
- Non-2xx → `{ isError: true, output: "HTTP <status>: <body>" }`.
237
-
238
- ## Images, PDFs and audio (content parts)
239
-
240
- Parts go in the **first** argument, next to your text, because the order of text and image is
241
- semantic to a model:
242
-
243
- ```ts
244
- import { attach, text } from "toolnexus"
245
-
246
- const { text: answer } = await agent.run(
247
- ["What is broken in this screenshot?", await attach("./shot.png")],
248
- { toolkit: tk },
249
- )
250
- ```
251
-
252
- `attach(source, opts?)` takes what you already hold: a filesystem path, a `data:` URL, an
253
- `https:` URL, native bytes (`Uint8Array`, `Buffer`, `ArrayBuffer`, any `ArrayBufferView` —
254
- `mimeType` required, bytes carry no extension), or a `Blob`/`File` straight off an upload.
255
- `fromPath`, `fromBytes`, `fromBlob`, `fromDataUrl`, `fromUrl` are the same thing spelled
256
- explicitly. Mime types come from a fixed extension table (`png jpg jpeg gif webp pdf mp3 wav`) —
257
- never sniffed, never read from the machine's mime database, so every port agrees. A plain string
258
- prompt is untouched and byte-identical to before.
259
-
260
- A tool can answer with media too — `parts` alongside the still-required `output`, which is what
261
- the transcript, compaction and any text-only provider keep seeing:
262
-
263
- ```ts
264
- defineTool({
265
- name: "screenshot",
266
- description: "Grab the screen",
267
- inputSchema: { type: "object", properties: {} },
268
- run: async () => ({
269
- output: "screenshot, 1280x720 png",
270
- parts: [await attach("./shot.png")],
271
- }),
272
- })
273
- ```
274
-
275
- **MCP tools' images now survive.** Non-text content from an MCP server — image, audio, embedded
276
- resource, `resource_link` — used to be filtered out silently; it is preserved as `parts` and
277
- emitted per provider style (Anthropic inside `tool_result`; OpenAI, whose `tool` message rejects
278
- an image, in a synthetic user message that never enters your transcript).
279
-
280
- Whatever you pass in, a part stores **bytes plus a `mimeType`** — never a handle, never a path —
281
- so a persisted transcript replays without the original file. A `Blob` is read **eagerly** at
282
- construction, and a handle you supply is read, not closed.
283
-
284
- ## Built-in tools
285
-
286
- A fifth source ships **10 built-in tools** — `bash`, `read`, `write`, `edit`, `grep`, `glob`,
287
- `webfetch`, `question`, `apply_patch`, `todowrite` (names + input schemas match opencode) — so an
288
- agent can act with zero wiring. They appear in the tool schema
289
- (`toOpenAI()`/`toAnthropic()`/`toGemini()`), like MCP tools — not the system prompt.
290
-
291
- **On by default.** One global toggle turns the whole source off, or a per-tool `tools` map
292
- disables individual builtins on the all-on baseline:
293
-
294
- ```ts
295
- const tk = await createToolkit({ builtins: false })
296
- // also accepts { disabled: true } or { enabled: false }
297
-
298
- // per-tool: drop bash, keep the other nine (unknown names ignored; whole-source-off still wins)
299
- const tk2 = await createToolkit({ builtins: { tools: { bash: false } } })
300
- ```
301
-
302
- `bash`/`write`/`edit`/`apply_patch` run commands and mutate the filesystem — the toggle is the
303
- off-switch for locked-down hosts.
304
-
305
- ## A2A agents (agent-to-agent)
306
-
307
- Call **remote A2A agents** (each of their skills becomes a tool) and serve your own toolkit as an
308
- agent other A2A peers can call. A genuine, minimal subset of real A2A (JSON-RPC 2.0; Agent Card at
309
- `/.well-known/agent-card.json`; `SendMessage` → poll `GetTask`). No streaming / push / auth in v1.
310
-
311
- **Outbound — call a remote agent.** Each advertised skill becomes a tool named `<agent>_<skill>`
312
- (`source: "a2a"`):
313
-
314
- ```ts
315
- import { createToolkit, agent } from "toolnexus"
316
-
317
- const tk = await createToolkit({
318
- agents: [agent({ card: "https://researcher.example.com/.well-known/agent-card.json" })],
319
- })
320
-
321
- // or add one at runtime (an Agent descriptor or a bare card URL):
322
- await tk.addAgent("https://writer.example.com/.well-known/agent-card.json")
323
- ```
324
-
325
- `agent({ card, headers?, timeout?, pollEvery? })` — `headers` support `${ENV}` expansion (never
326
- logged); `timeout` defaults to 300000ms, `pollEvery` to 1000ms. A config object can also carry an
327
- `agents` block (mirrors `mcpServers`). A failing agent is isolated — logged, contributes no tools,
328
- never fatal.
329
-
330
- **Inbound — serve your toolkit as an agent.** Opt in with the `a2a` profile; the Agent Card is
331
- built from your **SKILL.md skills** (never raw tools):
332
-
333
- ```ts
334
- const handle = await tk.serve("127.0.0.1:0", {
335
- client: agent, // a createClient(...) instance fulfils each task
336
- a2a: {
337
- name: "research-agent",
338
- description: "Answers research questions.",
339
- // skills: ["hello-world"], // subset of skills to advertise; omit ⇒ all
340
- store: "memory", // "memory" (default) | "file:<dir>" | a custom TaskStore
341
- },
342
- })
343
- console.log(handle.url) // GET /.well-known/agent-card.json ; POST / (SendMessage / GetTask)
344
- await handle.stop()
345
- ```
346
-
347
- `serve(addr, { client, a2a?, onTask? })` fulfils each inbound `SendMessage` task via the client.
348
- Task persistence is a pluggable `TaskStore` — in-memory default, `"file:<dir>"`, or your own. And
349
- it **remembers**: a message's A2A `contextId` keys the conversation through `client.ask`, so a
350
- peer's turns in the same context carry history via the client's `ConversationStore`.
351
-
352
- ## Serve as an MCP server (be a gateway)
353
-
354
- The inbound mirror of A2A: expose your **whole toolkit as an MCP server** so any MCP client — an IDE,
355
- another agent, a remote host — can call its tools. Point toolnexus at N MCP servers + skills + your
356
- own functions, then re-expose the union as **one** MCP server. Unlike A2A (which advertises skills and
357
- runs the client loop), the MCP client *is* the LLM host, so each `tools/call` dispatches straight to
358
- `Tool.execute` — no `client`, no tasks, no store.
42
+ Add real tool sources by pointing at them:
359
43
 
360
44
  ```ts
361
- // streamable-HTTP — an embeddable MCP server mounted at POST /mcp, beside any A2A routes:
362
- const srv = await tk.serve("127.0.0.1:0", {
363
- mcp: { name: "my-gateway" /*, tools: ["echo"] // subset; omit ⇒ every toolkit tool */ },
364
- onCall: (ev) => console.error(ev.name, ev.ms, ev.isError),
365
- })
366
- console.log(srv.url + "/mcp") // connect any MCP client here
367
- await srv.stop()
45
+ const tk = await createToolkit({ mcp: "mcp.json", skills: ["skills"] })
368
46
  ```
369
47
 
370
- `tools/list` advertises every toolkit tool (name **verbatim**, `inputSchema` = the tool's parameters);
371
- `mcp.tools` narrows the surface. The `mcp` profile can also live in the config file as a top-level
372
- **`mcpServer`** block (singular — distinct from the client-side `mcpServers`). (Transport is
373
- streamable-HTTP; a stdio transport for local clients like Claude Desktop is a planned follow-up.)
374
-
375
- ## Bring your own loop
376
-
377
- Don't want the host loop? Use the schema adapters and execute calls yourself:
378
-
379
- ```ts
380
- const tools = tk.toOpenAI() // or tk.toAnthropic() / tk.toGemini()
381
- const system = tk.skillsPrompt() // skills catalog for your system prompt
382
- // when the model returns a tool call { name, arguments }:
383
- const res = await tk.execute(name, args) // → { output, isError, metadata }
384
- ```
385
-
386
- ## The four sources
387
-
388
- | Source | How |
389
- |--------|-----|
390
- | **MCP servers** | an `mcp.json` (`mcpServers`/`servers`/`mcp`); local stdio + remote streamable-HTTP, `headers` for auth |
391
- | **Agent skills** | a folder of `<name>/SKILL.md`; a `skill` tool loads each on demand + a system-prompt catalog |
392
- | **Native tools** | `defineTool({...})` — a function becomes a tool |
393
- | **HTTP / REST** | `httpTool({...})` — an endpoint becomes a tool, `${ENV}` headers |
394
-
395
- All appear as one uniform `Tool` in `tk.tools()`. Built-ins are a fifth source; remote A2A agents
396
- a sixth; in-process **sub-agents** a seventh (below).
397
-
398
- ## Sub-agents & teams
399
-
400
- An **Agent is a Tool**: a system prompt × a scoped toolkit view × the client loop. One agent
401
- delegates to another **in-process** via one `task` tool — isolated context, one result back,
402
- tokens rolled up, hierarchical budgets, durable suspension (`SPEC.md §7D`). The surface lives in
403
- the **`agents`** namespace (never the A2A `agent` above):
404
-
405
- ```ts
406
- import { agents } from "toolnexus"
407
-
408
- const explore = agents.agent("explore", {
409
- does: "read-only research",
410
- uses: { tools: [lookup] }, // this child sees ONLY these tools
411
- })
412
- const coder = agents.agent("coder", {
413
- does: "implements changes",
414
- soulFile: "./AGENTS.md", // identity file → system prompt
415
- team: [explore], // team = the task tool's only targets; no team ⇒ no task tool
416
- budget: { maxTokens: 10_000 },
417
- })
418
-
419
- const r = await coder.run("fix the failing test", {
420
- llm: { baseUrl: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini" },
421
- })
422
- console.log(r.status, r.text, r.totalTokens) // "done" | "incomplete" | "pending", final text, tree-wide tokens
423
- ```
424
-
425
- `agents.asTool(...)` drops an agent back into `extraTools` for the classic API. Full guide:
426
- [Sub-agents & teams](https://muthuishere.github.io/toolnexus/subagents/).
427
-
428
- ## API
48
+ - `mcp.json` is the standard Claude-desktop-style config (`mcpServers` / `servers` / `mcp` keys all accepted).
49
+ - `skills/` is a folder of `**/SKILL.md` files, loaded on demand through one `skill` tool.
50
+ - Remote MCP `headers` values expand `${ENV_VAR}` at call time and are never logged.
429
51
 
430
- | Member | Description |
431
- |--------|-------------|
432
- | `createToolkit(opts)` | async factory → `Toolkit` (`mcpConfig`, `skillsDir`, `builtins`, `agents`, `extraTools`) |
433
- | `createClient(opts)` | the unified host loop (`baseUrl`, `style`, `model`, `store?`, `hooks?`, `maxTurns?`, `retries?`, `timeoutMs?`, `onMetric?`) |
434
- | `client.run(prompt, { toolkit, signal?, history? })` | stateless run → `RunResult` |
435
- | `client.ask(prompt, { toolkit, id?, on_text?, signal? })` | stateful with `id` (loads → runs → saves via `store`); one-shot without `id`; `on_text` streams text deltas |
436
- | `client.conversation({ toolkit })` | in-process multi-turn object — `.send(prompt)`, `.messages`, `.reset()` |
437
- | `client.stream(prompt, { toolkit, id? })` | async-iterate `StreamEvent`s (text deltas, tool calls/results, usage, done); `id` ⇒ stateful |
438
- | `client.metrics()` | Prometheus text exposition of cumulative metrics — mount at `GET /metrics` |
439
- | `ConversationStore` | `get(id)` / `save(id, messages)` — implement for file/db; default `InMemoryConversationStore` |
440
- | `tk.tools()` / `tk.get(name)` | the uniform tools |
441
- | `tk.register(...tools)` | add native/http/custom tools at runtime |
442
- | `tk.execute(name, args, ctx?)` | run a tool → `ToolResult` |
443
- | `tk.skillsPrompt()` | system-prompt skill catalog |
444
- | `tk.mcpStatus()` | per-server connection status |
445
- | `tk.toOpenAI()` / `toAnthropic()` / `toGemini()` | provider tool schemas |
446
- | `tk.serve(addr, { client?, a2a?, onTask?, mcp?, onCall? })` | serve the toolkit over HTTP — A2A agent and/or streamable-HTTP MCP server (`/mcp`) → `ServeHandle` |
447
- | `tk.addAgent(cardUrl \| Agent)` | fetch a remote agent's card and register its skills as tools |
448
- | `tk.close()` | disconnect MCP servers |
449
- | `defineTool` / `httpTool` / `agent` | build native / HTTP / A2A tools |
450
- | `InMemoryConversationStore` · `InMemoryTaskStore` / `FileTaskStore` | default stores (memory / A2A tasks) |
52
+ ## Documentation
451
53
 
452
- ## More
54
+ Everything else — the full surface, with runnable examples — lives on the docs site:
453
55
 
454
- Full docs, the other four language ports, and the shared cross-language behavior contract
455
- ([`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md)) — plus runnable
456
- `examples/`: **https://github.com/muthuishere/toolnexus**
56
+ | | |
57
+ |---|---|
58
+ | **Start here** | [Quickstart](https://muthuishere.github.io/toolnexus/quickstart/) · [Concepts](https://muthuishere.github.io/toolnexus/concepts/) · [Install](https://muthuishere.github.io/toolnexus/install/) |
59
+ | **Tool sources** | [MCP](https://muthuishere.github.io/toolnexus/mcp/) · [Skills](https://muthuishere.github.io/toolnexus/skills/) · [Native](https://muthuishere.github.io/toolnexus/native/) · [HTTP](https://muthuishere.github.io/toolnexus/http/) · [Built-ins](https://muthuishere.github.io/toolnexus/builtins/) · [A2A](https://muthuishere.github.io/toolnexus/a2a/) |
60
+ | **The loop** | [Streaming](https://muthuishere.github.io/toolnexus/streaming/) · [Memory](https://muthuishere.github.io/toolnexus/memory/) · [Suspension](https://muthuishere.github.io/toolnexus/suspension/) · [Resilience](https://muthuishere.github.io/toolnexus/resilience/) · [Observability](https://muthuishere.github.io/toolnexus/observability/) |
61
+ | **Agents** | [Sub-agents & teams](https://muthuishere.github.io/toolnexus/subagents/) · [Personas](https://muthuishere.github.io/toolnexus/persona-agents/) · [Typed decisions](https://muthuishere.github.io/toolnexus/judge/) |
62
+ | **API reference** | [JavaScript](https://muthuishere.github.io/toolnexus/api/javascript/) |
63
+ | **Cookbook** | [Zero to agent](https://muthuishere.github.io/toolnexus/cookbook/zero-to-agent/) · [MCP servers](https://muthuishere.github.io/toolnexus/cookbook/mcp-servers/) · [Agent skills](https://muthuishere.github.io/toolnexus/cookbook/agent-skills/) · [Judge](https://muthuishere.github.io/toolnexus/cookbook/judge/) |
457
64
 
458
- MIT licensed.
65
+ Contract across all seven ports: [`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md).
package/dist/acp.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { InProcessRequest, InProcessResponse } from "./client.js";
2
+ /** Options for {@link loadACP}. `command`/`args` spawn the ACP agent as a child
3
+ * process; everything else tunes the session handshake. */
4
+ export interface ACPOptions {
5
+ /** The ACP agent binary, e.g. `"devin"`. */
6
+ command: string;
7
+ /** Arguments to the agent, e.g. `["acp"]`. */
8
+ args?: string[];
9
+ /** Working directory for `session/new`. Real ACP agents (e.g. `devin acp`)
10
+ * reject a relative `cwd` with `-32602 Invalid params` — this is always
11
+ * resolved to an absolute path before it is sent. Default: `process.cwd()`. */
12
+ cwd?: string;
13
+ /** Extra environment variables for the child, merged over the current
14
+ * process environment. */
15
+ env?: Record<string, string>;
16
+ /** ACP `initialize` protocolVersion. Default `1`. */
17
+ protocolVersion?: number;
18
+ /** Safety-net timeout (ms) for one `session/prompt` turn — NOT specifically
19
+ * for the permission handshake, which this client always answers instantly.
20
+ * It exists only so a broken/unresponsive agent cannot hang a caller
21
+ * forever. Default 30000. */
22
+ permissionTimeoutMs?: number;
23
+ /** Optional `session/set_mode` sent once, right after `session/new`. */
24
+ mode?: string;
25
+ }
26
+ /** One warm ACP connection: one child process, one session, alive across many
27
+ * turns. `.generate` has exactly the shape {@link InProcessOptions.generate}
28
+ * wants, so it plugs straight into {@link createInProcessClient}. */
29
+ export interface ACPClient {
30
+ generate: (request: InProcessRequest) => Promise<InProcessResponse>;
31
+ /** Terminates the agent process. Idempotent — a second call is a no-op. */
32
+ close: () => Promise<void>;
33
+ }
34
+ /**
35
+ * Opens one ACP connection: spawns `command`, performs `initialize` →
36
+ * `session/new` (with an absolute `cwd` and an `mcpServers` array — the exact
37
+ * trap real `devin acp` enforces with `-32602` otherwise) → an optional
38
+ * `session/set_mode`, then returns a client whose `.generate` sends exactly
39
+ * one `session/prompt` per call, accumulating only `agent_message_chunk`
40
+ * text.
41
+ */
42
+ export declare function loadACP(opts: ACPOptions): Promise<ACPClient>;