@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,366 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { promptCompilerModule } from "./prompt-compiler.js";
|
|
3
|
+
import { providerFailureModule } from "./provider-failure.js";
|
|
4
|
+
const { ProviderFailure, classifyProviderTransportError, withProviderRecovery } = providerFailureModule;
|
|
5
|
+
const { compileProviderTurn } = promptCompilerModule;
|
|
6
|
+
const MAX_SOFT_RETRIES = 1;
|
|
7
|
+
const MAX_SESSION_RESETS = 1;
|
|
8
|
+
class SessionBoundProviderAdapter {
|
|
9
|
+
id;
|
|
10
|
+
#modelId;
|
|
11
|
+
#transport;
|
|
12
|
+
#sessionBindingStore;
|
|
13
|
+
#sessionId;
|
|
14
|
+
#bridgeRequestId;
|
|
15
|
+
#onTraceEvent;
|
|
16
|
+
#providerTurnCount = 0;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.id = options.providerId;
|
|
19
|
+
this.#modelId = options.modelId;
|
|
20
|
+
this.#transport = options.transport;
|
|
21
|
+
this.#sessionBindingStore = options.sessionBindingStore;
|
|
22
|
+
this.#sessionId = options.sessionId ?? `bridge-runtime:${crypto.randomUUID()}`;
|
|
23
|
+
this.#bridgeRequestId = options.bridgeRequestId ?? crypto.randomUUID();
|
|
24
|
+
this.#onTraceEvent = options.onTraceEvent;
|
|
25
|
+
}
|
|
26
|
+
async completeTurn(input) {
|
|
27
|
+
this.#providerTurnCount += 1;
|
|
28
|
+
const providerTurnId = `${this.#bridgeRequestId}:provider-turn-${this.#providerTurnCount}`;
|
|
29
|
+
const toolResultCount = input.conversation.entries.filter((entry) => entry.type === "tool_result").length;
|
|
30
|
+
const sessionHistoryTurns = input.conversation.sessionHistory?.length ?? 0;
|
|
31
|
+
let binding = this.#sessionBindingStore
|
|
32
|
+
? await this.#sessionBindingStore.loadBinding(this.id, this.#sessionId).catch(() => null)
|
|
33
|
+
: null;
|
|
34
|
+
let forceReplay = false;
|
|
35
|
+
let softRetryCount = 0;
|
|
36
|
+
let sessionResetCount = 0;
|
|
37
|
+
while (true) {
|
|
38
|
+
const compiled = compileProviderTurn({
|
|
39
|
+
conversation: input.conversation,
|
|
40
|
+
availableTools: input.availableTools,
|
|
41
|
+
runtimePlannerPrimed: binding?.runtimePlannerPrimed === true && !forceReplay,
|
|
42
|
+
forceReplay
|
|
43
|
+
});
|
|
44
|
+
const messages = compiled.messages;
|
|
45
|
+
const continuation = compiled.summary.turnType === "follow_up";
|
|
46
|
+
const providerSessionReused = binding !== null;
|
|
47
|
+
const attempt = softRetryCount + sessionResetCount + 1;
|
|
48
|
+
const attemptStartedAt = Date.now();
|
|
49
|
+
this.#onTraceEvent?.("provider_turn_started", {
|
|
50
|
+
bridgeRequestId: this.#bridgeRequestId,
|
|
51
|
+
providerTurnId,
|
|
52
|
+
providerTurnIndex: this.#providerTurnCount,
|
|
53
|
+
bridgeSessionId: this.#sessionId,
|
|
54
|
+
providerId: this.id,
|
|
55
|
+
modelId: this.#modelId,
|
|
56
|
+
providerSessionId: binding?.conversationId ?? null,
|
|
57
|
+
providerParentId: binding?.parentId ?? null,
|
|
58
|
+
providerSessionReused,
|
|
59
|
+
continuation,
|
|
60
|
+
toolFollowUp: toolResultCount > 0,
|
|
61
|
+
toolResultCount,
|
|
62
|
+
sessionHistoryTurns,
|
|
63
|
+
replayedFromBridgeSession: compiled.summary.replayedFromBridgeSession,
|
|
64
|
+
attempt
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
const response = await this.#transport.completeChat({
|
|
68
|
+
lane: "main",
|
|
69
|
+
providerId: this.id,
|
|
70
|
+
modelId: this.#modelId,
|
|
71
|
+
sessionId: this.#sessionId,
|
|
72
|
+
requestId: providerTurnId,
|
|
73
|
+
attempt,
|
|
74
|
+
continuation,
|
|
75
|
+
toolFollowUp: toolResultCount > 0,
|
|
76
|
+
providerSessionReused,
|
|
77
|
+
messages,
|
|
78
|
+
upstreamBinding: binding
|
|
79
|
+
? {
|
|
80
|
+
conversationId: binding.conversationId,
|
|
81
|
+
parentId: binding.parentId
|
|
82
|
+
}
|
|
83
|
+
: null
|
|
84
|
+
});
|
|
85
|
+
this.#assertNonEmptyResponse(response.content, {
|
|
86
|
+
hasBinding: binding !== null
|
|
87
|
+
});
|
|
88
|
+
const nextBinding = response.upstreamBinding
|
|
89
|
+
? {
|
|
90
|
+
...response.upstreamBinding,
|
|
91
|
+
runtimePlannerPrimed: Boolean(response.upstreamBinding.parentId)
|
|
92
|
+
}
|
|
93
|
+
: binding;
|
|
94
|
+
if (this.#sessionBindingStore && nextBinding) {
|
|
95
|
+
await this.#sessionBindingStore.saveBinding(this.id, this.#sessionId, nextBinding);
|
|
96
|
+
}
|
|
97
|
+
this.#onTraceEvent?.("provider_attempt_finished", {
|
|
98
|
+
bridgeRequestId: this.#bridgeRequestId,
|
|
99
|
+
providerTurnId,
|
|
100
|
+
providerTurnIndex: this.#providerTurnCount,
|
|
101
|
+
bridgeSessionId: this.#sessionId,
|
|
102
|
+
providerId: this.id,
|
|
103
|
+
modelId: this.#modelId,
|
|
104
|
+
providerSessionId: nextBinding?.conversationId ?? null,
|
|
105
|
+
providerParentId: nextBinding?.parentId ?? null,
|
|
106
|
+
providerSessionReused,
|
|
107
|
+
continuation,
|
|
108
|
+
toolFollowUp: toolResultCount > 0,
|
|
109
|
+
toolResultCount,
|
|
110
|
+
sessionHistoryTurns,
|
|
111
|
+
replayedFromBridgeSession: compiled.summary.replayedFromBridgeSession,
|
|
112
|
+
attempt,
|
|
113
|
+
latencyMs: Date.now() - attemptStartedAt,
|
|
114
|
+
extractedOutputEmpty: false,
|
|
115
|
+
recovery: {
|
|
116
|
+
softRetryCount,
|
|
117
|
+
sessionResetCount
|
|
118
|
+
},
|
|
119
|
+
outcome: "success"
|
|
120
|
+
});
|
|
121
|
+
return response.content;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const classified = withProviderRecovery(classifyProviderTransportError(error), {
|
|
125
|
+
softRetryCount,
|
|
126
|
+
sessionResetCount
|
|
127
|
+
});
|
|
128
|
+
const nextAction = selectRecoveryAction(classified, {
|
|
129
|
+
softRetryCount,
|
|
130
|
+
sessionResetCount,
|
|
131
|
+
hasBinding: binding !== null
|
|
132
|
+
});
|
|
133
|
+
this.#onTraceEvent?.("provider_attempt_finished", {
|
|
134
|
+
bridgeRequestId: this.#bridgeRequestId,
|
|
135
|
+
providerTurnId,
|
|
136
|
+
providerTurnIndex: this.#providerTurnCount,
|
|
137
|
+
bridgeSessionId: this.#sessionId,
|
|
138
|
+
providerId: this.id,
|
|
139
|
+
modelId: this.#modelId,
|
|
140
|
+
providerSessionId: binding?.conversationId ?? null,
|
|
141
|
+
providerParentId: binding?.parentId ?? null,
|
|
142
|
+
providerSessionReused,
|
|
143
|
+
continuation,
|
|
144
|
+
toolFollowUp: toolResultCount > 0,
|
|
145
|
+
toolResultCount,
|
|
146
|
+
sessionHistoryTurns,
|
|
147
|
+
replayedFromBridgeSession: compiled.summary.replayedFromBridgeSession,
|
|
148
|
+
attempt,
|
|
149
|
+
latencyMs: Date.now() - attemptStartedAt,
|
|
150
|
+
extractedOutputEmpty: classified.emptyOutput,
|
|
151
|
+
failure: {
|
|
152
|
+
kind: classified.kind,
|
|
153
|
+
code: classified.code
|
|
154
|
+
},
|
|
155
|
+
recovery: {
|
|
156
|
+
softRetryCount,
|
|
157
|
+
sessionResetCount
|
|
158
|
+
},
|
|
159
|
+
outcome: nextAction
|
|
160
|
+
});
|
|
161
|
+
if (nextAction === "soft_retry") {
|
|
162
|
+
softRetryCount += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (nextAction === "session_reset") {
|
|
166
|
+
if (!this.#sessionBindingStore) {
|
|
167
|
+
throw classified;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
await this.#sessionBindingStore.clearBinding(this.id, this.#sessionId);
|
|
171
|
+
}
|
|
172
|
+
catch (resetError) {
|
|
173
|
+
throw withProviderRecovery(new ProviderFailure({
|
|
174
|
+
kind: "permanent",
|
|
175
|
+
code: "session_reset_failed",
|
|
176
|
+
message: resetError instanceof Error ? resetError.message : String(resetError),
|
|
177
|
+
displayMessage: "Provider session reset failed.",
|
|
178
|
+
retryable: false,
|
|
179
|
+
sessionResetEligible: false,
|
|
180
|
+
cause: resetError
|
|
181
|
+
}), {
|
|
182
|
+
softRetryCount,
|
|
183
|
+
sessionResetCount
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
sessionResetCount += 1;
|
|
187
|
+
binding = null;
|
|
188
|
+
forceReplay = true;
|
|
189
|
+
this.#onTraceEvent?.("provider_session_reset", {
|
|
190
|
+
bridgeRequestId: this.#bridgeRequestId,
|
|
191
|
+
providerTurnId,
|
|
192
|
+
providerTurnIndex: this.#providerTurnCount,
|
|
193
|
+
bridgeSessionId: this.#sessionId,
|
|
194
|
+
providerId: this.id,
|
|
195
|
+
modelId: this.#modelId,
|
|
196
|
+
reason: {
|
|
197
|
+
kind: classified.kind,
|
|
198
|
+
code: classified.code
|
|
199
|
+
},
|
|
200
|
+
recovery: {
|
|
201
|
+
softRetryCount,
|
|
202
|
+
sessionResetCount
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
throw classified;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async repairInvalidResponse(input) {
|
|
212
|
+
const repairSessionId = `${this.#sessionId}:repair:${crypto.randomUUID()}`;
|
|
213
|
+
const repairRequestId = `${this.#bridgeRequestId}:provider-turn-${this.#providerTurnCount}:repair`;
|
|
214
|
+
const repairMessages = buildRepairMessages(input);
|
|
215
|
+
const toolResultCount = input.conversation.entries.filter((entry) => entry.type === "tool_result").length;
|
|
216
|
+
this.#onTraceEvent?.("provider_repair_started", {
|
|
217
|
+
bridgeRequestId: this.#bridgeRequestId,
|
|
218
|
+
providerId: this.id,
|
|
219
|
+
modelId: this.#modelId,
|
|
220
|
+
bridgeSessionId: this.#sessionId,
|
|
221
|
+
repairSessionId,
|
|
222
|
+
repairRequestId,
|
|
223
|
+
providerTurnIndex: this.#providerTurnCount
|
|
224
|
+
});
|
|
225
|
+
const startedAt = Date.now();
|
|
226
|
+
try {
|
|
227
|
+
const response = await this.#transport.completeChat({
|
|
228
|
+
lane: "repair",
|
|
229
|
+
providerId: this.id,
|
|
230
|
+
modelId: this.#modelId,
|
|
231
|
+
sessionId: repairSessionId,
|
|
232
|
+
requestId: repairRequestId,
|
|
233
|
+
attempt: 1,
|
|
234
|
+
continuation: false,
|
|
235
|
+
toolFollowUp: toolResultCount > 0,
|
|
236
|
+
providerSessionReused: false,
|
|
237
|
+
messages: repairMessages,
|
|
238
|
+
upstreamBinding: null
|
|
239
|
+
});
|
|
240
|
+
this.#assertNonEmptyResponse(response.content, {
|
|
241
|
+
hasBinding: false
|
|
242
|
+
});
|
|
243
|
+
this.#onTraceEvent?.("provider_repair_finished", {
|
|
244
|
+
bridgeRequestId: this.#bridgeRequestId,
|
|
245
|
+
providerId: this.id,
|
|
246
|
+
modelId: this.#modelId,
|
|
247
|
+
bridgeSessionId: this.#sessionId,
|
|
248
|
+
repairSessionId,
|
|
249
|
+
repairRequestId,
|
|
250
|
+
providerTurnIndex: this.#providerTurnCount,
|
|
251
|
+
latencyMs: Date.now() - startedAt,
|
|
252
|
+
contentLength: response.content.length,
|
|
253
|
+
outcome: "success"
|
|
254
|
+
});
|
|
255
|
+
return response.content;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const classified = classifyProviderTransportError(error);
|
|
259
|
+
this.#onTraceEvent?.("provider_repair_failed", {
|
|
260
|
+
bridgeRequestId: this.#bridgeRequestId,
|
|
261
|
+
providerId: this.id,
|
|
262
|
+
modelId: this.#modelId,
|
|
263
|
+
bridgeSessionId: this.#sessionId,
|
|
264
|
+
repairSessionId,
|
|
265
|
+
repairRequestId,
|
|
266
|
+
providerTurnIndex: this.#providerTurnCount,
|
|
267
|
+
latencyMs: Date.now() - startedAt,
|
|
268
|
+
failure: {
|
|
269
|
+
kind: classified.kind,
|
|
270
|
+
code: classified.code
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
#assertNonEmptyResponse(content, context) {
|
|
277
|
+
if (!content.trim()) {
|
|
278
|
+
throw new ProviderFailure({
|
|
279
|
+
kind: "transient",
|
|
280
|
+
code: "empty_response",
|
|
281
|
+
message: "Provider returned an empty response.",
|
|
282
|
+
retryable: true,
|
|
283
|
+
sessionResetEligible: context.hasBinding,
|
|
284
|
+
emptyOutput: true
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function selectRecoveryAction(failure, state) {
|
|
290
|
+
if (failure.retryable && state.softRetryCount < MAX_SOFT_RETRIES) {
|
|
291
|
+
return "soft_retry";
|
|
292
|
+
}
|
|
293
|
+
if ((failure.kind === "session_corruption" || failure.sessionResetEligible) &&
|
|
294
|
+
state.sessionResetCount < MAX_SESSION_RESETS &&
|
|
295
|
+
state.hasBinding) {
|
|
296
|
+
return "session_reset";
|
|
297
|
+
}
|
|
298
|
+
return "failed";
|
|
299
|
+
}
|
|
300
|
+
function buildRepairMessages(input) {
|
|
301
|
+
const latestToolResult = [...input.conversation.entries]
|
|
302
|
+
.reverse()
|
|
303
|
+
.find((entry) => entry.type === "tool_result");
|
|
304
|
+
return [
|
|
305
|
+
{
|
|
306
|
+
role: "system",
|
|
307
|
+
content: [
|
|
308
|
+
"You are the bridge repair lane.",
|
|
309
|
+
"Re-emit the same intent as exactly one valid bridge packet.",
|
|
310
|
+
"Return exactly one block only.",
|
|
311
|
+
"Use <final>...</final> for assistant text.",
|
|
312
|
+
'Use <tool>{"name":"tool_name","arguments":{...}}</tool> for one tool call.',
|
|
313
|
+
"No markdown.",
|
|
314
|
+
"No backticks.",
|
|
315
|
+
"No extra text before or after the block.",
|
|
316
|
+
"If using <tool>, the JSON must contain only name and arguments.",
|
|
317
|
+
"Do not invent new intent.",
|
|
318
|
+
"Do not explain.",
|
|
319
|
+
"Do not expose reasoning.",
|
|
320
|
+
"Do not perform side effects. Only repair the packet encoding."
|
|
321
|
+
].join("\n")
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
role: "user",
|
|
325
|
+
content: [
|
|
326
|
+
"Bridge packet protocol:",
|
|
327
|
+
"Return exactly one valid packet block.",
|
|
328
|
+
"The deterministic parser and tool schema validator are the final authority.",
|
|
329
|
+
"Available tools:",
|
|
330
|
+
renderRepairToolManifest(input.availableTools),
|
|
331
|
+
`Latest user request:\n${getRepairUserMessage(input.conversation)}`,
|
|
332
|
+
...(latestToolResult ? [`Latest tool result:\n${latestToolResult.rawText}`] : []),
|
|
333
|
+
`Raw invalid candidate output:\n${input.invalidResponse}`,
|
|
334
|
+
`Validation error:\n${input.validationError}`,
|
|
335
|
+
"Re-emit the same intent now as exactly one valid packet."
|
|
336
|
+
].join("\n\n")
|
|
337
|
+
}
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
function getRepairUserMessage(conversation) {
|
|
341
|
+
const userEntry = conversation.entries.find((entry) => entry.type === "user_message");
|
|
342
|
+
if (!userEntry) {
|
|
343
|
+
throw new Error("Conversation state is missing the initial user message.");
|
|
344
|
+
}
|
|
345
|
+
return userEntry.content;
|
|
346
|
+
}
|
|
347
|
+
function renderRepairToolManifest(availableTools) {
|
|
348
|
+
if (availableTools.length === 0) {
|
|
349
|
+
return "(none)";
|
|
350
|
+
}
|
|
351
|
+
return availableTools
|
|
352
|
+
.slice()
|
|
353
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
354
|
+
.map((tool) => {
|
|
355
|
+
const required = tool.inputSchema.required.length > 0 ? tool.inputSchema.required.join(", ") : "(none)";
|
|
356
|
+
const properties = Object.entries(tool.inputSchema.properties)
|
|
357
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
358
|
+
.map(([name, property]) => `${name}: ${property.type} - ${property.description}`)
|
|
359
|
+
.join("; ");
|
|
360
|
+
return `- ${tool.name}: ${tool.description} | required: ${required} | args: ${properties || "(none)"}`;
|
|
361
|
+
})
|
|
362
|
+
.join("\n");
|
|
363
|
+
}
|
|
364
|
+
export const sessionBoundProviderModule = {
|
|
365
|
+
SessionBoundProviderAdapter
|
|
366
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const PROVIDER_TOOL_NAME_ALIASES = {
|
|
2
|
+
execute_shell_command: "bash"
|
|
3
|
+
};
|
|
4
|
+
function normalizeProviderToolName(name) {
|
|
5
|
+
const trimmed = name.trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return trimmed;
|
|
8
|
+
}
|
|
9
|
+
return PROVIDER_TOOL_NAME_ALIASES[trimmed] ?? trimmed;
|
|
10
|
+
}
|
|
11
|
+
export const toolNameAliasesModule = {
|
|
12
|
+
normalizeProviderToolName
|
|
13
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RuntimeTool } from "../execution/types.ts";
|
|
2
|
+
type BashToolOptions = {
|
|
3
|
+
runtimeRoot: string;
|
|
4
|
+
};
|
|
5
|
+
declare function createBashTool(options: BashToolOptions): RuntimeTool;
|
|
6
|
+
export declare const bashModule: {
|
|
7
|
+
createBashTool: typeof createBashTool;
|
|
8
|
+
};
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { executionTypesModule } from "../execution/types.js";
|
|
7
|
+
const { ToolExecutionError } = executionTypesModule;
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const MAX_STDIO_BYTES = 64 * 1024;
|
|
10
|
+
function createBashTool(options) {
|
|
11
|
+
return {
|
|
12
|
+
definition: {
|
|
13
|
+
name: "bash",
|
|
14
|
+
description: "Run a shell command on the local system. Short-lived commands run synchronously. Long-running commands such as dev servers, watchers, and persistent processes start detached and return their pid and log path immediately.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
command: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Shell command to execute."
|
|
21
|
+
},
|
|
22
|
+
description: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Optional short explanation of what the command does."
|
|
25
|
+
},
|
|
26
|
+
cwd: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Optional working directory. Absolute paths are allowed."
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
required: ["command"]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async execute(args) {
|
|
35
|
+
const command = requireNonEmptyString(args, "command");
|
|
36
|
+
const cwd = resolveCwd(args.cwd, options.runtimeRoot);
|
|
37
|
+
if (isLikelyLongRunningBashCommand(command)) {
|
|
38
|
+
return startDetachedProcess(command, cwd, options.runtimeRoot);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const { stdout, stderr } = await execFileAsync("bash", ["-lc", command], {
|
|
42
|
+
cwd,
|
|
43
|
+
timeout: getExecTimeoutMs(),
|
|
44
|
+
maxBuffer: 1024 * 1024
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
command,
|
|
48
|
+
cwd,
|
|
49
|
+
exitCode: 0,
|
|
50
|
+
timedOut: false,
|
|
51
|
+
stdout: truncate(stdout),
|
|
52
|
+
stderr: truncate(stderr)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (!(error instanceof Error)) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const execError = error;
|
|
60
|
+
return {
|
|
61
|
+
command,
|
|
62
|
+
cwd,
|
|
63
|
+
exitCode: typeof execError.code === "number" ? execError.code : null,
|
|
64
|
+
signal: execError.signal ?? null,
|
|
65
|
+
timedOut: execError.killed === true && execError.signal === "SIGTERM",
|
|
66
|
+
stdout: truncate(execError.stdout ?? ""),
|
|
67
|
+
stderr: truncate(execError.stderr ?? execError.message)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function requireNonEmptyString(args, key) {
|
|
74
|
+
const value = args[key];
|
|
75
|
+
if (typeof value === "string" && value.trim()) {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
throw new ToolExecutionError("invalid_arguments", `${key} must be a non-empty string.`);
|
|
79
|
+
}
|
|
80
|
+
function resolveCwd(rawValue, runtimeRoot) {
|
|
81
|
+
if (typeof rawValue !== "string" || !rawValue.trim()) {
|
|
82
|
+
return runtimeRoot;
|
|
83
|
+
}
|
|
84
|
+
return path.isAbsolute(rawValue) ? path.resolve(rawValue) : path.resolve(runtimeRoot, rawValue);
|
|
85
|
+
}
|
|
86
|
+
function truncate(value) {
|
|
87
|
+
return value.length > MAX_STDIO_BYTES ? `${value.slice(0, MAX_STDIO_BYTES)}\n[truncated]` : value;
|
|
88
|
+
}
|
|
89
|
+
function getExecTimeoutMs() {
|
|
90
|
+
return Number(process.env.BRIDGE_TOOL_EXEC_TIMEOUT_MS ?? 30000);
|
|
91
|
+
}
|
|
92
|
+
function isLikelyLongRunningBashCommand(command) {
|
|
93
|
+
return [
|
|
94
|
+
/\bpython3?\s+-m\s+http\.server\b/u,
|
|
95
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?dev\b/u,
|
|
96
|
+
/\bnext\s+dev\b/u,
|
|
97
|
+
/\bvite\b/u,
|
|
98
|
+
/\bwebpack(?:-dev-server)?\s+serve\b/u,
|
|
99
|
+
/\bserve\b/u,
|
|
100
|
+
/\blive-server\b/u,
|
|
101
|
+
/\bnodemon\b/u,
|
|
102
|
+
/\buvicorn\b/u,
|
|
103
|
+
/\bflask\s+run\b/u,
|
|
104
|
+
/\brails\s+server\b/u,
|
|
105
|
+
/\bcargo\s+watch\b/u,
|
|
106
|
+
/\btail\s+-f\b/u,
|
|
107
|
+
/\bwatch\s+/u,
|
|
108
|
+
/\bsleep\s+(?:infinity|\d{3,})\b/u
|
|
109
|
+
].some((pattern) => pattern.test(command));
|
|
110
|
+
}
|
|
111
|
+
function startDetachedProcess(command, cwd, runtimeRoot) {
|
|
112
|
+
const startedAt = new Date().toISOString();
|
|
113
|
+
const processRoot = ensureDetachedProcessRoot(runtimeRoot);
|
|
114
|
+
const processId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
115
|
+
const logPath = path.join(processRoot, `${processId}.log`);
|
|
116
|
+
appendFileSync(logPath, `[bridge bash detached] ${startedAt}\n$ ${command}\n\n`, "utf8");
|
|
117
|
+
const logFd = openSync(logPath, "a");
|
|
118
|
+
try {
|
|
119
|
+
const child = spawn("bash", ["-lc", command], {
|
|
120
|
+
cwd,
|
|
121
|
+
detached: true,
|
|
122
|
+
stdio: ["ignore", logFd, logFd]
|
|
123
|
+
});
|
|
124
|
+
child.unref();
|
|
125
|
+
return {
|
|
126
|
+
command,
|
|
127
|
+
cwd,
|
|
128
|
+
exitCode: null,
|
|
129
|
+
timedOut: false,
|
|
130
|
+
stdout: "",
|
|
131
|
+
stderr: "",
|
|
132
|
+
detached: true,
|
|
133
|
+
pid: child.pid ?? null,
|
|
134
|
+
logPath,
|
|
135
|
+
startedAt
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
closeSync(logFd);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function ensureDetachedProcessRoot(runtimeRoot) {
|
|
143
|
+
const root = path.join(resolveDetachedProcessBase(runtimeRoot), "bridge-tool-processes");
|
|
144
|
+
mkdirSync(root, {
|
|
145
|
+
recursive: true
|
|
146
|
+
});
|
|
147
|
+
return root;
|
|
148
|
+
}
|
|
149
|
+
function resolveDetachedProcessBase(runtimeRoot) {
|
|
150
|
+
if (runtimeRoot.trim()) {
|
|
151
|
+
return runtimeRoot;
|
|
152
|
+
}
|
|
153
|
+
return os.tmpdir();
|
|
154
|
+
}
|
|
155
|
+
export const bashModule = {
|
|
156
|
+
createBashTool
|
|
157
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RuntimeTool } from "../execution/types.ts";
|
|
2
|
+
type EditToolOptions = {
|
|
3
|
+
runtimeRoot: string;
|
|
4
|
+
};
|
|
5
|
+
declare function createEditTool(options: EditToolOptions): RuntimeTool;
|
|
6
|
+
export declare const editModule: {
|
|
7
|
+
createEditTool: typeof createEditTool;
|
|
8
|
+
};
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { executionTypesModule } from "../execution/types.js";
|
|
4
|
+
import { runtimePathModule } from "./runtime-path.js";
|
|
5
|
+
import { textFileModule } from "./text-file.js";
|
|
6
|
+
const { ToolExecutionError } = executionTypesModule;
|
|
7
|
+
const { assertTextContent, MAX_FILE_READ_BYTES, MAX_FILE_WRITE_BYTES, readTextFileWithinLimit, writeTextFileAtomic } = textFileModule;
|
|
8
|
+
const { resolveRuntimePath } = runtimePathModule;
|
|
9
|
+
function createEditTool(options) {
|
|
10
|
+
return {
|
|
11
|
+
definition: {
|
|
12
|
+
name: "edit",
|
|
13
|
+
description: "Replace one exact text span in an existing UTF-8 text file.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
path: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Absolute path or path relative to the runtime root."
|
|
20
|
+
},
|
|
21
|
+
oldText: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Exact text to replace. Matching is whitespace-sensitive."
|
|
24
|
+
},
|
|
25
|
+
newText: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Replacement text."
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: ["path", "oldText", "newText"]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async execute(args) {
|
|
34
|
+
const resolvedPath = await resolveRuntimePath(options.runtimeRoot, requireString(args, "path"));
|
|
35
|
+
const oldText = requireString(args, "oldText");
|
|
36
|
+
const newText = requireString(args, "newText", false);
|
|
37
|
+
const { content } = await readTextFileWithinLimit({
|
|
38
|
+
absolutePath: resolvedPath,
|
|
39
|
+
relativePath: resolvedPath,
|
|
40
|
+
maxBytes: MAX_FILE_READ_BYTES,
|
|
41
|
+
operation: "read"
|
|
42
|
+
});
|
|
43
|
+
const matchCount = countExactOccurrences(content, oldText);
|
|
44
|
+
if (matchCount === 0) {
|
|
45
|
+
throw new ToolExecutionError("not_found", `Exact text was not found in "${resolvedPath}".`);
|
|
46
|
+
}
|
|
47
|
+
if (matchCount > 1) {
|
|
48
|
+
throw new ToolExecutionError("ambiguous_match", `Exact text matched ${matchCount} times in "${resolvedPath}". Refine oldText to one exact occurrence.`);
|
|
49
|
+
}
|
|
50
|
+
const nextContent = content.replace(oldText, newText);
|
|
51
|
+
const bytesWritten = assertTextContent(nextContent, resolvedPath, MAX_FILE_WRITE_BYTES);
|
|
52
|
+
try {
|
|
53
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
54
|
+
await writeTextFileAtomic(resolvedPath, nextContent);
|
|
55
|
+
return {
|
|
56
|
+
path: resolvedPath,
|
|
57
|
+
replaced: 1,
|
|
58
|
+
bytesWritten
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof ToolExecutionError) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
throw new ToolExecutionError("io_error", `Unable to edit file "${resolvedPath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function countExactOccurrences(content, search) {
|
|
71
|
+
let count = 0;
|
|
72
|
+
let startIndex = 0;
|
|
73
|
+
while (true) {
|
|
74
|
+
const matchIndex = content.indexOf(search, startIndex);
|
|
75
|
+
if (matchIndex < 0) {
|
|
76
|
+
return count;
|
|
77
|
+
}
|
|
78
|
+
count += 1;
|
|
79
|
+
startIndex = matchIndex + search.length;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function requireString(args, key, requireNonEmpty = true) {
|
|
83
|
+
const value = args[key];
|
|
84
|
+
if (typeof value !== "string") {
|
|
85
|
+
throw new ToolExecutionError("invalid_arguments", `${key} must be a string.`);
|
|
86
|
+
}
|
|
87
|
+
if (requireNonEmpty && !value.length) {
|
|
88
|
+
throw new ToolExecutionError("invalid_arguments", `${key} must be a non-empty string.`);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
export const editModule = {
|
|
93
|
+
createEditTool
|
|
94
|
+
};
|