mercury-agent 0.8.11 → 0.8.12
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/package.json
CHANGED
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import type { MessageAttachment, StoredMessage } from "../types.js";
|
|
14
14
|
import {
|
|
15
|
+
type CapabilitySource,
|
|
15
16
|
DEFAULT_CAPABILITIES,
|
|
16
17
|
type ModelCapabilities,
|
|
18
|
+
type WireModelCapabilities,
|
|
17
19
|
} from "./model-capabilities-core.js";
|
|
18
20
|
import { classifyPiFailure } from "./pi-failure-class.js";
|
|
19
21
|
import {
|
|
@@ -59,48 +61,60 @@ function backoffMs(attemptIndex: number): number {
|
|
|
59
61
|
return Math.min(cap, base + jitter);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
const CAPABILITY_SOURCES: readonly CapabilitySource[] = [
|
|
65
|
+
"env",
|
|
66
|
+
"yaml",
|
|
67
|
+
"builtin",
|
|
68
|
+
"default",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** Guessed capabilities, explicitly marked as such. */
|
|
72
|
+
function unverifiedCapabilities(): WireModelCapabilities {
|
|
73
|
+
return { ...DEFAULT_CAPABILITIES, source: "default" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parsePartialCapabilities(obj: unknown): WireModelCapabilities {
|
|
77
|
+
if (!obj || typeof obj !== "object") return unverifiedCapabilities();
|
|
64
78
|
const o = obj as Record<string, unknown>;
|
|
65
|
-
const out =
|
|
79
|
+
const out: WireModelCapabilities = unverifiedCapabilities();
|
|
66
80
|
if (typeof o.tools === "boolean") out.tools = o.tools;
|
|
67
81
|
if (typeof o.vision === "boolean") out.vision = o.vision;
|
|
68
82
|
if (typeof o.audio_input === "boolean") out.audio_input = o.audio_input;
|
|
69
83
|
if (typeof o.audio_output === "boolean") out.audio_output = o.audio_output;
|
|
70
84
|
if (typeof o.extended_thinking === "boolean")
|
|
71
85
|
out.extended_thinking = o.extended_thinking;
|
|
86
|
+
if (
|
|
87
|
+
typeof o.source === "string" &&
|
|
88
|
+
(CAPABILITY_SOURCES as readonly string[]).includes(o.source)
|
|
89
|
+
) {
|
|
90
|
+
out.source = o.source as CapabilitySource;
|
|
91
|
+
}
|
|
72
92
|
return out;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
/**
|
|
76
96
|
* Per-leg capabilities from host (MODEL_CHAIN_CAPABILITIES JSON array).
|
|
77
|
-
* When missing or invalid,
|
|
97
|
+
* When missing or invalid, falls back to guessed capabilities marked
|
|
98
|
+
* `source: "default"` so no unverified limitation is asserted to the model.
|
|
78
99
|
*/
|
|
79
100
|
function parseModelChainCapabilitiesFromEnv(
|
|
80
101
|
legCount: number,
|
|
81
|
-
):
|
|
102
|
+
): WireModelCapabilities[] {
|
|
103
|
+
const fallback = (): WireModelCapabilities[] =>
|
|
104
|
+
Array.from({ length: legCount }, unverifiedCapabilities);
|
|
105
|
+
|
|
82
106
|
const raw = process.env.MODEL_CHAIN_CAPABILITIES?.trim();
|
|
83
|
-
if (!raw)
|
|
84
|
-
return Array.from({ length: legCount }, () => ({
|
|
85
|
-
...DEFAULT_CAPABILITIES,
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
107
|
+
if (!raw) return fallback();
|
|
88
108
|
try {
|
|
89
109
|
const arr = JSON.parse(raw) as unknown;
|
|
90
|
-
if (!Array.isArray(arr))
|
|
91
|
-
|
|
92
|
-
...DEFAULT_CAPABILITIES,
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
const out: ModelCapabilities[] = [];
|
|
110
|
+
if (!Array.isArray(arr)) return fallback();
|
|
111
|
+
const out: WireModelCapabilities[] = [];
|
|
96
112
|
for (let i = 0; i < legCount; i++) {
|
|
97
113
|
out.push(parsePartialCapabilities(arr[i]));
|
|
98
114
|
}
|
|
99
115
|
return out;
|
|
100
116
|
} catch {
|
|
101
|
-
return
|
|
102
|
-
...DEFAULT_CAPABILITIES,
|
|
103
|
-
}));
|
|
117
|
+
return fallback();
|
|
104
118
|
}
|
|
105
119
|
}
|
|
106
120
|
|
|
@@ -191,33 +205,20 @@ function formatContextTimestamp(ms: number): string {
|
|
|
191
205
|
});
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
):
|
|
208
|
-
if (!attachments?.length) return false;
|
|
209
|
-
return attachments.some(
|
|
210
|
-
(a) =>
|
|
211
|
-
a.type === "audio" ||
|
|
212
|
-
a.type === "voice" ||
|
|
213
|
-
(a.mimeType?.toLowerCase().startsWith("audio/") ?? false),
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function buildCapabilitySection(
|
|
218
|
-
caps: ModelCapabilities,
|
|
219
|
-
payload: Payload,
|
|
220
|
-
): string {
|
|
208
|
+
/**
|
|
209
|
+
* Narrates only `tools` — the one capability pi does not track and Mercury can
|
|
210
|
+
* state as fact, since a `false` can only come from operator config.
|
|
211
|
+
*
|
|
212
|
+
* Vision and audio are deliberately absent. pi's `read` tool already emits
|
|
213
|
+
* "[Current model does not support images...]" at the moment an image is read,
|
|
214
|
+
* keyed to the model it is actually calling, and stays silent for model ids it
|
|
215
|
+
* does not recognise. Mercury's up-front version had to guess for unknown ids,
|
|
216
|
+
* and `DEFAULT_CAPABILITIES` guesses `vision: false` — which told every model
|
|
217
|
+
* newer than the pinned pi registry that it was blind. The audio flags could
|
|
218
|
+
* never be anything but `false` from a lookup (pi has no audio models, and
|
|
219
|
+
* voice notes are transcribed host-side), so they only ever added noise.
|
|
220
|
+
*/
|
|
221
|
+
export function buildCapabilitySection(caps: WireModelCapabilities): string {
|
|
221
222
|
const parts: string[] = ["## Current model capabilities"];
|
|
222
223
|
parts.push(
|
|
223
224
|
`This turn uses a model with the following constraints (do not assume you can exceed them):`,
|
|
@@ -225,15 +226,6 @@ function buildCapabilitySection(
|
|
|
225
226
|
parts.push(
|
|
226
227
|
`- **tools (bash / read / write / edit):** ${caps.tools ? "available" : "NOT available — you cannot run shell commands, read/write workspace files via tools, or use mrctl"}`,
|
|
227
228
|
);
|
|
228
|
-
parts.push(
|
|
229
|
-
`- **vision (images):** ${caps.vision ? "available" : "NOT available"}`,
|
|
230
|
-
);
|
|
231
|
-
parts.push(
|
|
232
|
-
`- **audio input:** ${caps.audio_input ? "available" : "NOT available"}`,
|
|
233
|
-
);
|
|
234
|
-
parts.push(
|
|
235
|
-
`- **audio output:** ${caps.audio_output ? "available" : "NOT available"}`,
|
|
236
|
-
);
|
|
237
229
|
|
|
238
230
|
if (!caps.tools) {
|
|
239
231
|
parts.push("");
|
|
@@ -242,20 +234,6 @@ function buildCapabilitySection(
|
|
|
242
234
|
);
|
|
243
235
|
}
|
|
244
236
|
|
|
245
|
-
if (!caps.vision && hasImageAttachments(payload.attachments)) {
|
|
246
|
-
parts.push("");
|
|
247
|
-
parts.push(
|
|
248
|
-
`**Note:** This model cannot process image pixels. Image files are still listed in <attachments /> with paths — you may reference paths and filenames but cannot interpret visual content.`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (!caps.audio_input && hasAudioAttachments(payload.attachments)) {
|
|
253
|
-
parts.push("");
|
|
254
|
-
parts.push(
|
|
255
|
-
`**Note:** This model cannot process audio. Voice attachments are listed with paths only.`,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
237
|
return parts.join("\n");
|
|
260
238
|
}
|
|
261
239
|
|
|
@@ -356,7 +334,7 @@ Your prompt may include \`<active_episodes>\` XML with time-bounded topics relev
|
|
|
356
334
|
parts.push(claudeCodePreamble);
|
|
357
335
|
}
|
|
358
336
|
parts.push(mercuryPlatform);
|
|
359
|
-
parts.push(buildCapabilitySection(caps
|
|
337
|
+
parts.push(buildCapabilitySection(caps));
|
|
360
338
|
parts.push(memory);
|
|
361
339
|
parts.push(destructiveOps);
|
|
362
340
|
parts.push(toolResultPresentation);
|
|
@@ -13,6 +13,22 @@ export type ModelCapabilities = {
|
|
|
13
13
|
|
|
14
14
|
export type ModelCapabilityKey = keyof ModelCapabilities;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Where a capability set came from. `"default"` means the model id matched
|
|
18
|
+
* nothing — the flags below are guesses, not facts, and callers must not
|
|
19
|
+
* present them to the model as constraints. See `buildCapabilitySection`.
|
|
20
|
+
*/
|
|
21
|
+
export type CapabilitySource = "env" | "yaml" | "builtin" | "default";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Capabilities as serialized to the container over `MODEL_CHAIN_CAPABILITIES`.
|
|
25
|
+
* `source` rides along so the container can tell a looked-up `false` from an
|
|
26
|
+
* assumed one.
|
|
27
|
+
*/
|
|
28
|
+
export type WireModelCapabilities = ModelCapabilities & {
|
|
29
|
+
source?: CapabilitySource;
|
|
30
|
+
};
|
|
31
|
+
|
|
16
32
|
/** Fallback when no builtin / YAML / env match. */
|
|
17
33
|
export const DEFAULT_CAPABILITIES: ModelCapabilities = {
|
|
18
34
|
tools: true,
|
|
@@ -9,18 +9,23 @@ import { parse as parseYaml } from "yaml";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import type { ModelLeg } from "../config.js";
|
|
11
11
|
import {
|
|
12
|
+
type CapabilitySource,
|
|
12
13
|
DEFAULT_CAPABILITIES,
|
|
13
14
|
type ModelCapabilities,
|
|
14
15
|
type ModelCapabilityKey,
|
|
16
|
+
type WireModelCapabilities,
|
|
15
17
|
} from "./model-capabilities-core.js";
|
|
16
18
|
|
|
17
19
|
export type {
|
|
20
|
+
CapabilitySource,
|
|
18
21
|
ModelCapabilities,
|
|
19
22
|
ModelCapabilityKey,
|
|
23
|
+
WireModelCapabilities,
|
|
20
24
|
} from "./model-capabilities-core.js";
|
|
21
25
|
export { DEFAULT_CAPABILITIES } from "./model-capabilities-core.js";
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
/** @deprecated Use `CapabilitySource` from model-capabilities-core. */
|
|
28
|
+
export type CapabilityResolveSource = CapabilitySource;
|
|
24
29
|
|
|
25
30
|
export type ResolvedModelCapabilities = {
|
|
26
31
|
capabilities: ModelCapabilities;
|
|
@@ -164,22 +169,40 @@ export function resolveModelChainCapabilities(
|
|
|
164
169
|
dataDirAbsolute: string,
|
|
165
170
|
envCaps: ModelCapabilities | null,
|
|
166
171
|
): {
|
|
167
|
-
chainCaps:
|
|
172
|
+
chainCaps: WireModelCapabilities[];
|
|
168
173
|
userMap: UserModelCapabilitiesMap | null;
|
|
169
174
|
} {
|
|
170
175
|
const userMap = loadUserModelCapabilitiesMap(dataDirAbsolute);
|
|
171
|
-
const chainCaps = chain.map((leg) =>
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
const chainCaps = chain.map((leg) => {
|
|
177
|
+
const { capabilities, source } = resolveModelCapabilitiesWithSource(
|
|
178
|
+
leg.model,
|
|
179
|
+
leg.provider,
|
|
180
|
+
userMap,
|
|
181
|
+
envCaps,
|
|
182
|
+
);
|
|
183
|
+
return { ...capabilities, source };
|
|
184
|
+
});
|
|
174
185
|
return { chainCaps, userMap };
|
|
175
186
|
}
|
|
176
187
|
|
|
177
188
|
export function chainSupportsRequirements(
|
|
178
189
|
requires: ModelCapabilityKey[],
|
|
179
|
-
chainCaps:
|
|
190
|
+
chainCaps: WireModelCapabilities[],
|
|
180
191
|
): boolean {
|
|
181
192
|
if (requires.length === 0) return true;
|
|
182
|
-
|
|
193
|
+
// A `false` from an unresolved model id (`source: "default"`) is a guess, not
|
|
194
|
+
// a fact — `DEFAULT_CAPABILITIES` assumes no vision, so every model newer
|
|
195
|
+
// than the pinned pi registry looks incapable here. Dropping a
|
|
196
|
+
// capability-gated extension or skill on that guess removes function from a
|
|
197
|
+
// model that may well support it, and does so invisibly at startup. Treat
|
|
198
|
+
// unknown as permissive: let the extension install and fail visibly at use
|
|
199
|
+
// time instead. This is the last consumer of `source` — the system prompt no
|
|
200
|
+
// longer narrates vision or audio at all (see `buildCapabilitySection`).
|
|
201
|
+
const satisfies = (caps: WireModelCapabilities, key: ModelCapabilityKey) =>
|
|
202
|
+
caps[key] === true || caps.source === "default";
|
|
203
|
+
return chainCaps.some((caps) =>
|
|
204
|
+
requires.every((key) => satisfies(caps, key)),
|
|
205
|
+
);
|
|
183
206
|
}
|
|
184
207
|
|
|
185
208
|
/** Log warnings for models that fell back to defaults (once per distinct model id). */
|
package/src/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type ModelCapabilities,
|
|
5
5
|
parseModelCapabilitiesEnv,
|
|
6
6
|
resolveModelChainCapabilities,
|
|
7
|
+
type WireModelCapabilities,
|
|
7
8
|
} from "./agent/model-capabilities.js";
|
|
8
9
|
import { mergeRawMercuryConfig } from "./config-file.js";
|
|
9
10
|
import { parseModelLegsArray } from "./config-model-chain.js";
|
|
@@ -305,7 +306,7 @@ export type AppConfig = z.infer<typeof schema> & {
|
|
|
305
306
|
/** Parsed MERCURY_MODEL_CAPABILITIES override, if valid. */
|
|
306
307
|
parsedModelCapabilitiesEnv: ModelCapabilities | null;
|
|
307
308
|
/** Capabilities per chain leg (same order as resolvedModelChain). */
|
|
308
|
-
resolvedModelChainCapabilities:
|
|
309
|
+
resolvedModelChainCapabilities: WireModelCapabilities[];
|
|
309
310
|
/** Effective budget after clamping to container timeout. */
|
|
310
311
|
effectiveModelChainBudgetMs: number;
|
|
311
312
|
/** Parsed `extensionDefaults` JSON: flat "ext.key" → value map. */
|