@tangle-network/agent-provider-cli-bridge 1.1.3 → 2.0.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
@@ -49,7 +49,7 @@ The lookup returns the server-issued digest only when all five planned coordinat
49
49
 
50
50
  ## Which call adds a turn
51
51
 
52
- Continuation has two tiers, and the tier depends on whether the environment object came from `create()` or from `get()`.
52
+ Creation, ordinary reconstruction and accepted transfer attachment have different admission authority.
53
53
 
54
54
  In the process that created the environment, `environment.dispatch()`, `environment.stream()`, and `session.prompt()` all add a turn.
55
55
  Passing the same `sessionId` continues the same CLI conversation.
@@ -58,10 +58,11 @@ After `provider.get(environmentId)` reconstructs the environment, only `session.
58
58
  It requires the `nativeContinuation` capability, which the provider publishes only for the Pi harness today, and only when the running Bridge proves the same contract.
59
59
  The reconstructed environment deliberately refuses every other way to start work: `environment.stream()` and `environment.dispatch()` throw "a reconstructed cli-bridge environment can only control an existing run", and `session.prompt()` throws "a reconstructed cli-bridge session cannot start another turn".
60
60
  A reconstructed environment for any other harness therefore controls its existing runs (status, cursor replay, result, cancellation, and interaction responses) but adds no turn.
61
- Restart-safe continuation for those harnesses needs native sessions in the Bridge server itself, which the Bridge does not have yet; it is tracked in the `drewstone/cli-bridge` repository and cannot be reached from this adapter.
61
+
62
+ When the exact deployed route confirms `contextTransfer.attach`, `provider.get(environmentId, { contextTransfer: request })` instead attaches the accepted transfer's first turn. The request must carry its exact profile. The adapter looks up the canonical operation, requires an available matching receipt for that environment, and returns it as `environment.contextTransferReceipt`. The resulting `dispatch`, `stream` and `session.prompt` accept only that request and its destination identities. They do not allocate another environment or replace a missing one. Changed context or profile fails before dispatch; a missing or uncertain receipt refuses attachment. Destroying this reconstructed handle closes its readers without canceling the accepted destination's runs.
62
63
 
63
64
  The capability document keeps `sessions.continue: true` on a reconstructed environment because the Agent Interface couples that flag to `retainedControl`: a document that reports `sessions.continue: false` while keeping retained run control fails `AgentEnvironmentCapabilitiesSchema` with "retained control requires exact run, result, event, cancellation, replay, detach, turn, and session identity together".
64
- `nativeContinuation` is the flag that states whether a reconstructed environment can add a turn: present means `continueNative` works, absent means no call on that environment starts one.
65
+ `nativeContinuation` states whether an ordinary reconstructed environment supports `continueNative`. It does not authorize the first turn of a separate transfer.
65
66
 
66
67
  A runtime caller continues a retained run with `RetainedRunHandle.continueNative`, not with `startRetainedRunInEnvironment`.
67
68
  `startRetainedRunInEnvironment` requires `provider.list` for its ownership proof, and this provider exposes no `list`; it then calls `environment.dispatch()`, which a reconstructed environment refuses.
@@ -82,6 +82,8 @@ export function narrowCliBridgeCapabilities(adapter, reported, selectedBackend,
82
82
  freshSession: true,
83
83
  requestIdempotency: true,
84
84
  lookup: true,
85
+ ...(adapter.contextTransfer.attach === true && reported.contextTransfer.attach === true
86
+ ? { attach: true } : {}),
85
87
  }
86
88
  : undefined;
87
89
  const sessions = {
@@ -9,6 +9,9 @@ export function createCliBridgeContextTransfer(options) {
9
9
  }
10
10
  export async function transferCliBridgeContext(options, request, signal) {
11
11
  const exact = ContextTransferRequestSchema.parse(request);
12
+ if (exact.workspace?.snapshot !== undefined) {
13
+ throw new Error("CLI Bridge provider does not support portable workspace snapshots");
14
+ }
12
15
  const transport = createCliBridgeTransport(options);
13
16
  try {
14
17
  const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/context-transfers`, {
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { agentEnvironmentCreateInputDigest, createAgentEnvironmentWithIdempotency, } from "@tangle-network/agent-interface/environment-provider";
2
- import { harnessTypeSchema, harnessSystemPromptIntents, snapshotAgentProfile, WorkspaceRequestSchema, workspaceCwdPathForBase, } from "@tangle-network/agent-interface";
1
+ import { agentEnvironmentCreateInputDigest, createAgentEnvironmentWithIdempotency, AgentEnvironmentGetOptionsSchema, } from "@tangle-network/agent-interface/environment-provider";
2
+ import { harnessTypeSchema, harnessSystemPromptIntents, snapshotAgentProfile, deepFreeze, ContextTransferReceiptSchema, WorkspaceRequestSchema, workspaceCwdPathForBase, } from "@tangle-network/agent-interface";
3
3
  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
+ import { createCliBridgeContextTransfer, lookupCliBridgeContextByEnvironment, lookupCliBridgeContext, } from "./context-transfer.js";
8
8
  import { assertCliBridgeProviderOptions, } from "./provider-options.js";
9
9
  import { narrowedCliBridgeObservation } from "./observation.js";
10
10
  import { cliBridgeEnvironmentId, optionalCliBridgeEnvironmentRoute, } from "./environment-identity.js";
@@ -13,6 +13,9 @@ export { safeEndpointFromBaseUrl } from "./observation.js";
13
13
  export { closeExactCliBridgeSession } from "./retained-control.js";
14
14
  export function createCliBridgeProvider(options) {
15
15
  assertCliBridgeProviderOptions(options);
16
+ if (options.capabilities?.create?.workspaceSnapshot === true) {
17
+ throw new Error("CLI Bridge provider does not support portable workspace snapshots");
18
+ }
16
19
  const name = options.name ?? "cli-bridge";
17
20
  const createRecords = new Map();
18
21
  const configuredBackend = selectedBackendFromRoute(options.defaultModel);
@@ -55,6 +58,9 @@ export function createCliBridgeProvider(options) {
55
58
  });
56
59
  };
57
60
  const createEnvironment = async (input) => {
61
+ if (input.workspace?.snapshot !== undefined) {
62
+ throw new Error("CLI Bridge provider does not support portable workspace snapshots");
63
+ }
58
64
  if (input.runtimeAttachments !== undefined) {
59
65
  throw new Error("CLI Bridge provider does not support runtime MCP attachments");
60
66
  }
@@ -111,9 +117,44 @@ export function createCliBridgeProvider(options) {
111
117
  if (id.length === 0 || id.trim() !== id) {
112
118
  throw new Error("cli-bridge environment id must be non-empty and have no outer whitespace");
113
119
  }
120
+ const operation = AgentEnvironmentGetOptionsSchema.parse(getOptions ?? {});
121
+ operation.signal?.throwIfAborted();
122
+ const accepted = operation.contextTransfer;
123
+ if (accepted !== undefined) {
124
+ if (accepted.profile === undefined)
125
+ throw new Error("CLI Bridge attachment requires the accepted exact profile");
126
+ if (accepted.workspace?.snapshot !== undefined)
127
+ throw new Error("CLI Bridge provider does not support portable workspace snapshots");
128
+ const destination = accepted.plan.destination;
129
+ if (destination.provider !== name || (destination.environmentId !== undefined && destination.environmentId !== id)) {
130
+ throw new Error("CLI Bridge attachment targets another provider or environment");
131
+ }
132
+ const backend = destination.runner;
133
+ const environmentInput = { profile: snapshotAgentProfile(accepted.profile), backend,
134
+ ...(accepted.workspace === undefined ? {} : { workspace: accepted.workspace }),
135
+ ...(accepted.resources === undefined ? {} : { resources: accepted.resources }),
136
+ idempotencyKey: accepted.operationId };
137
+ const model = resolveBridgeModel({ ...options, defaultModel: undefined }, environmentInput, { model: destination.model ?? accepted.profile.model?.default ?? backend }, accepted.profile);
138
+ const attachedOptions = { ...options, defaultModel: model };
139
+ const capabilities = await resolveCapabilities(backend, model, true, true);
140
+ if (capabilities.contextTransfer?.attach !== true)
141
+ throw new Error("CLI Bridge context transfer attachment is unsupported by the selected deployment");
142
+ const transfer = await lookupCliBridgeContext(options, accepted, operation.signal);
143
+ if (transfer === undefined || (transfer.status !== "accepted" && transfer.status !== "replayed") || transfer.environmentId !== id) {
144
+ throw new Error("CLI Bridge attachment requires an available exact transfer receipt");
145
+ }
146
+ operation.signal?.throwIfAborted();
147
+ return createCliBridgeEnvironment({
148
+ options: attachedOptions, providerName: name, environmentId: id, environmentInput,
149
+ allowDispatch: true, cancelRunsOnDestroy: false, capabilities,
150
+ selectedBackend: backend, selectedModel: model,
151
+ acceptedContextTransfer: accepted,
152
+ contextTransferReceipt: deepFreeze(ContextTransferReceiptSchema.parse(transfer)),
153
+ });
154
+ }
114
155
  const ownedRoute = optionalCliBridgeEnvironmentRoute(id);
115
156
  const transfer = ownedRoute === undefined
116
- ? await lookupCliBridgeContextByEnvironment(options, id, getOptions?.signal)
157
+ ? await lookupCliBridgeContextByEnvironment(options, id, operation.signal)
117
158
  : undefined;
118
159
  const backend = ownedRoute?.backend ?? transfer?.destination.runner;
119
160
  const destinationModel = transfer?.destination.model;
@@ -204,6 +245,7 @@ export function defaultCliBridgeCapabilities(harness) {
204
245
  freshSession: true,
205
246
  requestIdempotency: true,
206
247
  lookup: true,
248
+ attach: true,
207
249
  },
208
250
  ...(harness === "pi"
209
251
  ? {
@@ -1,6 +1,9 @@
1
1
  import type { AgentEnvironment, AgentEnvironmentCapabilities, AgentEnvironmentCreation, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { ContextTransferRequest, ContextTransferReceipt } from "@tangle-network/agent-interface";
2
3
  import type { CliBridgeProviderOptions } from "./provider-options.js";
3
4
  export interface CreateCliBridgeEnvironmentArgs {
5
+ readonly acceptedContextTransfer?: ContextTransferRequest;
6
+ readonly contextTransferReceipt?: ContextTransferReceipt;
4
7
  readonly options: CliBridgeProviderOptions;
5
8
  readonly providerName: string;
6
9
  readonly environmentInput: CreateAgentEnvironmentInput;
@@ -41,11 +41,11 @@ export function createCliBridgeEnvironment(args) {
41
41
  throw new Error("cli-bridge environment is destroyed");
42
42
  assertCliBridgeTurnAllowed(args.allowDispatch, "environment.stream()");
43
43
  if (args.selectedModel !== undefined &&
44
- args.capabilities.retainedControl !== undefined) {
44
+ (args.capabilities.retainedControl !== undefined || args.acceptedContextTransfer !== undefined)) {
45
45
  assertCliBridgeTurnModel(options, environmentInput, turn, args.selectedModel);
46
46
  }
47
47
  assertCliBridgeRequestedInteractions(args.capabilities, turn.interactions);
48
- const prepared = prepareCliBridgeRun(options, environmentInput, turn, providerName, environmentId, useNativeInteractions);
48
+ const prepared = prepareCliBridgeRun(options, environmentInput, turn, providerName, environmentId, useNativeInteractions, args.acceptedContextTransfer);
49
49
  // A streamed run reports usage across its events, so the totals are summed
50
50
  // here and recorded as one execution's measurement.
51
51
  let streamedUsage;
@@ -60,6 +60,7 @@ export function createCliBridgeEnvironment(args) {
60
60
  provider: providerName,
61
61
  ...(args.creation === undefined ? {} : { creation: args.creation }),
62
62
  capabilities: args.capabilities,
63
+ ...(args.contextTransferReceipt === undefined ? {} : { contextTransferReceipt: args.contextTransferReceipt }),
63
64
  ...(environmentInput.name ? { name: environmentInput.name } : {}),
64
65
  status: async (statusOptions) => {
65
66
  statusOptions?.signal?.throwIfAborted();
@@ -71,11 +72,11 @@ export function createCliBridgeEnvironment(args) {
71
72
  throw new Error("cli-bridge environment is destroyed");
72
73
  assertCliBridgeTurnAllowed(args.allowDispatch, "environment.dispatch()");
73
74
  if (args.selectedModel !== undefined &&
74
- args.capabilities.retainedControl !== undefined) {
75
+ (args.capabilities.retainedControl !== undefined || args.acceptedContextTransfer !== undefined)) {
75
76
  assertCliBridgeTurnModel(options, environmentInput, turn, args.selectedModel);
76
77
  }
77
78
  assertCliBridgeRequestedInteractions(args.capabilities, turn.interactions);
78
- const prepared = prepareCliBridgeRun(options, environmentInput, turn, providerName, environmentId, useNativeInteractions);
79
+ const prepared = prepareCliBridgeRun(options, environmentInput, turn, providerName, environmentId, useNativeInteractions, args.acceptedContextTransfer);
79
80
  return dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions, nativeSessions, useNativeInteractions);
80
81
  },
81
82
  session(id, sessionOptions) {
@@ -99,6 +100,7 @@ export function createCliBridgeEnvironment(args) {
99
100
  interactionsEnabled: respondToInteraction !== undefined,
100
101
  useNativeInteractions,
101
102
  selectedModel: args.selectedModel,
103
+ acceptedContextTransfer: args.acceptedContextTransfer,
102
104
  });
103
105
  },
104
106
  ...(respondToInteraction ? { respondToInteraction } : {}),
@@ -243,14 +245,14 @@ function createCliBridgeSession(args) {
243
245
  throw new Error(`cli-bridge session "${args.id}" cannot prompt session "${input.sessionId}"`);
244
246
  }
245
247
  if (args.selectedModel !== undefined &&
246
- args.capabilities.retainedControl !== undefined) {
248
+ (args.capabilities.retainedControl !== undefined || args.acceptedContextTransfer !== undefined)) {
247
249
  assertCliBridgeTurnModel(args.options, args.environmentInput, input, args.selectedModel);
248
250
  }
249
251
  assertCliBridgeRequestedInteractions(args.capabilities, input.interactions);
250
252
  const prepared = prepareCliBridgeRun(args.options, args.environmentInput, {
251
253
  ...input,
252
254
  sessionId: args.id,
253
- }, args.providerName, args.environmentId, args.useNativeInteractions);
255
+ }, args.providerName, args.environmentId, args.useNativeInteractions, args.acceptedContextTransfer);
254
256
  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);
255
257
  args.usageLog.record(prepared.run.executionId, result.usage);
256
258
  return { ...result, sessionId: args.id };
@@ -1,4 +1,4 @@
1
- import { type AgentExactRunControlRef, type AgentRunControlRef, type Sha256Digest } from "@tangle-network/agent-interface";
1
+ import { type ContextTransferRequest, 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
3
  import type { ContextTransferReceipt } from "@tangle-network/agent-interface";
4
4
  import type { CliBridgeProviderOptions } from "./provider-options.js";
@@ -37,7 +37,7 @@ export declare function runFromControlRef(controlRef: AgentExactRunControlRef):
37
37
  export declare function assertRunMatchesControlRef(run: CliBridgeRun, controlRef: AgentExactRunControlRef): void;
38
38
  /** Advance one session object to the exact run returned by native continuation. */
39
39
  export declare function advanceCliBridgeRun(run: CliBridgeRun, controlRef: AgentExactRunControlRef, providerName: string, runs: Map<string, CliBridgeRun>): void;
40
- export declare function prepareCliBridgeRun(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, originalTurn: AgentTurnInput, providerName: string, environmentId: string, requireStableIdentity?: boolean): PreparedCliBridgeRun;
40
+ export declare function prepareCliBridgeRun(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, originalTurn: AgentTurnInput, providerName: string, environmentId: string, requireStableIdentity?: boolean, acceptedContextTransfer?: ContextTransferRequest): PreparedCliBridgeRun;
41
41
  export declare function bindCliBridgeRun(prepared: CliBridgeRun, runs: Map<string, CliBridgeRun>): CliBridgeRun | undefined;
42
42
  export declare function restoreCliBridgeRun(run: CliBridgeRun, previous: CliBridgeRun | undefined, runs: Map<string, CliBridgeRun>): void;
43
43
  export declare function bindCliBridgeSession(run: CliBridgeRun, sessions: Map<string, CliBridgeSessionState>): CliBridgeRun | undefined;
@@ -1,4 +1,4 @@
1
- import { AgentExactRunControlRefSchema, AgentTurnInputSchema, } from "@tangle-network/agent-interface";
1
+ import { AgentExactRunControlRefSchema, AgentTurnInputSchema, canonicalAgentProfileDigest, } from "@tangle-network/agent-interface";
2
2
  import { assertCliBridgeRunId, cliBridgeRunId, toChatCompletionsBody, } from "./wire.js";
3
3
  export function exactControlRefForRun(run, providerName) {
4
4
  if (!run.sessionId || !run.requestDigest) {
@@ -70,8 +70,12 @@ export function advanceCliBridgeRun(run, controlRef, providerName, runs) {
70
70
  run.controlRef = Object.freeze(AgentExactRunControlRefSchema.parse(controlRef));
71
71
  runs.set(run.id, run);
72
72
  }
73
- export function prepareCliBridgeRun(options, environmentInput, originalTurn, providerName, environmentId, requireStableIdentity = false) {
73
+ export function prepareCliBridgeRun(options, environmentInput, originalTurn, providerName, environmentId, requireStableIdentity = false, acceptedContextTransfer) {
74
74
  const validatedTurn = AgentTurnInputSchema.parse(originalTurn);
75
+ if (acceptedContextTransfer !== undefined && (validatedTurn.contextTransfer?.requestDigest !== acceptedContextTransfer.requestDigest ||
76
+ (validatedTurn.profile !== undefined && canonicalAgentProfileDigest(validatedTurn.profile) !== acceptedContextTransfer.plan.destination.profileDigest))) {
77
+ throw new Error("CLI Bridge turn conflicts with its accepted context transfer");
78
+ }
75
79
  if (requireStableIdentity &&
76
80
  (validatedTurn.turnId === undefined || validatedTurn.executionId === undefined)) {
77
81
  throw new Error("native cli-bridge turns require stable turnId and executionId");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "1.1.3",
3
+ "version": "2.0.0",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,10 +64,11 @@
64
64
  "LICENSE"
65
65
  ],
66
66
  "dependencies": {
67
- "@tangle-network/agent-interface": "^4.0.0",
67
+ "@tangle-network/agent-interface": "^5.0.0",
68
68
  "undici": "8.11.2"
69
69
  },
70
70
  "devDependencies": {
71
+ "@tangle-network/agent-provider-testkit": "0.12.0",
71
72
  "@types/node": "26.6.3",
72
73
  "typescript": "7.0.2",
73
74
  "vitest": "5.0.2"