@tangle-network/agent-provider-cli-bridge 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/capability-discovery.d.ts +15 -0
- package/dist/capability-discovery.js +164 -0
- package/dist/environment-identity.d.ts +13 -0
- package/dist/environment-identity.js +87 -0
- package/dist/index.js +83 -12
- package/dist/interaction-response.d.ts +15 -0
- package/dist/interaction-response.js +100 -0
- package/dist/retained-canonical-event.d.ts +18 -0
- package/dist/retained-canonical-event.js +135 -0
- package/dist/retained-control.js +3 -1
- package/dist/retained-environment.d.ts +2 -0
- package/dist/retained-environment.js +49 -7
- package/dist/retained-execution.d.ts +4 -3
- package/dist/retained-execution.js +79 -34
- package/dist/retained-native.d.ts +27 -0
- package/dist/retained-native.js +204 -0
- package/dist/retained-run-state.d.ts +1 -1
- package/dist/retained-run-state.js +4 -0
- package/dist/retained-stream.d.ts +1 -1
- package/dist/retained-stream.js +67 -1
- package/dist/wire.d.ts +8 -2
- package/dist/wire.js +112 -5
- package/package.json +15 -4
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AgentExactRunControlRefSchema, RequestedInteractionsSchema, canonicalCandidateDigest, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { CliBridgeRequestRejectedError } from "./retained-stream.js";
|
|
3
|
+
import { requestHeaders, trimSlash, } from "./transport.js";
|
|
4
|
+
import { inlineProfile, safeJson, resolveBridgeModel, toRetainedSessionBody, toRetainedTurnBody, } from "./wire.js";
|
|
5
|
+
/** Interactions are valid only for the native retained Pi contract. */
|
|
6
|
+
export function supportsCliBridgeNativeInteractions(capabilities, selectedBackend) {
|
|
7
|
+
const interactions = capabilities.interactions;
|
|
8
|
+
const retainedControl = capabilities.retainedControl;
|
|
9
|
+
return Boolean(selectedBackend === "pi" &&
|
|
10
|
+
piInteractionCapabilitiesMatch(interactions) &&
|
|
11
|
+
capabilities.streaming.live &&
|
|
12
|
+
capabilities.streaming.replay &&
|
|
13
|
+
capabilities.streaming.detach &&
|
|
14
|
+
capabilities.streaming.turnIdempotency &&
|
|
15
|
+
capabilities.sessions.continue &&
|
|
16
|
+
retainedControl?.exactRunIdentity &&
|
|
17
|
+
retainedControl.resultIdentity &&
|
|
18
|
+
retainedControl.eventIdentity &&
|
|
19
|
+
retainedControl.cancellationIdempotency);
|
|
20
|
+
}
|
|
21
|
+
/** Reject posture the selected environment did not advertise before transport use. */
|
|
22
|
+
export function assertCliBridgeRequestedInteractions(capabilities, requested) {
|
|
23
|
+
if (requested === undefined)
|
|
24
|
+
return;
|
|
25
|
+
const parsed = RequestedInteractionsSchema.parse(requested);
|
|
26
|
+
const advertised = new Set(capabilities.interactions?.kinds ?? []);
|
|
27
|
+
const unsupported = Object.keys(parsed).filter((kind) => !advertised.has(kind));
|
|
28
|
+
if (unsupported.length > 0) {
|
|
29
|
+
throw new Error(`cli-bridge environment does not advertise requested interaction kind(s): ${unsupported.join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Keep capability discovery and every native turn on one exact model route. */
|
|
33
|
+
export function assertCliBridgeNativeTurnModel(options, environmentInput, turn, selectedModel) {
|
|
34
|
+
if (selectedModel === undefined) {
|
|
35
|
+
throw new Error("cli-bridge native interactions require an environment model");
|
|
36
|
+
}
|
|
37
|
+
const requestedModel = resolveBridgeModel(options, environmentInput, turn, inlineProfile(environmentInput.profile));
|
|
38
|
+
if (requestedModel !== selectedModel) {
|
|
39
|
+
throw new Error(`cli-bridge native environment model is ${JSON.stringify(selectedModel)}; create another environment for ${JSON.stringify(requestedModel)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function piInteractionCapabilitiesMatch(interactions) {
|
|
43
|
+
return Boolean(interactions &&
|
|
44
|
+
interactions.kinds.includes("permission") &&
|
|
45
|
+
interactions.answerFieldTypes.includes("select") &&
|
|
46
|
+
interactions.responseScopes.includes("interaction") &&
|
|
47
|
+
interactions.secretAnswers === false &&
|
|
48
|
+
interactions.concurrentRequests === false &&
|
|
49
|
+
interactions.replay === true &&
|
|
50
|
+
interactions.responseIdempotency === true);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create or reuse the server's retained native session for one external id.
|
|
54
|
+
*
|
|
55
|
+
* The cache stores the promise so concurrent first turns cannot create two
|
|
56
|
+
* native sessions for the same id.
|
|
57
|
+
*/
|
|
58
|
+
export async function ensureCliBridgeNativeSession(options, environmentInput, turn, sessionId, transport, sessions, signal) {
|
|
59
|
+
const profile = inlineProfile(environmentInput.profile);
|
|
60
|
+
const model = resolveBridgeModel(options, environmentInput, turn, profile);
|
|
61
|
+
const existing = sessions.get(sessionId);
|
|
62
|
+
if (existing) {
|
|
63
|
+
const session = await existing;
|
|
64
|
+
assertNativeSessionModel(session, model);
|
|
65
|
+
return session;
|
|
66
|
+
}
|
|
67
|
+
const pending = createCliBridgeNativeSession(options, environmentInput, profile, sessionId, model, transport, signal);
|
|
68
|
+
sessions.set(sessionId, pending);
|
|
69
|
+
try {
|
|
70
|
+
const session = await pending;
|
|
71
|
+
assertNativeSessionModel(session, model);
|
|
72
|
+
return session;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (sessions.get(sessionId) === pending)
|
|
76
|
+
sessions.delete(sessionId);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function createCliBridgeNativeSession(options, environmentInput, profile, sessionId, model, transport, signal) {
|
|
81
|
+
const body = toRetainedSessionBody(options, environmentInput, profile, sessionId, model);
|
|
82
|
+
const createRequestDigest = canonicalCandidateDigest(body);
|
|
83
|
+
const url = `${trimSlash(options.baseUrl)}/v1/sessions`;
|
|
84
|
+
const init = {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: requestHeaders(options),
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
...(signal ? { signal } : {}),
|
|
89
|
+
};
|
|
90
|
+
const response = await transport.fetch(url, init);
|
|
91
|
+
if (response.status === 409) {
|
|
92
|
+
return recoverConflictingNativeSession(options, sessionId, model, createRequestDigest, transport, signal, response);
|
|
93
|
+
}
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new CliBridgeRequestRejectedError(response.status, await response.text());
|
|
96
|
+
}
|
|
97
|
+
return parseNativeSessionView(await response.text(), sessionId, model, createRequestDigest);
|
|
98
|
+
}
|
|
99
|
+
async function recoverConflictingNativeSession(options, sessionId, model, createRequestDigest, transport, signal, conflictResponse) {
|
|
100
|
+
await conflictResponse.text();
|
|
101
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: requestHeaders(options),
|
|
104
|
+
...(signal ? { signal } : {}),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new CliBridgeRequestRejectedError(response.status, `session ${JSON.stringify(sessionId)} could not be recovered: ${await response.text()}`);
|
|
108
|
+
}
|
|
109
|
+
return parseNativeSessionView(await response.text(), sessionId, model, createRequestDigest);
|
|
110
|
+
}
|
|
111
|
+
function parseNativeSessionView(value, expectedId, expectedModel, expectedCreateRequestDigest) {
|
|
112
|
+
const parsed = safeJson(value);
|
|
113
|
+
const id = parsed?.id;
|
|
114
|
+
const model = parsed?.model;
|
|
115
|
+
const digest = parsed?.create_request_digest;
|
|
116
|
+
const parsedDigest = AgentExactRunControlRefSchema.shape.requestDigest.safeParse(digest);
|
|
117
|
+
if (typeof id !== "string" ||
|
|
118
|
+
typeof model !== "string" ||
|
|
119
|
+
!parsedDigest.success) {
|
|
120
|
+
throw new Error("cli-bridge retained session returned an invalid session view");
|
|
121
|
+
}
|
|
122
|
+
if (id !== expectedId) {
|
|
123
|
+
throw new Error(`cli-bridge returned session ${JSON.stringify(id)} for requested session ${JSON.stringify(expectedId)}`);
|
|
124
|
+
}
|
|
125
|
+
if (model !== expectedModel) {
|
|
126
|
+
throw new Error(`cli-bridge retained session ${JSON.stringify(expectedId)} is bound to model ${JSON.stringify(model)}`);
|
|
127
|
+
}
|
|
128
|
+
if (parsedDigest.data !== expectedCreateRequestDigest) {
|
|
129
|
+
throw new Error(`cli-bridge retained session ${JSON.stringify(expectedId)} has a different creation request digest`);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
model,
|
|
134
|
+
createRequestDigest: parsedDigest.data,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function assertNativeSessionModel(session, expectedModel) {
|
|
138
|
+
if (session.model !== expectedModel) {
|
|
139
|
+
throw new Error(`cli-bridge retained session ${JSON.stringify(session.id)} cannot change model from ${JSON.stringify(session.model)} to ${JSON.stringify(expectedModel)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Admit one native retained turn and bind its server digest to the run. */
|
|
143
|
+
export async function beginCliBridgeNativeTurn(options, providerName, environmentInput, turn, run, transport, sessions, signal, onAdmission) {
|
|
144
|
+
if (!run.sessionId) {
|
|
145
|
+
throw new Error("cli-bridge native retained turns require a session id");
|
|
146
|
+
}
|
|
147
|
+
const body = toRetainedTurnBody(turn, run, providerName);
|
|
148
|
+
const nativeSession = await ensureCliBridgeNativeSession(options, environmentInput, turn, run.sessionId, transport, sessions, signal);
|
|
149
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/sessions/${encodeURIComponent(run.sessionId)}/turns`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: requestHeaders(options),
|
|
152
|
+
body: JSON.stringify(body),
|
|
153
|
+
...(signal ? { signal } : {}),
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new CliBridgeRequestRejectedError(response.status, await response.text());
|
|
157
|
+
}
|
|
158
|
+
onAdmission?.();
|
|
159
|
+
const parsed = safeJson(await response.text());
|
|
160
|
+
const responseSession = parsed?.session;
|
|
161
|
+
const responseRun = parsed?.run;
|
|
162
|
+
const responseSessionId = responseSession && typeof responseSession === "object"
|
|
163
|
+
? responseSession.id
|
|
164
|
+
: undefined;
|
|
165
|
+
const responseSessionModel = responseSession && typeof responseSession === "object"
|
|
166
|
+
? responseSession.model
|
|
167
|
+
: undefined;
|
|
168
|
+
const responseSessionDigest = responseSession && typeof responseSession === "object"
|
|
169
|
+
? responseSession.create_request_digest
|
|
170
|
+
: undefined;
|
|
171
|
+
const parsedSessionDigest = AgentExactRunControlRefSchema.shape.requestDigest.safeParse(responseSessionDigest);
|
|
172
|
+
const runId = responseRun && typeof responseRun === "object"
|
|
173
|
+
? responseRun.id
|
|
174
|
+
: undefined;
|
|
175
|
+
const responseSessionIdFromRun = responseRun && typeof responseRun === "object"
|
|
176
|
+
? responseRun.sessionId
|
|
177
|
+
: undefined;
|
|
178
|
+
const executionId = responseRun && typeof responseRun === "object"
|
|
179
|
+
? responseRun.executionId
|
|
180
|
+
: undefined;
|
|
181
|
+
const requestDigestValue = responseRun && typeof responseRun === "object"
|
|
182
|
+
? responseRun.requestDigest
|
|
183
|
+
: undefined;
|
|
184
|
+
const requestDigest = AgentExactRunControlRefSchema.shape.requestDigest.safeParse(requestDigestValue);
|
|
185
|
+
if (!parsed ||
|
|
186
|
+
typeof responseSessionId !== "string" ||
|
|
187
|
+
responseSessionId !== run.sessionId ||
|
|
188
|
+
responseSessionModel !== nativeSession.model ||
|
|
189
|
+
!parsedSessionDigest.success ||
|
|
190
|
+
parsedSessionDigest.data !== nativeSession.createRequestDigest ||
|
|
191
|
+
typeof runId !== "string" ||
|
|
192
|
+
runId !== run.id ||
|
|
193
|
+
typeof responseSessionIdFromRun !== "string" ||
|
|
194
|
+
responseSessionIdFromRun !== run.sessionId ||
|
|
195
|
+
typeof executionId !== "string" ||
|
|
196
|
+
executionId !== run.executionId ||
|
|
197
|
+
!requestDigest.success) {
|
|
198
|
+
throw new Error("cli-bridge retained turn returned mismatched run coordinates");
|
|
199
|
+
}
|
|
200
|
+
if (run.requestDigest !== undefined && run.requestDigest !== requestDigest.data) {
|
|
201
|
+
throw new Error("cli-bridge retained turn changed the admitted request digest");
|
|
202
|
+
}
|
|
203
|
+
run.requestDigest = requestDigest.data;
|
|
204
|
+
}
|
|
@@ -20,7 +20,7 @@ export interface CliBridgeSessionState {
|
|
|
20
20
|
export interface CliBridgeRunSnapshot {
|
|
21
21
|
readonly id: string;
|
|
22
22
|
readonly requestDigest?: Sha256Digest;
|
|
23
|
-
readonly status: "running" | "done" | "error" | "cancelled";
|
|
23
|
+
readonly status: "running" | "done" | "error" | "cancelled" | "unknown";
|
|
24
24
|
readonly terminal: boolean;
|
|
25
25
|
}
|
|
26
26
|
export interface PreparedCliBridgeRun {
|
|
@@ -46,6 +46,10 @@ export function assertRunMatchesControlRef(run, controlRef) {
|
|
|
46
46
|
run.controlRef = controlRef;
|
|
47
47
|
}
|
|
48
48
|
export function prepareCliBridgeRun(options, environmentInput, originalTurn, environmentId, requireSession) {
|
|
49
|
+
if (requireSession &&
|
|
50
|
+
(originalTurn.turnId === undefined || originalTurn.executionId === undefined)) {
|
|
51
|
+
throw new Error("cli-bridge native retained turns require stable turnId and executionId values");
|
|
52
|
+
}
|
|
49
53
|
const turnId = originalTurn.turnId ?? crypto.randomUUID();
|
|
50
54
|
const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
|
|
51
55
|
const sessionId = originalTurn.sessionId ?? (requireSession ? runId : undefined);
|
|
@@ -6,4 +6,4 @@ export declare class CliBridgeRequestRejectedError extends Error {
|
|
|
6
6
|
readonly status: number;
|
|
7
7
|
constructor(status: number, detail: string);
|
|
8
8
|
}
|
|
9
|
-
export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal, onResponse?: (response: CliBridgeResponse) => void, readAuthoritativeRun?: () => Promise<CliBridgeRunSnapshot | null
|
|
9
|
+
export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal, onResponse?: (response: CliBridgeResponse) => void, readAuthoritativeRun?: () => Promise<CliBridgeRunSnapshot | null>, validateCanonicalEvent?: (event: AgentEnvironmentEvent) => void): AsyncIterable<AgentEnvironmentEvent>;
|
package/dist/retained-stream.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-event-cursor.js";
|
|
2
|
+
import { isTerminalCanonicalEvent, retainedCanonicalEvent, } from "./retained-canonical-event.js";
|
|
2
3
|
import { detachCliBridgeReader } from "./retained-control.js";
|
|
3
4
|
import { requestHeaders, trimSlash } from "./transport.js";
|
|
4
5
|
import { modelRequestsFromOpenAi, parseSse, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
|
|
@@ -18,7 +19,7 @@ class CliBridgeRunCancelledError extends Error {
|
|
|
18
19
|
this.name = "CliBridgeRunCancelledError";
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun) {
|
|
22
|
+
export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun, validateCanonicalEvent) {
|
|
22
23
|
const reconnect = requestBody === undefined;
|
|
23
24
|
const replay = retainedReplayRequest(lastEventId);
|
|
24
25
|
const response = await transport.fetch(reconnect
|
|
@@ -72,14 +73,53 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
72
73
|
let systemFingerprint;
|
|
73
74
|
let textBoundaryPending = false;
|
|
74
75
|
let sawProtocolEnd = false;
|
|
76
|
+
let protocol = "unknown";
|
|
77
|
+
let canonicalTerminalStatus;
|
|
78
|
+
let canonicalTerminalEvent;
|
|
75
79
|
let terminalFrameSeen = false;
|
|
76
80
|
let cancelledTerminalSeen = false;
|
|
77
81
|
let terminalFrame;
|
|
78
82
|
for await (const frame of parseSse(response.body)) {
|
|
79
83
|
if (frame.data === "[DONE]") {
|
|
84
|
+
if (protocol === "canonical") {
|
|
85
|
+
throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
|
|
86
|
+
}
|
|
87
|
+
protocol = "openai";
|
|
80
88
|
sawProtocolEnd = true;
|
|
81
89
|
continue;
|
|
82
90
|
}
|
|
91
|
+
if (frame.event !== undefined) {
|
|
92
|
+
if (protocol === "openai") {
|
|
93
|
+
throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
|
|
94
|
+
}
|
|
95
|
+
protocol = "canonical";
|
|
96
|
+
if (canonicalTerminalStatus !== undefined) {
|
|
97
|
+
throw new Error("cli-bridge emitted an event after canonical terminal status");
|
|
98
|
+
}
|
|
99
|
+
const event = retainedCanonicalEvent(frame, {
|
|
100
|
+
runId,
|
|
101
|
+
sessionId,
|
|
102
|
+
...(turn.executionId === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { executionId: turn.executionId }),
|
|
105
|
+
});
|
|
106
|
+
if (!event) {
|
|
107
|
+
throw new Error("cli-bridge emitted an empty canonical event");
|
|
108
|
+
}
|
|
109
|
+
validateCanonicalEvent?.(event);
|
|
110
|
+
if (isTerminalCanonicalEvent(event)) {
|
|
111
|
+
canonicalTerminalStatus = event.normalized.status;
|
|
112
|
+
canonicalTerminalEvent = event;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
yield event;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (protocol === "canonical") {
|
|
120
|
+
throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
|
|
121
|
+
}
|
|
122
|
+
protocol = "openai";
|
|
83
123
|
if (terminalFrameSeen)
|
|
84
124
|
continue;
|
|
85
125
|
const parsed = safeJson(frame.data);
|
|
@@ -219,6 +259,32 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
219
259
|
}
|
|
220
260
|
yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
|
|
221
261
|
}
|
|
262
|
+
if (protocol === "canonical") {
|
|
263
|
+
if (canonicalTerminalStatus === undefined) {
|
|
264
|
+
throw new Error("cli-bridge canonical stream ended without terminal status");
|
|
265
|
+
}
|
|
266
|
+
const snapshot = await terminalRunSnapshot(runId, readAuthoritativeRun);
|
|
267
|
+
const expectedStatus = canonicalTerminalStatus === "completed"
|
|
268
|
+
? "done"
|
|
269
|
+
: canonicalTerminalStatus === "failed"
|
|
270
|
+
? "error"
|
|
271
|
+
: "cancelled";
|
|
272
|
+
if (snapshot.status !== expectedStatus) {
|
|
273
|
+
throw new Error(`cli-bridge canonical terminal status ${JSON.stringify(canonicalTerminalStatus)} contradicts retained run status ${JSON.stringify(snapshot.status)}`);
|
|
274
|
+
}
|
|
275
|
+
if (!canonicalTerminalEvent) {
|
|
276
|
+
throw new Error("cli-bridge canonical terminal event was not retained");
|
|
277
|
+
}
|
|
278
|
+
yield canonicalTerminalEvent;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!sawProtocolEnd &&
|
|
282
|
+
reconnect &&
|
|
283
|
+
lastEventId !== undefined &&
|
|
284
|
+
protocol === "unknown") {
|
|
285
|
+
await terminalRunSnapshot(runId, readAuthoritativeRun);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
222
288
|
if (!sawProtocolEnd) {
|
|
223
289
|
throw new Error("cli-bridge stream ended without the [DONE] protocol marker");
|
|
224
290
|
}
|
package/dist/wire.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type { TokenUsage } from "@tangle-network/agent-interface";
|
|
1
|
+
import type { AgentProfile, AgentProfileRef, TokenUsage } from "@tangle-network/agent-interface";
|
|
2
2
|
import type { AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
3
3
|
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
4
|
-
import type { CliBridgeRunSnapshot } from "./retained-run-state.js";
|
|
4
|
+
import type { CliBridgeRun, CliBridgeRunSnapshot } from "./retained-run-state.js";
|
|
5
5
|
export declare function toChatCompletionsBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, runId: string): Record<string, unknown>;
|
|
6
|
+
export declare function resolveBridgeModel(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, turn: AgentTurnInput, profile: AgentProfile | undefined): string;
|
|
7
|
+
export declare function toRetainedSessionBody(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, profile: AgentProfile | undefined, sessionId: string, model: string): Record<string, unknown>;
|
|
8
|
+
/** Encode the exact retained turn contract without moving posture into metadata. */
|
|
9
|
+
export declare function toRetainedTurnBody(turn: AgentTurnInput, run: CliBridgeRun, providerName: string): Record<string, unknown>;
|
|
10
|
+
export declare function inlineProfile(profile: AgentProfileRef): AgentProfile | undefined;
|
|
6
11
|
export interface CliBridgeSseFrame {
|
|
7
12
|
readonly data: string;
|
|
8
13
|
readonly id?: string;
|
|
14
|
+
readonly event?: string;
|
|
9
15
|
}
|
|
10
16
|
export declare function parseSse(body: AsyncIterable<Uint8Array>): AsyncIterable<CliBridgeSseFrame>;
|
|
11
17
|
export declare function toolCallsFromDelta(value: unknown): Array<{
|
package/dist/wire.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { AgentExactRunControlRefSchema } from "@tangle-network/agent-interface";
|
|
2
|
+
import { AgentExactRunControlRefSchema, RequestedInteractionsSchema, } from "@tangle-network/agent-interface";
|
|
3
3
|
export function toChatCompletionsBody(options, environmentInput, turn, runId) {
|
|
4
4
|
const profile = inlineProfile(environmentInput.profile);
|
|
5
5
|
const execution = executionFromInput(options, environmentInput);
|
|
6
|
+
const interactions = turn.interactions === undefined
|
|
7
|
+
? undefined
|
|
8
|
+
: RequestedInteractionsSchema.parse(turn.interactions);
|
|
6
9
|
return {
|
|
7
10
|
model: resolveBridgeModel(options, environmentInput, turn, profile),
|
|
8
11
|
messages: messagesFromTurn(turn),
|
|
@@ -17,6 +20,7 @@ export function toChatCompletionsBody(options, environmentInput, turn, runId) {
|
|
|
17
20
|
...(environmentInput.env ? { env: environmentInput.env } : {}),
|
|
18
21
|
...(environmentInput.workspace?.cwd ? { cwd: environmentInput.workspace.cwd } : {}),
|
|
19
22
|
...(execution ? { execution } : {}),
|
|
23
|
+
...(interactions === undefined ? {} : { interactions }),
|
|
20
24
|
metadata: {
|
|
21
25
|
...(environmentInput.metadata ?? {}),
|
|
22
26
|
...(turn.context ?? {}),
|
|
@@ -24,7 +28,7 @@ export function toChatCompletionsBody(options, environmentInput, turn, runId) {
|
|
|
24
28
|
},
|
|
25
29
|
};
|
|
26
30
|
}
|
|
27
|
-
function resolveBridgeModel(options, environmentInput, turn, profile) {
|
|
31
|
+
export function resolveBridgeModel(options, environmentInput, turn, profile) {
|
|
28
32
|
const harness = environmentInput.backend ?? profile?.harness;
|
|
29
33
|
const model = turn.model ?? options.defaultModel ?? profile?.model?.default;
|
|
30
34
|
const provider = profile?.model?.provider;
|
|
@@ -43,6 +47,77 @@ function resolveBridgeModel(options, environmentInput, turn, profile) {
|
|
|
43
47
|
return `${harness}/${provider}/${model}`;
|
|
44
48
|
return `${harness}/${model}`;
|
|
45
49
|
}
|
|
50
|
+
export function toRetainedSessionBody(options, environmentInput, profile, sessionId, model) {
|
|
51
|
+
const execution = retainedExecutionFromInput(options, environmentInput);
|
|
52
|
+
return {
|
|
53
|
+
id: sessionId,
|
|
54
|
+
model,
|
|
55
|
+
interaction_policy: "interactive",
|
|
56
|
+
...(options.defaultMode ? { mode: options.defaultMode } : {}),
|
|
57
|
+
...(environmentInput.workspace?.cwd !== undefined
|
|
58
|
+
? { cwd: environmentInput.workspace.cwd }
|
|
59
|
+
: {}),
|
|
60
|
+
...(execution !== undefined ? { execution } : {}),
|
|
61
|
+
...(profile ? { agent_profile: profile } : {}),
|
|
62
|
+
...(environmentInput.env !== undefined ? { env: environmentInput.env } : {}),
|
|
63
|
+
...(environmentInput.metadata !== undefined
|
|
64
|
+
? { metadata: environmentInput.metadata }
|
|
65
|
+
: {}),
|
|
66
|
+
...(environmentInput.providerOptions !== undefined
|
|
67
|
+
? { provider_options: environmentInput.providerOptions }
|
|
68
|
+
: {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Encode the exact retained turn contract without moving posture into metadata. */
|
|
72
|
+
export function toRetainedTurnBody(turn, run, providerName) {
|
|
73
|
+
assertRetainedTurnInputSupported(turn);
|
|
74
|
+
const interactions = RequestedInteractionsSchema.parse(turn.interactions ?? {});
|
|
75
|
+
const content = {};
|
|
76
|
+
if (turn.prompt !== undefined) {
|
|
77
|
+
if (turn.prompt.length === 0) {
|
|
78
|
+
throw new Error("cli-bridge retained sessions require a non-empty message");
|
|
79
|
+
}
|
|
80
|
+
content.message = turn.prompt;
|
|
81
|
+
}
|
|
82
|
+
if (turn.parts !== undefined) {
|
|
83
|
+
if (turn.parts.length === 0) {
|
|
84
|
+
throw new Error("cli-bridge retained sessions require non-empty input parts");
|
|
85
|
+
}
|
|
86
|
+
content.parts = turn.parts;
|
|
87
|
+
}
|
|
88
|
+
if (turn.prompt === undefined && turn.parts === undefined) {
|
|
89
|
+
throw new Error("cli-bridge retained sessions require a non-empty message or input parts");
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...content,
|
|
93
|
+
turn_id: run.turnId,
|
|
94
|
+
execution_id: run.executionId,
|
|
95
|
+
run_id: run.id,
|
|
96
|
+
provider: providerName,
|
|
97
|
+
environment_id: run.environmentId,
|
|
98
|
+
...(turn.interactions === undefined ? {} : { interactions }),
|
|
99
|
+
...(turn.context !== undefined ? { context: turn.context } : {}),
|
|
100
|
+
...(turn.providerOptions !== undefined
|
|
101
|
+
? { provider_options: turn.providerOptions }
|
|
102
|
+
: {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function assertRetainedTurnInputSupported(turn) {
|
|
106
|
+
const unsupported = [];
|
|
107
|
+
if (turn.timeoutMs !== undefined)
|
|
108
|
+
unsupported.push("timeoutMs");
|
|
109
|
+
if (turn.controlRef !== undefined)
|
|
110
|
+
unsupported.push("controlRef");
|
|
111
|
+
if (turn.contextTransfer !== undefined)
|
|
112
|
+
unsupported.push("contextTransfer");
|
|
113
|
+
if (turn.nativeContinuation !== undefined)
|
|
114
|
+
unsupported.push("nativeContinuation");
|
|
115
|
+
if (turn.detach === true)
|
|
116
|
+
unsupported.push("detach");
|
|
117
|
+
if (unsupported.length > 0) {
|
|
118
|
+
throw new Error(`cli-bridge retained turns cannot represent ${unsupported.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
46
121
|
/** Keep task messages separate from the profile's harness-owned prompt controls. */
|
|
47
122
|
function messagesFromTurn(turn) {
|
|
48
123
|
return [{ role: "user", content: contentFromTurn(turn) }];
|
|
@@ -52,7 +127,7 @@ function contentFromTurn(turn) {
|
|
|
52
127
|
return turn.parts;
|
|
53
128
|
return turn.prompt ?? "";
|
|
54
129
|
}
|
|
55
|
-
function inlineProfile(profile) {
|
|
130
|
+
export function inlineProfile(profile) {
|
|
56
131
|
return typeof profile === "string" ? undefined : profile;
|
|
57
132
|
}
|
|
58
133
|
function executionFromInput(options, input) {
|
|
@@ -66,6 +141,34 @@ function executionFromInput(options, input) {
|
|
|
66
141
|
...(input.workspace.gitRef ? { gitRef: input.workspace.gitRef } : {}),
|
|
67
142
|
};
|
|
68
143
|
}
|
|
144
|
+
function retainedExecutionFromInput(options, input) {
|
|
145
|
+
const unsupported = [];
|
|
146
|
+
if (typeof input.profile === "string")
|
|
147
|
+
unsupported.push("named profile");
|
|
148
|
+
if (input.workspace?.environment !== undefined)
|
|
149
|
+
unsupported.push("workspace.environment");
|
|
150
|
+
if (input.workspace?.image !== undefined)
|
|
151
|
+
unsupported.push("workspace.image");
|
|
152
|
+
if (input.workspace?.repoUrl !== undefined)
|
|
153
|
+
unsupported.push("workspace.repoUrl");
|
|
154
|
+
if (input.workspace?.gitRef !== undefined)
|
|
155
|
+
unsupported.push("workspace.gitRef");
|
|
156
|
+
if (input.workspace?.providerOptions !== undefined) {
|
|
157
|
+
unsupported.push("workspace.providerOptions");
|
|
158
|
+
}
|
|
159
|
+
if (input.resources !== undefined)
|
|
160
|
+
unsupported.push("resources");
|
|
161
|
+
if (input.secrets !== undefined)
|
|
162
|
+
unsupported.push("secrets");
|
|
163
|
+
if (unsupported.length > 0) {
|
|
164
|
+
throw new Error(`cli-bridge retained sessions cannot represent ${unsupported.join(", ")}`);
|
|
165
|
+
}
|
|
166
|
+
const execution = executionFromInput(options, input);
|
|
167
|
+
if (execution?.kind === "sandbox") {
|
|
168
|
+
throw new Error("cli-bridge retained sessions cannot execute in a sandbox; use one-shot execution");
|
|
169
|
+
}
|
|
170
|
+
return execution;
|
|
171
|
+
}
|
|
69
172
|
export async function* parseSse(body) {
|
|
70
173
|
const decoder = new TextDecoder();
|
|
71
174
|
let buffer = "";
|
|
@@ -106,7 +209,11 @@ function dataFromFrame(frame) {
|
|
|
106
209
|
if (!data)
|
|
107
210
|
return undefined;
|
|
108
211
|
const id = lines.find((line) => line.startsWith("id:"))?.slice("id:".length).trim();
|
|
109
|
-
|
|
212
|
+
const event = lines
|
|
213
|
+
.find((line) => line.startsWith("event:"))
|
|
214
|
+
?.slice("event:".length)
|
|
215
|
+
.trim();
|
|
216
|
+
return { data, ...(id ? { id } : {}), ...(event ? { event } : {}) };
|
|
110
217
|
}
|
|
111
218
|
export function toolCallsFromDelta(value) {
|
|
112
219
|
if (!value || typeof value !== "object")
|
|
@@ -223,7 +330,7 @@ export function runSnapshot(value) {
|
|
|
223
330
|
const terminal = parsed.terminal;
|
|
224
331
|
if (typeof id !== "string" ||
|
|
225
332
|
(requestDigest !== undefined && !requestDigest.success) ||
|
|
226
|
-
!["running", "done", "error", "cancelled"].includes(String(status)) ||
|
|
333
|
+
!["running", "done", "error", "cancelled", "unknown"].includes(String(status)) ||
|
|
227
334
|
typeof terminal !== "boolean") {
|
|
228
335
|
throw new Error("cli-bridge run status returned an invalid snapshot");
|
|
229
336
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-cli-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AgentEnvironmentProvider adapter for cli-bridge
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -25,10 +25,18 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist/index.d.ts",
|
|
27
27
|
"dist/index.js",
|
|
28
|
+
"dist/environment-identity.d.ts",
|
|
29
|
+
"dist/environment-identity.js",
|
|
30
|
+
"dist/capability-discovery.d.ts",
|
|
31
|
+
"dist/capability-discovery.js",
|
|
32
|
+
"dist/interaction-response.d.ts",
|
|
33
|
+
"dist/interaction-response.js",
|
|
28
34
|
"dist/observation.d.ts",
|
|
29
35
|
"dist/observation.js",
|
|
30
36
|
"dist/provider-options.d.ts",
|
|
31
37
|
"dist/provider-options.js",
|
|
38
|
+
"dist/retained-canonical-event.d.ts",
|
|
39
|
+
"dist/retained-canonical-event.js",
|
|
32
40
|
"dist/retained-control.d.ts",
|
|
33
41
|
"dist/retained-control.js",
|
|
34
42
|
"dist/retained-environment.d.ts",
|
|
@@ -37,6 +45,8 @@
|
|
|
37
45
|
"dist/retained-event-cursor.js",
|
|
38
46
|
"dist/retained-execution.d.ts",
|
|
39
47
|
"dist/retained-execution.js",
|
|
48
|
+
"dist/retained-native.d.ts",
|
|
49
|
+
"dist/retained-native.js",
|
|
40
50
|
"dist/retained-run-state.d.ts",
|
|
41
51
|
"dist/retained-run-state.js",
|
|
42
52
|
"dist/retained-stream.d.ts",
|
|
@@ -58,9 +68,10 @@
|
|
|
58
68
|
"vitest": "^4.1.5"
|
|
59
69
|
},
|
|
60
70
|
"scripts": {
|
|
61
|
-
"build": "tsc -p tsconfig.json",
|
|
71
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json",
|
|
62
72
|
"check-types": "tsc --noEmit",
|
|
63
73
|
"clean": "rm -rf dist",
|
|
64
|
-
"test": "vitest run"
|
|
74
|
+
"test": "vitest run src",
|
|
75
|
+
"test:integration": "test -n \"$CLI_BRIDGE_INTEGRATION_ROOT\" || { echo 'CLI_BRIDGE_INTEGRATION_ROOT is required; see README' >&2; exit 1; }; vitest run tests/cli-bridge.integration.test.ts"
|
|
65
76
|
}
|
|
66
77
|
}
|