@mono-agent/agent-runtime 0.18.1 → 0.18.3
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/MIGRATION.md +32 -0
- package/README.md +50 -0
- package/package.json +1 -1
- package/src/agent/tools/agent-tool.js +16 -4
- package/src/agent/tools/web-controller.js +97 -7
- package/src/agent/tools/web-search.js +296 -60
- package/src/ai/providers/claude-cli.js +77 -22
- package/src/ai/providers/claude-sdk.js +54 -11
- package/src/ai/providers/claude-subagent-activity.js +719 -0
- package/src/ai/providers/codex-app.js +1039 -105
- package/src/ai/providers/pi-native/result-builder.js +11 -0
- package/src/ai/providers/pi-native.js +22 -2
- package/src/ai/providers/transport-errors.js +154 -0
- package/src/ai/runtime/router.js +42 -0
- package/src/ai/types.js +98 -10
- package/types/agent/tools/web-controller.d.ts +5 -1
- package/types/agent/tools/web-search.d.ts +13 -0
- package/types/ai/providers/claude-subagent-activity.d.ts +53 -0
- package/types/ai/providers/transport-errors.d.ts +44 -0
- package/types/ai/runtime/router.d.ts +9 -0
- package/types/ai/types.d.ts +231 -20
package/MIGRATION.md
CHANGED
|
@@ -17,6 +17,31 @@ the configuration schema.
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
+
## 0.18.2
|
|
21
|
+
|
|
22
|
+
- **Normalized native-subagent activity:** Claude SDK/CLI and Codex app-server
|
|
23
|
+
children now emit the exact `subagent_activity` lifecycle. Treat
|
|
24
|
+
`subagent.id` as the parent attachment key, `nativeId` only as provider
|
|
25
|
+
correlation metadata, and `phase: "message"` as child prose rather than
|
|
26
|
+
parent answer text or a completed tool call.
|
|
27
|
+
- **Explicit native configuration trust:** Claude SDK filesystem settings stay
|
|
28
|
+
disabled unless `settingSources` opts into `user`, `project`, or `local`.
|
|
29
|
+
Those sources may execute hooks and plugins, not only load agents. Codex
|
|
30
|
+
repository instructions remain disabled unless `codexLoadProjectDocs` is
|
|
31
|
+
true; explicit app-server arguments remain authoritative.
|
|
32
|
+
- **Provider-owned Codex agents:** non-empty caller-defined `nativeSubagents`
|
|
33
|
+
teammates now fail direct Codex startup with `skipped_capability_mismatch` so
|
|
34
|
+
a router may continue to Claude. Do not synthesize `collaborationMode` or
|
|
35
|
+
assume Claude profile definitions are portable to Codex.
|
|
36
|
+
- **Per-attempt policy projection:** `resolveAttempt().policyOptions` may replace
|
|
37
|
+
only `allowedTools`, `disallowedTools`, and `permissionMode` for the active
|
|
38
|
+
route. General resolver `options` still cannot replace protected request
|
|
39
|
+
fields.
|
|
40
|
+
- **Pi inline helper ceiling:** the runtime-owned `general-purpose` profile is
|
|
41
|
+
limited to its read-only defaults intersected with `subagents.inline.allowedTools`.
|
|
42
|
+
An empty intersection disables that fallback profile rather than restoring
|
|
43
|
+
wider defaults.
|
|
44
|
+
|
|
20
45
|
## 0.15.2
|
|
21
46
|
|
|
22
47
|
- **Tool-policy capability discovery:** built-in bridge capabilities now report
|
|
@@ -282,6 +307,13 @@ a `supports_native_subagents` requirement when a run passes
|
|
|
282
307
|
native-subagent runs, ensure at least one entry supports native subagents, or the
|
|
283
308
|
run reports exhausted instead of degrading silently.
|
|
284
309
|
|
|
310
|
+
Caller-defined `nativeSubagents.teammates` are a Claude-only projection. Codex
|
|
311
|
+
still advertises and reports its provider-owned native collaboration surface,
|
|
312
|
+
but a direct Codex attempt now rejects configured teammate/profile definitions
|
|
313
|
+
with `codex_native_subagent_definitions_unsupported` before transport; a router
|
|
314
|
+
may then continue to Claude. Use `codexLoadProjectDocs: true` to enable Codex's
|
|
315
|
+
repository instructions, not to define Codex collaboration profiles.
|
|
316
|
+
|
|
285
317
|
### 6. Diagnostics & internal behavior changes (no API change)
|
|
286
318
|
|
|
287
319
|
- **Pi multimodal**: image inputs are delivered to the model as image content
|
package/README.md
CHANGED
|
@@ -750,6 +750,9 @@ Per-call options (a non-exhaustive selection):
|
|
|
750
750
|
| `cwd` | `string` | Working directory for the agent's tools. |
|
|
751
751
|
| `allowedTools` | `string[]` | Built-in tool allowlist. Default: all. |
|
|
752
752
|
| `disallowedTools` | `string[]` | Block list. |
|
|
753
|
+
| `nativeSubagents` | `object` | Caller-defined Claude native `Task` profiles. Direct Codex rejects configured teammate definitions because Codex owns its collaboration agents. |
|
|
754
|
+
| `settingSources` | `("user" \| "project" \| "local")[]` | Claude Agent SDK filesystem settings opt-in. Omitted/empty disables those three sources; Anthropic managed settings still apply. |
|
|
755
|
+
| `codexLoadProjectDocs` | `boolean` | Codex app-server repository-instruction opt-in. Omitted/false sets `project_doc_max_bytes=0`; true restores Codex defaults. Explicit `codexAppServerArgs` wins. |
|
|
753
756
|
| `mcpServers` | `Record<string, McpServerConfig>` | Configured MCP servers (stdio / sse / http); on direct Codex, each forwarded server authorizes its own tool calls. |
|
|
754
757
|
| `sandboxPolicy` | `SandboxPolicy` | Optional fail-closed sandbox policy for built-in tools and stdio MCP process startup. |
|
|
755
758
|
| `webSearchConfig` | `{ backend?, endpoint? }` | Run-scoped local SearXNG/keyless WebSearch backend selection. |
|
|
@@ -789,6 +792,49 @@ second applied event. A throwing host `acknowledge` or `reject` callback cannot
|
|
|
789
792
|
change the native steering outcome; the Codex bridge reports it as a bounded
|
|
790
793
|
`live_input_callback_failed` runtime warning.
|
|
791
794
|
|
|
795
|
+
### Provider-native subagents and project instructions
|
|
796
|
+
|
|
797
|
+
Claude SDK runs are filesystem-isolated by default: mono-agent passes
|
|
798
|
+
`settingSources: []`, which disables user, project, and local settings sources,
|
|
799
|
+
including their `CLAUDE.md`, hooks, plugins, and `.claude/agents` profiles.
|
|
800
|
+
Anthropic managed settings remain in force and may still configure hooks or
|
|
801
|
+
plugins; `settingSources` is not a managed-policy bypass. Opt into only the
|
|
802
|
+
needed sources, for example `settingSources: ["project"]`. User, project, and
|
|
803
|
+
local settings may execute configured hooks and plugins, so enable only trusted
|
|
804
|
+
settings and avoid opting in while running in an untrusted checkout. This
|
|
805
|
+
option is SDK only. The Claude Code CLI performs its own settings discovery,
|
|
806
|
+
and mono-agent does not pass it a `--setting-sources` value.
|
|
807
|
+
|
|
808
|
+
Codex app-server owns its native collaboration agents and their profiles. The
|
|
809
|
+
bridge observes and normalizes their lifecycle, but it does not synthesize a
|
|
810
|
+
`collaborationMode` payload or inject caller-defined `nativeSubagents`
|
|
811
|
+
teammates. A non-empty configured teammate list fails before app-server startup
|
|
812
|
+
with `skipped_capability_mismatch`, allowing a fallback router to continue to a
|
|
813
|
+
Claude route.
|
|
814
|
+
|
|
815
|
+
Codex app-server runs disable automatic repository-instruction discovery by
|
|
816
|
+
default with `project_doc_max_bytes=0`. Set `codexLoadProjectDocs: true` when
|
|
817
|
+
Codex and its own collaboration agents should load repository instructions. If
|
|
818
|
+
`codexAppServerArgs` is supplied, that explicit argument vector is authoritative
|
|
819
|
+
and `codexLoadProjectDocs` does not alter it.
|
|
820
|
+
|
|
821
|
+
Provider-native and in-process delegation share `subagent_activity` telemetry.
|
|
822
|
+
`subagent.id` is the canonical parent attachment key: the initiating parent
|
|
823
|
+
tool-use id whenever the provider exposes it, or a stable synthetic key for an
|
|
824
|
+
orphan lifecycle record. `nativeId` is an optional provider task/thread id for
|
|
825
|
+
correlation only and never replaces that key.
|
|
826
|
+
The normalized phases are `agent_started`, `started`, `completed`, `message`,
|
|
827
|
+
and `agent_completed`. A `message` belongs to the child and must not be treated
|
|
828
|
+
as parent answer text or as a completed tool call.
|
|
829
|
+
|
|
830
|
+
Restrictive allowlists must still authorize the delegation surface. Include
|
|
831
|
+
`Agent` for the in-process built-in. Claude-native teammate definitions add
|
|
832
|
+
`Task` to an explicit allowed list automatically; filesystem profiles enabled
|
|
833
|
+
only through `settingSources` require callers to include `Task` themselves.
|
|
834
|
+
An explicit deny still wins. Direct Codex remains an allow-all-only bridge, so
|
|
835
|
+
a named restrictive allowlist fails before provider startup rather than being
|
|
836
|
+
silently widened.
|
|
837
|
+
|
|
792
838
|
Returns:
|
|
793
839
|
|
|
794
840
|
```ts
|
|
@@ -955,6 +1001,10 @@ Behaviour:
|
|
|
955
1001
|
- `resolveAttempt` runs once per attempt — including every same-model retry — and
|
|
956
1002
|
receives `{ attemptIndex, retryIndex }`, where `attemptIndex` stays the chain
|
|
957
1003
|
index. Its `cleanup` runs after each attempt.
|
|
1004
|
+
- `resolveAttempt().policyOptions` is the narrow host seam for translating one
|
|
1005
|
+
logical tool policy into the active provider's representation. It may replace
|
|
1006
|
+
only `allowedTools`, `disallowedTools`, and `permissionMode`; the resolver's
|
|
1007
|
+
general `options` bag still cannot replace protected request fields.
|
|
958
1008
|
|
|
959
1009
|
Chain entries can require backend capabilities via `requires: { structured_output: true, supports_mcp: true, ... }`; entries that don't satisfy the requirements are skipped (logged in `failoverHistory` as `failureKind: "skipped_capability_mismatch"`).
|
|
960
1010
|
|
package/package.json
CHANGED
|
@@ -84,7 +84,7 @@ State exactly what you want back ("return a bullet list of file:line and a one-l
|
|
|
84
84
|
/**
|
|
85
85
|
* @param {RuntimeSubagentsOptions} subagents
|
|
86
86
|
* @param {ReadonlyArray<RuntimeSubagentDefinition>} definitions
|
|
87
|
-
* @param {ReadonlyArray<string>|null} ceiling Tools
|
|
87
|
+
* @param {ReadonlyArray<string>|null} ceiling Tools available to the runtime-owned general-purpose or authored profiles, or null when authoring is off.
|
|
88
88
|
* @returns {string}
|
|
89
89
|
*/
|
|
90
90
|
function toolDescription(subagents, definitions, ceiling) {
|
|
@@ -309,7 +309,7 @@ export function createAgentTool(subagents, context = {}) {
|
|
|
309
309
|
}
|
|
310
310
|
const { profile, droppedTools } = authored
|
|
311
311
|
? buildInlineProfile(params, ceiling, names)
|
|
312
|
-
: { profile: resolveProfile(definitions, params?.name), droppedTools: [] };
|
|
312
|
+
: { profile: resolveProfile(definitions, params?.name, ceiling), droppedTools: [] };
|
|
313
313
|
if (profile === null) {
|
|
314
314
|
const available = [...names, GENERAL_PURPOSE_SUBAGENT].join(", ");
|
|
315
315
|
throw new Error(`Error: unknown subagent "${params?.name}". Available: ${available}.`);
|
|
@@ -482,7 +482,8 @@ export function createAgentTool(subagents, context = {}) {
|
|
|
482
482
|
}
|
|
483
483
|
|
|
484
484
|
/**
|
|
485
|
-
* The
|
|
485
|
+
* The ceiling for the runtime-owned general-purpose helper and authored
|
|
486
|
+
* subagents, or null when authoring is off.
|
|
486
487
|
*
|
|
487
488
|
* A host that enables authoring without stating a ceiling gets the read-only
|
|
488
489
|
* default rather than every built-in: the same reasoning as `normalizeProfile`,
|
|
@@ -548,14 +549,25 @@ function buildInlineProfile(params, ceiling, configuredNames) {
|
|
|
548
549
|
/**
|
|
549
550
|
* @param {ReadonlyArray<RuntimeSubagentDefinition>} definitions
|
|
550
551
|
* @param {string|undefined} name
|
|
552
|
+
* @param {ReadonlyArray<string>|null} ceiling Tool ceiling for the runtime-owned general-purpose profile.
|
|
551
553
|
* @returns {RuntimeSubagentDefinition|null}
|
|
552
554
|
*/
|
|
553
|
-
function resolveProfile(definitions, name) {
|
|
555
|
+
function resolveProfile(definitions, name, ceiling) {
|
|
554
556
|
if (name === undefined || name === null || name === GENERAL_PURPOSE_SUBAGENT) {
|
|
557
|
+
const allowedTools = ceiling === null
|
|
558
|
+
? DEFAULT_SUBAGENT_TOOLS
|
|
559
|
+
: DEFAULT_SUBAGENT_TOOLS.filter((tool) => ceiling.includes(tool));
|
|
560
|
+
// `normalizeProfile` treats an empty allow-list as "use the read-only
|
|
561
|
+
// default", which would silently widen this runtime-owned helper past a
|
|
562
|
+
// parent-policy ceiling that contains only write-capable tools.
|
|
563
|
+
if (allowedTools.length === 0) {
|
|
564
|
+
throw new Error(`Error: no read-only tools available to ${GENERAL_PURPOSE_SUBAGENT} within this agent's subagent ceiling (${ceiling?.join(", ") || "none"}). Use a configured profile, or do this yourself.`);
|
|
565
|
+
}
|
|
555
566
|
return normalizeProfile({
|
|
556
567
|
name: GENERAL_PURPOSE_SUBAGENT,
|
|
557
568
|
description: "Read-only researcher inheriting the main model.",
|
|
558
569
|
systemPrompt: "You are a focused research subagent. Work only from the task you were given — you cannot see the parent conversation and cannot ask anyone anything. Investigate with the tools you have, then finish with a written answer in exactly the shape the task requested. Cite file:line where relevant. Never modify files.",
|
|
570
|
+
allowedTools,
|
|
559
571
|
});
|
|
560
572
|
}
|
|
561
573
|
const found = definitions.find((definition) => definition.name === name);
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
5
|
+
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
4
6
|
import { performWebFetch } from "./web-fetch.js";
|
|
5
7
|
import { performWebSearch } from "./web-search.js";
|
|
6
8
|
|
|
7
9
|
const MAX_CACHE_ENTRIES = 64;
|
|
10
|
+
const MAX_SHARED_SEARCH_ENTRIES = 256;
|
|
11
|
+
const SHARED_SEARCH_TTL_MS = 15 * 60_000;
|
|
12
|
+
|
|
13
|
+
// Search results are shared process-wide rather than per model run. A run-scoped
|
|
14
|
+
// cache dies at the end of every turn and is invisible to sibling subagents, so
|
|
15
|
+
// an agent that keeps circling the same topic re-hits the network every time —
|
|
16
|
+
// which is exactly the request volume that gets keyless engines to rate-limit.
|
|
17
|
+
// One process is one agent instance, so the sharing stays inside one tenant.
|
|
18
|
+
//
|
|
19
|
+
// Only completed results are shared. In-flight promises stay per-controller on
|
|
20
|
+
// purpose: they carry the originating run's AbortSignal, and handing that to a
|
|
21
|
+
// sibling would let one turn's cancellation fail another turn's search.
|
|
22
|
+
/** @type {Map<string, {value: any, expiresAt: number}>} */
|
|
23
|
+
const sharedSearchCache = new Map();
|
|
8
24
|
|
|
9
25
|
/**
|
|
10
26
|
* One ephemeral web-tool controller for one model run. It owns in-memory
|
|
11
|
-
* deduplication, result
|
|
27
|
+
* deduplication, the fetch result cache, anonymous browser namespaces, and
|
|
28
|
+
* cleanup. Search results are the exception: they live in the process-wide
|
|
29
|
+
* cache above so sibling subagents and later turns can reuse them.
|
|
12
30
|
*
|
|
13
31
|
* @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any}} [options]
|
|
14
32
|
*/
|
|
@@ -22,7 +40,6 @@ export function createWebToolController({
|
|
|
22
40
|
browserRenderer,
|
|
23
41
|
} = {}) {
|
|
24
42
|
const namespace = `mono-agent-web-${randomUUID()}`;
|
|
25
|
-
const searchCache = new Map();
|
|
26
43
|
const fetchCache = new Map();
|
|
27
44
|
const searchInFlight = new Map();
|
|
28
45
|
const fetchInFlight = new Map();
|
|
@@ -66,15 +83,59 @@ export function createWebToolController({
|
|
|
66
83
|
}
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} key
|
|
88
|
+
* @param {() => Promise<any>} execute
|
|
89
|
+
*/
|
|
90
|
+
async function cachedSearch(key, execute) {
|
|
91
|
+
if (closed) return closedResult();
|
|
92
|
+
const cached = readSharedSearch(key);
|
|
93
|
+
if (cached) return withCacheHit(cached);
|
|
94
|
+
const active = searchInFlight.get(key);
|
|
95
|
+
if (active) return withCacheHit(await active);
|
|
96
|
+
const task = Promise.resolve().then(execute);
|
|
97
|
+
searchInFlight.set(key, task);
|
|
98
|
+
try {
|
|
99
|
+
const result = await task;
|
|
100
|
+
// Failures stay uncached; the backend cooldown in web-search.js is what
|
|
101
|
+
// stops a rate-limited engine from being hammered again.
|
|
102
|
+
if (!result.error) writeSharedSearch(key, cloneResult(result));
|
|
103
|
+
return result;
|
|
104
|
+
} finally {
|
|
105
|
+
searchInFlight.delete(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
69
109
|
return {
|
|
70
110
|
namespace,
|
|
71
111
|
|
|
72
112
|
async search(params, execution = {}) {
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
// The key must pin the backend, the endpoint AND the network policy the
|
|
114
|
+
// search actually ran under. A params-only key was safe while the cache
|
|
115
|
+
// lived and died with one run; process-wide it would let controllers with
|
|
116
|
+
// different backends read each other's results.
|
|
117
|
+
//
|
|
118
|
+
// The policy has to be the RESOLVED one, computed exactly as
|
|
119
|
+
// performWebSearch computes it: the effective policy is the context
|
|
120
|
+
// policy merged with the request policy, so keying on the request half
|
|
121
|
+
// alone would let a run whose context denies network read entries a
|
|
122
|
+
// network-allowed run had populated. Resolved per call because
|
|
123
|
+
// readToolRuntime() is mutable process state.
|
|
124
|
+
//
|
|
125
|
+
// The snapshot is then handed to performWebSearch as the request policy
|
|
126
|
+
// rather than letting it re-resolve. Execution is deferred by a microtask
|
|
127
|
+
// and updateToolContext mutates the context in place by design, so a
|
|
128
|
+
// re-resolve could enforce a policy the key never described — caching a
|
|
129
|
+
// network-allowed result under a denied key. Merging is monotonic, so
|
|
130
|
+
// passing the snapshot back in means enforcement can only be at least as
|
|
131
|
+
// strict as the key claims.
|
|
132
|
+
const resolvedCtx = ctx ?? readToolRuntime();
|
|
133
|
+
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
134
|
+
const key = stableKey({ params, searchConfig, policy });
|
|
135
|
+
return cachedSearch(key, async () => performWebSearch(params, {
|
|
75
136
|
searchConfig,
|
|
76
|
-
sandboxPolicy,
|
|
77
|
-
ctx,
|
|
137
|
+
sandboxPolicy: policy,
|
|
138
|
+
ctx: resolvedCtx,
|
|
78
139
|
fetchImpl,
|
|
79
140
|
signal: execution.signal,
|
|
80
141
|
}));
|
|
@@ -101,7 +162,9 @@ export function createWebToolController({
|
|
|
101
162
|
const pending = [...cleanups];
|
|
102
163
|
cleanups.clear();
|
|
103
164
|
await Promise.allSettled(pending.map((cleanup) => Promise.resolve().then(cleanup)));
|
|
104
|
-
|
|
165
|
+
// The shared search cache deliberately outlives the controller; only
|
|
166
|
+
// run-owned state (browser namespaces, fetch results, in-flight work) is
|
|
167
|
+
// dropped here.
|
|
105
168
|
fetchCache.clear();
|
|
106
169
|
searchInFlight.clear();
|
|
107
170
|
fetchInFlight.clear();
|
|
@@ -109,6 +172,33 @@ export function createWebToolController({
|
|
|
109
172
|
};
|
|
110
173
|
}
|
|
111
174
|
|
|
175
|
+
function readSharedSearch(key) {
|
|
176
|
+
const entry = sharedSearchCache.get(key);
|
|
177
|
+
if (!entry) return null;
|
|
178
|
+
if (Date.now() >= entry.expiresAt) {
|
|
179
|
+
sharedSearchCache.delete(key);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return entry.value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writeSharedSearch(key, value) {
|
|
186
|
+
sharedSearchCache.set(key, { value, expiresAt: Date.now() + SHARED_SEARCH_TTL_MS });
|
|
187
|
+
// Drop expired entries before falling back to insertion-order eviction, so a
|
|
188
|
+
// burst of stale keys cannot push a live one out.
|
|
189
|
+
for (const [entryKey, entry] of sharedSearchCache) {
|
|
190
|
+
if (Date.now() >= entry.expiresAt) sharedSearchCache.delete(entryKey);
|
|
191
|
+
}
|
|
192
|
+
while (sharedSearchCache.size > MAX_SHARED_SEARCH_ENTRIES) {
|
|
193
|
+
sharedSearchCache.delete(sharedSearchCache.keys().next().value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Test hook: the shared cache is module state and would leak between cases. */
|
|
198
|
+
export function __resetSharedSearchCacheForTests() {
|
|
199
|
+
sharedSearchCache.clear();
|
|
200
|
+
}
|
|
201
|
+
|
|
112
202
|
function stableKey(value) {
|
|
113
203
|
return JSON.stringify(sortValue(value));
|
|
114
204
|
}
|