maestro-agent-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +24 -0
- package/README.md +133 -0
- package/dist/agents/contracts.d.ts +49 -0
- package/dist/agents/contracts.d.ts.map +1 -0
- package/dist/agents/contracts.js +2 -0
- package/dist/agents/contracts.js.map +1 -0
- package/dist/agents/rollout/shared.d.ts +24 -0
- package/dist/agents/rollout/shared.d.ts.map +1 -0
- package/dist/agents/rollout/shared.js +105 -0
- package/dist/agents/rollout/shared.js.map +1 -0
- package/dist/core/agent.d.ts +71 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +22 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/loop.d.ts +26 -0
- package/dist/core/loop.d.ts.map +1 -0
- package/dist/core/loop.js +317 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +79 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +176 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/pool-cache.d.ts +103 -0
- package/dist/mcp/pool-cache.d.ts.map +1 -0
- package/dist/mcp/pool-cache.js +249 -0
- package/dist/mcp/pool-cache.js.map +1 -0
- package/dist/mcp/pool.d.ts +65 -0
- package/dist/mcp/pool.d.ts.map +1 -0
- package/dist/mcp/pool.js +86 -0
- package/dist/mcp/pool.js.map +1 -0
- package/dist/media/file-events.d.ts +8 -0
- package/dist/media/file-events.d.ts.map +1 -0
- package/dist/media/file-events.js +15 -0
- package/dist/media/file-events.js.map +1 -0
- package/dist/memory/active-task-template.d.ts +34 -0
- package/dist/memory/active-task-template.d.ts.map +1 -0
- package/dist/memory/active-task-template.js +63 -0
- package/dist/memory/active-task-template.js.map +1 -0
- package/dist/memory/compressor.d.ts +87 -0
- package/dist/memory/compressor.d.ts.map +1 -0
- package/dist/memory/compressor.js +164 -0
- package/dist/memory/compressor.js.map +1 -0
- package/dist/memory/hash.d.ts +17 -0
- package/dist/memory/hash.d.ts.map +1 -0
- package/dist/memory/hash.js +20 -0
- package/dist/memory/hash.js.map +1 -0
- package/dist/memory/prune.d.ts +117 -0
- package/dist/memory/prune.d.ts.map +1 -0
- package/dist/memory/prune.js +416 -0
- package/dist/memory/prune.js.map +1 -0
- package/dist/memory/reminder.d.ts +57 -0
- package/dist/memory/reminder.d.ts.map +1 -0
- package/dist/memory/reminder.js +57 -0
- package/dist/memory/reminder.js.map +1 -0
- package/dist/memory/scrubber.d.ts +28 -0
- package/dist/memory/scrubber.d.ts.map +1 -0
- package/dist/memory/scrubber.js +147 -0
- package/dist/memory/scrubber.js.map +1 -0
- package/dist/memory/token-estimate.d.ts +10 -0
- package/dist/memory/token-estimate.d.ts.map +1 -0
- package/dist/memory/token-estimate.js +69 -0
- package/dist/memory/token-estimate.js.map +1 -0
- package/dist/platform/config.d.ts +12 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/config.js +54 -0
- package/dist/platform/config.js.map +1 -0
- package/dist/platform/jsonl.d.ts +15 -0
- package/dist/platform/jsonl.d.ts.map +1 -0
- package/dist/platform/jsonl.js +80 -0
- package/dist/platform/jsonl.js.map +1 -0
- package/dist/platform/lifecycle.d.ts +22 -0
- package/dist/platform/lifecycle.d.ts.map +1 -0
- package/dist/platform/lifecycle.js +60 -0
- package/dist/platform/lifecycle.js.map +1 -0
- package/dist/platform/logger.d.ts +26 -0
- package/dist/platform/logger.d.ts.map +1 -0
- package/dist/platform/logger.js +41 -0
- package/dist/platform/logger.js.map +1 -0
- package/dist/platform/mcp-config.d.ts +15 -0
- package/dist/platform/mcp-config.d.ts.map +1 -0
- package/dist/platform/mcp-config.js +8 -0
- package/dist/platform/mcp-config.js.map +1 -0
- package/dist/provider.d.ts +81 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +444 -0
- package/dist/provider.js.map +1 -0
- package/dist/providers/anthropic.d.ts +132 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +518 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base.d.ts +140 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +2 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/deepseek.d.ts +118 -0
- package/dist/providers/deepseek.d.ts.map +1 -0
- package/dist/providers/deepseek.js +467 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/registry.d.ts +3 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +94 -0
- package/dist/registry.js.map +1 -0
- package/dist/session-store.d.ts +133 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +277 -0
- package/dist/session-store.js.map +1 -0
- package/dist/skills/curator.d.ts +104 -0
- package/dist/skills/curator.d.ts.map +1 -0
- package/dist/skills/curator.js +162 -0
- package/dist/skills/curator.js.map +1 -0
- package/dist/skills/index-builder.d.ts +42 -0
- package/dist/skills/index-builder.d.ts.map +1 -0
- package/dist/skills/index-builder.js +94 -0
- package/dist/skills/index-builder.js.map +1 -0
- package/dist/skills/loader.d.ts +107 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +286 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/preprocess.d.ts +45 -0
- package/dist/skills/preprocess.d.ts.map +1 -0
- package/dist/skills/preprocess.js +126 -0
- package/dist/skills/preprocess.js.map +1 -0
- package/dist/skills/usage.d.ts +75 -0
- package/dist/skills/usage.d.ts.map +1 -0
- package/dist/skills/usage.js +147 -0
- package/dist/skills/usage.js.map +1 -0
- package/dist/state/todos.d.ts +95 -0
- package/dist/state/todos.d.ts.map +1 -0
- package/dist/state/todos.js +198 -0
- package/dist/state/todos.js.map +1 -0
- package/dist/storage/conversations.d.ts +28 -0
- package/dist/storage/conversations.d.ts.map +1 -0
- package/dist/storage/conversations.js +8 -0
- package/dist/storage/conversations.js.map +1 -0
- package/dist/sub-agent/runner.d.ts +78 -0
- package/dist/sub-agent/runner.d.ts.map +1 -0
- package/dist/sub-agent/runner.js +215 -0
- package/dist/sub-agent/runner.js.map +1 -0
- package/dist/tools/builtin/agent.d.ts +33 -0
- package/dist/tools/builtin/agent.d.ts.map +1 -0
- package/dist/tools/builtin/agent.js +76 -0
- package/dist/tools/builtin/agent.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +11 -0
- package/dist/tools/builtin/bash.d.ts.map +1 -0
- package/dist/tools/builtin/bash.js +91 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/edit.d.ts +21 -0
- package/dist/tools/builtin/edit.d.ts.map +1 -0
- package/dist/tools/builtin/edit.js +238 -0
- package/dist/tools/builtin/edit.js.map +1 -0
- package/dist/tools/builtin/read.d.ts +17 -0
- package/dist/tools/builtin/read.d.ts.map +1 -0
- package/dist/tools/builtin/read.js +139 -0
- package/dist/tools/builtin/read.js.map +1 -0
- package/dist/tools/builtin/sandbox.d.ts +16 -0
- package/dist/tools/builtin/sandbox.d.ts.map +1 -0
- package/dist/tools/builtin/sandbox.js +58 -0
- package/dist/tools/builtin/sandbox.js.map +1 -0
- package/dist/tools/builtin/skill_view.d.ts +37 -0
- package/dist/tools/builtin/skill_view.d.ts.map +1 -0
- package/dist/tools/builtin/skill_view.js +82 -0
- package/dist/tools/builtin/skill_view.js.map +1 -0
- package/dist/tools/builtin/todo_write.d.ts +29 -0
- package/dist/tools/builtin/todo_write.d.ts.map +1 -0
- package/dist/tools/builtin/todo_write.js +96 -0
- package/dist/tools/builtin/todo_write.js.map +1 -0
- package/dist/tools/builtin/web_fetch.d.ts +10 -0
- package/dist/tools/builtin/web_fetch.d.ts.map +1 -0
- package/dist/tools/builtin/web_fetch.js +150 -0
- package/dist/tools/builtin/web_fetch.js.map +1 -0
- package/dist/tools/builtin/write.d.ts +35 -0
- package/dist/tools/builtin/write.d.ts.map +1 -0
- package/dist/tools/builtin/write.js +70 -0
- package/dist/tools/builtin/write.js.map +1 -0
- package/dist/tools/file-state.d.ts +99 -0
- package/dist/tools/file-state.d.ts.map +1 -0
- package/dist/tools/file-state.js +133 -0
- package/dist/tools/file-state.js.map +1 -0
- package/dist/tools/hooks/sandbox-fs.d.ts +25 -0
- package/dist/tools/hooks/sandbox-fs.d.ts.map +1 -0
- package/dist/tools/hooks/sandbox-fs.js +48 -0
- package/dist/tools/hooks/sandbox-fs.js.map +1 -0
- package/dist/tools/registry.d.ts +102 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +93 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable logger for the SDK.
|
|
3
|
+
*
|
|
4
|
+
* Upstream clawgram used `pino`. The SDK ships with a minimal console-backed
|
|
5
|
+
* default and exposes `setLogger()` so hosts can plug their own structured
|
|
6
|
+
* logger (pino, winston, bunyan, …). All SDK call sites import `logger` from
|
|
7
|
+
* this module and never assume a particular implementation.
|
|
8
|
+
*
|
|
9
|
+
* The interface mirrors pino's call shape — `(meta, msg)` or just `(msg)` —
|
|
10
|
+
* so existing call sites copied from clawgram work unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export interface Logger {
|
|
13
|
+
trace: LogFn;
|
|
14
|
+
debug: LogFn;
|
|
15
|
+
info: LogFn;
|
|
16
|
+
warn: LogFn;
|
|
17
|
+
error: LogFn;
|
|
18
|
+
fatal: LogFn;
|
|
19
|
+
}
|
|
20
|
+
export interface LogFn {
|
|
21
|
+
(obj: Record<string, unknown>, msg?: string): void;
|
|
22
|
+
(msg: string): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function setLogger(custom: Logger): void;
|
|
25
|
+
export declare const logger: Logger;
|
|
26
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/platform/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnD,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AA0BD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,eAAO,MAAM,MAAM,EAAE,MAInB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable logger for the SDK.
|
|
3
|
+
*
|
|
4
|
+
* Upstream clawgram used `pino`. The SDK ships with a minimal console-backed
|
|
5
|
+
* default and exposes `setLogger()` so hosts can plug their own structured
|
|
6
|
+
* logger (pino, winston, bunyan, …). All SDK call sites import `logger` from
|
|
7
|
+
* this module and never assume a particular implementation.
|
|
8
|
+
*
|
|
9
|
+
* The interface mirrors pino's call shape — `(meta, msg)` or just `(msg)` —
|
|
10
|
+
* so existing call sites copied from clawgram work unchanged.
|
|
11
|
+
*/
|
|
12
|
+
function makeConsoleLogger() {
|
|
13
|
+
const emit = (level) => (objOrMsg, msg) => {
|
|
14
|
+
if (typeof objOrMsg === "string") {
|
|
15
|
+
// biome-ignore lint/suspicious/noConsole: SDK default logger
|
|
16
|
+
console[level](`[${level}] ${objOrMsg}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// biome-ignore lint/suspicious/noConsole: SDK default logger
|
|
20
|
+
console[level](`[${level}] ${msg ?? ""}`, objOrMsg);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
trace: emit("log"),
|
|
25
|
+
debug: emit("log"),
|
|
26
|
+
info: emit("info"),
|
|
27
|
+
warn: emit("warn"),
|
|
28
|
+
error: emit("error"),
|
|
29
|
+
fatal: emit("error"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
let _logger = makeConsoleLogger();
|
|
33
|
+
export function setLogger(custom) {
|
|
34
|
+
_logger = custom;
|
|
35
|
+
}
|
|
36
|
+
export const logger = new Proxy({}, {
|
|
37
|
+
get(_target, prop) {
|
|
38
|
+
return _logger[prop];
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/platform/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgBH,SAAS,iBAAiB;IACxB,MAAM,IAAI,GACR,CAAC,KAAwC,EAAE,EAAE,CAC7C,CAAC,QAA0C,EAAE,GAAY,EAAE,EAAE;QAC3D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,6DAA6D;YAC7D,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC,CAAC;QAC3C,CAAC;aAAM,CAAC;YACN,6DAA6D;YAC7D,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,KAAK,GAAG,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;QACtD,CAAC;IACH,CAAC,CAAC;IACJ,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK,CAAU;QAC3B,KAAK,EAAE,IAAI,CAAC,KAAK,CAAU;QAC3B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAU;QAC3B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAU;QAC3B,KAAK,EAAE,IAAI,CAAC,OAAO,CAAU;QAC7B,KAAK,EAAE,IAAI,CAAC,OAAO,CAAU;KAC9B,CAAC;AACJ,CAAC;AAED,IAAI,OAAO,GAAW,iBAAiB,EAAE,CAAC;AAE1C,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,OAAO,GAAG,MAAM,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAW,IAAI,KAAK,CAAC,EAAY,EAAE;IACpD,GAAG,CAAC,OAAO,EAAE,IAAkB;QAC7B,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MaestroMcpServerSpec } from "../mcp/client.js";
|
|
2
|
+
import type { AgentQueryOptions } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Host-provided resolver: returns the MCP servers the loop should attach for
|
|
5
|
+
* a given query, keyed by server name. The SDK's MCP pool spawns them with
|
|
6
|
+
* the cache key derived from `(userId, session, groupId, agentKind)`.
|
|
7
|
+
*
|
|
8
|
+
* The SDK ships **no defaults** — hosts call `setMcpResolver()` to plug in
|
|
9
|
+
* their own, or skip it and run without MCP.
|
|
10
|
+
*/
|
|
11
|
+
export type McpServerMap = Record<string, MaestroMcpServerSpec>;
|
|
12
|
+
export type McpResolver = (opts: AgentQueryOptions) => McpServerMap;
|
|
13
|
+
export declare function setMcpResolver(resolver: McpResolver): void;
|
|
14
|
+
export declare function getMcpServersForQuery(opts: AgentQueryOptions): McpServerMap;
|
|
15
|
+
//# sourceMappingURL=mcp-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-config.d.ts","sourceRoot":"","sources":["../../src/platform/mcp-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;AAChE,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,iBAAiB,KAAK,YAAY,CAAC;AAIpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAE1D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,GAAG,YAAY,CAE3E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-config.js","sourceRoot":"","sources":["../../src/platform/mcp-config.ts"],"names":[],"mappings":"AAcA,IAAI,SAAS,GAAgB,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAExC,MAAM,UAAU,cAAc,CAAC,QAAqB;IAClD,SAAS,GAAG,QAAQ,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAuB;IAC3D,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Provider } from "./providers/base.js";
|
|
2
|
+
import type { AgentQueryOptions, UnifiedEvent } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Maestro SDK provider (TS port of Maestro Agent v0.13.0).
|
|
5
|
+
*
|
|
6
|
+
* Multi-turn resume: when `opts.sessionId` is set we hydrate prior messages
|
|
7
|
+
* from `~/.maestro/sessions/<id>.jsonl` (written by the previous turn's
|
|
8
|
+
* persistence path or by a cross-agent rollout). Otherwise we mint a fresh
|
|
9
|
+
* UUIDv4 — matching the contract claude/codex providers expose, so the
|
|
10
|
+
* router stays agent-agnostic and `{type:"session", sessionId}` always comes
|
|
11
|
+
* back on the first iteration.
|
|
12
|
+
*
|
|
13
|
+
* After the loop drains we write the updated history back to disk so a
|
|
14
|
+
* subsequent call resumes correctly. Failures here are logged but never
|
|
15
|
+
* thrown — losing one persistence round is a degraded experience, not a
|
|
16
|
+
* stream-breaking one.
|
|
17
|
+
*
|
|
18
|
+
* MCP integration: every server in `getMcpServersForQuery(opts)` is leased
|
|
19
|
+
* from the process-wide cache (`mcp/pool-cache.ts`) and its tools are
|
|
20
|
+
* registered under `mcp__<server>__<tool>` — the same name convention Claude
|
|
21
|
+
* SDK uses, so the model sees consistent tool names across providers in the
|
|
22
|
+
* same topic. Unlike claudeProvider / codexProvider — which hand `mcpServers`
|
|
23
|
+
* to a vendor SDK that owns MCP lifecycle internally — Maestro hits Anthropic's
|
|
24
|
+
* Messages API via raw fetch, so we own startup + dispatch + cleanup here.
|
|
25
|
+
* Cache key includes (userId, session, groupId) so two users never share an
|
|
26
|
+
* MCP client; the cache evicts idle clients via TTL + LRU cap so stale slots
|
|
27
|
+
* don't accumulate.
|
|
28
|
+
*
|
|
29
|
+
* Snapshot pinned to upstream Maestro v0.13.0 (MIT, Nous Research). See
|
|
30
|
+
* docs/maestro-integration.md for the porting roadmap.
|
|
31
|
+
*/
|
|
32
|
+
export declare function maestroProvider(opts: AgentQueryOptions): AsyncGenerator<UnifiedEvent>;
|
|
33
|
+
/**
|
|
34
|
+
* Compose the "Tool iterations remaining: N/M — <tone>" line that rides
|
|
35
|
+
* inside the per-iteration `<system-reminder>` block. Tone shifts with
|
|
36
|
+
* ABSOLUTE remaining count (not percentage of maxIter) — same threshold
|
|
37
|
+
* fires the same urgency regardless of effort level, so the model gets a
|
|
38
|
+
* consistent cue from a `low` run reaching 4-left as from a `xhigh` run
|
|
39
|
+
* reaching 4-left.
|
|
40
|
+
*
|
|
41
|
+
* Why absolute, not relative: at `low` (maxIter=5) the model crosses the
|
|
42
|
+
* wrap-up line almost immediately; at `xhigh` (maxIter=90) 75% left lasts
|
|
43
|
+
* ~22 turns. The user-visible behavior the threshold is targeting is "how
|
|
44
|
+
* many tool calls before I MUST stop" — that's an absolute count, not a
|
|
45
|
+
* fraction.
|
|
46
|
+
*
|
|
47
|
+
* Thresholds:
|
|
48
|
+
* - >= 10 → plenty of room. (no urgency)
|
|
49
|
+
* - 5..9 → pace yourself.
|
|
50
|
+
* - 2..4 → start wrapping up — consolidate, avoid new tool calls.
|
|
51
|
+
* - 0..1 → finalize NOW. Stop tooling, write the answer.
|
|
52
|
+
*
|
|
53
|
+
* Imperative phrasing matters: passing a bare count ("3 remaining") gets
|
|
54
|
+
* acknowledged but doesn't change behavior much. Pairing the count with a
|
|
55
|
+
* verb the model can act on ("start wrapping up", "finalize NOW") is what
|
|
56
|
+
* shifts the next-token distribution toward "emit final answer" instead
|
|
57
|
+
* of "call another tool". Exported so sub-agent / tests can share the
|
|
58
|
+
* exact phrasing if they need parity.
|
|
59
|
+
*/
|
|
60
|
+
export declare function iterationBudgetLine(remaining: number, max: number): string;
|
|
61
|
+
/**
|
|
62
|
+
* Pick the right provider adapter for a resolved model id. DeepSeek's V4
|
|
63
|
+
* family uses `deepseek-*` ids; everything else (Anthropic claude-* + future
|
|
64
|
+
* direct full ids) falls through to the Anthropic adapter. Exported so tests
|
|
65
|
+
* can lock the dispatch shape independently of `maestroProvider`'s I/O.
|
|
66
|
+
*/
|
|
67
|
+
export declare function providerForModel(resolvedModel: string): Provider;
|
|
68
|
+
/**
|
|
69
|
+
* Recognize the multiple shapes Node + WHATWG fetch use when a request is
|
|
70
|
+
* cancelled via `AbortController`:
|
|
71
|
+
* - DOMException with name "AbortError" (fetch / EventSource)
|
|
72
|
+
* - Error with name "AbortError" (older Node paths)
|
|
73
|
+
* - DOMException with code 20 (legacy ABORT_ERR code)
|
|
74
|
+
*
|
|
75
|
+
* Catches both so the abort detection isn't tied to a single runtime.
|
|
76
|
+
*
|
|
77
|
+
* Exported for unit coverage — used internally by `maestroProvider`'s catch
|
|
78
|
+
* branch to distinguish a user-initiated abort from a real provider crash.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isAbortError(err: unknown): boolean;
|
|
81
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAyC,MAAM,kBAAkB,CAAC;AA0BxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAuB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,CAAC,YAAY,CAAC,CAsV5F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAY1E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAKhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMlD"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AIAgent } from "./core/agent.js";
|
|
3
|
+
import { runConversation } from "./core/loop.js";
|
|
4
|
+
import { registerMcpTools, startMcpPool } from "./mcp/pool.js";
|
|
5
|
+
import { buildSystemReminder } from "./memory/reminder.js";
|
|
6
|
+
import { AnthropicProvider, effortToMaxIter, effortToThinkingBudget, } from "./providers/anthropic.js";
|
|
7
|
+
import { DeepseekProvider } from "./providers/deepseek.js";
|
|
8
|
+
import { maestroRegistry } from "./registry.js";
|
|
9
|
+
import { isWellFormedMessage, loadMaestroSession, saveMaestroSession, trimToSafePrefix, } from "./session-store.js";
|
|
10
|
+
import { curateSkills } from "./skills/curator.js";
|
|
11
|
+
import { buildSkillsIndex } from "./skills/index-builder.js";
|
|
12
|
+
import { loadSkillsCached } from "./skills/loader.js";
|
|
13
|
+
import { getTodoStore } from "./state/todos.js";
|
|
14
|
+
import { createAgentTool } from "./tools/builtin/agent.js";
|
|
15
|
+
import { bashTool } from "./tools/builtin/bash.js";
|
|
16
|
+
import { createEditTool } from "./tools/builtin/edit.js";
|
|
17
|
+
import { createReadTool } from "./tools/builtin/read.js";
|
|
18
|
+
import { createSkillViewTool } from "./tools/builtin/skill_view.js";
|
|
19
|
+
import { createTodoWriteTool } from "./tools/builtin/todo_write.js";
|
|
20
|
+
import { webFetchTool } from "./tools/builtin/web_fetch.js";
|
|
21
|
+
import { createWriteTool } from "./tools/builtin/write.js";
|
|
22
|
+
import { getFileStateTracker } from "./tools/file-state.js";
|
|
23
|
+
import { createSandboxFsHook } from "./tools/hooks/sandbox-fs.js";
|
|
24
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
25
|
+
import { logger } from "./platform/logger.js";
|
|
26
|
+
import { getMcpServersForQuery } from "./platform/mcp-config.js";
|
|
27
|
+
/**
|
|
28
|
+
* Maestro SDK provider (TS port of Maestro Agent v0.13.0).
|
|
29
|
+
*
|
|
30
|
+
* Multi-turn resume: when `opts.sessionId` is set we hydrate prior messages
|
|
31
|
+
* from `~/.maestro/sessions/<id>.jsonl` (written by the previous turn's
|
|
32
|
+
* persistence path or by a cross-agent rollout). Otherwise we mint a fresh
|
|
33
|
+
* UUIDv4 — matching the contract claude/codex providers expose, so the
|
|
34
|
+
* router stays agent-agnostic and `{type:"session", sessionId}` always comes
|
|
35
|
+
* back on the first iteration.
|
|
36
|
+
*
|
|
37
|
+
* After the loop drains we write the updated history back to disk so a
|
|
38
|
+
* subsequent call resumes correctly. Failures here are logged but never
|
|
39
|
+
* thrown — losing one persistence round is a degraded experience, not a
|
|
40
|
+
* stream-breaking one.
|
|
41
|
+
*
|
|
42
|
+
* MCP integration: every server in `getMcpServersForQuery(opts)` is leased
|
|
43
|
+
* from the process-wide cache (`mcp/pool-cache.ts`) and its tools are
|
|
44
|
+
* registered under `mcp__<server>__<tool>` — the same name convention Claude
|
|
45
|
+
* SDK uses, so the model sees consistent tool names across providers in the
|
|
46
|
+
* same topic. Unlike claudeProvider / codexProvider — which hand `mcpServers`
|
|
47
|
+
* to a vendor SDK that owns MCP lifecycle internally — Maestro hits Anthropic's
|
|
48
|
+
* Messages API via raw fetch, so we own startup + dispatch + cleanup here.
|
|
49
|
+
* Cache key includes (userId, session, groupId) so two users never share an
|
|
50
|
+
* MCP client; the cache evicts idle clients via TTL + LRU cap so stale slots
|
|
51
|
+
* don't accumulate.
|
|
52
|
+
*
|
|
53
|
+
* Snapshot pinned to upstream Maestro v0.13.0 (MIT, Nous Research). See
|
|
54
|
+
* docs/maestro-integration.md for the porting roadmap.
|
|
55
|
+
*/
|
|
56
|
+
export async function* maestroProvider(opts) {
|
|
57
|
+
// Provider instantiation is deferred until after model resolution so the
|
|
58
|
+
// right adapter (Anthropic / DeepSeek) is chosen based on the resolved
|
|
59
|
+
// model id. The env-var check happens at fromEnv() time inside each
|
|
60
|
+
// adapter — we surface its error as a normal `error` UnifiedEvent so the
|
|
61
|
+
// dispatcher doesn't see a synthetic crash.
|
|
62
|
+
// Resolve sessionId up-front so per-session resources (file-state tracker,
|
|
63
|
+
// skill_view) can key off a stable id. Either supplied by the caller
|
|
64
|
+
// (resume / cross-agent bridge) or minted now for a fresh session.
|
|
65
|
+
const sessionId = opts.sessionId ?? randomUUID();
|
|
66
|
+
// Per-session file-state tracker drives the Read-before-Edit gate. Module-
|
|
67
|
+
// level registry (see tools/file-state.ts) keeps the tracker alive across
|
|
68
|
+
// turns so a Read in turn N is still recorded when an Edit fires in N+1.
|
|
69
|
+
const fileTracker = getFileStateTracker(sessionId);
|
|
70
|
+
// Per-session TodoWrite store. Same module-level cache pattern — the
|
|
71
|
+
// store hydrates from `~/.maestro/sessions/<sid>.todos.json` on first
|
|
72
|
+
// access so a multi-turn plan survives across calls.
|
|
73
|
+
const todoStore = getTodoStore(sessionId);
|
|
74
|
+
const tools = new ToolRegistry();
|
|
75
|
+
// Filesystem sandbox runs as a PreToolUse hook so every tool with a
|
|
76
|
+
// `file_path` argument (Read/Write/Edit + future FS-touching MCP tools)
|
|
77
|
+
// shares one gate. Registered first so it fires before any caller-added
|
|
78
|
+
// hook in this turn.
|
|
79
|
+
tools.use(createSandboxFsHook());
|
|
80
|
+
tools.register(bashTool);
|
|
81
|
+
// Read/Write/Edit/WebFetch — claude SDK parity builtins. Same name + schema
|
|
82
|
+
// so the model's pretrained instinct calls them with the right shape, and
|
|
83
|
+
// prompt cache keys line up across agents when a topic is bridged.
|
|
84
|
+
// Read/Write/Edit also gate on the workspace sandbox (inline today; future
|
|
85
|
+
// Phase 2.1 migrates to a hook) and on the per-session file-state tracker
|
|
86
|
+
// so Edit can't mutate a path that hasn't been Read in this session.
|
|
87
|
+
tools.register(createReadTool({ tracker: fileTracker }));
|
|
88
|
+
tools.register(createWriteTool({ tracker: fileTracker }));
|
|
89
|
+
tools.register(createEditTool({ tracker: fileTracker }));
|
|
90
|
+
tools.register(webFetchTool);
|
|
91
|
+
tools.register(createTodoWriteTool({ store: todoStore }));
|
|
92
|
+
// --- Skills: load SKILL.md catalog + register `skill_view` ---------------
|
|
93
|
+
//
|
|
94
|
+
// Domain accuracy lever (Phase 2). The model gets:
|
|
95
|
+
// - A `## Skills (mandatory)` block appended to the system prompt (60-char
|
|
96
|
+
// summary per skill) so prefix caching covers the catalog across turns.
|
|
97
|
+
// - A `skill_view(name)` builtin so it can pull the full SKILL.md body on
|
|
98
|
+
// demand (progressive disclosure — saves the per-turn cost of inlining
|
|
99
|
+
// every skill body).
|
|
100
|
+
//
|
|
101
|
+
// Source dir is picked from `MAESTRO_SKILL_DIR` first so power users can
|
|
102
|
+
// point at a Clawgram-local catalog later without code change; the v0.13.0
|
|
103
|
+
// upstream tree at `~/__KEEP_MAESTRO_AGENT__/skills/` is the current default.
|
|
104
|
+
//
|
|
105
|
+
// Failures (rootDir missing, unreadable, every file malformed) reduce to an
|
|
106
|
+
// empty catalog — the loop still runs with just bash + MCP tools.
|
|
107
|
+
const skillsDir = process.env.MAESTRO_SKILL_DIR ?? "/Users/maestrobot/__KEEP_MAESTRO_AGENT__/skills";
|
|
108
|
+
let skillsBlock = "";
|
|
109
|
+
// Hoisted to outer scope so the Agent tool (registered below, after
|
|
110
|
+
// model/effort resolve) can pass the same skill catalog to sub-agents.
|
|
111
|
+
let loadedSkills = [];
|
|
112
|
+
try {
|
|
113
|
+
const skills = loadSkillsCached(skillsDir);
|
|
114
|
+
loadedSkills = skills;
|
|
115
|
+
if (skills.length > 0) {
|
|
116
|
+
// Curator filters the catalog: archived skills (agent-created, never
|
|
117
|
+
// viewed, >60 days old) are dropped from the system-prompt index but
|
|
118
|
+
// stay reachable via skill_view by exact name. Bundled skills under
|
|
119
|
+
// the upstream snapshot directory are protected from archival.
|
|
120
|
+
// skill_view still sees the full set — model can resolve any name
|
|
121
|
+
// the user explicitly mentions even if it's been archived.
|
|
122
|
+
const curated = curateSkills(skills);
|
|
123
|
+
const visibleSkills = curated.map((c) => c.skill);
|
|
124
|
+
tools.register(createSkillViewTool({ skills, sessionId })); // full set
|
|
125
|
+
skillsBlock = buildSkillsIndex(visibleSkills);
|
|
126
|
+
logger.info({
|
|
127
|
+
agent: "maestro",
|
|
128
|
+
skillsDir,
|
|
129
|
+
skillCount: skills.length,
|
|
130
|
+
visibleCount: visibleSkills.length,
|
|
131
|
+
archivedCount: skills.length - visibleSkills.length,
|
|
132
|
+
}, "maestroProvider: skill catalog loaded (curated)");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
logger.warn({ err: e, skillsDir }, "maestroProvider: skill catalog load failed (degraded)");
|
|
137
|
+
}
|
|
138
|
+
// --- MCP pool: spawn every configured server, register their tools -------
|
|
139
|
+
//
|
|
140
|
+
// Failures here are logged but non-fatal: a turn can still serve from
|
|
141
|
+
// builtins alone if every server is unhealthy. This mirrors the partial-
|
|
142
|
+
// availability stance of the existing Playwright exit-propagation path,
|
|
143
|
+
// where one dead MCP doesn't take out the rest of the toolset.
|
|
144
|
+
let mcpPool = null;
|
|
145
|
+
try {
|
|
146
|
+
const servers = getMcpServersForQuery(opts);
|
|
147
|
+
// Scope cache to (userId, session, groupId, agentKind) so two users never
|
|
148
|
+
// share an MCP client (privacy) and so two sessions within one user keep
|
|
149
|
+
// their own playwright instance (forum/dm scope rules). The cache hashes
|
|
150
|
+
// the spec on top of this, so a server spec change inside the same scope
|
|
151
|
+
// also creates a fresh client.
|
|
152
|
+
mcpPool = await startMcpPool(servers, {
|
|
153
|
+
userId: opts.userId,
|
|
154
|
+
session: opts.session,
|
|
155
|
+
groupId: opts.groupId,
|
|
156
|
+
agentKind: "maestro",
|
|
157
|
+
});
|
|
158
|
+
registerMcpTools(tools, mcpPool, opts.abortController?.signal);
|
|
159
|
+
logger.info({
|
|
160
|
+
agent: "maestro",
|
|
161
|
+
mcpServerCount: mcpPool.clients.length,
|
|
162
|
+
mcpToolCount: mcpPool.tools.length,
|
|
163
|
+
}, "maestroProvider: MCP pool ready");
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
logger.warn({ err: e }, "maestroProvider: MCP pool start failed — continuing without MCP");
|
|
167
|
+
}
|
|
168
|
+
const requestedModel = opts.model ?? maestroRegistry.defaultModel;
|
|
169
|
+
const resolvedModel = maestroRegistry.expandModelAlias(requestedModel);
|
|
170
|
+
// Resolve effort up-front (was previously deferred until after history
|
|
171
|
+
// hydration) so we can both build the initial system-reminder with the
|
|
172
|
+
// correct "iterations remaining: N/N" line and pass `maxIter` into
|
|
173
|
+
// `AIAgent` below. Default is the registry's `medium`, matching how
|
|
174
|
+
// claude-provider hands effort to its SDK when the caller doesn't pin one.
|
|
175
|
+
const resolvedEffort = opts.effort ?? maestroRegistry.defaultEffort;
|
|
176
|
+
const maxIter = effortToMaxIter(resolvedEffort);
|
|
177
|
+
// Pick provider by resolved model id prefix. DeepSeek models (`deepseek-*`)
|
|
178
|
+
// route to DeepseekProvider; anything else falls through to Anthropic. If
|
|
179
|
+
// the chosen adapter's env var is missing, close the MCP pool we already
|
|
180
|
+
// started before bailing so we don't leak subprocesses.
|
|
181
|
+
let provider;
|
|
182
|
+
try {
|
|
183
|
+
provider = providerForModel(resolvedModel);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (mcpPool) {
|
|
187
|
+
await mcpPool.close().catch((err) => {
|
|
188
|
+
logger.warn({ err }, "maestroProvider: mcp pool close after provider error failed");
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
yield {
|
|
192
|
+
type: "error",
|
|
193
|
+
content: e instanceof Error ? e.message : String(e),
|
|
194
|
+
};
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// --- Prior history hydration -------------------------------------------
|
|
198
|
+
//
|
|
199
|
+
// `sessionId` was already resolved at the top of this function so per-
|
|
200
|
+
// session resources (file-state tracker, skill_view) could be keyed off
|
|
201
|
+
// it. Three load cases for the persisted JSONL:
|
|
202
|
+
// 1. Caller supplied a sessionId AND file exists → resume.
|
|
203
|
+
// 2. Caller supplied a sessionId but file is missing/empty → keep the
|
|
204
|
+
// id (so cross-agent set_agent's pre-registered DB id stays valid)
|
|
205
|
+
// and start with empty history.
|
|
206
|
+
// 3. No sessionId → fresh UUIDv4 was minted above; no file to load.
|
|
207
|
+
const persisted = opts.sessionId ? loadMaestroSession(opts.sessionId) : null;
|
|
208
|
+
const priorMessages = (persisted ?? []).filter(isWellFormedMessage);
|
|
209
|
+
// Attach a `<system-reminder>` text block AFTER the user prompt on every
|
|
210
|
+
// new turn. Two reasons this lives at push time rather than on-wire:
|
|
211
|
+
// 1. Anthropic's automatic prompt cache breaks at the first byte
|
|
212
|
+
// divergence — past-turn user messages must be stable across future
|
|
213
|
+
// calls. Mutating wireMessages would make turn N's message differ
|
|
214
|
+
// between turn N's call (with reminder) and turn N+1's call (without
|
|
215
|
+
// reminder once canonical re-renders), nuking cache hits.
|
|
216
|
+
// 2. Compactor preserves the most-recent user message; the reminder
|
|
217
|
+
// rides along automatically. Historical turns keep the reminder
|
|
218
|
+
// that was true at THAT turn (sandbox could have flipped since) —
|
|
219
|
+
// drift across env-var changes is feature, not bug.
|
|
220
|
+
// Claude Code places `<system-reminder>` at the tail of user content; we
|
|
221
|
+
// match that order so the model's pretrained intuition treats the
|
|
222
|
+
// reminder as meta annotation, not as user intent.
|
|
223
|
+
// The reminder carries the iteration budget so the model can self-pace
|
|
224
|
+
// ("8 left → wrap up", "90 left → take your time"). Closure shared with
|
|
225
|
+
// the per-iteration builder below so first-turn and subsequent-turn
|
|
226
|
+
// reminders render with identical shape — the model sees the same fields
|
|
227
|
+
// in the same order, only the counts change.
|
|
228
|
+
const buildIterReminder = (iterationsRemaining) => buildSystemReminder({
|
|
229
|
+
sessionId,
|
|
230
|
+
todos: todoStore.list(),
|
|
231
|
+
extras: [iterationBudgetLine(iterationsRemaining, maxIter)],
|
|
232
|
+
});
|
|
233
|
+
const reminderText = buildIterReminder(maxIter);
|
|
234
|
+
const userBlocks = [
|
|
235
|
+
{ type: "text", text: opts.prompt },
|
|
236
|
+
{ type: "text", text: reminderText },
|
|
237
|
+
];
|
|
238
|
+
const messages = [...priorMessages, { role: "user", content: userBlocks }];
|
|
239
|
+
// Emit the session event before the first provider call so the router's
|
|
240
|
+
// recorder captures it in the unified conversation log — same shape as
|
|
241
|
+
// claude/codex `init` and `thread.started` events.
|
|
242
|
+
yield { type: "session", sessionId };
|
|
243
|
+
// Skills index goes into the system prompt (NOT a user message) so
|
|
244
|
+
// Anthropic's prefix cache covers it across every turn — the catalog only
|
|
245
|
+
// changes when SKILL.md files on disk change, while user messages roll
|
|
246
|
+
// every turn. Append, don't prepend: caller-supplied systemPrompt is
|
|
247
|
+
// identity / instructions that should still anchor the prompt, and the
|
|
248
|
+
// skills block reads naturally as a final "and also, here's what tools
|
|
249
|
+
// you have access to" section.
|
|
250
|
+
const augmentedSystemPrompt = skillsBlock
|
|
251
|
+
? `${opts.systemPrompt}\n\n${skillsBlock}`
|
|
252
|
+
: opts.systemPrompt;
|
|
253
|
+
// Register the `Agent` tool last — it captures the resolved model,
|
|
254
|
+
// effort, augmented system prompt (parent base for sub-agents), and the
|
|
255
|
+
// already-loaded skill catalog. Registered only on the PARENT call;
|
|
256
|
+
// sub-agents do NOT get an Agent tool because `runSubAgent` builds its
|
|
257
|
+
// own registry without registering one (advisor: depth=1 cap).
|
|
258
|
+
tools.register(createAgentTool({
|
|
259
|
+
parent: {
|
|
260
|
+
parentSessionId: sessionId,
|
|
261
|
+
parentSystemPrompt: augmentedSystemPrompt,
|
|
262
|
+
parentModel: resolvedModel,
|
|
263
|
+
...(resolvedEffort ? { parentEffort: resolvedEffort } : {}),
|
|
264
|
+
...(opts.abortController?.signal ? { parentAbortSignal: opts.abortController.signal } : {}),
|
|
265
|
+
skills: loadedSkills,
|
|
266
|
+
},
|
|
267
|
+
}));
|
|
268
|
+
const thinkingBudget = effortToThinkingBudget(resolvedEffort);
|
|
269
|
+
const agent = new AIAgent(provider, tools, {
|
|
270
|
+
model: resolvedModel,
|
|
271
|
+
systemPrompt: augmentedSystemPrompt,
|
|
272
|
+
// Effort-derived tool-iteration cap. The model sees the same number via
|
|
273
|
+
// the per-iteration `<system-reminder>` (see `buildIterReminder` above)
|
|
274
|
+
// so it can self-pace — low effort tells it to wrap up fast, xhigh
|
|
275
|
+
// gives it room to dig.
|
|
276
|
+
maxIterations: maxIter,
|
|
277
|
+
buildIterReminder,
|
|
278
|
+
...(thinkingBudget ? { thinkingBudget } : {}),
|
|
279
|
+
...(resolvedEffort ? { effort: resolvedEffort } : {}),
|
|
280
|
+
...(opts.abortController?.signal ? { abortSignal: opts.abortController.signal } : {}),
|
|
281
|
+
});
|
|
282
|
+
// Wire abort → close MCP pool early. Without this, an aborted turn could
|
|
283
|
+
// leave Playwright / OCR subprocesses spinning until the finally block,
|
|
284
|
+
// which only runs after the loop's awaited operation completes.
|
|
285
|
+
const abortSignal = opts.abortController?.signal;
|
|
286
|
+
const onAbortClosePool = () => {
|
|
287
|
+
if (mcpPool) {
|
|
288
|
+
mcpPool.close().catch((err) => {
|
|
289
|
+
logger.warn({ err }, "maestroProvider: mcp pool close on abort failed");
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
abortSignal?.addEventListener("abort", onAbortClosePool, { once: true });
|
|
294
|
+
logger.info({
|
|
295
|
+
agent: "maestro",
|
|
296
|
+
model: resolvedModel,
|
|
297
|
+
effort: resolvedEffort,
|
|
298
|
+
maxIter,
|
|
299
|
+
thinkingBudget: thinkingBudget ?? null,
|
|
300
|
+
sessionId,
|
|
301
|
+
session: opts.session ?? null,
|
|
302
|
+
resumed: priorMessages.length > 0,
|
|
303
|
+
priorTurns: priorMessages.length,
|
|
304
|
+
}, "maestroProvider: starting run_conversation");
|
|
305
|
+
let drained = false;
|
|
306
|
+
let aborted = false;
|
|
307
|
+
try {
|
|
308
|
+
for await (const event of runConversation(agent, messages)) {
|
|
309
|
+
yield event;
|
|
310
|
+
}
|
|
311
|
+
drained = true;
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
// Abort is a user-initiated signal, not a provider failure. claude/codex
|
|
315
|
+
// both silently return on AbortError (claude-provider relies on the SDK
|
|
316
|
+
// closing the stream, codex-provider catches `err.name === "AbortError"`
|
|
317
|
+
// and returns without yielding). Match that so the dispatcher doesn't
|
|
318
|
+
// see a synthetic "maestroProvider crashed: The operation was aborted"
|
|
319
|
+
// error event after the user simply moved on to a new prompt.
|
|
320
|
+
if (isAbortError(e) || abortSignal?.aborted) {
|
|
321
|
+
aborted = true;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
yield {
|
|
325
|
+
type: "error",
|
|
326
|
+
content: `maestroProvider crashed: ${e instanceof Error ? e.message : String(e)}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
abortSignal?.removeEventListener("abort", onAbortClosePool);
|
|
332
|
+
if (mcpPool) {
|
|
333
|
+
await mcpPool.close();
|
|
334
|
+
}
|
|
335
|
+
// Always persist a safe prefix — even on partial drain (abort mid-tool,
|
|
336
|
+
// Anthropic API throw, MCP crash). Earlier versions gated on
|
|
337
|
+
// `drained === true` to avoid persisting half-finished tool rounds, but
|
|
338
|
+
// in practice that gate dropped the entire turn on every abort and
|
|
339
|
+
// users saw "maestro forgets everything I just said". The new contract:
|
|
340
|
+
// - clean drain → save messages verbatim (no change)
|
|
341
|
+
// - partial / crashed turn → `trimToSafePrefix` strips orphan
|
|
342
|
+
// user-prompt or assistant-tool_use trailing entries so the next
|
|
343
|
+
// resume passes Anthropic's tool_use/tool_result pairing check
|
|
344
|
+
// If the trim collapses everything (e.g. the only push before the
|
|
345
|
+
// crash was the new user prompt), we skip the write so the previous
|
|
346
|
+
// good checkpoint stays intact.
|
|
347
|
+
try {
|
|
348
|
+
const safePrefix = drained ? messages : trimToSafePrefix(messages);
|
|
349
|
+
if (safePrefix.length > 0) {
|
|
350
|
+
saveMaestroSession(sessionId, safePrefix);
|
|
351
|
+
if (!drained && safePrefix.length < messages.length) {
|
|
352
|
+
logger.info({
|
|
353
|
+
sessionId,
|
|
354
|
+
fullLength: messages.length,
|
|
355
|
+
savedLength: safePrefix.length,
|
|
356
|
+
dropped: messages.length - safePrefix.length,
|
|
357
|
+
aborted,
|
|
358
|
+
}, "maestroProvider: persisted trimmed prefix after partial turn");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
logger.warn({ err, sessionId, turns: messages.length }, "maestroProvider: persist failed (best-effort)");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Compose the "Tool iterations remaining: N/M — <tone>" line that rides
|
|
369
|
+
* inside the per-iteration `<system-reminder>` block. Tone shifts with
|
|
370
|
+
* ABSOLUTE remaining count (not percentage of maxIter) — same threshold
|
|
371
|
+
* fires the same urgency regardless of effort level, so the model gets a
|
|
372
|
+
* consistent cue from a `low` run reaching 4-left as from a `xhigh` run
|
|
373
|
+
* reaching 4-left.
|
|
374
|
+
*
|
|
375
|
+
* Why absolute, not relative: at `low` (maxIter=5) the model crosses the
|
|
376
|
+
* wrap-up line almost immediately; at `xhigh` (maxIter=90) 75% left lasts
|
|
377
|
+
* ~22 turns. The user-visible behavior the threshold is targeting is "how
|
|
378
|
+
* many tool calls before I MUST stop" — that's an absolute count, not a
|
|
379
|
+
* fraction.
|
|
380
|
+
*
|
|
381
|
+
* Thresholds:
|
|
382
|
+
* - >= 10 → plenty of room. (no urgency)
|
|
383
|
+
* - 5..9 → pace yourself.
|
|
384
|
+
* - 2..4 → start wrapping up — consolidate, avoid new tool calls.
|
|
385
|
+
* - 0..1 → finalize NOW. Stop tooling, write the answer.
|
|
386
|
+
*
|
|
387
|
+
* Imperative phrasing matters: passing a bare count ("3 remaining") gets
|
|
388
|
+
* acknowledged but doesn't change behavior much. Pairing the count with a
|
|
389
|
+
* verb the model can act on ("start wrapping up", "finalize NOW") is what
|
|
390
|
+
* shifts the next-token distribution toward "emit final answer" instead
|
|
391
|
+
* of "call another tool". Exported so sub-agent / tests can share the
|
|
392
|
+
* exact phrasing if they need parity.
|
|
393
|
+
*/
|
|
394
|
+
export function iterationBudgetLine(remaining, max) {
|
|
395
|
+
let tone;
|
|
396
|
+
if (remaining >= 10) {
|
|
397
|
+
tone = "plenty of room.";
|
|
398
|
+
}
|
|
399
|
+
else if (remaining >= 5) {
|
|
400
|
+
tone = "pace yourself.";
|
|
401
|
+
}
|
|
402
|
+
else if (remaining >= 2) {
|
|
403
|
+
tone = "start wrapping up — consolidate findings, avoid new tool calls unless essential.";
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
tone = "finalize NOW. Stop tooling and write the final answer.";
|
|
407
|
+
}
|
|
408
|
+
return `Tool iterations remaining: ${remaining}/${max} — ${tone}`;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Pick the right provider adapter for a resolved model id. DeepSeek's V4
|
|
412
|
+
* family uses `deepseek-*` ids; everything else (Anthropic claude-* + future
|
|
413
|
+
* direct full ids) falls through to the Anthropic adapter. Exported so tests
|
|
414
|
+
* can lock the dispatch shape independently of `maestroProvider`'s I/O.
|
|
415
|
+
*/
|
|
416
|
+
export function providerForModel(resolvedModel) {
|
|
417
|
+
if (resolvedModel.startsWith("deepseek-")) {
|
|
418
|
+
return DeepseekProvider.fromEnv();
|
|
419
|
+
}
|
|
420
|
+
return AnthropicProvider.fromEnv();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Recognize the multiple shapes Node + WHATWG fetch use when a request is
|
|
424
|
+
* cancelled via `AbortController`:
|
|
425
|
+
* - DOMException with name "AbortError" (fetch / EventSource)
|
|
426
|
+
* - Error with name "AbortError" (older Node paths)
|
|
427
|
+
* - DOMException with code 20 (legacy ABORT_ERR code)
|
|
428
|
+
*
|
|
429
|
+
* Catches both so the abort detection isn't tied to a single runtime.
|
|
430
|
+
*
|
|
431
|
+
* Exported for unit coverage — used internally by `maestroProvider`'s catch
|
|
432
|
+
* branch to distinguish a user-initiated abort from a real provider crash.
|
|
433
|
+
*/
|
|
434
|
+
export function isAbortError(err) {
|
|
435
|
+
if (!err || typeof err !== "object")
|
|
436
|
+
return false;
|
|
437
|
+
const e = err;
|
|
438
|
+
if (e.name === "AbortError")
|
|
439
|
+
return true;
|
|
440
|
+
if (e.code === 20 || e.code === "ABORT_ERR")
|
|
441
|
+
return true;
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
//# sourceMappingURL=provider.js.map
|