@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,135 @@
1
+ import { RuntimeEventEnvelopeSchema, } from "@tangle-network/agent-interface";
2
+ /** Decode one explicitly typed retained-run frame without guessing its wire format. */
3
+ export function retainedCanonicalEvent(frame, context) {
4
+ if (frame.event === undefined)
5
+ return undefined;
6
+ if (frame.event === "error")
7
+ throw retainedCanonicalStreamError(frame.data);
8
+ const envelope = parseEnvelope(frame.data);
9
+ if (envelope.runId !== context.runId) {
10
+ throw new Error("cli-bridge canonical event belongs to another retained run");
11
+ }
12
+ if (frame.event !== envelope.event.type) {
13
+ throw new Error("cli-bridge SSE event type does not match its canonical envelope");
14
+ }
15
+ if (frame.id === undefined) {
16
+ throw new Error("cli-bridge canonical event has no replay cursor");
17
+ }
18
+ if (!isBaseTenInteger(frame.id) ||
19
+ envelope.sequence === 0 ||
20
+ Number(frame.id) !== envelope.sequence) {
21
+ throw new Error("cli-bridge canonical event cursor does not match its sequence");
22
+ }
23
+ const event = envelope.event;
24
+ const usage = usageFromCanonicalEvent(event);
25
+ return {
26
+ type: event.type,
27
+ // AgentSession.events({ since }) accepts a previously emitted event id.
28
+ // The bridge replays by its numeric SSE cursor, while eventId identifies
29
+ // the canonical envelope and is not a replay cursor.
30
+ id: frame.id,
31
+ data: {
32
+ ...eventPayload(event),
33
+ eventId: envelope.eventId,
34
+ sequence: envelope.sequence,
35
+ cursor: frame.id,
36
+ runId: context.runId,
37
+ sessionId: context.sessionId,
38
+ ...(context.executionId === undefined
39
+ ? {}
40
+ : { executionId: context.executionId }),
41
+ ...(envelope.occurredAt === undefined
42
+ ? {}
43
+ : { occurredAt: envelope.occurredAt }),
44
+ receivedAt: envelope.receivedAt,
45
+ },
46
+ normalized: event,
47
+ ...(usage === undefined ? {} : { usage }),
48
+ providerEvent: envelope,
49
+ };
50
+ }
51
+ export function isTerminalCanonicalEvent(event) {
52
+ return event.normalized?.type === "status" &&
53
+ ["completed", "failed", "cancelled"].includes(event.normalized.status);
54
+ }
55
+ function parseEnvelope(data) {
56
+ let value;
57
+ try {
58
+ value = JSON.parse(data);
59
+ }
60
+ catch (error) {
61
+ throw new Error("cli-bridge canonical event is not valid JSON", { cause: error });
62
+ }
63
+ const parsed = RuntimeEventEnvelopeSchema.safeParse(value);
64
+ if (!parsed.success) {
65
+ throw new Error("cli-bridge emitted an invalid canonical event envelope", {
66
+ cause: parsed.error,
67
+ });
68
+ }
69
+ return parsed.data;
70
+ }
71
+ function eventPayload(event) {
72
+ const { type: _type, ...payload } = event;
73
+ return payload;
74
+ }
75
+ function usageFromCanonicalEvent(event) {
76
+ if (event.type !== "raw")
77
+ return undefined;
78
+ const providerEvent = recordValue(event.event);
79
+ if (providerEvent?.type !== "usage")
80
+ return undefined;
81
+ const data = recordValue(providerEvent.data);
82
+ const usage = recordValue(providerEvent.usage) ?? recordValue(data?.usage);
83
+ if (!usage)
84
+ return undefined;
85
+ const inputTokens = nonNegativeInteger(usage.inputTokens);
86
+ const outputTokens = nonNegativeInteger(usage.outputTokens);
87
+ if (inputTokens === undefined || outputTokens === undefined)
88
+ return undefined;
89
+ return {
90
+ inputTokens,
91
+ outputTokens,
92
+ ...optionalInteger(usage, "totalTokens"),
93
+ ...optionalInteger(usage, "cacheReadInputTokens"),
94
+ ...optionalInteger(usage, "cacheCreationInputTokens"),
95
+ ...optionalInteger(usage, "reasoningTokens"),
96
+ ...optionalNumber(usage, "cost"),
97
+ };
98
+ }
99
+ function optionalInteger(source, key) {
100
+ const value = nonNegativeInteger(source[key]);
101
+ return value === undefined ? {} : { [key]: value };
102
+ }
103
+ function optionalNumber(source, key) {
104
+ const value = source[key];
105
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
106
+ ? { [key]: value }
107
+ : {};
108
+ }
109
+ function nonNegativeInteger(value) {
110
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
111
+ ? value
112
+ : undefined;
113
+ }
114
+ function recordValue(value) {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value)
116
+ ? value
117
+ : undefined;
118
+ }
119
+ function isBaseTenInteger(value) {
120
+ return /^(0|[1-9][0-9]*)$/u.test(value) && Number.isSafeInteger(Number(value));
121
+ }
122
+ function retainedCanonicalStreamError(data) {
123
+ let message = "cli-bridge canonical event stream failed";
124
+ try {
125
+ const parsed = recordValue(JSON.parse(data));
126
+ const error = recordValue(parsed?.error);
127
+ if (typeof error?.message === "string" && error.message.length > 0) {
128
+ message = error.message;
129
+ }
130
+ }
131
+ catch {
132
+ // The typed error frame is authoritative even when its detail is malformed.
133
+ }
134
+ return new Error(`cli-bridge: ${message}`);
135
+ }
@@ -57,7 +57,7 @@ async function performCliBridgeCancellation(options, transport, run) {
57
57
  throw new Error(`cli-bridge cancel ${response.status}: ${await response.text()}`);
58
58
  }
59
59
  let snapshot = cancelSnapshot(await response.text());
60
- assertCliBridgeRunSnapshotIdentity(snapshot, run.id);
60
+ assertCliBridgeRunSnapshotIdentity(snapshot, run.id, run.requestDigest);
61
61
  const waitBudgetMs = options.cancelWaitMs ?? 30_000;
62
62
  const deadline = Date.now() + waitBudgetMs;
63
63
  while (!snapshot.terminal) {
@@ -166,5 +166,7 @@ export function agentSessionStatusFromRun(snapshot) {
166
166
  return "failed";
167
167
  if (snapshot.status === "cancelled")
168
168
  return "cancelled";
169
+ if (snapshot.status === "unknown")
170
+ return "unknown";
169
171
  return "running";
170
172
  }
@@ -13,5 +13,7 @@ export interface CreateCliBridgeEnvironmentArgs {
13
13
  * action the document did not carry.
14
14
  */
15
15
  readonly capabilities: AgentEnvironmentCapabilities;
16
+ readonly selectedBackend?: string;
17
+ readonly selectedModel?: string;
16
18
  }
17
19
  export declare function createCliBridgeEnvironment(args: CreateCliBridgeEnvironmentArgs): AgentEnvironment;
@@ -1,6 +1,8 @@
1
1
  import { agentSessionStatusFromRun, cancelCliBridgeRun, cancelExactCliBridgeRun, getCliBridgeRun, } from "./retained-control.js";
2
2
  import { addTokenUsage, collectCliBridgeTurnResult, dispatchCliBridgeTurn, streamCliBridgeSessionEvents, streamTrackedCliBridgeTurn, } from "./retained-execution.js";
3
3
  import { createExecutionUsageLog, observeCliBridgeEnvironment, } from "./observation.js";
4
+ import { cliBridgeInteractionResponder } from "./interaction-response.js";
5
+ import { assertCliBridgeRequestedInteractions, assertCliBridgeNativeTurnModel, supportsCliBridgeNativeInteractions, } from "./retained-native.js";
4
6
  import { assertRunMatchesControlRef, exactControlRefForSession, prepareCliBridgeRun, runFromControlRef, } from "./retained-run-state.js";
5
7
  import { createCliBridgeTransport } from "./transport.js";
6
8
  export function createCliBridgeEnvironment(args) {
@@ -8,8 +10,18 @@ export function createCliBridgeEnvironment(args) {
8
10
  const transport = createCliBridgeTransport(options);
9
11
  const runs = new Map();
10
12
  const sessions = new Map();
13
+ const nativeSessions = new Map();
11
14
  const readers = new Set();
12
15
  const usageLog = createExecutionUsageLog();
16
+ const useNativeInteractions = supportsCliBridgeNativeInteractions(args.capabilities, args.selectedBackend);
17
+ const respondToInteraction = useNativeInteractions
18
+ ? cliBridgeInteractionResponder({
19
+ options,
20
+ transport,
21
+ providerName,
22
+ environmentId,
23
+ })
24
+ : undefined;
13
25
  let destroyed = false;
14
26
  let closePromise;
15
27
  const stream = async function* (turn) {
@@ -18,11 +30,15 @@ export function createCliBridgeEnvironment(args) {
18
30
  if (!args.allowDispatch) {
19
31
  throw new Error("a reconstructed cli-bridge environment can only control an existing run");
20
32
  }
21
- const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, false);
33
+ if (useNativeInteractions) {
34
+ assertCliBridgeNativeTurnModel(options, environmentInput, turn, args.selectedModel);
35
+ }
36
+ assertCliBridgeRequestedInteractions(args.capabilities, turn.interactions);
37
+ const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, useNativeInteractions);
22
38
  // A streamed run reports usage across its events, so the totals are summed
23
39
  // here and recorded as one execution's measurement.
24
40
  let streamedUsage;
25
- for await (const event of streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers)) {
41
+ for await (const event of streamTrackedCliBridgeTurn(options, environmentInput, providerName, prepared, transport, runs, sessions, readers, nativeSessions, useNativeInteractions)) {
26
42
  streamedUsage = addTokenUsage(streamedUsage, event.usage);
27
43
  usageLog.record(prepared.run.executionId, streamedUsage);
28
44
  yield event;
@@ -31,6 +47,7 @@ export function createCliBridgeEnvironment(args) {
31
47
  return {
32
48
  id: environmentId,
33
49
  provider: providerName,
50
+ capabilities: args.capabilities,
34
51
  ...(environmentInput.name ? { name: environmentInput.name } : {}),
35
52
  status: async (statusOptions) => {
36
53
  statusOptions?.signal?.throwIfAborted();
@@ -43,8 +60,12 @@ export function createCliBridgeEnvironment(args) {
43
60
  if (!args.allowDispatch) {
44
61
  throw new Error("a reconstructed cli-bridge environment cannot dispatch new work");
45
62
  }
63
+ if (useNativeInteractions) {
64
+ assertCliBridgeNativeTurnModel(options, environmentInput, turn, args.selectedModel);
65
+ }
66
+ assertCliBridgeRequestedInteractions(args.capabilities, turn.interactions);
46
67
  const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, true);
47
- return dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions);
68
+ return dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions, nativeSessions, useNativeInteractions);
48
69
  },
49
70
  session(id, sessionOptions) {
50
71
  sessionOptions?.signal?.throwIfAborted();
@@ -58,12 +79,18 @@ export function createCliBridgeEnvironment(args) {
58
79
  runs,
59
80
  sessions,
60
81
  readers,
82
+ nativeSessions,
61
83
  allowPrompt: args.allowDispatch,
62
84
  requestedControlRef: sessionOptions?.controlRef,
63
85
  isDestroyed: () => destroyed,
64
86
  usageLog,
87
+ capabilities: args.capabilities,
88
+ interactionsEnabled: respondToInteraction !== undefined,
89
+ useNativeInteractions,
90
+ selectedModel: args.selectedModel,
65
91
  });
66
92
  },
93
+ ...(respondToInteraction ? { respondToInteraction } : {}),
67
94
  ...(args.capabilities.observation
68
95
  ? {
69
96
  async observe(observeOptions) {
@@ -112,6 +139,7 @@ export function createCliBridgeEnvironment(args) {
112
139
  }
113
140
  await transport.close();
114
141
  sessions.clear();
142
+ nativeSessions.clear();
115
143
  runs.clear();
116
144
  })();
117
145
  closePromise = attempt;
@@ -165,6 +193,15 @@ function createCliBridgeSession(args) {
165
193
  throw new Error(`cli-bridge session "${args.id}" has no run`);
166
194
  return run;
167
195
  };
196
+ const respondToInteraction = args.interactionsEnabled
197
+ ? cliBridgeInteractionResponder({
198
+ sessionId: args.id,
199
+ options: args.options,
200
+ transport: args.transport,
201
+ providerName: args.providerName,
202
+ environmentId: args.environmentId,
203
+ })
204
+ : undefined;
168
205
  return {
169
206
  id: args.id,
170
207
  get controlRef() {
@@ -181,11 +218,11 @@ function createCliBridgeSession(args) {
181
218
  },
182
219
  async *events(eventOptions) {
183
220
  const run = requireCurrentRun();
184
- yield* streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, eventOptions);
221
+ yield* streamCliBridgeSessionEvents(args.options, args.environmentInput, args.providerName, run, args.transport, args.runs, args.readers, eventOptions);
185
222
  },
186
223
  async result(resultOptions) {
187
224
  const run = requireCurrentRun();
188
- const result = await collectCliBridgeTurnResult(streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, { since: "0", signal: resultOptions?.signal }), run, args.options, args.transport, resultOptions?.signal);
225
+ const result = await collectCliBridgeTurnResult(streamCliBridgeSessionEvents(args.options, args.environmentInput, args.providerName, run, args.transport, args.runs, args.readers, { since: "0", signal: resultOptions?.signal }), run, args.options, args.transport, resultOptions?.signal);
189
226
  args.usageLog.record(run.executionId, result.usage);
190
227
  return result;
191
228
  },
@@ -196,11 +233,15 @@ function createCliBridgeSession(args) {
196
233
  if (input.sessionId && input.sessionId !== args.id) {
197
234
  throw new Error(`cli-bridge session "${args.id}" cannot prompt session "${input.sessionId}"`);
198
235
  }
236
+ if (args.useNativeInteractions) {
237
+ assertCliBridgeNativeTurnModel(args.options, args.environmentInput, input, args.selectedModel);
238
+ }
239
+ assertCliBridgeRequestedInteractions(args.capabilities, input.interactions);
199
240
  const prepared = prepareCliBridgeRun(args.options, args.environmentInput, {
200
241
  ...input,
201
242
  sessionId: args.id,
202
- }, args.environmentId, false);
203
- const result = await collectCliBridgeTurnResult(streamTrackedCliBridgeTurn(args.options, args.environmentInput, prepared, args.transport, args.runs, args.sessions, args.readers), prepared.run, args.options, args.transport, input.signal);
243
+ }, args.environmentId, args.useNativeInteractions);
244
+ const result = await collectCliBridgeTurnResult(streamTrackedCliBridgeTurn(args.options, args.environmentInput, args.providerName, prepared, args.transport, args.runs, args.sessions, args.readers, args.nativeSessions, args.useNativeInteractions), prepared.run, args.options, args.transport, input.signal);
204
245
  args.usageLog.record(prepared.run.executionId, result.usage);
205
246
  return { ...result, sessionId: args.id };
206
247
  },
@@ -212,5 +253,6 @@ function createCliBridgeSession(args) {
212
253
  const run = requireCurrentRun();
213
254
  return cancelExactCliBridgeRun(args.options, args.transport, run, request, cancelOptions?.signal);
214
255
  },
256
+ ...(respondToInteraction ? { respondToInteraction } : {}),
215
257
  };
216
258
  }
@@ -2,10 +2,11 @@ import type { TokenUsage } from "@tangle-network/agent-interface";
2
2
  import type { AgentEnvironmentEvent, AgentSessionRef, AgentTurnResult, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
3
3
  import type { CliBridgeProviderOptions } from "./provider-options.js";
4
4
  import { type CliBridgeRun, type CliBridgeSessionState, type PreparedCliBridgeRun } from "./retained-run-state.js";
5
+ import { type CliBridgeNativeSessionCache } from "./retained-native.js";
5
6
  import type { CliBridgeTransport } from "./transport.js";
6
- export declare function streamTrackedCliBridgeTurn(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, prepared: PreparedCliBridgeRun, transport: CliBridgeTransport, runs: Map<string, CliBridgeRun>, sessions: Map<string, CliBridgeSessionState>, readers: Set<AbortController>): AsyncIterable<AgentEnvironmentEvent>;
7
- export declare function dispatchCliBridgeTurn(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, prepared: PreparedCliBridgeRun, transport: CliBridgeTransport, providerName: string, runs: Map<string, CliBridgeRun>, sessions: Map<string, CliBridgeSessionState>): Promise<AgentSessionRef>;
8
- export declare function streamCliBridgeSessionEvents(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, run: CliBridgeRun, transport: CliBridgeTransport, runs: Map<string, CliBridgeRun>, readers: Set<AbortController>, eventOptions?: {
7
+ export declare function streamTrackedCliBridgeTurn(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, providerName: string, prepared: PreparedCliBridgeRun, transport: CliBridgeTransport, runs: Map<string, CliBridgeRun>, sessions: Map<string, CliBridgeSessionState>, readers: Set<AbortController>, nativeSessions: CliBridgeNativeSessionCache, useNativeInteractions: boolean): AsyncIterable<AgentEnvironmentEvent>;
8
+ export declare function dispatchCliBridgeTurn(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, prepared: PreparedCliBridgeRun, transport: CliBridgeTransport, providerName: string, runs: Map<string, CliBridgeRun>, sessions: Map<string, CliBridgeSessionState>, nativeSessions: CliBridgeNativeSessionCache, useNativeInteractions: boolean): Promise<AgentSessionRef>;
9
+ export declare function streamCliBridgeSessionEvents(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, providerName: string, run: CliBridgeRun, transport: CliBridgeTransport, runs: Map<string, CliBridgeRun>, readers: Set<AbortController>, eventOptions?: {
9
10
  since?: string;
10
11
  executionId?: string;
11
12
  signal?: AbortSignal;
@@ -1,9 +1,10 @@
1
1
  import { cancelCliBridgeRun, captureCliBridgeRunIdentity, detachCliBridgeReader, getCliBridgeRun, } from "./retained-control.js";
2
2
  import { bindCliBridgeRun, bindCliBridgeSession, exactControlRefForRun, restoreCliBridgeRun, restoreCliBridgeSession, } from "./retained-run-state.js";
3
3
  import { CliBridgeRequestRejectedError, streamCliBridgeTurn, } from "./retained-stream.js";
4
+ import { beginCliBridgeNativeTurn, } from "./retained-native.js";
4
5
  import { requestHeaders, trimSlash } from "./transport.js";
5
6
  import { modelRequestCount } from "./wire.js";
6
- export async function* streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers) {
7
+ export async function* streamTrackedCliBridgeTurn(options, environmentInput, providerName, prepared, transport, runs, sessions, readers, nativeSessions, useNativeInteractions) {
7
8
  const originalTurn = prepared.turn;
8
9
  if (originalTurn.detach) {
9
10
  throw new Error("cli-bridge provider does not support detached turns");
@@ -26,9 +27,26 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pre
26
27
  readers.add(controller);
27
28
  let drained = false;
28
29
  let threw = false;
30
+ let admitted = false;
31
+ let admissionValidated = false;
29
32
  try {
30
- for await (const event of streamCliBridgeTurn(options, turn, run.requestBody, transport, run.id, originalTurn.lastEventId, signal, (response) => captureCliBridgeRunIdentity(response, run, true), () => getCliBridgeRun(options, transport, run, 30_000, signal))) {
31
- yield event;
33
+ if (useNativeInteractions) {
34
+ await beginCliBridgeNativeTurn(options, providerName, environmentInput, turn, run, transport, nativeSessions, signal, () => {
35
+ admitted = true;
36
+ });
37
+ admissionValidated = true;
38
+ for await (const event of streamCliBridgeTurn(options, turn, undefined, transport, run.id, originalTurn.lastEventId, signal, (response) => captureCliBridgeRunIdentity(response, run, false), () => getCliBridgeRun(options, transport, run, 30_000, signal), (event) => assertCanonicalEventBinding(event.normalized, run, providerName))) {
39
+ yield event;
40
+ }
41
+ }
42
+ else {
43
+ for await (const event of streamCliBridgeTurn(options, turn, run.requestBody, transport, run.id, originalTurn.lastEventId, signal, (response) => {
44
+ admitted = true;
45
+ captureCliBridgeRunIdentity(response, run, true);
46
+ admissionValidated = true;
47
+ }, () => getCliBridgeRun(options, transport, run, 30_000, signal))) {
48
+ yield event;
49
+ }
32
50
  }
33
51
  drained = true;
34
52
  if (runs.get(run.id) === run)
@@ -36,7 +54,7 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pre
36
54
  }
37
55
  catch (error) {
38
56
  threw = true;
39
- if (error instanceof CliBridgeRequestRejectedError) {
57
+ if (error instanceof CliBridgeRequestRejectedError && !admitted) {
40
58
  restoreCliBridgeRun(run, previousRun, runs);
41
59
  restoreCliBridgeSession(run, previousSessionRun, sessions);
42
60
  throw error;
@@ -52,8 +70,13 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pre
52
70
  if (runs.get(run.id) === run)
53
71
  runs.delete(run.id);
54
72
  }
55
- else if (originalTurn.signal?.aborted || environmentInput.signal?.aborted) {
56
- await cancelCliBridgeRun(options, transport, run);
73
+ else if ((admitted && !admissionValidated) ||
74
+ originalTurn.signal?.aborted ||
75
+ environmentInput.signal?.aborted) {
76
+ const cancellation = await cancelCliBridgeRun(options, transport, run);
77
+ if (cancellation.requestDigest !== undefined) {
78
+ run.requestDigest = cancellation.requestDigest;
79
+ }
57
80
  if (runs.get(run.id) === run)
58
81
  runs.delete(run.id);
59
82
  }
@@ -68,7 +91,7 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pre
68
91
  readers.delete(controller);
69
92
  }
70
93
  }
71
- export async function dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions) {
94
+ export async function dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions, nativeSessions, useNativeInteractions) {
72
95
  const run = prepared.run;
73
96
  const previousRun = bindCliBridgeRun(run, runs);
74
97
  const previousSessionRun = bindCliBridgeSession(run, sessions);
@@ -80,35 +103,44 @@ export async function dispatchCliBridgeTurn(options, environmentInput, prepared,
80
103
  let accepted = false;
81
104
  let responseBody;
82
105
  try {
83
- const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
84
- method: "POST",
85
- headers: {
86
- ...requestHeaders(options),
87
- accept: "text/event-stream",
88
- ...(run.sessionId ? { "x-session-id": run.sessionId } : {}),
89
- },
90
- body: run.requestBody,
91
- ...(signal ? { signal } : {}),
92
- });
93
- if (!response.ok) {
94
- let detail = "request rejected";
95
- try {
96
- detail = await response.text();
97
- }
98
- catch {
99
- // The HTTP status already proves this request was rejected.
106
+ if (useNativeInteractions) {
107
+ await beginCliBridgeNativeTurn(options, providerName, environmentInput, prepared.turn, run, transport, nativeSessions, signal, () => {
108
+ accepted = true;
109
+ });
110
+ }
111
+ else {
112
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
113
+ method: "POST",
114
+ headers: {
115
+ ...requestHeaders(options),
116
+ accept: "text/event-stream",
117
+ ...(run.sessionId ? { "x-session-id": run.sessionId } : {}),
118
+ },
119
+ body: run.requestBody,
120
+ ...(signal ? { signal } : {}),
121
+ });
122
+ if (!response.ok) {
123
+ let detail = "request rejected";
124
+ try {
125
+ detail = await response.text();
126
+ }
127
+ catch {
128
+ // The HTTP status already proves this request was rejected.
129
+ }
130
+ throw new CliBridgeRequestRejectedError(response.status, detail);
100
131
  }
101
- throw new CliBridgeRequestRejectedError(response.status, detail);
132
+ accepted = true;
133
+ if (!response.body)
134
+ throw new Error("cli-bridge response body is empty");
135
+ responseBody = response.body;
136
+ captureCliBridgeRunIdentity(response, run, true);
102
137
  }
103
- accepted = true;
104
- if (!response.body)
105
- throw new Error("cli-bridge response body is empty");
106
- responseBody = response.body;
107
- captureCliBridgeRunIdentity(response, run, true);
108
138
  const controlRef = exactControlRefForRun(run, providerName);
109
139
  run.controlRef = controlRef;
110
- await detachCliBridgeReader(responseBody);
111
- responseBody = undefined;
140
+ if (responseBody) {
141
+ await detachCliBridgeReader(responseBody);
142
+ responseBody = undefined;
143
+ }
112
144
  return {
113
145
  id: run.sessionId,
114
146
  provider: providerName,
@@ -151,7 +183,7 @@ export async function dispatchCliBridgeTurn(options, environmentInput, prepared,
151
183
  throw failure;
152
184
  }
153
185
  }
154
- export async function* streamCliBridgeSessionEvents(options, environmentInput, run, transport, runs, readers, eventOptions) {
186
+ export async function* streamCliBridgeSessionEvents(options, environmentInput, providerName, run, transport, runs, readers, eventOptions) {
155
187
  if (eventOptions?.executionId !== undefined &&
156
188
  eventOptions.executionId !== run.executionId) {
157
189
  throw new Error("cli-bridge event request targets another retained execution");
@@ -171,7 +203,7 @@ export async function* streamCliBridgeSessionEvents(options, environmentInput, r
171
203
  ...(run.sessionId ? { sessionId: run.sessionId } : {}),
172
204
  executionId: run.executionId,
173
205
  turnId: run.turnId,
174
- }, undefined, transport, run.id, eventOptions?.since ?? "0", signal, (response) => captureCliBridgeRunIdentity(response, run, false), () => getCliBridgeRun(options, transport, run, 30_000, signal));
206
+ }, undefined, transport, run.id, eventOptions?.since ?? "0", signal, (response) => captureCliBridgeRunIdentity(response, run, false), () => getCliBridgeRun(options, transport, run, 30_000, signal), (event) => assertCanonicalEventBinding(event.normalized, run, providerName));
175
207
  drained = true;
176
208
  if (runs.get(run.id) === run)
177
209
  runs.delete(run.id);
@@ -184,6 +216,19 @@ export async function* streamCliBridgeSessionEvents(options, environmentInput, r
184
216
  readers.delete(controller);
185
217
  }
186
218
  }
219
+ function assertCanonicalEventBinding(event, run, providerName) {
220
+ if (event?.type !== "interaction")
221
+ return;
222
+ const binding = event.request.binding;
223
+ if (binding.runId !== run.id ||
224
+ binding.provider !== providerName ||
225
+ binding.environmentId !== run.environmentId ||
226
+ binding.sessionId !== run.sessionId ||
227
+ binding.executionId !== run.executionId ||
228
+ binding.interactionId !== event.request.id) {
229
+ throw new Error("cli-bridge interaction event does not bind to the retained run");
230
+ }
231
+ }
187
232
  export async function collectCliBridgeTurnResult(source, run, options, transport, signal) {
188
233
  const events = [];
189
234
  let text = "";
@@ -0,0 +1,27 @@
1
+ import type { AgentEnvironmentCapabilities, AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
2
+ import { type Sha256Digest, type RequestedInteractions } from "@tangle-network/agent-interface";
3
+ import type { CliBridgeProviderOptions } from "./provider-options.js";
4
+ import type { CliBridgeRun } from "./retained-run-state.js";
5
+ import type { CliBridgeTransport } from "./transport.js";
6
+ /** The server-owned native session identity cached by one provider handle. */
7
+ export interface CliBridgeNativeSession {
8
+ readonly id: string;
9
+ readonly model: string;
10
+ readonly createRequestDigest: Sha256Digest;
11
+ }
12
+ export type CliBridgeNativeSessionCache = Map<string, Promise<CliBridgeNativeSession>>;
13
+ /** Interactions are valid only for the native retained Pi contract. */
14
+ export declare function supportsCliBridgeNativeInteractions(capabilities: AgentEnvironmentCapabilities, selectedBackend?: string): boolean;
15
+ /** Reject posture the selected environment did not advertise before transport use. */
16
+ export declare function assertCliBridgeRequestedInteractions(capabilities: AgentEnvironmentCapabilities, requested: RequestedInteractions | undefined): void;
17
+ /** Keep capability discovery and every native turn on one exact model route. */
18
+ export declare function assertCliBridgeNativeTurnModel(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, selectedModel: string | undefined): void;
19
+ /**
20
+ * Create or reuse the server's retained native session for one external id.
21
+ *
22
+ * The cache stores the promise so concurrent first turns cannot create two
23
+ * native sessions for the same id.
24
+ */
25
+ export declare function ensureCliBridgeNativeSession(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, sessionId: string, transport: CliBridgeTransport, sessions: CliBridgeNativeSessionCache, signal?: AbortSignal): Promise<CliBridgeNativeSession>;
26
+ /** Admit one native retained turn and bind its server digest to the run. */
27
+ export declare function beginCliBridgeNativeTurn(options: CliBridgeProviderOptions, providerName: string, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, run: CliBridgeRun, transport: CliBridgeTransport, sessions: CliBridgeNativeSessionCache, signal?: AbortSignal, onAdmission?: () => void): Promise<void>;