@tangle-network/agent-provider-cli-bridge 0.5.0 → 0.6.1

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.
@@ -0,0 +1,276 @@
1
+ import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-event-cursor.js";
2
+ import { requestHeaders, trimSlash } from "./transport.js";
3
+ import { modelRequestsFromOpenAi, parseSse, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
4
+ export class CliBridgeRequestRejectedError extends Error {
5
+ status;
6
+ constructor(status, detail) {
7
+ super(`cli-bridge ${status}: ${detail}`);
8
+ this.status = status;
9
+ this.name = "CliBridgeRequestRejectedError";
10
+ }
11
+ }
12
+ export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onAccepted) {
13
+ const reconnect = requestBody === undefined;
14
+ const replay = retainedReplayRequest(lastEventId);
15
+ const response = await transport.fetch(reconnect
16
+ ? `${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(runId)}/events`
17
+ : `${trimSlash(options.baseUrl)}/v1/chat/completions`, {
18
+ method: reconnect ? "GET" : "POST",
19
+ headers: {
20
+ ...requestHeaders(options),
21
+ accept: "text/event-stream",
22
+ ...(!reconnect && turn.sessionId ? { "x-session-id": turn.sessionId } : {}),
23
+ ...(replay.serverCursor === undefined
24
+ ? {}
25
+ : { "last-event-id": replay.serverCursor }),
26
+ },
27
+ ...(requestBody === undefined ? {} : { body: requestBody }),
28
+ ...(signal ? { signal } : {}),
29
+ });
30
+ if (!response.ok) {
31
+ let detail = "request rejected";
32
+ try {
33
+ detail = await response.text();
34
+ }
35
+ catch {
36
+ // The HTTP status already proves this request was rejected.
37
+ }
38
+ throw new CliBridgeRequestRejectedError(response.status, detail);
39
+ }
40
+ onAccepted?.(response);
41
+ if (!response.body)
42
+ throw new Error("cli-bridge response body is empty");
43
+ let text = "";
44
+ const sessionId = turn.sessionId ?? runId;
45
+ const messageId = turn.turnId ?? `${sessionId}:assistant`;
46
+ const emittedToolCalls = new Set();
47
+ let completed = false;
48
+ let sawUsage = false;
49
+ let observedModelRequests = 0;
50
+ let modelRequestsKnown = false;
51
+ let servedModel;
52
+ let systemFingerprint;
53
+ let textBoundaryPending = false;
54
+ let terminalCursor;
55
+ let sawProtocolEnd = false;
56
+ for await (const frame of parseSse(response.body)) {
57
+ if (frame.data === "[DONE]") {
58
+ sawProtocolEnd = true;
59
+ continue;
60
+ }
61
+ const parsed = safeJson(frame.data);
62
+ if (!parsed)
63
+ continue;
64
+ if (parsed.error && typeof parsed.error === "object") {
65
+ const error = parsed.error;
66
+ const message = typeof error.message === "string" ? error.message : "cli-bridge error";
67
+ yield* retainedEventsWithIdentity([{
68
+ type: "status",
69
+ data: { status: "failed", error: message },
70
+ normalized: { type: "status", status: "failed", detail: message },
71
+ }], frame.id, runId, sessionId, turn.executionId, replay.anchor);
72
+ throw new Error(`cli-bridge: ${message}`);
73
+ }
74
+ const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
75
+ const delta = choice?.delta;
76
+ const responseIdentity = responseIdentityFromOpenAi(parsed);
77
+ if (responseIdentity?.model !== undefined)
78
+ servedModel = responseIdentity.model;
79
+ if (responseIdentity?.system_fingerprint !== undefined) {
80
+ systemFingerprint = responseIdentity.system_fingerprint;
81
+ }
82
+ const identityData = {
83
+ ...(servedModel === undefined ? {} : { model: servedModel }),
84
+ ...(systemFingerprint === undefined ? {} : { system_fingerprint: systemFingerprint }),
85
+ };
86
+ const toolCallDeltas = toolCallsFromDelta(delta);
87
+ const rawChunk = delta && typeof delta.content === "string" ? delta.content : "";
88
+ const chunk = rawChunk &&
89
+ textBoundaryPending &&
90
+ toolCallDeltas.length === 0 &&
91
+ text.length > 0 &&
92
+ !/\s$/u.test(text) &&
93
+ !/^\s/u.test(rawChunk)
94
+ ? `\n\n${rawChunk}`
95
+ : rawChunk;
96
+ if (rawChunk && toolCallDeltas.length === 0)
97
+ textBoundaryPending = false;
98
+ const nextUsage = usageFromOpenAi(parsed.usage);
99
+ const nextModelRequests = modelRequestsFromOpenAi(parsed.usage);
100
+ const frameEvents = [];
101
+ if (nextUsage || nextModelRequests !== undefined) {
102
+ if (nextUsage)
103
+ sawUsage = true;
104
+ if (nextModelRequests !== undefined) {
105
+ observedModelRequests += nextModelRequests;
106
+ modelRequestsKnown = true;
107
+ }
108
+ frameEvents.push({
109
+ type: "usage",
110
+ data: {
111
+ ...identityData,
112
+ ...(nextModelRequests === undefined ? {} : { modelRequests: nextModelRequests }),
113
+ },
114
+ ...(nextUsage ? { usage: nextUsage } : {}),
115
+ });
116
+ }
117
+ if (chunk) {
118
+ text += chunk;
119
+ const part = {
120
+ id: `${messageId}:text`,
121
+ sessionID: sessionId,
122
+ messageID: messageId,
123
+ type: "text",
124
+ text,
125
+ };
126
+ const normalized = {
127
+ type: "message.part.updated",
128
+ part,
129
+ delta: chunk,
130
+ };
131
+ frameEvents.push({
132
+ type: "message.part.updated",
133
+ data: { ...identityData, part, delta: chunk },
134
+ normalized,
135
+ });
136
+ }
137
+ for (const toolCall of toolCallDeltas) {
138
+ const callId = toolCall.id ?? `${messageId}:tool:${toolCall.index}`;
139
+ if (!toolCall.name || emittedToolCalls.has(callId))
140
+ continue;
141
+ emittedToolCalls.add(callId);
142
+ const part = {
143
+ id: callId,
144
+ sessionID: sessionId,
145
+ messageID: messageId,
146
+ type: "tool",
147
+ callID: callId,
148
+ tool: toolCall.name,
149
+ state: { status: "pending", input: {} },
150
+ };
151
+ const normalized = {
152
+ type: "message.part.updated",
153
+ part,
154
+ };
155
+ frameEvents.push({
156
+ type: "message.part.updated",
157
+ data: { part },
158
+ normalized,
159
+ });
160
+ }
161
+ if (toolCallDeltas.length > 0)
162
+ textBoundaryPending = true;
163
+ if (choice?.finish_reason) {
164
+ if (choice.finish_reason === "error") {
165
+ frameEvents.push({
166
+ type: "status",
167
+ data: { status: "failed", error: "cli-bridge returned finish_reason=error" },
168
+ normalized: {
169
+ type: "status",
170
+ status: "failed",
171
+ detail: "cli-bridge returned finish_reason=error",
172
+ },
173
+ });
174
+ yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
175
+ throw new Error("cli-bridge returned finish_reason=error");
176
+ }
177
+ completed = true;
178
+ if (lastEventId && requestBody !== undefined) {
179
+ terminalCursor = frame.id === undefined
180
+ ? undefined
181
+ : `${frame.id}:${frameEvents.length}`;
182
+ }
183
+ else {
184
+ frameEvents.push({
185
+ type: "result",
186
+ data: {
187
+ finalText: text,
188
+ finishReason: choice.finish_reason,
189
+ status: "completed",
190
+ ...identityData,
191
+ ...(modelRequestsKnown ? { modelRequests: observedModelRequests } : {}),
192
+ },
193
+ });
194
+ }
195
+ }
196
+ yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
197
+ }
198
+ if (!sawProtocolEnd) {
199
+ throw new Error("cli-bridge stream ended without the [DONE] protocol marker");
200
+ }
201
+ if (!completed && lastEventId === undefined) {
202
+ throw new Error("cli-bridge stream ended without a terminal result");
203
+ }
204
+ if (lastEventId && requestBody !== undefined) {
205
+ const result = await readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted);
206
+ const finalModelRequests = result.modelRequests ??
207
+ (modelRequestsKnown ? observedModelRequests : undefined);
208
+ const finalCursor = terminalCursor ?? lastEventId;
209
+ yield {
210
+ type: "result",
211
+ data: {
212
+ finalText: result.text,
213
+ finishReason: result.finishReason,
214
+ status: "completed",
215
+ ...(result.model === undefined ? {} : { model: result.model }),
216
+ ...(result.system_fingerprint === undefined
217
+ ? {}
218
+ : { system_fingerprint: result.system_fingerprint }),
219
+ runId,
220
+ sessionId,
221
+ ...(turn.executionId === undefined ? {} : { executionId: turn.executionId }),
222
+ ...(finalCursor === undefined ? {} : { cursor: finalCursor }),
223
+ ...(finalModelRequests === undefined
224
+ ? {}
225
+ : { modelRequests: finalModelRequests }),
226
+ },
227
+ ...(result.usage && !sawUsage ? { usage: result.usage } : {}),
228
+ ...(finalCursor === undefined ? {} : { id: finalCursor }),
229
+ };
230
+ }
231
+ }
232
+ async function readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted) {
233
+ const body = safeJson(requestBody);
234
+ if (!body)
235
+ throw new Error("cli-bridge replay request is not valid JSON");
236
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
237
+ method: "POST",
238
+ headers: requestHeaders(options),
239
+ body: JSON.stringify({ ...body, stream: false }),
240
+ ...(signal ? { signal } : {}),
241
+ });
242
+ if (!response.ok) {
243
+ throw new Error(`cli-bridge replay result ${response.status}: ${await response.text()}`);
244
+ }
245
+ onAccepted?.(response);
246
+ const parsed = safeJson(await response.text());
247
+ if (parsed?.error && typeof parsed.error === "object") {
248
+ const error = parsed.error;
249
+ const message = typeof error.message === "string" ? error.message : "cli-bridge replay failed";
250
+ throw new Error(`cli-bridge replay result failed: ${message}`);
251
+ }
252
+ const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : undefined;
253
+ const message = choice?.message && typeof choice.message === "object"
254
+ ? choice.message
255
+ : undefined;
256
+ if (typeof message?.content !== "string" ||
257
+ typeof choice?.finish_reason !== "string") {
258
+ throw new Error("cli-bridge replay result returned an invalid completion");
259
+ }
260
+ if (choice.finish_reason === "error" || choice.finish_reason === "timeout") {
261
+ throw new Error(`cli-bridge replay result ended ${choice.finish_reason}`);
262
+ }
263
+ const usage = usageFromOpenAi(parsed?.usage);
264
+ const modelRequests = modelRequestsFromOpenAi(parsed?.usage);
265
+ const responseIdentity = responseIdentityFromOpenAi(parsed);
266
+ return {
267
+ text: message.content,
268
+ finishReason: choice.finish_reason,
269
+ ...(usage ? { usage } : {}),
270
+ ...(modelRequests === undefined ? {} : { modelRequests }),
271
+ ...(responseIdentity?.model === undefined ? {} : { model: responseIdentity.model }),
272
+ ...(responseIdentity?.system_fingerprint === undefined
273
+ ? {}
274
+ : { system_fingerprint: responseIdentity.system_fingerprint }),
275
+ };
276
+ }
@@ -0,0 +1,23 @@
1
+ import type { CliBridgeProviderOptions } from "./provider-options.js";
2
+ export interface CliBridgeTransport {
3
+ fetch(input: string, init: CliBridgeRequest): Promise<CliBridgeResponse>;
4
+ close(): Promise<void>;
5
+ }
6
+ export interface CliBridgeRequest {
7
+ method: "GET" | "POST";
8
+ headers: Record<string, string>;
9
+ body?: string;
10
+ signal?: AbortSignal;
11
+ }
12
+ export interface CliBridgeResponse {
13
+ readonly ok: boolean;
14
+ readonly status: number;
15
+ readonly body: AsyncIterable<Uint8Array> | null;
16
+ readonly headers: {
17
+ get(name: string): string | null;
18
+ };
19
+ text(): Promise<string>;
20
+ }
21
+ export declare function createCliBridgeTransport(options: CliBridgeProviderOptions): CliBridgeTransport;
22
+ export declare function requestHeaders(options: CliBridgeProviderOptions): Record<string, string>;
23
+ export declare function trimSlash(value: string): string;
@@ -0,0 +1,32 @@
1
+ import { Agent, fetch as undiciFetch } from "undici";
2
+ export function createCliBridgeTransport(options) {
3
+ if (options.fetch) {
4
+ const fetch = options.fetch;
5
+ return {
6
+ fetch: (input, init) => fetch(input, init),
7
+ close: async () => { },
8
+ };
9
+ }
10
+ const dispatcher = new Agent({
11
+ headersTimeout: options.headersTimeoutMs ?? 0,
12
+ bodyTimeout: options.bodyTimeoutMs ?? 0,
13
+ });
14
+ return {
15
+ fetch: (input, init) => undiciFetch(input, {
16
+ ...init,
17
+ dispatcher,
18
+ }),
19
+ close: async () => {
20
+ await dispatcher.close();
21
+ },
22
+ };
23
+ }
24
+ export function requestHeaders(options) {
25
+ return {
26
+ "content-type": "application/json",
27
+ ...(options.bearerToken ? { authorization: `Bearer ${options.bearerToken}` } : {}),
28
+ };
29
+ }
30
+ export function trimSlash(value) {
31
+ return value.replace(/\/+$/, "");
32
+ }
package/dist/wire.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { TokenUsage } from "@tangle-network/agent-interface";
2
+ import type { AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
3
+ import type { CliBridgeProviderOptions } from "./provider-options.js";
4
+ import type { CliBridgeRunSnapshot } from "./retained-run-state.js";
5
+ export declare function toChatCompletionsBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, runId: string): Record<string, unknown>;
6
+ export interface CliBridgeSseFrame {
7
+ readonly data: string;
8
+ readonly id?: string;
9
+ }
10
+ export declare function parseSse(body: AsyncIterable<Uint8Array>): AsyncIterable<CliBridgeSseFrame>;
11
+ export declare function toolCallsFromDelta(value: unknown): Array<{
12
+ id?: string;
13
+ index: number;
14
+ name?: string;
15
+ }>;
16
+ export declare function safeJson(value: string): Record<string, unknown> | null;
17
+ export declare function usageFromOpenAi(value: unknown): TokenUsage | undefined;
18
+ export declare function modelRequestsFromOpenAi(value: unknown): number | undefined;
19
+ /** Preserve the provider identity that cli-bridge returns on each completion frame. */
20
+ export declare function responseIdentityFromOpenAi(value: unknown): {
21
+ model?: string;
22
+ system_fingerprint?: string;
23
+ } | undefined;
24
+ export declare function modelRequestCount(value: unknown): number | undefined;
25
+ export declare function cliBridgeRunId(environmentId: string, turn: AgentTurnInput, turnId: string): string;
26
+ export declare function runSnapshot(value: string): CliBridgeRunSnapshot;
27
+ export declare function cancelSnapshot(value: string): CliBridgeRunSnapshot;
package/dist/wire.js ADDED
@@ -0,0 +1,243 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AgentExactRunControlRefSchema } from "@tangle-network/agent-interface";
3
+ export function toChatCompletionsBody(options, environmentInput, turn, runId) {
4
+ const profile = inlineProfile(environmentInput.profile);
5
+ const execution = executionFromInput(options, environmentInput);
6
+ return {
7
+ model: resolveBridgeModel(options, environmentInput, turn, profile),
8
+ messages: messagesFromTurn(turn),
9
+ stream: true,
10
+ ...(turn.sessionId ? { session_id: turn.sessionId } : {}),
11
+ run_id: runId,
12
+ ...(options.defaultMode ? { mode: options.defaultMode } : {}),
13
+ ...(profile ? { agent_profile: profile } : {}),
14
+ ...(profile?.model?.reasoningEffort
15
+ ? { effort: profile.model.reasoningEffort }
16
+ : {}),
17
+ ...(environmentInput.env ? { env: environmentInput.env } : {}),
18
+ ...(environmentInput.workspace?.cwd ? { cwd: environmentInput.workspace.cwd } : {}),
19
+ ...(execution ? { execution } : {}),
20
+ metadata: {
21
+ ...(environmentInput.metadata ?? {}),
22
+ ...(turn.context ?? {}),
23
+ ...(turn.providerOptions ?? {}),
24
+ },
25
+ };
26
+ }
27
+ function resolveBridgeModel(options, environmentInput, turn, profile) {
28
+ const harness = environmentInput.backend ?? profile?.harness;
29
+ const model = turn.model ?? options.defaultModel ?? profile?.model?.default;
30
+ const provider = profile?.model?.provider;
31
+ if (!harness) {
32
+ if (model)
33
+ return model;
34
+ throw new Error("createCliBridgeProvider requires an explicit bridge model or a profile/backend harness");
35
+ }
36
+ if (!model || model === harness)
37
+ return harness;
38
+ if (model.startsWith(`${harness}/`))
39
+ return model;
40
+ if (model.includes("/"))
41
+ return `${harness}/${model}`;
42
+ if (provider)
43
+ return `${harness}/${provider}/${model}`;
44
+ return `${harness}/${model}`;
45
+ }
46
+ /** Keep task messages separate from the profile's harness-owned prompt controls. */
47
+ function messagesFromTurn(turn) {
48
+ return [{ role: "user", content: contentFromTurn(turn) }];
49
+ }
50
+ function contentFromTurn(turn) {
51
+ if (turn.parts)
52
+ return turn.parts;
53
+ return turn.prompt ?? "";
54
+ }
55
+ function inlineProfile(profile) {
56
+ return typeof profile === "string" ? undefined : profile;
57
+ }
58
+ function executionFromInput(options, input) {
59
+ if (options.defaultExecution)
60
+ return options.defaultExecution;
61
+ if (!input.workspace?.repoUrl)
62
+ return undefined;
63
+ return {
64
+ kind: "sandbox",
65
+ repoUrl: input.workspace.repoUrl,
66
+ ...(input.workspace.gitRef ? { gitRef: input.workspace.gitRef } : {}),
67
+ };
68
+ }
69
+ export async function* parseSse(body) {
70
+ const decoder = new TextDecoder();
71
+ let buffer = "";
72
+ for await (const value of body) {
73
+ buffer += decoder.decode(value, { stream: true });
74
+ let boundary = findFrameBoundary(buffer);
75
+ while (boundary) {
76
+ const frame = buffer.slice(0, boundary.index);
77
+ buffer = buffer.slice(boundary.index + boundary.length);
78
+ const data = dataFromFrame(frame);
79
+ if (data)
80
+ yield data;
81
+ boundary = findFrameBoundary(buffer);
82
+ }
83
+ }
84
+ buffer += decoder.decode();
85
+ if (buffer) {
86
+ const data = dataFromFrame(buffer);
87
+ if (data)
88
+ yield data;
89
+ }
90
+ }
91
+ function findFrameBoundary(value) {
92
+ const lf = value.indexOf("\n\n");
93
+ const crlf = value.indexOf("\r\n\r\n");
94
+ if (lf === -1 && crlf === -1)
95
+ return undefined;
96
+ if (crlf !== -1 && (lf === -1 || crlf < lf))
97
+ return { index: crlf, length: 4 };
98
+ return { index: lf, length: 2 };
99
+ }
100
+ function dataFromFrame(frame) {
101
+ const lines = frame.split(/\r?\n/);
102
+ const data = lines
103
+ .filter((line) => line.startsWith("data:"))
104
+ .map((line) => line.slice("data:".length).trimStart())
105
+ .join("\n");
106
+ if (!data)
107
+ return undefined;
108
+ const id = lines.find((line) => line.startsWith("id:"))?.slice("id:".length).trim();
109
+ return { data, ...(id ? { id } : {}) };
110
+ }
111
+ export function toolCallsFromDelta(value) {
112
+ if (!value || typeof value !== "object")
113
+ return [];
114
+ const calls = value.tool_calls;
115
+ if (!Array.isArray(calls))
116
+ return [];
117
+ return calls.flatMap((call, position) => {
118
+ if (!call || typeof call !== "object")
119
+ return [];
120
+ const record = call;
121
+ const fn = record.function;
122
+ const name = fn && typeof fn === "object" && typeof fn.name === "string"
123
+ ? fn.name
124
+ : undefined;
125
+ const index = nonNegativeSafeInteger(record.index) ?? position;
126
+ const id = typeof record.id === "string" ? record.id : undefined;
127
+ return [{ ...(id ? { id } : {}), index, ...(name ? { name } : {}) }];
128
+ });
129
+ }
130
+ export function safeJson(value) {
131
+ try {
132
+ const parsed = JSON.parse(value);
133
+ return parsed && typeof parsed === "object" ? parsed : null;
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
139
+ export function usageFromOpenAi(value) {
140
+ if (!value || typeof value !== "object")
141
+ return undefined;
142
+ const record = value;
143
+ const inputTokens = nonNegativeSafeInteger(record.prompt_tokens) ??
144
+ nonNegativeSafeInteger(record.input_tokens);
145
+ const outputTokens = nonNegativeSafeInteger(record.completion_tokens) ??
146
+ nonNegativeSafeInteger(record.output_tokens);
147
+ if (inputTokens === undefined || outputTokens === undefined)
148
+ return undefined;
149
+ const totalTokens = nonNegativeSafeInteger(record.total_tokens);
150
+ const cacheReadInputTokens = nonNegativeSafeInteger(record.cache_read_input_tokens);
151
+ const cacheCreationInputTokens = nonNegativeSafeInteger(record.cache_creation_input_tokens);
152
+ const reasoningTokens = nonNegativeSafeInteger(record.reasoning_tokens);
153
+ const cost = finiteNonNegativeNumber(record.cost);
154
+ return {
155
+ inputTokens,
156
+ outputTokens,
157
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
158
+ ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
159
+ ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
160
+ ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
161
+ ...(cost !== undefined ? { cost } : {}),
162
+ };
163
+ }
164
+ export function modelRequestsFromOpenAi(value) {
165
+ if (!value || typeof value !== "object")
166
+ return undefined;
167
+ return modelRequestCount(value.model_requests);
168
+ }
169
+ /** Preserve the provider identity that cli-bridge returns on each completion frame. */
170
+ export function responseIdentityFromOpenAi(value) {
171
+ if (!value || typeof value !== "object")
172
+ return undefined;
173
+ const record = value;
174
+ const model = nonEmptyString(record.model);
175
+ const systemFingerprint = nonEmptyString(record.system_fingerprint);
176
+ if (model === undefined && systemFingerprint === undefined)
177
+ return undefined;
178
+ return {
179
+ ...(model === undefined ? {} : { model }),
180
+ ...(systemFingerprint === undefined ? {} : { system_fingerprint: systemFingerprint }),
181
+ };
182
+ }
183
+ export function modelRequestCount(value) {
184
+ return nonNegativeSafeInteger(value);
185
+ }
186
+ function nonNegativeSafeInteger(value) {
187
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
188
+ ? value
189
+ : undefined;
190
+ }
191
+ function finiteNonNegativeNumber(value) {
192
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
193
+ ? value
194
+ : undefined;
195
+ }
196
+ function nonEmptyString(value) {
197
+ return typeof value === "string" && value.length > 0 ? value : undefined;
198
+ }
199
+ export function cliBridgeRunId(environmentId, turn, turnId) {
200
+ if (turn.executionId &&
201
+ turn.executionId.length <= 128 &&
202
+ /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(turn.executionId)) {
203
+ return turn.executionId;
204
+ }
205
+ const digest = createHash("sha256")
206
+ .update(environmentId)
207
+ .update("\0")
208
+ .update(turn.sessionId ?? "")
209
+ .update("\0")
210
+ .update(turn.executionId ?? turnId)
211
+ .digest("hex");
212
+ return `agent-${digest}`;
213
+ }
214
+ export function runSnapshot(value) {
215
+ const parsed = safeJson(value);
216
+ if (!parsed)
217
+ throw new Error("cli-bridge run status returned invalid JSON");
218
+ const id = parsed.id;
219
+ const requestDigest = parsed.requestDigest === undefined
220
+ ? undefined
221
+ : AgentExactRunControlRefSchema.shape.requestDigest.safeParse(parsed.requestDigest);
222
+ const status = parsed.status;
223
+ const terminal = parsed.terminal;
224
+ if (typeof id !== "string" ||
225
+ (requestDigest !== undefined && !requestDigest.success) ||
226
+ !["running", "done", "error", "cancelled"].includes(String(status)) ||
227
+ typeof terminal !== "boolean") {
228
+ throw new Error("cli-bridge run status returned an invalid snapshot");
229
+ }
230
+ return {
231
+ id,
232
+ ...(requestDigest?.success ? { requestDigest: requestDigest.data } : {}),
233
+ status: status,
234
+ terminal,
235
+ };
236
+ }
237
+ export function cancelSnapshot(value) {
238
+ const parsed = safeJson(value);
239
+ if (!parsed || !parsed.run || typeof parsed.run !== "object") {
240
+ throw new Error("cli-bridge cancel returned an invalid snapshot");
241
+ }
242
+ return runSnapshot(JSON.stringify(parsed.run));
243
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,12 +25,30 @@
25
25
  "files": [
26
26
  "dist/index.d.ts",
27
27
  "dist/index.js",
28
+ "dist/provider-options.d.ts",
29
+ "dist/provider-options.js",
30
+ "dist/retained-control.d.ts",
31
+ "dist/retained-control.js",
32
+ "dist/retained-environment.d.ts",
33
+ "dist/retained-environment.js",
34
+ "dist/retained-event-cursor.d.ts",
35
+ "dist/retained-event-cursor.js",
36
+ "dist/retained-execution.d.ts",
37
+ "dist/retained-execution.js",
38
+ "dist/retained-run-state.d.ts",
39
+ "dist/retained-run-state.js",
40
+ "dist/retained-stream.d.ts",
41
+ "dist/retained-stream.js",
42
+ "dist/transport.d.ts",
43
+ "dist/transport.js",
44
+ "dist/wire.d.ts",
45
+ "dist/wire.js",
28
46
  "README.md",
29
47
  "LICENSE"
30
48
  ],
31
49
  "dependencies": {
32
- "@tangle-network/agent-interface": "0.47.0",
33
- "undici": "7.28.0"
50
+ "@tangle-network/agent-interface": "0.48.0",
51
+ "undici": "7.29.0"
34
52
  },
35
53
  "devDependencies": {
36
54
  "@types/node": "25.6.0",