@tangle-network/agent-provider-cli-bridge 0.7.8 → 0.8.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.
@@ -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>;
@@ -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>;