@poolzin/pool-bot 2026.2.23 → 2026.2.25
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/CHANGELOG.md +29 -0
- package/dist/acp/client.js +207 -18
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-scope.js +10 -0
- package/dist/agents/bash-process-registry.test-helpers.js +29 -0
- package/dist/agents/bash-tools.exec-approval-request.js +20 -0
- package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/models-config.providers.js +299 -182
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.js +15 -18
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/browser/config.js +26 -0
- package/dist/browser/navigation-guard.js +31 -0
- package/dist/browser/routes/agent.act.js +431 -424
- package/dist/browser/routes/agent.shared.js +47 -3
- package/dist/browser/routes/agent.snapshot.js +122 -116
- package/dist/browser/routes/agent.storage.js +303 -297
- package/dist/browser/routes/tabs.js +154 -100
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +25 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +2 -1
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +21 -0
- package/dist/cli/plugins-cli.js +245 -61
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +223 -136
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/route.js +1 -1
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/commands/agent.js +163 -87
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/onboard-helpers.js +4 -4
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/compat/legacy-names.js +2 -2
- package/dist/config/commands.js +3 -0
- package/dist/config/config.js +1 -1
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +9 -0
- package/dist/config/io.js +571 -171
- package/dist/config/merge-patch.js +50 -4
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.js +58 -570
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +8 -7
- package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
- package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
- package/dist/control-ui/index.html +1 -1
- package/dist/daemon/cmd-argv.js +21 -0
- package/dist/daemon/cmd-set.js +58 -0
- package/dist/daemon/service-types.js +1 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/gateway/auth.js +38 -3
- package/dist/gateway/call.js +149 -68
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +23 -0
- package/dist/gateway/openresponses-http.js +120 -110
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +3 -2
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-http.js +236 -52
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +461 -130
- package/dist/gateway/server-methods/config.js +193 -150
- package/dist/gateway/server-methods/nodes.helpers.js +12 -0
- package/dist/gateway/server-methods/nodes.js +251 -69
- package/dist/gateway/server-methods/push.js +53 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +5 -0
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-ws-runtime.js +1 -0
- package/dist/gateway/server.impl.js +296 -139
- package/dist/gateway/session-preview.test-helpers.js +11 -0
- package/dist/gateway/startup-auth.js +126 -0
- package/dist/gateway/test-helpers.agent-results.js +15 -0
- package/dist/gateway/test-helpers.mocks.js +37 -14
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/hooks/bundled/session-memory/handler.js +165 -34
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/device-pairing.js +148 -167
- package/dist/infra/exec-approvals-allowlist.js +19 -70
- package/dist/infra/exec-approvals-analysis.js +44 -17
- package/dist/infra/exec-safe-bin-policy.js +269 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/git-root.js +61 -0
- package/dist/infra/heartbeat-active-hours.js +2 -2
- package/dist/infra/heartbeat-reason.js +40 -0
- package/dist/infra/heartbeat-runner.js +72 -32
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/node-pairing.js +50 -105
- package/dist/infra/npm-integrity.js +45 -0
- package/dist/infra/npm-pack-install.js +40 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/message-action-runner.js +107 -327
- package/dist/infra/outbound/message.js +59 -36
- package/dist/infra/outbound/outbound-policy.js +52 -25
- package/dist/infra/outbound/outbound-send-service.js +58 -71
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/push-apns.js +365 -0
- package/dist/infra/restart-sentinel.js +16 -1
- package/dist/infra/restart.js +229 -26
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/update-startup.js +86 -9
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/memory/test-manager.js +8 -0
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/loader.js +192 -26
- package/dist/plugins/logger.js +8 -0
- package/dist/plugins/manifest-registry.js +3 -0
- package/dist/plugins/path-safety.js +34 -0
- package/dist/plugins/registry.js +5 -2
- package/dist/plugins/runtime/index.js +271 -206
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/security/audit-channel.js +8 -19
- package/dist/security/audit-extra.async.js +354 -182
- package/dist/security/audit-extra.js +11 -1
- package/dist/security/audit-extra.sync.js +340 -33
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +145 -371
- package/dist/security/dm-policy-shared.js +24 -0
- package/dist/security/external-content.js +20 -8
- package/dist/security/fix.js +49 -85
- package/dist/security/scan-paths.js +20 -0
- package/dist/security/secret-equal.js +3 -7
- package/dist/security/windows-acl.js +30 -15
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/operator-scope-compat.js +37 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/telegram/bot-message-dispatch.js +366 -164
- package/dist/telegram/draft-stream.js +30 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/test-utils/command-runner.js +6 -0
- package/dist/test-utils/internal-hook-event-payload.js +10 -0
- package/dist/test-utils/model-auth-mock.js +12 -0
- package/dist/test-utils/provider-usage-fetch.js +14 -0
- package/dist/test-utils/temp-home.js +33 -0
- package/dist/tui/components/chat-log.js +9 -0
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui.js +181 -45
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/media.js +4 -0
- package/docs/tools/slash-commands.md +5 -1
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/feishu/src/external-keys.ts +19 -0
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/lobster/src/windows-spawn.ts +193 -0
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { complete
|
|
4
|
-
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
|
|
2
|
+
import { complete } from "@mariozechner/pi-ai";
|
|
5
3
|
import { Type } from "@sinclair/typebox";
|
|
6
4
|
import { resolveUserPath } from "../../utils.js";
|
|
7
|
-
import { loadWebMedia } from "../../web/media.js";
|
|
5
|
+
import { getDefaultLocalRoots, loadWebMedia } from "../../web/media.js";
|
|
8
6
|
import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js";
|
|
9
7
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
|
10
8
|
import { minimaxUnderstandImage } from "../minimax-vlm.js";
|
|
@@ -12,13 +10,26 @@ import { getApiKeyForModel, requireApiKey, resolveEnvApiKey } from "../model-aut
|
|
|
12
10
|
import { runWithImageModelFallback } from "../model-fallback.js";
|
|
13
11
|
import { resolveConfiguredModelRef } from "../model-selection.js";
|
|
14
12
|
import { ensurePoolbotModelsJson } from "../models-config.js";
|
|
15
|
-
import {
|
|
13
|
+
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
|
|
14
|
+
import { normalizeWorkspaceDir } from "../workspace-dir.js";
|
|
16
15
|
import { coerceImageAssistantText, coerceImageModelConfig, decodeDataUrl, resolveProviderVisionModelFromConfig, } from "./image-tool.helpers.js";
|
|
17
16
|
const DEFAULT_PROMPT = "Describe the image.";
|
|
17
|
+
const ANTHROPIC_IMAGE_PRIMARY = "anthropic/claude-opus-4-6";
|
|
18
|
+
const ANTHROPIC_IMAGE_FALLBACK = "anthropic/claude-opus-4-5";
|
|
19
|
+
const DEFAULT_MAX_IMAGES = 20;
|
|
18
20
|
export const __testing = {
|
|
19
21
|
decodeDataUrl,
|
|
20
22
|
coerceImageAssistantText,
|
|
23
|
+
resolveImageToolMaxTokens,
|
|
21
24
|
};
|
|
25
|
+
function resolveImageToolMaxTokens(modelMaxTokens, requestedMaxTokens = 4096) {
|
|
26
|
+
if (typeof modelMaxTokens !== "number" ||
|
|
27
|
+
!Number.isFinite(modelMaxTokens) ||
|
|
28
|
+
modelMaxTokens <= 0) {
|
|
29
|
+
return requestedMaxTokens;
|
|
30
|
+
}
|
|
31
|
+
return Math.min(requestedMaxTokens, modelMaxTokens);
|
|
32
|
+
}
|
|
22
33
|
function resolveDefaultModelRef(cfg) {
|
|
23
34
|
if (cfg) {
|
|
24
35
|
const resolved = resolveConfiguredModelRef({
|
|
@@ -31,8 +42,9 @@ function resolveDefaultModelRef(cfg) {
|
|
|
31
42
|
return { provider: DEFAULT_PROVIDER, model: DEFAULT_MODEL };
|
|
32
43
|
}
|
|
33
44
|
function hasAuthForProvider(params) {
|
|
34
|
-
if (resolveEnvApiKey(params.provider)?.apiKey)
|
|
45
|
+
if (resolveEnvApiKey(params.provider)?.apiKey) {
|
|
35
46
|
return true;
|
|
47
|
+
}
|
|
36
48
|
const store = ensureAuthProfileStore(params.agentDir, {
|
|
37
49
|
allowKeychainPrompt: false,
|
|
38
50
|
});
|
|
@@ -67,10 +79,12 @@ export function resolveImageModelConfigForTool(params) {
|
|
|
67
79
|
const fallbacks = [];
|
|
68
80
|
const addFallback = (modelRef) => {
|
|
69
81
|
const ref = (modelRef ?? "").trim();
|
|
70
|
-
if (!ref)
|
|
82
|
+
if (!ref) {
|
|
71
83
|
return;
|
|
72
|
-
|
|
84
|
+
}
|
|
85
|
+
if (fallbacks.includes(ref)) {
|
|
73
86
|
return;
|
|
87
|
+
}
|
|
74
88
|
fallbacks.push(ref);
|
|
75
89
|
};
|
|
76
90
|
const providerVisionFromConfig = resolveProviderVisionModelFromConfig({
|
|
@@ -89,17 +103,22 @@ export function resolveImageModelConfigForTool(params) {
|
|
|
89
103
|
else if (providerOk && providerVisionFromConfig) {
|
|
90
104
|
preferred = providerVisionFromConfig;
|
|
91
105
|
}
|
|
106
|
+
else if (primary.provider === "zai" && providerOk) {
|
|
107
|
+
preferred = "zai/glm-4.6v";
|
|
108
|
+
}
|
|
92
109
|
else if (primary.provider === "openai" && openaiOk) {
|
|
93
110
|
preferred = "openai/gpt-5-mini";
|
|
94
111
|
}
|
|
95
112
|
else if (primary.provider === "anthropic" && anthropicOk) {
|
|
96
|
-
preferred =
|
|
113
|
+
preferred = ANTHROPIC_IMAGE_PRIMARY;
|
|
97
114
|
}
|
|
98
115
|
if (preferred?.trim()) {
|
|
99
|
-
if (openaiOk)
|
|
116
|
+
if (openaiOk) {
|
|
100
117
|
addFallback("openai/gpt-5-mini");
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
}
|
|
119
|
+
if (anthropicOk) {
|
|
120
|
+
addFallback(ANTHROPIC_IMAGE_FALLBACK);
|
|
121
|
+
}
|
|
103
122
|
// Don't duplicate primary in fallbacks.
|
|
104
123
|
const pruned = fallbacks.filter((ref) => ref !== preferred);
|
|
105
124
|
return {
|
|
@@ -109,15 +128,19 @@ export function resolveImageModelConfigForTool(params) {
|
|
|
109
128
|
}
|
|
110
129
|
// Cross-provider fallback when we can't pair with the primary provider.
|
|
111
130
|
if (openaiOk) {
|
|
112
|
-
if (anthropicOk)
|
|
113
|
-
addFallback(
|
|
131
|
+
if (anthropicOk) {
|
|
132
|
+
addFallback(ANTHROPIC_IMAGE_FALLBACK);
|
|
133
|
+
}
|
|
114
134
|
return {
|
|
115
135
|
primary: "openai/gpt-5-mini",
|
|
116
136
|
...(fallbacks.length ? { fallbacks } : {}),
|
|
117
137
|
};
|
|
118
138
|
}
|
|
119
139
|
if (anthropicOk) {
|
|
120
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
primary: ANTHROPIC_IMAGE_PRIMARY,
|
|
142
|
+
fallbacks: [ANTHROPIC_IMAGE_FALLBACK],
|
|
143
|
+
};
|
|
121
144
|
}
|
|
122
145
|
return null;
|
|
123
146
|
}
|
|
@@ -131,15 +154,16 @@ function pickMaxBytes(cfg, maxBytesMb) {
|
|
|
131
154
|
}
|
|
132
155
|
return undefined;
|
|
133
156
|
}
|
|
134
|
-
function buildImageContext(prompt,
|
|
157
|
+
function buildImageContext(prompt, images) {
|
|
158
|
+
const content = [{ type: "text", text: prompt }];
|
|
159
|
+
for (const img of images) {
|
|
160
|
+
content.push({ type: "image", data: img.base64, mimeType: img.mimeType });
|
|
161
|
+
}
|
|
135
162
|
return {
|
|
136
163
|
messages: [
|
|
137
164
|
{
|
|
138
165
|
role: "user",
|
|
139
|
-
content
|
|
140
|
-
{ type: "text", text: prompt },
|
|
141
|
-
{ type: "image", data: base64, mimeType },
|
|
142
|
-
],
|
|
166
|
+
content,
|
|
143
167
|
timestamp: Date.now(),
|
|
144
168
|
},
|
|
145
169
|
],
|
|
@@ -149,29 +173,32 @@ async function resolveSandboxedImagePath(params) {
|
|
|
149
173
|
const normalize = (p) => (p.startsWith("file://") ? p.slice("file://".length) : p);
|
|
150
174
|
const filePath = normalize(params.imagePath);
|
|
151
175
|
try {
|
|
152
|
-
const
|
|
176
|
+
const resolved = params.sandbox.bridge.resolvePath({
|
|
153
177
|
filePath,
|
|
154
|
-
cwd: params.
|
|
155
|
-
root: params.sandboxRoot,
|
|
178
|
+
cwd: params.sandbox.root,
|
|
156
179
|
});
|
|
157
|
-
return { resolved:
|
|
180
|
+
return { resolved: resolved.hostPath };
|
|
158
181
|
}
|
|
159
182
|
catch (err) {
|
|
160
183
|
const name = path.basename(filePath);
|
|
161
184
|
const candidateRel = path.join("media", "inbound", name);
|
|
162
|
-
const candidateAbs = path.join(params.sandboxRoot, candidateRel);
|
|
163
185
|
try {
|
|
164
|
-
await
|
|
186
|
+
const stat = await params.sandbox.bridge.stat({
|
|
187
|
+
filePath: candidateRel,
|
|
188
|
+
cwd: params.sandbox.root,
|
|
189
|
+
});
|
|
190
|
+
if (!stat) {
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
165
193
|
}
|
|
166
194
|
catch {
|
|
167
195
|
throw err;
|
|
168
196
|
}
|
|
169
|
-
const out =
|
|
197
|
+
const out = params.sandbox.bridge.resolvePath({
|
|
170
198
|
filePath: candidateRel,
|
|
171
|
-
cwd: params.
|
|
172
|
-
root: params.sandboxRoot,
|
|
199
|
+
cwd: params.sandbox.root,
|
|
173
200
|
});
|
|
174
|
-
return { resolved: out.
|
|
201
|
+
return { resolved: out.hostPath, rewrittenFrom: filePath };
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
204
|
async function runImagePrompt(params) {
|
|
@@ -208,8 +235,10 @@ async function runImagePrompt(params) {
|
|
|
208
235
|
});
|
|
209
236
|
const apiKey = requireApiKey(apiKeyInfo, model.provider);
|
|
210
237
|
authStorage.setRuntimeApiKey(model.provider, apiKey);
|
|
211
|
-
|
|
238
|
+
// MiniMax VLM only supports a single image; use the first one.
|
|
212
239
|
if (model.provider === "minimax") {
|
|
240
|
+
const first = params.images[0];
|
|
241
|
+
const imageDataUrl = `data:${first.mimeType};base64,${first.base64}`;
|
|
213
242
|
const text = await minimaxUnderstandImage({
|
|
214
243
|
apiKey,
|
|
215
244
|
prompt: params.prompt,
|
|
@@ -218,11 +247,11 @@ async function runImagePrompt(params) {
|
|
|
218
247
|
});
|
|
219
248
|
return { text, provider: model.provider, model: model.id };
|
|
220
249
|
}
|
|
221
|
-
const context = buildImageContext(params.prompt, params.
|
|
222
|
-
const message =
|
|
250
|
+
const context = buildImageContext(params.prompt, params.images);
|
|
251
|
+
const message = await complete(model, context, {
|
|
223
252
|
apiKey,
|
|
224
|
-
maxTokens:
|
|
225
|
-
})
|
|
253
|
+
maxTokens: resolveImageToolMaxTokens(model.maxTokens),
|
|
254
|
+
});
|
|
226
255
|
const text = coerceImageAssistantText({
|
|
227
256
|
message,
|
|
228
257
|
provider: model.provider,
|
|
@@ -255,53 +284,76 @@ export function createImageTool(options) {
|
|
|
255
284
|
cfg: options?.config,
|
|
256
285
|
agentDir,
|
|
257
286
|
});
|
|
258
|
-
if (!imageModelConfig)
|
|
287
|
+
if (!imageModelConfig) {
|
|
259
288
|
return null;
|
|
289
|
+
}
|
|
260
290
|
// If model has native vision, images in the prompt are auto-injected
|
|
261
291
|
// so this tool is only needed when image wasn't provided in the prompt
|
|
262
292
|
const description = options?.modelHasVision
|
|
263
|
-
? "Analyze
|
|
264
|
-
: "Analyze
|
|
293
|
+
? "Analyze one or more images with a vision model. Use image for a single path/URL, or images for multiple (up to 20). Only use this tool when images were NOT already provided in the user's message. Images mentioned in the prompt are automatically visible to you."
|
|
294
|
+
: "Analyze one or more images with the configured image model (agents.defaults.imageModel). Use image for a single path/URL, or images for multiple (up to 20). Provide a prompt describing what to analyze.";
|
|
295
|
+
const localRoots = (() => {
|
|
296
|
+
const roots = getDefaultLocalRoots();
|
|
297
|
+
const workspaceDir = normalizeWorkspaceDir(options?.workspaceDir);
|
|
298
|
+
if (!workspaceDir) {
|
|
299
|
+
return roots;
|
|
300
|
+
}
|
|
301
|
+
return Array.from(new Set([...roots, workspaceDir]));
|
|
302
|
+
})();
|
|
265
303
|
return {
|
|
266
304
|
label: "Image",
|
|
267
305
|
name: "image",
|
|
268
306
|
description,
|
|
269
307
|
parameters: Type.Object({
|
|
270
308
|
prompt: Type.Optional(Type.String()),
|
|
271
|
-
image: Type.String(),
|
|
309
|
+
image: Type.Optional(Type.String({ description: "Single image path or URL." })),
|
|
310
|
+
images: Type.Optional(Type.Array(Type.String(), {
|
|
311
|
+
description: "Multiple image paths or URLs (up to maxImages, default 20).",
|
|
312
|
+
})),
|
|
272
313
|
model: Type.Optional(Type.String()),
|
|
273
314
|
maxBytesMb: Type.Optional(Type.Number()),
|
|
315
|
+
maxImages: Type.Optional(Type.Number()),
|
|
274
316
|
}),
|
|
275
317
|
execute: async (_toolCallId, args) => {
|
|
276
318
|
const record = args && typeof args === "object" ? args : {};
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
319
|
+
// MARK: - Normalize image + images input and dedupe while preserving order
|
|
320
|
+
const imageCandidates = [];
|
|
321
|
+
if (typeof record.image === "string") {
|
|
322
|
+
imageCandidates.push(record.image);
|
|
323
|
+
}
|
|
324
|
+
if (Array.isArray(record.images)) {
|
|
325
|
+
imageCandidates.push(...record.images.filter((v) => typeof v === "string"));
|
|
326
|
+
}
|
|
327
|
+
const seenImages = new Set();
|
|
328
|
+
const imageInputs = [];
|
|
329
|
+
for (const candidate of imageCandidates) {
|
|
330
|
+
const trimmedCandidate = candidate.trim();
|
|
331
|
+
const normalizedForDedupe = trimmedCandidate.startsWith("@")
|
|
332
|
+
? trimmedCandidate.slice(1).trim()
|
|
333
|
+
: trimmedCandidate;
|
|
334
|
+
if (!normalizedForDedupe || seenImages.has(normalizedForDedupe)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
seenImages.add(normalizedForDedupe);
|
|
338
|
+
imageInputs.push(trimmedCandidate);
|
|
339
|
+
}
|
|
340
|
+
if (imageInputs.length === 0) {
|
|
282
341
|
throw new Error("image required");
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const isFileUrl = /^file:/i.test(imageRaw);
|
|
291
|
-
const isHttpUrl = /^https?:\/\//i.test(imageRaw);
|
|
292
|
-
const isDataUrl = /^data:/i.test(imageRaw);
|
|
293
|
-
if (hasScheme && !looksLikeWindowsDrivePath && !isFileUrl && !isHttpUrl && !isDataUrl) {
|
|
342
|
+
}
|
|
343
|
+
// MARK: - Enforce max images cap
|
|
344
|
+
const maxImagesRaw = typeof record.maxImages === "number" ? record.maxImages : undefined;
|
|
345
|
+
const maxImages = typeof maxImagesRaw === "number" && Number.isFinite(maxImagesRaw) && maxImagesRaw > 0
|
|
346
|
+
? Math.floor(maxImagesRaw)
|
|
347
|
+
: DEFAULT_MAX_IMAGES;
|
|
348
|
+
if (imageInputs.length > maxImages) {
|
|
294
349
|
return {
|
|
295
350
|
content: [
|
|
296
351
|
{
|
|
297
352
|
type: "text",
|
|
298
|
-
text: `
|
|
353
|
+
text: `Too many images: ${imageInputs.length} provided, maximum is ${maxImages}. Please reduce the number of images.`,
|
|
299
354
|
},
|
|
300
355
|
],
|
|
301
|
-
details: {
|
|
302
|
-
error: "unsupported_image_reference",
|
|
303
|
-
image: imageRawInput,
|
|
304
|
-
},
|
|
356
|
+
details: { error: "too_many_images", count: imageInputs.length, max: maxImages },
|
|
305
357
|
};
|
|
306
358
|
}
|
|
307
359
|
const promptRaw = typeof record.prompt === "string" && record.prompt.trim()
|
|
@@ -310,58 +362,121 @@ export function createImageTool(options) {
|
|
|
310
362
|
const modelOverride = typeof record.model === "string" && record.model.trim() ? record.model.trim() : undefined;
|
|
311
363
|
const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
|
312
364
|
const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (imageRaw
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
365
|
+
const sandboxConfig = options?.sandbox && options?.sandbox.root.trim()
|
|
366
|
+
? { root: options.sandbox.root.trim(), bridge: options.sandbox.bridge }
|
|
367
|
+
: null;
|
|
368
|
+
// MARK: - Load and resolve each image
|
|
369
|
+
const loadedImages = [];
|
|
370
|
+
for (const imageRawInput of imageInputs) {
|
|
371
|
+
const trimmed = imageRawInput.trim();
|
|
372
|
+
const imageRaw = trimmed.startsWith("@") ? trimmed.slice(1).trim() : trimmed;
|
|
373
|
+
if (!imageRaw) {
|
|
374
|
+
throw new Error("image required (empty string in array)");
|
|
375
|
+
}
|
|
376
|
+
// The tool accepts file paths, file/data URLs, or http(s) URLs. In some
|
|
377
|
+
// agent/model contexts, images can be referenced as pseudo-URIs like
|
|
378
|
+
// `image:0` (e.g. "first image in the prompt"). We don't have access to a
|
|
379
|
+
// shared image registry here, so fail gracefully instead of attempting to
|
|
380
|
+
// `fs.readFile("image:0")` and producing a noisy ENOENT.
|
|
381
|
+
const looksLikeWindowsDrivePath = /^[a-zA-Z]:[\\/]/.test(imageRaw);
|
|
382
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(imageRaw);
|
|
383
|
+
const isFileUrl = /^file:/i.test(imageRaw);
|
|
384
|
+
const isHttpUrl = /^https?:\/\//i.test(imageRaw);
|
|
385
|
+
const isDataUrl = /^data:/i.test(imageRaw);
|
|
386
|
+
if (hasScheme && !looksLikeWindowsDrivePath && !isFileUrl && !isHttpUrl && !isDataUrl) {
|
|
387
|
+
return {
|
|
388
|
+
content: [
|
|
389
|
+
{
|
|
390
|
+
type: "text",
|
|
391
|
+
text: `Unsupported image reference: ${imageRawInput}. Use a file path, a file:// URL, a data: URL, or an http(s) URL.`,
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
details: {
|
|
395
|
+
error: "unsupported_image_reference",
|
|
396
|
+
image: imageRawInput,
|
|
397
|
+
},
|
|
336
398
|
};
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
399
|
+
}
|
|
400
|
+
if (sandboxConfig && isHttpUrl) {
|
|
401
|
+
throw new Error("Sandboxed image tool does not allow remote URLs.");
|
|
402
|
+
}
|
|
403
|
+
const resolvedImage = (() => {
|
|
404
|
+
if (sandboxConfig) {
|
|
405
|
+
return imageRaw;
|
|
406
|
+
}
|
|
407
|
+
if (imageRaw.startsWith("~")) {
|
|
408
|
+
return resolveUserPath(imageRaw);
|
|
409
|
+
}
|
|
410
|
+
return imageRaw;
|
|
411
|
+
})();
|
|
412
|
+
const resolvedPathInfo = isDataUrl
|
|
413
|
+
? { resolved: "" }
|
|
414
|
+
: sandboxConfig
|
|
415
|
+
? await resolveSandboxedImagePath({
|
|
416
|
+
sandbox: sandboxConfig,
|
|
417
|
+
imagePath: resolvedImage,
|
|
418
|
+
})
|
|
419
|
+
: {
|
|
420
|
+
resolved: resolvedImage.startsWith("file://")
|
|
421
|
+
? resolvedImage.slice("file://".length)
|
|
422
|
+
: resolvedImage,
|
|
423
|
+
};
|
|
424
|
+
const resolvedPath = isDataUrl ? null : resolvedPathInfo.resolved;
|
|
425
|
+
const media = isDataUrl
|
|
426
|
+
? decodeDataUrl(resolvedImage)
|
|
427
|
+
: sandboxConfig
|
|
428
|
+
? await loadWebMedia(resolvedPath ?? resolvedImage, {
|
|
429
|
+
maxBytes,
|
|
430
|
+
sandboxValidated: true,
|
|
431
|
+
readFile: (filePath) => sandboxConfig.bridge.readFile({ filePath, cwd: sandboxConfig.root }),
|
|
432
|
+
})
|
|
433
|
+
: await loadWebMedia(resolvedPath ?? resolvedImage, {
|
|
434
|
+
maxBytes,
|
|
435
|
+
localRoots,
|
|
436
|
+
});
|
|
437
|
+
if (media.kind !== "image") {
|
|
438
|
+
throw new Error(`Unsupported media type: ${media.kind}`);
|
|
439
|
+
}
|
|
440
|
+
const mimeType = ("contentType" in media && media.contentType) ||
|
|
441
|
+
("mimeType" in media && media.mimeType) ||
|
|
442
|
+
"image/png";
|
|
443
|
+
const base64 = media.buffer.toString("base64");
|
|
444
|
+
loadedImages.push({
|
|
445
|
+
base64,
|
|
446
|
+
mimeType,
|
|
447
|
+
resolvedImage,
|
|
448
|
+
...(resolvedPathInfo.rewrittenFrom
|
|
449
|
+
? { rewrittenFrom: resolvedPathInfo.rewrittenFrom }
|
|
450
|
+
: {}),
|
|
451
|
+
});
|
|
343
452
|
}
|
|
344
|
-
|
|
345
|
-
("mimeType" in media && media.mimeType) ||
|
|
346
|
-
"image/png";
|
|
347
|
-
const base64 = media.buffer.toString("base64");
|
|
453
|
+
// MARK: - Run image prompt with all loaded images
|
|
348
454
|
const result = await runImagePrompt({
|
|
349
455
|
cfg: options?.config,
|
|
350
456
|
agentDir,
|
|
351
457
|
imageModelConfig,
|
|
352
458
|
modelOverride,
|
|
353
459
|
prompt: promptRaw,
|
|
354
|
-
base64,
|
|
355
|
-
mimeType,
|
|
460
|
+
images: loadedImages.map((img) => ({ base64: img.base64, mimeType: img.mimeType })),
|
|
356
461
|
});
|
|
462
|
+
const imageDetails = loadedImages.length === 1
|
|
463
|
+
? {
|
|
464
|
+
image: loadedImages[0].resolvedImage,
|
|
465
|
+
...(loadedImages[0].rewrittenFrom
|
|
466
|
+
? { rewrittenFrom: loadedImages[0].rewrittenFrom }
|
|
467
|
+
: {}),
|
|
468
|
+
}
|
|
469
|
+
: {
|
|
470
|
+
images: loadedImages.map((img) => ({
|
|
471
|
+
image: img.resolvedImage,
|
|
472
|
+
...(img.rewrittenFrom ? { rewrittenFrom: img.rewrittenFrom } : {}),
|
|
473
|
+
})),
|
|
474
|
+
};
|
|
357
475
|
return {
|
|
358
476
|
content: [{ type: "text", text: result.text }],
|
|
359
477
|
details: {
|
|
360
478
|
model: `${result.provider}/${result.model}`,
|
|
361
|
-
|
|
362
|
-
...(resolvedPathInfo.rewrittenFrom
|
|
363
|
-
? { rewrittenFrom: resolvedPathInfo.rewrittenFrom }
|
|
364
|
-
: {}),
|
|
479
|
+
...imageDetails,
|
|
365
480
|
attempts: result.attempts,
|
|
366
481
|
},
|
|
367
482
|
};
|