@hybridaione/hybridclaw 0.2.2 → 0.2.6
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 +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- 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 +19 -3
- 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 +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- 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 +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- 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 +16 -8
- 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 +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -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 +563 -73
- 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 +179 -24
- 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 +48 -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/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 +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- 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 +29 -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 +146 -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 +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- 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 +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- 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 +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- 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 +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -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 +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- 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 +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -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 +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- 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 =
|
|
@@ -414,7 +520,7 @@ async function callHybridAIWithRetry(params: {
|
|
|
414
520
|
attempt += 1;
|
|
415
521
|
await emitRuntimeEvent({ event: 'before_model_call', attempt });
|
|
416
522
|
try {
|
|
417
|
-
let response
|
|
523
|
+
let response: Awaited<ReturnType<typeof callHybridAI>>;
|
|
418
524
|
if (onTextDelta) {
|
|
419
525
|
try {
|
|
420
526
|
response = await callHybridAIStream(
|
|
@@ -429,20 +535,41 @@ async function callHybridAIWithRetry(params: {
|
|
|
429
535
|
);
|
|
430
536
|
} catch (streamErr) {
|
|
431
537
|
const fallbackEligible =
|
|
432
|
-
streamErr instanceof HybridAIRequestError
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
538
|
+
streamErr instanceof HybridAIRequestError &&
|
|
539
|
+
streamErr.status >= 400 &&
|
|
540
|
+
streamErr.status < 500 &&
|
|
541
|
+
streamErr.status !== 429;
|
|
436
542
|
if (!fallbackEligible) throw streamErr;
|
|
437
|
-
response = await callHybridAI(
|
|
543
|
+
response = await callHybridAI(
|
|
544
|
+
baseUrl,
|
|
545
|
+
apiKey,
|
|
546
|
+
model,
|
|
547
|
+
chatbotId,
|
|
548
|
+
enableRag,
|
|
549
|
+
history,
|
|
550
|
+
tools,
|
|
551
|
+
);
|
|
438
552
|
}
|
|
439
553
|
} else {
|
|
440
|
-
response = await callHybridAI(
|
|
554
|
+
response = await callHybridAI(
|
|
555
|
+
baseUrl,
|
|
556
|
+
apiKey,
|
|
557
|
+
model,
|
|
558
|
+
chatbotId,
|
|
559
|
+
enableRag,
|
|
560
|
+
history,
|
|
561
|
+
tools,
|
|
562
|
+
);
|
|
441
563
|
}
|
|
442
|
-
await emitRuntimeEvent({
|
|
564
|
+
await emitRuntimeEvent({
|
|
565
|
+
event: 'after_model_call',
|
|
566
|
+
attempt,
|
|
567
|
+
toolCallCount: response.choices[0]?.message?.tool_calls?.length || 0,
|
|
568
|
+
});
|
|
443
569
|
return response;
|
|
444
570
|
} catch (err) {
|
|
445
|
-
const retryable =
|
|
571
|
+
const retryable =
|
|
572
|
+
RETRY_ENABLED && isRetryableError(err) && attempt < RETRY_MAX_ATTEMPTS;
|
|
446
573
|
await emitRuntimeEvent({
|
|
447
574
|
event: retryable ? 'model_retry' : 'model_error',
|
|
448
575
|
attempt,
|
|
@@ -467,15 +594,21 @@ async function processRequest(
|
|
|
467
594
|
chatbotId: string,
|
|
468
595
|
enableRag: boolean,
|
|
469
596
|
tools: ToolDefinition[],
|
|
597
|
+
effectiveUserPromptOverride?: string,
|
|
470
598
|
): Promise<ContainerOutput> {
|
|
471
|
-
await emitRuntimeEvent({
|
|
599
|
+
await emitRuntimeEvent({
|
|
600
|
+
event: 'before_agent_start',
|
|
601
|
+
messageCount: messages.length,
|
|
602
|
+
});
|
|
472
603
|
const history: ChatMessage[] = [...messages];
|
|
473
604
|
const toolsUsed: string[] = [];
|
|
474
605
|
const toolExecutions: ToolExecution[] = [];
|
|
475
606
|
const artifacts: ArtifactMetadata[] = [];
|
|
476
607
|
const artifactPaths = new Set<string>();
|
|
477
608
|
const tokenUsage = createTokenUsageStats();
|
|
478
|
-
const
|
|
609
|
+
const effectiveUserPrompt =
|
|
610
|
+
effectiveUserPromptOverride || latestUserPrompt(messages);
|
|
611
|
+
const ralphSeedPrompt = RALPH_ENABLED ? effectiveUserPrompt : '';
|
|
479
612
|
const maxModelTurns = resolveMaxModelTurns();
|
|
480
613
|
let ralphExtraIterations = 0;
|
|
481
614
|
let iterations = 0;
|
|
@@ -485,7 +618,7 @@ async function processRequest(
|
|
|
485
618
|
tokenUsage.modelCalls += 1;
|
|
486
619
|
tokenUsage.estimatedPromptTokens += estimateMessageTokens(history);
|
|
487
620
|
|
|
488
|
-
let response
|
|
621
|
+
let response: Awaited<ReturnType<typeof callHybridAIWithRetry>>;
|
|
489
622
|
try {
|
|
490
623
|
response = await callHybridAIWithRetry({
|
|
491
624
|
baseUrl,
|
|
@@ -507,7 +640,11 @@ async function processRequest(
|
|
|
507
640
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
508
641
|
error: `API error: ${err instanceof Error ? err.message : String(err)}`,
|
|
509
642
|
};
|
|
510
|
-
await emitRuntimeEvent({
|
|
643
|
+
await emitRuntimeEvent({
|
|
644
|
+
event: 'turn_end',
|
|
645
|
+
status: failed.status,
|
|
646
|
+
toolsUsed,
|
|
647
|
+
});
|
|
511
648
|
return failed;
|
|
512
649
|
}
|
|
513
650
|
|
|
@@ -524,13 +661,21 @@ async function processRequest(
|
|
|
524
661
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
525
662
|
error: 'No response from API',
|
|
526
663
|
};
|
|
527
|
-
await emitRuntimeEvent({
|
|
664
|
+
await emitRuntimeEvent({
|
|
665
|
+
event: 'turn_end',
|
|
666
|
+
status: failed.status,
|
|
667
|
+
toolsUsed,
|
|
668
|
+
});
|
|
528
669
|
return failed;
|
|
529
670
|
}
|
|
530
671
|
|
|
531
|
-
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
672
|
+
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
673
|
+
choice.message.content,
|
|
674
|
+
);
|
|
532
675
|
if (choice.message.tool_calls?.length) {
|
|
533
|
-
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
676
|
+
tokenUsage.estimatedCompletionTokens += estimateTextTokens(
|
|
677
|
+
JSON.stringify(choice.message.tool_calls),
|
|
678
|
+
);
|
|
534
679
|
}
|
|
535
680
|
|
|
536
681
|
const assistantMessage: ChatMessage = {
|
|
@@ -556,12 +701,19 @@ async function processRequest(
|
|
|
556
701
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
557
702
|
toolExecutions,
|
|
558
703
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
704
|
+
effectiveUserPrompt,
|
|
559
705
|
};
|
|
560
|
-
await emitRuntimeEvent({
|
|
706
|
+
await emitRuntimeEvent({
|
|
707
|
+
event: 'turn_end',
|
|
708
|
+
status: completed.status,
|
|
709
|
+
toolsUsed: completed.toolsUsed,
|
|
710
|
+
});
|
|
561
711
|
return completed;
|
|
562
712
|
}
|
|
563
713
|
|
|
564
|
-
const canContinue =
|
|
714
|
+
const canContinue =
|
|
715
|
+
RALPH_MAX_EXTRA_ITERATIONS < 0 ||
|
|
716
|
+
ralphExtraIterations < RALPH_MAX_EXTRA_ITERATIONS;
|
|
565
717
|
if (canContinue) {
|
|
566
718
|
ralphExtraIterations += 1;
|
|
567
719
|
history.push({
|
|
@@ -569,8 +721,10 @@ async function processRequest(
|
|
|
569
721
|
content: buildRalphPrompt(ralphSeedPrompt, branchChoice == null),
|
|
570
722
|
});
|
|
571
723
|
console.error(
|
|
572
|
-
`[ralph] continue ${ralphExtraIterations}`
|
|
573
|
-
|
|
724
|
+
`[ralph] continue ${ralphExtraIterations}` +
|
|
725
|
+
(RALPH_MAX_EXTRA_ITERATIONS < 0
|
|
726
|
+
? ''
|
|
727
|
+
: `/${RALPH_MAX_EXTRA_ITERATIONS}`),
|
|
574
728
|
);
|
|
575
729
|
continue;
|
|
576
730
|
}
|
|
@@ -583,24 +737,121 @@ async function processRequest(
|
|
|
583
737
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
584
738
|
toolExecutions,
|
|
585
739
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
740
|
+
effectiveUserPrompt,
|
|
586
741
|
};
|
|
587
|
-
await emitRuntimeEvent({
|
|
742
|
+
await emitRuntimeEvent({
|
|
743
|
+
event: 'turn_end',
|
|
744
|
+
status: completed.status,
|
|
745
|
+
toolsUsed: completed.toolsUsed,
|
|
746
|
+
});
|
|
588
747
|
return completed;
|
|
589
748
|
}
|
|
590
749
|
|
|
591
750
|
for (const call of toolCalls) {
|
|
592
751
|
const toolName = call.function.name;
|
|
752
|
+
const approval = approvalRuntime.evaluateToolCall({
|
|
753
|
+
toolName,
|
|
754
|
+
argsJson: call.function.arguments,
|
|
755
|
+
latestUserPrompt: effectiveUserPrompt,
|
|
756
|
+
});
|
|
757
|
+
|
|
593
758
|
toolsUsed.push(toolName);
|
|
594
|
-
|
|
759
|
+
const toolPreview =
|
|
760
|
+
approval.tier === 'yellow'
|
|
761
|
+
? approvalRuntime.formatYellowNarration(approval)
|
|
762
|
+
: call.function.arguments.slice(0, 100);
|
|
763
|
+
console.error(`[tool] ${toolName}: ${toolPreview}`);
|
|
595
764
|
const toolStart = Date.now();
|
|
596
|
-
|
|
765
|
+
if (approval.decision === 'required') {
|
|
766
|
+
const toolDuration = Date.now() - toolStart;
|
|
767
|
+
const prompt = approvalRuntime.formatApprovalRequest(approval);
|
|
768
|
+
toolExecutions.push({
|
|
769
|
+
name: toolName,
|
|
770
|
+
arguments: call.function.arguments,
|
|
771
|
+
result: prompt,
|
|
772
|
+
durationMs: toolDuration,
|
|
773
|
+
isError: false,
|
|
774
|
+
blocked: true,
|
|
775
|
+
blockedReason: approval.reason,
|
|
776
|
+
approvalTier: approval.tier,
|
|
777
|
+
approvalBaseTier: approval.baseTier,
|
|
778
|
+
approvalDecision: approval.decision,
|
|
779
|
+
approvalActionKey: approval.actionKey,
|
|
780
|
+
approvalReason: approval.reason,
|
|
781
|
+
approvalRequestId: approval.requestId,
|
|
782
|
+
});
|
|
783
|
+
const waitingForApproval: ContainerOutput = {
|
|
784
|
+
status: 'success',
|
|
785
|
+
result: prompt,
|
|
786
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
787
|
+
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
788
|
+
toolExecutions,
|
|
789
|
+
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
790
|
+
effectiveUserPrompt,
|
|
791
|
+
};
|
|
792
|
+
await emitRuntimeEvent({
|
|
793
|
+
event: 'turn_end',
|
|
794
|
+
status: waitingForApproval.status,
|
|
795
|
+
toolsUsed: waitingForApproval.toolsUsed,
|
|
796
|
+
});
|
|
797
|
+
return waitingForApproval;
|
|
798
|
+
}
|
|
799
|
+
if (approval.decision === 'denied') {
|
|
800
|
+
const toolDuration = Date.now() - toolStart;
|
|
801
|
+
const denialText = `Approval denied: ${approval.reason}`;
|
|
802
|
+
toolExecutions.push({
|
|
803
|
+
name: toolName,
|
|
804
|
+
arguments: call.function.arguments,
|
|
805
|
+
result: denialText,
|
|
806
|
+
durationMs: toolDuration,
|
|
807
|
+
isError: true,
|
|
808
|
+
blocked: true,
|
|
809
|
+
blockedReason: approval.reason,
|
|
810
|
+
approvalTier: approval.tier,
|
|
811
|
+
approvalBaseTier: approval.baseTier,
|
|
812
|
+
approvalDecision: approval.decision,
|
|
813
|
+
approvalActionKey: approval.actionKey,
|
|
814
|
+
approvalReason: approval.reason,
|
|
815
|
+
approvalRequestId: approval.requestId,
|
|
816
|
+
});
|
|
817
|
+
const denied: ContainerOutput = {
|
|
818
|
+
status: 'success',
|
|
819
|
+
result: denialText,
|
|
820
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
821
|
+
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
822
|
+
toolExecutions,
|
|
823
|
+
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
824
|
+
effectiveUserPrompt,
|
|
825
|
+
};
|
|
826
|
+
await emitRuntimeEvent({
|
|
827
|
+
event: 'turn_end',
|
|
828
|
+
status: denied.status,
|
|
829
|
+
toolsUsed: denied.toolsUsed,
|
|
830
|
+
});
|
|
831
|
+
return denied;
|
|
832
|
+
}
|
|
833
|
+
if (
|
|
834
|
+
approval.tier === 'yellow' &&
|
|
835
|
+
approval.implicitDelayMs &&
|
|
836
|
+
approval.implicitDelayMs > 0
|
|
837
|
+
) {
|
|
838
|
+
await sleep(approval.implicitDelayMs);
|
|
839
|
+
}
|
|
840
|
+
const blockedReason = await runBeforeToolHooks(
|
|
841
|
+
toolName,
|
|
842
|
+
call.function.arguments,
|
|
843
|
+
);
|
|
597
844
|
const result = blockedReason
|
|
598
845
|
? `Tool blocked by security hook: ${blockedReason}`
|
|
599
846
|
: await executeTool(toolName, call.function.arguments);
|
|
600
847
|
const toolDuration = Date.now() - toolStart;
|
|
601
848
|
const isError = inferToolError(result, blockedReason);
|
|
849
|
+
const succeeded = !blockedReason && !isError;
|
|
850
|
+
approvalRuntime.afterToolExecution(approval, succeeded);
|
|
602
851
|
await runAfterToolHooks(toolName, call.function.arguments, result);
|
|
603
|
-
console.error(
|
|
852
|
+
console.error(
|
|
853
|
+
`[tool] ${toolName} result (${toolDuration}ms): ${result.slice(0, 100)}`,
|
|
854
|
+
);
|
|
604
855
|
toolExecutions.push({
|
|
605
856
|
name: toolName,
|
|
606
857
|
arguments: call.function.arguments,
|
|
@@ -609,6 +860,12 @@ async function processRequest(
|
|
|
609
860
|
isError,
|
|
610
861
|
blocked: Boolean(blockedReason),
|
|
611
862
|
blockedReason: blockedReason || undefined,
|
|
863
|
+
approvalTier: approval.tier,
|
|
864
|
+
approvalBaseTier: approval.baseTier,
|
|
865
|
+
approvalDecision: blockedReason ? 'denied' : approval.decision,
|
|
866
|
+
approvalActionKey: approval.actionKey,
|
|
867
|
+
approvalReason: approval.reason,
|
|
868
|
+
approvalRequestId: approval.requestId,
|
|
612
869
|
});
|
|
613
870
|
for (const artifact of extractToolArtifacts(toolName, result)) {
|
|
614
871
|
if (artifactPaths.has(artifact.path)) continue;
|
|
@@ -627,8 +884,13 @@ async function processRequest(
|
|
|
627
884
|
toolExecutions,
|
|
628
885
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
629
886
|
error: result,
|
|
887
|
+
effectiveUserPrompt,
|
|
630
888
|
};
|
|
631
|
-
await emitRuntimeEvent({
|
|
889
|
+
await emitRuntimeEvent({
|
|
890
|
+
event: 'turn_end',
|
|
891
|
+
status: failed.status,
|
|
892
|
+
toolsUsed,
|
|
893
|
+
});
|
|
632
894
|
return failed;
|
|
633
895
|
}
|
|
634
896
|
}
|
|
@@ -637,13 +899,20 @@ async function processRequest(
|
|
|
637
899
|
const lastAssistant = history.filter((m) => m.role === 'assistant').pop();
|
|
638
900
|
const completed: ContainerOutput = {
|
|
639
901
|
status: 'success',
|
|
640
|
-
result:
|
|
902
|
+
result:
|
|
903
|
+
stripRalphChoiceTags(lastAssistant?.content || null) ||
|
|
904
|
+
'Max tool iterations reached.',
|
|
641
905
|
toolsUsed: [...new Set(toolsUsed)],
|
|
642
906
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
643
907
|
toolExecutions,
|
|
644
908
|
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
909
|
+
effectiveUserPrompt,
|
|
645
910
|
};
|
|
646
|
-
await emitRuntimeEvent({
|
|
911
|
+
await emitRuntimeEvent({
|
|
912
|
+
event: 'turn_end',
|
|
913
|
+
status: completed.status,
|
|
914
|
+
toolsUsed: completed.toolsUsed,
|
|
915
|
+
});
|
|
647
916
|
return completed;
|
|
648
917
|
}
|
|
649
918
|
|
|
@@ -652,7 +921,9 @@ async function processRequest(
|
|
|
652
921
|
*/
|
|
653
922
|
function resolveTools(input: ContainerInput): ToolDefinition[] {
|
|
654
923
|
let tools = input.allowedTools
|
|
655
|
-
? TOOL_DEFINITIONS.filter((t) =>
|
|
924
|
+
? TOOL_DEFINITIONS.filter((t) =>
|
|
925
|
+
input.allowedTools?.includes(t.function.name),
|
|
926
|
+
)
|
|
656
927
|
: [...TOOL_DEFINITIONS];
|
|
657
928
|
if (Array.isArray(input.blockedTools) && input.blockedTools.length > 0) {
|
|
658
929
|
const blocked = new Set(
|
|
@@ -671,66 +942,105 @@ function shouldRetryWithoutNativeVision(error: string | undefined): boolean {
|
|
|
671
942
|
const normalized = String(error || '').toLowerCase();
|
|
672
943
|
if (!normalized) return false;
|
|
673
944
|
return (
|
|
674
|
-
normalized.includes('image_url')
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
945
|
+
normalized.includes('image_url') ||
|
|
946
|
+
normalized.includes('unsupported image') ||
|
|
947
|
+
normalized.includes('unsupported content') ||
|
|
948
|
+
normalized.includes('vision') ||
|
|
949
|
+
normalized.includes('multimodal') ||
|
|
950
|
+
normalized.includes('content part')
|
|
680
951
|
);
|
|
681
952
|
}
|
|
682
953
|
|
|
683
954
|
async function main(): Promise<void> {
|
|
684
|
-
console.error(
|
|
955
|
+
console.error(
|
|
956
|
+
`[hybridclaw-agent] started, idle timeout ${IDLE_TIMEOUT_MS}ms`,
|
|
957
|
+
);
|
|
685
958
|
|
|
686
959
|
// First request arrives via stdin (contains apiKey — never written to disk)
|
|
687
960
|
const stdinData = await readStdinLine();
|
|
688
961
|
const firstInput: ContainerInput = JSON.parse(stdinData);
|
|
689
962
|
storedApiKey = firstInput.apiKey;
|
|
690
963
|
|
|
691
|
-
console.error(
|
|
964
|
+
console.error(
|
|
965
|
+
`[hybridclaw-agent] processing first request (${firstInput.messages.length} messages)`,
|
|
966
|
+
);
|
|
692
967
|
|
|
693
968
|
resetSideEffects();
|
|
694
969
|
setScheduledTasks(firstInput.scheduledTasks);
|
|
695
970
|
setSessionContext(firstInput.sessionId);
|
|
696
|
-
setGatewayContext(
|
|
697
|
-
|
|
971
|
+
setGatewayContext(
|
|
972
|
+
firstInput.gatewayBaseUrl,
|
|
973
|
+
firstInput.gatewayApiToken,
|
|
974
|
+
firstInput.channelId,
|
|
975
|
+
);
|
|
976
|
+
setModelContext(
|
|
977
|
+
firstInput.baseUrl,
|
|
978
|
+
storedApiKey,
|
|
979
|
+
firstInput.model,
|
|
980
|
+
firstInput.chatbotId,
|
|
981
|
+
);
|
|
698
982
|
setMediaContext(firstInput.media);
|
|
699
983
|
const firstMessages = await injectNativeVisionContent(
|
|
700
984
|
firstInput.messages,
|
|
701
985
|
firstInput.model,
|
|
702
986
|
firstInput.media,
|
|
703
987
|
);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
console.error('[
|
|
988
|
+
const firstPrelude = approvalRuntime.handleApprovalResponse(firstMessages);
|
|
989
|
+
const firstPromptOverride = firstPrelude?.replayPrompt;
|
|
990
|
+
const firstPreparedMessages = firstPromptOverride
|
|
991
|
+
? replaceLatestUserPrompt(firstMessages, firstPromptOverride)
|
|
992
|
+
: firstMessages;
|
|
993
|
+
|
|
994
|
+
let firstOutput: ContainerOutput;
|
|
995
|
+
if (firstPrelude?.immediateMessage && !firstPromptOverride) {
|
|
996
|
+
firstOutput = {
|
|
997
|
+
status: 'success',
|
|
998
|
+
result: firstPrelude.immediateMessage,
|
|
999
|
+
toolsUsed: [],
|
|
1000
|
+
toolExecutions: [],
|
|
1001
|
+
effectiveUserPrompt: latestUserPrompt(firstPreparedMessages),
|
|
1002
|
+
};
|
|
1003
|
+
console.error('[approval] resolved user response without model run');
|
|
1004
|
+
} else {
|
|
720
1005
|
firstOutput = await processRequest(
|
|
721
|
-
|
|
1006
|
+
firstPreparedMessages,
|
|
722
1007
|
storedApiKey,
|
|
723
1008
|
firstInput.baseUrl,
|
|
724
1009
|
firstInput.model,
|
|
725
1010
|
firstInput.chatbotId,
|
|
726
1011
|
firstInput.enableRag,
|
|
727
1012
|
resolveTools(firstInput),
|
|
1013
|
+
firstPromptOverride,
|
|
728
1014
|
);
|
|
1015
|
+
if (
|
|
1016
|
+
firstPreparedMessages !== firstInput.messages &&
|
|
1017
|
+
firstOutput.status === 'error' &&
|
|
1018
|
+
shouldRetryWithoutNativeVision(firstOutput.error)
|
|
1019
|
+
) {
|
|
1020
|
+
console.error(
|
|
1021
|
+
'[media] native vision injection rejected by model; retrying without image parts',
|
|
1022
|
+
);
|
|
1023
|
+
const firstRetryMessages = firstPromptOverride
|
|
1024
|
+
? replaceLatestUserPrompt(firstInput.messages, firstPromptOverride)
|
|
1025
|
+
: firstInput.messages;
|
|
1026
|
+
firstOutput = await processRequest(
|
|
1027
|
+
firstRetryMessages,
|
|
1028
|
+
storedApiKey,
|
|
1029
|
+
firstInput.baseUrl,
|
|
1030
|
+
firstInput.model,
|
|
1031
|
+
firstInput.chatbotId,
|
|
1032
|
+
firstInput.enableRag,
|
|
1033
|
+
resolveTools(firstInput),
|
|
1034
|
+
firstPromptOverride,
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
729
1037
|
}
|
|
730
1038
|
|
|
731
1039
|
firstOutput.sideEffects = getPendingSideEffects();
|
|
732
1040
|
writeOutput(firstOutput);
|
|
733
|
-
console.error(
|
|
1041
|
+
console.error(
|
|
1042
|
+
`[hybridclaw-agent] first request complete: ${firstOutput.status}`,
|
|
1043
|
+
);
|
|
734
1044
|
|
|
735
1045
|
// Subsequent requests come via IPC file polling
|
|
736
1046
|
while (true) {
|
|
@@ -744,12 +1054,18 @@ async function main(): Promise<void> {
|
|
|
744
1054
|
// Use stored apiKey — IPC file no longer contains it
|
|
745
1055
|
const apiKey = input.apiKey || storedApiKey;
|
|
746
1056
|
|
|
747
|
-
console.error(
|
|
1057
|
+
console.error(
|
|
1058
|
+
`[hybridclaw-agent] processing request (${input.messages.length} messages)`,
|
|
1059
|
+
);
|
|
748
1060
|
|
|
749
1061
|
resetSideEffects();
|
|
750
1062
|
setScheduledTasks(input.scheduledTasks);
|
|
751
1063
|
setSessionContext(input.sessionId);
|
|
752
|
-
setGatewayContext(
|
|
1064
|
+
setGatewayContext(
|
|
1065
|
+
input.gatewayBaseUrl,
|
|
1066
|
+
input.gatewayApiToken,
|
|
1067
|
+
input.channelId,
|
|
1068
|
+
);
|
|
753
1069
|
setModelContext(input.baseUrl, apiKey, input.model, input.chatbotId);
|
|
754
1070
|
setMediaContext(input.media);
|
|
755
1071
|
const preparedMessages = await injectNativeVisionContent(
|
|
@@ -757,30 +1073,56 @@ async function main(): Promise<void> {
|
|
|
757
1073
|
input.model,
|
|
758
1074
|
input.media,
|
|
759
1075
|
);
|
|
1076
|
+
const prelude = approvalRuntime.handleApprovalResponse(preparedMessages);
|
|
1077
|
+
const promptOverride = prelude?.replayPrompt;
|
|
1078
|
+
const messagesForRequest = promptOverride
|
|
1079
|
+
? replaceLatestUserPrompt(preparedMessages, promptOverride)
|
|
1080
|
+
: preparedMessages;
|
|
1081
|
+
|
|
1082
|
+
if (prelude?.immediateMessage && !promptOverride) {
|
|
1083
|
+
const immediate: ContainerOutput = {
|
|
1084
|
+
status: 'success',
|
|
1085
|
+
result: prelude.immediateMessage,
|
|
1086
|
+
toolsUsed: [],
|
|
1087
|
+
toolExecutions: [],
|
|
1088
|
+
effectiveUserPrompt: latestUserPrompt(messagesForRequest),
|
|
1089
|
+
};
|
|
1090
|
+
immediate.sideEffects = getPendingSideEffects();
|
|
1091
|
+
writeOutput(immediate);
|
|
1092
|
+
console.error('[approval] resolved user response without model run');
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
760
1095
|
|
|
761
1096
|
let output = await processRequest(
|
|
762
|
-
|
|
1097
|
+
messagesForRequest,
|
|
763
1098
|
apiKey,
|
|
764
1099
|
input.baseUrl,
|
|
765
1100
|
input.model,
|
|
766
1101
|
input.chatbotId,
|
|
767
1102
|
input.enableRag,
|
|
768
1103
|
resolveTools(input),
|
|
1104
|
+
promptOverride,
|
|
769
1105
|
);
|
|
770
1106
|
if (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1107
|
+
messagesForRequest !== input.messages &&
|
|
1108
|
+
output.status === 'error' &&
|
|
1109
|
+
shouldRetryWithoutNativeVision(output.error)
|
|
774
1110
|
) {
|
|
775
|
-
console.error(
|
|
1111
|
+
console.error(
|
|
1112
|
+
'[media] native vision injection rejected by model; retrying without image parts',
|
|
1113
|
+
);
|
|
1114
|
+
const retryMessages = promptOverride
|
|
1115
|
+
? replaceLatestUserPrompt(input.messages, promptOverride)
|
|
1116
|
+
: input.messages;
|
|
776
1117
|
output = await processRequest(
|
|
777
|
-
|
|
1118
|
+
retryMessages,
|
|
778
1119
|
apiKey,
|
|
779
1120
|
input.baseUrl,
|
|
780
1121
|
input.model,
|
|
781
1122
|
input.chatbotId,
|
|
782
1123
|
input.enableRag,
|
|
783
1124
|
resolveTools(input),
|
|
1125
|
+
promptOverride,
|
|
784
1126
|
);
|
|
785
1127
|
}
|
|
786
1128
|
|