@uncensoredcode/openbridge 0.1.0
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/README.md +117 -0
- package/bin/openbridge.js +10 -0
- package/package.json +85 -0
- package/packages/cli/dist/args.d.ts +30 -0
- package/packages/cli/dist/args.js +160 -0
- package/packages/cli/dist/cli.d.ts +2 -0
- package/packages/cli/dist/cli.js +9 -0
- package/packages/cli/dist/index.d.ts +26 -0
- package/packages/cli/dist/index.js +76 -0
- package/packages/runtime/dist/assistant-protocol.d.ts +34 -0
- package/packages/runtime/dist/assistant-protocol.js +121 -0
- package/packages/runtime/dist/execution/in-process.d.ts +14 -0
- package/packages/runtime/dist/execution/in-process.js +45 -0
- package/packages/runtime/dist/execution/types.d.ts +49 -0
- package/packages/runtime/dist/execution/types.js +20 -0
- package/packages/runtime/dist/index.d.ts +86 -0
- package/packages/runtime/dist/index.js +60 -0
- package/packages/runtime/dist/normalizers/index.d.ts +6 -0
- package/packages/runtime/dist/normalizers/index.js +12 -0
- package/packages/runtime/dist/normalizers/legacy-packet.d.ts +6 -0
- package/packages/runtime/dist/normalizers/legacy-packet.js +131 -0
- package/packages/runtime/dist/output-sanitizer.d.ts +23 -0
- package/packages/runtime/dist/output-sanitizer.js +78 -0
- package/packages/runtime/dist/packet-extractor.d.ts +17 -0
- package/packages/runtime/dist/packet-extractor.js +43 -0
- package/packages/runtime/dist/packet-normalizer.d.ts +21 -0
- package/packages/runtime/dist/packet-normalizer.js +47 -0
- package/packages/runtime/dist/prompt-compiler.d.ts +28 -0
- package/packages/runtime/dist/prompt-compiler.js +301 -0
- package/packages/runtime/dist/protocol.d.ts +44 -0
- package/packages/runtime/dist/protocol.js +165 -0
- package/packages/runtime/dist/provider-failure.d.ts +52 -0
- package/packages/runtime/dist/provider-failure.js +236 -0
- package/packages/runtime/dist/provider.d.ts +40 -0
- package/packages/runtime/dist/provider.js +1 -0
- package/packages/runtime/dist/runtime.d.ts +86 -0
- package/packages/runtime/dist/runtime.js +462 -0
- package/packages/runtime/dist/session-bound-provider.d.ts +52 -0
- package/packages/runtime/dist/session-bound-provider.js +366 -0
- package/packages/runtime/dist/tool-name-aliases.d.ts +5 -0
- package/packages/runtime/dist/tool-name-aliases.js +13 -0
- package/packages/runtime/dist/tools/bash.d.ts +9 -0
- package/packages/runtime/dist/tools/bash.js +157 -0
- package/packages/runtime/dist/tools/edit.d.ts +9 -0
- package/packages/runtime/dist/tools/edit.js +94 -0
- package/packages/runtime/dist/tools/index.d.ts +39 -0
- package/packages/runtime/dist/tools/index.js +27 -0
- package/packages/runtime/dist/tools/list-dir.d.ts +9 -0
- package/packages/runtime/dist/tools/list-dir.js +127 -0
- package/packages/runtime/dist/tools/read.d.ts +9 -0
- package/packages/runtime/dist/tools/read.js +56 -0
- package/packages/runtime/dist/tools/registry.d.ts +15 -0
- package/packages/runtime/dist/tools/registry.js +38 -0
- package/packages/runtime/dist/tools/runtime-path.d.ts +7 -0
- package/packages/runtime/dist/tools/runtime-path.js +22 -0
- package/packages/runtime/dist/tools/search-files.d.ts +9 -0
- package/packages/runtime/dist/tools/search-files.js +149 -0
- package/packages/runtime/dist/tools/text-file.d.ts +32 -0
- package/packages/runtime/dist/tools/text-file.js +101 -0
- package/packages/runtime/dist/tools/workspace-path.d.ts +17 -0
- package/packages/runtime/dist/tools/workspace-path.js +70 -0
- package/packages/runtime/dist/tools/write.d.ts +9 -0
- package/packages/runtime/dist/tools/write.js +59 -0
- package/packages/server/dist/bridge/bridge-model-catalog.d.ts +56 -0
- package/packages/server/dist/bridge/bridge-model-catalog.js +100 -0
- package/packages/server/dist/bridge/bridge-runtime-service.d.ts +61 -0
- package/packages/server/dist/bridge/bridge-runtime-service.js +1386 -0
- package/packages/server/dist/bridge/chat-completions/chat-completion-service.d.ts +127 -0
- package/packages/server/dist/bridge/chat-completions/chat-completion-service.js +1026 -0
- package/packages/server/dist/bridge/index.d.ts +335 -0
- package/packages/server/dist/bridge/index.js +45 -0
- package/packages/server/dist/bridge/live-provider-extraction-canary.d.ts +69 -0
- package/packages/server/dist/bridge/live-provider-extraction-canary.js +186 -0
- package/packages/server/dist/bridge/providers/generic-provider-transport.d.ts +53 -0
- package/packages/server/dist/bridge/providers/generic-provider-transport.js +973 -0
- package/packages/server/dist/bridge/providers/provider-session-resolver.d.ts +17 -0
- package/packages/server/dist/bridge/providers/provider-session-resolver.js +95 -0
- package/packages/server/dist/bridge/providers/provider-streams.d.ts +80 -0
- package/packages/server/dist/bridge/providers/provider-streams.js +844 -0
- package/packages/server/dist/bridge/providers/provider-transport-profile.d.ts +194 -0
- package/packages/server/dist/bridge/providers/provider-transport-profile.js +198 -0
- package/packages/server/dist/bridge/providers/web-provider-transport.d.ts +30 -0
- package/packages/server/dist/bridge/providers/web-provider-transport.js +151 -0
- package/packages/server/dist/bridge/state/file-bridge-state-store.d.ts +36 -0
- package/packages/server/dist/bridge/state/file-bridge-state-store.js +164 -0
- package/packages/server/dist/bridge/stores/local-session-package-store.d.ts +23 -0
- package/packages/server/dist/bridge/stores/local-session-package-store.js +548 -0
- package/packages/server/dist/bridge/stores/provider-store.d.ts +94 -0
- package/packages/server/dist/bridge/stores/provider-store.js +143 -0
- package/packages/server/dist/bridge/stores/session-backed-provider-store.d.ts +7 -0
- package/packages/server/dist/bridge/stores/session-backed-provider-store.js +26 -0
- package/packages/server/dist/bridge/stores/session-package-store.d.ts +286 -0
- package/packages/server/dist/bridge/stores/session-package-store.js +1527 -0
- package/packages/server/dist/bridge/stores/session-store.d.ts +120 -0
- package/packages/server/dist/bridge/stores/session-store.js +139 -0
- package/packages/server/dist/cli/index.d.ts +9 -0
- package/packages/server/dist/cli/index.js +6 -0
- package/packages/server/dist/cli/main.d.ts +2 -0
- package/packages/server/dist/cli/main.js +9 -0
- package/packages/server/dist/cli/run-bridge-server-cli.d.ts +54 -0
- package/packages/server/dist/cli/run-bridge-server-cli.js +371 -0
- package/packages/server/dist/client/bridge-api-client.d.ts +61 -0
- package/packages/server/dist/client/bridge-api-client.js +267 -0
- package/packages/server/dist/client/index.d.ts +11 -0
- package/packages/server/dist/client/index.js +11 -0
- package/packages/server/dist/config/bridge-server-config.d.ts +52 -0
- package/packages/server/dist/config/bridge-server-config.js +118 -0
- package/packages/server/dist/config/index.d.ts +20 -0
- package/packages/server/dist/config/index.js +8 -0
- package/packages/server/dist/http/bridge-api-route-context.d.ts +14 -0
- package/packages/server/dist/http/bridge-api-route-context.js +1 -0
- package/packages/server/dist/http/create-bridge-api-server.d.ts +72 -0
- package/packages/server/dist/http/create-bridge-api-server.js +225 -0
- package/packages/server/dist/http/index.d.ts +5 -0
- package/packages/server/dist/http/index.js +5 -0
- package/packages/server/dist/http/parse-request.d.ts +6 -0
- package/packages/server/dist/http/parse-request.js +27 -0
- package/packages/server/dist/http/register-bridge-api-routes.d.ts +7 -0
- package/packages/server/dist/http/register-bridge-api-routes.js +17 -0
- package/packages/server/dist/http/routes/admin-routes.d.ts +7 -0
- package/packages/server/dist/http/routes/admin-routes.js +135 -0
- package/packages/server/dist/http/routes/chat-completions-route.d.ts +7 -0
- package/packages/server/dist/http/routes/chat-completions-route.js +49 -0
- package/packages/server/dist/http/routes/health-routes.d.ts +6 -0
- package/packages/server/dist/http/routes/health-routes.js +7 -0
- package/packages/server/dist/http/routes/message-routes.d.ts +7 -0
- package/packages/server/dist/http/routes/message-routes.js +7 -0
- package/packages/server/dist/index.d.ts +85 -0
- package/packages/server/dist/index.js +28 -0
- package/packages/server/dist/security/bridge-auth.d.ts +9 -0
- package/packages/server/dist/security/bridge-auth.js +41 -0
- package/packages/server/dist/security/cors-policy.d.ts +5 -0
- package/packages/server/dist/security/cors-policy.js +34 -0
- package/packages/server/dist/security/index.d.ts +16 -0
- package/packages/server/dist/security/index.js +12 -0
- package/packages/server/dist/security/redact-sensitive-values.d.ts +19 -0
- package/packages/server/dist/security/redact-sensitive-values.js +67 -0
- package/packages/server/dist/shared/api-schema.d.ts +133 -0
- package/packages/server/dist/shared/api-schema.js +1 -0
- package/packages/server/dist/shared/bridge-api-error.d.ts +17 -0
- package/packages/server/dist/shared/bridge-api-error.js +19 -0
- package/packages/server/dist/shared/index.d.ts +7 -0
- package/packages/server/dist/shared/index.js +7 -0
- package/packages/server/dist/shared/output.d.ts +5 -0
- package/packages/server/dist/shared/output.js +14 -0
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { bridgeRuntime } from "@uncensoredcode/openbridge/runtime";
|
|
3
|
+
import { bridgeApiErrorModule } from "../shared/bridge-api-error.js";
|
|
4
|
+
import { outputModule } from "../shared/output.js";
|
|
5
|
+
import { bridgeModelCatalogModule } from "./bridge-model-catalog.js";
|
|
6
|
+
import { providerSessionResolverModule } from "./providers/provider-session-resolver.js";
|
|
7
|
+
import { providerStreamsModule } from "./providers/provider-streams.js";
|
|
8
|
+
import { webProviderTransportModule } from "./providers/web-provider-transport.js";
|
|
9
|
+
import { fileBridgeStateStoreModule } from "./state/file-bridge-state-store.js";
|
|
10
|
+
const { classifyProviderTransportError, compileProviderTurn, createMessagePacket, createToolRequestPacket, extractPacketCandidate, parseAssistantResponse, parseZcPacket, serializeAssistantResponse, InProcessToolExecutor, normalizeProviderToolName, normalizeProviderPacket, ProviderFailure, SessionBoundProviderAdapter, createDefaultRuntimeTools, createSecondaryRuntimeTools, runBridgeRuntime, serializeProviderFailure } = bridgeRuntime;
|
|
11
|
+
const { BridgeApiError } = bridgeApiErrorModule;
|
|
12
|
+
const { sanitizeBridgeApiOutput } = outputModule;
|
|
13
|
+
const { defaultModelForProvider } = bridgeModelCatalogModule;
|
|
14
|
+
const { createBridgeProviderSessionResolver } = providerSessionResolverModule;
|
|
15
|
+
const { extractIncrementalPacketMessage } = providerStreamsModule;
|
|
16
|
+
const { WebProviderTransport } = webProviderTransportModule;
|
|
17
|
+
const { FileBridgeStateStore } = fileBridgeStateStoreModule;
|
|
18
|
+
function createBridgeRuntimeService(dependencies) {
|
|
19
|
+
const stateStore = new FileBridgeStateStore(dependencies.config.stateRoot);
|
|
20
|
+
const sessionBindingStore = dependencies.sessionBindingStore ?? stateStore;
|
|
21
|
+
const emitLog = dependencies.onLog ?? defaultLogEvent;
|
|
22
|
+
const transport = dependencies.transport ??
|
|
23
|
+
new WebProviderTransport({
|
|
24
|
+
providerSessionResolver: createBridgeProviderSessionResolver({
|
|
25
|
+
sessionPackageStore: dependencies.sessionPackageStore,
|
|
26
|
+
stateStore
|
|
27
|
+
}),
|
|
28
|
+
loadProvider: dependencies.loadProvider
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
async respond(request, pathSessionId) {
|
|
32
|
+
const normalized = normalizeBridgeRequest(request, pathSessionId, dependencies.config, dependencies.loadProvider);
|
|
33
|
+
const sessionHistory = await stateStore.loadSessionHistory(normalized.sessionId);
|
|
34
|
+
return executeBridgeRequest({
|
|
35
|
+
...normalized,
|
|
36
|
+
sessionHistory,
|
|
37
|
+
persistSession: true
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
async execute(request) {
|
|
41
|
+
return executeBridgeRequest({
|
|
42
|
+
sessionId: request.sessionId,
|
|
43
|
+
input: request.input,
|
|
44
|
+
providerId: request.providerId,
|
|
45
|
+
modelId: request.modelId,
|
|
46
|
+
metadata: request.metadata,
|
|
47
|
+
toolProfile: request.toolProfile ?? "default",
|
|
48
|
+
sessionHistory: request.sessionHistory ?? [],
|
|
49
|
+
persistSession: request.persistSession ?? false
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
async completeChatCompletionPacket(request) {
|
|
53
|
+
const bindingStore = request.persistSession
|
|
54
|
+
? sessionBindingStore
|
|
55
|
+
: createInMemorySessionBindingStore();
|
|
56
|
+
const providerBindingBefore = await bindingStore.loadBinding(request.providerId, request.sessionId);
|
|
57
|
+
try {
|
|
58
|
+
const completion = await completeValidatedChatCompletionPacket(request, providerBindingBefore, {
|
|
59
|
+
mode: "complete",
|
|
60
|
+
transport
|
|
61
|
+
});
|
|
62
|
+
if (completion.nextBinding) {
|
|
63
|
+
await bindingStore.saveBinding(request.providerId, request.sessionId, completion.nextBinding);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
packet: completion.packet,
|
|
67
|
+
providerBindingReused: providerBindingBefore !== null
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw classifyChatCompletionPacketFailure(error, request, providerBindingBefore !== null);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async streamChatCompletionPacket(request) {
|
|
75
|
+
const bindingStore = request.persistSession
|
|
76
|
+
? sessionBindingStore
|
|
77
|
+
: createInMemorySessionBindingStore();
|
|
78
|
+
const providerBindingBefore = await bindingStore.loadBinding(request.providerId, request.sessionId);
|
|
79
|
+
const providerBindingReused = providerBindingBefore !== null;
|
|
80
|
+
try {
|
|
81
|
+
const completion = await completeValidatedChatCompletionPacket(request, providerBindingBefore, {
|
|
82
|
+
mode: isStreamingProviderTransport(transport) ? "stream" : "complete",
|
|
83
|
+
transport
|
|
84
|
+
});
|
|
85
|
+
if (completion.nextBinding) {
|
|
86
|
+
await bindingStore.saveBinding(request.providerId, request.sessionId, completion.nextBinding);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
providerBindingReused,
|
|
90
|
+
packet: Promise.resolve(completion.packet),
|
|
91
|
+
content: singleProviderFragmentStream(serializeChatCompletionPacket(completion.packet))
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw classifyChatCompletionPacketFailure(error, request, providerBindingReused);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
async streamChatCompletion(request) {
|
|
99
|
+
const normalized = {
|
|
100
|
+
sessionId: request.sessionId,
|
|
101
|
+
input: request.input,
|
|
102
|
+
providerId: request.providerId,
|
|
103
|
+
modelId: request.modelId,
|
|
104
|
+
metadata: request.metadata,
|
|
105
|
+
toolProfile: request.toolProfile ?? "default",
|
|
106
|
+
sessionHistory: request.sessionHistory ?? [],
|
|
107
|
+
persistSession: request.persistSession ?? false
|
|
108
|
+
};
|
|
109
|
+
if (isStreamingProviderTransport(transport)) {
|
|
110
|
+
const bindingStore = normalized.persistSession
|
|
111
|
+
? sessionBindingStore
|
|
112
|
+
: createInMemorySessionBindingStore();
|
|
113
|
+
try {
|
|
114
|
+
const providerBindingBefore = await bindingStore.loadBinding(normalized.providerId, normalized.sessionId);
|
|
115
|
+
const toolExecutor = new InProcessToolExecutor({
|
|
116
|
+
tools: createToolSet(normalized.toolProfile, dependencies.config.runtimeRoot)
|
|
117
|
+
});
|
|
118
|
+
const availableTools = await toolExecutor.getAvailableTools();
|
|
119
|
+
const compiled = compileProviderTurn({
|
|
120
|
+
conversation: {
|
|
121
|
+
sessionHistory: normalized.sessionHistory,
|
|
122
|
+
entries: [
|
|
123
|
+
{
|
|
124
|
+
type: "user_message",
|
|
125
|
+
content: normalized.input
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
availableTools,
|
|
130
|
+
runtimePlannerPrimed: providerBindingBefore?.runtimePlannerPrimed === true,
|
|
131
|
+
forceReplay: false
|
|
132
|
+
});
|
|
133
|
+
const stream = await transport.streamChat({
|
|
134
|
+
lane: "main",
|
|
135
|
+
providerId: normalized.providerId,
|
|
136
|
+
modelId: normalized.modelId,
|
|
137
|
+
sessionId: normalized.sessionId,
|
|
138
|
+
requestId: crypto.randomUUID(),
|
|
139
|
+
attempt: 1,
|
|
140
|
+
continuation: compiled.summary.turnType === "follow_up",
|
|
141
|
+
toolFollowUp: false,
|
|
142
|
+
providerSessionReused: providerBindingBefore !== null,
|
|
143
|
+
messages: compiled.messages,
|
|
144
|
+
upstreamBinding: providerBindingBefore
|
|
145
|
+
? {
|
|
146
|
+
conversationId: providerBindingBefore.conversationId,
|
|
147
|
+
parentId: providerBindingBefore.parentId
|
|
148
|
+
}
|
|
149
|
+
: null
|
|
150
|
+
});
|
|
151
|
+
return (async function* () {
|
|
152
|
+
let rawOutput = "";
|
|
153
|
+
let emittedOutput = "";
|
|
154
|
+
for await (const chunk of stream.content) {
|
|
155
|
+
rawOutput += chunk.content;
|
|
156
|
+
const visibleContent = extractIncrementalPacketMessage(rawOutput);
|
|
157
|
+
if (visibleContent.startsWith(emittedOutput) &&
|
|
158
|
+
visibleContent.length > emittedOutput.length) {
|
|
159
|
+
const delta = visibleContent.slice(emittedOutput.length);
|
|
160
|
+
emittedOutput = visibleContent;
|
|
161
|
+
yield delta;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const upstreamBinding = await stream.upstreamBinding;
|
|
165
|
+
if (upstreamBinding) {
|
|
166
|
+
await bindingStore.saveBinding(normalized.providerId, normalized.sessionId, {
|
|
167
|
+
...upstreamBinding,
|
|
168
|
+
runtimePlannerPrimed: Boolean(upstreamBinding.parentId)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const output = sanitizeBridgeApiOutput(rawOutput).content;
|
|
172
|
+
if (output.startsWith(emittedOutput) && output.length > emittedOutput.length) {
|
|
173
|
+
yield output.slice(emittedOutput.length);
|
|
174
|
+
}
|
|
175
|
+
else if (!emittedOutput && output) {
|
|
176
|
+
yield output;
|
|
177
|
+
}
|
|
178
|
+
if (normalized.persistSession && output) {
|
|
179
|
+
await stateStore.appendSessionTurn(normalized.sessionId, {
|
|
180
|
+
userMessage: normalized.input,
|
|
181
|
+
assistantMessage: output,
|
|
182
|
+
assistantMode: "final"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw classifyProviderFailure(serializeProviderFailure(classifyProviderTransportError(error)), {
|
|
189
|
+
sessionId: normalized.sessionId,
|
|
190
|
+
provider: {
|
|
191
|
+
id: normalized.providerId,
|
|
192
|
+
model: normalized.modelId
|
|
193
|
+
},
|
|
194
|
+
steps: 1,
|
|
195
|
+
tools: {
|
|
196
|
+
used: [],
|
|
197
|
+
calls: []
|
|
198
|
+
},
|
|
199
|
+
recovery: {
|
|
200
|
+
softRetryCount: 0,
|
|
201
|
+
providerSessionResetCount: 0
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const response = await executeBridgeRequest(normalized);
|
|
207
|
+
return singleChunkStream(response.output);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
async function executeBridgeRequest(request) {
|
|
211
|
+
const requestId = crypto.randomUUID();
|
|
212
|
+
const bindingStore = request.persistSession
|
|
213
|
+
? sessionBindingStore
|
|
214
|
+
: createInMemorySessionBindingStore();
|
|
215
|
+
const providerBindingBefore = await bindingStore.loadBinding(request.providerId, request.sessionId);
|
|
216
|
+
const recoverySummary = {
|
|
217
|
+
softRetryCount: 0,
|
|
218
|
+
providerSessionResetCount: 0,
|
|
219
|
+
repair: createInitialRepairRecoverySummary()
|
|
220
|
+
};
|
|
221
|
+
emitLog({
|
|
222
|
+
scope: "request",
|
|
223
|
+
event: "bridge_request_started",
|
|
224
|
+
requestId,
|
|
225
|
+
detail: {
|
|
226
|
+
bridgeSessionId: request.sessionId,
|
|
227
|
+
providerId: request.providerId,
|
|
228
|
+
modelId: request.modelId,
|
|
229
|
+
sessionHistoryTurns: request.sessionHistory.length,
|
|
230
|
+
providerBindingReused: providerBindingBefore !== null
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
const provider = new SessionBoundProviderAdapter({
|
|
234
|
+
providerId: request.providerId,
|
|
235
|
+
modelId: request.modelId,
|
|
236
|
+
sessionId: request.sessionId,
|
|
237
|
+
bridgeRequestId: requestId,
|
|
238
|
+
sessionBindingStore: bindingStore,
|
|
239
|
+
transport,
|
|
240
|
+
onTraceEvent(type, detail) {
|
|
241
|
+
const record = asRecord(detail);
|
|
242
|
+
if (record.outcome === "soft_retry") {
|
|
243
|
+
recoverySummary.softRetryCount += 1;
|
|
244
|
+
}
|
|
245
|
+
if (type === "provider_session_reset") {
|
|
246
|
+
recoverySummary.providerSessionResetCount += 1;
|
|
247
|
+
}
|
|
248
|
+
emitLog({
|
|
249
|
+
scope: "provider",
|
|
250
|
+
event: type,
|
|
251
|
+
requestId,
|
|
252
|
+
detail: record
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const toolExecutor = new InProcessToolExecutor({
|
|
257
|
+
tools: createToolSet(request.toolProfile, dependencies.config.runtimeRoot)
|
|
258
|
+
});
|
|
259
|
+
const outcome = await runBridgeRuntime({
|
|
260
|
+
userMessage: request.input,
|
|
261
|
+
sessionHistory: request.sessionHistory,
|
|
262
|
+
provider,
|
|
263
|
+
toolExecutor,
|
|
264
|
+
config: {
|
|
265
|
+
maxSteps: dependencies.config.maxSteps,
|
|
266
|
+
onEvent(event) {
|
|
267
|
+
updateRepairRecoverySummary(recoverySummary.repair, event);
|
|
268
|
+
emitLog({
|
|
269
|
+
scope: "runtime",
|
|
270
|
+
event: event.type,
|
|
271
|
+
requestId,
|
|
272
|
+
detail: summarizeRuntimeEvent(event)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
emitLog({
|
|
278
|
+
scope: "request",
|
|
279
|
+
event: "bridge_request_finished",
|
|
280
|
+
requestId,
|
|
281
|
+
detail: {
|
|
282
|
+
bridgeSessionId: request.sessionId,
|
|
283
|
+
providerId: request.providerId,
|
|
284
|
+
modelId: request.modelId,
|
|
285
|
+
outcomeMode: outcome.mode,
|
|
286
|
+
steps: outcome.steps,
|
|
287
|
+
recovery: recoverySummary
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
if (outcome.mode === "fail") {
|
|
291
|
+
throw classifyRuntimeFailure(request.sessionId, request.providerId, request.modelId, outcome, recoverySummary);
|
|
292
|
+
}
|
|
293
|
+
if (request.persistSession) {
|
|
294
|
+
await stateStore.appendSessionTurn(request.sessionId, {
|
|
295
|
+
userMessage: request.input,
|
|
296
|
+
assistantMessage: outcome.message,
|
|
297
|
+
assistantMode: outcome.mode
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const successfulOutcome = outcome;
|
|
301
|
+
return buildBridgeMessageResponse({
|
|
302
|
+
sessionId: request.sessionId,
|
|
303
|
+
providerId: request.providerId,
|
|
304
|
+
modelId: request.modelId,
|
|
305
|
+
metadata: request.metadata
|
|
306
|
+
}, successfulOutcome, providerBindingBefore !== null, recoverySummary);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function isStreamingProviderTransport(transport) {
|
|
310
|
+
return "streamChat" in transport && typeof transport.streamChat === "function";
|
|
311
|
+
}
|
|
312
|
+
async function* singleChunkStream(content) {
|
|
313
|
+
if (content) {
|
|
314
|
+
yield content;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function* singleProviderFragmentStream(content) {
|
|
318
|
+
if (content) {
|
|
319
|
+
yield {
|
|
320
|
+
content,
|
|
321
|
+
responseId: "",
|
|
322
|
+
conversationId: "",
|
|
323
|
+
eventCountDelta: 0,
|
|
324
|
+
fragmentCountDelta: 1
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function serializeChatCompletionPacket(packet) {
|
|
329
|
+
return serializeAssistantResponse(packet);
|
|
330
|
+
}
|
|
331
|
+
async function completeValidatedChatCompletionPacket(request, providerBindingBefore, options) {
|
|
332
|
+
let binding = providerBindingBefore;
|
|
333
|
+
let currentRequest = request;
|
|
334
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
335
|
+
const response = await executeChatCompletionTransportAttempt(currentRequest, binding, attempt, attempt === 1 ? options.mode : "complete", options.transport);
|
|
336
|
+
const nextBinding = response.upstreamBinding
|
|
337
|
+
? {
|
|
338
|
+
...response.upstreamBinding,
|
|
339
|
+
runtimePlannerPrimed: binding?.runtimePlannerPrimed
|
|
340
|
+
}
|
|
341
|
+
: binding;
|
|
342
|
+
try {
|
|
343
|
+
return {
|
|
344
|
+
packet: validateChatCompletionPacket(response.content, currentRequest.providerId, currentRequest.toolFollowUp, currentRequest.tools, currentRequest.toolChoice),
|
|
345
|
+
nextBinding
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
const repairHint = buildChatCompletionRepairHint(error);
|
|
350
|
+
if (!repairHint || attempt === 3) {
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
currentRequest = {
|
|
354
|
+
...request,
|
|
355
|
+
messages: buildChatCompletionRepairMessages(request.messages, repairHint)
|
|
356
|
+
};
|
|
357
|
+
binding = nextBinding;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
throw new Error("unreachable");
|
|
361
|
+
}
|
|
362
|
+
async function executeChatCompletionTransportAttempt(request, upstreamBinding, attempt, mode, transport) {
|
|
363
|
+
const baseRequest = {
|
|
364
|
+
lane: "main",
|
|
365
|
+
providerId: request.providerId,
|
|
366
|
+
modelId: request.modelId,
|
|
367
|
+
sessionId: request.sessionId,
|
|
368
|
+
requestId: crypto.randomUUID(),
|
|
369
|
+
attempt,
|
|
370
|
+
continuation: request.continuation,
|
|
371
|
+
toolFollowUp: request.toolFollowUp,
|
|
372
|
+
providerSessionReused: upstreamBinding !== null,
|
|
373
|
+
messages: request.messages,
|
|
374
|
+
upstreamBinding: upstreamBinding
|
|
375
|
+
? {
|
|
376
|
+
conversationId: upstreamBinding.conversationId,
|
|
377
|
+
parentId: upstreamBinding.parentId
|
|
378
|
+
}
|
|
379
|
+
: null
|
|
380
|
+
};
|
|
381
|
+
if (mode === "stream" && isStreamingProviderTransport(transport)) {
|
|
382
|
+
const stream = await transport.streamChat(baseRequest);
|
|
383
|
+
let content = "";
|
|
384
|
+
for await (const chunk of stream.content) {
|
|
385
|
+
content += chunk.content;
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
content,
|
|
389
|
+
upstreamBinding: await stream.upstreamBinding
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return await transport.completeChat(baseRequest);
|
|
393
|
+
}
|
|
394
|
+
function normalizeBridgeRequest(request, pathSessionId, config, loadProvider) {
|
|
395
|
+
if (!isRecord(request)) {
|
|
396
|
+
throw new BridgeApiError({
|
|
397
|
+
statusCode: 400,
|
|
398
|
+
code: "invalid_request",
|
|
399
|
+
message: "Request body must be a JSON object."
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const bodySessionId = optionalTrimmedString(request.sessionId, "sessionId");
|
|
403
|
+
if (pathSessionId && bodySessionId && pathSessionId !== bodySessionId) {
|
|
404
|
+
throw new BridgeApiError({
|
|
405
|
+
statusCode: 400,
|
|
406
|
+
code: "invalid_request",
|
|
407
|
+
message: "sessionId in the request body must match the sessionId path parameter."
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const sessionId = pathSessionId ?? bodySessionId;
|
|
411
|
+
if (!sessionId) {
|
|
412
|
+
throw new BridgeApiError({
|
|
413
|
+
statusCode: 400,
|
|
414
|
+
code: "invalid_request",
|
|
415
|
+
message: "sessionId is required."
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
const input = resolveInput(request);
|
|
419
|
+
const providerId = optionalTrimmedString(request.provider, "provider") ?? config.defaultProvider;
|
|
420
|
+
if (!providerId) {
|
|
421
|
+
throw new BridgeApiError({
|
|
422
|
+
statusCode: 400,
|
|
423
|
+
code: "provider_required",
|
|
424
|
+
message: "provider is required when BRIDGE_PROVIDER is not configured."
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const modelId = optionalTrimmedString(request.model, "model") ??
|
|
428
|
+
config.defaultModel ??
|
|
429
|
+
defaultModelForProvider(loadProvider?.(providerId) ?? null);
|
|
430
|
+
if (!modelId) {
|
|
431
|
+
throw new BridgeApiError({
|
|
432
|
+
statusCode: 400,
|
|
433
|
+
code: "model_required",
|
|
434
|
+
message: "model is required when BRIDGE_MODEL is not configured."
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const metadata = normalizeMetadata(request.metadata);
|
|
438
|
+
const toolProfile = normalizeToolProfile(request.toolProfile);
|
|
439
|
+
return {
|
|
440
|
+
sessionId,
|
|
441
|
+
input,
|
|
442
|
+
providerId,
|
|
443
|
+
modelId,
|
|
444
|
+
metadata,
|
|
445
|
+
toolProfile
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function resolveInput(request) {
|
|
449
|
+
const input = optionalTrimmedString(request.input, "input");
|
|
450
|
+
const message = optionalTrimmedString(request.message, "message");
|
|
451
|
+
if (input && message && input !== message) {
|
|
452
|
+
throw new BridgeApiError({
|
|
453
|
+
statusCode: 400,
|
|
454
|
+
code: "invalid_request",
|
|
455
|
+
message: "input and message must match when both are provided."
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const resolved = input ?? message;
|
|
459
|
+
if (!resolved) {
|
|
460
|
+
throw new BridgeApiError({
|
|
461
|
+
statusCode: 400,
|
|
462
|
+
code: "invalid_request",
|
|
463
|
+
message: "input is required."
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return resolved;
|
|
467
|
+
}
|
|
468
|
+
function normalizeMetadata(value) {
|
|
469
|
+
if (value === undefined) {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
if (isRecord(value)) {
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
throw new BridgeApiError({
|
|
476
|
+
statusCode: 400,
|
|
477
|
+
code: "invalid_request",
|
|
478
|
+
message: "metadata must be a JSON object."
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function normalizeToolProfile(value) {
|
|
482
|
+
if (value === undefined) {
|
|
483
|
+
return "default";
|
|
484
|
+
}
|
|
485
|
+
if (value === "default" || value === "workspace") {
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
throw new BridgeApiError({
|
|
489
|
+
statusCode: 400,
|
|
490
|
+
code: "invalid_request",
|
|
491
|
+
message: 'toolProfile must be either "default" or "workspace".'
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function createToolSet(toolProfile, runtimeRoot) {
|
|
495
|
+
const defaultTools = createDefaultRuntimeTools(runtimeRoot);
|
|
496
|
+
if (toolProfile !== "workspace") {
|
|
497
|
+
return defaultTools;
|
|
498
|
+
}
|
|
499
|
+
return [...defaultTools, ...createSecondaryRuntimeTools(runtimeRoot)];
|
|
500
|
+
}
|
|
501
|
+
function classifyRuntimeFailure(sessionId, providerId, modelId, outcome, recoverySummary) {
|
|
502
|
+
const toolCalls = outcome.conversation.entries
|
|
503
|
+
.filter((entry) => entry.type === "tool_result")
|
|
504
|
+
.map((entry) => ({
|
|
505
|
+
id: entry.result.id,
|
|
506
|
+
name: entry.result.name,
|
|
507
|
+
ok: entry.result.ok
|
|
508
|
+
}));
|
|
509
|
+
const details = {
|
|
510
|
+
sessionId,
|
|
511
|
+
provider: {
|
|
512
|
+
id: providerId,
|
|
513
|
+
model: modelId
|
|
514
|
+
},
|
|
515
|
+
steps: outcome.steps,
|
|
516
|
+
tools: {
|
|
517
|
+
used: [...new Set(toolCalls.map((call) => call.name))],
|
|
518
|
+
calls: toolCalls
|
|
519
|
+
},
|
|
520
|
+
recovery: recoverySummary
|
|
521
|
+
};
|
|
522
|
+
if (outcome.failure?.source === "provider") {
|
|
523
|
+
return classifyProviderFailure(outcome.failure.provider, details);
|
|
524
|
+
}
|
|
525
|
+
if (outcome.failure?.source === "protocol") {
|
|
526
|
+
return new BridgeApiError({
|
|
527
|
+
statusCode: 502,
|
|
528
|
+
code: "provider_protocol_failure",
|
|
529
|
+
message: outcome.failure.message,
|
|
530
|
+
details: {
|
|
531
|
+
...details,
|
|
532
|
+
failure: {
|
|
533
|
+
source: "protocol",
|
|
534
|
+
code: outcome.failure.code
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (outcome.failure?.source === "runtime" && outcome.failure.code === "max_steps_exhausted") {
|
|
540
|
+
return new BridgeApiError({
|
|
541
|
+
statusCode: 502,
|
|
542
|
+
code: "runtime_exhausted",
|
|
543
|
+
message: outcome.failure.message,
|
|
544
|
+
details: {
|
|
545
|
+
...details,
|
|
546
|
+
failure: {
|
|
547
|
+
source: "runtime",
|
|
548
|
+
code: outcome.failure.code
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
if (toolCalls.some((call) => call.ok === false)) {
|
|
554
|
+
return new BridgeApiError({
|
|
555
|
+
statusCode: 502,
|
|
556
|
+
code: "tool_failure",
|
|
557
|
+
message: outcome.message,
|
|
558
|
+
details
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return new BridgeApiError({
|
|
562
|
+
statusCode: 502,
|
|
563
|
+
code: "runtime_failure",
|
|
564
|
+
message: outcome.message,
|
|
565
|
+
details
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function buildBridgeMessageResponse(request, outcome, providerBindingReused, recoverySummary) {
|
|
569
|
+
const sanitized = sanitizeBridgeApiOutput(outcome.message);
|
|
570
|
+
const toolCalls = outcome.conversation.entries
|
|
571
|
+
.filter((entry) => entry.type === "tool_result")
|
|
572
|
+
.map((entry) => ({
|
|
573
|
+
id: entry.result.id,
|
|
574
|
+
name: entry.result.name,
|
|
575
|
+
ok: entry.result.ok,
|
|
576
|
+
payload: entry.result.payload
|
|
577
|
+
}));
|
|
578
|
+
const lastSuccessfulCall = [...toolCalls].reverse().find((call) => call.ok);
|
|
579
|
+
return {
|
|
580
|
+
sessionId: request.sessionId,
|
|
581
|
+
output: sanitized.content,
|
|
582
|
+
outcome: {
|
|
583
|
+
mode: outcome.mode,
|
|
584
|
+
steps: outcome.steps
|
|
585
|
+
},
|
|
586
|
+
provider: {
|
|
587
|
+
id: request.providerId,
|
|
588
|
+
model: request.modelId
|
|
589
|
+
},
|
|
590
|
+
session: {
|
|
591
|
+
providerBindingReused
|
|
592
|
+
},
|
|
593
|
+
tools: {
|
|
594
|
+
used: [...new Set(toolCalls.map((call) => call.name))],
|
|
595
|
+
calls: toolCalls.map((call) => ({
|
|
596
|
+
id: call.id,
|
|
597
|
+
name: call.name,
|
|
598
|
+
ok: call.ok
|
|
599
|
+
})),
|
|
600
|
+
lastSuccessfulCall: lastSuccessfulCall
|
|
601
|
+
? {
|
|
602
|
+
id: lastSuccessfulCall.id,
|
|
603
|
+
name: lastSuccessfulCall.name,
|
|
604
|
+
payload: lastSuccessfulCall.payload
|
|
605
|
+
}
|
|
606
|
+
: undefined
|
|
607
|
+
},
|
|
608
|
+
meta: {
|
|
609
|
+
outputSanitized: sanitized.sanitized,
|
|
610
|
+
sanitizationReason: sanitized.sanitized ? sanitized.reason : undefined,
|
|
611
|
+
requestMetadata: request.metadata,
|
|
612
|
+
recovery: recoverySummary
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function classifyProviderFailure(failure, details) {
|
|
617
|
+
const errorDetails = {
|
|
618
|
+
...details,
|
|
619
|
+
failure
|
|
620
|
+
};
|
|
621
|
+
const safeFailureMessage = formatSafeProviderFailureMessage(failure);
|
|
622
|
+
switch (failure.code) {
|
|
623
|
+
case "transport_timeout":
|
|
624
|
+
return new BridgeApiError({
|
|
625
|
+
statusCode: 504,
|
|
626
|
+
code: "provider_timeout",
|
|
627
|
+
message: safeFailureMessage,
|
|
628
|
+
details: errorDetails
|
|
629
|
+
});
|
|
630
|
+
case "empty_response":
|
|
631
|
+
case "empty_extracted_response":
|
|
632
|
+
case "empty_final_message":
|
|
633
|
+
return new BridgeApiError({
|
|
634
|
+
statusCode: 502,
|
|
635
|
+
code: "provider_empty_response",
|
|
636
|
+
message: safeFailureMessage,
|
|
637
|
+
details: errorDetails
|
|
638
|
+
});
|
|
639
|
+
case "packet_extraction_failed":
|
|
640
|
+
case "packet_normalization_failed":
|
|
641
|
+
case "packet_validation_failed":
|
|
642
|
+
return new BridgeApiError({
|
|
643
|
+
statusCode: 502,
|
|
644
|
+
code: "provider_protocol_failure",
|
|
645
|
+
message: safeFailureMessage,
|
|
646
|
+
details: errorDetails
|
|
647
|
+
});
|
|
648
|
+
case "authentication_failed":
|
|
649
|
+
return new BridgeApiError({
|
|
650
|
+
statusCode: 502,
|
|
651
|
+
code: "provider_auth_failure",
|
|
652
|
+
message: safeFailureMessage,
|
|
653
|
+
details: errorDetails
|
|
654
|
+
});
|
|
655
|
+
case "request_invalid":
|
|
656
|
+
case "unsupported_request":
|
|
657
|
+
return new BridgeApiError({
|
|
658
|
+
statusCode: 400,
|
|
659
|
+
code: "provider_request_failure",
|
|
660
|
+
message: safeFailureMessage,
|
|
661
|
+
details: errorDetails
|
|
662
|
+
});
|
|
663
|
+
case "session_reset_failed":
|
|
664
|
+
return new BridgeApiError({
|
|
665
|
+
statusCode: 502,
|
|
666
|
+
code: "provider_session_reset_failed",
|
|
667
|
+
message: safeFailureMessage,
|
|
668
|
+
details: errorDetails
|
|
669
|
+
});
|
|
670
|
+
case "transport_error":
|
|
671
|
+
default:
|
|
672
|
+
return new BridgeApiError({
|
|
673
|
+
statusCode: 502,
|
|
674
|
+
code: "provider_failure",
|
|
675
|
+
message: safeFailureMessage,
|
|
676
|
+
details: errorDetails
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function formatSafeProviderFailureMessage(failure) {
|
|
681
|
+
if (failure.code === "transport_error") {
|
|
682
|
+
const details = failure.details;
|
|
683
|
+
if (details &&
|
|
684
|
+
typeof details === "object" &&
|
|
685
|
+
!Array.isArray(details) &&
|
|
686
|
+
typeof details.stage === "string" &&
|
|
687
|
+
typeof details.httpStatus === "number") {
|
|
688
|
+
return `Provider request failed during ${details.stage} with HTTP ${details.httpStatus}${formatProviderRecoverySummary(failure.recovery)}.`;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return failure.message;
|
|
692
|
+
}
|
|
693
|
+
function formatProviderRecoverySummary(recovery) {
|
|
694
|
+
const parts = [];
|
|
695
|
+
if (recovery.softRetryCount > 0) {
|
|
696
|
+
parts.push(`${recovery.softRetryCount} soft retr${recovery.softRetryCount === 1 ? "y" : "ies"}`);
|
|
697
|
+
}
|
|
698
|
+
if (recovery.sessionResetCount > 0) {
|
|
699
|
+
parts.push(`${recovery.sessionResetCount} provider-session reset${recovery.sessionResetCount === 1 ? "" : "s"}`);
|
|
700
|
+
}
|
|
701
|
+
if (parts.length === 0) {
|
|
702
|
+
return "";
|
|
703
|
+
}
|
|
704
|
+
return ` after ${parts.join(" and ")}`;
|
|
705
|
+
}
|
|
706
|
+
function validateChatCompletionPacket(content, providerId, allowVisibleTextFinal, tools, toolChoice) {
|
|
707
|
+
if (!content.trim()) {
|
|
708
|
+
throw new ProviderFailure({
|
|
709
|
+
kind: "transient",
|
|
710
|
+
code: "empty_response",
|
|
711
|
+
message: "Provider returned an empty response.",
|
|
712
|
+
retryable: true,
|
|
713
|
+
sessionResetEligible: false,
|
|
714
|
+
emptyOutput: true
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const packet = parseToolAwareAssistantResponse(providerId, content, allowVisibleTextFinal);
|
|
719
|
+
validateChatCompletionAssistantResponse(packet, tools, toolChoice);
|
|
720
|
+
return packet;
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (error instanceof ProviderFailure) {
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
throw new ProviderFailure({
|
|
727
|
+
kind: "protocol",
|
|
728
|
+
code: "packet_validation_failed",
|
|
729
|
+
message: `Invalid assistant response: ${error instanceof Error ? error.message : String(error)}`,
|
|
730
|
+
displayMessage: "Provider returned malformed or unusable output.",
|
|
731
|
+
retryable: false,
|
|
732
|
+
sessionResetEligible: false,
|
|
733
|
+
cause: error
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function parseToolAwareAssistantResponse(providerId, content, allowVisibleTextFinal) {
|
|
738
|
+
const repairedSimplePacket = repairMalformedSimpleAssistantPacket(content);
|
|
739
|
+
try {
|
|
740
|
+
return normalizeAssistantResponseToolNames(parseAssistantResponse(repairedSimplePacket));
|
|
741
|
+
}
|
|
742
|
+
catch (assistantProtocolError) {
|
|
743
|
+
const canonicalPacket = coerceMalformedCanonicalPacket(content) ??
|
|
744
|
+
coerceToolAwareProviderOutput(content) ??
|
|
745
|
+
(allowVisibleTextFinal ? wrapVisibleTextAsFinalPacket(content) : null) ??
|
|
746
|
+
normalizeExtractedProviderPacket(providerId, content);
|
|
747
|
+
if (!canonicalPacket) {
|
|
748
|
+
throw assistantProtocolError;
|
|
749
|
+
}
|
|
750
|
+
const packet = parseZcPacket(canonicalPacket);
|
|
751
|
+
switch (packet.mode) {
|
|
752
|
+
case "final":
|
|
753
|
+
return {
|
|
754
|
+
type: "final",
|
|
755
|
+
message: packet.message
|
|
756
|
+
};
|
|
757
|
+
case "tool_request":
|
|
758
|
+
return {
|
|
759
|
+
type: "tool",
|
|
760
|
+
toolCall: {
|
|
761
|
+
id: packet.toolCall.id,
|
|
762
|
+
name: normalizeProviderToolName(packet.toolCall.name),
|
|
763
|
+
arguments: packet.toolCall.args
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
case "ask_user":
|
|
767
|
+
case "fail":
|
|
768
|
+
throw new Error(`Packet mode "${packet.mode}" is not supported for OpenAI-style chat completions.`);
|
|
769
|
+
default:
|
|
770
|
+
throw new Error("Unsupported packet mode.");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function normalizeAssistantResponseToolNames(packet) {
|
|
775
|
+
if (packet.type !== "tool") {
|
|
776
|
+
return packet;
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
...packet,
|
|
780
|
+
toolCall: {
|
|
781
|
+
...packet.toolCall,
|
|
782
|
+
name: normalizeProviderToolName(packet.toolCall.name)
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function repairMalformedSimpleAssistantPacket(content) {
|
|
787
|
+
const trimmed = content.trim();
|
|
788
|
+
if (/^<tool>[\s\S]*<\/tool_call>$/u.test(trimmed) && !trimmed.includes("</tool>")) {
|
|
789
|
+
return trimmed.replace(/<\/tool_call>$/u, "</tool>");
|
|
790
|
+
}
|
|
791
|
+
const repairedLeadingBlock = extractLeadingSimpleFinalBlock(trimmed);
|
|
792
|
+
if (repairedLeadingBlock) {
|
|
793
|
+
return repairedLeadingBlock;
|
|
794
|
+
}
|
|
795
|
+
return trimmed;
|
|
796
|
+
}
|
|
797
|
+
function extractLeadingSimpleFinalBlock(content) {
|
|
798
|
+
const openTag = "<final>";
|
|
799
|
+
const closeTag = "</final>";
|
|
800
|
+
if (!content.startsWith(openTag)) {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
const closeIndex = content.indexOf(closeTag);
|
|
804
|
+
if (closeIndex < 0) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
const block = content.slice(0, closeIndex + closeTag.length);
|
|
808
|
+
const trailing = content.slice(closeIndex + closeTag.length).trim();
|
|
809
|
+
if (!trailing) {
|
|
810
|
+
return block;
|
|
811
|
+
}
|
|
812
|
+
return trailing.startsWith("<") ? null : block;
|
|
813
|
+
}
|
|
814
|
+
function validateChatCompletionAssistantResponse(packet, tools, toolChoice) {
|
|
815
|
+
if (packet.type === "final") {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (toolChoice === "none") {
|
|
819
|
+
throw new ProviderFailure({
|
|
820
|
+
kind: "permanent",
|
|
821
|
+
code: "unsupported_request",
|
|
822
|
+
message: "Provider requested a tool call even though tool calls are disabled for this request.",
|
|
823
|
+
displayMessage: "Provider requested a disabled tool call.",
|
|
824
|
+
retryable: false,
|
|
825
|
+
sessionResetEligible: false,
|
|
826
|
+
details: {
|
|
827
|
+
toolName: packet.toolCall.name
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
if (typeof toolChoice === "object" && packet.toolCall.name !== toolChoice.function.name) {
|
|
832
|
+
throw new ProviderFailure({
|
|
833
|
+
kind: "permanent",
|
|
834
|
+
code: "request_invalid",
|
|
835
|
+
message: `Provider requested tool "${packet.toolCall.name}" but only "${toolChoice.function.name}" is allowed for this request.`,
|
|
836
|
+
displayMessage: `Provider requested unavailable tool "${packet.toolCall.name}".`,
|
|
837
|
+
retryable: false,
|
|
838
|
+
sessionResetEligible: false,
|
|
839
|
+
details: {
|
|
840
|
+
toolName: packet.toolCall.name,
|
|
841
|
+
allowedToolNames: [toolChoice.function.name]
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
const tool = tools.find((candidate) => candidate.function.name === packet.toolCall.name);
|
|
846
|
+
if (!tool) {
|
|
847
|
+
throw new ProviderFailure({
|
|
848
|
+
kind: "permanent",
|
|
849
|
+
code: "request_invalid",
|
|
850
|
+
message: `Provider requested tool "${packet.toolCall.name}" but it is not present in the bridge tool manifest.`,
|
|
851
|
+
displayMessage: `Provider requested unavailable tool "${packet.toolCall.name}".`,
|
|
852
|
+
retryable: false,
|
|
853
|
+
sessionResetEligible: false,
|
|
854
|
+
details: {
|
|
855
|
+
toolName: packet.toolCall.name,
|
|
856
|
+
availableToolNames: tools.map((candidate) => candidate.function.name)
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
const schema = isRecord(tool.function.parameters) ? tool.function.parameters : null;
|
|
861
|
+
if (!schema) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const validationError = validateJsonSchemaValue(packet.toolCall.arguments, schema, "arguments");
|
|
865
|
+
if (validationError) {
|
|
866
|
+
throw new Error(validationError);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function buildChatCompletionRepairHint(error) {
|
|
870
|
+
if (!(error instanceof ProviderFailure)) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
if (error.code !== "packet_validation_failed") {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
return [
|
|
877
|
+
"Protocol error.",
|
|
878
|
+
"",
|
|
879
|
+
"You must answer using exactly one of these formats:",
|
|
880
|
+
"",
|
|
881
|
+
"<final>",
|
|
882
|
+
"your text here",
|
|
883
|
+
"</final>",
|
|
884
|
+
"",
|
|
885
|
+
"<tool>",
|
|
886
|
+
'{"name":"tool_name","arguments":{}}',
|
|
887
|
+
"</tool>",
|
|
888
|
+
"",
|
|
889
|
+
"Rules:",
|
|
890
|
+
"- no markdown",
|
|
891
|
+
"- no backticks",
|
|
892
|
+
"- no extra text",
|
|
893
|
+
"- exactly one block only",
|
|
894
|
+
"- if using <tool>, arguments must be valid JSON",
|
|
895
|
+
"",
|
|
896
|
+
"Re-emit your previous intent now."
|
|
897
|
+
].join(" ");
|
|
898
|
+
}
|
|
899
|
+
function buildChatCompletionRepairMessages(messages, repairHint) {
|
|
900
|
+
return [
|
|
901
|
+
...messages,
|
|
902
|
+
{
|
|
903
|
+
role: "user",
|
|
904
|
+
content: repairHint
|
|
905
|
+
}
|
|
906
|
+
];
|
|
907
|
+
}
|
|
908
|
+
function classifyChatCompletionPacketFailure(error, request, providerBindingReused) {
|
|
909
|
+
return classifyProviderFailure(serializeProviderFailure(classifyProviderTransportError(error)), {
|
|
910
|
+
sessionId: request.sessionId,
|
|
911
|
+
provider: {
|
|
912
|
+
id: request.providerId,
|
|
913
|
+
model: request.modelId
|
|
914
|
+
},
|
|
915
|
+
continuation: request.continuation,
|
|
916
|
+
toolFollowUp: request.toolFollowUp,
|
|
917
|
+
providerBindingReused,
|
|
918
|
+
requestMetadata: request.metadata
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
function coerceToolAwareProviderOutput(content) {
|
|
922
|
+
return (wrapLooseZcPacketAsCanonicalPacket(content) ??
|
|
923
|
+
wrapBareToolCallAsCanonicalPacket(content) ??
|
|
924
|
+
wrapBareFinalLikeMessageAsCanonicalPacket(content));
|
|
925
|
+
}
|
|
926
|
+
function coerceMalformedCanonicalPacket(content) {
|
|
927
|
+
const trimmed = content.trim();
|
|
928
|
+
const rootMatch = trimmed.match(/^<zc_packet version="1">([\s\S]*)<\/zc_packet>$/u);
|
|
929
|
+
if (!rootMatch) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const rootBody = rootMatch[1] ?? "";
|
|
933
|
+
const modeMatch = rootBody.match(/<mode>(final|tool_request|ask_user|fail)<\/mode>/u);
|
|
934
|
+
if (!modeMatch?.[1]) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
const mode = modeMatch[1];
|
|
938
|
+
const remainder = rootBody.replace(modeMatch[0], "");
|
|
939
|
+
if (mode === "tool_request") {
|
|
940
|
+
const parsedToolCall = parseLooseToolCall(remainder);
|
|
941
|
+
if (!parsedToolCall) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
return createToolRequestPacket(parsedToolCall);
|
|
945
|
+
}
|
|
946
|
+
const messageMatch = remainder.match(/<message>([\s\S]*?)<\/message>/u);
|
|
947
|
+
if (!messageMatch) {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
return createMessagePacket(mode, parseLenientCanonicalMessage(messageMatch[1] ?? ""));
|
|
951
|
+
}
|
|
952
|
+
function parseLenientCanonicalMessage(raw) {
|
|
953
|
+
const trimmed = raw.trim();
|
|
954
|
+
if (!trimmed) {
|
|
955
|
+
return "";
|
|
956
|
+
}
|
|
957
|
+
if (trimmed.startsWith("<![CDATA[")) {
|
|
958
|
+
const cdataEnd = trimmed.indexOf("]]>");
|
|
959
|
+
if (cdataEnd >= 0) {
|
|
960
|
+
return trimmed.slice("<![CDATA[".length, cdataEnd).trim();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return trimmed;
|
|
964
|
+
}
|
|
965
|
+
function wrapLooseZcPacketAsCanonicalPacket(content) {
|
|
966
|
+
const trimmed = content.trim();
|
|
967
|
+
const match = trimmed.match(/^<zc_packet\b([^>]*)>([\s\S]*)<\/zc_packet>$/u);
|
|
968
|
+
if (!match) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
const attributes = match[1] ?? "";
|
|
972
|
+
if (/\bversion="1"/u.test(attributes)) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
const modeMatch = attributes.match(/\bmode="(final|tool_request|ask_user|fail)"/u);
|
|
976
|
+
if (!modeMatch) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
const mode = modeMatch[1];
|
|
980
|
+
const body = (match[2] ?? "").trim();
|
|
981
|
+
if (!body) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
if (mode === "tool_request") {
|
|
985
|
+
return wrapBareToolCallAsCanonicalPacket(body);
|
|
986
|
+
}
|
|
987
|
+
const message = parseLooseMessageBody(body);
|
|
988
|
+
return message ? createMessagePacket(mode, message) : null;
|
|
989
|
+
}
|
|
990
|
+
function wrapBareToolCallAsCanonicalPacket(content) {
|
|
991
|
+
const trimmed = repairMalformedBareToolCall(content.trim());
|
|
992
|
+
if (!trimmed || !/^<tool_call\b[\s\S]*<\/tool_call>$/u.test(trimmed)) {
|
|
993
|
+
const parsedToolCall = parseLooseToolCall(content);
|
|
994
|
+
return parsedToolCall ? createToolRequestPacket(parsedToolCall) : null;
|
|
995
|
+
}
|
|
996
|
+
return `<zc_packet version="1"><mode>tool_request</mode>${trimmed}</zc_packet>`;
|
|
997
|
+
}
|
|
998
|
+
function repairMalformedBareToolCall(trimmed) {
|
|
999
|
+
if (/^<tool_call\b[\s\S]*<\/tool_call>$/u.test(trimmed)) {
|
|
1000
|
+
return trimmed;
|
|
1001
|
+
}
|
|
1002
|
+
if (/^<tool_call\b[\s\S]*<tool_call>\s*$/u.test(trimmed) && !trimmed.includes("</tool_call>")) {
|
|
1003
|
+
return trimmed.replace(/<tool_call>\s*$/u, "</tool_call>");
|
|
1004
|
+
}
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
function parseLooseToolCall(content) {
|
|
1008
|
+
const toolMatch = content.match(/<tool_call\b([^>]*)>([\s\S]*?)<\/tool_call>/u);
|
|
1009
|
+
if (!toolMatch) {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
const attributes = parseLooseXmlAttributes(toolMatch[1] ?? "");
|
|
1013
|
+
const rawBody = (toolMatch[2] ?? "").trim();
|
|
1014
|
+
const parsedJsonArguments = parseLooseToolArguments(rawBody);
|
|
1015
|
+
const parsedTaggedToolCall = parseTaggedToolCallBody(rawBody);
|
|
1016
|
+
const name = (attributes.name ?? "").trim() || parsedTaggedToolCall?.name || "";
|
|
1017
|
+
const id = (attributes.id ?? "").trim() || "call_1";
|
|
1018
|
+
const args = parsedJsonArguments ?? parsedTaggedToolCall?.args;
|
|
1019
|
+
if (!name || !args) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
id,
|
|
1024
|
+
name,
|
|
1025
|
+
args
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function parseLooseXmlAttributes(source) {
|
|
1029
|
+
const attributes = {};
|
|
1030
|
+
for (const match of source.matchAll(/\b([A-Za-z_][\w:.-]*)=(?:"([^"]*)"|'([^']*)')/g)) {
|
|
1031
|
+
const key = match[1]?.trim();
|
|
1032
|
+
if (!key) {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
attributes[key] = decodeXmlEntityValue((match[2] ?? match[3] ?? "").trim());
|
|
1036
|
+
}
|
|
1037
|
+
return attributes;
|
|
1038
|
+
}
|
|
1039
|
+
function parseLooseToolArguments(raw) {
|
|
1040
|
+
const stripped = stripCodeFence(raw.trim());
|
|
1041
|
+
if (!stripped) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
const parsed = JSON.parse(stripped);
|
|
1046
|
+
return isRecord(parsed) ? parsed : null;
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function parseTaggedToolCallBody(raw) {
|
|
1053
|
+
const functionMatch = raw.match(/<function=(?:"([^"]+)"|'([^']+)'|([^>\s]+))>([\s\S]*?)<\/function>/u);
|
|
1054
|
+
const functionName = decodeXmlEntityValue((functionMatch?.[1] ?? functionMatch?.[2] ?? functionMatch?.[3] ?? "").trim());
|
|
1055
|
+
const functionBody = (functionMatch?.[4] ?? "").trim();
|
|
1056
|
+
if (!functionName || !functionBody) {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
const args = {};
|
|
1060
|
+
for (const match of functionBody.matchAll(/<parameter=(?:"([^"]+)"|'([^']+)'|([^>\s]+))>([\s\S]*?)<\/parameter>/g)) {
|
|
1061
|
+
const key = decodeXmlEntityValue((match[1] ?? match[2] ?? match[3] ?? "").trim());
|
|
1062
|
+
if (!key) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const value = parseLooseParameterValue(match[4] ?? "");
|
|
1066
|
+
if (value === undefined) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
args[key] = value;
|
|
1070
|
+
}
|
|
1071
|
+
return Object.keys(args).length > 0
|
|
1072
|
+
? {
|
|
1073
|
+
name: functionName,
|
|
1074
|
+
args
|
|
1075
|
+
}
|
|
1076
|
+
: null;
|
|
1077
|
+
}
|
|
1078
|
+
function parseLooseParameterValue(raw) {
|
|
1079
|
+
const trimmed = raw.trim();
|
|
1080
|
+
if (!trimmed) {
|
|
1081
|
+
return "";
|
|
1082
|
+
}
|
|
1083
|
+
const parsedJson = parseLooseToolArguments(trimmed);
|
|
1084
|
+
if (parsedJson) {
|
|
1085
|
+
return parsedJson;
|
|
1086
|
+
}
|
|
1087
|
+
return decodeXmlEntityValue(trimmed);
|
|
1088
|
+
}
|
|
1089
|
+
function stripCodeFence(raw) {
|
|
1090
|
+
const fencedMatch = raw.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/u);
|
|
1091
|
+
return fencedMatch?.[1]?.trim() ?? raw;
|
|
1092
|
+
}
|
|
1093
|
+
function decodeXmlEntityValue(value) {
|
|
1094
|
+
return value
|
|
1095
|
+
.replaceAll(""", '"')
|
|
1096
|
+
.replaceAll("'", "'")
|
|
1097
|
+
.replaceAll("<", "<")
|
|
1098
|
+
.replaceAll(">", ">")
|
|
1099
|
+
.replaceAll("&", "&");
|
|
1100
|
+
}
|
|
1101
|
+
function wrapBareFinalLikeMessageAsCanonicalPacket(content) {
|
|
1102
|
+
const trimmed = content.trim();
|
|
1103
|
+
const match = trimmed.match(/^<(final|ask_user|fail)>([\s\S]*)<\/\1>$/u);
|
|
1104
|
+
if (!match) {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
const mode = match[1];
|
|
1108
|
+
const message = parseLooseMessageBody(match[2] ?? "");
|
|
1109
|
+
return message ? createMessagePacket(mode, message) : null;
|
|
1110
|
+
}
|
|
1111
|
+
function parseLooseMessageBody(raw) {
|
|
1112
|
+
const trimmed = raw.trim();
|
|
1113
|
+
if (!trimmed) {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
if (trimmed.startsWith("<![CDATA[") && trimmed.endsWith("]]>")) {
|
|
1117
|
+
return trimmed.slice("<![CDATA[".length, -"]]>".length).trim();
|
|
1118
|
+
}
|
|
1119
|
+
if (trimmed.includes("<")) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
return trimmed;
|
|
1123
|
+
}
|
|
1124
|
+
function wrapVisibleTextAsFinalPacket(content) {
|
|
1125
|
+
const sanitized = sanitizeBridgeApiOutput(content);
|
|
1126
|
+
if (sanitized.sanitized || !sanitized.content.trim()) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
return createMessagePacket("final", sanitized.content);
|
|
1130
|
+
}
|
|
1131
|
+
function normalizeExtractedProviderPacket(providerId, content) {
|
|
1132
|
+
const extracted = extractPacketCandidate(content);
|
|
1133
|
+
if (!extracted.ok) {
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
const normalized = normalizeProviderPacket(providerId, extracted.packetText);
|
|
1137
|
+
return normalized.ok ? normalized.canonicalPacket : null;
|
|
1138
|
+
}
|
|
1139
|
+
function validateJsonSchemaValue(value, schema, path) {
|
|
1140
|
+
if (Array.isArray(schema.enum) &&
|
|
1141
|
+
schema.enum.length > 0 &&
|
|
1142
|
+
!schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
1143
|
+
return `${path} must be one of the allowed enum values.`;
|
|
1144
|
+
}
|
|
1145
|
+
if ("const" in schema && !Object.is(schema.const, value)) {
|
|
1146
|
+
return `${path} must match the required constant value.`;
|
|
1147
|
+
}
|
|
1148
|
+
const schemaType = typeof schema.type === "string" ? schema.type : null;
|
|
1149
|
+
if (!schemaType) {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
switch (schemaType) {
|
|
1153
|
+
case "object": {
|
|
1154
|
+
if (!isRecord(value)) {
|
|
1155
|
+
return `${path} must be an object.`;
|
|
1156
|
+
}
|
|
1157
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1158
|
+
const required = Array.isArray(schema.required)
|
|
1159
|
+
? schema.required.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
1160
|
+
: [];
|
|
1161
|
+
const additionalProperties = schema.additionalProperties;
|
|
1162
|
+
for (const key of required) {
|
|
1163
|
+
if (!(key in value)) {
|
|
1164
|
+
return `${path}.${key} is required.`;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
1168
|
+
const childSchema = properties[key];
|
|
1169
|
+
if (isRecord(childSchema)) {
|
|
1170
|
+
const childError = validateJsonSchemaValue(childValue, childSchema, `${path}.${key}`);
|
|
1171
|
+
if (childError) {
|
|
1172
|
+
return childError;
|
|
1173
|
+
}
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (additionalProperties === false) {
|
|
1177
|
+
return `${path}.${key} is not allowed.`;
|
|
1178
|
+
}
|
|
1179
|
+
if (isRecord(additionalProperties)) {
|
|
1180
|
+
const childError = validateJsonSchemaValue(childValue, additionalProperties, `${path}.${key}`);
|
|
1181
|
+
if (childError) {
|
|
1182
|
+
return childError;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
case "array": {
|
|
1189
|
+
if (!Array.isArray(value)) {
|
|
1190
|
+
return `${path} must be an array.`;
|
|
1191
|
+
}
|
|
1192
|
+
if (isRecord(schema.items)) {
|
|
1193
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1194
|
+
const childError = validateJsonSchemaValue(value[index], schema.items, `${path}[${index}]`);
|
|
1195
|
+
if (childError) {
|
|
1196
|
+
return childError;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
case "string":
|
|
1203
|
+
return typeof value === "string" ? null : `${path} must be a string.`;
|
|
1204
|
+
case "boolean":
|
|
1205
|
+
return typeof value === "boolean" ? null : `${path} must be a boolean.`;
|
|
1206
|
+
case "number":
|
|
1207
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
1208
|
+
? null
|
|
1209
|
+
: `${path} must be a number.`;
|
|
1210
|
+
case "integer":
|
|
1211
|
+
return typeof value === "number" && Number.isInteger(value)
|
|
1212
|
+
? null
|
|
1213
|
+
: `${path} must be an integer.`;
|
|
1214
|
+
case "null":
|
|
1215
|
+
return value === null ? null : `${path} must be null.`;
|
|
1216
|
+
default:
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function optionalTrimmedString(value, key) {
|
|
1221
|
+
if (value === undefined) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof value !== "string") {
|
|
1225
|
+
throw new BridgeApiError({
|
|
1226
|
+
statusCode: 400,
|
|
1227
|
+
code: "invalid_request",
|
|
1228
|
+
message: `${key} must be a string.`
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
const trimmed = value.trim();
|
|
1232
|
+
if (!trimmed) {
|
|
1233
|
+
throw new BridgeApiError({
|
|
1234
|
+
statusCode: 400,
|
|
1235
|
+
code: "invalid_request",
|
|
1236
|
+
message: `${key} must be a non-empty string.`
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
return trimmed;
|
|
1240
|
+
}
|
|
1241
|
+
function isRecord(value) {
|
|
1242
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1243
|
+
}
|
|
1244
|
+
function asRecord(value) {
|
|
1245
|
+
return isRecord(value) ? value : {};
|
|
1246
|
+
}
|
|
1247
|
+
function summarizeRuntimeEvent(event) {
|
|
1248
|
+
const type = typeof event.type === "string" ? event.type : "unknown";
|
|
1249
|
+
switch (type) {
|
|
1250
|
+
case "provider_response":
|
|
1251
|
+
return {
|
|
1252
|
+
type,
|
|
1253
|
+
step: event.step,
|
|
1254
|
+
durationMs: event.durationMs,
|
|
1255
|
+
rawTextLength: typeof event.rawText === "string" ? event.rawText.length : 0
|
|
1256
|
+
};
|
|
1257
|
+
case "tool_result": {
|
|
1258
|
+
const result = asRecord(event.result);
|
|
1259
|
+
return {
|
|
1260
|
+
type,
|
|
1261
|
+
step: event.step,
|
|
1262
|
+
durationMs: event.durationMs,
|
|
1263
|
+
tool: {
|
|
1264
|
+
id: result.id,
|
|
1265
|
+
name: result.name,
|
|
1266
|
+
ok: result.ok
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
case "packet_parsed":
|
|
1271
|
+
return {
|
|
1272
|
+
type,
|
|
1273
|
+
step: event.step,
|
|
1274
|
+
mode: event.mode
|
|
1275
|
+
};
|
|
1276
|
+
case "packet_parse_failed":
|
|
1277
|
+
return {
|
|
1278
|
+
type,
|
|
1279
|
+
step: event.step,
|
|
1280
|
+
error: event.error
|
|
1281
|
+
};
|
|
1282
|
+
case "main_response_invalid":
|
|
1283
|
+
return {
|
|
1284
|
+
type,
|
|
1285
|
+
step: event.step,
|
|
1286
|
+
error: event.error,
|
|
1287
|
+
rawTextLength: event.rawTextLength
|
|
1288
|
+
};
|
|
1289
|
+
case "repair_attempted":
|
|
1290
|
+
return {
|
|
1291
|
+
type,
|
|
1292
|
+
step: event.step
|
|
1293
|
+
};
|
|
1294
|
+
case "repair_valid":
|
|
1295
|
+
return {
|
|
1296
|
+
type,
|
|
1297
|
+
step: event.step,
|
|
1298
|
+
mode: event.mode,
|
|
1299
|
+
rawTextLength: event.rawTextLength
|
|
1300
|
+
};
|
|
1301
|
+
case "repair_failed":
|
|
1302
|
+
return {
|
|
1303
|
+
type,
|
|
1304
|
+
step: event.step,
|
|
1305
|
+
reason: event.reason,
|
|
1306
|
+
error: event.error,
|
|
1307
|
+
providerFailure: event.providerFailure
|
|
1308
|
+
};
|
|
1309
|
+
case "outcome": {
|
|
1310
|
+
const outcome = asRecord(event.outcome);
|
|
1311
|
+
const failure = asRecord(outcome.failure);
|
|
1312
|
+
return {
|
|
1313
|
+
type,
|
|
1314
|
+
outcome: {
|
|
1315
|
+
mode: outcome.mode,
|
|
1316
|
+
steps: outcome.steps,
|
|
1317
|
+
failureSource: failure.source
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
default:
|
|
1322
|
+
return event;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function createInitialRepairRecoverySummary() {
|
|
1326
|
+
return {
|
|
1327
|
+
attempted: false,
|
|
1328
|
+
attemptCount: 0,
|
|
1329
|
+
outcome: "not_needed",
|
|
1330
|
+
invalidCount: 0
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function updateRepairRecoverySummary(summary, event) {
|
|
1334
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
1335
|
+
switch (type) {
|
|
1336
|
+
case "main_response_invalid":
|
|
1337
|
+
summary.invalidCount += 1;
|
|
1338
|
+
break;
|
|
1339
|
+
case "repair_attempted":
|
|
1340
|
+
summary.attempted = true;
|
|
1341
|
+
summary.attemptCount = 1;
|
|
1342
|
+
if (summary.outcome === "not_needed") {
|
|
1343
|
+
summary.outcome = "failed";
|
|
1344
|
+
}
|
|
1345
|
+
break;
|
|
1346
|
+
case "repair_valid":
|
|
1347
|
+
summary.attempted = true;
|
|
1348
|
+
summary.attemptCount = 1;
|
|
1349
|
+
summary.outcome = "valid";
|
|
1350
|
+
delete summary.failureReason;
|
|
1351
|
+
break;
|
|
1352
|
+
case "repair_failed":
|
|
1353
|
+
summary.attempted = true;
|
|
1354
|
+
summary.attemptCount = 1;
|
|
1355
|
+
summary.outcome = "failed";
|
|
1356
|
+
if (event.reason === "provider_failure" || event.reason === "protocol_invalid") {
|
|
1357
|
+
summary.failureReason = event.reason;
|
|
1358
|
+
}
|
|
1359
|
+
break;
|
|
1360
|
+
default:
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function createInMemorySessionBindingStore() {
|
|
1365
|
+
const bindings = new Map();
|
|
1366
|
+
return {
|
|
1367
|
+
async loadBinding(providerId, sessionId) {
|
|
1368
|
+
return bindings.get(createBindingKey(providerId, sessionId)) ?? null;
|
|
1369
|
+
},
|
|
1370
|
+
async saveBinding(providerId, sessionId, binding) {
|
|
1371
|
+
bindings.set(createBindingKey(providerId, sessionId), structuredClone(binding));
|
|
1372
|
+
},
|
|
1373
|
+
async clearBinding(providerId, sessionId) {
|
|
1374
|
+
bindings.delete(createBindingKey(providerId, sessionId));
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function createBindingKey(providerId, sessionId) {
|
|
1379
|
+
return `${providerId}:${sessionId}`;
|
|
1380
|
+
}
|
|
1381
|
+
function defaultLogEvent(event) {
|
|
1382
|
+
console.log(`[BridgeService][${event.scope}] ${event.event} ${JSON.stringify({ requestId: event.requestId, ...event.detail })}`);
|
|
1383
|
+
}
|
|
1384
|
+
export const bridgeRuntimeServiceModule = {
|
|
1385
|
+
createBridgeRuntimeService
|
|
1386
|
+
};
|