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.
- package/README.md +214 -163
- package/dist/core/agent.d.ts +7 -1
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +2 -0
- package/dist/core/agent.js.map +1 -1
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +77 -15
- package/dist/core/loop.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/pool.d.ts +2 -2
- package/dist/mcp/pool.d.ts.map +1 -1
- package/dist/mcp/pool.js.map +1 -1
- package/dist/platform/env-bootstrap.js +4 -1
- package/dist/platform/env-bootstrap.js.map +1 -1
- package/dist/platform/logger.d.ts.map +1 -1
- package/dist/platform/logger.js.map +1 -1
- package/dist/platform/version.d.ts +1 -1
- package/dist/platform/version.js +1 -1
- package/dist/provider.d.ts +72 -17
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +119 -29
- package/dist/provider.js.map +1 -1
- package/dist/providers/anthropic.d.ts +98 -26
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +158 -31
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/deepseek.d.ts +16 -5
- package/dist/providers/deepseek.d.ts.map +1 -1
- package/dist/providers/deepseek.js +17 -6
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/sub-agent/runner.d.ts.map +1 -1
- package/dist/sub-agent/runner.js +1 -1
- package/dist/sub-agent/runner.js.map +1 -1
- package/dist/tools/builtin/bash.d.ts.map +1 -1
- package/dist/tools/builtin/bash.js.map +1 -1
- package/dist/tools/builtin/edit.d.ts.map +1 -1
- package/dist/tools/builtin/edit.js.map +1 -1
- package/dist/tools/builtin/multi_edit.d.ts.map +1 -1
- package/dist/tools/builtin/multi_edit.js.map +1 -1
- package/dist/tools/builtin/write.d.ts.map +1 -1
- package/dist/tools/builtin/write.js.map +1 -1
- package/dist/tools/index.d.ts +36 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,25 +4,18 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/maestro-agent-sdk)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
**Embeddable agent SDK
|
|
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
|
-
|
|
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
|
|
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`
|
|
111
|
-
>
|
|
112
|
-
>
|
|
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
|
-
>
|
|
115
|
-
>
|
|
116
|
-
>
|
|
117
|
-
>
|
|
118
|
-
>
|
|
119
|
-
>
|
|
120
|
-
>
|
|
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.
|
|
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
|
-
##
|
|
264
|
+
## Positioning — a building block, not a product
|
|
336
265
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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).
|
|
450
|
+
[MIT](./LICENSE).
|
package/dist/core/agent.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/agent.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/core/agent.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/core/agent.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/core/loop.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
...(
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
368
|
-
|
|
425
|
+
...(usageAcc.cacheCreationInputTokens
|
|
426
|
+
? { cacheCreationInputTokens: usageAcc.cacheCreationInputTokens }
|
|
427
|
+
: {}),
|
|
428
|
+
...(usageAcc.cacheReadInputTokens
|
|
429
|
+
? { cacheReadInputTokens: usageAcc.cacheReadInputTokens }
|
|
430
|
+
: {}),
|
|
369
431
|
},
|
|
370
432
|
};
|
|
371
433
|
}
|