@tangle-network/agent-provider-cli-bridge 0.9.6 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,7 +102,9 @@ Pi retained sessions also expose typed permission interactions and native contin
102
102
  The provider stores the selected harness, exact model route, and canonical create digest inside its opaque environment identifier.
103
103
  This identifier lets `provider.get()` reconstruct profile-selected routes after process death without a cache or repeated configuration.
104
104
  The create digest prevents an altered profile or workspace from reusing the previous retained identity.
105
- `provider.get()` accepts only identifiers created by this provider and rejects plain caller identifiers.
105
+ An accepted portable-context destination can supply its exact environment identifier through `requestedId`.
106
+ The provider recovers that caller-owned route from cli-bridge's durable transfer receipt.
107
+ An unknown caller-owned identifier returns `null`.
106
108
  The provider queries `/v1/capabilities` for each retained route restored through `provider.get()`.
107
109
  `provider.capabilities()` queries the configured default route before the Runtime selects retained execution.
108
110
  The provider shares concurrent discovery requests and refreshes every later query.
@@ -63,7 +63,26 @@ export function narrowCliBridgeCapabilities(adapter, reported, selectedBackend,
63
63
  };
64
64
  const nativeContinuation = supportsCliBridgeNativeContinuation(adapter) &&
65
65
  supportsCliBridgeNativeContinuation(reported)
66
- ? { atomicBoundary: true, requestIdempotency: true }
66
+ ? {
67
+ atomicBoundary: true,
68
+ requestIdempotency: true,
69
+ ...(adapter.nativeContinuation?.admissionControl === true &&
70
+ reported.nativeContinuation?.admissionControl === true
71
+ ? { admissionControl: true }
72
+ : {}),
73
+ }
74
+ : undefined;
75
+ const contextTransfer = adapter.contextTransfer?.freshSession === true &&
76
+ adapter.contextTransfer.requestIdempotency === true &&
77
+ adapter.contextTransfer.lookup === true &&
78
+ reported.contextTransfer?.freshSession === true &&
79
+ reported.contextTransfer.requestIdempotency === true &&
80
+ reported.contextTransfer.lookup === true
81
+ ? {
82
+ freshSession: true,
83
+ requestIdempotency: true,
84
+ lookup: true,
85
+ }
67
86
  : undefined;
68
87
  const sessions = {
69
88
  continue: adapter.sessions.continue && reported.sessions.continue,
@@ -118,6 +137,7 @@ export function narrowCliBridgeCapabilities(adapter, reported, selectedBackend,
118
137
  sessions,
119
138
  ...(retainedControl === undefined ? {} : { retainedControl }),
120
139
  ...(nativeContinuation === undefined ? {} : { nativeContinuation }),
140
+ ...(contextTransfer === undefined ? {} : { contextTransfer }),
121
141
  ...(interactions === undefined ? {} : { interactions }),
122
142
  // Remote workspace support cannot create absent adapter methods.
123
143
  workspace: {
@@ -0,0 +1,8 @@
1
+ import { type AgentContextTransferProvider, type ContextTransferRequest, type ContextTransferResult, type ContextTransferReceipt } from "@tangle-network/agent-interface";
2
+ import type { CliBridgeProviderOptions } from "./provider-options.js";
3
+ /** Bind the shared portable-context contract to CLI Bridge's durable endpoint. */
4
+ export declare function createCliBridgeContextTransfer(options: CliBridgeProviderOptions): AgentContextTransferProvider;
5
+ export declare function transferCliBridgeContext(options: CliBridgeProviderOptions, request: ContextTransferRequest, signal?: AbortSignal): Promise<ContextTransferResult>;
6
+ export declare function lookupCliBridgeContext(options: CliBridgeProviderOptions, request: ContextTransferRequest, signal?: AbortSignal): Promise<ContextTransferResult | undefined>;
7
+ /** Recover the accepted destination route for one caller-owned environment id. */
8
+ export declare function lookupCliBridgeContextByEnvironment(options: CliBridgeProviderOptions, environmentId: string, signal?: AbortSignal): Promise<ContextTransferReceipt | undefined>;
@@ -0,0 +1,117 @@
1
+ import { ContextTransferRequestSchema, ContextTransferResultSchema, contextTransferResultMatchesRequest, } from "@tangle-network/agent-interface";
2
+ import { createCliBridgeTransport, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
3
+ /** Bind the shared portable-context contract to CLI Bridge's durable endpoint. */
4
+ export function createCliBridgeContextTransfer(options) {
5
+ return {
6
+ transfer: (request, transferOptions) => transferCliBridgeContext(options, request, transferOptions?.signal),
7
+ lookup: (request, lookupOptions) => lookupCliBridgeContext(options, request, lookupOptions?.signal),
8
+ };
9
+ }
10
+ export async function transferCliBridgeContext(options, request, signal) {
11
+ const exact = ContextTransferRequestSchema.parse(request);
12
+ const transport = createCliBridgeTransport(options);
13
+ try {
14
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/context-transfers`, {
15
+ method: "POST",
16
+ headers: requestHeaders(options),
17
+ body: JSON.stringify(exact),
18
+ ...(signal === undefined ? {} : { signal }),
19
+ });
20
+ const body = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
21
+ return parseContextTransferResponse(exact, response.status, body);
22
+ }
23
+ catch (error) {
24
+ signal?.throwIfAborted();
25
+ return transportFailure(exact, error);
26
+ }
27
+ finally {
28
+ await transport.close();
29
+ }
30
+ }
31
+ export async function lookupCliBridgeContext(options, request, signal) {
32
+ const exact = ContextTransferRequestSchema.parse(request);
33
+ const transport = createCliBridgeTransport(options);
34
+ try {
35
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/context-transfers/${encodeURIComponent(exact.operationId)}?request_digest=${encodeURIComponent(exact.requestDigest)}`, {
36
+ method: "GET",
37
+ headers: requestHeaders(options),
38
+ ...(signal === undefined ? {} : { signal }),
39
+ });
40
+ const body = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
41
+ if (response.status === 404)
42
+ return undefined;
43
+ return parseContextTransferResponse(exact, response.status, body);
44
+ }
45
+ catch (error) {
46
+ signal?.throwIfAborted();
47
+ return transportFailure(exact, error);
48
+ }
49
+ finally {
50
+ await transport.close();
51
+ }
52
+ }
53
+ /** Recover the accepted destination route for one caller-owned environment id. */
54
+ export async function lookupCliBridgeContextByEnvironment(options, environmentId, signal) {
55
+ const transport = createCliBridgeTransport(options);
56
+ try {
57
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/context-transfer-environments/${encodeURIComponent(environmentId)}`, {
58
+ method: "GET",
59
+ headers: requestHeaders(options),
60
+ ...(signal === undefined ? {} : { signal }),
61
+ });
62
+ const body = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
63
+ if (response.status === 404)
64
+ return undefined;
65
+ const parsed = parseContextTransferResult(body, response.status);
66
+ if ((parsed.status !== "accepted" && parsed.status !== "replayed") ||
67
+ parsed.environmentId !== environmentId) {
68
+ throw new Error("CLI Bridge returned a context transfer for another environment");
69
+ }
70
+ return parsed;
71
+ }
72
+ finally {
73
+ await transport.close();
74
+ }
75
+ }
76
+ function parseContextTransferResponse(request, status, body) {
77
+ let value;
78
+ try {
79
+ value = JSON.parse(body);
80
+ }
81
+ catch (error) {
82
+ return transportFailure(request, new Error(`CLI Bridge context transfer returned HTTP ${status} with invalid JSON`, {
83
+ cause: error,
84
+ }));
85
+ }
86
+ const parsed = ContextTransferResultSchema.safeParse(value);
87
+ if (!parsed.success || !contextTransferResultMatchesRequest(request, parsed.data)) {
88
+ return transportFailure(request, new Error(`CLI Bridge context transfer returned HTTP ${status} with an invalid result`));
89
+ }
90
+ return parsed.data;
91
+ }
92
+ function parseContextTransferResult(body, status) {
93
+ let value;
94
+ try {
95
+ value = JSON.parse(body);
96
+ }
97
+ catch (error) {
98
+ throw new Error(`CLI Bridge context transfer returned HTTP ${status} with invalid JSON`, {
99
+ cause: error,
100
+ });
101
+ }
102
+ const parsed = ContextTransferResultSchema.safeParse(value);
103
+ if (!parsed.success) {
104
+ throw new Error(`CLI Bridge context transfer returned HTTP ${status} with an invalid result`);
105
+ }
106
+ return parsed.data;
107
+ }
108
+ function transportFailure(request, error) {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ return ContextTransferResultSchema.parse({
111
+ status: "transport_failure",
112
+ operationId: request.operationId,
113
+ requestDigest: request.requestDigest,
114
+ message: message.slice(0, 4_096) || "CLI Bridge context transfer failed",
115
+ retryable: true,
116
+ });
117
+ }
@@ -11,3 +11,5 @@ export interface CliBridgeEnvironmentRoute {
11
11
  export declare function cliBridgeEnvironmentId(route: Omit<CliBridgeEnvironmentRoute, "createDigest">, createDigest: Sha256Digest, idempotencyKey?: string): string;
12
12
  /** Recover the exact route from a provider-owned retained identifier. */
13
13
  export declare function cliBridgeEnvironmentRoute(id: string): CliBridgeEnvironmentRoute;
14
+ /** Parse a provider-owned route, or identify a caller-owned destination id. */
15
+ export declare function optionalCliBridgeEnvironmentRoute(id: string): CliBridgeEnvironmentRoute | undefined;
@@ -60,6 +60,12 @@ export function cliBridgeEnvironmentRoute(id) {
60
60
  createDigest,
61
61
  };
62
62
  }
63
+ /** Parse a provider-owned route, or identify a caller-owned destination id. */
64
+ export function optionalCliBridgeEnvironmentRoute(id) {
65
+ if (!id.startsWith(CLI_BRIDGE_ENVIRONMENT_ID_PREFIX))
66
+ return undefined;
67
+ return cliBridgeEnvironmentRoute(id);
68
+ }
63
69
  function optionalRouteValue(value) {
64
70
  return value === null || routeValue(value);
65
71
  }
package/dist/index.js CHANGED
@@ -4,9 +4,10 @@ import { createCliBridgeEnvironment, } from "./retained-environment.js";
4
4
  import { lookupExactCliBridgeRun, } from "./retained-control.js";
5
5
  import { cachedCliBridgeCapabilityDiscovery, narrowCliBridgeCapabilities, } from "./capability-discovery.js";
6
6
  import { supportsCliBridgeNativeInteractions, } from "./retained-native.js";
7
+ import { createCliBridgeContextTransfer, lookupCliBridgeContextByEnvironment, } from "./context-transfer.js";
7
8
  import { assertCliBridgeProviderOptions, } from "./provider-options.js";
8
9
  import { narrowedCliBridgeObservation } from "./observation.js";
9
- import { cliBridgeEnvironmentId, cliBridgeEnvironmentRoute, } from "./environment-identity.js";
10
+ import { cliBridgeEnvironmentId, optionalCliBridgeEnvironmentRoute, } from "./environment-identity.js";
10
11
  import { resolveBridgeModel } from "./wire.js";
11
12
  export { safeEndpointFromBaseUrl } from "./observation.js";
12
13
  export function createCliBridgeProvider(options) {
@@ -68,7 +69,8 @@ export function createCliBridgeProvider(options) {
68
69
  const model = hasEnvironmentModel
69
70
  ? resolveBridgeModel(options, environmentInput, {}, profile)
70
71
  : undefined;
71
- const environmentId = cliBridgeEnvironmentId({ backend: selectedBackend, model }, agentEnvironmentCreateInputDigest(environmentInput), input.idempotencyKey);
72
+ const environmentId = input.requestedId ?? cliBridgeEnvironmentId({ backend: selectedBackend, model }, agentEnvironmentCreateInputDigest(environmentInput), input.idempotencyKey);
73
+ assertRequestedEnvironmentId(environmentId);
72
74
  return createCliBridgeEnvironment({
73
75
  options,
74
76
  providerName: name,
@@ -85,6 +87,7 @@ export function createCliBridgeProvider(options) {
85
87
  };
86
88
  return {
87
89
  name,
90
+ contextTransfer: createCliBridgeContextTransfer(options),
88
91
  lookupRun: (input) => lookupExactCliBridgeRun(options, name, input),
89
92
  capabilities: () => {
90
93
  if (options.defaultModel !== undefined) {
@@ -95,31 +98,41 @@ export function createCliBridgeProvider(options) {
95
98
  create(input) {
96
99
  return createAgentEnvironmentWithIdempotency(createRecords, input, () => createEnvironment(input));
97
100
  },
98
- async get(id) {
101
+ async get(id, getOptions) {
99
102
  if (id.length === 0 || id.trim() !== id) {
100
103
  throw new Error("cli-bridge environment id must be non-empty and have no outer whitespace");
101
104
  }
102
- const route = cliBridgeEnvironmentRoute(id);
105
+ const ownedRoute = optionalCliBridgeEnvironmentRoute(id);
106
+ const transfer = ownedRoute === undefined
107
+ ? await lookupCliBridgeContextByEnvironment(options, id, getOptions?.signal)
108
+ : undefined;
109
+ const backend = ownedRoute?.backend ?? transfer?.destination.runner;
110
+ const destinationModel = transfer?.destination.model;
111
+ const model = ownedRoute?.model ?? (destinationModel === undefined || destinationModel === backend || destinationModel.startsWith(`${backend}/`)
112
+ ? destinationModel ?? backend
113
+ : `${backend}/${destinationModel}`);
114
+ if (ownedRoute === undefined && transfer === undefined)
115
+ return null;
103
116
  return createCliBridgeEnvironment({
104
117
  options,
105
118
  providerName: name,
106
119
  environmentInput: {
107
120
  profile: { name: "reconnected" },
108
- ...(route.backend === undefined ? {} : { backend: route.backend }),
121
+ ...(backend === undefined ? {} : { backend }),
109
122
  idempotencyKey: id,
110
123
  },
111
124
  environmentId: id,
112
125
  allowDispatch: false,
113
126
  cancelRunsOnDestroy: false,
114
- capabilities: await resolveCapabilities(route.backend, route.model, true, true),
115
- selectedBackend: route.backend,
116
- selectedModel: route.model,
127
+ capabilities: await resolveCapabilities(backend, model, true, true),
128
+ selectedBackend: backend,
129
+ selectedModel: model,
117
130
  });
118
131
  },
119
132
  };
120
133
  }
121
134
  function withoutRemoteRetainedCapabilities(capabilities) {
122
- const { retainedControl: _retainedControl, nativeContinuation: _nativeContinuation, interactions: _interactions, ...local } = capabilities;
135
+ const { retainedControl: _retainedControl, nativeContinuation: _nativeContinuation, contextTransfer: _contextTransfer, interactions: _interactions, ...local } = capabilities;
123
136
  return local;
124
137
  }
125
138
  function selectedBackendFromInput(input, configuredBackend) {
@@ -135,6 +148,13 @@ function selectedBackendFromRoute(route) {
135
148
  const parsed = harnessTypeSchema.safeParse(candidate);
136
149
  return parsed.success ? parsed.data : undefined;
137
150
  }
151
+ function assertRequestedEnvironmentId(value) {
152
+ if (value.length === 0 ||
153
+ value.length > 256 ||
154
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value)) {
155
+ throw new Error("cli-bridge requested environment id must be 1-256 URL-safe characters");
156
+ }
157
+ }
138
158
  /**
139
159
  * Describe the bridge backend, not this adapter.
140
160
  *
@@ -171,11 +191,17 @@ export function defaultCliBridgeCapabilities(harness) {
171
191
  eventIdentity: true,
172
192
  cancellationIdempotency: true,
173
193
  },
194
+ contextTransfer: {
195
+ freshSession: true,
196
+ requestIdempotency: true,
197
+ lookup: true,
198
+ },
174
199
  ...(harness === "pi"
175
200
  ? {
176
201
  nativeContinuation: {
177
202
  atomicBoundary: true,
178
203
  requestIdempotency: true,
204
+ admissionControl: true,
179
205
  },
180
206
  }
181
207
  : {}),
@@ -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, assertCliBridgeTurnModel, continueCliBridgeNative, readCliBridgeNativeContextBoundary, supportsCliBridgeNativeContinuation, supportsCliBridgeNativeInteractions, } from "./retained-native.js";
5
+ import { assertCliBridgeRequestedInteractions, assertCliBridgeTurnModel, continueCliBridgeNative, readCliBridgeNativeContextBoundary, supportsCliBridgeNativeContinuation, supportsCliBridgeNativeAdmissionControl, 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
  /**
@@ -269,7 +269,7 @@ function createCliBridgeSession(args) {
269
269
  return readCliBridgeNativeContextBoundary(args.options, args.providerName, args.transport, requireCurrentRun(), boundaryOptions?.signal);
270
270
  },
271
271
  async continueNative(request, continuationOptions) {
272
- const result = await continueCliBridgeNative(args.options, args.providerName, args.transport, requireCurrentRun(), request, continuationOptions, args.runs);
272
+ const result = await continueCliBridgeNative(args.options, args.providerName, args.transport, requireCurrentRun(), request, continuationOptions, args.runs, supportsCliBridgeNativeAdmissionControl(args.capabilities));
273
273
  if ("result" in result && result.result.usage !== undefined) {
274
274
  args.usageLog.record(result.controlRef.executionId, result.result.usage);
275
275
  }
@@ -4,6 +4,7 @@ import { CliBridgeRequestRejectedError, streamCliBridgeTurn, } from "./retained-
4
4
  import { beginCliBridgeNativeTurn, } from "./retained-native.js";
5
5
  import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
6
6
  import { modelRequestCount } from "./wire.js";
7
+ import { lookupCliBridgeContext } from "./context-transfer.js";
7
8
  export async function* streamTrackedCliBridgeTurn(options, environmentInput, providerName, prepared, transport, runs, sessions, readers, nativeSessions, useNativeInteractions) {
8
9
  const originalTurn = prepared.turn;
9
10
  if (originalTurn.detach) {
@@ -48,6 +49,7 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pro
48
49
  yield event;
49
50
  }
50
51
  }
52
+ await confirmCliBridgeContextTransfer(options, originalTurn, run, signal);
51
53
  drained = true;
52
54
  if (runs.get(run.id) === run)
53
55
  runs.delete(run.id);
@@ -142,6 +144,7 @@ export async function dispatchCliBridgeTurn(options, environmentInput, prepared,
142
144
  }
143
145
  const controlRef = exactControlRefForRun(run, providerName);
144
146
  run.controlRef = controlRef;
147
+ await confirmCliBridgeContextTransfer(options, prepared.turn, run, signal);
145
148
  if (responseBody) {
146
149
  await detachCliBridgeReader(responseBody);
147
150
  responseBody = undefined;
@@ -150,6 +153,9 @@ export async function dispatchCliBridgeTurn(options, environmentInput, prepared,
150
153
  id: run.sessionId,
151
154
  provider: providerName,
152
155
  controlRef,
156
+ ...(run.contextTransferReceipt === undefined
157
+ ? {}
158
+ : { contextTransferReceipt: run.contextTransferReceipt }),
153
159
  metadata: {
154
160
  runId: run.id,
155
161
  requestDigest: run.requestDigest,
@@ -188,6 +194,16 @@ export async function dispatchCliBridgeTurn(options, environmentInput, prepared,
188
194
  throw failure;
189
195
  }
190
196
  }
197
+ async function confirmCliBridgeContextTransfer(options, turn, run, signal) {
198
+ if (turn.contextTransfer === undefined)
199
+ return;
200
+ const transfer = await lookupCliBridgeContext(options, turn.contextTransfer, signal);
201
+ if (transfer === undefined ||
202
+ (transfer.status !== "accepted" && transfer.status !== "replayed")) {
203
+ throw new Error("cli-bridge did not confirm admitted portable context");
204
+ }
205
+ run.contextTransferReceipt = transfer;
206
+ }
191
207
  export async function* streamCliBridgeSessionEvents(options, environmentInput, providerName, run, transport, runs, readers, eventOptions) {
192
208
  if (eventOptions?.executionId !== undefined &&
193
209
  eventOptions.executionId !== run.executionId) {
@@ -302,6 +318,9 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
302
318
  }
303
319
  : {}),
304
320
  ...(run.sessionId ? { sessionId: run.sessionId } : {}),
321
+ ...(run.contextTransferReceipt === undefined
322
+ ? {}
323
+ : { contextTransferReceipt: run.contextTransferReceipt }),
305
324
  ...(usage ? { usage } : {}),
306
325
  metadata: {
307
326
  runId: run.id,
@@ -14,6 +14,8 @@ export type CliBridgeNativeSessionCache = Map<string, Promise<CliBridgeNativeSes
14
14
  export declare function supportsCliBridgeNativeInteractions(capabilities: AgentEnvironmentCapabilities, selectedBackend?: string): boolean;
15
15
  /** Native continuation is exposed only when the Bridge proves both guarantees. */
16
16
  export declare function supportsCliBridgeNativeContinuation(capabilities: AgentEnvironmentCapabilities): boolean;
17
+ /** Early continuation control is valid only with the complete retained-run contract. */
18
+ export declare function supportsCliBridgeNativeAdmissionControl(capabilities: AgentEnvironmentCapabilities): boolean;
17
19
  /** Reject posture the selected environment did not advertise before transport use. */
18
20
  export declare function assertCliBridgeRequestedInteractions(capabilities: AgentEnvironmentCapabilities, requested: RequestedInteractions | undefined): void;
19
21
  /** Keep capability discovery and every native turn on one exact model route. */
@@ -21,7 +23,7 @@ export declare function assertCliBridgeTurnModel(options: CliBridgeProviderOptio
21
23
  /** Read the Bridge-owned exact boundary for the current retained run. */
22
24
  export declare function readCliBridgeNativeContextBoundary(options: CliBridgeProviderOptions, providerName: string, transport: CliBridgeTransport, run: CliBridgeRun, signal?: AbortSignal): Promise<NativeContextBoundaryProof | null>;
23
25
  /** Send one canonical same-session continuation and advance the live run identity. */
24
- export declare function continueCliBridgeNative(options: CliBridgeProviderOptions, providerName: string, transport: CliBridgeTransport, run: CliBridgeRun, request: NativeContextContinuationRequest, continuationOptions: AgentNativeContextContinuationOptions, runs: Map<string, CliBridgeRun>): Promise<AgentNativeContextContinuationResult>;
26
+ export declare function continueCliBridgeNative(options: CliBridgeProviderOptions, providerName: string, transport: CliBridgeTransport, run: CliBridgeRun, request: NativeContextContinuationRequest, continuationOptions: AgentNativeContextContinuationOptions, runs: Map<string, CliBridgeRun>, admissionControl: boolean): Promise<AgentNativeContextContinuationResult>;
25
27
  /**
26
28
  * Create or reuse the server's retained native session for one external id.
27
29
  *
@@ -1,4 +1,4 @@
1
- import { AgentNativeContextContinuationResultSchema, AgentExactRunControlRefSchema, NativeContextBoundaryProofSchema, NativeContextContinuationRequestSchema, RequestedInteractionsSchema, agentNativeContextContinuationResultMatchesRequest, canonicalCandidateDigest, nativeContextContinuationTurnDigest, sameAgentRunControlRef, } from "@tangle-network/agent-interface";
1
+ import { AgentNativeContextContinuationResultSchema, AgentNativeContextContinuationAdmissionSchema, AgentExactRunControlRefSchema, NativeContextBoundaryProofSchema, NativeContextContinuationRequestSchema, RequestedInteractionsSchema, agentNativeContextContinuationResultMatchesRequest, agentNativeContextContinuationAdmissionMatchesRequest, canonicalCandidateDigest, nativeContextContinuationTurnDigest, sameAgentRunControlRef, } from "@tangle-network/agent-interface";
2
2
  import { CliBridgeRequestRejectedError } from "./retained-stream.js";
3
3
  import { advanceCliBridgeRun, exactControlRefForRun, } from "./retained-run-state.js";
4
4
  import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
@@ -26,6 +26,16 @@ export function supportsCliBridgeNativeContinuation(capabilities) {
26
26
  capabilities.nativeContinuation?.atomicBoundary === true &&
27
27
  capabilities.nativeContinuation.requestIdempotency === true);
28
28
  }
29
+ /** Early continuation control is valid only with the complete retained-run contract. */
30
+ export function supportsCliBridgeNativeAdmissionControl(capabilities) {
31
+ const retained = capabilities.retainedControl;
32
+ return Boolean(supportsCliBridgeNativeContinuation(capabilities) &&
33
+ capabilities.nativeContinuation?.admissionControl === true &&
34
+ retained?.exactRunIdentity === true &&
35
+ retained.resultIdentity === true &&
36
+ retained.eventIdentity === true &&
37
+ retained.cancellationIdempotency === true);
38
+ }
29
39
  /** Reject posture the selected environment did not advertise before transport use. */
30
40
  export function assertCliBridgeRequestedInteractions(capabilities, requested) {
31
41
  if (requested === undefined)
@@ -88,7 +98,7 @@ export async function readCliBridgeNativeContextBoundary(options, providerName,
88
98
  return proof.data;
89
99
  }
90
100
  /** Send one canonical same-session continuation and advance the live run identity. */
91
- export async function continueCliBridgeNative(options, providerName, transport, run, request, continuationOptions, runs) {
101
+ export async function continueCliBridgeNative(options, providerName, transport, run, request, continuationOptions, runs, admissionControl) {
92
102
  const exactRequest = NativeContextContinuationRequestSchema.parse(request);
93
103
  const current = currentCliBridgeControlRef(run, providerName);
94
104
  if (!sameAgentRunControlRef(current, exactRequest.run)) {
@@ -98,37 +108,99 @@ export async function continueCliBridgeNative(options, providerName, transport,
98
108
  exactRequest.turnDigest) {
99
109
  throw new Error("cli-bridge native continuation turn does not match its request digest");
100
110
  }
111
+ if (continuationOptions.onAdmission !== undefined && !admissionControl) {
112
+ throw new Error("cli-bridge native continuation does not advertise early admission control");
113
+ }
101
114
  const operation = continuationSignal(continuationOptions);
115
+ const endpoint = `${trimSlash(options.baseUrl)}/v1/sessions/${encodeURIComponent(exactRequest.run.sessionId)}/continue`;
116
+ const body = JSON.stringify({ request: exactRequest, turn: continuationOptions.turn });
117
+ const requestInit = {
118
+ method: "POST",
119
+ headers: requestHeaders(options),
120
+ body,
121
+ ...(operation.signal ? { signal: operation.signal } : {}),
122
+ };
102
123
  try {
103
124
  operation.signal?.throwIfAborted();
104
- const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/sessions/${encodeURIComponent(exactRequest.run.sessionId)}/continue`, {
105
- method: "POST",
106
- headers: requestHeaders(options),
107
- body: JSON.stringify({ request: exactRequest, turn: continuationOptions.turn }),
108
- ...(operation.signal ? { signal: operation.signal } : {}),
109
- });
125
+ const response = await transport.fetch(continuationOptions.onAdmission === undefined
126
+ ? endpoint
127
+ : `${endpoint}?return=admission`, requestInit);
110
128
  const responseText = await readBoundedCliBridgeResponse(response, MAX_NATIVE_CONTINUATION_RESPONSE_BYTES, operation.signal);
129
+ if (continuationOptions.onAdmission !== undefined && response.status === 202) {
130
+ const admission = parseNativeContinuationAdmission(responseText);
131
+ assertContinuationAdmissionBinding(exactRequest, admission);
132
+ assertContinuationAdmissionHeaders(response, admission.controlRef);
133
+ advanceCliBridgeRun(run, admission.controlRef, providerName, runs);
134
+ continuationOptions.onAdmission(admission.controlRef);
135
+ operation.signal?.throwIfAborted();
136
+ const terminalResponse = await transport.fetch(endpoint, requestInit);
137
+ const terminalText = await readBoundedCliBridgeResponse(terminalResponse, MAX_NATIVE_CONTINUATION_RESPONSE_BYTES, operation.signal);
138
+ return acceptNativeContinuationResult(terminalResponse, terminalText, exactRequest, run, providerName, runs);
139
+ }
111
140
  const outcome = parseNativeContinuationResult(responseText);
112
- assertContinuationAcknowledgementBinding(exactRequest, outcome);
113
- if (outcome.acknowledgement.status === "accepted" ||
114
- outcome.acknowledgement.status === "replayed") {
115
- if (!agentNativeContextContinuationResultMatchesRequest(exactRequest, outcome)) {
116
- throw new Error("cli-bridge native continuation returned an acknowledgement for another request");
117
- }
118
- if (!response.ok) {
119
- throw new Error(`cli-bridge native continuation returned HTTP ${response.status} for an accepted result`);
120
- }
121
- if (!("controlRef" in outcome)) {
122
- throw new Error("cli-bridge native continuation omitted its current control reference");
123
- }
124
- advanceCliBridgeRun(run, outcome.controlRef, providerName, runs);
141
+ if (continuationOptions.onAdmission !== undefined &&
142
+ (outcome.acknowledgement.status === "accepted" ||
143
+ outcome.acknowledgement.status === "replayed")) {
144
+ throw new Error("cli-bridge native continuation omitted its advertised early admission");
125
145
  }
126
- return outcome;
146
+ return acceptParsedNativeContinuationResult(response, outcome, exactRequest, run, providerName, runs);
127
147
  }
128
148
  finally {
129
149
  operation.dispose();
130
150
  }
131
151
  }
152
+ function acceptNativeContinuationResult(response, responseText, request, run, providerName, runs) {
153
+ return acceptParsedNativeContinuationResult(response, parseNativeContinuationResult(responseText), request, run, providerName, runs);
154
+ }
155
+ function acceptParsedNativeContinuationResult(response, outcome, exactRequest, run, providerName, runs) {
156
+ assertContinuationAcknowledgementBinding(exactRequest, outcome);
157
+ if (outcome.acknowledgement.status === "accepted" ||
158
+ outcome.acknowledgement.status === "replayed") {
159
+ if (!agentNativeContextContinuationResultMatchesRequest(exactRequest, outcome)) {
160
+ throw new Error("cli-bridge native continuation returned an acknowledgement for another request");
161
+ }
162
+ if (!response.ok) {
163
+ throw new Error(`cli-bridge native continuation returned HTTP ${response.status} for an accepted result`);
164
+ }
165
+ if (!("controlRef" in outcome)) {
166
+ throw new Error("cli-bridge native continuation omitted its current control reference");
167
+ }
168
+ advanceCliBridgeRun(run, outcome.controlRef, providerName, runs);
169
+ }
170
+ return outcome;
171
+ }
172
+ function parseNativeContinuationAdmission(value) {
173
+ const parsed = safeJson(value);
174
+ try {
175
+ return AgentNativeContextContinuationAdmissionSchema.parse(parsed);
176
+ }
177
+ catch (error) {
178
+ throw new Error("cli-bridge native continuation returned an invalid admission", {
179
+ cause: error,
180
+ });
181
+ }
182
+ }
183
+ function assertContinuationAdmissionBinding(request, admission) {
184
+ if (!agentNativeContextContinuationAdmissionMatchesRequest(request, admission)) {
185
+ throw new Error("cli-bridge native continuation admission is not bound to the request");
186
+ }
187
+ }
188
+ function assertContinuationAdmissionHeaders(response, controlRef) {
189
+ const expected = new Map([
190
+ ["x-run-id", controlRef.runId],
191
+ ["x-run-request-digest", controlRef.requestDigest],
192
+ ["x-run-provider", controlRef.provider],
193
+ ["x-run-environment-id", controlRef.environmentId],
194
+ ["x-run-session-id", controlRef.sessionId],
195
+ ["x-run-execution-id", controlRef.executionId],
196
+ ["location", `/v1/runs/${encodeURIComponent(controlRef.runId)}`],
197
+ ]);
198
+ for (const [name, value] of expected) {
199
+ if (response.headers.get(name) !== value) {
200
+ throw new Error(`cli-bridge native continuation admission has invalid ${name}`);
201
+ }
202
+ }
203
+ }
132
204
  function currentCliBridgeControlRef(run, providerName) {
133
205
  if (run.controlRef !== undefined) {
134
206
  const controlRef = AgentExactRunControlRefSchema.parse(run.controlRef);
@@ -1,5 +1,6 @@
1
1
  import { type AgentExactRunControlRef, type AgentRunControlRef, type Sha256Digest } from "@tangle-network/agent-interface";
2
2
  import type { AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
3
+ import type { ContextTransferReceipt } from "@tangle-network/agent-interface";
3
4
  import type { CliBridgeProviderOptions } from "./provider-options.js";
4
5
  export interface CliBridgeRun {
5
6
  id: string;
@@ -13,6 +14,7 @@ export interface CliBridgeRun {
13
14
  requestDigest?: Sha256Digest;
14
15
  controlRef?: AgentExactRunControlRef;
15
16
  cancellation?: Promise<CliBridgeRunSnapshot>;
17
+ contextTransferReceipt?: ContextTransferReceipt;
16
18
  }
17
19
  export interface CliBridgeSessionState {
18
20
  readonly id: string;
@@ -77,9 +77,17 @@ export function prepareCliBridgeRun(options, environmentInput, originalTurn, pro
77
77
  throw new Error("native cli-bridge turns require stable turnId and executionId");
78
78
  }
79
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;
80
+ const destination = validatedTurn.contextTransfer?.plan.destination;
81
+ const runId = destination?.runId ?? cliBridgeRunId(environmentId, validatedTurn, turnId);
82
+ const sessionId = validatedTurn.sessionId ?? destination?.sessionId ?? runId;
83
+ const executionId = validatedTurn.executionId ?? destination?.executionId ?? runId;
84
+ if (destination !== undefined &&
85
+ (destination.provider !== providerName ||
86
+ destination.environmentId !== environmentId ||
87
+ destination.sessionId !== sessionId ||
88
+ destination.executionId !== executionId)) {
89
+ throw new Error("cli-bridge context transfer targets another destination");
90
+ }
83
91
  const turn = {
84
92
  ...validatedTurn,
85
93
  turnId,
package/dist/wire.js CHANGED
@@ -24,6 +24,9 @@ export function toChatCompletionsBody(options, environmentInput, turn, coordinat
24
24
  ...(environmentInput.workspace?.cwd ? { cwd: environmentInput.workspace.cwd } : {}),
25
25
  ...(execution ? { execution } : {}),
26
26
  ...(interactions === undefined ? {} : { interactions }),
27
+ ...(turn.contextTransfer === undefined
28
+ ? {}
29
+ : { context_transfer: turn.contextTransfer }),
27
30
  metadata: {
28
31
  ...(environmentInput.metadata ?? {}),
29
32
  ...(turn.context ?? {}),
@@ -99,6 +102,9 @@ export function toRetainedTurnBody(turn, run, providerName) {
99
102
  provider: providerName,
100
103
  environment_id: run.environmentId,
101
104
  ...(turn.interactions === undefined ? {} : { interactions }),
105
+ ...(turn.contextTransfer === undefined
106
+ ? {}
107
+ : { context_transfer: turn.contextTransfer }),
102
108
  ...(turn.context !== undefined ? { context: turn.context } : {}),
103
109
  ...(turn.providerOptions !== undefined
104
110
  ? { provider_options: turn.providerOptions }
@@ -111,8 +117,6 @@ function assertRetainedTurnInputSupported(turn) {
111
117
  unsupported.push("timeoutMs");
112
118
  if (turn.controlRef !== undefined)
113
119
  unsupported.push("controlRef");
114
- if (turn.contextTransfer !== undefined)
115
- unsupported.push("contextTransfer");
116
120
  if (turn.nativeContinuation !== undefined)
117
121
  unsupported.push("nativeContinuation");
118
122
  // `detach` controls the provider's response reader. The bridge request stays unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "0.9.6",
3
+ "version": "0.11.0",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,8 @@
29
29
  "dist/environment-identity.js",
30
30
  "dist/capability-discovery.d.ts",
31
31
  "dist/capability-discovery.js",
32
+ "dist/context-transfer.d.ts",
33
+ "dist/context-transfer.js",
32
34
  "dist/interaction-response.d.ts",
33
35
  "dist/interaction-response.js",
34
36
  "dist/observation.d.ts",
@@ -59,13 +61,13 @@
59
61
  "LICENSE"
60
62
  ],
61
63
  "dependencies": {
62
- "@tangle-network/agent-interface": "^1.5.0",
64
+ "@tangle-network/agent-interface": "^1.9.0",
63
65
  "undici": "7.29.0"
64
66
  },
65
67
  "devDependencies": {
66
- "@types/node": "25.6.0",
67
- "typescript": "^6.0.3",
68
- "vitest": "^4.1.5"
68
+ "@types/node": "26.4.0",
69
+ "typescript": "7.0.2",
70
+ "vitest": "4.1.11"
69
71
  },
70
72
  "scripts": {
71
73
  "build": "pnpm run clean && tsc -p tsconfig.build.json",