@hybridaione/hybridclaw 0.2.3 → 0.2.7
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/container/src/index.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { URL } from 'url';
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { URL } from 'node:url';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { TrustedCoworkerApprovalRuntime } from './approval-policy.js';
|
|
6
|
+
import {
|
|
7
|
+
emitRuntimeEvent,
|
|
8
|
+
runAfterToolHooks,
|
|
9
|
+
runBeforeToolHooks,
|
|
10
|
+
} from './extensions.js';
|
|
11
|
+
import {
|
|
12
|
+
callHybridAI,
|
|
13
|
+
callHybridAIStream,
|
|
14
|
+
HybridAIRequestError,
|
|
15
|
+
} from './hybridai-client.js';
|
|
7
16
|
import { waitForInput, writeOutput } from './ipc.js';
|
|
8
17
|
import {
|
|
9
18
|
accumulateApiUsage,
|
|
@@ -36,16 +45,33 @@ import type {
|
|
|
36
45
|
} from './types.js';
|
|
37
46
|
|
|
38
47
|
const MAX_ITERATIONS = 20;
|
|
39
|
-
const IDLE_TIMEOUT_MS = parseInt(
|
|
48
|
+
const IDLE_TIMEOUT_MS = parseInt(
|
|
49
|
+
process.env.CONTAINER_IDLE_TIMEOUT || '300000',
|
|
50
|
+
10,
|
|
51
|
+
); // 5 min
|
|
40
52
|
const RETRY_ENABLED = process.env.HYBRIDCLAW_RETRY_ENABLED !== 'false';
|
|
41
|
-
const RETRY_MAX_ATTEMPTS = Math.max(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
53
|
+
const RETRY_MAX_ATTEMPTS = Math.max(
|
|
54
|
+
1,
|
|
55
|
+
parseInt(process.env.HYBRIDCLAW_RETRY_MAX_ATTEMPTS || '3', 10),
|
|
56
|
+
);
|
|
57
|
+
const RETRY_BASE_DELAY_MS = Math.max(
|
|
58
|
+
100,
|
|
59
|
+
parseInt(process.env.HYBRIDCLAW_RETRY_BASE_DELAY_MS || '2000', 10),
|
|
60
|
+
);
|
|
61
|
+
const RETRY_MAX_DELAY_MS = Math.max(
|
|
62
|
+
RETRY_BASE_DELAY_MS,
|
|
63
|
+
parseInt(process.env.HYBRIDCLAW_RETRY_MAX_DELAY_MS || '8000', 10),
|
|
64
|
+
);
|
|
65
|
+
const RAW_RALPH_MAX_EXTRA_ITERATIONS = Number.parseInt(
|
|
66
|
+
process.env.HYBRIDCLAW_RALPH_MAX_ITERATIONS || '0',
|
|
67
|
+
10,
|
|
68
|
+
);
|
|
69
|
+
const RALPH_MAX_EXTRA_ITERATIONS = Number.isFinite(
|
|
70
|
+
RAW_RALPH_MAX_EXTRA_ITERATIONS,
|
|
71
|
+
)
|
|
72
|
+
? RAW_RALPH_MAX_EXTRA_ITERATIONS === -1
|
|
47
73
|
? -1
|
|
48
|
-
: Math.max(0, Math.min(64, RAW_RALPH_MAX_EXTRA_ITERATIONS))
|
|
74
|
+
: Math.max(0, Math.min(64, RAW_RALPH_MAX_EXTRA_ITERATIONS))
|
|
49
75
|
: 0;
|
|
50
76
|
const RALPH_ENABLED = RALPH_MAX_EXTRA_ITERATIONS !== 0;
|
|
51
77
|
const WORKSPACE_ROOT = '/workspace';
|
|
@@ -67,6 +93,7 @@ const DISCORD_CDN_HOST_PATTERNS: RegExp[] = [
|
|
|
67
93
|
/^cdn\.discordapp\.net$/i,
|
|
68
94
|
/^images-ext-\d+\.discordapp\.net$/i,
|
|
69
95
|
];
|
|
96
|
+
const approvalRuntime = new TrustedCoworkerApprovalRuntime();
|
|
70
97
|
|
|
71
98
|
/** API key received once via stdin, held in memory for the container lifetime. */
|
|
72
99
|
let storedApiKey = '';
|
|
@@ -97,16 +124,25 @@ function normalizeAllowedLocalImagePath(rawPath: string): string | null {
|
|
|
97
124
|
|
|
98
125
|
const candidate = trimmed.startsWith('/')
|
|
99
126
|
? path.posix.normalize(normalizePathSlashes(trimmed))
|
|
100
|
-
: path.posix.normalize(
|
|
127
|
+
: path.posix.normalize(
|
|
128
|
+
path.posix.join(workspace, normalizePathSlashes(trimmed)),
|
|
129
|
+
);
|
|
101
130
|
|
|
102
|
-
const underWorkspace =
|
|
103
|
-
|
|
131
|
+
const underWorkspace =
|
|
132
|
+
candidate === workspace || candidate.startsWith(`${workspace}/`);
|
|
133
|
+
const underMediaRoot =
|
|
134
|
+
candidate === mediaRoot || candidate.startsWith(`${mediaRoot}/`);
|
|
104
135
|
if (!underWorkspace && !underMediaRoot) return null;
|
|
105
136
|
return candidate;
|
|
106
137
|
}
|
|
107
138
|
|
|
108
|
-
function inferImageMimeType(
|
|
109
|
-
|
|
139
|
+
function inferImageMimeType(
|
|
140
|
+
filePath: string,
|
|
141
|
+
fallbackMime: string | null | undefined,
|
|
142
|
+
): string {
|
|
143
|
+
const normalizedFallback = String(fallbackMime || '')
|
|
144
|
+
.trim()
|
|
145
|
+
.toLowerCase();
|
|
110
146
|
if (normalizedFallback.startsWith('image/')) return normalizedFallback;
|
|
111
147
|
const ext = path.posix.extname(filePath).toLowerCase();
|
|
112
148
|
return ARTIFACT_MIME_TYPES[ext] || 'image/png';
|
|
@@ -120,46 +156,58 @@ function isSafeDiscordCdnUrl(raw: string): boolean {
|
|
|
120
156
|
return false;
|
|
121
157
|
}
|
|
122
158
|
if (parsed.protocol !== 'https:') return false;
|
|
123
|
-
return DISCORD_CDN_HOST_PATTERNS.some((pattern) =>
|
|
159
|
+
return DISCORD_CDN_HOST_PATTERNS.some((pattern) =>
|
|
160
|
+
pattern.test(parsed.hostname),
|
|
161
|
+
);
|
|
124
162
|
}
|
|
125
163
|
|
|
126
164
|
function modelSupportsNativeVision(model: string): boolean {
|
|
127
165
|
const normalized = model.toLowerCase();
|
|
128
166
|
if (!normalized) return false;
|
|
129
167
|
if (
|
|
130
|
-
normalized.includes('gpt-5')
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
normalized.includes('gpt-5') ||
|
|
169
|
+
normalized.includes('gpt-4o') ||
|
|
170
|
+
normalized.includes('gpt-4.1') ||
|
|
171
|
+
normalized.includes('o1') ||
|
|
172
|
+
normalized.includes('o3') ||
|
|
173
|
+
normalized.includes('vision') ||
|
|
174
|
+
normalized.includes('multimodal') ||
|
|
175
|
+
normalized.includes('gemini') ||
|
|
176
|
+
normalized.includes('claude-3')
|
|
139
177
|
) {
|
|
140
178
|
return true;
|
|
141
179
|
}
|
|
142
180
|
return false;
|
|
143
181
|
}
|
|
144
182
|
|
|
145
|
-
async function resolveMediaImagePartUrl(
|
|
146
|
-
|
|
183
|
+
async function resolveMediaImagePartUrl(
|
|
184
|
+
item: MediaContextItem,
|
|
185
|
+
): Promise<string | null> {
|
|
186
|
+
const localPath = item.path
|
|
187
|
+
? normalizeAllowedLocalImagePath(item.path)
|
|
188
|
+
: null;
|
|
147
189
|
if (localPath) {
|
|
148
190
|
try {
|
|
149
191
|
const image = await fs.promises.readFile(localPath);
|
|
150
192
|
if (image.length > NATIVE_VISION_MAX_IMAGE_BYTES) {
|
|
151
|
-
console.error(
|
|
193
|
+
console.error(
|
|
194
|
+
`[media] skipping ${localPath}: ${image.length}B exceeds native vision max`,
|
|
195
|
+
);
|
|
152
196
|
} else {
|
|
153
197
|
const mimeType = inferImageMimeType(localPath, item.mimeType);
|
|
154
198
|
const base64 = image.toString('base64');
|
|
155
199
|
return `data:${mimeType};base64,${base64}`;
|
|
156
200
|
}
|
|
157
201
|
} catch (err) {
|
|
158
|
-
console.error(
|
|
202
|
+
console.error(
|
|
203
|
+
`[media] failed to read local media ${localPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
204
|
+
);
|
|
159
205
|
}
|
|
160
206
|
}
|
|
161
207
|
|
|
162
|
-
const fallbackCandidates = [item.url, item.originalUrl]
|
|
208
|
+
const fallbackCandidates = [item.url, item.originalUrl]
|
|
209
|
+
.map((value) => String(value || '').trim())
|
|
210
|
+
.filter(Boolean);
|
|
163
211
|
for (const candidate of fallbackCandidates) {
|
|
164
212
|
if (!isSafeDiscordCdnUrl(candidate)) continue;
|
|
165
213
|
return candidate;
|
|
@@ -193,12 +241,17 @@ async function injectNativeVisionContent(
|
|
|
193
241
|
if (latestUserIndex < 0) return messages;
|
|
194
242
|
|
|
195
243
|
const cloned = messages.map((msg) => ({ ...msg }));
|
|
196
|
-
const existingText = normalizeMessageContentToText(
|
|
244
|
+
const existingText = normalizeMessageContentToText(
|
|
245
|
+
cloned[latestUserIndex].content,
|
|
246
|
+
);
|
|
197
247
|
const contentParts: ChatContentPart[] = [];
|
|
198
248
|
const nativeVisionHint =
|
|
199
249
|
'[NativeVision] Image parts are attached in this message. Analyze them directly and skip extra vision tool pre-analysis unless explicitly required.';
|
|
200
250
|
if (existingText) {
|
|
201
|
-
contentParts.push({
|
|
251
|
+
contentParts.push({
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: `${existingText}\n\n${nativeVisionHint}`,
|
|
254
|
+
});
|
|
202
255
|
} else {
|
|
203
256
|
contentParts.push({ type: 'text', text: nativeVisionHint });
|
|
204
257
|
}
|
|
@@ -207,7 +260,9 @@ async function injectNativeVisionContent(
|
|
|
207
260
|
...cloned[latestUserIndex],
|
|
208
261
|
content: contentParts,
|
|
209
262
|
};
|
|
210
|
-
console.error(
|
|
263
|
+
console.error(
|
|
264
|
+
`[media] injected ${imageParts.length} native vision image part(s) for model ${model}`,
|
|
265
|
+
);
|
|
211
266
|
return cloned;
|
|
212
267
|
}
|
|
213
268
|
|
|
@@ -254,26 +309,63 @@ function isRetryableError(err: unknown): boolean {
|
|
|
254
309
|
return err.status === 429 || (err.status >= 500 && err.status <= 504);
|
|
255
310
|
}
|
|
256
311
|
const message = err instanceof Error ? err.message : String(err);
|
|
257
|
-
return /fetch failed|network|socket|timeout|timed out|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(
|
|
312
|
+
return /fetch failed|network|socket|timeout|timed out|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(
|
|
313
|
+
message,
|
|
314
|
+
);
|
|
258
315
|
}
|
|
259
316
|
|
|
260
317
|
function inferToolError(result: string, blockedReason: string | null): boolean {
|
|
261
318
|
if (blockedReason) return true;
|
|
262
|
-
return /\b(error|failed|denied|forbidden|timed out|timeout|exception|invalid)\b/i.test(
|
|
319
|
+
return /\b(error|failed|denied|forbidden|timed out|timeout|exception|invalid)\b/i.test(
|
|
320
|
+
result,
|
|
321
|
+
);
|
|
263
322
|
}
|
|
264
323
|
|
|
265
324
|
function latestUserPrompt(messages: ChatMessage[]): string {
|
|
266
325
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
267
326
|
const message = messages[i];
|
|
268
327
|
if (message.role !== 'user') continue;
|
|
269
|
-
const text = normalizeMessageContentToText(message.content)
|
|
328
|
+
const text = normalizeMessageContentToText(message.content)
|
|
329
|
+
.replace(/\s+/g, ' ')
|
|
330
|
+
.trim();
|
|
270
331
|
if (!text) continue;
|
|
271
332
|
return text.slice(0, 1_200);
|
|
272
333
|
}
|
|
273
334
|
return 'Continue the task';
|
|
274
335
|
}
|
|
275
336
|
|
|
276
|
-
function
|
|
337
|
+
function cloneMessageWithTextContent(
|
|
338
|
+
message: ChatMessage,
|
|
339
|
+
text: string,
|
|
340
|
+
): ChatMessage {
|
|
341
|
+
if (typeof message.content === 'string' || message.content == null) {
|
|
342
|
+
return {
|
|
343
|
+
...message,
|
|
344
|
+
content: text,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
...message,
|
|
349
|
+
content: [{ type: 'text', text }],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function replaceLatestUserPrompt(
|
|
354
|
+
messages: ChatMessage[],
|
|
355
|
+
prompt: string,
|
|
356
|
+
): ChatMessage[] {
|
|
357
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
358
|
+
if (messages[i].role !== 'user') continue;
|
|
359
|
+
const cloned = messages.map((entry) => ({ ...entry }));
|
|
360
|
+
cloned[i] = cloneMessageWithTextContent(cloned[i], prompt);
|
|
361
|
+
return cloned;
|
|
362
|
+
}
|
|
363
|
+
return [...messages, { role: 'user', content: prompt }];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseRalphChoice(
|
|
367
|
+
content: ChatMessageContent,
|
|
368
|
+
): 'CONTINUE' | 'STOP' | null {
|
|
277
369
|
const normalizedContent = normalizeMessageContentToText(content);
|
|
278
370
|
if (!normalizedContent) return null;
|
|
279
371
|
const re = /<choice>\s*([^<]*)\s*<\/choice>/gi;
|
|
@@ -299,7 +391,9 @@ function stripRalphChoiceTags(content: ChatMessageContent): string | null {
|
|
|
299
391
|
}
|
|
300
392
|
|
|
301
393
|
function buildRalphPrompt(taskPrompt: string, missingChoice: boolean): string {
|
|
302
|
-
const punctuatedPrompt = /[.!?]$/.test(taskPrompt)
|
|
394
|
+
const punctuatedPrompt = /[.!?]$/.test(taskPrompt)
|
|
395
|
+
? taskPrompt
|
|
396
|
+
: `${taskPrompt}.`;
|
|
303
397
|
const lines = [
|
|
304
398
|
`${punctuatedPrompt} (You are running in an automated loop where the same prompt is fed repeatedly. Only choose STOP when the task is fully complete. Including it will stop further iterations. If you are not 100% sure, choose CONTINUE.)`,
|
|
305
399
|
'',
|
|
@@ -311,7 +405,9 @@ function buildRalphPrompt(taskPrompt: string, missingChoice: boolean): string {
|
|
|
311
405
|
];
|
|
312
406
|
if (missingChoice) {
|
|
313
407
|
lines.push('');
|
|
314
|
-
lines.push(
|
|
408
|
+
lines.push(
|
|
409
|
+
'Your last response did not include a valid choice. Include exactly one: CONTINUE or STOP.',
|
|
410
|
+
);
|
|
315
411
|
}
|
|
316
412
|
return lines.join('\n');
|
|
317
413
|
}
|
|
@@ -333,7 +429,10 @@ function normalizeArtifactPath(rawPath: unknown): string | null {
|
|
|
333
429
|
const normalized = value.replace(/\\/g, '/');
|
|
334
430
|
if (path.posix.isAbsolute(normalized)) {
|
|
335
431
|
const cleanAbs = path.posix.normalize(normalized);
|
|
336
|
-
if (
|
|
432
|
+
if (
|
|
433
|
+
cleanAbs === WORKSPACE_ROOT ||
|
|
434
|
+
cleanAbs.startsWith(`${WORKSPACE_ROOT}/`)
|
|
435
|
+
) {
|
|
337
436
|
return cleanAbs;
|
|
338
437
|
}
|
|
339
438
|
return null;
|
|
@@ -344,7 +443,10 @@ function normalizeArtifactPath(rawPath: unknown): string | null {
|
|
|
344
443
|
return path.posix.join(WORKSPACE_ROOT, clean);
|
|
345
444
|
}
|
|
346
445
|
|
|
347
|
-
function extractToolArtifacts(
|
|
446
|
+
function extractToolArtifacts(
|
|
447
|
+
toolName: string,
|
|
448
|
+
result: string,
|
|
449
|
+
): ArtifactMetadata[] {
|
|
348
450
|
let parsed: Record<string, unknown> | null = null;
|
|
349
451
|
try {
|
|
350
452
|
const value = JSON.parse(result) as unknown;
|
|
@@ -358,7 +460,11 @@ function extractToolArtifacts(toolName: string, result: string): ArtifactMetadat
|
|
|
358
460
|
if (!parsed || parsed.success !== true) return [];
|
|
359
461
|
const artifacts: ArtifactMetadata[] = [];
|
|
360
462
|
|
|
361
|
-
const addArtifact = (
|
|
463
|
+
const addArtifact = (
|
|
464
|
+
rawPath: unknown,
|
|
465
|
+
rawFilename?: unknown,
|
|
466
|
+
rawMimeType?: unknown,
|
|
467
|
+
): void => {
|
|
362
468
|
const normalizedPath = normalizeArtifactPath(rawPath);
|
|
363
469
|
if (!normalizedPath) return;
|
|
364
470
|
const filename =
|
|
@@ -396,6 +502,7 @@ async function callHybridAIWithRetry(params: {
|
|
|
396
502
|
history: ChatMessage[];
|
|
397
503
|
tools: ToolDefinition[];
|
|
398
504
|
onTextDelta?: (delta: string) => void;
|
|
505
|
+
maxTokens?: number;
|
|
399
506
|
}): Promise<Awaited<ReturnType<typeof callHybridAI>>> {
|
|
400
507
|
const {
|
|
401
508
|
baseUrl,
|
|
@@ -406,6 +513,7 @@ async function callHybridAIWithRetry(params: {
|
|
|
406
513
|
history,
|
|
407
514
|
tools,
|
|
408
515
|
onTextDelta,
|
|
516
|
+
maxTokens,
|
|
409
517
|
} = params;
|
|
410
518
|
let attempt = 0;
|
|
411
519
|
let delayMs = RETRY_BASE_DELAY_MS;
|
|
@@ -414,7 +522,7 @@ async function callHybridAIWithRetry(params: {
|
|
|
414
522
|
attempt += 1;
|
|
415
523
|
await emitRuntimeEvent({ event: 'before_model_call', attempt });
|
|
416
524
|
try {
|
|
417
|
-
let response
|
|
525
|
+
let response: Awaited<ReturnType<typeof callHybridAI>>;
|
|
418
526
|
if (onTextDelta) {
|
|
419
527
|
try {
|
|
420
528
|
response = await callHybridAIStream(
|
|
@@ -426,23 +534,47 @@ async function callHybridAIWithRetry(params: {
|
|
|
426
534
|
history,
|
|
427
535
|
tools,
|
|
428
536
|
onTextDelta,
|
|
537
|
+
maxTokens,
|
|
429
538
|
);
|
|
430
539
|
} catch (streamErr) {
|
|
431
540
|
const fallbackEligible =
|
|
432
|
-
streamErr instanceof HybridAIRequestError
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
541
|
+
streamErr instanceof HybridAIRequestError &&
|
|
542
|
+
streamErr.status >= 400 &&
|
|
543
|
+
streamErr.status < 500 &&
|
|
544
|
+
streamErr.status !== 429;
|
|
436
545
|
if (!fallbackEligible) throw streamErr;
|
|
437
|
-
response = await callHybridAI(
|
|
546
|
+
response = await callHybridAI(
|
|
547
|
+
baseUrl,
|
|
548
|
+
apiKey,
|
|
549
|
+
model,
|
|
550
|
+
chatbotId,
|
|
551
|
+
enableRag,
|
|
552
|
+
history,
|
|
553
|
+
tools,
|
|
554
|
+
maxTokens,
|
|
555
|
+
);
|
|
438
556
|
}
|
|
439
557
|
} else {
|
|
440
|
-
response = await callHybridAI(
|
|
558
|
+
response = await callHybridAI(
|
|
559
|
+
baseUrl,
|
|
560
|
+
apiKey,
|
|
561
|
+
model,
|
|
562
|
+
chatbotId,
|
|
563
|
+
enableRag,
|
|
564
|
+
history,
|
|
565
|
+
tools,
|
|
566
|
+
maxTokens,
|
|
567
|
+
);
|
|
441
568
|
}
|
|
442
|
-
await emitRuntimeEvent({
|
|
569
|
+
await emitRuntimeEvent({
|
|
570
|
+
event: 'after_model_call',
|
|
571
|
+
attempt,
|
|
572
|
+
toolCallCount: response.choices[0]?.message?.tool_calls?.length || 0,
|
|
573
|
+
});
|
|
443
574
|
return response;
|
|
444
575
|
} catch (err) {
|
|
445
|
-
const retryable =
|
|
576
|
+
const retryable =
|
|
577
|
+
RETRY_ENABLED && isRetryableError(err) && attempt < RETRY_MAX_ATTEMPTS;
|
|
446
578
|
await emitRuntimeEvent({
|
|
447
579
|
event: retryable ? 'model_retry' : 'model_error',
|
|
448
580
|
attempt,
|
|
@@ -467,15 +599,22 @@ async function processRequest(
|
|
|
467
599
|
chatbotId: string,
|
|
468
600
|
enableRag: boolean,
|
|
469
601
|
tools: ToolDefinition[],
|
|
602
|
+
maxTokens?: number,
|
|
603
|
+
effectiveUserPromptOverride?: string,
|
|
470
604
|
): Promise<ContainerOutput> {
|
|
471
|
-
await emitRuntimeEvent({
|
|
605
|
+
await emitRuntimeEvent({
|
|
606
|
+
event: 'before_agent_start',
|
|
607
|
+
messageCount: messages.length,
|
|
608
|
+
});
|
|
472
609
|
const history: ChatMessage[] = [...messages];
|
|
473
610
|
const toolsUsed: string[] = [];
|
|
474
611
|
const toolExecutions: ToolExecution[] = [];
|
|
475
612
|
const artifacts: ArtifactMetadata[] = [];
|
|
476
613
|
const artifactPaths = new Set<string>();
|
|
477
614
|
const tokenUsage = createTokenUsageStats();
|
|
478
|
-
const
|
|
615
|
+
const effectiveUserPrompt =
|
|
616
|
+
effectiveUserPromptOverride || latestUserPrompt(messages);
|
|
617
|
+
const ralphSeedPrompt = RALPH_ENABLED ? effectiveUserPrompt : '';
|
|
479
618
|
const maxModelTurns = resolveMaxModelTurns();
|
|
480
619
|
let ralphExtraIterations = 0;
|
|
481
620
|
let iterations = 0;
|
|
@@ -485,7 +624,7 @@ async function processRequest(
|
|
|
485
624
|
tokenUsage.modelCalls += 1;
|
|
486
625
|
tokenUsage.estimatedPromptTokens += estimateMessageTokens(history);
|
|
487
626
|
|
|
488
|
-
let response
|
|
627
|
+
let response: Awaited<ReturnType<typeof callHybridAIWithRetry>>;
|
|
489
628
|
try {
|
|
490
629
|
response = await callHybridAIWithRetry({
|
|
491
630
|
baseUrl,
|
|
@@ -496,6 +635,7 @@ async function processRequest(
|
|
|
496
635
|
history,
|
|
497
636
|
tools,
|
|
498
637
|
onTextDelta: emitStreamDelta,
|
|
638
|
+
maxTokens,
|
|
499
639
|
});
|
|
500
640
|
} catch (err) {
|
|
501
641
|
const failed: ContainerOutput = {
|
|
@@ -507,7 +647,11 @@ async function processRequest(
|
|
|
507
647
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
508
648
|
error: `API error: ${err instanceof Error ? err.message : String(err)}`,
|
|
509
649
|
};
|
|
510
|
-
await emitRuntimeEvent({
|
|
650
|
+
await emitRuntimeEvent({
|
|
651
|
+
event: 'turn_end',
|
|
652
|
+
status: failed.status,
|
|
653
|
+
toolsUsed,
|
|
654
|
+
});
|
|
511
655
|
return failed;
|
|
512
656
|
}
|
|
513
657
|
|
|
@@ -524,13 +668,21 @@ async function processRequest(
|
|
|
524
668
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
525
669
|
error: 'No response from API',
|
|
526
670
|
};
|
|
527
|
-
await emitRuntimeEvent({
|
|
671
|
+
await emitRuntimeEvent({
|
|
672
|
+
event: 'turn_end',
|
|
673
|
+
status: failed.status,
|
|
674
|
+
toolsUsed,
|
|
675
|
+
});
|
|
528
676
|
return failed;
|
|
529
677
|
}
|
|
530
678
|
|
|
531
|
-
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
679
|
+
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
680
|
+
choice.message.content,
|
|
681
|
+
);
|
|
532
682
|
if (choice.message.tool_calls?.length) {
|
|
533
|
-
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
683
|
+
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
684
|
+
JSON.stringify(choice.message.tool_calls),
|
|
685
|
+
);
|
|
534
686
|
}
|
|
535
687
|
|
|
536
688
|
const assistantMessage: ChatMessage = {
|
|
@@ -556,12 +708,19 @@ async function processRequest(
|
|
|
556
708
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
557
709
|
toolExecutions,
|
|
558
710
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
711
|
+
effectiveUserPrompt,
|
|
559
712
|
};
|
|
560
|
-
await emitRuntimeEvent({
|
|
713
|
+
await emitRuntimeEvent({
|
|
714
|
+
event: 'turn_end',
|
|
715
|
+
status: completed.status,
|
|
716
|
+
toolsUsed: completed.toolsUsed,
|
|
717
|
+
});
|
|
561
718
|
return completed;
|
|
562
719
|
}
|
|
563
720
|
|
|
564
|
-
const canContinue =
|
|
721
|
+
const canContinue =
|
|
722
|
+
RALPH_MAX_EXTRA_ITERATIONS < 0 ||
|
|
723
|
+
ralphExtraIterations < RALPH_MAX_EXTRA_ITERATIONS;
|
|
565
724
|
if (canContinue) {
|
|
566
725
|
ralphExtraIterations += 1;
|
|
567
726
|
history.push({
|
|
@@ -569,8 +728,10 @@ async function processRequest(
|
|
|
569
728
|
content: buildRalphPrompt(ralphSeedPrompt, branchChoice == null),
|
|
570
729
|
});
|
|
571
730
|
console.error(
|
|
572
|
-
`[ralph] continue ${ralphExtraIterations}`
|
|
573
|
-
|
|
731
|
+
`[ralph] continue ${ralphExtraIterations}` +
|
|
732
|
+
(RALPH_MAX_EXTRA_ITERATIONS < 0
|
|
733
|
+
? ''
|
|
734
|
+
: `/${RALPH_MAX_EXTRA_ITERATIONS}`),
|
|
574
735
|
);
|
|
575
736
|
continue;
|
|
576
737
|
}
|
|
@@ -583,24 +744,121 @@ async function processRequest(
|
|
|
583
744
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
584
745
|
toolExecutions,
|
|
585
746
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
747
|
+
effectiveUserPrompt,
|
|
586
748
|
};
|
|
587
|
-
await emitRuntimeEvent({
|
|
749
|
+
await emitRuntimeEvent({
|
|
750
|
+
event: 'turn_end',
|
|
751
|
+
status: completed.status,
|
|
752
|
+
toolsUsed: completed.toolsUsed,
|
|
753
|
+
});
|
|
588
754
|
return completed;
|
|
589
755
|
}
|
|
590
756
|
|
|
591
757
|
for (const call of toolCalls) {
|
|
592
758
|
const toolName = call.function.name;
|
|
759
|
+
const approval = approvalRuntime.evaluateToolCall({
|
|
760
|
+
toolName,
|
|
761
|
+
argsJson: call.function.arguments,
|
|
762
|
+
latestUserPrompt: effectiveUserPrompt,
|
|
763
|
+
});
|
|
764
|
+
|
|
593
765
|
toolsUsed.push(toolName);
|
|
594
|
-
|
|
766
|
+
const toolPreview =
|
|
767
|
+
approval.tier === 'yellow'
|
|
768
|
+
? approvalRuntime.formatYellowNarration(approval)
|
|
769
|
+
: call.function.arguments.slice(0, 100);
|
|
770
|
+
console.error(`[tool] ${toolName}: ${toolPreview}`);
|
|
595
771
|
const toolStart = Date.now();
|
|
596
|
-
|
|
772
|
+
if (approval.decision === 'required') {
|
|
773
|
+
const toolDuration = Date.now() - toolStart;
|
|
774
|
+
const prompt = approvalRuntime.formatApprovalRequest(approval);
|
|
775
|
+
toolExecutions.push({
|
|
776
|
+
name: toolName,
|
|
777
|
+
arguments: call.function.arguments,
|
|
778
|
+
result: prompt,
|
|
779
|
+
durationMs: toolDuration,
|
|
780
|
+
isError: false,
|
|
781
|
+
blocked: true,
|
|
782
|
+
blockedReason: approval.reason,
|
|
783
|
+
approvalTier: approval.tier,
|
|
784
|
+
approvalBaseTier: approval.baseTier,
|
|
785
|
+
approvalDecision: approval.decision,
|
|
786
|
+
approvalActionKey: approval.actionKey,
|
|
787
|
+
approvalReason: approval.reason,
|
|
788
|
+
approvalRequestId: approval.requestId,
|
|
789
|
+
});
|
|
790
|
+
const waitingForApproval: ContainerOutput = {
|
|
791
|
+
status: 'success',
|
|
792
|
+
result: prompt,
|
|
793
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
794
|
+
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
795
|
+
toolExecutions,
|
|
796
|
+
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
797
|
+
effectiveUserPrompt,
|
|
798
|
+
};
|
|
799
|
+
await emitRuntimeEvent({
|
|
800
|
+
event: 'turn_end',
|
|
801
|
+
status: waitingForApproval.status,
|
|
802
|
+
toolsUsed: waitingForApproval.toolsUsed,
|
|
803
|
+
});
|
|
804
|
+
return waitingForApproval;
|
|
805
|
+
}
|
|
806
|
+
if (approval.decision === 'denied') {
|
|
807
|
+
const toolDuration = Date.now() - toolStart;
|
|
808
|
+
const denialText = `Approval denied: ${approval.reason}`;
|
|
809
|
+
toolExecutions.push({
|
|
810
|
+
name: toolName,
|
|
811
|
+
arguments: call.function.arguments,
|
|
812
|
+
result: denialText,
|
|
813
|
+
durationMs: toolDuration,
|
|
814
|
+
isError: true,
|
|
815
|
+
blocked: true,
|
|
816
|
+
blockedReason: approval.reason,
|
|
817
|
+
approvalTier: approval.tier,
|
|
818
|
+
approvalBaseTier: approval.baseTier,
|
|
819
|
+
approvalDecision: approval.decision,
|
|
820
|
+
approvalActionKey: approval.actionKey,
|
|
821
|
+
approvalReason: approval.reason,
|
|
822
|
+
approvalRequestId: approval.requestId,
|
|
823
|
+
});
|
|
824
|
+
const denied: ContainerOutput = {
|
|
825
|
+
status: 'success',
|
|
826
|
+
result: denialText,
|
|
827
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
828
|
+
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
829
|
+
toolExecutions,
|
|
830
|
+
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
831
|
+
effectiveUserPrompt,
|
|
832
|
+
};
|
|
833
|
+
await emitRuntimeEvent({
|
|
834
|
+
event: 'turn_end',
|
|
835
|
+
status: denied.status,
|
|
836
|
+
toolsUsed: denied.toolsUsed,
|
|
837
|
+
});
|
|
838
|
+
return denied;
|
|
839
|
+
}
|
|
840
|
+
if (
|
|
841
|
+
approval.tier === 'yellow' &&
|
|
842
|
+
approval.implicitDelayMs &&
|
|
843
|
+
approval.implicitDelayMs > 0
|
|
844
|
+
) {
|
|
845
|
+
await sleep(approval.implicitDelayMs);
|
|
846
|
+
}
|
|
847
|
+
const blockedReason = await runBeforeToolHooks(
|
|
848
|
+
toolName,
|
|
849
|
+
call.function.arguments,
|
|
850
|
+
);
|
|
597
851
|
const result = blockedReason
|
|
598
852
|
? `Tool blocked by security hook: ${blockedReason}`
|
|
599
853
|
: await executeTool(toolName, call.function.arguments);
|
|
600
854
|
const toolDuration = Date.now() - toolStart;
|
|
601
855
|
const isError = inferToolError(result, blockedReason);
|
|
856
|
+
const succeeded = !blockedReason && !isError;
|
|
857
|
+
approvalRuntime.afterToolExecution(approval, succeeded);
|
|
602
858
|
await runAfterToolHooks(toolName, call.function.arguments, result);
|
|
603
|
-
console.error(
|
|
859
|
+
console.error(
|
|
860
|
+
`[tool] ${toolName} result (${toolDuration}ms): ${result.slice(0, 100)}`,
|
|
861
|
+
);
|
|
604
862
|
toolExecutions.push({
|
|
605
863
|
name: toolName,
|
|
606
864
|
arguments: call.function.arguments,
|
|
@@ -609,6 +867,12 @@ async function processRequest(
|
|
|
609
867
|
isError,
|
|
610
868
|
blocked: Boolean(blockedReason),
|
|
611
869
|
blockedReason: blockedReason || undefined,
|
|
870
|
+
approvalTier: approval.tier,
|
|
871
|
+
approvalBaseTier: approval.baseTier,
|
|
872
|
+
approvalDecision: blockedReason ? 'denied' : approval.decision,
|
|
873
|
+
approvalActionKey: approval.actionKey,
|
|
874
|
+
approvalReason: approval.reason,
|
|
875
|
+
approvalRequestId: approval.requestId,
|
|
612
876
|
});
|
|
613
877
|
for (const artifact of extractToolArtifacts(toolName, result)) {
|
|
614
878
|
if (artifactPaths.has(artifact.path)) continue;
|
|
@@ -627,8 +891,13 @@ async function processRequest(
|
|
|
627
891
|
toolExecutions,
|
|
628
892
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
629
893
|
error: result,
|
|
894
|
+
effectiveUserPrompt,
|
|
630
895
|
};
|
|
631
|
-
await emitRuntimeEvent({
|
|
896
|
+
await emitRuntimeEvent({
|
|
897
|
+
event: 'turn_end',
|
|
898
|
+
status: failed.status,
|
|
899
|
+
toolsUsed,
|
|
900
|
+
});
|
|
632
901
|
return failed;
|
|
633
902
|
}
|
|
634
903
|
}
|
|
@@ -637,13 +906,20 @@ async function processRequest(
|
|
|
637
906
|
const lastAssistant = history.filter((m) => m.role === 'assistant').pop();
|
|
638
907
|
const completed: ContainerOutput = {
|
|
639
908
|
status: 'success',
|
|
640
|
-
result:
|
|
909
|
+
result:
|
|
910
|
+
stripRalphChoiceTags(lastAssistant?.content || null) ||
|
|
911
|
+
'Max tool iterations reached.',
|
|
641
912
|
toolsUsed: [...new Set(toolsUsed)],
|
|
642
913
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
643
914
|
toolExecutions,
|
|
644
915
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
916
|
+
effectiveUserPrompt,
|
|
645
917
|
};
|
|
646
|
-
await emitRuntimeEvent({
|
|
918
|
+
await emitRuntimeEvent({
|
|
919
|
+
event: 'turn_end',
|
|
920
|
+
status: completed.status,
|
|
921
|
+
toolsUsed: completed.toolsUsed,
|
|
922
|
+
});
|
|
647
923
|
return completed;
|
|
648
924
|
}
|
|
649
925
|
|
|
@@ -652,7 +928,9 @@ async function processRequest(
|
|
|
652
928
|
*/
|
|
653
929
|
function resolveTools(input: ContainerInput): ToolDefinition[] {
|
|
654
930
|
let tools = input.allowedTools
|
|
655
|
-
? TOOL_DEFINITIONS.filter((t) =>
|
|
931
|
+
? TOOL_DEFINITIONS.filter((t) =>
|
|
932
|
+
input.allowedTools?.includes(t.function.name),
|
|
933
|
+
)
|
|
656
934
|
: [...TOOL_DEFINITIONS];
|
|
657
935
|
if (Array.isArray(input.blockedTools) && input.blockedTools.length > 0) {
|
|
658
936
|
const blocked = new Set(
|
|
@@ -662,7 +940,7 @@ function resolveTools(input: ContainerInput): ToolDefinition[] {
|
|
|
662
940
|
);
|
|
663
941
|
tools = tools.filter((tool) => !blocked.has(tool.function.name));
|
|
664
942
|
}
|
|
665
|
-
// Sort alphabetically for deterministic
|
|
943
|
+
// Sort alphabetically for deterministic tool ordering (request/cache stability)
|
|
666
944
|
tools.sort((a, b) => a.function.name.localeCompare(b.function.name));
|
|
667
945
|
return tools;
|
|
668
946
|
}
|
|
@@ -671,66 +949,107 @@ function shouldRetryWithoutNativeVision(error: string | undefined): boolean {
|
|
|
671
949
|
const normalized = String(error || '').toLowerCase();
|
|
672
950
|
if (!normalized) return false;
|
|
673
951
|
return (
|
|
674
|
-
normalized.includes('image_url')
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
952
|
+
normalized.includes('image_url') ||
|
|
953
|
+
normalized.includes('unsupported image') ||
|
|
954
|
+
normalized.includes('unsupported content') ||
|
|
955
|
+
normalized.includes('vision') ||
|
|
956
|
+
normalized.includes('multimodal') ||
|
|
957
|
+
normalized.includes('content part')
|
|
680
958
|
);
|
|
681
959
|
}
|
|
682
960
|
|
|
683
961
|
async function main(): Promise<void> {
|
|
684
|
-
console.error(
|
|
962
|
+
console.error(
|
|
963
|
+
`[hybridclaw-agent] started, idle timeout ${IDLE_TIMEOUT_MS}ms`,
|
|
964
|
+
);
|
|
685
965
|
|
|
686
966
|
// First request arrives via stdin (contains apiKey — never written to disk)
|
|
687
967
|
const stdinData = await readStdinLine();
|
|
688
968
|
const firstInput: ContainerInput = JSON.parse(stdinData);
|
|
689
969
|
storedApiKey = firstInput.apiKey;
|
|
690
970
|
|
|
691
|
-
console.error(
|
|
971
|
+
console.error(
|
|
972
|
+
`[hybridclaw-agent] processing first request (${firstInput.messages.length} messages)`,
|
|
973
|
+
);
|
|
692
974
|
|
|
693
975
|
resetSideEffects();
|
|
694
976
|
setScheduledTasks(firstInput.scheduledTasks);
|
|
695
977
|
setSessionContext(firstInput.sessionId);
|
|
696
|
-
setGatewayContext(
|
|
697
|
-
|
|
978
|
+
setGatewayContext(
|
|
979
|
+
firstInput.gatewayBaseUrl,
|
|
980
|
+
firstInput.gatewayApiToken,
|
|
981
|
+
firstInput.channelId,
|
|
982
|
+
);
|
|
983
|
+
setModelContext(
|
|
984
|
+
firstInput.baseUrl,
|
|
985
|
+
storedApiKey,
|
|
986
|
+
firstInput.model,
|
|
987
|
+
firstInput.chatbotId,
|
|
988
|
+
);
|
|
698
989
|
setMediaContext(firstInput.media);
|
|
699
990
|
const firstMessages = await injectNativeVisionContent(
|
|
700
991
|
firstInput.messages,
|
|
701
992
|
firstInput.model,
|
|
702
993
|
firstInput.media,
|
|
703
994
|
);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
console.error('[
|
|
995
|
+
const firstPrelude = approvalRuntime.handleApprovalResponse(firstMessages);
|
|
996
|
+
const firstPromptOverride = firstPrelude?.replayPrompt;
|
|
997
|
+
const firstPreparedMessages = firstPromptOverride
|
|
998
|
+
? replaceLatestUserPrompt(firstMessages, firstPromptOverride)
|
|
999
|
+
: firstMessages;
|
|
1000
|
+
|
|
1001
|
+
let firstOutput: ContainerOutput;
|
|
1002
|
+
if (firstPrelude?.immediateMessage && !firstPromptOverride) {
|
|
1003
|
+
firstOutput = {
|
|
1004
|
+
status: 'success',
|
|
1005
|
+
result: firstPrelude.immediateMessage,
|
|
1006
|
+
toolsUsed: [],
|
|
1007
|
+
toolExecutions: [],
|
|
1008
|
+
effectiveUserPrompt: latestUserPrompt(firstPreparedMessages),
|
|
1009
|
+
};
|
|
1010
|
+
console.error('[approval] resolved user response without model run');
|
|
1011
|
+
} else {
|
|
720
1012
|
firstOutput = await processRequest(
|
|
721
|
-
|
|
1013
|
+
firstPreparedMessages,
|
|
722
1014
|
storedApiKey,
|
|
723
1015
|
firstInput.baseUrl,
|
|
724
1016
|
firstInput.model,
|
|
725
1017
|
firstInput.chatbotId,
|
|
726
1018
|
firstInput.enableRag,
|
|
727
1019
|
resolveTools(firstInput),
|
|
1020
|
+
firstInput.maxTokens,
|
|
1021
|
+
firstPromptOverride,
|
|
728
1022
|
);
|
|
1023
|
+
if (
|
|
1024
|
+
firstPreparedMessages !== firstInput.messages &&
|
|
1025
|
+
firstOutput.status === 'error' &&
|
|
1026
|
+
shouldRetryWithoutNativeVision(firstOutput.error)
|
|
1027
|
+
) {
|
|
1028
|
+
console.error(
|
|
1029
|
+
'[media] native vision injection rejected by model; retrying without image parts',
|
|
1030
|
+
);
|
|
1031
|
+
const firstRetryMessages = firstPromptOverride
|
|
1032
|
+
? replaceLatestUserPrompt(firstInput.messages, firstPromptOverride)
|
|
1033
|
+
: firstInput.messages;
|
|
1034
|
+
firstOutput = await processRequest(
|
|
1035
|
+
firstRetryMessages,
|
|
1036
|
+
storedApiKey,
|
|
1037
|
+
firstInput.baseUrl,
|
|
1038
|
+
firstInput.model,
|
|
1039
|
+
firstInput.chatbotId,
|
|
1040
|
+
firstInput.enableRag,
|
|
1041
|
+
resolveTools(firstInput),
|
|
1042
|
+
firstInput.maxTokens,
|
|
1043
|
+
firstPromptOverride,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
729
1046
|
}
|
|
730
1047
|
|
|
731
1048
|
firstOutput.sideEffects = getPendingSideEffects();
|
|
732
1049
|
writeOutput(firstOutput);
|
|
733
|
-
console.error(
|
|
1050
|
+
console.error(
|
|
1051
|
+
`[hybridclaw-agent] first request complete: ${firstOutput.status}`,
|
|
1052
|
+
);
|
|
734
1053
|
|
|
735
1054
|
// Subsequent requests come via IPC file polling
|
|
736
1055
|
while (true) {
|
|
@@ -744,12 +1063,18 @@ async function main(): Promise<void> {
|
|
|
744
1063
|
// Use stored apiKey — IPC file no longer contains it
|
|
745
1064
|
const apiKey = input.apiKey || storedApiKey;
|
|
746
1065
|
|
|
747
|
-
console.error(
|
|
1066
|
+
console.error(
|
|
1067
|
+
`[hybridclaw-agent] processing request (${input.messages.length} messages)`,
|
|
1068
|
+
);
|
|
748
1069
|
|
|
749
1070
|
resetSideEffects();
|
|
750
1071
|
setScheduledTasks(input.scheduledTasks);
|
|
751
1072
|
setSessionContext(input.sessionId);
|
|
752
|
-
setGatewayContext(
|
|
1073
|
+
setGatewayContext(
|
|
1074
|
+
input.gatewayBaseUrl,
|
|
1075
|
+
input.gatewayApiToken,
|
|
1076
|
+
input.channelId,
|
|
1077
|
+
);
|
|
753
1078
|
setModelContext(input.baseUrl, apiKey, input.model, input.chatbotId);
|
|
754
1079
|
setMediaContext(input.media);
|
|
755
1080
|
const preparedMessages = await injectNativeVisionContent(
|
|
@@ -757,30 +1082,58 @@ async function main(): Promise<void> {
|
|
|
757
1082
|
input.model,
|
|
758
1083
|
input.media,
|
|
759
1084
|
);
|
|
1085
|
+
const prelude = approvalRuntime.handleApprovalResponse(preparedMessages);
|
|
1086
|
+
const promptOverride = prelude?.replayPrompt;
|
|
1087
|
+
const messagesForRequest = promptOverride
|
|
1088
|
+
? replaceLatestUserPrompt(preparedMessages, promptOverride)
|
|
1089
|
+
: preparedMessages;
|
|
1090
|
+
|
|
1091
|
+
if (prelude?.immediateMessage && !promptOverride) {
|
|
1092
|
+
const immediate: ContainerOutput = {
|
|
1093
|
+
status: 'success',
|
|
1094
|
+
result: prelude.immediateMessage,
|
|
1095
|
+
toolsUsed: [],
|
|
1096
|
+
toolExecutions: [],
|
|
1097
|
+
effectiveUserPrompt: latestUserPrompt(messagesForRequest),
|
|
1098
|
+
};
|
|
1099
|
+
immediate.sideEffects = getPendingSideEffects();
|
|
1100
|
+
writeOutput(immediate);
|
|
1101
|
+
console.error('[approval] resolved user response without model run');
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
760
1104
|
|
|
761
1105
|
let output = await processRequest(
|
|
762
|
-
|
|
1106
|
+
messagesForRequest,
|
|
763
1107
|
apiKey,
|
|
764
1108
|
input.baseUrl,
|
|
765
1109
|
input.model,
|
|
766
1110
|
input.chatbotId,
|
|
767
1111
|
input.enableRag,
|
|
768
1112
|
resolveTools(input),
|
|
1113
|
+
input.maxTokens,
|
|
1114
|
+
promptOverride,
|
|
769
1115
|
);
|
|
770
1116
|
if (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1117
|
+
messagesForRequest !== input.messages &&
|
|
1118
|
+
output.status === 'error' &&
|
|
1119
|
+
shouldRetryWithoutNativeVision(output.error)
|
|
774
1120
|
) {
|
|
775
|
-
console.error(
|
|
1121
|
+
console.error(
|
|
1122
|
+
'[media] native vision injection rejected by model; retrying without image parts',
|
|
1123
|
+
);
|
|
1124
|
+
const retryMessages = promptOverride
|
|
1125
|
+
? replaceLatestUserPrompt(input.messages, promptOverride)
|
|
1126
|
+
: input.messages;
|
|
776
1127
|
output = await processRequest(
|
|
777
|
-
|
|
1128
|
+
retryMessages,
|
|
778
1129
|
apiKey,
|
|
779
1130
|
input.baseUrl,
|
|
780
1131
|
input.model,
|
|
781
1132
|
input.chatbotId,
|
|
782
1133
|
input.enableRag,
|
|
783
1134
|
resolveTools(input),
|
|
1135
|
+
input.maxTokens,
|
|
1136
|
+
promptOverride,
|
|
784
1137
|
);
|
|
785
1138
|
}
|
|
786
1139
|
|