@tangle-network/agent-provider-cli-bridge 0.7.7 → 0.8.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.
@@ -0,0 +1,204 @@
1
+ import { AgentExactRunControlRefSchema, RequestedInteractionsSchema, canonicalCandidateDigest, } from "@tangle-network/agent-interface";
2
+ import { CliBridgeRequestRejectedError } from "./retained-stream.js";
3
+ import { requestHeaders, trimSlash, } from "./transport.js";
4
+ import { inlineProfile, safeJson, resolveBridgeModel, toRetainedSessionBody, toRetainedTurnBody, } from "./wire.js";
5
+ /** Interactions are valid only for the native retained Pi contract. */
6
+ export function supportsCliBridgeNativeInteractions(capabilities, selectedBackend) {
7
+ const interactions = capabilities.interactions;
8
+ const retainedControl = capabilities.retainedControl;
9
+ return Boolean(selectedBackend === "pi" &&
10
+ piInteractionCapabilitiesMatch(interactions) &&
11
+ capabilities.streaming.live &&
12
+ capabilities.streaming.replay &&
13
+ capabilities.streaming.detach &&
14
+ capabilities.streaming.turnIdempotency &&
15
+ capabilities.sessions.continue &&
16
+ retainedControl?.exactRunIdentity &&
17
+ retainedControl.resultIdentity &&
18
+ retainedControl.eventIdentity &&
19
+ retainedControl.cancellationIdempotency);
20
+ }
21
+ /** Reject posture the selected environment did not advertise before transport use. */
22
+ export function assertCliBridgeRequestedInteractions(capabilities, requested) {
23
+ if (requested === undefined)
24
+ return;
25
+ const parsed = RequestedInteractionsSchema.parse(requested);
26
+ const advertised = new Set(capabilities.interactions?.kinds ?? []);
27
+ const unsupported = Object.keys(parsed).filter((kind) => !advertised.has(kind));
28
+ if (unsupported.length > 0) {
29
+ throw new Error(`cli-bridge environment does not advertise requested interaction kind(s): ${unsupported.join(", ")}`);
30
+ }
31
+ }
32
+ /** Keep capability discovery and every native turn on one exact model route. */
33
+ export function assertCliBridgeNativeTurnModel(options, environmentInput, turn, selectedModel) {
34
+ if (selectedModel === undefined) {
35
+ throw new Error("cli-bridge native interactions require an environment model");
36
+ }
37
+ const requestedModel = resolveBridgeModel(options, environmentInput, turn, inlineProfile(environmentInput.profile));
38
+ if (requestedModel !== selectedModel) {
39
+ throw new Error(`cli-bridge native environment model is ${JSON.stringify(selectedModel)}; create another environment for ${JSON.stringify(requestedModel)}`);
40
+ }
41
+ }
42
+ function piInteractionCapabilitiesMatch(interactions) {
43
+ return Boolean(interactions &&
44
+ interactions.kinds.includes("permission") &&
45
+ interactions.answerFieldTypes.includes("select") &&
46
+ interactions.responseScopes.includes("interaction") &&
47
+ interactions.secretAnswers === false &&
48
+ interactions.concurrentRequests === false &&
49
+ interactions.replay === true &&
50
+ interactions.responseIdempotency === true);
51
+ }
52
+ /**
53
+ * Create or reuse the server's retained native session for one external id.
54
+ *
55
+ * The cache stores the promise so concurrent first turns cannot create two
56
+ * native sessions for the same id.
57
+ */
58
+ export async function ensureCliBridgeNativeSession(options, environmentInput, turn, sessionId, transport, sessions, signal) {
59
+ const profile = inlineProfile(environmentInput.profile);
60
+ const model = resolveBridgeModel(options, environmentInput, turn, profile);
61
+ const existing = sessions.get(sessionId);
62
+ if (existing) {
63
+ const session = await existing;
64
+ assertNativeSessionModel(session, model);
65
+ return session;
66
+ }
67
+ const pending = createCliBridgeNativeSession(options, environmentInput, profile, sessionId, model, transport, signal);
68
+ sessions.set(sessionId, pending);
69
+ try {
70
+ const session = await pending;
71
+ assertNativeSessionModel(session, model);
72
+ return session;
73
+ }
74
+ catch (error) {
75
+ if (sessions.get(sessionId) === pending)
76
+ sessions.delete(sessionId);
77
+ throw error;
78
+ }
79
+ }
80
+ async function createCliBridgeNativeSession(options, environmentInput, profile, sessionId, model, transport, signal) {
81
+ const body = toRetainedSessionBody(options, environmentInput, profile, sessionId, model);
82
+ const createRequestDigest = canonicalCandidateDigest(body);
83
+ const url = `${trimSlash(options.baseUrl)}/v1/sessions`;
84
+ const init = {
85
+ method: "POST",
86
+ headers: requestHeaders(options),
87
+ body: JSON.stringify(body),
88
+ ...(signal ? { signal } : {}),
89
+ };
90
+ const response = await transport.fetch(url, init);
91
+ if (response.status === 409) {
92
+ return recoverConflictingNativeSession(options, sessionId, model, createRequestDigest, transport, signal, response);
93
+ }
94
+ if (!response.ok) {
95
+ throw new CliBridgeRequestRejectedError(response.status, await response.text());
96
+ }
97
+ return parseNativeSessionView(await response.text(), sessionId, model, createRequestDigest);
98
+ }
99
+ async function recoverConflictingNativeSession(options, sessionId, model, createRequestDigest, transport, signal, conflictResponse) {
100
+ await conflictResponse.text();
101
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/sessions/${encodeURIComponent(sessionId)}`, {
102
+ method: "GET",
103
+ headers: requestHeaders(options),
104
+ ...(signal ? { signal } : {}),
105
+ });
106
+ if (!response.ok) {
107
+ throw new CliBridgeRequestRejectedError(response.status, `session ${JSON.stringify(sessionId)} could not be recovered: ${await response.text()}`);
108
+ }
109
+ return parseNativeSessionView(await response.text(), sessionId, model, createRequestDigest);
110
+ }
111
+ function parseNativeSessionView(value, expectedId, expectedModel, expectedCreateRequestDigest) {
112
+ const parsed = safeJson(value);
113
+ const id = parsed?.id;
114
+ const model = parsed?.model;
115
+ const digest = parsed?.create_request_digest;
116
+ const parsedDigest = AgentExactRunControlRefSchema.shape.requestDigest.safeParse(digest);
117
+ if (typeof id !== "string" ||
118
+ typeof model !== "string" ||
119
+ !parsedDigest.success) {
120
+ throw new Error("cli-bridge retained session returned an invalid session view");
121
+ }
122
+ if (id !== expectedId) {
123
+ throw new Error(`cli-bridge returned session ${JSON.stringify(id)} for requested session ${JSON.stringify(expectedId)}`);
124
+ }
125
+ if (model !== expectedModel) {
126
+ throw new Error(`cli-bridge retained session ${JSON.stringify(expectedId)} is bound to model ${JSON.stringify(model)}`);
127
+ }
128
+ if (parsedDigest.data !== expectedCreateRequestDigest) {
129
+ throw new Error(`cli-bridge retained session ${JSON.stringify(expectedId)} has a different creation request digest`);
130
+ }
131
+ return {
132
+ id,
133
+ model,
134
+ createRequestDigest: parsedDigest.data,
135
+ };
136
+ }
137
+ function assertNativeSessionModel(session, expectedModel) {
138
+ if (session.model !== expectedModel) {
139
+ throw new Error(`cli-bridge retained session ${JSON.stringify(session.id)} cannot change model from ${JSON.stringify(session.model)} to ${JSON.stringify(expectedModel)}`);
140
+ }
141
+ }
142
+ /** Admit one native retained turn and bind its server digest to the run. */
143
+ export async function beginCliBridgeNativeTurn(options, providerName, environmentInput, turn, run, transport, sessions, signal, onAdmission) {
144
+ if (!run.sessionId) {
145
+ throw new Error("cli-bridge native retained turns require a session id");
146
+ }
147
+ const body = toRetainedTurnBody(turn, run, providerName);
148
+ const nativeSession = await ensureCliBridgeNativeSession(options, environmentInput, turn, run.sessionId, transport, sessions, signal);
149
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/sessions/${encodeURIComponent(run.sessionId)}/turns`, {
150
+ method: "POST",
151
+ headers: requestHeaders(options),
152
+ body: JSON.stringify(body),
153
+ ...(signal ? { signal } : {}),
154
+ });
155
+ if (!response.ok) {
156
+ throw new CliBridgeRequestRejectedError(response.status, await response.text());
157
+ }
158
+ onAdmission?.();
159
+ const parsed = safeJson(await response.text());
160
+ const responseSession = parsed?.session;
161
+ const responseRun = parsed?.run;
162
+ const responseSessionId = responseSession && typeof responseSession === "object"
163
+ ? responseSession.id
164
+ : undefined;
165
+ const responseSessionModel = responseSession && typeof responseSession === "object"
166
+ ? responseSession.model
167
+ : undefined;
168
+ const responseSessionDigest = responseSession && typeof responseSession === "object"
169
+ ? responseSession.create_request_digest
170
+ : undefined;
171
+ const parsedSessionDigest = AgentExactRunControlRefSchema.shape.requestDigest.safeParse(responseSessionDigest);
172
+ const runId = responseRun && typeof responseRun === "object"
173
+ ? responseRun.id
174
+ : undefined;
175
+ const responseSessionIdFromRun = responseRun && typeof responseRun === "object"
176
+ ? responseRun.sessionId
177
+ : undefined;
178
+ const executionId = responseRun && typeof responseRun === "object"
179
+ ? responseRun.executionId
180
+ : undefined;
181
+ const requestDigestValue = responseRun && typeof responseRun === "object"
182
+ ? responseRun.requestDigest
183
+ : undefined;
184
+ const requestDigest = AgentExactRunControlRefSchema.shape.requestDigest.safeParse(requestDigestValue);
185
+ if (!parsed ||
186
+ typeof responseSessionId !== "string" ||
187
+ responseSessionId !== run.sessionId ||
188
+ responseSessionModel !== nativeSession.model ||
189
+ !parsedSessionDigest.success ||
190
+ parsedSessionDigest.data !== nativeSession.createRequestDigest ||
191
+ typeof runId !== "string" ||
192
+ runId !== run.id ||
193
+ typeof responseSessionIdFromRun !== "string" ||
194
+ responseSessionIdFromRun !== run.sessionId ||
195
+ typeof executionId !== "string" ||
196
+ executionId !== run.executionId ||
197
+ !requestDigest.success) {
198
+ throw new Error("cli-bridge retained turn returned mismatched run coordinates");
199
+ }
200
+ if (run.requestDigest !== undefined && run.requestDigest !== requestDigest.data) {
201
+ throw new Error("cli-bridge retained turn changed the admitted request digest");
202
+ }
203
+ run.requestDigest = requestDigest.data;
204
+ }
@@ -20,7 +20,7 @@ export interface CliBridgeSessionState {
20
20
  export interface CliBridgeRunSnapshot {
21
21
  readonly id: string;
22
22
  readonly requestDigest?: Sha256Digest;
23
- readonly status: "running" | "done" | "error" | "cancelled";
23
+ readonly status: "running" | "done" | "error" | "cancelled" | "unknown";
24
24
  readonly terminal: boolean;
25
25
  }
26
26
  export interface PreparedCliBridgeRun {
@@ -46,6 +46,10 @@ export function assertRunMatchesControlRef(run, controlRef) {
46
46
  run.controlRef = controlRef;
47
47
  }
48
48
  export function prepareCliBridgeRun(options, environmentInput, originalTurn, environmentId, requireSession) {
49
+ if (requireSession &&
50
+ (originalTurn.turnId === undefined || originalTurn.executionId === undefined)) {
51
+ throw new Error("cli-bridge native retained turns require stable turnId and executionId values");
52
+ }
49
53
  const turnId = originalTurn.turnId ?? crypto.randomUUID();
50
54
  const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
51
55
  const sessionId = originalTurn.sessionId ?? (requireSession ? runId : undefined);
@@ -6,4 +6,4 @@ export declare class CliBridgeRequestRejectedError extends Error {
6
6
  readonly status: number;
7
7
  constructor(status: number, detail: string);
8
8
  }
9
- export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal, onResponse?: (response: CliBridgeResponse) => void, readAuthoritativeRun?: () => Promise<CliBridgeRunSnapshot | null>): AsyncIterable<AgentEnvironmentEvent>;
9
+ export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal, onResponse?: (response: CliBridgeResponse) => void, readAuthoritativeRun?: () => Promise<CliBridgeRunSnapshot | null>, validateCanonicalEvent?: (event: AgentEnvironmentEvent) => void): AsyncIterable<AgentEnvironmentEvent>;
@@ -1,4 +1,5 @@
1
1
  import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-event-cursor.js";
2
+ import { isTerminalCanonicalEvent, retainedCanonicalEvent, } from "./retained-canonical-event.js";
2
3
  import { detachCliBridgeReader } from "./retained-control.js";
3
4
  import { requestHeaders, trimSlash } from "./transport.js";
4
5
  import { modelRequestsFromOpenAi, parseSse, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
@@ -18,7 +19,7 @@ class CliBridgeRunCancelledError extends Error {
18
19
  this.name = "CliBridgeRunCancelledError";
19
20
  }
20
21
  }
21
- export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun) {
22
+ export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun, validateCanonicalEvent) {
22
23
  const reconnect = requestBody === undefined;
23
24
  const replay = retainedReplayRequest(lastEventId);
24
25
  const response = await transport.fetch(reconnect
@@ -72,14 +73,53 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
72
73
  let systemFingerprint;
73
74
  let textBoundaryPending = false;
74
75
  let sawProtocolEnd = false;
76
+ let protocol = "unknown";
77
+ let canonicalTerminalStatus;
78
+ let canonicalTerminalEvent;
75
79
  let terminalFrameSeen = false;
76
80
  let cancelledTerminalSeen = false;
77
81
  let terminalFrame;
78
82
  for await (const frame of parseSse(response.body)) {
79
83
  if (frame.data === "[DONE]") {
84
+ if (protocol === "canonical") {
85
+ throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
86
+ }
87
+ protocol = "openai";
80
88
  sawProtocolEnd = true;
81
89
  continue;
82
90
  }
91
+ if (frame.event !== undefined) {
92
+ if (protocol === "openai") {
93
+ throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
94
+ }
95
+ protocol = "canonical";
96
+ if (canonicalTerminalStatus !== undefined) {
97
+ throw new Error("cli-bridge emitted an event after canonical terminal status");
98
+ }
99
+ const event = retainedCanonicalEvent(frame, {
100
+ runId,
101
+ sessionId,
102
+ ...(turn.executionId === undefined
103
+ ? {}
104
+ : { executionId: turn.executionId }),
105
+ });
106
+ if (!event) {
107
+ throw new Error("cli-bridge emitted an empty canonical event");
108
+ }
109
+ validateCanonicalEvent?.(event);
110
+ if (isTerminalCanonicalEvent(event)) {
111
+ canonicalTerminalStatus = event.normalized.status;
112
+ canonicalTerminalEvent = event;
113
+ }
114
+ else {
115
+ yield event;
116
+ }
117
+ continue;
118
+ }
119
+ if (protocol === "canonical") {
120
+ throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
121
+ }
122
+ protocol = "openai";
83
123
  if (terminalFrameSeen)
84
124
  continue;
85
125
  const parsed = safeJson(frame.data);
@@ -219,6 +259,32 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
219
259
  }
220
260
  yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
221
261
  }
262
+ if (protocol === "canonical") {
263
+ if (canonicalTerminalStatus === undefined) {
264
+ throw new Error("cli-bridge canonical stream ended without terminal status");
265
+ }
266
+ const snapshot = await terminalRunSnapshot(runId, readAuthoritativeRun);
267
+ const expectedStatus = canonicalTerminalStatus === "completed"
268
+ ? "done"
269
+ : canonicalTerminalStatus === "failed"
270
+ ? "error"
271
+ : "cancelled";
272
+ if (snapshot.status !== expectedStatus) {
273
+ throw new Error(`cli-bridge canonical terminal status ${JSON.stringify(canonicalTerminalStatus)} contradicts retained run status ${JSON.stringify(snapshot.status)}`);
274
+ }
275
+ if (!canonicalTerminalEvent) {
276
+ throw new Error("cli-bridge canonical terminal event was not retained");
277
+ }
278
+ yield canonicalTerminalEvent;
279
+ return;
280
+ }
281
+ if (!sawProtocolEnd &&
282
+ reconnect &&
283
+ lastEventId !== undefined &&
284
+ protocol === "unknown") {
285
+ await terminalRunSnapshot(runId, readAuthoritativeRun);
286
+ return;
287
+ }
222
288
  if (!sawProtocolEnd) {
223
289
  throw new Error("cli-bridge stream ended without the [DONE] protocol marker");
224
290
  }
package/dist/wire.d.ts CHANGED
@@ -1,11 +1,17 @@
1
- import type { TokenUsage } from "@tangle-network/agent-interface";
1
+ import type { AgentProfile, AgentProfileRef, TokenUsage } from "@tangle-network/agent-interface";
2
2
  import type { AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
3
3
  import type { CliBridgeProviderOptions } from "./provider-options.js";
4
- import type { CliBridgeRunSnapshot } from "./retained-run-state.js";
4
+ import type { CliBridgeRun, CliBridgeRunSnapshot } from "./retained-run-state.js";
5
5
  export declare function toChatCompletionsBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, runId: string): Record<string, unknown>;
6
+ export declare function resolveBridgeModel(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, profile: AgentProfile | undefined): string;
7
+ export declare function toRetainedSessionBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, profile: AgentProfile | undefined, sessionId: string, model: string): Record<string, unknown>;
8
+ /** Encode the exact retained turn contract without moving posture into metadata. */
9
+ export declare function toRetainedTurnBody(turn: AgentTurnInput, run: CliBridgeRun, providerName: string): Record<string, unknown>;
10
+ export declare function inlineProfile(profile: AgentProfileRef): AgentProfile | undefined;
6
11
  export interface CliBridgeSseFrame {
7
12
  readonly data: string;
8
13
  readonly id?: string;
14
+ readonly event?: string;
9
15
  }
10
16
  export declare function parseSse(body: AsyncIterable<Uint8Array>): AsyncIterable<CliBridgeSseFrame>;
11
17
  export declare function toolCallsFromDelta(value: unknown): Array<{
package/dist/wire.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
- import { AgentExactRunControlRefSchema } from "@tangle-network/agent-interface";
2
+ import { AgentExactRunControlRefSchema, RequestedInteractionsSchema, } from "@tangle-network/agent-interface";
3
3
  export function toChatCompletionsBody(options, environmentInput, turn, runId) {
4
4
  const profile = inlineProfile(environmentInput.profile);
5
5
  const execution = executionFromInput(options, environmentInput);
6
+ const interactions = turn.interactions === undefined
7
+ ? undefined
8
+ : RequestedInteractionsSchema.parse(turn.interactions);
6
9
  return {
7
10
  model: resolveBridgeModel(options, environmentInput, turn, profile),
8
11
  messages: messagesFromTurn(turn),
@@ -17,6 +20,7 @@ export function toChatCompletionsBody(options, environmentInput, turn, runId) {
17
20
  ...(environmentInput.env ? { env: environmentInput.env } : {}),
18
21
  ...(environmentInput.workspace?.cwd ? { cwd: environmentInput.workspace.cwd } : {}),
19
22
  ...(execution ? { execution } : {}),
23
+ ...(interactions === undefined ? {} : { interactions }),
20
24
  metadata: {
21
25
  ...(environmentInput.metadata ?? {}),
22
26
  ...(turn.context ?? {}),
@@ -24,7 +28,7 @@ export function toChatCompletionsBody(options, environmentInput, turn, runId) {
24
28
  },
25
29
  };
26
30
  }
27
- function resolveBridgeModel(options, environmentInput, turn, profile) {
31
+ export function resolveBridgeModel(options, environmentInput, turn, profile) {
28
32
  const harness = environmentInput.backend ?? profile?.harness;
29
33
  const model = turn.model ?? options.defaultModel ?? profile?.model?.default;
30
34
  const provider = profile?.model?.provider;
@@ -43,6 +47,77 @@ function resolveBridgeModel(options, environmentInput, turn, profile) {
43
47
  return `${harness}/${provider}/${model}`;
44
48
  return `${harness}/${model}`;
45
49
  }
50
+ export function toRetainedSessionBody(options, environmentInput, profile, sessionId, model) {
51
+ const execution = retainedExecutionFromInput(options, environmentInput);
52
+ return {
53
+ id: sessionId,
54
+ model,
55
+ interaction_policy: "interactive",
56
+ ...(options.defaultMode ? { mode: options.defaultMode } : {}),
57
+ ...(environmentInput.workspace?.cwd !== undefined
58
+ ? { cwd: environmentInput.workspace.cwd }
59
+ : {}),
60
+ ...(execution !== undefined ? { execution } : {}),
61
+ ...(profile ? { agent_profile: profile } : {}),
62
+ ...(environmentInput.env !== undefined ? { env: environmentInput.env } : {}),
63
+ ...(environmentInput.metadata !== undefined
64
+ ? { metadata: environmentInput.metadata }
65
+ : {}),
66
+ ...(environmentInput.providerOptions !== undefined
67
+ ? { provider_options: environmentInput.providerOptions }
68
+ : {}),
69
+ };
70
+ }
71
+ /** Encode the exact retained turn contract without moving posture into metadata. */
72
+ export function toRetainedTurnBody(turn, run, providerName) {
73
+ assertRetainedTurnInputSupported(turn);
74
+ const interactions = RequestedInteractionsSchema.parse(turn.interactions ?? {});
75
+ const content = {};
76
+ if (turn.prompt !== undefined) {
77
+ if (turn.prompt.length === 0) {
78
+ throw new Error("cli-bridge retained sessions require a non-empty message");
79
+ }
80
+ content.message = turn.prompt;
81
+ }
82
+ if (turn.parts !== undefined) {
83
+ if (turn.parts.length === 0) {
84
+ throw new Error("cli-bridge retained sessions require non-empty input parts");
85
+ }
86
+ content.parts = turn.parts;
87
+ }
88
+ if (turn.prompt === undefined && turn.parts === undefined) {
89
+ throw new Error("cli-bridge retained sessions require a non-empty message or input parts");
90
+ }
91
+ return {
92
+ ...content,
93
+ turn_id: run.turnId,
94
+ execution_id: run.executionId,
95
+ run_id: run.id,
96
+ provider: providerName,
97
+ environment_id: run.environmentId,
98
+ ...(turn.interactions === undefined ? {} : { interactions }),
99
+ ...(turn.context !== undefined ? { context: turn.context } : {}),
100
+ ...(turn.providerOptions !== undefined
101
+ ? { provider_options: turn.providerOptions }
102
+ : {}),
103
+ };
104
+ }
105
+ function assertRetainedTurnInputSupported(turn) {
106
+ const unsupported = [];
107
+ if (turn.timeoutMs !== undefined)
108
+ unsupported.push("timeoutMs");
109
+ if (turn.controlRef !== undefined)
110
+ unsupported.push("controlRef");
111
+ if (turn.contextTransfer !== undefined)
112
+ unsupported.push("contextTransfer");
113
+ if (turn.nativeContinuation !== undefined)
114
+ unsupported.push("nativeContinuation");
115
+ if (turn.detach === true)
116
+ unsupported.push("detach");
117
+ if (unsupported.length > 0) {
118
+ throw new Error(`cli-bridge retained turns cannot represent ${unsupported.join(", ")}`);
119
+ }
120
+ }
46
121
  /** Keep task messages separate from the profile's harness-owned prompt controls. */
47
122
  function messagesFromTurn(turn) {
48
123
  return [{ role: "user", content: contentFromTurn(turn) }];
@@ -52,7 +127,7 @@ function contentFromTurn(turn) {
52
127
  return turn.parts;
53
128
  return turn.prompt ?? "";
54
129
  }
55
- function inlineProfile(profile) {
130
+ export function inlineProfile(profile) {
56
131
  return typeof profile === "string" ? undefined : profile;
57
132
  }
58
133
  function executionFromInput(options, input) {
@@ -66,6 +141,34 @@ function executionFromInput(options, input) {
66
141
  ...(input.workspace.gitRef ? { gitRef: input.workspace.gitRef } : {}),
67
142
  };
68
143
  }
144
+ function retainedExecutionFromInput(options, input) {
145
+ const unsupported = [];
146
+ if (typeof input.profile === "string")
147
+ unsupported.push("named profile");
148
+ if (input.workspace?.environment !== undefined)
149
+ unsupported.push("workspace.environment");
150
+ if (input.workspace?.image !== undefined)
151
+ unsupported.push("workspace.image");
152
+ if (input.workspace?.repoUrl !== undefined)
153
+ unsupported.push("workspace.repoUrl");
154
+ if (input.workspace?.gitRef !== undefined)
155
+ unsupported.push("workspace.gitRef");
156
+ if (input.workspace?.providerOptions !== undefined) {
157
+ unsupported.push("workspace.providerOptions");
158
+ }
159
+ if (input.resources !== undefined)
160
+ unsupported.push("resources");
161
+ if (input.secrets !== undefined)
162
+ unsupported.push("secrets");
163
+ if (unsupported.length > 0) {
164
+ throw new Error(`cli-bridge retained sessions cannot represent ${unsupported.join(", ")}`);
165
+ }
166
+ const execution = executionFromInput(options, input);
167
+ if (execution?.kind === "sandbox") {
168
+ throw new Error("cli-bridge retained sessions cannot execute in a sandbox; use one-shot execution");
169
+ }
170
+ return execution;
171
+ }
69
172
  export async function* parseSse(body) {
70
173
  const decoder = new TextDecoder();
71
174
  let buffer = "";
@@ -106,7 +209,11 @@ function dataFromFrame(frame) {
106
209
  if (!data)
107
210
  return undefined;
108
211
  const id = lines.find((line) => line.startsWith("id:"))?.slice("id:".length).trim();
109
- return { data, ...(id ? { id } : {}) };
212
+ const event = lines
213
+ .find((line) => line.startsWith("event:"))
214
+ ?.slice("event:".length)
215
+ .trim();
216
+ return { data, ...(id ? { id } : {}), ...(event ? { event } : {}) };
110
217
  }
111
218
  export function toolCallsFromDelta(value) {
112
219
  if (!value || typeof value !== "object")
@@ -223,7 +330,7 @@ export function runSnapshot(value) {
223
330
  const terminal = parsed.terminal;
224
331
  if (typeof id !== "string" ||
225
332
  (requestDigest !== undefined && !requestDigest.success) ||
226
- !["running", "done", "error", "cancelled"].includes(String(status)) ||
333
+ !["running", "done", "error", "cancelled", "unknown"].includes(String(status)) ||
227
334
  typeof terminal !== "boolean") {
228
335
  throw new Error("cli-bridge run status returned an invalid snapshot");
229
336
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "0.7.7",
4
- "description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
3
+ "version": "0.8.0",
4
+ "description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./dist/index.js",
@@ -25,10 +25,18 @@
25
25
  "files": [
26
26
  "dist/index.d.ts",
27
27
  "dist/index.js",
28
+ "dist/environment-identity.d.ts",
29
+ "dist/environment-identity.js",
30
+ "dist/capability-discovery.d.ts",
31
+ "dist/capability-discovery.js",
32
+ "dist/interaction-response.d.ts",
33
+ "dist/interaction-response.js",
28
34
  "dist/observation.d.ts",
29
35
  "dist/observation.js",
30
36
  "dist/provider-options.d.ts",
31
37
  "dist/provider-options.js",
38
+ "dist/retained-canonical-event.d.ts",
39
+ "dist/retained-canonical-event.js",
32
40
  "dist/retained-control.d.ts",
33
41
  "dist/retained-control.js",
34
42
  "dist/retained-environment.d.ts",
@@ -37,6 +45,8 @@
37
45
  "dist/retained-event-cursor.js",
38
46
  "dist/retained-execution.d.ts",
39
47
  "dist/retained-execution.js",
48
+ "dist/retained-native.d.ts",
49
+ "dist/retained-native.js",
40
50
  "dist/retained-run-state.d.ts",
41
51
  "dist/retained-run-state.js",
42
52
  "dist/retained-stream.d.ts",
@@ -58,9 +68,10 @@
58
68
  "vitest": "^4.1.5"
59
69
  },
60
70
  "scripts": {
61
- "build": "tsc -p tsconfig.json",
71
+ "build": "pnpm run clean && tsc -p tsconfig.build.json",
62
72
  "check-types": "tsc --noEmit",
63
73
  "clean": "rm -rf dist",
64
- "test": "vitest run"
74
+ "test": "vitest run src",
75
+ "test:integration": "test -n \"$CLI_BRIDGE_INTEGRATION_ROOT\" || { echo 'CLI_BRIDGE_INTEGRATION_ROOT is required; see README' >&2; exit 1; }; vitest run tests/cli-bridge.integration.test.ts"
65
76
  }
66
77
  }