@tangle-network/agent-provider-cli-bridge 0.9.1 → 0.9.3
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 +7 -2
- package/dist/capability-discovery.d.ts +1 -1
- package/dist/capability-discovery.js +7 -6
- package/dist/index.d.ts +8 -2
- package/dist/index.js +27 -12
- package/dist/interaction-response.js +2 -2
- package/dist/provider-options.d.ts +1 -0
- package/dist/provider-options.js +4 -0
- package/dist/retained-control.d.ts +12 -2
- package/dist/retained-control.js +157 -34
- package/dist/retained-environment.js +13 -10
- package/dist/retained-execution.d.ts +2 -0
- package/dist/retained-execution.js +38 -12
- package/dist/retained-native.d.ts +1 -1
- package/dist/retained-native.js +22 -56
- package/dist/retained-run-state.d.ts +3 -1
- package/dist/retained-run-state.js +27 -14
- package/dist/retained-stream.js +12 -10
- package/dist/transport.d.ts +3 -0
- package/dist/transport.js +80 -0
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +35 -8
- package/package.json +1 -1
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
|
|
73
|
-
|
|
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
|
|
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}: ${
|
|
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(
|
|
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
|
|
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.
|
|
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
|
|
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 (
|
|
42
|
-
options.capabilities !== undefined ||
|
|
43
|
-
selectedBackend !== "pi" ||
|
|
44
|
-
model === undefined) {
|
|
43
|
+
if (options.capabilities !== undefined)
|
|
45
44
|
return local;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
package/dist/provider-options.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/retained-control.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { AgentExactRunControlRefSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, agentRunCancellationAcknowledgementMatchesRequest, } from "@tangle-network/agent-interface";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
110
|
-
});
|
|
111
|
-
const responseText = await response
|
|
112
|
-
|
|
113
|
-
|
|
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(
|
|
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}: ${
|
|
236
|
+
throw new Error(`cli-bridge run status ${response.status}: ${responseText}`);
|
|
148
237
|
}
|
|
149
|
-
const snapshot = runSnapshot(
|
|
150
|
-
|
|
238
|
+
const snapshot = runSnapshot(responseText);
|
|
239
|
+
bindCliBridgeRunSnapshot(run, snapshot);
|
|
151
240
|
return snapshot;
|
|
152
241
|
}
|
|
153
|
-
function
|
|
154
|
-
if (snapshot.
|
|
155
|
-
throw new Error(
|
|
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
|
-
|
|
158
|
-
|
|
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,
|
|
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 (
|
|
34
|
-
|
|
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 (
|
|
64
|
-
|
|
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,
|
|
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.
|
|
237
|
-
|
|
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 };
|
|
@@ -12,5 +12,7 @@ export declare function streamCliBridgeSessionEvents(options: CliBridgeProviderO
|
|
|
12
12
|
signal?: AbortSignal;
|
|
13
13
|
}): AsyncIterable<AgentEnvironmentEvent>;
|
|
14
14
|
export declare function collectCliBridgeTurnResult(source: AsyncIterable<AgentEnvironmentEvent>, run: CliBridgeRun, options: CliBridgeProviderOptions, transport: CliBridgeTransport, signal?: AbortSignal): Promise<AgentTurnResult>;
|
|
15
|
+
/** Keep reasoning and tool updates out of the assistant's visible result text. */
|
|
16
|
+
export declare function visibleTextDelta(event: AgentEnvironmentEvent): string | undefined;
|
|
15
17
|
/** Sum the usage a run reported across its events into one total. */
|
|
16
18
|
export declare function addTokenUsage(current: TokenUsage | undefined, next: TokenUsage | undefined): TokenUsage | undefined;
|
|
@@ -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 =
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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.
|
|
@@ -246,8 +251,10 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
|
|
|
246
251
|
if (typeof finalText === "string") {
|
|
247
252
|
text = finalText;
|
|
248
253
|
}
|
|
249
|
-
else
|
|
250
|
-
|
|
254
|
+
else {
|
|
255
|
+
const delta = visibleTextDelta(event);
|
|
256
|
+
if (delta !== undefined)
|
|
257
|
+
text += delta;
|
|
251
258
|
}
|
|
252
259
|
usage = addTokenUsage(usage, event.usage);
|
|
253
260
|
const eventModel = event.data.model;
|
|
@@ -312,6 +319,25 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
|
|
|
312
319
|
events,
|
|
313
320
|
};
|
|
314
321
|
}
|
|
322
|
+
/** Keep reasoning and tool updates out of the assistant's visible result text. */
|
|
323
|
+
export function visibleTextDelta(event) {
|
|
324
|
+
if (event.normalized?.type === "message.part.updated") {
|
|
325
|
+
if (event.normalized.part.type === "text" &&
|
|
326
|
+
typeof event.normalized.delta === "string") {
|
|
327
|
+
return event.normalized.delta;
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const part = event.data.part;
|
|
332
|
+
if (typeof part === "object" && part !== null && !Array.isArray(part)) {
|
|
333
|
+
return "type" in part &&
|
|
334
|
+
part.type === "text" &&
|
|
335
|
+
typeof event.data.delta === "string"
|
|
336
|
+
? event.data.delta
|
|
337
|
+
: undefined;
|
|
338
|
+
}
|
|
339
|
+
return typeof event.data.delta === "string" ? event.data.delta : undefined;
|
|
340
|
+
}
|
|
315
341
|
/** Sum the usage a run reported across its events into one total. */
|
|
316
342
|
export function addTokenUsage(current, next) {
|
|
317
343
|
if (!next)
|
|
@@ -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
|
|
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. */
|
package/dist/retained-native.js
CHANGED
|
@@ -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
|
|
41
|
+
export function assertCliBridgeTurnModel(options, environmentInput, turn, selectedModel) {
|
|
42
42
|
if (selectedModel === undefined) {
|
|
43
|
-
throw new Error("cli-bridge
|
|
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
|
|
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
|
|
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
|
|
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
|
|
248
|
+
throw new CliBridgeRequestRejectedError(response.status, await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal));
|
|
292
249
|
}
|
|
293
|
-
return parseNativeSessionView(await response
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 =
|
|
76
|
-
const runId = cliBridgeRunId(environmentId,
|
|
77
|
-
const sessionId =
|
|
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
|
-
...
|
|
84
|
+
...validatedTurn,
|
|
80
85
|
turnId,
|
|
81
|
-
|
|
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
|
-
|
|
89
|
-
executionId
|
|
95
|
+
sessionId,
|
|
96
|
+
executionId,
|
|
90
97
|
turnId,
|
|
91
|
-
requestBody: JSON.stringify(toChatCompletionsBody(options, environmentInput, turn,
|
|
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
|
};
|
package/dist/retained-stream.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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));
|
package/dist/transport.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
308
|
-
turn
|
|
309
|
-
|
|
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(
|
|
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