@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.
Files changed (145) hide show
  1. package/README.md +117 -0
  2. package/bin/openbridge.js +10 -0
  3. package/package.json +85 -0
  4. package/packages/cli/dist/args.d.ts +30 -0
  5. package/packages/cli/dist/args.js +160 -0
  6. package/packages/cli/dist/cli.d.ts +2 -0
  7. package/packages/cli/dist/cli.js +9 -0
  8. package/packages/cli/dist/index.d.ts +26 -0
  9. package/packages/cli/dist/index.js +76 -0
  10. package/packages/runtime/dist/assistant-protocol.d.ts +34 -0
  11. package/packages/runtime/dist/assistant-protocol.js +121 -0
  12. package/packages/runtime/dist/execution/in-process.d.ts +14 -0
  13. package/packages/runtime/dist/execution/in-process.js +45 -0
  14. package/packages/runtime/dist/execution/types.d.ts +49 -0
  15. package/packages/runtime/dist/execution/types.js +20 -0
  16. package/packages/runtime/dist/index.d.ts +86 -0
  17. package/packages/runtime/dist/index.js +60 -0
  18. package/packages/runtime/dist/normalizers/index.d.ts +6 -0
  19. package/packages/runtime/dist/normalizers/index.js +12 -0
  20. package/packages/runtime/dist/normalizers/legacy-packet.d.ts +6 -0
  21. package/packages/runtime/dist/normalizers/legacy-packet.js +131 -0
  22. package/packages/runtime/dist/output-sanitizer.d.ts +23 -0
  23. package/packages/runtime/dist/output-sanitizer.js +78 -0
  24. package/packages/runtime/dist/packet-extractor.d.ts +17 -0
  25. package/packages/runtime/dist/packet-extractor.js +43 -0
  26. package/packages/runtime/dist/packet-normalizer.d.ts +21 -0
  27. package/packages/runtime/dist/packet-normalizer.js +47 -0
  28. package/packages/runtime/dist/prompt-compiler.d.ts +28 -0
  29. package/packages/runtime/dist/prompt-compiler.js +301 -0
  30. package/packages/runtime/dist/protocol.d.ts +44 -0
  31. package/packages/runtime/dist/protocol.js +165 -0
  32. package/packages/runtime/dist/provider-failure.d.ts +52 -0
  33. package/packages/runtime/dist/provider-failure.js +236 -0
  34. package/packages/runtime/dist/provider.d.ts +40 -0
  35. package/packages/runtime/dist/provider.js +1 -0
  36. package/packages/runtime/dist/runtime.d.ts +86 -0
  37. package/packages/runtime/dist/runtime.js +462 -0
  38. package/packages/runtime/dist/session-bound-provider.d.ts +52 -0
  39. package/packages/runtime/dist/session-bound-provider.js +366 -0
  40. package/packages/runtime/dist/tool-name-aliases.d.ts +5 -0
  41. package/packages/runtime/dist/tool-name-aliases.js +13 -0
  42. package/packages/runtime/dist/tools/bash.d.ts +9 -0
  43. package/packages/runtime/dist/tools/bash.js +157 -0
  44. package/packages/runtime/dist/tools/edit.d.ts +9 -0
  45. package/packages/runtime/dist/tools/edit.js +94 -0
  46. package/packages/runtime/dist/tools/index.d.ts +39 -0
  47. package/packages/runtime/dist/tools/index.js +27 -0
  48. package/packages/runtime/dist/tools/list-dir.d.ts +9 -0
  49. package/packages/runtime/dist/tools/list-dir.js +127 -0
  50. package/packages/runtime/dist/tools/read.d.ts +9 -0
  51. package/packages/runtime/dist/tools/read.js +56 -0
  52. package/packages/runtime/dist/tools/registry.d.ts +15 -0
  53. package/packages/runtime/dist/tools/registry.js +38 -0
  54. package/packages/runtime/dist/tools/runtime-path.d.ts +7 -0
  55. package/packages/runtime/dist/tools/runtime-path.js +22 -0
  56. package/packages/runtime/dist/tools/search-files.d.ts +9 -0
  57. package/packages/runtime/dist/tools/search-files.js +149 -0
  58. package/packages/runtime/dist/tools/text-file.d.ts +32 -0
  59. package/packages/runtime/dist/tools/text-file.js +101 -0
  60. package/packages/runtime/dist/tools/workspace-path.d.ts +17 -0
  61. package/packages/runtime/dist/tools/workspace-path.js +70 -0
  62. package/packages/runtime/dist/tools/write.d.ts +9 -0
  63. package/packages/runtime/dist/tools/write.js +59 -0
  64. package/packages/server/dist/bridge/bridge-model-catalog.d.ts +56 -0
  65. package/packages/server/dist/bridge/bridge-model-catalog.js +100 -0
  66. package/packages/server/dist/bridge/bridge-runtime-service.d.ts +61 -0
  67. package/packages/server/dist/bridge/bridge-runtime-service.js +1386 -0
  68. package/packages/server/dist/bridge/chat-completions/chat-completion-service.d.ts +127 -0
  69. package/packages/server/dist/bridge/chat-completions/chat-completion-service.js +1026 -0
  70. package/packages/server/dist/bridge/index.d.ts +335 -0
  71. package/packages/server/dist/bridge/index.js +45 -0
  72. package/packages/server/dist/bridge/live-provider-extraction-canary.d.ts +69 -0
  73. package/packages/server/dist/bridge/live-provider-extraction-canary.js +186 -0
  74. package/packages/server/dist/bridge/providers/generic-provider-transport.d.ts +53 -0
  75. package/packages/server/dist/bridge/providers/generic-provider-transport.js +973 -0
  76. package/packages/server/dist/bridge/providers/provider-session-resolver.d.ts +17 -0
  77. package/packages/server/dist/bridge/providers/provider-session-resolver.js +95 -0
  78. package/packages/server/dist/bridge/providers/provider-streams.d.ts +80 -0
  79. package/packages/server/dist/bridge/providers/provider-streams.js +844 -0
  80. package/packages/server/dist/bridge/providers/provider-transport-profile.d.ts +194 -0
  81. package/packages/server/dist/bridge/providers/provider-transport-profile.js +198 -0
  82. package/packages/server/dist/bridge/providers/web-provider-transport.d.ts +30 -0
  83. package/packages/server/dist/bridge/providers/web-provider-transport.js +151 -0
  84. package/packages/server/dist/bridge/state/file-bridge-state-store.d.ts +36 -0
  85. package/packages/server/dist/bridge/state/file-bridge-state-store.js +164 -0
  86. package/packages/server/dist/bridge/stores/local-session-package-store.d.ts +23 -0
  87. package/packages/server/dist/bridge/stores/local-session-package-store.js +548 -0
  88. package/packages/server/dist/bridge/stores/provider-store.d.ts +94 -0
  89. package/packages/server/dist/bridge/stores/provider-store.js +143 -0
  90. package/packages/server/dist/bridge/stores/session-backed-provider-store.d.ts +7 -0
  91. package/packages/server/dist/bridge/stores/session-backed-provider-store.js +26 -0
  92. package/packages/server/dist/bridge/stores/session-package-store.d.ts +286 -0
  93. package/packages/server/dist/bridge/stores/session-package-store.js +1527 -0
  94. package/packages/server/dist/bridge/stores/session-store.d.ts +120 -0
  95. package/packages/server/dist/bridge/stores/session-store.js +139 -0
  96. package/packages/server/dist/cli/index.d.ts +9 -0
  97. package/packages/server/dist/cli/index.js +6 -0
  98. package/packages/server/dist/cli/main.d.ts +2 -0
  99. package/packages/server/dist/cli/main.js +9 -0
  100. package/packages/server/dist/cli/run-bridge-server-cli.d.ts +54 -0
  101. package/packages/server/dist/cli/run-bridge-server-cli.js +371 -0
  102. package/packages/server/dist/client/bridge-api-client.d.ts +61 -0
  103. package/packages/server/dist/client/bridge-api-client.js +267 -0
  104. package/packages/server/dist/client/index.d.ts +11 -0
  105. package/packages/server/dist/client/index.js +11 -0
  106. package/packages/server/dist/config/bridge-server-config.d.ts +52 -0
  107. package/packages/server/dist/config/bridge-server-config.js +118 -0
  108. package/packages/server/dist/config/index.d.ts +20 -0
  109. package/packages/server/dist/config/index.js +8 -0
  110. package/packages/server/dist/http/bridge-api-route-context.d.ts +14 -0
  111. package/packages/server/dist/http/bridge-api-route-context.js +1 -0
  112. package/packages/server/dist/http/create-bridge-api-server.d.ts +72 -0
  113. package/packages/server/dist/http/create-bridge-api-server.js +225 -0
  114. package/packages/server/dist/http/index.d.ts +5 -0
  115. package/packages/server/dist/http/index.js +5 -0
  116. package/packages/server/dist/http/parse-request.d.ts +6 -0
  117. package/packages/server/dist/http/parse-request.js +27 -0
  118. package/packages/server/dist/http/register-bridge-api-routes.d.ts +7 -0
  119. package/packages/server/dist/http/register-bridge-api-routes.js +17 -0
  120. package/packages/server/dist/http/routes/admin-routes.d.ts +7 -0
  121. package/packages/server/dist/http/routes/admin-routes.js +135 -0
  122. package/packages/server/dist/http/routes/chat-completions-route.d.ts +7 -0
  123. package/packages/server/dist/http/routes/chat-completions-route.js +49 -0
  124. package/packages/server/dist/http/routes/health-routes.d.ts +6 -0
  125. package/packages/server/dist/http/routes/health-routes.js +7 -0
  126. package/packages/server/dist/http/routes/message-routes.d.ts +7 -0
  127. package/packages/server/dist/http/routes/message-routes.js +7 -0
  128. package/packages/server/dist/index.d.ts +85 -0
  129. package/packages/server/dist/index.js +28 -0
  130. package/packages/server/dist/security/bridge-auth.d.ts +9 -0
  131. package/packages/server/dist/security/bridge-auth.js +41 -0
  132. package/packages/server/dist/security/cors-policy.d.ts +5 -0
  133. package/packages/server/dist/security/cors-policy.js +34 -0
  134. package/packages/server/dist/security/index.d.ts +16 -0
  135. package/packages/server/dist/security/index.js +12 -0
  136. package/packages/server/dist/security/redact-sensitive-values.d.ts +19 -0
  137. package/packages/server/dist/security/redact-sensitive-values.js +67 -0
  138. package/packages/server/dist/shared/api-schema.d.ts +133 -0
  139. package/packages/server/dist/shared/api-schema.js +1 -0
  140. package/packages/server/dist/shared/bridge-api-error.d.ts +17 -0
  141. package/packages/server/dist/shared/bridge-api-error.js +19 -0
  142. package/packages/server/dist/shared/index.d.ts +7 -0
  143. package/packages/server/dist/shared/index.js +7 -0
  144. package/packages/server/dist/shared/output.d.ts +5 -0
  145. 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,5 @@
1
+ declare function normalizeProviderToolName(name: string): string;
2
+ export declare const toolNameAliasesModule: {
3
+ normalizeProviderToolName: typeof normalizeProviderToolName;
4
+ };
5
+ export {};
@@ -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
+ };