maestro-agent-sdk 0.1.13 → 0.1.16

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.
Files changed (52) hide show
  1. package/README.md +214 -163
  2. package/dist/core/agent.d.ts +7 -1
  3. package/dist/core/agent.d.ts.map +1 -1
  4. package/dist/core/agent.js +2 -0
  5. package/dist/core/agent.js.map +1 -1
  6. package/dist/core/loop.d.ts.map +1 -1
  7. package/dist/core/loop.js +77 -15
  8. package/dist/core/loop.js.map +1 -1
  9. package/dist/index.d.ts +7 -6
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -5
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/pool.d.ts +2 -2
  14. package/dist/mcp/pool.d.ts.map +1 -1
  15. package/dist/mcp/pool.js.map +1 -1
  16. package/dist/platform/env-bootstrap.js +4 -1
  17. package/dist/platform/env-bootstrap.js.map +1 -1
  18. package/dist/platform/logger.d.ts.map +1 -1
  19. package/dist/platform/logger.js.map +1 -1
  20. package/dist/platform/version.d.ts +1 -1
  21. package/dist/platform/version.js +1 -1
  22. package/dist/provider.d.ts +72 -17
  23. package/dist/provider.d.ts.map +1 -1
  24. package/dist/provider.js +119 -29
  25. package/dist/provider.js.map +1 -1
  26. package/dist/providers/anthropic.d.ts +98 -26
  27. package/dist/providers/anthropic.d.ts.map +1 -1
  28. package/dist/providers/anthropic.js +158 -31
  29. package/dist/providers/anthropic.js.map +1 -1
  30. package/dist/providers/deepseek.d.ts +16 -5
  31. package/dist/providers/deepseek.d.ts.map +1 -1
  32. package/dist/providers/deepseek.js +17 -6
  33. package/dist/providers/deepseek.js.map +1 -1
  34. package/dist/sub-agent/runner.d.ts.map +1 -1
  35. package/dist/sub-agent/runner.js +1 -1
  36. package/dist/sub-agent/runner.js.map +1 -1
  37. package/dist/tools/builtin/bash.d.ts.map +1 -1
  38. package/dist/tools/builtin/bash.js.map +1 -1
  39. package/dist/tools/builtin/edit.d.ts.map +1 -1
  40. package/dist/tools/builtin/edit.js.map +1 -1
  41. package/dist/tools/builtin/multi_edit.d.ts.map +1 -1
  42. package/dist/tools/builtin/multi_edit.js.map +1 -1
  43. package/dist/tools/builtin/write.d.ts.map +1 -1
  44. package/dist/tools/builtin/write.js.map +1 -1
  45. package/dist/tools/index.d.ts +36 -0
  46. package/dist/tools/index.d.ts.map +1 -0
  47. package/dist/tools/index.js +40 -0
  48. package/dist/tools/index.js.map +1 -0
  49. package/dist/types.d.ts +78 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js.map +1 -1
  52. package/package.json +1 -1
package/README.md CHANGED
@@ -4,25 +4,18 @@
4
4
  [![npm version](https://img.shields.io/npm/v/maestro-agent-sdk.svg)](https://www.npmjs.com/package/maestro-agent-sdk)
5
5
  [![license](https://img.shields.io/npm/l/maestro-agent-sdk.svg)](./LICENSE)
6
6
 
7
- **Embeddable agent SDK that ships skills, memory, and MCP out of the box.**
7
+ **Embeddable agent SDK skills, memory, MCP, and host-controlled guardrails out of the box.**
8
8
  Anthropic + DeepSeek today, BYO-provider in one file. No CLI, no gateway, no host lock-in.
9
9
 
10
10
  > **Status:** Early port (v0.1.x). Active development. API surface may change before 1.0.
11
11
 
12
- Inspired by [Claude Code](https://www.anthropic.com/claude-code) and [`hermes-agent`](https://github.com/NousResearch/hermes-agent) same agent-loop shape, repackaged as an embeddable TypeScript library.
13
-
14
- ### How it compares
15
-
16
- | | What you get |
17
- |---|---|
18
- | **vs [`@anthropic-ai/claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-typescript)** | Multi-provider from day one (Anthropic + DeepSeek), with skills (`SKILL.md` / `skill.md` indexing), memory (auto context compaction), and MCP client pool built in — not provided as separate add-ons. |
19
- | **vs LangChain / LangGraph** | Thin loop, no DSL. A provider is one adapter file; a tool is `{ name, description, schema, run }`. You read the source in an afternoon. |
12
+ A generalizable agent runtime. Swap providers, inject your own logger/MCP resolver/hooks, and embed it in any host process no framework, no lock-in.
20
13
 
21
14
  ## What's in the box
22
15
 
23
- - **Agent loop** — provider-driven tool-calling loop with iteration cap, abort signal, and event stream.
16
+ - **Agent loop** — provider-driven tool-calling loop with iteration cap, abort signal, LLM pre/post guardrail hooks, and event stream.
24
17
  - **Pluggable providers** — first-class adapters for Anthropic (Claude) and DeepSeek V4; provider-neutral message schema so adding OpenAI / Gemini / Ollama is a thin file.
25
- - **Built-in tools** — `bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `Agent` (sub-agent delegation), `TaskCreate`/`TaskUpdate`/`TaskList`/`TaskGet`, `WebFetch`, `skill_view`, `skill_write`. Bring your own via `ToolRegistry`. Grep shells out to ripgrep (`rg`) so install it if you want the tool active; the SDK surfaces a structured error pointing to the install path when missing.
18
+ - **Built-in tools** — `bash`, `Read`, `Write`, `Edit`, `MultiEdit`, `Glob`, `Grep`, `Agent` (sub-agent delegation), `TaskCreate`/`TaskUpdate`/`TaskList`/`TaskGet`, `WebFetch` (optional SSRF policy via `createWebFetchTool`), `skill_view`, `skill_write`. Bring your own via `ToolRegistry`. Grep shells out to ripgrep (`rg`) so install it if you want the tool active; the SDK surfaces a structured error pointing to the install path when missing. Tool primitives are also importable from the `maestro-agent-sdk/tools` subpath when you don't need the rest of the runtime.
26
19
  - **MCP** — built-in client pool (stdio + SSE) so any MCP server (`@modelcontextprotocol/sdk`) shows up as tools.
27
20
  - **Skills** — per-workspace `.skills/<skillKey>/<name>/skill.md` packages with FTS-style indexing, on-demand body load (`skill_view`), and agent-autonomous authoring (`skill_write`).
28
21
  - **Memory** — automatic context compression (summarization + pruning) when the token budget is hit. Reuses the agent's own model for compaction — no separate model knob.
@@ -107,133 +100,67 @@ for await (const event of runConversation(agent, "Summarize today's news.")) {
107
100
  }
108
101
  ```
109
102
 
110
- > **Effort scale.** `effort` drives both the thinking budget _and_ the
111
- > tool-iteration cap. The model also sees its remaining-iteration count in a
112
- > `<system-reminder>` block every turn so it can self-pace. Knobs:
103
+ > **Effort scale (v0.1.16+).** `effort` controls two orthogonal knobs:
104
+ >
105
+ > 1. **Reasoning depth** thinking budget on Anthropic (`thinking.budget_tokens`),
106
+ > `reasoning_effort` on DeepSeek.
107
+ > 2. **Working-mode persona** — a `## Working mode` block injected into the
108
+ > system prompt with imperative verbs the model conditions on from turn 1
109
+ > (e.g. `low` → "answer fast, one Read max", `max` → "exhaustive,
110
+ > enumerate failure modes"). Pure function of `effort`, prefix-cache stable.
111
+ >
112
+ > The **tool-iteration cap is no longer derived from effort** as of v0.1.16 —
113
+ > it's a single host-tunable default (`DEFAULT_MAX_ITERATIONS = 90`) that you
114
+ > override per call via `AgentQueryOptions.maxIterations`. The default
115
+ > matches v0.1.15's old `xhigh` cap (the "extended exploration" baseline)
116
+ > so existing callers that didn't pin a value see no behavior change. This
117
+ > lets a host mix and match: `effort: "low"` + `maxIterations: 90` (terse,
118
+ > but don't trip on a surprise sub-task), or `effort: "max"` +
119
+ > `maxIterations: 30` (think hard, but stay snappy).
120
+ >
121
+ > | effort | thinking budget (Anthropic) | DeepSeek `reasoning_effort` |
122
+ > |---------|----------------------------:|:---------------------------:|
123
+ > | `low` | 2 048 | `low` |
124
+ > | `medium`| 8 192 | `medium` |
125
+ > | `high` | 16 384 | `high` |
126
+ > | `xhigh` | 16 384 | `high` |
127
+ > | `max` | 32 768 | `max` |
128
+ >
129
+ > **`xhigh` shares `high`'s thinking ceiling** — the difference is persona,
130
+ > not budget. `xhigh` tells the model to use the same allowance more broadly
131
+ > (hold multiple hypotheses, survey, name edge cases). On sonnet-4-6 /
132
+ > haiku-4-5 the answer-quality return on thinking above ~16K dropped off
133
+ > sharply in practice, so the lever became persona instead of more tokens.
134
+ >
135
+ > **`max` is halved from v0.1.15's 65 536** — 64K thinking is rarely fully
136
+ > utilized in a single turn; the latency penalty was unrecouped. 32K is the
137
+ > ceiling for "really chew on this" without paying for headroom the model
138
+ > doesn't reach.
113
139
  >
114
- > | effort | thinking budget | iteration cap |
115
- > |---------|----------------:|--------------:|
116
- > | `low` | 2 048 | 5 |
117
- > | `medium`| 8 192 | 20 |
118
- > | `high` | 16 384 | 50 |
119
- > | `xhigh` | 32 768 | 90 |
120
- > | `max` | 65 536 | 200 |
140
+ > DeepSeek's API ships four tiers; maestro's `xhigh` maps to DeepSeek `high`
141
+ > (not `max`) so that `max` stays reserved for the explicit "deepest
142
+ > reasoning" opt-in.
143
+ >
144
+ > **Turn-adaptive budget (v0.1.16).** The per-turn thinking budget the loop
145
+ > actually sends to the API is *not* constant — it's resolved through
146
+ > `thinkingBudgetForTurn(base, iter, maxIter)`:
147
+ >
148
+ > - **First turn** (`iter == 0`) — full base. Planning gets the full
149
+ > allowance because a careful first-turn plan saves tool calls later.
150
+ > - **Middle turns** — full base. Interleaved thinking between tool calls
151
+ > is what Anthropic's interleaved-thinking beta is for; cutting it
152
+ > mid-flow defeats the beta.
153
+ > - **Last 3 turns** (wrap-up zone) — `base / 4`, floored at 1024 (the
154
+ > Anthropic API minimum). The iteration reminder has already flipped to
155
+ > "finalize NOW"; spending another 16K thinking on a turn that mostly
156
+ > emits final text is pure latency waste.
157
+ >
158
+ > The model still sees a remaining-iteration count in the per-turn
159
+ > `<system-reminder>` so it can self-pace within the cap you set.
121
160
 
122
161
  More runnable scripts live under [`examples/`](./examples) — Anthropic, DeepSeek,
123
162
  a custom-tool walkthrough, and a `skill_write` demo.
124
163
 
125
- ## Skills — per-workspace, agent-autonomous
126
-
127
- Skill catalog routing is deterministic from `(opts.cwd, opts.skillKey)`:
128
-
129
- ```
130
- skillKey set → <cwd>/.skills/<skillKey>/
131
- skillKey unset → <cwd>/.skills/default/ (uses MAESTRO_DEFAULT_SKILL_KEY)
132
- ```
133
-
134
- Every skill lives under a **named key** subdirectory. The SDK never reads from
135
- `<cwd>/.skills/` directly, so a host can list "which profiles exist in this
136
- workspace?" with one `readdir`. One workspace can host multiple disjoint
137
- catalogs (e.g. `legal/`, `coding/`, `research/`) and each session selects its
138
- profile by passing `skillKey`.
139
-
140
- ### On-disk layout
141
-
142
- ```
143
- <cwd>/.skills/
144
- ├── default/ ← skillKey omitted
145
- │ └── general/note-template/skill.md
146
- ├── legal/ ← skillKey: "legal"
147
- │ └── general/
148
- │ ├── ocr/
149
- │ │ ├── skill.md
150
- │ │ ├── scripts/preprocess.py
151
- │ │ └── references/api.md
152
- │ └── hearing-report/skill.md
153
- └── coding/ ← skillKey: "coding"
154
- └── general/code-review/skill.md
155
- ```
156
-
157
- ### Manifest format (clawgram-style)
158
-
159
- Two filename conventions are accepted: `SKILL.md` (upstream v0.13.0 with YAML
160
- frontmatter) and `skill.md` (lowercase, body-based). For new skills the
161
- clawgram convention is recommended:
162
-
163
- ```markdown
164
- # OCR 텍스트 추출 (English subtitle)
165
-
166
- > **Description**: OCR, 이미지 읽어줘, PDF 텍스트 추출 요청 시 트리거.
167
-
168
- ## Required MCP
169
- - ocr
170
- - paddleocr
171
-
172
- ## 트리거
173
- - ...
174
-
175
- ## 프로세스
176
- ### 1. 이미지 준비
177
- ### 2. paddleocr 실행
178
-
179
- ## Gotchas
180
- - 흐릿한 이미지는 deskew 필요
181
- ```
182
-
183
- The first heading is the display title; the `> **Description**: ...` blockquote
184
- carries the trigger keywords (this drives system-prompt activation). The
185
- loader extracts the description from either YAML frontmatter or this
186
- blockquote — both styles can coexist in the same `.skills/<key>/` tree.
187
-
188
- ### Authoring from inside the agent — `skill_write`
189
-
190
- The model can persist new skills mid-session, including adjacent assets
191
- (scripts, templates, references), in one transactional call:
192
-
193
- ```ts
194
- skill_write({
195
- name: "ocr", // kebab-case, becomes the folder name
196
- content: "# OCR ...\n\n> **Description**: OCR, 이미지 읽어줘\n\n...",
197
- files: {
198
- "scripts/preprocess.py": "import cv2\n...",
199
- "scripts/run.sh": "#!/bin/bash\n...",
200
- "templates/report.html": "<!doctype html>...",
201
- "references/paddleocr-api.md": "# PaddleOCR API\n...",
202
- },
203
- overwrite: false, // default: refuse to clobber
204
- });
205
- ```
206
-
207
- Resulting layout under `<skillsDir>/ocr/`:
208
-
209
- ```
210
- ocr/
211
- ├── skill.md ← from `content`
212
- ├── scripts/
213
- │ ├── preprocess.py
214
- │ └── run.sh
215
- ├── templates/report.html
216
- └── references/paddleocr-api.md
217
- ```
218
-
219
- Safety:
220
-
221
- - kebab-case validation on `name`
222
- - relative-path validation on every `files` key (rejects `..` escapes,
223
- absolute prefixes, backslashes, and the reserved `skill.md` name)
224
- - `overwrite=false` → batch aborts BEFORE any disk touch if any target
225
- already exists (validate-all-then-write)
226
- - cache invalidation on success → the new skill appears in the NEXT turn's
227
- `<available_skills>` catalog (intentionally not the current turn — would
228
- break the prompt cache)
229
-
230
- ### Reading from the model side — `skill_view`
231
-
232
- The system prompt only carries name + summary per skill (FTS-style index).
233
- When the model decides a skill is relevant it calls `skill_view(name)` and
234
- gets the full body back, with a `[Skill directory: ...]` hint so relative
235
- paths in the body resolve against the right cwd.
236
-
237
164
  ## Configuration
238
165
 
239
166
  Per-call options on `AgentQueryOptions`:
@@ -241,6 +168,8 @@ Per-call options on `AgentQueryOptions`:
241
168
  | Option | Required | Purpose |
242
169
  |---|---|---|
243
170
  | `cwd` | ✓ | Workspace root. Drives `.skills/` location, rollout `_meta`, and the `mkdir` invariant. |
171
+ | `effort` | — | Reasoning depth + working-mode persona (`low`/`medium`/`high`/`xhigh`/`max`). See the effort table above. |
172
+ | `maxIterations` | — | Tool-iteration cap. Omit for `DEFAULT_MAX_ITERATIONS = 90`. Decoupled from `effort` as of v0.1.16 — controls turn budget, not reasoning depth. |
244
173
  | `skillKey` | — | Named skill profile within `<cwd>/.skills/`. Omit for `default`. |
245
174
  | `allowedSkills` | — | Per-call name whitelist applied before curation. |
246
175
  | `sessionMetadata` | — | Opaque host bag round-tripped via the rollout `_meta` header. |
@@ -322,7 +251,7 @@ Each session JSONL at `~/.maestro/sessions/<sessionId>.jsonl` carries a
322
251
  `_meta` header line for forensics and host-side indexing:
323
252
 
324
253
  ```jsonl
325
- {"_meta":{"version":1,"cwd":"/path","skillKey":"legal","userId":"...","createdAt":"2026-05-18T...","sdkVersion":"0.1.5","skillsDir":"...","metadata":{...}}}
254
+ {"_meta":{"version":1,"cwd":"/path","skillKey":"legal","userId":"...","createdAt":"2026-05-18T...","sdkVersion":"0.1.x","skillsDir":"...","metadata":{...}}}
326
255
  {"role":"user","content":"..."}
327
256
  {"role":"assistant","content":[...]}
328
257
  ```
@@ -332,25 +261,18 @@ treats their first line as a regular message. Hosts that want to inspect
332
261
  session metadata without reading the full message log can call
333
262
  `loadMaestroSessionMeta(sessionId)`.
334
263
 
335
- ## Architecture
264
+ ## Positioning — a building block, not a product
336
265
 
337
- ```
338
- src/
339
- ├── core/ AIAgent class + run_conversation loop
340
- ├── tools/ ToolRegistry + builtin tools + PreToolUse/PostToolUse hook surface
341
- ├── providers/ Provider adapters (anthropic, deepseek)
342
- ├── mcp/ MCP client pool (stdio + SSE)
343
- ├── skills/ Skill loader, index builder, usage tracker, curator
344
- ├── memory/ Context compressor, token estimator, reminders, scrubber
345
- ├── state/ Per-session todo store
346
- ├── sub-agent/ Sub-agent runner for the `Agent` tool
347
- ├── platform/ Injectable host adapters (logger, lifecycle, config, jsonl, version, mcp-config)
348
- ├── agents/ Cross-agent rollout helpers + per-agent registry contract
349
- ├── storage/ ConversationReader DI (host supplies past turns for cross-agent forks)
350
- └── media/ File-event extraction from inline `[FILE:/path]` tags
351
- ```
266
+ maestro-agent-sdk is an agent *runtime*, not an agent *product*. You pick the UI, the provider mix, the guardrail rules, the storage layer.
267
+
268
+ | Project | Layer | Key trade-off |
269
+ |---------|-------|---------------|
270
+ | **maestro-agent-sdk** | Embeddable SDK | Agent loop only — no CLI, no UI, no fixed product shape. Host injects logger, MCP resolver, session store, guardrails. |
271
+ | **hermes-agent** | Full-featured app | TUI, web dashboard, gateway, cron, Discord/Feishu. All-in-one opinionated and coupled to its own host. |
272
+ | **OpenAI Agents SDK** | SDK + scaffold | Strong guardrails/tracing/handoffs, but multi-agent by design — heavier abstraction surface. |
273
+ | **oh-my-claudecode** | Orchestration plugin | Sits on Claude Code agent loop. Value is team mode, LSP tools, session replay. |
352
274
 
353
- The `platform/`, `storage/`, and `agents/contracts` modules expose **injection points** so the SDK never assumes a particular host process.
275
+ **maestro-agent-sdk leaves product decisions to you.** Same `AIAgent` works in a Telegram bot, cron runner, or code review pipeline.
354
276
 
355
277
  ## Host integration (DI)
356
278
 
@@ -374,26 +296,155 @@ setMcpResolver((opts) => ({
374
296
  setConversationReader((userId, topic, groupId) => myStore.read({ userId, topic, groupId }));
375
297
  ```
376
298
 
299
+
300
+ ## Skills — drop a directory, get indexed context
301
+
302
+ Skills are `SKILL.md` (or `skill.md`) files inside `<cwd>/.skills/<skillKey>/<name>/`. The SDK walks that tree on first turn, parses each file's YAML frontmatter, and appends a `## Skills (mandatory)` block to the system prompt with one `name + 60-char description` line per skill. Bodies stay on disk — the model calls `skill_view(name)` to load the full markdown on demand. Index is cached per (root, mtime, TTL) so subsequent turns pay no walk cost.
303
+
304
+ ```ts
305
+ import { maestroProvider } from "maestro-agent-sdk";
306
+
307
+ // `maestroProvider` is the batteries-included entry point: it builds the
308
+ // ToolRegistry, wires builtin tools + skills + MCP, and drives the loop.
309
+ for await (const event of maestroProvider({
310
+ cwd: "/path/to/workspace", // .skills/ resolved relative to this
311
+ skillKey: "legal", // → /path/to/workspace/.skills/legal/<name>/SKILL.md
312
+ prompt: "Draft a contract clause for ...",
313
+ userId: "alice",
314
+ session: "thread-42",
315
+ // skill_view + skill_write tools are auto-registered; the model picks
316
+ // which skill body to load per turn.
317
+ })) {
318
+ if (event.type === "text_delta") process.stdout.write(event.content);
319
+ }
320
+ ```
321
+
322
+ **Creating skills:** `skill_write(name, body)` → writes `SKILL.md` into the named directory; the index hot-reloads.
323
+ **Loading skills:** `skill_view(name)` → returns the full markdown body to the model.
324
+ **Security:** every `SKILL.md` is scanned at index-time for prompt-injection, exfiltration, and destructive shell patterns. A flagged file is dropped from the catalog with a logged reason.
325
+
326
+ ## Hooks & Guardrails — LLM pre/post + tool hooks
327
+
328
+ ### LLM Pre Hook — inspect every API call
329
+
330
+ Fires right before every provider call. The host can pass through, replace the user-visible content, or tripwire the entire run. Receives the full message array (system + history + current turn).
331
+
332
+ ```ts
333
+ import { AIAgent, AnthropicProvider, ToolRegistry } from "maestro-agent-sdk";
334
+
335
+ const agent = new AIAgent(provider, tools, {
336
+ model: "claude-sonnet-4-6",
337
+ systemPrompt: "...",
338
+ llmPreHook: async (messages, { abortSignal }) => {
339
+ const lastUser = messages.filter((m) => m.role === "user").at(-1);
340
+ const text = typeof lastUser?.content === "string" ? lastUser.content : "";
341
+ if (/api[_-]?key|password/i.test(text)) {
342
+ return {
343
+ decision: "reject_content",
344
+ message: "Sensitive credential detected — please rephrase without secrets.",
345
+ };
346
+ }
347
+ if (/rm -rf \//.test(text)) {
348
+ return { decision: "tripwire", message: "Destructive request blocked." };
349
+ }
350
+ return { decision: "allow" };
351
+ },
352
+ });
353
+ ```
354
+
355
+ ### LLM Post Hook — validate the final turn
356
+
357
+ Fires when the model produced a turn-complete response (no pending tool calls), before the `result` event is yielded. Use for output redaction, API-key leak detection, or final policy enforcement.
358
+
359
+ ```ts
360
+ const agent = new AIAgent(provider, tools, {
361
+ // ...
362
+ llmPostHook: async (text, { messages }) => {
363
+ if (/sk-[a-zA-Z0-9]{20,}/.test(text)) {
364
+ return {
365
+ decision: "reject_content",
366
+ message: "[redacted: API key leak detected in assistant output]",
367
+ };
368
+ }
369
+ return { decision: "allow" };
370
+ },
371
+ });
372
+ ```
373
+
374
+ ### Tool hooks — per-tool pre/post
375
+
376
+ `ToolRegistry.use({ pre, post })` brackets every `dispatch()`. Pre can `allow` / `modify` / `block`; post sees the actual outcome via `status: "ok" | "blocked" | "error"` (since v0.1.14) so audit/telemetry hooks observe denied and failed calls too.
377
+
378
+ ```ts
379
+ import { ToolRegistry, type PreToolUseDecision } from "maestro-agent-sdk";
380
+
381
+ const tools = new ToolRegistry();
382
+ // ... register builtin tools ...
383
+
384
+ tools.use({
385
+ name: "fs-allowlist",
386
+ pre: ({ toolName, input }): PreToolUseDecision => {
387
+ if (toolName !== "Write" && toolName !== "Edit") return { decision: "allow" };
388
+ const path = String(input.file_path ?? "");
389
+ if (!path.startsWith("/workspace/")) {
390
+ return { decision: "block", error: `path '${path}' outside allowlist` };
391
+ }
392
+ return { decision: "allow" };
393
+ },
394
+ post: ({ toolName, status, error, output }) => {
395
+ metrics.increment(`tool.${toolName}.${status}`);
396
+ if (status === "error") logger.warn({ toolName, error }, "tool failed");
397
+ return {}; // pass output through unchanged
398
+ },
399
+ });
400
+ ```
401
+
402
+ ### Guardrail decisions
403
+
404
+ | Decision | Effect |
405
+ |----------|--------|
406
+ | `allow` | Proceed normally |
407
+ | `reject_content` | Replace the message/result, continue execution |
408
+ | `tripwire` | Abort the entire agent run immediately (LLM hooks only) |
409
+ | `modify` | (Tool pre hooks only) Substitute the tool's `input` before dispatch |
410
+ | `block` | (Tool pre hooks only) Skip tool execution, return the supplied error |
411
+
412
+ ## MCP — zero-config client pool
413
+
414
+ Wire an `McpResolver` and the SDK lazily spawns, caches, and reuses MCP subprocess clients across turns. Cache key includes `(userId, session, groupId, agentKind, server, specHash)` — two users never share a client, and same-server / same-spec calls within a session reuse the warm process.
415
+
416
+ ```ts
417
+ import { setMcpResolver } from "maestro-agent-sdk";
418
+
419
+ setMcpResolver((opts) => ({
420
+ playwright: {
421
+ command: "playwright-mcp",
422
+ args: ["--user-data-dir", `/tmp/pw-${opts.userId}`],
423
+ },
424
+ // SSE transport
425
+ search: { type: "sse", url: "https://internal.example.com/mcp" },
426
+ }));
427
+ ```
428
+
429
+ - **Lazy spawn** — servers start on first tool call, not at agent creation.
430
+ - **Pool cache** — `(userId, session, groupId, agentKind, server, specHash)` keyed; idle TTL 5 min, LRU cap 16 (override via `MAESTRO_MCP_POOL_IDLE_TTL_MS` / `MAESTRO_MCP_POOL_MAX`).
431
+ - **In-flight dedup** — concurrent acquires on the same key await one `start()` instead of double-spawning (v0.1.14).
432
+ - **Env values in cache hash** — `{ TOKEN: alice }` and `{ TOKEN: bob }` get separate processes by default; opt high-churn keys out via `setMcpCacheIgnoreEnvKeys(["DEPTH"])` (v0.1.14).
433
+ - **stdio + SSE** — both transports supported via `MaestroMcpServerSpec`.
434
+ - **Graceful shutdown** — `SIGINT` / `SIGTERM` closes every cached client before exit.
435
+
377
436
  ## Development
378
437
 
379
438
  ```bash
380
439
  git clone git@github.com:maestrojeong/maestro-agent-sdk.git
381
440
  cd maestro-agent-sdk
382
- npm install
441
+ bun install # also supported
442
+ npm install # alternative
383
443
  npm run typecheck # tsc --noEmit
384
444
  npm run build # tsc + tsc-alias → dist/
385
- npm test # vitest, 426 tests (+11 skipped without ripgrep)
445
+ npm test # vitest, 437 tests (+11 skipped without ripgrep)
386
446
  ```
387
447
 
388
- ### Known gaps
389
-
390
- Two test files are currently excluded in `vitest.config.ts`:
391
-
392
- - `maestro-registry.test.ts`
393
- - `maestro-session-store.test.ts`
394
-
395
- They rely on host-side helpers (`appendConversationEvent`, `getConversationPath`) and on the strict workspace-root check that the SDK loosened. They'll come back online once we wire them through the `ConversationReader` DI hook.
396
-
397
448
  ## License
398
449
 
399
- [MIT](./LICENSE). Design influenced by [Claude Code](https://www.anthropic.com/claude-code) and Nous Research's [`hermes-agent`](https://github.com/NousResearch/hermes-agent) (also MIT); see [NOTICE](./NOTICE) for attribution details.
450
+ [MIT](./LICENSE).
@@ -1,6 +1,6 @@
1
1
  import type { Provider } from "../providers/base.js";
2
2
  import type { ToolRegistry } from "../tools/registry.js";
3
- import type { EffortLevel } from "../types.js";
3
+ import type { EffortLevel, LlmPostHook, LlmPreHook } from "../types.js";
4
4
  /**
5
5
  * AIAgent — minimal TS port of upstream `run_agent.py::AIAgent`.
6
6
  *
@@ -56,6 +56,10 @@ export interface AIAgentConfig {
56
56
  * only fires for subsequent tool_result turns.
57
57
  */
58
58
  buildIterReminder?: (iterationsRemaining: number) => string | null;
59
+ /** LLM Pre Hook — fires right before every provider API call. */
60
+ llmPreHook?: LlmPreHook;
61
+ /** LLM Post Hook — fires on turn-complete (no tool calls) before `result` event. */
62
+ llmPostHook?: LlmPostHook;
59
63
  }
60
64
  export declare class AIAgent {
61
65
  readonly provider: Provider;
@@ -65,6 +69,8 @@ export declare class AIAgent {
65
69
  effort?: EffortLevel;
66
70
  abortSignal?: AbortSignal;
67
71
  buildIterReminder?: (iterationsRemaining: number) => string | null;
72
+ llmPreHook?: LlmPreHook;
73
+ llmPostHook?: LlmPostHook;
68
74
  };
69
75
  constructor(provider: Provider, tools: ToolRegistry, config: AIAgentConfig);
70
76
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/core/agent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;;;;;GASG;AAEH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,iBAAiB,CAAC,EAAE,CAAC,mBAAmB,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACpE;AAED,qBAAa,OAAO;IAClB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CACvB,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,cAAc,GAAG,eAAe,GAAG,WAAW,CAAC,CAC9E,GAAG;QACF,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,WAAW,CAAC,EAAE,WAAW,CAAC;QAC1B,iBAAiB,CAAC,EAAE,CAAC,mBAAmB,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;KACpE,CAAC;gBAEU,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa;CAgB3E"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/core/agent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEpE;;;;;;;;;GASG;AAEH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,iBAAiB,CAAC,EAAE,CAAC,mBAAmB,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACnE,iEAAiE;IACjE,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,oFAAoF;IACpF,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,OAAO;IAClB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CACvB,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,cAAc,GAAG,eAAe,GAAG,WAAW,CAAC,CAC9E,GAAG;QACF,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,WAAW,CAAC,EAAE,WAAW,CAAC;QAC1B,iBAAiB,CAAC,EAAE,CAAC,mBAAmB,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;QACnE,UAAU,CAAC,EAAE,UAAU,CAAC;QACxB,WAAW,CAAC,EAAE,WAAW,CAAC;KAC3B,CAAC;gBAEU,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa;CAkB3E"}
@@ -16,6 +16,8 @@ export class AIAgent {
16
16
  ...(config.effort ? { effort: config.effort } : {}),
17
17
  ...(config.abortSignal ? { abortSignal: config.abortSignal } : {}),
18
18
  ...(config.buildIterReminder ? { buildIterReminder: config.buildIterReminder } : {}),
19
+ ...(config.llmPreHook ? { llmPreHook: config.llmPreHook } : {}),
20
+ ...(config.llmPostHook ? { llmPostHook: config.llmPostHook } : {}),
19
21
  };
20
22
  }
21
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/core/agent.ts"],"names":[],"mappings":"AA8DA,MAAM,OAAO,OAAO;IACT,QAAQ,CAAW;IACnB,KAAK,CAAe;IACpB,MAAM,CAOb;IAEF,YAAY,QAAkB,EAAE,KAAmB,EAAE,MAAqB;QACxE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG;YACZ,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;YACzC,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,IAAI;YACnC,GAAG,CAAC,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,cAAc,GAAG,CAAC;gBACpD,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE;gBAC3C,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrF,CAAC;IACJ,CAAC;CACF"}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/core/agent.ts"],"names":[],"mappings":"AAkEA,MAAM,OAAO,OAAO;IACT,QAAQ,CAAW;IACnB,KAAK,CAAe;IACpB,MAAM,CASb;IAEF,YAAY,QAAkB,EAAE,KAAmB,EAAE,MAAqB;QACxE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG;YACZ,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;YACzC,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,IAAI;YACnC,GAAG,CAAC,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,cAAc,GAAG,CAAC;gBACpD,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE;gBAC3C,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/D,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC;IACJ,CAAC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/core/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAK5C,OAAO,KAAK,EAAwB,eAAe,EAAoB,MAAM,kBAAkB,CAAC;AAChG,OAAO,KAAK,EAA2B,YAAY,EAAE,MAAM,SAAS,CAAC;AA0BrE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAuB,eAAe,CACpC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,eAAe,EAAE,GAC1B,cAAc,CAAC,YAAY,CAAC,CAgV9B"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/core/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAM5C,OAAO,KAAK,EAAwB,eAAe,EAAoB,MAAM,kBAAkB,CAAC;AAChG,OAAO,KAAK,EAAc,YAAY,EAAE,MAAM,SAAS,CAAC;AAwBxD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAuB,eAAe,CACpC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,eAAe,EAAE,GAC1B,cAAc,CAAC,YAAY,CAAC,CAkZ9B"}
package/dist/core/loop.js CHANGED
@@ -2,13 +2,12 @@ import { extractFileEvents } from "../media/file-events.js";
2
2
  import { compressIfNeeded } from "../memory/compressor.js";
3
3
  import { StreamingContextScrubber, scrubString } from "../memory/scrubber.js";
4
4
  import { logger } from "../platform/logger.js";
5
- const EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
6
- function nextEffortLevel(current) {
7
- const idx = EFFORT_LEVELS.indexOf(current ?? "medium");
8
- if (idx < 0 || idx >= EFFORT_LEVELS.length - 1)
9
- return null;
10
- return EFFORT_LEVELS[idx + 1];
11
- }
5
+ import { thinkingBudgetForTurn } from "../providers/anthropic.js";
6
+ // v0.1.16: removed `EFFORT_LEVELS` + `nextEffortLevel`. The previous
7
+ // max_iterations result message recommended bumping effort to get more
8
+ // turns, but the iteration cap is now decoupled from effort and
9
+ // controlled directly by `AgentQueryOptions.maxIterations`. The message
10
+ // surfaces that hint instead, so the helpers are no longer needed.
12
11
  /**
13
12
  * Cap applied to the `content` field of `tool_result` UnifiedEvents emitted to
14
13
  * the dispatcher. Matches the 200-char ceiling already enforced by
@@ -87,16 +86,49 @@ export async function* runConversation(agent, messages) {
87
86
  // progressive typing UX. complete() stays as the fallback for providers
88
87
  // that haven't implemented stream() yet (e.g. an early Phase 5 OpenAI
89
88
  // adapter could ship stream() in a follow-up).
89
+ // v0.1.16: thinking budget is turn-adaptive. The base budget on
90
+ // `agent.config.thinkingBudget` reflects the caller's effort; for the
91
+ // wire call we resolve it through `thinkingBudgetForTurn` so the
92
+ // wrap-up zone (last 3 turns) trims down to 1/4 base. First + middle
93
+ // turns get the full base. See `thinkingBudgetForTurn` for the
94
+ // rationale; the helper handles the undefined / zero base no-op and
95
+ // the Anthropic >= 1024 minimum internally.
96
+ const turnBudget = thinkingBudgetForTurn(agent.config.thinkingBudget, iterations, maxIter);
90
97
  const callOpts = {
91
98
  model: agent.config.model,
92
99
  messages: wireMessages,
93
100
  system: agent.config.systemPrompt,
94
101
  tools: agent.tools.schemas(),
95
102
  maxTokens: agent.config.maxTokens,
96
- ...(agent.config.thinkingBudget ? { thinkingBudget: agent.config.thinkingBudget } : {}),
103
+ ...(turnBudget ? { thinkingBudget: turnBudget } : {}),
97
104
  ...(agent.config.effort ? { effort: agent.config.effort } : {}),
98
105
  ...(agent.config.abortSignal ? { abortSignal: agent.config.abortSignal } : {}),
99
106
  };
107
+ // ─── LLM Pre Hook ───
108
+ // Host guardrail runs before the provider sees the request. tripwire
109
+ // aborts the entire run; reject_content injects a rejection message as
110
+ // a user turn and lets the model respond to it next iteration.
111
+ if (agent.config.llmPreHook) {
112
+ const preResult = await agent.config.llmPreHook(wireMessages, {
113
+ ...(agent.config.abortSignal ? { abortSignal: agent.config.abortSignal } : {}),
114
+ });
115
+ if (preResult.decision === "tripwire") {
116
+ yield {
117
+ type: "error",
118
+ content: preResult.message ?? "guardrail: pre-hook tripwire",
119
+ };
120
+ return;
121
+ }
122
+ if (preResult.decision === "reject_content" && preResult.message) {
123
+ messages.push({
124
+ role: "user",
125
+ content: [{ type: "text", text: preResult.message }],
126
+ });
127
+ yield { type: "user_message", content: preResult.message };
128
+ continue;
129
+ }
130
+ // allow — fall through to provider call
131
+ }
100
132
  let response;
101
133
  let assistantText = "";
102
134
  const toolUses = [];
@@ -255,10 +287,33 @@ export async function* runConversation(agent, messages) {
255
287
  // history exactly once.
256
288
  messages.push({ role: "assistant", content: assistantBlocks });
257
289
  if (toolUses.length === 0) {
290
+ // ─── LLM Post Hook ───
291
+ // Host guardrail validates the final assistant text before the `result`
292
+ // event. tripwire replaces the result with an error; reject_content
293
+ // rewrites the content field so the caller surfaces the rejection message.
294
+ let resultContent = assistantText;
295
+ if (agent.config.llmPostHook) {
296
+ // Snapshot the current conversation (excludes the assistant turn just
297
+ // pushed — it's now the last entry in `messages`).
298
+ const postResult = await agent.config.llmPostHook(assistantText, {
299
+ messages,
300
+ ...(agent.config.abortSignal ? { abortSignal: agent.config.abortSignal } : {}),
301
+ });
302
+ if (postResult.decision === "tripwire") {
303
+ yield {
304
+ type: "error",
305
+ content: postResult.message ?? "guardrail: post-hook tripwire",
306
+ };
307
+ return;
308
+ }
309
+ if (postResult.decision === "reject_content") {
310
+ resultContent = postResult.message ?? resultContent;
311
+ }
312
+ }
258
313
  // No more tools — turn complete.
259
314
  yield {
260
315
  type: "result",
261
- content: assistantText,
316
+ content: resultContent,
262
317
  stopReason: response.stopReason,
263
318
  usage: {
264
319
  inputTokens: usageAcc.inputTokens,
@@ -354,18 +409,25 @@ export async function* runConversation(agent, messages) {
354
409
  messages.push({ role: "user", content: toolResultBlocks });
355
410
  iterations++;
356
411
  }
357
- const nextEffort = nextEffortLevel(agent.config.effort);
412
+ // v0.1.16: the iteration cap is no longer derived from effort, so the
413
+ // "raise effort to get more turns" affordance from earlier versions is
414
+ // misleading. We still surface `effort` in the message for context (the
415
+ // host may want to raise reasoning depth too), but the actionable hint
416
+ // is to raise `maxIterations` via AgentQueryOptions, which is the only
417
+ // knob that controls the cap now.
358
418
  yield {
359
419
  type: "result",
360
- content: nextEffort
361
- ? `Task didn't finish within the ${maxIter}-turn budget at effort='${agent.config.effort ?? "default"}'. Try increasing effort to '${nextEffort}' — run: set_effort ${nextEffort}`
362
- : `Task didn't finish within the ${maxIter}-turn budget at effort='${agent.config.effort ?? "default"}' (already at max).`,
420
+ content: `Task didn't finish within the ${maxIter}-turn budget at effort='${agent.config.effort ?? "default"}'. Raise the cap via AgentQueryOptions.maxIterations (currently defaulting to ${maxIter}) — and/or increase effort for deeper per-turn reasoning.`,
363
421
  stopReason: "max_iterations",
364
422
  usage: {
365
423
  inputTokens: usageAcc.inputTokens,
366
424
  outputTokens: usageAcc.outputTokens,
367
- ...(usageAcc.cacheCreationInputTokens ? { cacheCreationInputTokens: usageAcc.cacheCreationInputTokens } : {}),
368
- ...(usageAcc.cacheReadInputTokens ? { cacheReadInputTokens: usageAcc.cacheReadInputTokens } : {}),
425
+ ...(usageAcc.cacheCreationInputTokens
426
+ ? { cacheCreationInputTokens: usageAcc.cacheCreationInputTokens }
427
+ : {}),
428
+ ...(usageAcc.cacheReadInputTokens
429
+ ? { cacheReadInputTokens: usageAcc.cacheReadInputTokens }
430
+ : {}),
369
431
  },
370
432
  };
371
433
  }