@tangle-network/agent-provider-cli-bridge 0.6.4 → 0.7.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 +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -1
- package/dist/observation.d.ts +49 -0
- package/dist/observation.js +141 -0
- package/dist/retained-environment.d.ts +7 -1
- package/dist/retained-environment.js +30 -3
- package/dist/retained-execution.d.ts +3 -0
- package/dist/retained-execution.js +2 -1
- package/dist/retained-stream.js +83 -10
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -58,3 +58,24 @@ Stopping a direct `environment.stream()` reader or destroying the environment ca
|
|
|
58
58
|
|
|
59
59
|
Response headers and streamed bodies have no transport timeout by default.
|
|
60
60
|
For unattended runs, set `headersTimeoutMs`, `bodyTimeoutMs`, or an `AbortSignal` so an unresponsive bridge cannot wait forever.
|
|
61
|
+
|
|
62
|
+
## Environment observation
|
|
63
|
+
|
|
64
|
+
`environment.observe()` returns the normalized `AgentEnvironmentObservation` when the provider document carries an `observation` block.
|
|
65
|
+
Every surface keeps a freshness discriminator, so a value this bridge cannot report is visibly absent instead of arriving as a measured zero.
|
|
66
|
+
|
|
67
|
+
| Surface | cli-bridge source | State when the source is missing |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `identity` | provider name and environment id | always known |
|
|
70
|
+
| `lifecycle.status` | `running` until the environment is destroyed | always known |
|
|
71
|
+
| `endpoint` | scheme, host, and explicit port of `baseUrl` | `unavailable` |
|
|
72
|
+
| `placement.verified` | `defaultExecution.kind` as `sandbox` or `local` | always known |
|
|
73
|
+
| `modelUsage` | the token usage and cost the newest execution measured | `unavailable` |
|
|
74
|
+
|
|
75
|
+
cli-bridge forwards a turn and provisions no compute, so it states no requested or effective compute shape, no resource use, and no compute cost.
|
|
76
|
+
It holds no account, so it states no plan, credit balance, quota, or billing period.
|
|
77
|
+
Those five surfaces are never claimed in the capability document and always carry `unavailable` with the reason.
|
|
78
|
+
|
|
79
|
+
The usage of one run is summed across its events and attributed to that run's execution id, which the observation carries as the provenance source.
|
|
80
|
+
An environment rebuilt with `provider.get(id)` has measured nothing yet, so it reports the usage surface as unavailable until a turn completes through it.
|
|
81
|
+
The endpoint holds only a scheme, a host, and an explicit port; userinfo, path, and query are dropped, and the bearer beside the base URL is never read.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AgentEnvironmentCapabilities, AgentEnvironmentProvider } from "@ta
|
|
|
2
2
|
import { type HarnessType } from "@tangle-network/agent-interface";
|
|
3
3
|
import { type CliBridgeProviderOptions } from "./provider-options.js";
|
|
4
4
|
export type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
5
|
+
export { safeEndpointFromBaseUrl } from "./observation.js";
|
|
5
6
|
export declare function createCliBridgeProvider(options: CliBridgeProviderOptions): AgentEnvironmentProvider;
|
|
6
7
|
/**
|
|
7
8
|
* Describe the bridge backend, not this adapter.
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { harnessSystemPromptIntents, snapshotAgentProfile, } from "@tangle-network/agent-interface";
|
|
2
2
|
import { createCliBridgeEnvironment } from "./retained-environment.js";
|
|
3
3
|
import { assertCliBridgeProviderOptions, } from "./provider-options.js";
|
|
4
|
+
import { narrowedCliBridgeObservation } from "./observation.js";
|
|
5
|
+
export { safeEndpointFromBaseUrl } from "./observation.js";
|
|
4
6
|
export function createCliBridgeProvider(options) {
|
|
5
7
|
assertCliBridgeProviderOptions(options);
|
|
6
8
|
const name = options.name ?? "cli-bridge";
|
|
9
|
+
// The observation surfaces are declared as intent and narrowed to the
|
|
10
|
+
// sources this bridge can put a value on, so the environment offers the
|
|
11
|
+
// operation exactly where the document claims it.
|
|
12
|
+
const resolveCapabilities = () => {
|
|
13
|
+
const declared = options.capabilities ?? defaultCliBridgeCapabilities();
|
|
14
|
+
return declared.observation === undefined
|
|
15
|
+
? declared
|
|
16
|
+
: {
|
|
17
|
+
...declared,
|
|
18
|
+
observation: narrowedCliBridgeObservation(declared.observation, options),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
7
21
|
return {
|
|
8
22
|
name,
|
|
9
|
-
capabilities:
|
|
23
|
+
capabilities: resolveCapabilities,
|
|
10
24
|
async create(input) {
|
|
11
25
|
if (typeof input.profile === "string") {
|
|
12
26
|
throw new Error(`createCliBridgeProvider requires an inline AgentProfile; named profile "${input.profile}" is unsupported`);
|
|
@@ -23,6 +37,7 @@ export function createCliBridgeProvider(options) {
|
|
|
23
37
|
environmentId,
|
|
24
38
|
allowDispatch: true,
|
|
25
39
|
cancelRunsOnDestroy: true,
|
|
40
|
+
capabilities: resolveCapabilities(),
|
|
26
41
|
});
|
|
27
42
|
},
|
|
28
43
|
async get(id) {
|
|
@@ -36,6 +51,7 @@ export function createCliBridgeProvider(options) {
|
|
|
36
51
|
environmentId: id,
|
|
37
52
|
allowDispatch: false,
|
|
38
53
|
cancelRunsOnDestroy: false,
|
|
54
|
+
capabilities: resolveCapabilities(),
|
|
39
55
|
});
|
|
40
56
|
},
|
|
41
57
|
};
|
|
@@ -88,5 +104,19 @@ export function defaultCliBridgeCapabilities(harness) {
|
|
|
88
104
|
placement: true,
|
|
89
105
|
usage: true,
|
|
90
106
|
confidential: false,
|
|
107
|
+
// cli-bridge measures what a turn cost and where it forwarded that turn.
|
|
108
|
+
// It provisions no compute and holds no account, so the surfaces that
|
|
109
|
+
// describe provisioned resources and billing are never claimed.
|
|
110
|
+
observation: {
|
|
111
|
+
identity: true,
|
|
112
|
+
lifecycle: true,
|
|
113
|
+
endpoint: true,
|
|
114
|
+
placement: true,
|
|
115
|
+
resources: false,
|
|
116
|
+
resourceUse: false,
|
|
117
|
+
modelUsage: true,
|
|
118
|
+
computeBilling: false,
|
|
119
|
+
accountUsage: false,
|
|
120
|
+
},
|
|
91
121
|
};
|
|
92
122
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AgentEnvironmentCapabilities, AgentEnvironmentObservation, SafeEndpoint, TokenUsage } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
3
|
+
/** One execution's accumulated token usage and cost. */
|
|
4
|
+
interface ExecutionUsageRecord {
|
|
5
|
+
executionId: string;
|
|
6
|
+
usage: TokenUsage;
|
|
7
|
+
observedAt: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The newest per-execution usage this environment handle measured.
|
|
11
|
+
*
|
|
12
|
+
* cli-bridge reports usage on the events of one run, so the totals are summed
|
|
13
|
+
* per execution and the newest execution answers the environment-scoped
|
|
14
|
+
* observation. The record lives with the handle: an environment rebuilt by id
|
|
15
|
+
* starts with none, and the observation then reports the usage surface as
|
|
16
|
+
* unavailable rather than as a measured zero.
|
|
17
|
+
*/
|
|
18
|
+
export interface ExecutionUsageLog {
|
|
19
|
+
record(executionId: string | undefined, usage: TokenUsage | undefined): void;
|
|
20
|
+
latest(): ExecutionUsageRecord | undefined;
|
|
21
|
+
}
|
|
22
|
+
export declare function createExecutionUsageLog(): ExecutionUsageLog;
|
|
23
|
+
/**
|
|
24
|
+
* Read the credential-free network location of the cli-bridge endpoint.
|
|
25
|
+
*
|
|
26
|
+
* Only the scheme, host, and explicit port are carried. Userinfo, path, and
|
|
27
|
+
* query are dropped, and the bearer beside the base URL is never read, so an
|
|
28
|
+
* endpoint payload cannot transport a credential.
|
|
29
|
+
*/
|
|
30
|
+
export declare function safeEndpointFromBaseUrl(baseUrl: string | undefined): SafeEndpoint | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Clear every observation surface no source backs.
|
|
33
|
+
*
|
|
34
|
+
* cli-bridge forwards a turn to a remote bridge and reports what that turn
|
|
35
|
+
* cost. It never provisions compute, so it states no compute shape, no
|
|
36
|
+
* resource use, no compute cost, and no account plan. The endpoint rests on a
|
|
37
|
+
* base URL that parses.
|
|
38
|
+
*/
|
|
39
|
+
export declare function narrowedCliBridgeObservation(declared: NonNullable<AgentEnvironmentCapabilities["observation"]>, options: CliBridgeProviderOptions): NonNullable<AgentEnvironmentCapabilities["observation"]>;
|
|
40
|
+
export interface CliBridgeObservationSources {
|
|
41
|
+
options: CliBridgeProviderOptions;
|
|
42
|
+
provider: string;
|
|
43
|
+
environmentId: string;
|
|
44
|
+
destroyed: boolean;
|
|
45
|
+
usageLog: ExecutionUsageLog;
|
|
46
|
+
}
|
|
47
|
+
/** Build the normalized, freshness-tagged observation of one environment. */
|
|
48
|
+
export declare function observeCliBridgeEnvironment(sources: CliBridgeObservationSources): AgentEnvironmentObservation;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AgentEnvironmentObservationSchema } from "@tangle-network/agent-interface";
|
|
2
|
+
export function createExecutionUsageLog() {
|
|
3
|
+
let latest;
|
|
4
|
+
return {
|
|
5
|
+
record(executionId, usage) {
|
|
6
|
+
// Usage without the execution it belongs to cannot be attributed, and an
|
|
7
|
+
// execution without usage measured nothing. Neither is recorded.
|
|
8
|
+
if (executionId === undefined || executionId.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
if (usage === undefined)
|
|
11
|
+
return;
|
|
12
|
+
latest = {
|
|
13
|
+
executionId,
|
|
14
|
+
usage,
|
|
15
|
+
observedAt: new Date().toISOString(),
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
latest() {
|
|
19
|
+
return latest === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: { ...latest, usage: { ...latest.usage } };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the credential-free network location of the cli-bridge endpoint.
|
|
27
|
+
*
|
|
28
|
+
* Only the scheme, host, and explicit port are carried. Userinfo, path, and
|
|
29
|
+
* query are dropped, and the bearer beside the base URL is never read, so an
|
|
30
|
+
* endpoint payload cannot transport a credential.
|
|
31
|
+
*/
|
|
32
|
+
export function safeEndpointFromBaseUrl(baseUrl) {
|
|
33
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = new URL(baseUrl);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const scheme = parsed.protocol.replace(/:$/, "");
|
|
43
|
+
const host = parsed.hostname;
|
|
44
|
+
if (scheme.length === 0 || host.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
const port = parsed.port === "" ? undefined : Number.parseInt(parsed.port, 10);
|
|
47
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return { scheme, host, ...(port === undefined ? {} : { port }) };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clear every observation surface no source backs.
|
|
54
|
+
*
|
|
55
|
+
* cli-bridge forwards a turn to a remote bridge and reports what that turn
|
|
56
|
+
* cost. It never provisions compute, so it states no compute shape, no
|
|
57
|
+
* resource use, no compute cost, and no account plan. The endpoint rests on a
|
|
58
|
+
* base URL that parses.
|
|
59
|
+
*/
|
|
60
|
+
export function narrowedCliBridgeObservation(declared, options) {
|
|
61
|
+
const endpoint = safeEndpointFromBaseUrl(options.baseUrl) !== undefined;
|
|
62
|
+
return {
|
|
63
|
+
identity: declared.identity,
|
|
64
|
+
lifecycle: declared.lifecycle,
|
|
65
|
+
endpoint: endpoint ? declared.endpoint : false,
|
|
66
|
+
placement: declared.placement,
|
|
67
|
+
resources: false,
|
|
68
|
+
resourceUse: false,
|
|
69
|
+
modelUsage: declared.modelUsage,
|
|
70
|
+
computeBilling: false,
|
|
71
|
+
accountUsage: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Build the normalized, freshness-tagged observation of one environment. */
|
|
75
|
+
export function observeCliBridgeEnvironment(sources) {
|
|
76
|
+
const capturedAt = new Date().toISOString();
|
|
77
|
+
const provenance = {
|
|
78
|
+
origin: "reported",
|
|
79
|
+
observedAt: capturedAt,
|
|
80
|
+
source: "cli-bridge",
|
|
81
|
+
};
|
|
82
|
+
const subject = {
|
|
83
|
+
provider: sources.provider,
|
|
84
|
+
environmentId: sources.environmentId,
|
|
85
|
+
};
|
|
86
|
+
const endpoint = safeEndpointFromBaseUrl(sources.options.baseUrl);
|
|
87
|
+
const latest = sources.usageLog.latest();
|
|
88
|
+
const absent = (reason) => ({
|
|
89
|
+
state: "unavailable",
|
|
90
|
+
reason,
|
|
91
|
+
});
|
|
92
|
+
const noCompute = "cli-bridge forwards a turn and provisions no compute";
|
|
93
|
+
const noAccount = "cli-bridge reports no account plan, credit, or quota";
|
|
94
|
+
const noProbe = "cli-bridge never probes the bridge, so its runtime state is unproven";
|
|
95
|
+
return AgentEnvironmentObservationSchema.parse({
|
|
96
|
+
subject,
|
|
97
|
+
capturedAt,
|
|
98
|
+
identity: { state: "known", value: subject, provenance },
|
|
99
|
+
// A destroyed handle stopped because this adapter stopped it, which is the
|
|
100
|
+
// only lifecycle fact cli-bridge establishes. A live handle holds a base
|
|
101
|
+
// URL and nothing else, and configuration is not a running runtime.
|
|
102
|
+
lifecycle: sources.destroyed
|
|
103
|
+
? { state: "known", value: { status: "stopped" }, provenance }
|
|
104
|
+
: absent(noProbe),
|
|
105
|
+
endpoint: endpoint === undefined
|
|
106
|
+
? absent("the cli-bridge base URL states no reachable host")
|
|
107
|
+
: { state: "known", value: endpoint, provenance },
|
|
108
|
+
// The configured execution kind is where the caller asked the turn to run,
|
|
109
|
+
// so it is carried as the request. cli-bridge runs no placement check, so
|
|
110
|
+
// nothing verifies it.
|
|
111
|
+
placement: {
|
|
112
|
+
requested: {
|
|
113
|
+
kind: sources.options.defaultExecution?.kind === "sandbox" ? "sandbox" : "local",
|
|
114
|
+
},
|
|
115
|
+
verified: absent(noProbe),
|
|
116
|
+
},
|
|
117
|
+
resources: { effective: absent(noCompute) },
|
|
118
|
+
resourceUse: { current: absent(noCompute), peak: absent(noCompute) },
|
|
119
|
+
modelUsage: latest === undefined
|
|
120
|
+
? absent("no execution measured token usage through this environment handle")
|
|
121
|
+
: {
|
|
122
|
+
state: "known",
|
|
123
|
+
value: latest.usage,
|
|
124
|
+
// The provenance source names the execution the usage belongs to,
|
|
125
|
+
// because cli-bridge measures usage per run and never for the
|
|
126
|
+
// environment.
|
|
127
|
+
provenance: {
|
|
128
|
+
origin: "reported",
|
|
129
|
+
observedAt: latest.observedAt,
|
|
130
|
+
source: latest.executionId,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
computeBilling: absent(noCompute),
|
|
134
|
+
accountUsage: {
|
|
135
|
+
plan: absent(noAccount),
|
|
136
|
+
credits: absent(noAccount),
|
|
137
|
+
quota: absent(noAccount),
|
|
138
|
+
period: absent(noAccount),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentEnvironment, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
1
|
+
import type { AgentEnvironment, AgentEnvironmentCapabilities, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
2
2
|
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
3
3
|
export interface CreateCliBridgeEnvironmentArgs {
|
|
4
4
|
readonly options: CliBridgeProviderOptions;
|
|
@@ -7,5 +7,11 @@ export interface CreateCliBridgeEnvironmentArgs {
|
|
|
7
7
|
readonly environmentId: string;
|
|
8
8
|
readonly allowDispatch: boolean;
|
|
9
9
|
readonly cancelRunsOnDestroy: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The document this provider publishes. The environment offers an optional
|
|
12
|
+
* operation only where the document claims it, so a caller never selects an
|
|
13
|
+
* action the document did not carry.
|
|
14
|
+
*/
|
|
15
|
+
readonly capabilities: AgentEnvironmentCapabilities;
|
|
10
16
|
}
|
|
11
17
|
export declare function createCliBridgeEnvironment(args: CreateCliBridgeEnvironmentArgs): AgentEnvironment;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { agentSessionStatusFromRun, cancelCliBridgeRun, cancelExactCliBridgeRun, getCliBridgeRun, } from "./retained-control.js";
|
|
2
|
-
import { collectCliBridgeTurnResult, dispatchCliBridgeTurn, streamCliBridgeSessionEvents, streamTrackedCliBridgeTurn, } from "./retained-execution.js";
|
|
2
|
+
import { addTokenUsage, collectCliBridgeTurnResult, dispatchCliBridgeTurn, streamCliBridgeSessionEvents, streamTrackedCliBridgeTurn, } from "./retained-execution.js";
|
|
3
|
+
import { createExecutionUsageLog, observeCliBridgeEnvironment, } from "./observation.js";
|
|
3
4
|
import { assertRunMatchesControlRef, exactControlRefForSession, prepareCliBridgeRun, runFromControlRef, } from "./retained-run-state.js";
|
|
4
5
|
import { createCliBridgeTransport } from "./transport.js";
|
|
5
6
|
export function createCliBridgeEnvironment(args) {
|
|
@@ -8,6 +9,7 @@ export function createCliBridgeEnvironment(args) {
|
|
|
8
9
|
const runs = new Map();
|
|
9
10
|
const sessions = new Map();
|
|
10
11
|
const readers = new Set();
|
|
12
|
+
const usageLog = createExecutionUsageLog();
|
|
11
13
|
let destroyed = false;
|
|
12
14
|
let closePromise;
|
|
13
15
|
const stream = async function* (turn) {
|
|
@@ -17,7 +19,14 @@ export function createCliBridgeEnvironment(args) {
|
|
|
17
19
|
throw new Error("a reconstructed cli-bridge environment can only control an existing run");
|
|
18
20
|
}
|
|
19
21
|
const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, false);
|
|
20
|
-
|
|
22
|
+
// A streamed run reports usage across its events, so the totals are summed
|
|
23
|
+
// here and recorded as one execution's measurement.
|
|
24
|
+
let streamedUsage;
|
|
25
|
+
for await (const event of streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers)) {
|
|
26
|
+
streamedUsage = addTokenUsage(streamedUsage, event.usage);
|
|
27
|
+
usageLog.record(prepared.run.executionId, streamedUsage);
|
|
28
|
+
yield event;
|
|
29
|
+
}
|
|
21
30
|
};
|
|
22
31
|
return {
|
|
23
32
|
id: environmentId,
|
|
@@ -52,8 +61,23 @@ export function createCliBridgeEnvironment(args) {
|
|
|
52
61
|
allowPrompt: args.allowDispatch,
|
|
53
62
|
requestedControlRef: sessionOptions?.controlRef,
|
|
54
63
|
isDestroyed: () => destroyed,
|
|
64
|
+
usageLog,
|
|
55
65
|
});
|
|
56
66
|
},
|
|
67
|
+
...(args.capabilities.observation
|
|
68
|
+
? {
|
|
69
|
+
async observe(observeOptions) {
|
|
70
|
+
observeOptions?.signal?.throwIfAborted();
|
|
71
|
+
return observeCliBridgeEnvironment({
|
|
72
|
+
options,
|
|
73
|
+
provider: providerName,
|
|
74
|
+
environmentId,
|
|
75
|
+
destroyed,
|
|
76
|
+
usageLog,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
57
81
|
placement: async (placementOptions) => {
|
|
58
82
|
placementOptions?.signal?.throwIfAborted();
|
|
59
83
|
return {
|
|
@@ -161,7 +185,9 @@ function createCliBridgeSession(args) {
|
|
|
161
185
|
},
|
|
162
186
|
async result(resultOptions) {
|
|
163
187
|
const run = requireCurrentRun();
|
|
164
|
-
|
|
188
|
+
const result = await 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);
|
|
189
|
+
args.usageLog.record(run.executionId, result.usage);
|
|
190
|
+
return result;
|
|
165
191
|
},
|
|
166
192
|
async prompt(input) {
|
|
167
193
|
if (!args.allowPrompt) {
|
|
@@ -175,6 +201,7 @@ function createCliBridgeSession(args) {
|
|
|
175
201
|
sessionId: args.id,
|
|
176
202
|
}, args.environmentId, false);
|
|
177
203
|
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);
|
|
204
|
+
args.usageLog.record(prepared.run.executionId, result.usage);
|
|
178
205
|
return { ...result, sessionId: args.id };
|
|
179
206
|
},
|
|
180
207
|
async cancel(cancelOptions) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TokenUsage } from "@tangle-network/agent-interface";
|
|
1
2
|
import type { AgentEnvironmentEvent, AgentSessionRef, AgentTurnResult, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
2
3
|
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
3
4
|
import { type CliBridgeRun, type CliBridgeSessionState, type PreparedCliBridgeRun } from "./retained-run-state.js";
|
|
@@ -10,3 +11,5 @@ export declare function streamCliBridgeSessionEvents(options: CliBridgeProviderO
|
|
|
10
11
|
signal?: AbortSignal;
|
|
11
12
|
}): AsyncIterable<AgentEnvironmentEvent>;
|
|
12
13
|
export declare function collectCliBridgeTurnResult(source: AsyncIterable<AgentEnvironmentEvent>, run: CliBridgeRun, options: CliBridgeProviderOptions, transport: CliBridgeTransport, signal?: AbortSignal): Promise<AgentTurnResult>;
|
|
14
|
+
/** Sum the usage a run reported across its events into one total. */
|
|
15
|
+
export declare function addTokenUsage(current: TokenUsage | undefined, next: TokenUsage | undefined): TokenUsage | undefined;
|
|
@@ -267,7 +267,8 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
|
|
|
267
267
|
events,
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
|
-
|
|
270
|
+
/** Sum the usage a run reported across its events into one total. */
|
|
271
|
+
export function addTokenUsage(current, next) {
|
|
271
272
|
if (!next)
|
|
272
273
|
return current;
|
|
273
274
|
if (!current)
|
package/dist/retained-stream.js
CHANGED
|
@@ -9,6 +9,14 @@ export class CliBridgeRequestRejectedError extends Error {
|
|
|
9
9
|
this.name = "CliBridgeRequestRejectedError";
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
class CliBridgeRunCancelledError extends Error {
|
|
13
|
+
detail;
|
|
14
|
+
constructor(detail) {
|
|
15
|
+
super(`cli-bridge run cancelled: ${detail}`);
|
|
16
|
+
this.detail = detail;
|
|
17
|
+
this.name = "CliBridgeRunCancelledError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
12
20
|
export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onAccepted) {
|
|
13
21
|
const reconnect = requestBody === undefined;
|
|
14
22
|
const replay = retainedReplayRequest(lastEventId);
|
|
@@ -53,23 +61,37 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
53
61
|
let textBoundaryPending = false;
|
|
54
62
|
let terminalCursor;
|
|
55
63
|
let sawProtocolEnd = false;
|
|
64
|
+
let terminalFrameSeen = false;
|
|
65
|
+
let cancelledTerminalSeen = false;
|
|
56
66
|
for await (const frame of parseSse(response.body)) {
|
|
57
67
|
if (frame.data === "[DONE]") {
|
|
58
68
|
sawProtocolEnd = true;
|
|
59
69
|
continue;
|
|
60
70
|
}
|
|
71
|
+
if (terminalFrameSeen)
|
|
72
|
+
continue;
|
|
61
73
|
const parsed = safeJson(frame.data);
|
|
62
74
|
if (!parsed)
|
|
63
75
|
continue;
|
|
64
76
|
if (parsed.error && typeof parsed.error === "object") {
|
|
65
77
|
const error = parsed.error;
|
|
66
78
|
const message = typeof error.message === "string" ? error.message : "cli-bridge error";
|
|
79
|
+
const cancelled = error.type === "run_cancelled";
|
|
67
80
|
yield* retainedEventsWithIdentity([{
|
|
68
81
|
type: "status",
|
|
69
|
-
data: { status: "failed", error: message },
|
|
70
|
-
normalized: {
|
|
82
|
+
data: { status: cancelled ? "cancelled" : "failed", error: message },
|
|
83
|
+
normalized: {
|
|
84
|
+
type: "status",
|
|
85
|
+
status: cancelled ? "cancelled" : "failed",
|
|
86
|
+
detail: message,
|
|
87
|
+
},
|
|
71
88
|
}], frame.id, runId, sessionId, turn.executionId, replay.anchor);
|
|
72
|
-
|
|
89
|
+
terminalFrameSeen = true;
|
|
90
|
+
completed = cancelled;
|
|
91
|
+
cancelledTerminalSeen = cancelled;
|
|
92
|
+
if (!cancelled)
|
|
93
|
+
throw new Error(`cli-bridge: ${message}`);
|
|
94
|
+
continue;
|
|
73
95
|
}
|
|
74
96
|
const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
|
|
75
97
|
const delta = choice?.delta;
|
|
@@ -161,6 +183,7 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
161
183
|
if (toolCallDeltas.length > 0)
|
|
162
184
|
textBoundaryPending = true;
|
|
163
185
|
if (choice?.finish_reason) {
|
|
186
|
+
terminalFrameSeen = true;
|
|
164
187
|
if (choice.finish_reason === "error") {
|
|
165
188
|
frameEvents.push({
|
|
166
189
|
type: "status",
|
|
@@ -201,8 +224,42 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
201
224
|
if (!completed && lastEventId === undefined) {
|
|
202
225
|
throw new Error("cli-bridge stream ended without a terminal result");
|
|
203
226
|
}
|
|
204
|
-
if (lastEventId && requestBody !== undefined) {
|
|
205
|
-
|
|
227
|
+
if (lastEventId && requestBody !== undefined && !cancelledTerminalSeen) {
|
|
228
|
+
let result;
|
|
229
|
+
try {
|
|
230
|
+
result = await readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error instanceof CliBridgeRunCancelledError) {
|
|
234
|
+
// A normal finish frame can race with the bridge's authoritative
|
|
235
|
+
// cancelled state. Replace the withheld result at the same cursor.
|
|
236
|
+
if (terminalCursor !== undefined) {
|
|
237
|
+
yield {
|
|
238
|
+
type: "status",
|
|
239
|
+
data: {
|
|
240
|
+
status: "cancelled",
|
|
241
|
+
error: error.detail,
|
|
242
|
+
runId,
|
|
243
|
+
sessionId,
|
|
244
|
+
...(turn.executionId === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: { executionId: turn.executionId }),
|
|
247
|
+
cursor: terminalCursor,
|
|
248
|
+
},
|
|
249
|
+
normalized: {
|
|
250
|
+
type: "status",
|
|
251
|
+
status: "cancelled",
|
|
252
|
+
detail: error.detail,
|
|
253
|
+
},
|
|
254
|
+
id: terminalCursor,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// If no new terminal frame followed the requested cursor, the caller
|
|
258
|
+
// already consumed the cancellation event. Do not duplicate it.
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
206
263
|
const finalModelRequests = result.modelRequests ??
|
|
207
264
|
(modelRequestsKnown ? observedModelRequests : undefined);
|
|
208
265
|
const finalCursor = terminalCursor ?? lastEventId;
|
|
@@ -240,14 +297,21 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal,
|
|
|
240
297
|
...(signal ? { signal } : {}),
|
|
241
298
|
});
|
|
242
299
|
if (!response.ok) {
|
|
243
|
-
|
|
300
|
+
const responseText = await response.text();
|
|
301
|
+
const error = cliBridgeError(safeJson(responseText));
|
|
302
|
+
if (error?.type === "run_cancelled") {
|
|
303
|
+
throw new CliBridgeRunCancelledError(cliBridgeErrorMessage(error));
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`cli-bridge replay result ${response.status}: ${responseText}`);
|
|
244
306
|
}
|
|
245
307
|
onAccepted?.(response);
|
|
246
308
|
const parsed = safeJson(await response.text());
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
309
|
+
const error = cliBridgeError(parsed);
|
|
310
|
+
if (error) {
|
|
311
|
+
if (error.type === "run_cancelled") {
|
|
312
|
+
throw new CliBridgeRunCancelledError(cliBridgeErrorMessage(error));
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`cli-bridge replay result failed: ${cliBridgeErrorMessage(error)}`);
|
|
251
315
|
}
|
|
252
316
|
const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : undefined;
|
|
253
317
|
const message = choice?.message && typeof choice.message === "object"
|
|
@@ -274,3 +338,12 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal,
|
|
|
274
338
|
: { system_fingerprint: responseIdentity.system_fingerprint }),
|
|
275
339
|
};
|
|
276
340
|
}
|
|
341
|
+
function cliBridgeError(parsed) {
|
|
342
|
+
const error = parsed?.error;
|
|
343
|
+
return error && typeof error === "object" && !Array.isArray(error)
|
|
344
|
+
? error
|
|
345
|
+
: undefined;
|
|
346
|
+
}
|
|
347
|
+
function cliBridgeErrorMessage(error) {
|
|
348
|
+
return typeof error.message === "string" ? error.message : "cli-bridge replay failed";
|
|
349
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-cli-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist/index.d.ts",
|
|
27
27
|
"dist/index.js",
|
|
28
|
+
"dist/observation.d.ts",
|
|
29
|
+
"dist/observation.js",
|
|
28
30
|
"dist/provider-options.d.ts",
|
|
29
31
|
"dist/provider-options.js",
|
|
30
32
|
"dist/retained-control.d.ts",
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
"LICENSE"
|
|
48
50
|
],
|
|
49
51
|
"dependencies": {
|
|
50
|
-
"@tangle-network/agent-interface": "0.
|
|
52
|
+
"@tangle-network/agent-interface": "0.52.0",
|
|
51
53
|
"undici": "7.29.0"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|