@tangle-network/agent-provider-cli-bridge 0.9.1 → 0.9.2

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.
package/README.md CHANGED
@@ -40,6 +40,8 @@ Persist the returned `controlRef` before treating dispatch as accepted.
40
40
  It contains the exact provider, environment, session, execution, run, and request digest needed after a process restart.
41
41
  Reconstruct the environment with `provider.get(controlRef.environmentId)`, then call `environment.session(controlRef.sessionId, { controlRef })`.
42
42
  The provider rejects any coordinate or digest mismatch instead of attaching to a different run.
43
+ If the caller crashes before saving `controlRef`, call `provider.lookupRun()` with the planned run coordinates.
44
+ The lookup returns the server-issued digest only when all five planned coordinates match the retained run.
43
45
 
44
46
  The bridge model is selected from run data in this order: the turn, the provider default, or the profile's `harness` plus `model.default`.
45
47
  Execution fails before network use when none is present.
@@ -69,8 +71,11 @@ The provider stores the selected harness, exact model route, and canonical creat
69
71
  This identifier lets `provider.get()` reconstruct profile-selected routes after process death without a cache or repeated configuration.
70
72
  The create digest prevents an altered profile or workspace from reusing the previous retained identity.
71
73
  `provider.get()` accepts only identifiers created by this provider and rejects plain caller identifiers.
72
- The provider queries `/v1/capabilities` for the retained Pi route.
73
- The provider caches the valid response and enables native interactions only when the running Bridge proves the complete retained contract.
74
+ The provider queries `/v1/capabilities` for each retained route restored through `provider.get()`.
75
+ `provider.capabilities()` queries the configured default route before the Runtime selects retained execution.
76
+ The provider shares concurrent discovery requests and refreshes every later query.
77
+ It enables retained control only when the running Bridge proves the complete contract.
78
+ It enables native interactions only when the running Bridge proves the Pi contract.
74
79
  Every native turn stays on the model route used for that discovery; another model requires another environment.
75
80
  An explicit capability document remains available for a caller that already performed the same discovery.
76
81
  That capability selects cli-bridge's native `/v1/sessions` and `/turns` transport; it never sends an interactive turn to `/v1/chat/completions`.
@@ -2,7 +2,7 @@ import { type AgentEnvironmentCapabilities } from "@tangle-network/agent-interfa
2
2
  import type { CliBridgeProviderOptions } from "./provider-options.js";
3
3
  /** Read the retained contract from the exact Bridge model route. */
4
4
  export declare function discoverCliBridgeCapabilities(options: CliBridgeProviderOptions, model: string): Promise<AgentEnvironmentCapabilities>;
5
- /** Share one readiness request between capability checks and environment creation. */
5
+ /** Share concurrent readiness requests without retaining stale model truth. */
6
6
  export declare function cachedCliBridgeCapabilityDiscovery(options: CliBridgeProviderOptions): (model: string) => Promise<AgentEnvironmentCapabilities>;
7
7
  /**
8
8
  * Publish only operations implemented by this adapter and proved by Bridge.
@@ -1,6 +1,6 @@
1
1
  import { AgentEnvironmentCapabilitiesSchema, } from "@tangle-network/agent-interface/environment-provider";
2
2
  import { supportsCliBridgeNativeContinuation, supportsCliBridgeNativeInteractions, } from "./retained-native.js";
3
- import { createCliBridgeTransport, requestHeaders, trimSlash, } from "./transport.js";
3
+ import { createCliBridgeTransport, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
4
4
  /** Read the retained contract from the exact Bridge model route. */
5
5
  export async function discoverCliBridgeCapabilities(options, model) {
6
6
  const transport = createCliBridgeTransport(options);
@@ -9,12 +9,13 @@ export async function discoverCliBridgeCapabilities(options, model) {
9
9
  method: "GET",
10
10
  headers: requestHeaders(options),
11
11
  });
12
+ const responseText = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES);
12
13
  if (!response.ok) {
13
- throw new Error(`cli-bridge capability discovery returned HTTP ${response.status}: ${await response.text()}`);
14
+ throw new Error(`cli-bridge capability discovery returned HTTP ${response.status}: ${responseText}`);
14
15
  }
15
16
  let value;
16
17
  try {
17
- value = JSON.parse(await response.text());
18
+ value = JSON.parse(responseText);
18
19
  }
19
20
  catch (error) {
20
21
  throw new Error("cli-bridge capability discovery returned invalid JSON", { cause: error });
@@ -31,7 +32,7 @@ export async function discoverCliBridgeCapabilities(options, model) {
31
32
  await transport.close();
32
33
  }
33
34
  }
34
- /** Share one readiness request between capability checks and environment creation. */
35
+ /** Share concurrent readiness requests without retaining stale model truth. */
35
36
  export function cachedCliBridgeCapabilityDiscovery(options) {
36
37
  const cache = new Map();
37
38
  return (model) => {
@@ -40,10 +41,10 @@ export function cachedCliBridgeCapabilityDiscovery(options) {
40
41
  return existing;
41
42
  const pending = discoverCliBridgeCapabilities(options, model);
42
43
  cache.set(model, pending);
43
- void pending.catch(() => {
44
+ void pending.finally(() => {
44
45
  if (cache.get(model) === pending)
45
46
  cache.delete(model);
46
- });
47
+ }).catch(() => { });
47
48
  return pending;
48
49
  };
49
50
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import type { AgentEnvironmentCapabilities, AgentEnvironmentProvider } from "@tangle-network/agent-interface/environment-provider";
2
- import { type HarnessType } from "@tangle-network/agent-interface";
2
+ import { type AgentExactRunControlRef, type HarnessType } from "@tangle-network/agent-interface";
3
+ import { type CliBridgeRunLookupInput } from "./retained-control.js";
3
4
  import { type CliBridgeProviderOptions } from "./provider-options.js";
4
5
  export type { CliBridgeProviderOptions } from "./provider-options.js";
6
+ export type { CliBridgeRunLookupInput } from "./retained-control.js";
5
7
  export { safeEndpointFromBaseUrl } from "./observation.js";
6
- export declare function createCliBridgeProvider(options: CliBridgeProviderOptions): AgentEnvironmentProvider;
8
+ export interface CliBridgeProvider extends AgentEnvironmentProvider {
9
+ /** Recover the server-issued digest for one pre-dispatch retained admission. */
10
+ lookupRun(input: CliBridgeRunLookupInput): Promise<AgentExactRunControlRef | null>;
11
+ }
12
+ export declare function createCliBridgeProvider(options: CliBridgeProviderOptions): CliBridgeProvider;
7
13
  /**
8
14
  * Describe the bridge backend, not this adapter.
9
15
  *
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { agentEnvironmentCreateInputDigest, createAgentEnvironmentWithIdempotency, } from "@tangle-network/agent-interface/environment-provider";
2
2
  import { harnessTypeSchema, harnessSystemPromptIntents, snapshotAgentProfile, } from "@tangle-network/agent-interface";
3
3
  import { createCliBridgeEnvironment, } from "./retained-environment.js";
4
+ import { lookupExactCliBridgeRun, } from "./retained-control.js";
4
5
  import { cachedCliBridgeCapabilityDiscovery, narrowCliBridgeCapabilities, } from "./capability-discovery.js";
5
6
  import { supportsCliBridgeNativeInteractions, } from "./retained-native.js";
6
7
  import { assertCliBridgeProviderOptions, } from "./provider-options.js";
@@ -14,6 +15,7 @@ export function createCliBridgeProvider(options) {
14
15
  const createRecords = new Map();
15
16
  const configuredBackend = selectedBackendFromRoute(options.defaultModel);
16
17
  const discoverCapabilities = cachedCliBridgeCapabilityDiscovery(options);
18
+ const remotelyVerifiedRoutes = new Set();
17
19
  // The observation surfaces are declared as intent and narrowed to the
18
20
  // sources this bridge can put a value on, so the environment offers the
19
21
  // operation exactly where the document claims it.
@@ -36,17 +38,19 @@ export function createCliBridgeProvider(options) {
36
38
  const { interactions: _interactions, ...withoutInteractions } = narrowed;
37
39
  return withoutInteractions;
38
40
  };
39
- const resolveCapabilities = (selectedBackend, model, allowNativeInteractions = false) => {
41
+ const resolveCapabilities = (selectedBackend, model, allowNativeInteractions = false, discoverRemote = false) => {
40
42
  const local = localCapabilities(selectedBackend, allowNativeInteractions);
41
- if (!allowNativeInteractions ||
42
- options.capabilities !== undefined ||
43
- selectedBackend !== "pi" ||
44
- model === undefined) {
43
+ if (options.capabilities !== undefined)
45
44
  return local;
46
- }
47
- return discoverCapabilities(model).then((reported) => narrowCliBridgeCapabilities(local, reported, selectedBackend, {
48
- preserveAdapterObservation: true,
49
- }));
45
+ if (model === undefined)
46
+ return withoutRemoteRetainedCapabilities(local);
47
+ if (!discoverRemote)
48
+ return withoutRemoteRetainedCapabilities(local);
49
+ return discoverCapabilities(model).then((reported) => {
50
+ return narrowCliBridgeCapabilities(local, reported, selectedBackend, {
51
+ preserveAdapterObservation: true,
52
+ });
53
+ });
50
54
  };
51
55
  const createEnvironment = async (input) => {
52
56
  if (typeof input.profile === "string") {
@@ -72,14 +76,21 @@ export function createCliBridgeProvider(options) {
72
76
  environmentId,
73
77
  allowDispatch: true,
74
78
  cancelRunsOnDestroy: true,
75
- capabilities: await resolveCapabilities(selectedBackend, model, true),
79
+ capabilities: await resolveCapabilities(selectedBackend, model, true, selectedBackend === "pi" ||
80
+ (model !== undefined && remotelyVerifiedRoutes.has(model))),
76
81
  selectedBackend,
77
82
  selectedModel: model,
78
83
  });
79
84
  };
80
85
  return {
81
86
  name,
82
- capabilities: () => resolveCapabilities(configuredBackend, options.defaultModel, true),
87
+ lookupRun: (input) => lookupExactCliBridgeRun(options, name, input),
88
+ capabilities: () => {
89
+ if (options.defaultModel !== undefined) {
90
+ remotelyVerifiedRoutes.add(options.defaultModel);
91
+ }
92
+ return resolveCapabilities(configuredBackend, options.defaultModel, true, true);
93
+ },
83
94
  create(input) {
84
95
  return createAgentEnvironmentWithIdempotency(createRecords, input, () => createEnvironment(input));
85
96
  },
@@ -99,13 +110,17 @@ export function createCliBridgeProvider(options) {
99
110
  environmentId: id,
100
111
  allowDispatch: false,
101
112
  cancelRunsOnDestroy: false,
102
- capabilities: await resolveCapabilities(route.backend, route.model, true),
113
+ capabilities: await resolveCapabilities(route.backend, route.model, true, true),
103
114
  selectedBackend: route.backend,
104
115
  selectedModel: route.model,
105
116
  });
106
117
  },
107
118
  };
108
119
  }
120
+ function withoutRemoteRetainedCapabilities(capabilities) {
121
+ const { retainedControl: _retainedControl, nativeContinuation: _nativeContinuation, interactions: _interactions, ...local } = capabilities;
122
+ return local;
123
+ }
109
124
  function selectedBackendFromInput(input, configuredBackend) {
110
125
  if (input.backend !== undefined)
111
126
  return input.backend;
@@ -1,5 +1,5 @@
1
1
  import { InteractionAcknowledgementSchema, InteractionResponseCommandSchema, } from "@tangle-network/agent-interface";
2
- import { requestHeaders, trimSlash, } from "./transport.js";
2
+ import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
3
3
  function transportFailure(command, error, retryable = true) {
4
4
  return InteractionAcknowledgementSchema.parse({
5
5
  operationId: command.operationId,
@@ -89,7 +89,7 @@ export function cliBridgeInteractionResponder(options) {
89
89
  let acknowledgement;
90
90
  const retryableOnMalformed = response.status >= 500 || (response.status >= 200 && response.status < 300);
91
91
  try {
92
- acknowledgement = InteractionAcknowledgementSchema.parse(JSON.parse(await response.text()));
92
+ acknowledgement = InteractionAcknowledgementSchema.parse(JSON.parse(await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, operation?.signal)));
93
93
  }
94
94
  catch (error) {
95
95
  return transportFailure(exact, error, retryableOnMalformed);
@@ -1,4 +1,5 @@
1
1
  import type { AgentEnvironmentCapabilities } from "@tangle-network/agent-interface/environment-provider";
2
+ export declare const MAX_CLI_BRIDGE_TIMEOUT_MS = 2147483647;
2
3
  export interface CliBridgeProviderOptions {
3
4
  baseUrl: string;
4
5
  bearerToken?: string;
@@ -1,3 +1,4 @@
1
+ export const MAX_CLI_BRIDGE_TIMEOUT_MS = 2_147_483_647;
1
2
  export function assertCliBridgeProviderOptions(options) {
2
3
  assertTimeout(options.headersTimeoutMs, "headersTimeoutMs");
3
4
  assertTimeout(options.bodyTimeoutMs, "bodyTimeoutMs");
@@ -7,4 +8,7 @@ function assertTimeout(value, name) {
7
8
  if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
8
9
  throw new Error(`createCliBridgeProvider ${name} must be a non-negative integer`);
9
10
  }
11
+ if (value !== undefined && value > MAX_CLI_BRIDGE_TIMEOUT_MS) {
12
+ throw new Error(`createCliBridgeProvider ${name} must be no greater than ${MAX_CLI_BRIDGE_TIMEOUT_MS}`);
13
+ }
10
14
  }
@@ -1,11 +1,21 @@
1
- import { type AgentRunCancellationAcknowledgement, type AgentRunCancellationRequest } from "@tangle-network/agent-interface";
1
+ import { type AgentExactRunControlRef, type AgentRunCancellationAcknowledgement, type AgentRunCancellationRequest } from "@tangle-network/agent-interface";
2
2
  import type { AgentSessionStatus } from "@tangle-network/agent-interface/environment-provider";
3
- import type { CliBridgeProviderOptions } from "./provider-options.js";
3
+ import { type CliBridgeProviderOptions } from "./provider-options.js";
4
4
  import type { CliBridgeRun, CliBridgeRunSnapshot } from "./retained-run-state.js";
5
5
  import type { CliBridgeResponse, CliBridgeTransport } from "./transport.js";
6
+ export declare function cliBridgeCancellationSignal(options: CliBridgeProviderOptions): AbortSignal;
6
7
  export declare function captureCliBridgeRunIdentity(response: CliBridgeResponse, run: CliBridgeRun, required: boolean): void;
7
8
  export declare function detachCliBridgeReader(body: AsyncIterable<Uint8Array>): Promise<void>;
8
9
  export declare function cancelCliBridgeRun(options: CliBridgeProviderOptions, transport: CliBridgeTransport, run: CliBridgeRun, signal?: AbortSignal): Promise<CliBridgeRunSnapshot>;
9
10
  export declare function cancelExactCliBridgeRun(options: CliBridgeProviderOptions, transport: CliBridgeTransport, run: CliBridgeRun, request: AgentRunCancellationRequest, signal?: AbortSignal): Promise<AgentRunCancellationAcknowledgement>;
10
11
  export declare function getCliBridgeRun(options: CliBridgeProviderOptions, transport: CliBridgeTransport, run: CliBridgeRun, waitMs?: number, signal?: AbortSignal): Promise<CliBridgeRunSnapshot | null>;
12
+ export interface CliBridgeRunLookupInput {
13
+ readonly runId: string;
14
+ readonly environmentId: string;
15
+ readonly sessionId: string;
16
+ readonly executionId: string;
17
+ readonly signal?: AbortSignal;
18
+ }
19
+ /** Recover one exact Bridge run after dispatch succeeded but local admission did not. */
20
+ export declare function lookupExactCliBridgeRun(options: CliBridgeProviderOptions, providerName: string, input: CliBridgeRunLookupInput): Promise<AgentExactRunControlRef | null>;
11
21
  export declare function agentSessionStatusFromRun(snapshot: CliBridgeRunSnapshot): AgentSessionStatus;
@@ -1,6 +1,12 @@
1
- import { AgentExactRunControlRefSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, agentRunCancellationAcknowledgementMatchesRequest, } from "@tangle-network/agent-interface";
2
- import { requestHeaders, trimSlash } from "./transport.js";
3
- import { cancelSnapshot, runSnapshot } from "./wire.js";
1
+ import { AgentExactRunControlRefSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, agentRunCancellationRequestDigest, agentRunCancellationAcknowledgementMatchesRequest, } from "@tangle-network/agent-interface";
2
+ import { assertCliBridgeProviderOptions, } from "./provider-options.js";
3
+ import { createCliBridgeTransport, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
4
+ import { assertCliBridgeRunId, runSnapshot } from "./wire.js";
5
+ const DEFAULT_CANCELLATION_WAIT_MS = 30_000;
6
+ export function cliBridgeCancellationSignal(options) {
7
+ assertCliBridgeProviderOptions(options);
8
+ return AbortSignal.timeout(options.cancelWaitMs ?? DEFAULT_CANCELLATION_WAIT_MS);
9
+ }
4
10
  export function captureCliBridgeRunIdentity(response, run, required) {
5
11
  const responseRunId = response.headers.get("x-run-id");
6
12
  const requestDigestHeader = response.headers.get("x-run-request-digest");
@@ -24,8 +30,34 @@ export function captureCliBridgeRunIdentity(response, run, required) {
24
30
  if (run.requestDigest !== undefined && requestDigest === null) {
25
31
  throw new Error(`cli-bridge replay response omitted X-Run-Request-Digest for run "${run.id}"`);
26
32
  }
27
- if (requestDigest !== null)
33
+ const responseProvider = response.headers.get("x-run-provider");
34
+ const responseEnvironmentId = response.headers.get("x-run-environment-id");
35
+ const responseSessionId = response.headers.get("x-run-session-id");
36
+ const responseExecutionId = response.headers.get("x-run-execution-id");
37
+ const completeCoordinates = [
38
+ responseRunId,
39
+ requestDigest,
40
+ responseProvider,
41
+ responseEnvironmentId,
42
+ responseSessionId,
43
+ responseExecutionId,
44
+ ].every((value) => value !== null);
45
+ if ((required || run.controlRef !== undefined) && !completeCoordinates) {
46
+ throw new Error("cli-bridge response omitted exact run coordinates");
47
+ }
48
+ if (completeCoordinates) {
49
+ bindExactControlRef(run, AgentExactRunControlRefSchema.parse({
50
+ runId: responseRunId,
51
+ requestDigest,
52
+ provider: responseProvider,
53
+ environmentId: responseEnvironmentId,
54
+ sessionId: responseSessionId,
55
+ executionId: responseExecutionId,
56
+ }));
57
+ }
58
+ else if (requestDigest !== null) {
28
59
  run.requestDigest = requestDigest;
60
+ }
29
61
  }
30
62
  export async function detachCliBridgeReader(body) {
31
63
  const cancellable = body;
@@ -48,23 +80,43 @@ export async function cancelCliBridgeRun(options, transport, run, signal) {
48
80
  return waitForOperation(run.cancellation, signal);
49
81
  }
50
82
  async function performCliBridgeCancellation(options, transport, run) {
51
- const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
83
+ const waitBudgetMs = options.cancelWaitMs ?? DEFAULT_CANCELLATION_WAIT_MS;
84
+ const deadline = Date.now() + waitBudgetMs;
85
+ const signal = cliBridgeCancellationSignal(options);
86
+ let snapshot = null;
87
+ if (run.controlRef === undefined) {
88
+ snapshot = await getCliBridgeRun(options, transport, run, undefined, signal);
89
+ if (snapshot === null) {
90
+ throw new Error(`cli-bridge cannot safely cancel unknown run "${run.id}"`);
91
+ }
92
+ if (snapshot.terminal)
93
+ return snapshot;
94
+ }
95
+ const exactRequest = automaticCancellationRequest(run);
96
+ const response = await waitForOperation(transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
52
97
  method: "POST",
53
98
  headers: requestHeaders(options),
54
- body: "{}",
55
- });
56
- if (!response.ok) {
57
- throw new Error(`cli-bridge cancel ${response.status}: ${await response.text()}`);
99
+ body: JSON.stringify(exactRequest),
100
+ signal,
101
+ }), signal);
102
+ const responseText = await waitForOperation(readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal), signal);
103
+ const acknowledgement = parseExactCancellationAcknowledgement(exactRequest, response.status, responseText);
104
+ if (acknowledgement.status === "conflict" ||
105
+ acknowledgement.status === "unknown" ||
106
+ acknowledgement.effect === "unknown") {
107
+ throw new Error(`cli-bridge did not prove cancellation for run "${run.id}": ${acknowledgement.status}`);
58
108
  }
59
- let snapshot = cancelSnapshot(await response.text());
60
- assertCliBridgeRunSnapshotIdentity(snapshot, run.id, run.requestDigest);
61
- const waitBudgetMs = options.cancelWaitMs ?? 30_000;
62
- const deadline = Date.now() + waitBudgetMs;
63
- while (!snapshot.terminal) {
109
+ if (acknowledgement.effect === "cancelled") {
110
+ snapshot = exactTerminalCancellationSnapshot(run, "cancelled");
111
+ }
112
+ else if (acknowledgement.effect === "not_live") {
113
+ snapshot = exactTerminalCancellationSnapshot(run, "unknown");
114
+ }
115
+ while (snapshot === null || !snapshot.terminal) {
64
116
  const remainingMs = deadline - Date.now();
65
117
  if (remainingMs <= 0)
66
118
  break;
67
- snapshot = await getCliBridgeRun(options, transport, run, Math.min(remainingMs, 30_000));
119
+ snapshot = await getCliBridgeRun(options, transport, run, Math.min(remainingMs, 30_000), signal);
68
120
  if (snapshot === null) {
69
121
  throw new Error(`cli-bridge lost run "${run.id}" before confirming cancellation`);
70
122
  }
@@ -77,6 +129,32 @@ async function performCliBridgeCancellation(options, transport, run) {
77
129
  }
78
130
  return snapshot;
79
131
  }
132
+ function exactTerminalCancellationSnapshot(run, status) {
133
+ if (run.controlRef === undefined) {
134
+ throw new Error("cli-bridge exact cancellation lost its bound run identity");
135
+ }
136
+ return {
137
+ id: run.id,
138
+ requestDigest: run.controlRef.requestDigest,
139
+ controlRef: run.controlRef,
140
+ status,
141
+ terminal: true,
142
+ };
143
+ }
144
+ function automaticCancellationRequest(run) {
145
+ if (run.controlRef === undefined) {
146
+ throw new Error(`cli-bridge cannot safely cancel run "${run.id}" without complete exact coordinates`);
147
+ }
148
+ const material = {
149
+ operationId: `cancel-${run.controlRef.requestDigest.slice("sha256:".length)}`,
150
+ run: run.controlRef,
151
+ reason: "provider cleanup",
152
+ };
153
+ return AgentRunCancellationRequestSchema.parse({
154
+ ...material,
155
+ requestDigest: agentRunCancellationRequestDigest(material),
156
+ });
157
+ }
80
158
  function clearCancellation(run, cancellation) {
81
159
  if (run.cancellation === cancellation)
82
160
  run.cancellation = undefined;
@@ -84,7 +162,10 @@ function clearCancellation(run, cancellation) {
84
162
  function waitForOperation(operation, signal) {
85
163
  if (!signal)
86
164
  return operation;
87
- signal.throwIfAborted();
165
+ if (signal.aborted) {
166
+ void operation.catch(() => { });
167
+ return Promise.reject(signal.reason);
168
+ }
88
169
  return new Promise((resolve, reject) => {
89
170
  const abort = () => reject(signal.reason);
90
171
  signal.addEventListener("abort", abort, { once: true });
@@ -102,15 +183,22 @@ export async function cancelExactCliBridgeRun(options, transport, run, request,
102
183
  if (!run.controlRef || !sameControlRef(run.controlRef, exactRequest.run)) {
103
184
  throw new Error("cli-bridge cancellation targets another retained run");
104
185
  }
105
- const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
186
+ const timeoutSignal = cliBridgeCancellationSignal(options);
187
+ const operationSignal = signal === undefined
188
+ ? timeoutSignal
189
+ : AbortSignal.any([signal, timeoutSignal]);
190
+ const response = await waitForOperation(transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
106
191
  method: "POST",
107
192
  headers: requestHeaders(options),
108
193
  body: JSON.stringify(exactRequest),
109
- ...(signal ? { signal } : {}),
110
- });
111
- const responseText = await response.text();
112
- if (!response.ok && response.status !== 409) {
113
- throw new Error(`cli-bridge cancel ${response.status}: ${responseText}`);
194
+ signal: operationSignal,
195
+ }), operationSignal);
196
+ const responseText = await waitForOperation(readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, operationSignal), operationSignal);
197
+ return parseExactCancellationAcknowledgement(exactRequest, response.status, responseText);
198
+ }
199
+ function parseExactCancellationAcknowledgement(request, status, responseText) {
200
+ if ((status < 200 || status >= 300) && status !== 409) {
201
+ throw new Error(`cli-bridge cancel ${status}: ${responseText}`);
114
202
  }
115
203
  let acknowledgement;
116
204
  try {
@@ -121,7 +209,7 @@ export async function cancelExactCliBridgeRun(options, transport, run, request,
121
209
  cause: error,
122
210
  });
123
211
  }
124
- if (!agentRunCancellationAcknowledgementMatchesRequest(exactRequest, acknowledgement)) {
212
+ if (!agentRunCancellationAcknowledgementMatchesRequest(request, acknowledgement)) {
125
213
  throw new Error("cli-bridge returned a cancellation acknowledgement for another request");
126
214
  }
127
215
  return acknowledgement;
@@ -136,27 +224,62 @@ function sameControlRef(left, right) {
136
224
  }
137
225
  export async function getCliBridgeRun(options, transport, run, waitMs, signal) {
138
226
  const query = waitMs === undefined ? "" : `?wait_ms=${waitMs}`;
139
- const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}${query}`, {
227
+ const response = await waitForOperation(transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}${query}`, {
140
228
  method: "GET",
141
229
  headers: requestHeaders(options),
142
230
  ...(signal ? { signal } : {}),
143
- });
231
+ }), signal);
232
+ const responseText = await waitForOperation(readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal), signal);
144
233
  if (response.status === 404)
145
234
  return null;
146
235
  if (!response.ok) {
147
- throw new Error(`cli-bridge run status ${response.status}: ${await response.text()}`);
236
+ throw new Error(`cli-bridge run status ${response.status}: ${responseText}`);
148
237
  }
149
- const snapshot = runSnapshot(await response.text());
150
- assertCliBridgeRunSnapshotIdentity(snapshot, run.id, run.requestDigest);
238
+ const snapshot = runSnapshot(responseText);
239
+ bindCliBridgeRunSnapshot(run, snapshot);
151
240
  return snapshot;
152
241
  }
153
- function assertCliBridgeRunSnapshotIdentity(snapshot, expectedRunId, expectedRequestDigest) {
154
- if (snapshot.id !== expectedRunId) {
155
- throw new Error(`cli-bridge returned run "${snapshot.id}" for requested run "${expectedRunId}"`);
242
+ function bindCliBridgeRunSnapshot(run, snapshot) {
243
+ if (snapshot.controlRef === undefined) {
244
+ throw new Error("cli-bridge run status omitted exact run coordinates");
245
+ }
246
+ bindExactControlRef(run, snapshot.controlRef);
247
+ }
248
+ function bindExactControlRef(run, controlRef) {
249
+ if (controlRef.runId !== run.id ||
250
+ controlRef.provider !== run.provider ||
251
+ controlRef.environmentId !== run.environmentId ||
252
+ controlRef.sessionId !== run.sessionId ||
253
+ controlRef.executionId !== run.executionId ||
254
+ (run.requestDigest !== undefined && controlRef.requestDigest !== run.requestDigest)) {
255
+ throw new Error("cli-bridge returned another retained run identity");
256
+ }
257
+ run.requestDigest = controlRef.requestDigest;
258
+ run.controlRef = Object.freeze(controlRef);
259
+ }
260
+ /** Recover one exact Bridge run after dispatch succeeded but local admission did not. */
261
+ export async function lookupExactCliBridgeRun(options, providerName, input) {
262
+ const runId = assertCliBridgeRunId(AgentExactRunControlRefSchema.shape.runId.parse(input.runId));
263
+ const environmentId = AgentExactRunControlRefSchema.shape.environmentId.parse(input.environmentId);
264
+ const sessionId = AgentExactRunControlRefSchema.shape.sessionId.parse(input.sessionId);
265
+ const executionId = AgentExactRunControlRefSchema.shape.executionId.parse(input.executionId);
266
+ const transport = createCliBridgeTransport(options);
267
+ const run = {
268
+ id: runId,
269
+ provider: providerName,
270
+ environmentId,
271
+ sessionId,
272
+ executionId,
273
+ turnId: executionId,
274
+ requestBody: "",
275
+ readers: new Set(),
276
+ };
277
+ try {
278
+ const snapshot = await getCliBridgeRun(options, transport, run, undefined, input.signal);
279
+ return snapshot?.controlRef ?? null;
156
280
  }
157
- if (expectedRequestDigest !== undefined &&
158
- snapshot.requestDigest !== expectedRequestDigest) {
159
- throw new Error(`cli-bridge returned request digest "${snapshot.requestDigest}" for run "${expectedRunId}"`);
281
+ finally {
282
+ await transport.close();
160
283
  }
161
284
  }
162
285
  export function agentSessionStatusFromRun(snapshot) {
@@ -2,7 +2,7 @@ import { agentSessionStatusFromRun, cancelCliBridgeRun, cancelExactCliBridgeRun,
2
2
  import { addTokenUsage, collectCliBridgeTurnResult, dispatchCliBridgeTurn, streamCliBridgeSessionEvents, streamTrackedCliBridgeTurn, } from "./retained-execution.js";
3
3
  import { createExecutionUsageLog, observeCliBridgeEnvironment, } from "./observation.js";
4
4
  import { cliBridgeInteractionResponder } from "./interaction-response.js";
5
- import { assertCliBridgeRequestedInteractions, assertCliBridgeNativeTurnModel, continueCliBridgeNative, readCliBridgeNativeContextBoundary, supportsCliBridgeNativeContinuation, supportsCliBridgeNativeInteractions, } from "./retained-native.js";
5
+ import { assertCliBridgeRequestedInteractions, assertCliBridgeTurnModel, continueCliBridgeNative, readCliBridgeNativeContextBoundary, supportsCliBridgeNativeContinuation, supportsCliBridgeNativeInteractions, } from "./retained-native.js";
6
6
  import { assertRunMatchesControlRef, exactControlRefForSession, prepareCliBridgeRun, runFromControlRef, } from "./retained-run-state.js";
7
7
  import { createCliBridgeTransport } from "./transport.js";
8
8
  export function createCliBridgeEnvironment(args) {
@@ -30,11 +30,12 @@ export function createCliBridgeEnvironment(args) {
30
30
  if (!args.allowDispatch) {
31
31
  throw new Error("a reconstructed cli-bridge environment can only control an existing run");
32
32
  }
33
- if (useNativeInteractions) {
34
- assertCliBridgeNativeTurnModel(options, environmentInput, turn, args.selectedModel);
33
+ if (args.selectedModel !== undefined &&
34
+ args.capabilities.retainedControl !== undefined) {
35
+ assertCliBridgeTurnModel(options, environmentInput, turn, args.selectedModel);
35
36
  }
36
37
  assertCliBridgeRequestedInteractions(args.capabilities, turn.interactions);
37
- const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, useNativeInteractions);
38
+ const prepared = prepareCliBridgeRun(options, environmentInput, turn, providerName, environmentId, useNativeInteractions);
38
39
  // A streamed run reports usage across its events, so the totals are summed
39
40
  // here and recorded as one execution's measurement.
40
41
  let streamedUsage;
@@ -60,11 +61,12 @@ export function createCliBridgeEnvironment(args) {
60
61
  if (!args.allowDispatch) {
61
62
  throw new Error("a reconstructed cli-bridge environment cannot dispatch new work");
62
63
  }
63
- if (useNativeInteractions) {
64
- assertCliBridgeNativeTurnModel(options, environmentInput, turn, args.selectedModel);
64
+ if (args.selectedModel !== undefined &&
65
+ args.capabilities.retainedControl !== undefined) {
66
+ assertCliBridgeTurnModel(options, environmentInput, turn, args.selectedModel);
65
67
  }
66
68
  assertCliBridgeRequestedInteractions(args.capabilities, turn.interactions);
67
- const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, true);
69
+ const prepared = prepareCliBridgeRun(options, environmentInput, turn, providerName, environmentId, useNativeInteractions);
68
70
  return dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions, nativeSessions, useNativeInteractions);
69
71
  },
70
72
  session(id, sessionOptions) {
@@ -233,14 +235,15 @@ function createCliBridgeSession(args) {
233
235
  if (input.sessionId && input.sessionId !== args.id) {
234
236
  throw new Error(`cli-bridge session "${args.id}" cannot prompt session "${input.sessionId}"`);
235
237
  }
236
- if (args.useNativeInteractions) {
237
- assertCliBridgeNativeTurnModel(args.options, args.environmentInput, input, args.selectedModel);
238
+ if (args.selectedModel !== undefined &&
239
+ args.capabilities.retainedControl !== undefined) {
240
+ assertCliBridgeTurnModel(args.options, args.environmentInput, input, args.selectedModel);
238
241
  }
239
242
  assertCliBridgeRequestedInteractions(args.capabilities, input.interactions);
240
243
  const prepared = prepareCliBridgeRun(args.options, args.environmentInput, {
241
244
  ...input,
242
245
  sessionId: args.id,
243
- }, args.environmentId, args.useNativeInteractions);
246
+ }, args.providerName, args.environmentId, args.useNativeInteractions);
244
247
  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);
245
248
  args.usageLog.record(prepared.run.executionId, result.usage);
246
249
  return { ...result, sessionId: args.id };
@@ -1,8 +1,8 @@
1
- import { cancelCliBridgeRun, captureCliBridgeRunIdentity, detachCliBridgeReader, getCliBridgeRun, } from "./retained-control.js";
1
+ import { cancelCliBridgeRun, captureCliBridgeRunIdentity, cliBridgeCancellationSignal, 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
4
  import { beginCliBridgeNativeTurn, } from "./retained-native.js";
5
- import { requestHeaders, trimSlash } from "./transport.js";
5
+ import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
6
6
  import { modelRequestCount } from "./wire.js";
7
7
  export async function* streamTrackedCliBridgeTurn(options, environmentInput, providerName, prepared, transport, runs, sessions, readers, nativeSessions, useNativeInteractions) {
8
8
  const originalTurn = prepared.turn;
@@ -43,7 +43,7 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pro
43
43
  for await (const event of streamCliBridgeTurn(options, turn, run.requestBody, transport, run.id, originalTurn.lastEventId, signal, (response) => {
44
44
  admitted = true;
45
45
  captureCliBridgeRunIdentity(response, run, true);
46
- admissionValidated = true;
46
+ admissionValidated = response.body !== null;
47
47
  }, () => getCliBridgeRun(options, transport, run, 30_000, signal))) {
48
48
  yield event;
49
49
  }
@@ -61,7 +61,7 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pro
61
61
  }
62
62
  let snapshot;
63
63
  try {
64
- snapshot = await getCliBridgeRun(options, transport, run);
64
+ snapshot = await getCliBridgeRun(options, transport, run, undefined, cliBridgeCancellationSignal(options));
65
65
  }
66
66
  catch {
67
67
  snapshot = undefined;
@@ -73,12 +73,17 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pro
73
73
  else if ((admitted && !admissionValidated) ||
74
74
  originalTurn.signal?.aborted ||
75
75
  environmentInput.signal?.aborted) {
76
- const cancellation = await cancelCliBridgeRun(options, transport, run);
77
- if (cancellation.requestDigest !== undefined) {
78
- run.requestDigest = cancellation.requestDigest;
76
+ try {
77
+ const cancellation = await cancelCliBridgeRun(options, transport, run);
78
+ if (cancellation.requestDigest !== undefined) {
79
+ run.requestDigest = cancellation.requestDigest;
80
+ }
81
+ if (runs.get(run.id) === run)
82
+ runs.delete(run.id);
83
+ }
84
+ catch (cleanupError) {
85
+ throw new AggregateError([error, cleanupError], `cli-bridge stream "${run.id}" failed and exact cancellation was not confirmed`);
79
86
  }
80
- if (runs.get(run.id) === run)
81
- runs.delete(run.id);
82
87
  }
83
88
  throw error;
84
89
  }
@@ -122,7 +127,7 @@ export async function dispatchCliBridgeTurn(options, environmentInput, prepared,
122
127
  if (!response.ok) {
123
128
  let detail = "request rejected";
124
129
  try {
125
- detail = await response.text();
130
+ detail = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
126
131
  }
127
132
  catch {
128
133
  // The HTTP status already proves this request was rejected.
@@ -17,7 +17,7 @@ export declare function supportsCliBridgeNativeContinuation(capabilities: AgentE
17
17
  /** Reject posture the selected environment did not advertise before transport use. */
18
18
  export declare function assertCliBridgeRequestedInteractions(capabilities: AgentEnvironmentCapabilities, requested: RequestedInteractions | undefined): void;
19
19
  /** Keep capability discovery and every native turn on one exact model route. */
20
- export declare function assertCliBridgeNativeTurnModel(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, selectedModel: string | undefined): void;
20
+ export declare function assertCliBridgeTurnModel(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, selectedModel: string | undefined): void;
21
21
  /** Read the Bridge-owned exact boundary for the current retained run. */
22
22
  export declare function readCliBridgeNativeContextBoundary(options: CliBridgeProviderOptions, providerName: string, transport: CliBridgeTransport, run: CliBridgeRun, signal?: AbortSignal): Promise<NativeContextBoundaryProof | null>;
23
23
  /** Send one canonical same-session continuation and advance the live run identity. */
@@ -1,7 +1,7 @@
1
1
  import { AgentNativeContextContinuationResultSchema, AgentExactRunControlRefSchema, NativeContextBoundaryProofSchema, NativeContextContinuationRequestSchema, RequestedInteractionsSchema, agentNativeContextContinuationResultMatchesRequest, canonicalCandidateDigest, nativeContextContinuationTurnDigest, } from "@tangle-network/agent-interface";
2
2
  import { CliBridgeRequestRejectedError } from "./retained-stream.js";
3
3
  import { advanceCliBridgeRun, exactControlRefForRun, } from "./retained-run-state.js";
4
- import { requestHeaders, trimSlash, } from "./transport.js";
4
+ import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
5
5
  import { inlineProfile, safeJson, resolveBridgeModel, toRetainedSessionBody, toRetainedTurnBody, } from "./wire.js";
6
6
  const MAX_NATIVE_CONTINUATION_RESPONSE_BYTES = 1_048_576;
7
7
  /** Interactions are valid only for the native retained Pi contract. */
@@ -38,13 +38,13 @@ export function assertCliBridgeRequestedInteractions(capabilities, requested) {
38
38
  }
39
39
  }
40
40
  /** Keep capability discovery and every native turn on one exact model route. */
41
- export function assertCliBridgeNativeTurnModel(options, environmentInput, turn, selectedModel) {
41
+ export function assertCliBridgeTurnModel(options, environmentInput, turn, selectedModel) {
42
42
  if (selectedModel === undefined) {
43
- throw new Error("cli-bridge native interactions require an environment model");
43
+ throw new Error("cli-bridge retained execution requires an environment model");
44
44
  }
45
45
  const requestedModel = resolveBridgeModel(options, environmentInput, turn, inlineProfile(environmentInput.profile));
46
46
  if (requestedModel !== selectedModel) {
47
- throw new Error(`cli-bridge native environment model is ${JSON.stringify(selectedModel)}; create another environment for ${JSON.stringify(requestedModel)}`);
47
+ throw new Error(`cli-bridge environment model is ${JSON.stringify(selectedModel)}; create another environment for ${JSON.stringify(requestedModel)}`);
48
48
  }
49
49
  }
50
50
  function piInteractionCapabilitiesMatch(interactions) {
@@ -65,7 +65,7 @@ export async function readCliBridgeNativeContextBoundary(options, providerName,
65
65
  headers: requestHeaders(options),
66
66
  ...(signal ? { signal } : {}),
67
67
  });
68
- const responseText = await readBoundedResponseText(response, signal);
68
+ const responseText = await readBoundedCliBridgeResponse(response, MAX_NATIVE_CONTINUATION_RESPONSE_BYTES, signal);
69
69
  if (!response.ok) {
70
70
  throw new CliBridgeRequestRejectedError(response.status, responseText);
71
71
  }
@@ -107,7 +107,7 @@ export async function continueCliBridgeNative(options, providerName, transport,
107
107
  body: JSON.stringify({ request: exactRequest, turn: continuationOptions.turn }),
108
108
  ...(operation.signal ? { signal: operation.signal } : {}),
109
109
  });
110
- const responseText = await readBoundedResponseText(response, operation.signal);
110
+ const responseText = await readBoundedCliBridgeResponse(response, MAX_NATIVE_CONTINUATION_RESPONSE_BYTES, operation.signal);
111
111
  const outcome = parseNativeContinuationResult(responseText);
112
112
  assertContinuationAcknowledgementBinding(exactRequest, outcome);
113
113
  if (outcome.acknowledgement.status === "accepted" ||
@@ -202,49 +202,6 @@ function continuationSignal(options) {
202
202
  dispose: () => clearTimeout(timer),
203
203
  };
204
204
  }
205
- async function readBoundedResponseText(response, signal) {
206
- const contentLength = response.headers.get("content-length");
207
- if (contentLength !== null) {
208
- const parsedLength = Number(contentLength);
209
- if (Number.isSafeInteger(parsedLength) &&
210
- parsedLength > MAX_NATIVE_CONTINUATION_RESPONSE_BYTES) {
211
- throw new Error("cli-bridge native continuation response exceeds its byte limit");
212
- }
213
- }
214
- if (response.body === null) {
215
- signal?.throwIfAborted();
216
- const text = await response.text();
217
- if (new TextEncoder().encode(text).byteLength > MAX_NATIVE_CONTINUATION_RESPONSE_BYTES) {
218
- throw new Error("cli-bridge native continuation response exceeds its byte limit");
219
- }
220
- return text;
221
- }
222
- const iterator = response.body[Symbol.asyncIterator]();
223
- const decoder = new TextDecoder();
224
- let bytes = 0;
225
- let text = "";
226
- let complete = false;
227
- try {
228
- while (true) {
229
- signal?.throwIfAborted();
230
- const next = await iterator.next();
231
- if (next.done)
232
- break;
233
- bytes += next.value.byteLength;
234
- if (bytes > MAX_NATIVE_CONTINUATION_RESPONSE_BYTES) {
235
- throw new Error("cli-bridge native continuation response exceeds its byte limit");
236
- }
237
- text += decoder.decode(next.value, { stream: true });
238
- }
239
- complete = true;
240
- return text + decoder.decode();
241
- }
242
- finally {
243
- if (!complete) {
244
- await iterator.return?.();
245
- }
246
- }
247
- }
248
205
  /**
249
206
  * Create or reuse the server's retained native session for one external id.
250
207
  *
@@ -288,21 +245,21 @@ async function createCliBridgeNativeSession(options, environmentInput, profile,
288
245
  return recoverConflictingNativeSession(options, sessionId, model, createRequestDigest, transport, signal, response);
289
246
  }
290
247
  if (!response.ok) {
291
- throw new CliBridgeRequestRejectedError(response.status, await response.text());
248
+ throw new CliBridgeRequestRejectedError(response.status, await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal));
292
249
  }
293
- return parseNativeSessionView(await response.text(), sessionId, model, createRequestDigest);
250
+ return parseNativeSessionView(await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal), sessionId, model, createRequestDigest);
294
251
  }
295
252
  async function recoverConflictingNativeSession(options, sessionId, model, createRequestDigest, transport, signal, conflictResponse) {
296
- await conflictResponse.text();
253
+ await readBoundedCliBridgeResponse(conflictResponse, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
297
254
  const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/sessions/${encodeURIComponent(sessionId)}`, {
298
255
  method: "GET",
299
256
  headers: requestHeaders(options),
300
257
  ...(signal ? { signal } : {}),
301
258
  });
302
259
  if (!response.ok) {
303
- throw new CliBridgeRequestRejectedError(response.status, `session ${JSON.stringify(sessionId)} could not be recovered: ${await response.text()}`);
260
+ throw new CliBridgeRequestRejectedError(response.status, `session ${JSON.stringify(sessionId)} could not be recovered: ${await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal)}`);
304
261
  }
305
- return parseNativeSessionView(await response.text(), sessionId, model, createRequestDigest);
262
+ return parseNativeSessionView(await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal), sessionId, model, createRequestDigest);
306
263
  }
307
264
  function parseNativeSessionView(value, expectedId, expectedModel, expectedCreateRequestDigest) {
308
265
  const parsed = safeJson(value);
@@ -349,10 +306,10 @@ export async function beginCliBridgeNativeTurn(options, providerName, environmen
349
306
  ...(signal ? { signal } : {}),
350
307
  });
351
308
  if (!response.ok) {
352
- throw new CliBridgeRequestRejectedError(response.status, await response.text());
309
+ throw new CliBridgeRequestRejectedError(response.status, await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal));
353
310
  }
354
311
  onAdmission?.();
355
- const parsed = safeJson(await response.text());
312
+ const parsed = safeJson(await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal));
356
313
  const responseSession = parsed?.session;
357
314
  const responseRun = parsed?.run;
358
315
  const responseSessionId = responseSession && typeof responseSession === "object"
@@ -374,6 +331,12 @@ export async function beginCliBridgeNativeTurn(options, providerName, environmen
374
331
  const executionId = responseRun && typeof responseRun === "object"
375
332
  ? responseRun.executionId
376
333
  : undefined;
334
+ const responseProvider = responseRun && typeof responseRun === "object"
335
+ ? responseRun.provider
336
+ : undefined;
337
+ const responseEnvironmentId = responseRun && typeof responseRun === "object"
338
+ ? responseRun.environmentId
339
+ : undefined;
377
340
  const requestDigestValue = responseRun && typeof responseRun === "object"
378
341
  ? responseRun.requestDigest
379
342
  : undefined;
@@ -390,6 +353,8 @@ export async function beginCliBridgeNativeTurn(options, providerName, environmen
390
353
  responseSessionIdFromRun !== run.sessionId ||
391
354
  typeof executionId !== "string" ||
392
355
  executionId !== run.executionId ||
356
+ responseProvider !== providerName ||
357
+ responseEnvironmentId !== run.environmentId ||
393
358
  !requestDigest.success) {
394
359
  throw new Error("cli-bridge retained turn returned mismatched run coordinates");
395
360
  }
@@ -397,4 +362,5 @@ export async function beginCliBridgeNativeTurn(options, providerName, environmen
397
362
  throw new Error("cli-bridge retained turn changed the admitted request digest");
398
363
  }
399
364
  run.requestDigest = requestDigest.data;
365
+ run.controlRef = exactControlRefForRun(run, providerName);
400
366
  }
@@ -3,6 +3,7 @@ import type { AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-networ
3
3
  import type { CliBridgeProviderOptions } from "./provider-options.js";
4
4
  export interface CliBridgeRun {
5
5
  id: string;
6
+ readonly provider: string;
6
7
  readonly environmentId: string;
7
8
  readonly sessionId?: string;
8
9
  executionId: string;
@@ -20,6 +21,7 @@ export interface CliBridgeSessionState {
20
21
  export interface CliBridgeRunSnapshot {
21
22
  readonly id: string;
22
23
  readonly requestDigest?: Sha256Digest;
24
+ readonly controlRef?: AgentExactRunControlRef;
23
25
  readonly status: "running" | "done" | "error" | "cancelled" | "unknown";
24
26
  readonly terminal: boolean;
25
27
  }
@@ -33,7 +35,7 @@ export declare function runFromControlRef(controlRef: AgentExactRunControlRef):
33
35
  export declare function assertRunMatchesControlRef(run: CliBridgeRun, controlRef: AgentExactRunControlRef): void;
34
36
  /** Advance one session object to the exact run returned by native continuation. */
35
37
  export declare function advanceCliBridgeRun(run: CliBridgeRun, controlRef: AgentExactRunControlRef, providerName: string, runs: Map<string, CliBridgeRun>): void;
36
- export declare function prepareCliBridgeRun(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, originalTurn: AgentTurnInput, environmentId: string, requireSession: boolean): PreparedCliBridgeRun;
38
+ export declare function prepareCliBridgeRun(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, originalTurn: AgentTurnInput, providerName: string, environmentId: string, requireStableIdentity?: boolean): PreparedCliBridgeRun;
37
39
  export declare function bindCliBridgeRun(prepared: CliBridgeRun, runs: Map<string, CliBridgeRun>): CliBridgeRun | undefined;
38
40
  export declare function restoreCliBridgeRun(run: CliBridgeRun, previous: CliBridgeRun | undefined, runs: Map<string, CliBridgeRun>): void;
39
41
  export declare function bindCliBridgeSession(run: CliBridgeRun, sessions: Map<string, CliBridgeSessionState>): CliBridgeRun | undefined;
@@ -1,5 +1,5 @@
1
- import { AgentExactRunControlRefSchema, } from "@tangle-network/agent-interface";
2
- import { cliBridgeRunId, toChatCompletionsBody } from "./wire.js";
1
+ import { AgentExactRunControlRefSchema, AgentTurnInputSchema, } from "@tangle-network/agent-interface";
2
+ import { assertCliBridgeRunId, cliBridgeRunId, toChatCompletionsBody, } from "./wire.js";
3
3
  export function exactControlRefForRun(run, providerName) {
4
4
  if (!run.sessionId || !run.requestDigest) {
5
5
  throw new Error("cli-bridge did not return complete retained run coordinates");
@@ -15,6 +15,7 @@ export function exactControlRefForRun(run, providerName) {
15
15
  }
16
16
  export function exactControlRefForSession(value, providerName, environmentId, sessionId) {
17
17
  const controlRef = Object.freeze(AgentExactRunControlRefSchema.parse(value));
18
+ assertCliBridgeRunId(controlRef.runId);
18
19
  if (controlRef.provider !== providerName ||
19
20
  controlRef.environmentId !== environmentId ||
20
21
  controlRef.sessionId !== sessionId) {
@@ -25,6 +26,7 @@ export function exactControlRefForSession(value, providerName, environmentId, se
25
26
  export function runFromControlRef(controlRef) {
26
27
  return {
27
28
  id: controlRef.runId,
29
+ provider: controlRef.provider,
28
30
  environmentId: controlRef.environmentId,
29
31
  sessionId: controlRef.sessionId,
30
32
  executionId: controlRef.executionId,
@@ -37,6 +39,7 @@ export function runFromControlRef(controlRef) {
37
39
  }
38
40
  export function assertRunMatchesControlRef(run, controlRef) {
39
41
  if (run.id !== controlRef.runId ||
42
+ run.provider !== controlRef.provider ||
40
43
  run.environmentId !== controlRef.environmentId ||
41
44
  run.sessionId !== controlRef.sessionId ||
42
45
  run.executionId !== controlRef.executionId ||
@@ -67,28 +70,38 @@ export function advanceCliBridgeRun(run, controlRef, providerName, runs) {
67
70
  run.controlRef = Object.freeze(AgentExactRunControlRefSchema.parse(controlRef));
68
71
  runs.set(run.id, run);
69
72
  }
70
- export function prepareCliBridgeRun(options, environmentInput, originalTurn, environmentId, requireSession) {
71
- if (requireSession &&
72
- (originalTurn.turnId === undefined || originalTurn.executionId === undefined)) {
73
- throw new Error("cli-bridge native retained turns require stable turnId and executionId values");
73
+ export function prepareCliBridgeRun(options, environmentInput, originalTurn, providerName, environmentId, requireStableIdentity = false) {
74
+ const validatedTurn = AgentTurnInputSchema.parse(originalTurn);
75
+ if (requireStableIdentity &&
76
+ (validatedTurn.turnId === undefined || validatedTurn.executionId === undefined)) {
77
+ throw new Error("native cli-bridge turns require stable turnId and executionId");
74
78
  }
75
- const turnId = originalTurn.turnId ?? crypto.randomUUID();
76
- const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
77
- const sessionId = originalTurn.sessionId ?? (requireSession ? runId : undefined);
79
+ const turnId = validatedTurn.turnId ?? crypto.randomUUID();
80
+ const runId = cliBridgeRunId(environmentId, validatedTurn, turnId);
81
+ const sessionId = validatedTurn.sessionId ?? runId;
82
+ const executionId = validatedTurn.executionId ?? runId;
78
83
  const turn = {
79
- ...originalTurn,
84
+ ...validatedTurn,
80
85
  turnId,
81
- ...(sessionId ? { sessionId } : {}),
86
+ sessionId,
87
+ executionId,
82
88
  };
83
89
  return {
84
90
  turn,
85
91
  run: {
86
92
  id: runId,
93
+ provider: providerName,
87
94
  environmentId,
88
- ...(sessionId ? { sessionId } : {}),
89
- executionId: originalTurn.executionId ?? runId,
95
+ sessionId,
96
+ executionId,
90
97
  turnId,
91
- requestBody: JSON.stringify(toChatCompletionsBody(options, environmentInput, turn, runId)),
98
+ requestBody: JSON.stringify(toChatCompletionsBody(options, environmentInput, turn, {
99
+ runId,
100
+ provider: providerName,
101
+ environmentId,
102
+ sessionId,
103
+ executionId,
104
+ })),
92
105
  readers: new Set(),
93
106
  },
94
107
  };
@@ -1,7 +1,7 @@
1
1
  import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-event-cursor.js";
2
2
  import { isTerminalCanonicalEvent, retainedCanonicalEvent, } from "./retained-canonical-event.js";
3
3
  import { detachCliBridgeReader } from "./retained-control.js";
4
- import { requestHeaders, trimSlash } from "./transport.js";
4
+ import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, MAX_CLI_BRIDGE_RESULT_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
5
5
  import { modelRequestsFromOpenAi, parseSse, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
6
6
  export class CliBridgeRequestRejectedError extends Error {
7
7
  status;
@@ -40,27 +40,29 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
40
40
  if (!response.ok) {
41
41
  let detail = "request rejected";
42
42
  try {
43
- detail = await response.text();
43
+ detail = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
44
44
  }
45
45
  catch {
46
46
  // The HTTP status already proves this request was rejected.
47
47
  }
48
48
  throw new CliBridgeRequestRejectedError(response.status, detail);
49
49
  }
50
- if (!response.body)
51
- throw new Error("cli-bridge response body is empty");
52
50
  try {
53
51
  onResponse?.(response);
54
52
  }
55
53
  catch (error) {
56
- try {
57
- await detachCliBridgeReader(response.body);
58
- }
59
- catch (detachError) {
60
- throw new AggregateError([error, detachError], `cli-bridge rejected response identity for run "${runId}" and could not detach its reader`);
54
+ if (response.body) {
55
+ try {
56
+ await detachCliBridgeReader(response.body);
57
+ }
58
+ catch (detachError) {
59
+ throw new AggregateError([error, detachError], `cli-bridge rejected response identity for run "${runId}" and could not detach its reader`);
60
+ }
61
61
  }
62
62
  throw error;
63
63
  }
64
+ if (!response.body)
65
+ throw new Error("cli-bridge response body is empty");
64
66
  let text = "";
65
67
  const sessionId = turn.sessionId ?? runId;
66
68
  const messageId = turn.turnId ?? `${sessionId}:assistant`;
@@ -439,7 +441,7 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal,
439
441
  body: JSON.stringify({ ...body, stream: false }),
440
442
  ...(signal ? { signal } : {}),
441
443
  });
442
- const responseText = await response.text();
444
+ const responseText = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_RESULT_RESPONSE_BYTES, signal);
443
445
  onResponse?.(response);
444
446
  if (!response.ok) {
445
447
  const error = cliBridgeError(safeJson(responseText));
@@ -1,4 +1,6 @@
1
1
  import type { CliBridgeProviderOptions } from "./provider-options.js";
2
+ export declare const MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES: number;
3
+ export declare const MAX_CLI_BRIDGE_RESULT_RESPONSE_BYTES: number;
2
4
  export interface CliBridgeTransport {
3
5
  fetch(input: string, init: CliBridgeRequest): Promise<CliBridgeResponse>;
4
6
  close(): Promise<void>;
@@ -21,3 +23,4 @@ export interface CliBridgeResponse {
21
23
  export declare function createCliBridgeTransport(options: CliBridgeProviderOptions): CliBridgeTransport;
22
24
  export declare function requestHeaders(options: CliBridgeProviderOptions): Record<string, string>;
23
25
  export declare function trimSlash(value: string): string;
26
+ export declare function readBoundedCliBridgeResponse(response: CliBridgeResponse, maxBytes: number, signal?: AbortSignal): Promise<string>;
package/dist/transport.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { Agent, fetch as undiciFetch } from "undici";
2
+ export const MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES = 64 * 1024;
3
+ export const MAX_CLI_BRIDGE_RESULT_RESPONSE_BYTES = 16 * 1024 * 1024;
2
4
  export function createCliBridgeTransport(options) {
3
5
  if (options.fetch) {
4
6
  const fetch = options.fetch;
@@ -30,3 +32,81 @@ export function requestHeaders(options) {
30
32
  export function trimSlash(value) {
31
33
  return value.replace(/\/+$/, "");
32
34
  }
35
+ export async function readBoundedCliBridgeResponse(response, maxBytes, signal) {
36
+ signal?.throwIfAborted();
37
+ if (response.body === null)
38
+ return "";
39
+ const iterator = response.body[Symbol.asyncIterator]();
40
+ const decoder = new TextDecoder();
41
+ const chunks = [];
42
+ let bytes = 0;
43
+ let closePromise;
44
+ const close = () => {
45
+ if (closePromise === undefined) {
46
+ closePromise = (async () => {
47
+ await iterator.return?.();
48
+ })();
49
+ }
50
+ return closePromise;
51
+ };
52
+ try {
53
+ while (true) {
54
+ const next = await nextCliBridgeResponseChunk(iterator, signal, close);
55
+ if (next.done)
56
+ break;
57
+ bytes += next.value.byteLength;
58
+ if (bytes > maxBytes) {
59
+ throw new Error(`cli-bridge response exceeded ${maxBytes} bytes`);
60
+ }
61
+ chunks.push(decoder.decode(next.value, { stream: true }));
62
+ }
63
+ chunks.push(decoder.decode());
64
+ return chunks.join("");
65
+ }
66
+ finally {
67
+ if (signal?.aborted) {
68
+ void close().catch(() => { });
69
+ }
70
+ else {
71
+ await close();
72
+ }
73
+ }
74
+ }
75
+ function nextCliBridgeResponseChunk(iterator, signal, close) {
76
+ if (signal === undefined)
77
+ return iterator.next();
78
+ if (signal.aborted) {
79
+ void close().catch(() => { });
80
+ return Promise.reject(signal.reason);
81
+ }
82
+ return new Promise((resolve, reject) => {
83
+ let settled = false;
84
+ const finish = (callback) => {
85
+ if (settled)
86
+ return;
87
+ settled = true;
88
+ signal.removeEventListener("abort", onAbort);
89
+ callback();
90
+ };
91
+ const onAbort = () => {
92
+ finish(() => reject(signal.reason));
93
+ void close().catch(() => { });
94
+ };
95
+ signal.addEventListener("abort", onAbort, { once: true });
96
+ if (signal.aborted) {
97
+ onAbort();
98
+ return;
99
+ }
100
+ let next;
101
+ try {
102
+ next = Promise.resolve(iterator.next());
103
+ }
104
+ catch (error) {
105
+ finish(() => reject(error));
106
+ return;
107
+ }
108
+ if (settled)
109
+ return;
110
+ void next.then((result) => finish(() => resolve(result)), (error) => finish(() => reject(error)));
111
+ });
112
+ }
package/dist/wire.d.ts CHANGED
@@ -2,7 +2,13 @@ import type { AgentProfile, AgentProfileRef, TokenUsage } from "@tangle-network/
2
2
  import type { AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
3
3
  import type { CliBridgeProviderOptions } from "./provider-options.js";
4
4
  import type { CliBridgeRun, CliBridgeRunSnapshot } from "./retained-run-state.js";
5
- export declare function toChatCompletionsBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, runId: string): Record<string, unknown>;
5
+ export declare function toChatCompletionsBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, coordinates: {
6
+ readonly runId: string;
7
+ readonly provider: string;
8
+ readonly environmentId: string;
9
+ readonly sessionId: string;
10
+ readonly executionId: string;
11
+ }): Record<string, unknown>;
6
12
  export declare function resolveBridgeModel(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, profile: AgentProfile | undefined): string;
7
13
  export declare function toRetainedSessionBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, profile: AgentProfile | undefined, sessionId: string, model: string): Record<string, unknown>;
8
14
  /** Encode the exact retained turn contract without moving posture into metadata. */
@@ -29,5 +35,6 @@ export declare function responseIdentityFromOpenAi(value: unknown): {
29
35
  } | undefined;
30
36
  export declare function modelRequestCount(value: unknown): number | undefined;
31
37
  export declare function cliBridgeRunId(environmentId: string, turn: AgentTurnInput, turnId: string): string;
38
+ export declare function assertCliBridgeRunId(value: string): string;
32
39
  export declare function runSnapshot(value: string): CliBridgeRunSnapshot;
33
40
  export declare function cancelSnapshot(value: string): CliBridgeRunSnapshot;
package/dist/wire.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AgentExactRunControlRefSchema, RequestedInteractionsSchema, } from "@tangle-network/agent-interface";
3
- export function toChatCompletionsBody(options, environmentInput, turn, runId) {
3
+ export function toChatCompletionsBody(options, environmentInput, turn, coordinates) {
4
4
  const profile = inlineProfile(environmentInput.profile);
5
5
  const execution = executionFromInput(options, environmentInput);
6
6
  const interactions = turn.interactions === undefined
@@ -10,8 +10,11 @@ export function toChatCompletionsBody(options, environmentInput, turn, runId) {
10
10
  model: resolveBridgeModel(options, environmentInput, turn, profile),
11
11
  messages: messagesFromTurn(turn),
12
12
  stream: true,
13
- ...(turn.sessionId ? { session_id: turn.sessionId } : {}),
14
- run_id: runId,
13
+ session_id: coordinates.sessionId,
14
+ run_id: coordinates.runId,
15
+ provider: coordinates.provider,
16
+ environment_id: coordinates.environmentId,
17
+ execution_id: coordinates.executionId,
15
18
  ...(options.defaultMode ? { mode: options.defaultMode } : {}),
16
19
  ...(profile ? { agent_profile: profile } : {}),
17
20
  ...(profile?.model?.reasoningEffort
@@ -304,20 +307,34 @@ function nonEmptyString(value) {
304
307
  return typeof value === "string" && value.length > 0 ? value : undefined;
305
308
  }
306
309
  export function cliBridgeRunId(environmentId, turn, turnId) {
307
- if (turn.executionId &&
308
- turn.executionId.length <= 128 &&
309
- /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(turn.executionId)) {
310
+ if (turnId.length === 0) {
311
+ throw new Error("cli-bridge turn id must be non-empty");
312
+ }
313
+ if (turn.executionId !== undefined && turn.executionId.length === 0) {
314
+ throw new Error("cli-bridge execution id must be non-empty");
315
+ }
316
+ if (turn.executionId !== undefined && isCliBridgeRunId(turn.executionId)) {
310
317
  return turn.executionId;
311
318
  }
319
+ const executionId = turn.executionId ?? turnId;
312
320
  const digest = createHash("sha256")
313
321
  .update(environmentId)
314
322
  .update("\0")
315
323
  .update(turn.sessionId ?? "")
316
324
  .update("\0")
317
- .update(turn.executionId ?? turnId)
325
+ .update(executionId)
318
326
  .digest("hex");
319
327
  return `agent-${digest}`;
320
328
  }
329
+ export function assertCliBridgeRunId(value) {
330
+ if (!isCliBridgeRunId(value)) {
331
+ throw new Error("cli-bridge run id must be 1-128 URL-safe characters");
332
+ }
333
+ return value;
334
+ }
335
+ function isCliBridgeRunId(value) {
336
+ return value.length <= 128 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value);
337
+ }
321
338
  export function runSnapshot(value) {
322
339
  const parsed = safeJson(value);
323
340
  if (!parsed)
@@ -328,15 +345,25 @@ export function runSnapshot(value) {
328
345
  : AgentExactRunControlRefSchema.shape.requestDigest.safeParse(parsed.requestDigest);
329
346
  const status = parsed.status;
330
347
  const terminal = parsed.terminal;
348
+ const controlRef = AgentExactRunControlRefSchema.safeParse({
349
+ runId: id,
350
+ provider: parsed.provider,
351
+ environmentId: parsed.environmentId,
352
+ sessionId: parsed.sessionId,
353
+ executionId: parsed.executionId,
354
+ requestDigest: requestDigest?.success ? requestDigest.data : undefined,
355
+ });
331
356
  if (typeof id !== "string" ||
332
357
  (requestDigest !== undefined && !requestDigest.success) ||
333
358
  !["running", "done", "error", "cancelled", "unknown"].includes(String(status)) ||
334
- typeof terminal !== "boolean") {
359
+ typeof terminal !== "boolean" ||
360
+ (status === "running" && terminal)) {
335
361
  throw new Error("cli-bridge run status returned an invalid snapshot");
336
362
  }
337
363
  return {
338
364
  id,
339
365
  ...(requestDigest?.success ? { requestDigest: requestDigest.data } : {}),
366
+ ...(controlRef.success ? { controlRef: controlRef.data } : {}),
340
367
  status: status,
341
368
  terminal,
342
369
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
5
5
  "type": "module",
6
6
  "license": "MIT",