mercury-agent 0.4.5
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 +22 -0
- package/README.md +438 -0
- package/container/Dockerfile +127 -0
- package/container/Dockerfile.base +109 -0
- package/container/Dockerfile.power +17 -0
- package/container/agent-package.json +8 -0
- package/container/build.sh +54 -0
- package/docs/TODOS.md +147 -0
- package/docs/auth/dashboard.md +28 -0
- package/docs/auth/overview.md +109 -0
- package/docs/auth/whatsapp.md +173 -0
- package/docs/configuration.md +54 -0
- package/docs/container-lifecycle.md +349 -0
- package/docs/context-architecture.md +87 -0
- package/docs/deployment.md +199 -0
- package/docs/extensions.md +375 -0
- package/docs/graceful-shutdown.md +62 -0
- package/docs/kb-distillation.md +77 -0
- package/docs/media/overview.md +140 -0
- package/docs/media/whatsapp.md +171 -0
- package/docs/memory.md +137 -0
- package/docs/permissions.md +217 -0
- package/docs/pipeline.md +228 -0
- package/docs/prd-chat-memory.md +76 -0
- package/docs/prd-config-load.md +82 -0
- package/docs/rate-limiting.md +166 -0
- package/docs/scheduler.md +288 -0
- package/docs/setup-discord.md +100 -0
- package/docs/setup-slack.md +119 -0
- package/docs/setup-whatsapp.md +94 -0
- package/docs/subagents.md +166 -0
- package/docs/web-search.md +62 -0
- package/examples/extensions/README.md +12 -0
- package/examples/extensions/charts/index.ts +13 -0
- package/examples/extensions/charts/skill/SKILL.md +98 -0
- package/examples/extensions/gws/README.md +52 -0
- package/examples/extensions/gws/index.ts +106 -0
- package/examples/extensions/gws/skill/SKILL.md +57 -0
- package/examples/extensions/gws/skill/references/calendar.md +101 -0
- package/examples/extensions/gws/skill/references/docs.md +65 -0
- package/examples/extensions/gws/skill/references/drive.md +79 -0
- package/examples/extensions/gws/skill/references/gmail.md +85 -0
- package/examples/extensions/gws/skill/references/sheets.md +60 -0
- package/examples/extensions/napkin/index.ts +821 -0
- package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
- package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
- package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
- package/examples/extensions/napkin/skill/SKILL.md +728 -0
- package/examples/extensions/pdf/index.ts +23 -0
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
- package/examples/extensions/pdf/skill/SKILL.md +314 -0
- package/examples/extensions/pdf/skill/forms.md +294 -0
- package/examples/extensions/pdf/skill/reference.md +612 -0
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/examples/extensions/permission-guard/index.ts +65 -0
- package/examples/extensions/pinchtab/index.ts +199 -0
- package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
- package/examples/extensions/pinchtab/skill/references/api.md +297 -0
- package/examples/extensions/pinchtab/skill/references/env.md +45 -0
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
- package/examples/extensions/tradestation/host/refresh.ts +102 -0
- package/examples/extensions/tradestation/index.ts +153 -0
- package/examples/extensions/tradestation/skill/SKILL.md +67 -0
- package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
- package/examples/extensions/voice-synth/index.ts +94 -0
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
- package/examples/extensions/voice-transcribe/index.ts +381 -0
- package/examples/extensions/voice-transcribe/requirements.txt +8 -0
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
- package/examples/extensions/web-search/index.ts +22 -0
- package/examples/extensions/web-search/skill/SKILL.md +114 -0
- package/examples/extensions/web-search/skill/references/apartments.md +178 -0
- package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
- package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
- package/examples/extensions/web-search/skill/references/flights.md +133 -0
- package/examples/extensions/web-search/skill/references/hotels.md +148 -0
- package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
- package/examples/extensions/yahoo-mail/cli/package.json +13 -0
- package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
- package/examples/extensions/yahoo-mail/index.ts +57 -0
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
- package/package.json +106 -0
- package/resources/agents/explore.md +50 -0
- package/resources/agents/worker.md +24 -0
- package/resources/builtin-extensions.txt +3 -0
- package/resources/connection-env-vars.json +25 -0
- package/resources/extensions/.gitkeep +0 -0
- package/resources/pi-extensions/subagent/agents.ts +126 -0
- package/resources/pi-extensions/subagent/index.ts +964 -0
- package/resources/profiles/coding/AGENTS.md +43 -0
- package/resources/profiles/coding/mercury-profile.yaml +15 -0
- package/resources/profiles/general/AGENTS.md +31 -0
- package/resources/profiles/general/mercury-profile.yaml +15 -0
- package/resources/profiles/research/AGENTS.md +40 -0
- package/resources/profiles/research/mercury-profile.yaml +15 -0
- package/resources/skills/config/SKILL.md +25 -0
- package/resources/skills/context/SKILL.md +33 -0
- package/resources/skills/conversation-recap/SKILL.md +19 -0
- package/resources/skills/media/SKILL.md +27 -0
- package/resources/skills/mutes/SKILL.md +31 -0
- package/resources/skills/permissions/SKILL.md +19 -0
- package/resources/skills/preferences/SKILL.md +31 -0
- package/resources/skills/recall/SKILL.md +24 -0
- package/resources/skills/roles/SKILL.md +18 -0
- package/resources/skills/spaces/SKILL.md +18 -0
- package/resources/skills/tasks/SKILL.md +45 -0
- package/resources/templates/AGENTS.md +157 -0
- package/resources/templates/env.template +34 -0
- package/resources/templates/mercury.example.yaml +75 -0
- package/src/adapters/discord-native.ts +534 -0
- package/src/adapters/discord.ts +38 -0
- package/src/adapters/setup.ts +89 -0
- package/src/adapters/slack.ts +9 -0
- package/src/adapters/whatsapp-media.ts +337 -0
- package/src/adapters/whatsapp.ts +629 -0
- package/src/agent/api-socket.ts +127 -0
- package/src/agent/container-entry.ts +967 -0
- package/src/agent/container-error.ts +49 -0
- package/src/agent/container-runner.ts +1272 -0
- package/src/agent/model-capabilities-core.ts +23 -0
- package/src/agent/model-capabilities.ts +231 -0
- package/src/agent/pi-failure-class.ts +83 -0
- package/src/agent/pi-jsonl-parser.ts +306 -0
- package/src/agent/preferences-prompt.ts +20 -0
- package/src/agent/user-error-messages.ts +78 -0
- package/src/bridges/discord.ts +171 -0
- package/src/bridges/slack.ts +177 -0
- package/src/bridges/teams.ts +160 -0
- package/src/bridges/telegram.ts +571 -0
- package/src/bridges/whatsapp.ts +290 -0
- package/src/chat-shim.ts +259 -0
- package/src/cli/mercury.ts +2508 -0
- package/src/cli/mrctl-http.ts +27 -0
- package/src/cli/mrctl.ts +611 -0
- package/src/cli/whatsapp-auth.ts +260 -0
- package/src/config-file.ts +397 -0
- package/src/config-model-chain.ts +30 -0
- package/src/config.ts +316 -0
- package/src/core/api-types.ts +58 -0
- package/src/core/api.ts +105 -0
- package/src/core/commands.ts +76 -0
- package/src/core/conversation.ts +47 -0
- package/src/core/handler.ts +206 -0
- package/src/core/media.ts +200 -0
- package/src/core/mute-duration.ts +22 -0
- package/src/core/outbox.ts +76 -0
- package/src/core/permissions.ts +192 -0
- package/src/core/profiles.ts +245 -0
- package/src/core/rate-limiter.ts +127 -0
- package/src/core/router.ts +191 -0
- package/src/core/routes/chat.ts +172 -0
- package/src/core/routes/config-builtin.ts +107 -0
- package/src/core/routes/config.ts +81 -0
- package/src/core/routes/connections.ts +190 -0
- package/src/core/routes/console.ts +668 -0
- package/src/core/routes/control.ts +46 -0
- package/src/core/routes/conversations.ts +66 -0
- package/src/core/routes/dashboard.ts +2491 -0
- package/src/core/routes/extensions.ts +37 -0
- package/src/core/routes/index.ts +14 -0
- package/src/core/routes/media.ts +72 -0
- package/src/core/routes/messages.ts +37 -0
- package/src/core/routes/mutes.ts +89 -0
- package/src/core/routes/prefs.ts +95 -0
- package/src/core/routes/roles.ts +125 -0
- package/src/core/routes/spaces.ts +60 -0
- package/src/core/routes/storage.ts +126 -0
- package/src/core/routes/tasks.ts +189 -0
- package/src/core/routes/tradestation.ts +268 -0
- package/src/core/routes/tts.ts +51 -0
- package/src/core/runtime.ts +1140 -0
- package/src/core/space-queue.ts +103 -0
- package/src/core/storage-cleanup.ts +140 -0
- package/src/core/storage-guard.ts +24 -0
- package/src/core/task-scheduler.ts +132 -0
- package/src/core/telegram-format.ts +178 -0
- package/src/core/trigger.ts +142 -0
- package/src/dashboard/index.html +729 -0
- package/src/dashboard/tokens.css +53 -0
- package/src/extensions/api.ts +252 -0
- package/src/extensions/catalog.ts +117 -0
- package/src/extensions/config-registry.ts +83 -0
- package/src/extensions/context.ts +36 -0
- package/src/extensions/hooks.ts +156 -0
- package/src/extensions/image-builder.ts +617 -0
- package/src/extensions/installer.ts +306 -0
- package/src/extensions/jobs.ts +122 -0
- package/src/extensions/loader.ts +271 -0
- package/src/extensions/permission-guard.ts +52 -0
- package/src/extensions/reserved.ts +28 -0
- package/src/extensions/skills.ts +123 -0
- package/src/extensions/types.ts +462 -0
- package/src/logger.ts +174 -0
- package/src/main.ts +586 -0
- package/src/server.ts +391 -0
- package/src/storage/db.ts +1624 -0
- package/src/storage/memory.ts +45 -0
- package/src/storage/pi-auth.ts +95 -0
- package/src/text/markdown.ts +117 -0
- package/src/text/rtl.ts +38 -0
- package/src/tradestation/host-api.ts +77 -0
- package/src/tradestation/pending-orders.ts +69 -0
- package/src/tts/azure.ts +52 -0
- package/src/tts/google.ts +128 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/language.ts +20 -0
- package/src/tts/synthesize.ts +133 -0
- package/src/types.ts +295 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal model capability types + defaults (no yaml/zod).
|
|
3
|
+
* Used by container-entry; the full agent image only copies this file, not model-capabilities.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ModelCapabilities = {
|
|
7
|
+
tools: boolean;
|
|
8
|
+
vision: boolean;
|
|
9
|
+
audio_input: boolean;
|
|
10
|
+
audio_output: boolean;
|
|
11
|
+
extended_thinking: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ModelCapabilityKey = keyof ModelCapabilities;
|
|
15
|
+
|
|
16
|
+
/** Fallback when no builtin / YAML / env match. */
|
|
17
|
+
export const DEFAULT_CAPABILITIES: ModelCapabilities = {
|
|
18
|
+
tools: true,
|
|
19
|
+
vision: false,
|
|
20
|
+
audio_input: false,
|
|
21
|
+
audio_output: false,
|
|
22
|
+
extended_thinking: false,
|
|
23
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model capability flags — used to adapt prompts, pi tool flags, and skill installation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { getModels, type KnownProvider } from "@mariozechner/pi-ai";
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import type { ModelLeg } from "../config.js";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_CAPABILITIES,
|
|
13
|
+
type ModelCapabilities,
|
|
14
|
+
type ModelCapabilityKey,
|
|
15
|
+
} from "./model-capabilities-core.js";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
ModelCapabilities,
|
|
19
|
+
ModelCapabilityKey,
|
|
20
|
+
} from "./model-capabilities-core.js";
|
|
21
|
+
export { DEFAULT_CAPABILITIES } from "./model-capabilities-core.js";
|
|
22
|
+
|
|
23
|
+
export type CapabilityResolveSource = "env" | "yaml" | "builtin" | "default";
|
|
24
|
+
|
|
25
|
+
export type ResolvedModelCapabilities = {
|
|
26
|
+
capabilities: ModelCapabilities;
|
|
27
|
+
source: CapabilityResolveSource;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function mergePartialCapabilities(
|
|
31
|
+
partial: Partial<Record<ModelCapabilityKey, unknown>>,
|
|
32
|
+
): ModelCapabilities {
|
|
33
|
+
const out = { ...DEFAULT_CAPABILITIES };
|
|
34
|
+
for (const k of Object.keys(partial) as ModelCapabilityKey[]) {
|
|
35
|
+
const v = partial[k];
|
|
36
|
+
if (typeof v === "boolean") out[k] = v;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const partialCapsSchema = z.object({
|
|
42
|
+
tools: z.boolean().optional(),
|
|
43
|
+
vision: z.boolean().optional(),
|
|
44
|
+
audio_input: z.boolean().optional(),
|
|
45
|
+
audio_output: z.boolean().optional(),
|
|
46
|
+
extended_thinking: z.boolean().optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const yamlFileSchema = z.object({
|
|
50
|
+
models: z.record(z.string(), partialCapsSchema),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type UserModelCapabilitiesMap = Map<string, ModelCapabilities>;
|
|
54
|
+
|
|
55
|
+
/** Path to optional user overrides: `<dataDir>/model-capabilities.yaml` */
|
|
56
|
+
export function modelCapabilitiesYamlPath(dataDirAbsolute: string): string {
|
|
57
|
+
return path.join(dataDirAbsolute, "model-capabilities.yaml");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load `.mercury/model-capabilities.yaml` (under resolved data dir).
|
|
62
|
+
* Returns null if missing or invalid (callers may log).
|
|
63
|
+
*/
|
|
64
|
+
export function loadUserModelCapabilitiesMap(
|
|
65
|
+
dataDirAbsolute: string,
|
|
66
|
+
): UserModelCapabilitiesMap | null {
|
|
67
|
+
const yamlPath = modelCapabilitiesYamlPath(dataDirAbsolute);
|
|
68
|
+
if (!existsSync(yamlPath)) return null;
|
|
69
|
+
try {
|
|
70
|
+
const raw = readFileSync(yamlPath, "utf8");
|
|
71
|
+
const doc = parseYaml(raw) as unknown;
|
|
72
|
+
const parsed = yamlFileSchema.safeParse(doc);
|
|
73
|
+
if (!parsed.success) return null;
|
|
74
|
+
const map = new Map<string, ModelCapabilities>();
|
|
75
|
+
for (const [modelId, partial] of Object.entries(parsed.data.models)) {
|
|
76
|
+
map.set(modelId.trim(), mergePartialCapabilities(partial));
|
|
77
|
+
}
|
|
78
|
+
return map;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Look up capabilities for a model using pi's MODELS registry as the source of truth.
|
|
86
|
+
* Derives: vision (image in input), audio_input (audio in input), extended_thinking (reasoning).
|
|
87
|
+
* tools defaults to true — pi has no tools field; all pi-known models support tool use.
|
|
88
|
+
* audio_output is not tracked by pi; always false.
|
|
89
|
+
*/
|
|
90
|
+
function matchBuiltinCapabilities(
|
|
91
|
+
provider: string,
|
|
92
|
+
modelId: string,
|
|
93
|
+
): ModelCapabilities | null {
|
|
94
|
+
// Cast to KnownProvider — getModels returns [] for unrecognised providers at runtime
|
|
95
|
+
const model = getModels(provider as KnownProvider).find(
|
|
96
|
+
(m) => m.id === modelId,
|
|
97
|
+
);
|
|
98
|
+
if (!model) return null;
|
|
99
|
+
return {
|
|
100
|
+
tools: true,
|
|
101
|
+
vision: model.input.includes("image"),
|
|
102
|
+
audio_input: false, // pi types input as ("text"|"image")[]; no audio models exist yet
|
|
103
|
+
audio_output: false,
|
|
104
|
+
extended_thinking: model.reasoning,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Parse MERCURY_MODEL_CAPABILITIES JSON; returns null if unset or invalid. */
|
|
109
|
+
export function parseModelCapabilitiesEnv(
|
|
110
|
+
raw: string | undefined,
|
|
111
|
+
): ModelCapabilities | null {
|
|
112
|
+
const trimmed = raw?.trim();
|
|
113
|
+
if (!trimmed) return null;
|
|
114
|
+
try {
|
|
115
|
+
const json = JSON.parse(trimmed) as unknown;
|
|
116
|
+
const r = partialCapsSchema.safeParse(json);
|
|
117
|
+
if (!r.success) return null;
|
|
118
|
+
return mergePartialCapabilities(r.data);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve capabilities for one model id (single leg).
|
|
126
|
+
* Priority: env global override → YAML exact model key → pi MODELS lookup → default.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveModelCapabilitiesWithSource(
|
|
129
|
+
modelId: string,
|
|
130
|
+
provider: string,
|
|
131
|
+
userMap: UserModelCapabilitiesMap | null,
|
|
132
|
+
envCaps: ModelCapabilities | null,
|
|
133
|
+
): ResolvedModelCapabilities {
|
|
134
|
+
if (envCaps) {
|
|
135
|
+
return { capabilities: envCaps, source: "env" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const exact = userMap?.get(modelId.trim());
|
|
139
|
+
if (exact) {
|
|
140
|
+
return { capabilities: exact, source: "yaml" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const builtin = matchBuiltinCapabilities(provider, modelId);
|
|
144
|
+
if (builtin) {
|
|
145
|
+
return { capabilities: builtin, source: "builtin" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { capabilities: { ...DEFAULT_CAPABILITIES }, source: "default" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function resolveModelCapabilities(
|
|
152
|
+
modelId: string,
|
|
153
|
+
provider: string,
|
|
154
|
+
userMap: UserModelCapabilitiesMap | null,
|
|
155
|
+
envCaps: ModelCapabilities | null,
|
|
156
|
+
): ModelCapabilities {
|
|
157
|
+
return resolveModelCapabilitiesWithSource(modelId, provider, userMap, envCaps)
|
|
158
|
+
.capabilities;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Capabilities for each leg in the model chain (same length as `chain`). */
|
|
162
|
+
export function resolveModelChainCapabilities(
|
|
163
|
+
chain: ModelLeg[],
|
|
164
|
+
dataDirAbsolute: string,
|
|
165
|
+
envCaps: ModelCapabilities | null,
|
|
166
|
+
): {
|
|
167
|
+
chainCaps: ModelCapabilities[];
|
|
168
|
+
userMap: UserModelCapabilitiesMap | null;
|
|
169
|
+
} {
|
|
170
|
+
const userMap = loadUserModelCapabilitiesMap(dataDirAbsolute);
|
|
171
|
+
const chainCaps = chain.map((leg) =>
|
|
172
|
+
resolveModelCapabilities(leg.model, leg.provider, userMap, envCaps),
|
|
173
|
+
);
|
|
174
|
+
return { chainCaps, userMap };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function chainSupportsRequirements(
|
|
178
|
+
requires: ModelCapabilityKey[],
|
|
179
|
+
chainCaps: ModelCapabilities[],
|
|
180
|
+
): boolean {
|
|
181
|
+
if (requires.length === 0) return true;
|
|
182
|
+
return chainCaps.some((caps) => requires.every((key) => caps[key] === true));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Log warnings for models that fell back to defaults (once per distinct model id). */
|
|
186
|
+
export function logUnknownModelCapabilityWarnings(
|
|
187
|
+
chain: ModelLeg[],
|
|
188
|
+
dataDirAbsolute: string,
|
|
189
|
+
envCaps: ModelCapabilities | null,
|
|
190
|
+
log: { warn: (msg: string, obj?: Record<string, unknown>) => void },
|
|
191
|
+
): void {
|
|
192
|
+
if (envCaps) return;
|
|
193
|
+
const userMap = loadUserModelCapabilitiesMap(dataDirAbsolute);
|
|
194
|
+
const seen = new Set<string>();
|
|
195
|
+
|
|
196
|
+
for (const leg of chain) {
|
|
197
|
+
const id = leg.model.trim();
|
|
198
|
+
if (seen.has(id)) continue;
|
|
199
|
+
seen.add(id);
|
|
200
|
+
|
|
201
|
+
const { source } = resolveModelCapabilitiesWithSource(
|
|
202
|
+
id,
|
|
203
|
+
leg.provider,
|
|
204
|
+
userMap,
|
|
205
|
+
null,
|
|
206
|
+
);
|
|
207
|
+
if (source === "default") {
|
|
208
|
+
log.warn(
|
|
209
|
+
`Model "${id}" not in built-in capability map and not in model-capabilities.yaml; assuming default capabilities (tools=true, vision=false). Set MERCURY_MODEL_CAPABILITIES or add .mercury/model-capabilities.yaml to override.`,
|
|
210
|
+
{ model: id },
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function logExtensionCapabilityMismatches(
|
|
217
|
+
extensions: Array<{ name: string; requires?: ModelCapabilityKey[] }>,
|
|
218
|
+
chainCaps: ModelCapabilities[],
|
|
219
|
+
log: { warn: (msg: string, obj?: Record<string, unknown>) => void },
|
|
220
|
+
): void {
|
|
221
|
+
for (const ext of extensions) {
|
|
222
|
+
const req = ext.requires;
|
|
223
|
+
if (!req?.length) continue;
|
|
224
|
+
if (!chainSupportsRequirements(req, chainCaps)) {
|
|
225
|
+
log.warn(
|
|
226
|
+
`Extension "${ext.name}" requires ${req.join(", ")} but no model leg in MERCURY_MODEL_CHAIN supports it; extension skills will not be installed.`,
|
|
227
|
+
{ extension: ext.name, requires: req },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify pi CLI failures for retry vs next-model vs fail-fast.
|
|
3
|
+
* Uses stderr/stdout text heuristics (pi does not expose structured HTTP codes here).
|
|
4
|
+
*/
|
|
5
|
+
export type PiFailureClass = "failFast" | "retryable" | "fallbackable";
|
|
6
|
+
|
|
7
|
+
export function classifyPiFailure(text: string): PiFailureClass {
|
|
8
|
+
if (/provider ["']cursor["'] is no longer supported/i.test(text)) {
|
|
9
|
+
return "failFast";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
/\b401\b|\b403\b|invalid\s+api\s+key|incorrect\s+api\s+key|authentication\s+failed|invalid\s+authentication|unauthorized|access\s+denied/i.test(
|
|
14
|
+
text,
|
|
15
|
+
)
|
|
16
|
+
) {
|
|
17
|
+
return "failFast";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
/context\s+length|maximum\s+context|token\s+limit|too\s+many\s+tokens|prompt\s+is\s+too\s+long|max\s+tokens|request\s+too\s+large|maximum\s+tokens/i.test(
|
|
22
|
+
text,
|
|
23
|
+
)
|
|
24
|
+
) {
|
|
25
|
+
return "fallbackable";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
/tool\s+(use|calling)\s+not\s+supported|tools?\s+not\s+supported|function\s+calling\s+not\s+(supported|available)|does\s+not\s+support\s+tools?|model\s+does\s+not\s+support\s+(tools?|function)|no\s+tool\s+use|unsupported.*\btools?\b/i.test(
|
|
30
|
+
text,
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
return "fallbackable";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
/tool\s+call\s+validation\s+failed|attempted\s+to\s+call\s+tool.*which\s+was\s+not\s+in\s+request\.tools/i.test(
|
|
38
|
+
text,
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
return "fallbackable";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The model/provider returned a response the runtime couldn't turn into usable
|
|
45
|
+
// output. Two forms:
|
|
46
|
+
// 1. An unmapped finish reason — pi-ai's exhaustive switch throws
|
|
47
|
+
// "Unhandled stop reason: <X>" (e.g. Gemini's MALFORMED_RESPONSE, which is
|
|
48
|
+
// absent from the bundled @google/genai enum).
|
|
49
|
+
// 2. A raw malformed finish-reason enum token surfaced directly.
|
|
50
|
+
// An identical retry against the same leg reproduces it and just burns the chain
|
|
51
|
+
// budget, so fall through to the next model leg instead. The enum arm is
|
|
52
|
+
// case-sensitive on the uppercase token so prose like "malformed response" in an
|
|
53
|
+
// unrelated message can't trip it.
|
|
54
|
+
if (
|
|
55
|
+
/unhandled\s+stop\s+reason/i.test(text) ||
|
|
56
|
+
/\bMALFORMED_(RESPONSE|FUNCTION_CALL|TOOL_CALL)\b/.test(text)
|
|
57
|
+
) {
|
|
58
|
+
return "fallbackable";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (/\b429\b|rate[_\s]+limit/i.test(text)) {
|
|
62
|
+
return "fallbackable";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
/\b402\b|insufficient\s+credits?|not\s+enough\s+credits?|purchase\s+(more\s+)?credits?|no\s+credits?/i.test(
|
|
67
|
+
text,
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
return "fallbackable";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
/\b502\b|\b503\b|\b504\b|timeout|timed\s+out|ETIMEDOUT|ECONNRESET|temporarily\s+unavailable|overload|try\s+again|service\s+unavailable|bad\s+gateway|gateway\s+timeout/i.test(
|
|
75
|
+
text,
|
|
76
|
+
)
|
|
77
|
+
) {
|
|
78
|
+
return "retryable";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default: assume transient; bounded retries + chain limit prevent infinite spin.
|
|
82
|
+
return "retryable";
|
|
83
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { TokenUsage } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export interface PiJsonlParseResult {
|
|
4
|
+
reply: string;
|
|
5
|
+
usage?: TokenUsage;
|
|
6
|
+
/**
|
|
7
|
+
* When set, the Pi run failed at the model layer (e.g. HTTP 429 inside JSONL)
|
|
8
|
+
* while `pi` still exited 0. Host should treat this as a failed invocation
|
|
9
|
+
* (retry / next model leg), not as a user-visible reply string.
|
|
10
|
+
*/
|
|
11
|
+
piFailureMessage?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Set when the model emitted a tool call as raw text (JSON or XML format) instead
|
|
14
|
+
* of a proper tool_use block that pi can execute. The leaked call was stripped from
|
|
15
|
+
* the reply. Caller may retry with tools disabled to get a clean text-only response.
|
|
16
|
+
*/
|
|
17
|
+
hadToolLeakage?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const USAGE_EVENTS = new Set(["turn_end", "message_end"]);
|
|
21
|
+
|
|
22
|
+
/** Pi `--print --mode json` lines — if we see these, never fall back to raw stdout. */
|
|
23
|
+
const PI_STRUCTURED_EVENT_TYPES = new Set([
|
|
24
|
+
"session",
|
|
25
|
+
"agent_start",
|
|
26
|
+
"agent_end",
|
|
27
|
+
"turn_start",
|
|
28
|
+
"turn_end",
|
|
29
|
+
"message_start",
|
|
30
|
+
"message_end",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function stdoutLooksLikeStructuredPiJsonl(stdout: string): boolean {
|
|
34
|
+
for (const line of stdout.trim().split("\n")) {
|
|
35
|
+
if (!line.trim()) continue;
|
|
36
|
+
let event: Record<string, unknown>;
|
|
37
|
+
try {
|
|
38
|
+
event = JSON.parse(line) as Record<string, unknown>;
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const t = event.type;
|
|
43
|
+
if (typeof t === "string" && PI_STRUCTURED_EVENT_TYPES.has(t)) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Model/API failure embedded in an assistant JSONL message (pi still exits 0). */
|
|
49
|
+
function extractAssistantFailure(
|
|
50
|
+
msg: Record<string, unknown>,
|
|
51
|
+
): string | undefined {
|
|
52
|
+
const stop = msg.stopReason;
|
|
53
|
+
const errRaw = msg.errorMessage;
|
|
54
|
+
|
|
55
|
+
if (stop === "error" || stop === "aborted") {
|
|
56
|
+
if (typeof errRaw === "string" && errRaw.trim()) return errRaw.trim();
|
|
57
|
+
return `stopReason: ${String(stop)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof errRaw === "string" && errRaw.trim()) return errRaw.trim();
|
|
61
|
+
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function usageRecordHasSignal(u: Record<string, unknown>): boolean {
|
|
66
|
+
const input =
|
|
67
|
+
Number(u.input ?? u.input_tokens ?? u.promptTokens ?? u.prompt_tokens) || 0;
|
|
68
|
+
const output =
|
|
69
|
+
Number(
|
|
70
|
+
u.output ?? u.output_tokens ?? u.completionTokens ?? u.completion_tokens,
|
|
71
|
+
) || 0;
|
|
72
|
+
const total = Number(u.totalTokens ?? u.total_tokens) || 0;
|
|
73
|
+
const cacheRead = Number(u.cacheRead ?? u.cache_read) || 0;
|
|
74
|
+
const cacheWrite = Number(u.cacheWrite ?? u.cache_write) || 0;
|
|
75
|
+
const cost = u.cost as Record<string, number> | undefined;
|
|
76
|
+
const costTotal = cost?.total ?? 0;
|
|
77
|
+
return (
|
|
78
|
+
input > 0 ||
|
|
79
|
+
output > 0 ||
|
|
80
|
+
total > 0 ||
|
|
81
|
+
cacheRead > 0 ||
|
|
82
|
+
cacheWrite > 0 ||
|
|
83
|
+
costTotal > 0
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Some providers/models put tool-style content in the assistant *text* channel
|
|
89
|
+
* instead of structured tool calls. Two known formats:
|
|
90
|
+
* 1. JSON blob: `bash{"command":"mrctl …"}`
|
|
91
|
+
* 2. XML format: `<function name="bash" parameters="{…}" />`
|
|
92
|
+
* Strip these so raw tool calls don't leak into Telegram/WhatsApp.
|
|
93
|
+
* Returns any meaningful text that preceded the blob, or empty string.
|
|
94
|
+
*/
|
|
95
|
+
export function sanitizeLeakedToolCallText(text: string): string {
|
|
96
|
+
const raw = text.trim();
|
|
97
|
+
if (!raw) return raw;
|
|
98
|
+
|
|
99
|
+
// XML format: <function name="bash" ...> or <function name="bash" .../>
|
|
100
|
+
const xmlFuncMatch = raw.match(/^([\s\S]*?)<function\s+name=["']bash["']/i);
|
|
101
|
+
if (xmlFuncMatch) {
|
|
102
|
+
return xmlFuncMatch[1].trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// JSON format: bash{"command":...}
|
|
106
|
+
const jsonStart = raw.indexOf('{"command"');
|
|
107
|
+
if (jsonStart < 0) return raw;
|
|
108
|
+
|
|
109
|
+
const prefix = raw.slice(0, jsonStart).trim();
|
|
110
|
+
|
|
111
|
+
if (/^(bash|sh)/i.test(prefix) || !prefix) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return prefix;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addUsageTotals(
|
|
119
|
+
u: Record<string, unknown>,
|
|
120
|
+
into: {
|
|
121
|
+
inputSum: number;
|
|
122
|
+
outputSum: number;
|
|
123
|
+
totalOnlySum: number;
|
|
124
|
+
cacheReadSum: number;
|
|
125
|
+
cacheWriteSum: number;
|
|
126
|
+
costSum: number;
|
|
127
|
+
},
|
|
128
|
+
): void {
|
|
129
|
+
const input =
|
|
130
|
+
Number(u.input ?? u.input_tokens ?? u.promptTokens ?? u.prompt_tokens) || 0;
|
|
131
|
+
const output =
|
|
132
|
+
Number(
|
|
133
|
+
u.output ?? u.output_tokens ?? u.completionTokens ?? u.completion_tokens,
|
|
134
|
+
) || 0;
|
|
135
|
+
const total = Number(u.totalTokens ?? u.total_tokens) || 0;
|
|
136
|
+
into.inputSum += input;
|
|
137
|
+
into.outputSum += output;
|
|
138
|
+
if (input === 0 && output === 0 && total > 0) into.totalOnlySum += total;
|
|
139
|
+
into.cacheReadSum += Number(u.cacheRead ?? u.cache_read) || 0;
|
|
140
|
+
into.cacheWriteSum += Number(u.cacheWrite ?? u.cache_write) || 0;
|
|
141
|
+
const cost = u.cost as Record<string, number> | undefined;
|
|
142
|
+
into.costSum += cost?.total ?? 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse pi `--print --mode json` stdout: JSONL events where assistant text and
|
|
147
|
+
* usage appear on `message_end` and/or `turn_end` (pi-agent-core emits both;
|
|
148
|
+
* usage is often only on `message_end`). Sums usage across matching events in
|
|
149
|
+
* one process. If both event types carry usage, only `message_end` is summed to
|
|
150
|
+
* avoid double-counting the same turn.
|
|
151
|
+
*/
|
|
152
|
+
export function parsePiPrintJsonlOutput(stdout: string): PiJsonlParseResult {
|
|
153
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
154
|
+
|
|
155
|
+
let usageSource: "message_end" | "turn_end" | null = null;
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
let event: Record<string, unknown>;
|
|
158
|
+
try {
|
|
159
|
+
event = JSON.parse(line) as Record<string, unknown>;
|
|
160
|
+
} catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (event.type !== "message_end") continue;
|
|
164
|
+
const msg = event.message as Record<string, unknown> | undefined;
|
|
165
|
+
if (msg?.role !== "assistant") continue;
|
|
166
|
+
const u = msg.usage as Record<string, unknown> | undefined;
|
|
167
|
+
if (u && usageRecordHasSignal(u)) {
|
|
168
|
+
usageSource = "message_end";
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!usageSource) {
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
let event: Record<string, unknown>;
|
|
175
|
+
try {
|
|
176
|
+
event = JSON.parse(line) as Record<string, unknown>;
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (event.type !== "turn_end") continue;
|
|
181
|
+
const msg = event.message as Record<string, unknown> | undefined;
|
|
182
|
+
if (msg?.role !== "assistant") continue;
|
|
183
|
+
const u = msg.usage as Record<string, unknown> | undefined;
|
|
184
|
+
if (u && usageRecordHasSignal(u)) {
|
|
185
|
+
usageSource = "turn_end";
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let reply = "";
|
|
192
|
+
const sums = {
|
|
193
|
+
inputSum: 0,
|
|
194
|
+
outputSum: 0,
|
|
195
|
+
totalOnlySum: 0,
|
|
196
|
+
cacheReadSum: 0,
|
|
197
|
+
cacheWriteSum: 0,
|
|
198
|
+
costSum: 0,
|
|
199
|
+
};
|
|
200
|
+
let structuredTurns = 0;
|
|
201
|
+
let lastModel: string | undefined;
|
|
202
|
+
let lastProvider: string | undefined;
|
|
203
|
+
let lastAssistantFailure: string | undefined;
|
|
204
|
+
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
let event: Record<string, unknown>;
|
|
207
|
+
try {
|
|
208
|
+
event = JSON.parse(line) as Record<string, unknown>;
|
|
209
|
+
} catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const et = event.type;
|
|
214
|
+
if (!USAGE_EVENTS.has(et as string)) continue;
|
|
215
|
+
|
|
216
|
+
const msg = event.message as Record<string, unknown> | undefined;
|
|
217
|
+
if (msg?.role !== "assistant") continue;
|
|
218
|
+
|
|
219
|
+
structuredTurns += 1;
|
|
220
|
+
|
|
221
|
+
const content = msg.content as
|
|
222
|
+
| Array<{ type: string; text?: string }>
|
|
223
|
+
| undefined;
|
|
224
|
+
const textFromContent = content
|
|
225
|
+
? content
|
|
226
|
+
.filter((c) => c.type === "text" && c.text)
|
|
227
|
+
.map((c) => c.text)
|
|
228
|
+
.join("\n")
|
|
229
|
+
: "";
|
|
230
|
+
|
|
231
|
+
if (textFromContent) {
|
|
232
|
+
reply = textFromContent;
|
|
233
|
+
lastAssistantFailure = undefined;
|
|
234
|
+
} else {
|
|
235
|
+
const fd = extractAssistantFailure(msg);
|
|
236
|
+
if (fd) lastAssistantFailure = fd;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (usageSource && et === usageSource) {
|
|
240
|
+
const u = msg.usage as Record<string, unknown> | undefined;
|
|
241
|
+
if (u) addUsageTotals(u, sums);
|
|
242
|
+
}
|
|
243
|
+
if (typeof msg.model === "string" && msg.model) lastModel = msg.model;
|
|
244
|
+
if (typeof msg.provider === "string" && msg.provider)
|
|
245
|
+
lastProvider = msg.provider;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const structuredStream =
|
|
249
|
+
structuredTurns > 0 || stdoutLooksLikeStructuredPiJsonl(stdout);
|
|
250
|
+
let piFailureMessage: string | undefined;
|
|
251
|
+
if (lastAssistantFailure && !reply.trim()) {
|
|
252
|
+
piFailureMessage = lastAssistantFailure;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!reply.trim() && !piFailureMessage) {
|
|
256
|
+
if (structuredStream) {
|
|
257
|
+
reply = "Done.";
|
|
258
|
+
} else {
|
|
259
|
+
reply = stdout.trim() || "Done.";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (piFailureMessage) {
|
|
264
|
+
return { reply: "", piFailureMessage };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const replyBeforeSanitize = reply.trim();
|
|
268
|
+
reply = sanitizeLeakedToolCallText(reply);
|
|
269
|
+
const hadToolLeakage = reply !== replyBeforeSanitize;
|
|
270
|
+
if (!reply.trim()) {
|
|
271
|
+
reply = "Done.";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const hasUsage =
|
|
275
|
+
sums.inputSum > 0 ||
|
|
276
|
+
sums.outputSum > 0 ||
|
|
277
|
+
sums.totalOnlySum > 0 ||
|
|
278
|
+
sums.cacheReadSum > 0 ||
|
|
279
|
+
sums.cacheWriteSum > 0 ||
|
|
280
|
+
sums.costSum > 0;
|
|
281
|
+
|
|
282
|
+
let usage: TokenUsage | undefined;
|
|
283
|
+
if (structuredTurns > 0 && hasUsage) {
|
|
284
|
+
const totalFromIO = sums.inputSum + sums.outputSum;
|
|
285
|
+
const totalTokens =
|
|
286
|
+
totalFromIO > 0
|
|
287
|
+
? totalFromIO
|
|
288
|
+
: sums.totalOnlySum > 0
|
|
289
|
+
? sums.totalOnlySum
|
|
290
|
+
: 0;
|
|
291
|
+
usage = {
|
|
292
|
+
inputTokens: sums.inputSum,
|
|
293
|
+
outputTokens: sums.outputSum,
|
|
294
|
+
...(totalTokens > 0 ? { totalTokens } : {}),
|
|
295
|
+
...(sums.cacheReadSum > 0 ? { cacheReadTokens: sums.cacheReadSum } : {}),
|
|
296
|
+
...(sums.cacheWriteSum > 0
|
|
297
|
+
? { cacheWriteTokens: sums.cacheWriteSum }
|
|
298
|
+
: {}),
|
|
299
|
+
...(sums.costSum > 0 ? { cost: sums.costSum } : {}),
|
|
300
|
+
...(lastModel ? { model: lastModel } : {}),
|
|
301
|
+
...(lastProvider ? { provider: lastProvider } : {}),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { reply, usage, ...(hadToolLeakage ? { hadToolLeakage: true } : {}) };
|
|
306
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Escape text for XML element bodies. */
|
|
2
|
+
export function escapeXmlText(s: string): string {
|
|
3
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function escapeXmlAttr(s: string): string {
|
|
7
|
+
return escapeXmlText(s).replace(/"/g, """);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Build `<preferences>...</preferences>` block for the agent user prompt, or null if empty. */
|
|
11
|
+
export function formatPreferencesXml(
|
|
12
|
+
preferences?: Array<{ key: string; value: string }>,
|
|
13
|
+
): string | null {
|
|
14
|
+
if (!preferences?.length) return null;
|
|
15
|
+
const lines = preferences.map(
|
|
16
|
+
(p) =>
|
|
17
|
+
` <pref key="${escapeXmlAttr(p.key)}">${escapeXmlText(p.value)}</pref>`,
|
|
18
|
+
);
|
|
19
|
+
return ["<preferences>", ...lines, "</preferences>"].join("\n");
|
|
20
|
+
}
|