@tangle-network/agent-provider-cli-bridge 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.d.ts +5 -27
- package/dist/index.js +38 -998
- package/dist/provider-options.d.ts +26 -0
- package/dist/provider-options.js +10 -0
- package/dist/retained-control.d.ts +11 -0
- package/dist/retained-control.js +170 -0
- package/dist/retained-environment.d.ts +11 -0
- package/dist/retained-environment.js +189 -0
- package/dist/retained-event-cursor.d.ts +18 -0
- package/dist/retained-event-cursor.js +68 -0
- package/dist/retained-execution.d.ts +12 -0
- package/dist/retained-execution.js +304 -0
- package/dist/retained-run-state.d.ts +38 -0
- package/dist/retained-run-state.js +97 -0
- package/dist/retained-stream.d.ts +8 -0
- package/dist/retained-stream.js +276 -0
- package/dist/transport.d.ts +23 -0
- package/dist/transport.js +32 -0
- package/dist/wire.d.ts +27 -0
- package/dist/wire.js +243 -0
- package/package.json +21 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentEnvironmentCapabilities } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
export interface CliBridgeProviderOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
bearerToken?: string;
|
|
5
|
+
defaultModel?: string;
|
|
6
|
+
defaultMode?: "byob" | "hosted-safe" | "hosted-sandboxed";
|
|
7
|
+
defaultExecution?: {
|
|
8
|
+
kind: "host";
|
|
9
|
+
} | {
|
|
10
|
+
kind: "sandbox";
|
|
11
|
+
repoUrl?: string;
|
|
12
|
+
gitRef?: string;
|
|
13
|
+
capability?: string;
|
|
14
|
+
ttlSeconds?: number;
|
|
15
|
+
};
|
|
16
|
+
/** Maximum wait for response headers. Defaults to no timeout. */
|
|
17
|
+
headersTimeoutMs?: number;
|
|
18
|
+
/** Maximum idle time between response body chunks. Defaults to no timeout. */
|
|
19
|
+
bodyTimeoutMs?: number;
|
|
20
|
+
/** Maximum wait for cli-bridge to confirm cancellation. Defaults to 30 seconds. */
|
|
21
|
+
cancelWaitMs?: number;
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
name?: string;
|
|
24
|
+
capabilities?: AgentEnvironmentCapabilities;
|
|
25
|
+
}
|
|
26
|
+
export declare function assertCliBridgeProviderOptions(options: CliBridgeProviderOptions): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function assertCliBridgeProviderOptions(options) {
|
|
2
|
+
assertTimeout(options.headersTimeoutMs, "headersTimeoutMs");
|
|
3
|
+
assertTimeout(options.bodyTimeoutMs, "bodyTimeoutMs");
|
|
4
|
+
assertTimeout(options.cancelWaitMs, "cancelWaitMs");
|
|
5
|
+
}
|
|
6
|
+
function assertTimeout(value, name) {
|
|
7
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
|
8
|
+
throw new Error(`createCliBridgeProvider ${name} must be a non-negative integer`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentRunCancellationAcknowledgement, type AgentRunCancellationRequest } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { AgentSessionStatus } from "@tangle-network/agent-interface/environment-provider";
|
|
3
|
+
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
4
|
+
import type { CliBridgeRun, CliBridgeRunSnapshot } from "./retained-run-state.js";
|
|
5
|
+
import type { CliBridgeResponse, CliBridgeTransport } from "./transport.js";
|
|
6
|
+
export declare function captureCliBridgeRunIdentity(response: CliBridgeResponse, run: CliBridgeRun, required: boolean): void;
|
|
7
|
+
export declare function detachCliBridgeReader(body: AsyncIterable<Uint8Array>): Promise<void>;
|
|
8
|
+
export declare function cancelCliBridgeRun(options: CliBridgeProviderOptions, transport: CliBridgeTransport, run: CliBridgeRun, signal?: AbortSignal): Promise<CliBridgeRunSnapshot>;
|
|
9
|
+
export declare function cancelExactCliBridgeRun(options: CliBridgeProviderOptions, transport: CliBridgeTransport, run: CliBridgeRun, request: AgentRunCancellationRequest, signal?: AbortSignal): Promise<AgentRunCancellationAcknowledgement>;
|
|
10
|
+
export declare function getCliBridgeRun(options: CliBridgeProviderOptions, transport: CliBridgeTransport, run: CliBridgeRun, waitMs?: number, signal?: AbortSignal): Promise<CliBridgeRunSnapshot | null>;
|
|
11
|
+
export declare function agentSessionStatusFromRun(snapshot: CliBridgeRunSnapshot): AgentSessionStatus;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { AgentExactRunControlRefSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, agentRunCancellationAcknowledgementMatchesRequest, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { requestHeaders, trimSlash } from "./transport.js";
|
|
3
|
+
import { cancelSnapshot, runSnapshot } from "./wire.js";
|
|
4
|
+
export function captureCliBridgeRunIdentity(response, run, required) {
|
|
5
|
+
const responseRunId = response.headers.get("x-run-id");
|
|
6
|
+
const requestDigestHeader = response.headers.get("x-run-request-digest");
|
|
7
|
+
const requestDigest = requestDigestHeader === null
|
|
8
|
+
? null
|
|
9
|
+
: AgentExactRunControlRefSchema.shape.requestDigest.parse(requestDigestHeader);
|
|
10
|
+
if (responseRunId !== null && responseRunId !== run.id) {
|
|
11
|
+
throw new Error(`cli-bridge accepted run "${responseRunId}" for requested run "${run.id}"`);
|
|
12
|
+
}
|
|
13
|
+
if ((required || run.requestDigest !== undefined) && responseRunId === null) {
|
|
14
|
+
throw new Error("cli-bridge response omitted X-Run-Id");
|
|
15
|
+
}
|
|
16
|
+
if (required && requestDigest === null) {
|
|
17
|
+
throw new Error("cli-bridge dispatch response omitted X-Run-Request-Digest");
|
|
18
|
+
}
|
|
19
|
+
if (requestDigest !== null &&
|
|
20
|
+
run.requestDigest !== undefined &&
|
|
21
|
+
requestDigest !== run.requestDigest) {
|
|
22
|
+
throw new Error(`cli-bridge changed request digest for run "${run.id}" from "${run.requestDigest}" to "${requestDigest}"`);
|
|
23
|
+
}
|
|
24
|
+
if (run.requestDigest !== undefined && requestDigest === null) {
|
|
25
|
+
throw new Error(`cli-bridge replay response omitted X-Run-Request-Digest for run "${run.id}"`);
|
|
26
|
+
}
|
|
27
|
+
if (requestDigest !== null)
|
|
28
|
+
run.requestDigest = requestDigest;
|
|
29
|
+
}
|
|
30
|
+
export async function detachCliBridgeReader(body) {
|
|
31
|
+
const cancellable = body;
|
|
32
|
+
if (cancellable.cancel) {
|
|
33
|
+
await cancellable.cancel();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const iterator = body[Symbol.asyncIterator]();
|
|
37
|
+
if (!iterator.return) {
|
|
38
|
+
throw new Error("cli-bridge response body cannot detach its reader");
|
|
39
|
+
}
|
|
40
|
+
await iterator.return();
|
|
41
|
+
}
|
|
42
|
+
export async function cancelCliBridgeRun(options, transport, run, signal) {
|
|
43
|
+
if (!run.cancellation) {
|
|
44
|
+
const cancellation = performCliBridgeCancellation(options, transport, run);
|
|
45
|
+
run.cancellation = cancellation;
|
|
46
|
+
void cancellation.then(() => clearCancellation(run, cancellation), () => clearCancellation(run, cancellation));
|
|
47
|
+
}
|
|
48
|
+
return waitForOperation(run.cancellation, signal);
|
|
49
|
+
}
|
|
50
|
+
async function performCliBridgeCancellation(options, transport, run) {
|
|
51
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: requestHeaders(options),
|
|
54
|
+
body: "{}",
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`cli-bridge cancel ${response.status}: ${await response.text()}`);
|
|
58
|
+
}
|
|
59
|
+
let snapshot = cancelSnapshot(await response.text());
|
|
60
|
+
assertCliBridgeRunSnapshotIdentity(snapshot, run.id);
|
|
61
|
+
const waitBudgetMs = options.cancelWaitMs ?? 30_000;
|
|
62
|
+
const deadline = Date.now() + waitBudgetMs;
|
|
63
|
+
while (!snapshot.terminal) {
|
|
64
|
+
const remainingMs = deadline - Date.now();
|
|
65
|
+
if (remainingMs <= 0)
|
|
66
|
+
break;
|
|
67
|
+
snapshot = await getCliBridgeRun(options, transport, run, Math.min(remainingMs, 30_000));
|
|
68
|
+
if (snapshot === null) {
|
|
69
|
+
throw new Error(`cli-bridge lost run "${run.id}" before confirming cancellation`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!snapshot?.terminal) {
|
|
73
|
+
throw new Error(`cli-bridge run "${run.id}" did not confirm terminal cancellation`);
|
|
74
|
+
}
|
|
75
|
+
for (const reader of run.readers) {
|
|
76
|
+
reader.abort(new DOMException(`cli-bridge run ended ${snapshot.status}`, "AbortError"));
|
|
77
|
+
}
|
|
78
|
+
return snapshot;
|
|
79
|
+
}
|
|
80
|
+
function clearCancellation(run, cancellation) {
|
|
81
|
+
if (run.cancellation === cancellation)
|
|
82
|
+
run.cancellation = undefined;
|
|
83
|
+
}
|
|
84
|
+
function waitForOperation(operation, signal) {
|
|
85
|
+
if (!signal)
|
|
86
|
+
return operation;
|
|
87
|
+
signal.throwIfAborted();
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const abort = () => reject(signal.reason);
|
|
90
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
91
|
+
void operation.then((value) => {
|
|
92
|
+
signal.removeEventListener("abort", abort);
|
|
93
|
+
resolve(value);
|
|
94
|
+
}, (error) => {
|
|
95
|
+
signal.removeEventListener("abort", abort);
|
|
96
|
+
reject(error);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export async function cancelExactCliBridgeRun(options, transport, run, request, signal) {
|
|
101
|
+
const exactRequest = AgentRunCancellationRequestSchema.parse(request);
|
|
102
|
+
if (!run.controlRef || !sameControlRef(run.controlRef, exactRequest.run)) {
|
|
103
|
+
throw new Error("cli-bridge cancellation targets another retained run");
|
|
104
|
+
}
|
|
105
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: requestHeaders(options),
|
|
108
|
+
body: JSON.stringify(exactRequest),
|
|
109
|
+
...(signal ? { signal } : {}),
|
|
110
|
+
});
|
|
111
|
+
const responseText = await response.text();
|
|
112
|
+
if (!response.ok && response.status !== 409) {
|
|
113
|
+
throw new Error(`cli-bridge cancel ${response.status}: ${responseText}`);
|
|
114
|
+
}
|
|
115
|
+
let acknowledgement;
|
|
116
|
+
try {
|
|
117
|
+
acknowledgement = AgentRunCancellationAcknowledgementSchema.parse(JSON.parse(responseText));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw new Error("cli-bridge returned an invalid exact cancellation acknowledgement", {
|
|
121
|
+
cause: error,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (!agentRunCancellationAcknowledgementMatchesRequest(exactRequest, acknowledgement)) {
|
|
125
|
+
throw new Error("cli-bridge returned a cancellation acknowledgement for another request");
|
|
126
|
+
}
|
|
127
|
+
return acknowledgement;
|
|
128
|
+
}
|
|
129
|
+
function sameControlRef(left, right) {
|
|
130
|
+
return left.runId === right.runId &&
|
|
131
|
+
left.provider === right.provider &&
|
|
132
|
+
left.environmentId === right.environmentId &&
|
|
133
|
+
left.sessionId === right.sessionId &&
|
|
134
|
+
left.executionId === right.executionId &&
|
|
135
|
+
left.requestDigest === right.requestDigest;
|
|
136
|
+
}
|
|
137
|
+
export async function getCliBridgeRun(options, transport, run, waitMs, signal) {
|
|
138
|
+
const query = waitMs === undefined ? "" : `?wait_ms=${waitMs}`;
|
|
139
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}${query}`, {
|
|
140
|
+
method: "GET",
|
|
141
|
+
headers: requestHeaders(options),
|
|
142
|
+
...(signal ? { signal } : {}),
|
|
143
|
+
});
|
|
144
|
+
if (response.status === 404)
|
|
145
|
+
return null;
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw new Error(`cli-bridge run status ${response.status}: ${await response.text()}`);
|
|
148
|
+
}
|
|
149
|
+
const snapshot = runSnapshot(await response.text());
|
|
150
|
+
assertCliBridgeRunSnapshotIdentity(snapshot, run.id, run.requestDigest);
|
|
151
|
+
return snapshot;
|
|
152
|
+
}
|
|
153
|
+
function assertCliBridgeRunSnapshotIdentity(snapshot, expectedRunId, expectedRequestDigest) {
|
|
154
|
+
if (snapshot.id !== expectedRunId) {
|
|
155
|
+
throw new Error(`cli-bridge returned run "${snapshot.id}" for requested run "${expectedRunId}"`);
|
|
156
|
+
}
|
|
157
|
+
if (expectedRequestDigest !== undefined &&
|
|
158
|
+
snapshot.requestDigest !== expectedRequestDigest) {
|
|
159
|
+
throw new Error(`cli-bridge returned request digest "${snapshot.requestDigest}" for run "${expectedRunId}"`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function agentSessionStatusFromRun(snapshot) {
|
|
163
|
+
if (snapshot.status === "done")
|
|
164
|
+
return "completed";
|
|
165
|
+
if (snapshot.status === "error")
|
|
166
|
+
return "failed";
|
|
167
|
+
if (snapshot.status === "cancelled")
|
|
168
|
+
return "cancelled";
|
|
169
|
+
return "running";
|
|
170
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentEnvironment, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
3
|
+
export interface CreateCliBridgeEnvironmentArgs {
|
|
4
|
+
readonly options: CliBridgeProviderOptions;
|
|
5
|
+
readonly providerName: string;
|
|
6
|
+
readonly environmentInput: CreateAgentEnvironmentInput;
|
|
7
|
+
readonly environmentId: string;
|
|
8
|
+
readonly allowDispatch: boolean;
|
|
9
|
+
readonly cancelRunsOnDestroy: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function createCliBridgeEnvironment(args: CreateCliBridgeEnvironmentArgs): AgentEnvironment;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { agentSessionStatusFromRun, cancelCliBridgeRun, cancelExactCliBridgeRun, getCliBridgeRun, } from "./retained-control.js";
|
|
2
|
+
import { collectCliBridgeTurnResult, dispatchCliBridgeTurn, streamCliBridgeSessionEvents, streamTrackedCliBridgeTurn, } from "./retained-execution.js";
|
|
3
|
+
import { assertRunMatchesControlRef, exactControlRefForSession, prepareCliBridgeRun, runFromControlRef, } from "./retained-run-state.js";
|
|
4
|
+
import { createCliBridgeTransport } from "./transport.js";
|
|
5
|
+
export function createCliBridgeEnvironment(args) {
|
|
6
|
+
const { options, providerName, environmentInput, environmentId } = args;
|
|
7
|
+
const transport = createCliBridgeTransport(options);
|
|
8
|
+
const runs = new Map();
|
|
9
|
+
const sessions = new Map();
|
|
10
|
+
const readers = new Set();
|
|
11
|
+
let destroyed = false;
|
|
12
|
+
let closePromise;
|
|
13
|
+
const stream = async function* (turn) {
|
|
14
|
+
if (destroyed)
|
|
15
|
+
throw new Error("cli-bridge environment is destroyed");
|
|
16
|
+
if (!args.allowDispatch) {
|
|
17
|
+
throw new Error("a reconstructed cli-bridge environment can only control an existing run");
|
|
18
|
+
}
|
|
19
|
+
const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, false);
|
|
20
|
+
yield* streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers);
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
id: environmentId,
|
|
24
|
+
provider: providerName,
|
|
25
|
+
...(environmentInput.name ? { name: environmentInput.name } : {}),
|
|
26
|
+
status: async (statusOptions) => {
|
|
27
|
+
statusOptions?.signal?.throwIfAborted();
|
|
28
|
+
return destroyed ? "stopped" : "running";
|
|
29
|
+
},
|
|
30
|
+
stream,
|
|
31
|
+
async dispatch(turn) {
|
|
32
|
+
if (destroyed)
|
|
33
|
+
throw new Error("cli-bridge environment is destroyed");
|
|
34
|
+
if (!args.allowDispatch) {
|
|
35
|
+
throw new Error("a reconstructed cli-bridge environment cannot dispatch new work");
|
|
36
|
+
}
|
|
37
|
+
const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, true);
|
|
38
|
+
return dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions);
|
|
39
|
+
},
|
|
40
|
+
session(id, sessionOptions) {
|
|
41
|
+
sessionOptions?.signal?.throwIfAborted();
|
|
42
|
+
return createCliBridgeSession({
|
|
43
|
+
id,
|
|
44
|
+
providerName,
|
|
45
|
+
options,
|
|
46
|
+
environmentInput,
|
|
47
|
+
environmentId,
|
|
48
|
+
transport,
|
|
49
|
+
runs,
|
|
50
|
+
sessions,
|
|
51
|
+
readers,
|
|
52
|
+
allowPrompt: args.allowDispatch,
|
|
53
|
+
requestedControlRef: sessionOptions?.controlRef,
|
|
54
|
+
isDestroyed: () => destroyed,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
placement: async (placementOptions) => {
|
|
58
|
+
placementOptions?.signal?.throwIfAborted();
|
|
59
|
+
return {
|
|
60
|
+
kind: options.defaultExecution?.kind === "sandbox" ? "sandbox" : "local",
|
|
61
|
+
providerMetadata: { baseUrl: options.baseUrl },
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
async destroy(destroyOptions) {
|
|
65
|
+
if (closePromise)
|
|
66
|
+
return closePromise;
|
|
67
|
+
destroyOptions?.signal?.throwIfAborted();
|
|
68
|
+
destroyed = true;
|
|
69
|
+
let cleanupConfirmed = false;
|
|
70
|
+
const attempt = (async () => {
|
|
71
|
+
if (args.cancelRunsOnDestroy) {
|
|
72
|
+
const cancellations = await Promise.allSettled(Array.from(runs.values()).map(async (run) => {
|
|
73
|
+
const snapshot = await cancelCliBridgeRun(options, transport, run, destroyOptions?.signal);
|
|
74
|
+
if (runs.get(run.id) === run)
|
|
75
|
+
runs.delete(run.id);
|
|
76
|
+
return snapshot;
|
|
77
|
+
}));
|
|
78
|
+
const failures = cancellations.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
79
|
+
if (failures.length === 1)
|
|
80
|
+
throw failures[0];
|
|
81
|
+
if (failures.length > 1) {
|
|
82
|
+
throw new AggregateError(failures, "cli-bridge environment cancellation failed");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
cleanupConfirmed = true;
|
|
86
|
+
for (const reader of readers) {
|
|
87
|
+
reader.abort(new DOMException("cli-bridge environment was destroyed", "AbortError"));
|
|
88
|
+
}
|
|
89
|
+
await transport.close();
|
|
90
|
+
sessions.clear();
|
|
91
|
+
runs.clear();
|
|
92
|
+
})();
|
|
93
|
+
closePromise = attempt;
|
|
94
|
+
try {
|
|
95
|
+
await attempt;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
closePromise = undefined;
|
|
99
|
+
if (!cleanupConfirmed)
|
|
100
|
+
destroyed = false;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function createCliBridgeSession(args) {
|
|
107
|
+
const requestedControlRef = args.requestedControlRef === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: exactControlRefForSession(args.requestedControlRef, args.providerName, args.environmentId, args.id);
|
|
110
|
+
if (requestedControlRef) {
|
|
111
|
+
const current = args.sessions.get(args.id)?.current;
|
|
112
|
+
if (current) {
|
|
113
|
+
assertRunMatchesControlRef(current, requestedControlRef);
|
|
114
|
+
const indexed = args.runs.get(requestedControlRef.runId);
|
|
115
|
+
if (indexed)
|
|
116
|
+
assertRunMatchesControlRef(indexed, requestedControlRef);
|
|
117
|
+
else
|
|
118
|
+
args.runs.set(current.id, current);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const existing = args.runs.get(requestedControlRef.runId);
|
|
122
|
+
if (existing) {
|
|
123
|
+
assertRunMatchesControlRef(existing, requestedControlRef);
|
|
124
|
+
args.sessions.set(args.id, { id: args.id, current: existing });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const reconstructed = runFromControlRef(requestedControlRef);
|
|
128
|
+
args.runs.set(reconstructed.id, reconstructed);
|
|
129
|
+
args.sessions.set(args.id, { id: args.id, current: reconstructed });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const currentRun = () => {
|
|
134
|
+
if (args.isDestroyed())
|
|
135
|
+
throw new Error("cli-bridge environment is destroyed");
|
|
136
|
+
return args.sessions.get(args.id)?.current;
|
|
137
|
+
};
|
|
138
|
+
const requireCurrentRun = () => {
|
|
139
|
+
const run = currentRun();
|
|
140
|
+
if (!run)
|
|
141
|
+
throw new Error(`cli-bridge session "${args.id}" has no run`);
|
|
142
|
+
return run;
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
id: args.id,
|
|
146
|
+
get controlRef() {
|
|
147
|
+
return currentRun()?.controlRef;
|
|
148
|
+
},
|
|
149
|
+
async status(statusOptions) {
|
|
150
|
+
const run = currentRun();
|
|
151
|
+
if (!run)
|
|
152
|
+
return null;
|
|
153
|
+
const snapshot = await getCliBridgeRun(args.options, args.transport, run, undefined, statusOptions?.signal);
|
|
154
|
+
if (!snapshot)
|
|
155
|
+
return null;
|
|
156
|
+
return agentSessionStatusFromRun(snapshot);
|
|
157
|
+
},
|
|
158
|
+
async *events(eventOptions) {
|
|
159
|
+
const run = requireCurrentRun();
|
|
160
|
+
yield* streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, eventOptions);
|
|
161
|
+
},
|
|
162
|
+
async result(resultOptions) {
|
|
163
|
+
const run = requireCurrentRun();
|
|
164
|
+
return collectCliBridgeTurnResult(streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, { since: "0", signal: resultOptions?.signal }), run, args.options, args.transport, resultOptions?.signal);
|
|
165
|
+
},
|
|
166
|
+
async prompt(input) {
|
|
167
|
+
if (!args.allowPrompt) {
|
|
168
|
+
throw new Error("a reconstructed cli-bridge session cannot start another turn");
|
|
169
|
+
}
|
|
170
|
+
if (input.sessionId && input.sessionId !== args.id) {
|
|
171
|
+
throw new Error(`cli-bridge session "${args.id}" cannot prompt session "${input.sessionId}"`);
|
|
172
|
+
}
|
|
173
|
+
const prepared = prepareCliBridgeRun(args.options, args.environmentInput, {
|
|
174
|
+
...input,
|
|
175
|
+
sessionId: args.id,
|
|
176
|
+
}, args.environmentId, false);
|
|
177
|
+
const result = await collectCliBridgeTurnResult(streamTrackedCliBridgeTurn(args.options, args.environmentInput, prepared, args.transport, args.runs, args.sessions, args.readers), prepared.run, args.options, args.transport, input.signal);
|
|
178
|
+
return { ...result, sessionId: args.id };
|
|
179
|
+
},
|
|
180
|
+
async cancel(cancelOptions) {
|
|
181
|
+
const run = requireCurrentRun();
|
|
182
|
+
await cancelCliBridgeRun(args.options, args.transport, run, cancelOptions?.signal);
|
|
183
|
+
},
|
|
184
|
+
async cancelRun(request, cancelOptions) {
|
|
185
|
+
const run = requireCurrentRun();
|
|
186
|
+
return cancelExactCliBridgeRun(args.options, args.transport, run, request, cancelOptions?.signal);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
interface RetainedReplayAnchor {
|
|
3
|
+
readonly frameId: string;
|
|
4
|
+
readonly eventIndex: number;
|
|
5
|
+
}
|
|
6
|
+
export interface RetainedReplayRequest {
|
|
7
|
+
readonly serverCursor?: string;
|
|
8
|
+
readonly anchor?: RetainedReplayAnchor;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Translate one provider event cursor into the frame cursor understood by
|
|
12
|
+
* cli-bridge. A composite cursor replays its source frame, then filters the
|
|
13
|
+
* canonical events that the caller already committed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function retainedReplayRequest(cursor?: string): RetainedReplayRequest;
|
|
16
|
+
/** Add exact per-event identity to all canonical events from one SSE frame. */
|
|
17
|
+
export declare function retainedEventsWithIdentity(events: readonly AgentEnvironmentEvent[], frameId?: string, runId?: string, sessionId?: string, executionId?: string, replayAnchor?: RetainedReplayAnchor): Iterable<AgentEnvironmentEvent>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate one provider event cursor into the frame cursor understood by
|
|
3
|
+
* cli-bridge. A composite cursor replays its source frame, then filters the
|
|
4
|
+
* canonical events that the caller already committed.
|
|
5
|
+
*/
|
|
6
|
+
export function retainedReplayRequest(cursor) {
|
|
7
|
+
if (cursor === undefined)
|
|
8
|
+
return {};
|
|
9
|
+
if (isBaseTenInteger(cursor)) {
|
|
10
|
+
assertSafeInteger(cursor, "cli-bridge replay cursor");
|
|
11
|
+
return { serverCursor: cursor };
|
|
12
|
+
}
|
|
13
|
+
const match = /^(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/u.exec(cursor);
|
|
14
|
+
if (!match)
|
|
15
|
+
throw new Error("cli-bridge replay cursor is invalid");
|
|
16
|
+
const frameId = match[1];
|
|
17
|
+
const eventIndexText = match[2];
|
|
18
|
+
const frameSequence = assertSafeInteger(frameId, "cli-bridge replay frame");
|
|
19
|
+
const eventIndex = assertSafeInteger(eventIndexText, "cli-bridge replay event index");
|
|
20
|
+
if (frameSequence === 0) {
|
|
21
|
+
throw new Error("cli-bridge retained event frame must be positive");
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
serverCursor: String(frameSequence - 1),
|
|
25
|
+
anchor: { frameId, eventIndex },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Add exact per-event identity to all canonical events from one SSE frame. */
|
|
29
|
+
export function* retainedEventsWithIdentity(events, frameId, runId, sessionId, executionId, replayAnchor) {
|
|
30
|
+
if (frameId === undefined) {
|
|
31
|
+
yield* events;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const frameSequence = assertSafeInteger(frameId, "cli-bridge retained event id");
|
|
35
|
+
if (frameSequence === 0) {
|
|
36
|
+
throw new Error("cli-bridge retained event id must be positive");
|
|
37
|
+
}
|
|
38
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
39
|
+
if (replayAnchor?.frameId === frameId &&
|
|
40
|
+
index <= replayAnchor.eventIndex) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const event = events[index];
|
|
44
|
+
const cursor = `${frameId}:${index}`;
|
|
45
|
+
yield {
|
|
46
|
+
...event,
|
|
47
|
+
id: cursor,
|
|
48
|
+
data: {
|
|
49
|
+
...event.data,
|
|
50
|
+
cursor,
|
|
51
|
+
...(runId === undefined ? {} : { runId }),
|
|
52
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
53
|
+
...(executionId === undefined ? {} : { executionId }),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isBaseTenInteger(value) {
|
|
59
|
+
return /^(0|[1-9][0-9]*)$/u.test(value);
|
|
60
|
+
}
|
|
61
|
+
function assertSafeInteger(value, label) {
|
|
62
|
+
if (!isBaseTenInteger(value))
|
|
63
|
+
throw new Error(`${label} must be a base-10 integer`);
|
|
64
|
+
const parsed = Number(value);
|
|
65
|
+
if (!Number.isSafeInteger(parsed))
|
|
66
|
+
throw new Error(`${label} exceeds the supported range`);
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentEnvironmentEvent, AgentSessionRef, AgentTurnResult, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
3
|
+
import { type CliBridgeRun, type CliBridgeSessionState, type PreparedCliBridgeRun } from "./retained-run-state.js";
|
|
4
|
+
import type { CliBridgeTransport } from "./transport.js";
|
|
5
|
+
export declare function streamTrackedCliBridgeTurn(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, prepared: PreparedCliBridgeRun, transport: CliBridgeTransport, runs: Map<string, CliBridgeRun>, sessions: Map<string, CliBridgeSessionState>, readers: Set<AbortController>): AsyncIterable<AgentEnvironmentEvent>;
|
|
6
|
+
export declare function dispatchCliBridgeTurn(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, prepared: PreparedCliBridgeRun, transport: CliBridgeTransport, providerName: string, runs: Map<string, CliBridgeRun>, sessions: Map<string, CliBridgeSessionState>): Promise<AgentSessionRef>;
|
|
7
|
+
export declare function streamCliBridgeSessionEvents(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, run: CliBridgeRun, transport: CliBridgeTransport, runs: Map<string, CliBridgeRun>, readers: Set<AbortController>, eventOptions?: {
|
|
8
|
+
since?: string;
|
|
9
|
+
executionId?: string;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): AsyncIterable<AgentEnvironmentEvent>;
|
|
12
|
+
export declare function collectCliBridgeTurnResult(source: AsyncIterable<AgentEnvironmentEvent>, run: CliBridgeRun, options: CliBridgeProviderOptions, transport: CliBridgeTransport, signal?: AbortSignal): Promise<AgentTurnResult>;
|