@juno-ai/bind 8.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +405 -58
- package/completion/index.d.ts +1 -0
- package/completion/index.js +1 -0
- package/completion/text-stream.d.ts +311 -0
- package/completion/text-stream.js +273 -0
- package/contracts/index.d.ts +1 -1
- package/contracts/index.js +1 -1
- package/contracts/turn.d.ts +77 -2
- package/contracts/turn.js +35 -2
- package/loop/index.d.ts +2 -1
- package/loop/index.js +1 -1
- package/loop/tool-loop.d.ts +131 -12
- package/loop/tool-loop.js +285 -49
- package/package.json +5 -1
- package/plugins/dispatch.d.ts +130 -0
- package/plugins/dispatch.js +241 -0
- package/plugins/index.d.ts +2 -0
- package/plugins/index.js +2 -0
- package/plugins/tool-message.d.ts +23 -0
- package/plugins/tool-message.js +31 -0
- package/plugins/tool.d.ts +1 -1
- package/run/index.d.ts +1 -1
- package/run/index.js +1 -1
- package/run/tool-batch.d.ts +51 -1
- package/run/tool-batch.js +59 -1
- package/testing/index.d.ts +153 -0
- package/testing/index.js +188 -0
- package/tools/control-chars.d.ts +23 -0
- package/tools/control-chars.js +35 -0
package/contracts/turn.d.ts
CHANGED
|
@@ -50,18 +50,67 @@ export interface ModelTurnResult {
|
|
|
50
50
|
* completed assistant turn.
|
|
51
51
|
*/
|
|
52
52
|
export type TurnFn = (messages: readonly TranscriptMessage[], tools: readonly WireToolDefinition[] | undefined, signal: AbortSignal | undefined) => Promise<ModelTurnResult>;
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Why a run stopped — the single vocabulary a host reports an outcome in,
|
|
55
|
+
* instead of re-deriving it from a handful of loop-state flags.
|
|
56
|
+
*
|
|
57
|
+
* `runToolLoop` returns five of the six directly:
|
|
58
|
+
*
|
|
59
|
+
* - `done` — the model produced a turn with no tool calls (and no
|
|
60
|
+
* `onTurnWouldEnd` nudge pushed it forward).
|
|
61
|
+
* - `waiting_for_reply` — a tool asked a human a question and the run is
|
|
62
|
+
* blocked until someone answers. **Nothing happens until they do.**
|
|
63
|
+
* - `resuming_later` — a tool scheduled its own resume (a sleep, a timer).
|
|
64
|
+
* The run paused on purpose and will come back by itself.
|
|
65
|
+
* - `iteration_limit` — `maxIterations` was exhausted with the model still
|
|
66
|
+
* calling tools. The run did not finish; it was cut off.
|
|
67
|
+
* - `aborted` — the batch `signal` fired, or `shouldStop` asked to stop.
|
|
68
|
+
*
|
|
69
|
+
* The two pause reasons are split rather than one `suspended` because they are
|
|
70
|
+
* the outcomes whose consequences differ most: one needs a person to act, the
|
|
71
|
+
* other needs no one to do anything. Collapsed into a single value, a host
|
|
72
|
+
* that wanted to tell them apart had to go back and read `state.suspended` —
|
|
73
|
+
* which is the loop-state-flag reconstruction this type exists to replace.
|
|
74
|
+
*
|
|
75
|
+
* `deadline` is the one the loop does not return, and deliberately so: the
|
|
76
|
+
* wall-clock budget is a **throw-based** port (`throwIfTimedOut`), so an
|
|
77
|
+
* expired budget leaves the loop as a `RunTimeoutError` rather than a value.
|
|
78
|
+
* A host maps its catch block onto this vocabulary — `classifyRunFailure`
|
|
79
|
+
* from `@juno-ai/bind/run` recognises the same condition as `"timed_out"`.
|
|
80
|
+
* Returning `aborted` for an expired deadline would be worse than not
|
|
81
|
+
* returning it at all: the loop genuinely cannot distinguish a deadline signal
|
|
82
|
+
* from a cancellation signal once both are combined into one `AbortSignal`.
|
|
83
|
+
*/
|
|
84
|
+
export type StopReason = "done" | "waiting_for_reply" | "resuming_later" | "iteration_limit" | "deadline" | "aborted";
|
|
55
85
|
/**
|
|
56
86
|
* Cumulative run accounting: model-time and tool-time reported separately —
|
|
57
87
|
* task wall-clock conflates provider inference speed with tool execution,
|
|
58
88
|
* and consumers comparing models need the model's contribution isolated.
|
|
59
89
|
*/
|
|
60
90
|
export interface RunStats {
|
|
91
|
+
/**
|
|
92
|
+
* Agent turns — one per model completion the loop iterated on. Auxiliary
|
|
93
|
+
* model calls (a compaction pass) contribute their tokens, cost and model
|
|
94
|
+
* time but NOT a turn, so this stays comparable with the loop's iteration
|
|
95
|
+
* budget. See {@link accumulateAuxiliarySpend}.
|
|
96
|
+
*/
|
|
61
97
|
readonly turns: number;
|
|
98
|
+
/**
|
|
99
|
+
* Tool calls that were actually **dispatched**. A call the batch refused at
|
|
100
|
+
* claim time (an aborted `signal`) never ran, so it is not counted here even
|
|
101
|
+
* though the model requested it — the gap between this and the requested
|
|
102
|
+
* count is exactly the work an abort prevented.
|
|
103
|
+
*/
|
|
62
104
|
readonly toolCalls: number;
|
|
63
105
|
readonly inputTokens: number;
|
|
64
106
|
readonly outputTokens: number;
|
|
107
|
+
/**
|
|
108
|
+
* Provider-reported cached input tokens, summed across turns. Zero is
|
|
109
|
+
* indistinguishable from "the transport could not report it" — a turn whose
|
|
110
|
+
* `cachedInputTokens` is `null` contributes nothing rather than poisoning the
|
|
111
|
+
* total, so read this as a floor.
|
|
112
|
+
*/
|
|
113
|
+
readonly cachedInputTokens: number;
|
|
65
114
|
readonly costCents: number;
|
|
66
115
|
/** Sum of model `generationMs` across turns. */
|
|
67
116
|
readonly modelTimeMs: number;
|
|
@@ -75,6 +124,32 @@ export interface RunStats {
|
|
|
75
124
|
export declare function emptyRunStats(): RunStats;
|
|
76
125
|
/** Fold one completed model turn into cumulative run stats. */
|
|
77
126
|
export declare function accumulateTurn(stats: RunStats, turn: ModelTurnResult): RunStats;
|
|
127
|
+
/**
|
|
128
|
+
* Model spend that is not an agent turn — today, a compaction pass.
|
|
129
|
+
*
|
|
130
|
+
* Split out rather than folded through {@link accumulateTurn} because the two
|
|
131
|
+
* numbers answer different questions. A compaction is a real model call that
|
|
132
|
+
* costs real money and real latency, so its tokens, cost and time belong in the
|
|
133
|
+
* run's totals; it is *not* an iteration the agent spent making progress, so
|
|
134
|
+
* counting it in `turns` would make `stats.turns` incomparable with the loop's
|
|
135
|
+
* `maxIterations` and quietly overstate how much thinking the agent did.
|
|
136
|
+
*/
|
|
137
|
+
export interface AuxiliarySpend {
|
|
138
|
+
readonly inputTokens: number;
|
|
139
|
+
readonly outputTokens: number;
|
|
140
|
+
/**
|
|
141
|
+
* Required, unlike the two below, because a caller that cannot price a call
|
|
142
|
+
* still knows it cost *something* and should pass `0` deliberately rather
|
|
143
|
+
* than omit it. Time and cache figures are genuinely unknowable to some
|
|
144
|
+
* callers, so they are optional and contribute nothing when absent.
|
|
145
|
+
*/
|
|
146
|
+
readonly costCents: number;
|
|
147
|
+
/** Wall time of the auxiliary model call, if measured. */
|
|
148
|
+
readonly modelTimeMs?: number;
|
|
149
|
+
readonly cachedInputTokens?: number | null;
|
|
150
|
+
}
|
|
151
|
+
/** Fold auxiliary model spend into a run's totals without counting a turn. */
|
|
152
|
+
export declare function accumulateAuxiliarySpend(stats: RunStats, spend: AuxiliarySpend): RunStats;
|
|
78
153
|
/** Fold one dispatched tool call's duration into cumulative run stats. */
|
|
79
154
|
export declare function accumulateToolCall(stats: RunStats, toolName: string, durationMs: number): RunStats;
|
|
80
155
|
/**
|
package/contracts/turn.js
CHANGED
|
@@ -4,6 +4,7 @@ export function emptyRunStats() {
|
|
|
4
4
|
toolCalls: 0,
|
|
5
5
|
inputTokens: 0,
|
|
6
6
|
outputTokens: 0,
|
|
7
|
+
cachedInputTokens: 0,
|
|
7
8
|
costCents: 0,
|
|
8
9
|
modelTimeMs: 0,
|
|
9
10
|
toolTimeMs: 0,
|
|
@@ -20,11 +21,41 @@ export function accumulateTurn(stats, turn) {
|
|
|
20
21
|
turns: stats.turns + 1,
|
|
21
22
|
inputTokens: stats.inputTokens + turn.usage.inputTokens,
|
|
22
23
|
outputTokens,
|
|
24
|
+
cachedInputTokens: stats.cachedInputTokens + (turn.usage.cachedInputTokens ?? 0),
|
|
23
25
|
costCents: stats.costCents + (turn.usage.costCents ?? 0),
|
|
24
26
|
modelTimeMs,
|
|
25
27
|
outputTokensPerSecond: modelTimeMs > 0 ? (outputTokens / modelTimeMs) * 1000 : null,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
30
|
+
/** Fold auxiliary model spend into a run's totals without counting a turn. */
|
|
31
|
+
export function accumulateAuxiliarySpend(stats, spend) {
|
|
32
|
+
const outputTokens = stats.outputTokens + spend.outputTokens;
|
|
33
|
+
const modelTimeMs = stats.modelTimeMs + (spend.modelTimeMs ?? 0);
|
|
34
|
+
return {
|
|
35
|
+
...stats,
|
|
36
|
+
inputTokens: stats.inputTokens + spend.inputTokens,
|
|
37
|
+
outputTokens,
|
|
38
|
+
cachedInputTokens: stats.cachedInputTokens + (spend.cachedInputTokens ?? 0),
|
|
39
|
+
costCents: stats.costCents + spend.costCents,
|
|
40
|
+
modelTimeMs,
|
|
41
|
+
// Recomputed, not carried: the added output tokens and model time both move
|
|
42
|
+
// the rate, and leaving the old value would report a throughput that
|
|
43
|
+
// matches neither the turns nor the totals now stored beside it.
|
|
44
|
+
outputTokensPerSecond: modelTimeMs > 0 ? (outputTokens / modelTimeMs) * 1000 : null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read a tool's accumulated time without going through `Object.prototype`.
|
|
49
|
+
*
|
|
50
|
+
* A bare `breakdown[name] ?? 0` reads inherited properties, so a tool legally
|
|
51
|
+
* named `constructor` or `toString` returns a *function*, and `fn + duration`
|
|
52
|
+
* silently produces a string — a corrupted `toolTimeBreakdownMs` entry that
|
|
53
|
+
* typechecks as `number`. Tool names come from the model, so this is reachable
|
|
54
|
+
* on any run. Same guard the plugin registry already applies to alias lookups.
|
|
55
|
+
*/
|
|
56
|
+
function ownDuration(breakdown, toolName) {
|
|
57
|
+
return Object.hasOwn(breakdown, toolName) ? breakdown[toolName] : 0;
|
|
58
|
+
}
|
|
28
59
|
/** Fold one dispatched tool call's duration into cumulative run stats. */
|
|
29
60
|
export function accumulateToolCall(stats, toolName, durationMs) {
|
|
30
61
|
return {
|
|
@@ -33,7 +64,7 @@ export function accumulateToolCall(stats, toolName, durationMs) {
|
|
|
33
64
|
toolTimeMs: stats.toolTimeMs + durationMs,
|
|
34
65
|
toolTimeBreakdownMs: {
|
|
35
66
|
...stats.toolTimeBreakdownMs,
|
|
36
|
-
[toolName]: (stats.toolTimeBreakdownMs
|
|
67
|
+
[toolName]: ownDuration(stats.toolTimeBreakdownMs, toolName) + durationMs,
|
|
37
68
|
},
|
|
38
69
|
};
|
|
39
70
|
}
|
|
@@ -67,14 +98,16 @@ export function accumulateRun(stats, run) {
|
|
|
67
98
|
...stats.toolTimeBreakdownMs,
|
|
68
99
|
};
|
|
69
100
|
for (const [toolName, durationMs] of Object.entries(run.toolTimeBreakdownMs)) {
|
|
101
|
+
// Own-property read, for the same reason as `accumulateToolCall`.
|
|
70
102
|
toolTimeBreakdownMs[toolName] =
|
|
71
|
-
(toolTimeBreakdownMs
|
|
103
|
+
ownDuration(toolTimeBreakdownMs, toolName) + durationMs;
|
|
72
104
|
}
|
|
73
105
|
return {
|
|
74
106
|
turns: stats.turns + run.turns,
|
|
75
107
|
toolCalls: stats.toolCalls + run.toolCalls,
|
|
76
108
|
inputTokens: stats.inputTokens + run.inputTokens,
|
|
77
109
|
outputTokens,
|
|
110
|
+
cachedInputTokens: stats.cachedInputTokens + run.cachedInputTokens,
|
|
78
111
|
costCents: stats.costCents + run.costCents,
|
|
79
112
|
modelTimeMs,
|
|
80
113
|
toolTimeMs: stats.toolTimeMs + run.toolTimeMs,
|
package/loop/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type { StopReason, RunStats } from "../contracts/turn.js";
|
|
2
|
+
export { runToolLoop, MissingActivationPortError, type ToolLoopParams, type ToolLoopResult, type ToolLoopState, type ToolLoopTurn, type ToolCallOutcome, type CompactionApplied, type RunStatus, } from "./tool-loop.js";
|
package/loop/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { runToolLoop, } from "./tool-loop.js";
|
|
1
|
+
export { runToolLoop, MissingActivationPortError, } from "./tool-loop.js";
|
package/loop/tool-loop.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
|
+
import { type RunStats, type StopReason } from "../contracts/turn.js";
|
|
2
3
|
/**
|
|
3
4
|
* The agent iteration engine: call the model, run the tools it asked for,
|
|
4
5
|
* repeat until it stops asking. Everything that *happens* as a result — status
|
|
@@ -19,11 +20,13 @@ import type OpenAI from "openai";
|
|
|
19
20
|
* than thrown. Without it they are invisible — the model sees them, your
|
|
20
21
|
* logs do not.
|
|
21
22
|
*
|
|
22
|
-
* Turn accounting is
|
|
23
|
-
* (`ToolLoopTurn`), not the richer `ModelTurnResult` in
|
|
24
|
-
* with its timings. The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Turn accounting is the flat usage a host's `callModel` returns
|
|
24
|
+
* (`ToolLoopTurn`), not the richer `ModelTurnResult` in
|
|
25
|
+
* `@juno-ai/bind/contracts` with its timings. The loop bridges them: it
|
|
26
|
+
* measures each `callModel` with an injectable `now` and folds the pair
|
|
27
|
+
* through `accumulateTurn`, so `runToolLoop` returns a full `RunStats`
|
|
28
|
+
* without a host changing the shape it already returns. `ttftMs` is the one
|
|
29
|
+
* field that cannot cross — only the transport sees the first byte.
|
|
27
30
|
*/
|
|
28
31
|
/** One model completion's message + the provider usage the loop accounts for. */
|
|
29
32
|
export interface ToolLoopTurn {
|
|
@@ -31,6 +34,12 @@ export interface ToolLoopTurn {
|
|
|
31
34
|
inputTokens: number;
|
|
32
35
|
outputTokens: number;
|
|
33
36
|
costCents: number;
|
|
37
|
+
/**
|
|
38
|
+
* Provider-reported cached input tokens, when the transport can report them.
|
|
39
|
+
* Optional — omit it (or pass `null`) and the run's `cachedInputTokens` total
|
|
40
|
+
* simply does not count this turn, rather than counting it as a zero.
|
|
41
|
+
*/
|
|
42
|
+
cachedInputTokens?: number | null;
|
|
34
43
|
}
|
|
35
44
|
/**
|
|
36
45
|
* Outcome of running one tool call inside an assistant `tool_calls` batch.
|
|
@@ -82,6 +91,8 @@ export interface CompactionApplied {
|
|
|
82
91
|
inputTokens: number;
|
|
83
92
|
outputTokens: number;
|
|
84
93
|
costCents: number;
|
|
94
|
+
/** Provider-reported cached input tokens for the compaction call, if known. */
|
|
95
|
+
cachedInputTokens?: number | null;
|
|
85
96
|
/** Persist the compacted session + activity row. Runs after accounting. */
|
|
86
97
|
persist: () => Promise<void>;
|
|
87
98
|
}
|
|
@@ -123,10 +134,79 @@ export interface ToolLoopState {
|
|
|
123
134
|
};
|
|
124
135
|
}
|
|
125
136
|
export type RunStatus = "thinking" | "thinking_with_tools" | "executing_tools";
|
|
137
|
+
/**
|
|
138
|
+
* A tool asked the loop to activate a plugin or an instruction module, and the
|
|
139
|
+
* port that would do it was not wired.
|
|
140
|
+
*
|
|
141
|
+
* Reported through `onToolCallRejected` rather than thrown: the call itself
|
|
142
|
+
* succeeded and its tool message is already correct, so failing the run would
|
|
143
|
+
* be worse than the missing activation. But it must not be silent — before
|
|
144
|
+
* these ports were optional this was a compile error, and the runtime symptom
|
|
145
|
+
* (an agent that keeps loading a plugin it never receives) points nowhere near
|
|
146
|
+
* the cause.
|
|
147
|
+
*
|
|
148
|
+
* Match on `error.name === "MissingActivationPortError"` rather than
|
|
149
|
+
* `instanceof` if you consume this package from a projected or re-bundled copy
|
|
150
|
+
* — two copies of a class in one module graph make `instanceof` silently
|
|
151
|
+
* false, and this package is Copybara-projected and republished.
|
|
152
|
+
*/
|
|
153
|
+
export declare class MissingActivationPortError extends Error {
|
|
154
|
+
readonly port: "activatePlugins" | "activateSkills";
|
|
155
|
+
readonly name = "MissingActivationPortError";
|
|
156
|
+
constructor(port: "activatePlugins" | "activateSkills");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* What a completed loop reports back.
|
|
160
|
+
*
|
|
161
|
+
* Returned rather than folded into `ToolLoopState` because these are the run's
|
|
162
|
+
* *conclusion*, not its live progress: a caller reads `state` mid-run for a
|
|
163
|
+
* heartbeat, and reads this once, after. Before it existed every host
|
|
164
|
+
* re-derived the outcome from three flags and an iteration count, and each got
|
|
165
|
+
* a slightly different answer.
|
|
166
|
+
*/
|
|
167
|
+
export interface ToolLoopResult {
|
|
168
|
+
/**
|
|
169
|
+
* Why the loop stopped. See {@link StopReason} — `deadline` arrives as a
|
|
170
|
+
* thrown error rather than a value, and is never returned here.
|
|
171
|
+
*
|
|
172
|
+
* `aborted` means only "the batch signal was set, or `shouldStop` said so".
|
|
173
|
+
* It does not say *which*, because a combined signal cannot. A host that
|
|
174
|
+
* needs timeout-vs-cancellation reaches for its own `RunDeadline` (and
|
|
175
|
+
* `classifyRunFailure`), not for this value.
|
|
176
|
+
*/
|
|
177
|
+
readonly stopReason: StopReason;
|
|
178
|
+
/**
|
|
179
|
+
* Cumulative accounting for the run: turns, dispatched tool calls, tokens
|
|
180
|
+
* (including provider-reported cached input), cost, and the model-time vs
|
|
181
|
+
* tool-time split with a per-tool breakdown.
|
|
182
|
+
*
|
|
183
|
+
* **`stats` is this invocation's contribution alone**; `state` is whatever
|
|
184
|
+
* the caller seeded plus that. They match only when the caller seeded zeros
|
|
185
|
+
* — a host resuming a run seeds `state` from the stored totals, and then
|
|
186
|
+
* `state.costCents` is the run's lifetime cost while `stats.costCents` is
|
|
187
|
+
* this leg's. Bill from whichever you mean, and do not substitute one for
|
|
188
|
+
* the other.
|
|
189
|
+
*
|
|
190
|
+
* They also differ on tool calls on purpose: `state.toolCalls` counts what
|
|
191
|
+
* the model *requested* (it drives a live progress indicator, so it has to
|
|
192
|
+
* rise the moment a batch is dispatched), while `stats.toolCalls` counts
|
|
193
|
+
* what actually *ran*. An aborted batch is exactly the gap between them.
|
|
194
|
+
*
|
|
195
|
+
* Both are lost if the loop throws — a deadline, a cancellation, or a fatal
|
|
196
|
+
* tool error leaves no return value, so `state` (which is mutated in place)
|
|
197
|
+
* is the only accounting that survives those exits.
|
|
198
|
+
*/
|
|
199
|
+
readonly stats: RunStats;
|
|
200
|
+
}
|
|
126
201
|
export interface ToolLoopParams {
|
|
127
202
|
state: ToolLoopState;
|
|
128
|
-
/**
|
|
129
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Active plugin set. The loop does not read it — `buildTools` and
|
|
205
|
+
* `activatePlugins` are the host's own closures over it — so it is optional
|
|
206
|
+
* and passing one is purely a convenience for a host that likes threading it
|
|
207
|
+
* through explicitly.
|
|
208
|
+
*/
|
|
209
|
+
activePlugins?: Set<string>;
|
|
130
210
|
maxIterations: number;
|
|
131
211
|
/** Call the model with the current transcript + tool defs. `onOutputProgress`
|
|
132
212
|
* (optional) receives a running estimate of THIS call's output tokens as the
|
|
@@ -137,15 +217,38 @@ export interface ToolLoopParams {
|
|
|
137
217
|
buildTools: () => OpenAI.ChatCompletionTool[];
|
|
138
218
|
/** Execute one tool call → the `tool` message + control signals. */
|
|
139
219
|
runToolCall: (toolCall: OpenAI.ChatCompletionMessageToolCall) => Promise<ToolCallOutcome>;
|
|
140
|
-
/**
|
|
141
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Activate newly loaded plugins (mutate the catalog/active set). Optional:
|
|
222
|
+
* a host with a fixed tool surface has nothing to activate, and requiring an
|
|
223
|
+
* empty function from it bought nothing.
|
|
224
|
+
*/
|
|
225
|
+
activatePlugins?: (pluginNames: string[]) => void;
|
|
142
226
|
/**
|
|
143
227
|
* Activate newly loaded skills: inject their bodies into the system prompt's
|
|
144
228
|
* instructions section and refresh the catalog. Async because a host may
|
|
145
229
|
* re-read the module body from storage. Expected to no-op for a ref the agent
|
|
146
|
-
* cannot access — the loop does not pre-validate them.
|
|
230
|
+
* cannot access — the loop does not pre-validate them. Optional, as above.
|
|
147
231
|
*/
|
|
148
|
-
activateSkills
|
|
232
|
+
activateSkills?: (skillRefs: string[]) => Promise<void> | void;
|
|
233
|
+
/**
|
|
234
|
+
* Clock for the model-time and tool-time measurements in
|
|
235
|
+
* {@link ToolLoopResult.stats}. Defaults to `Date.now`, the package's one
|
|
236
|
+
* sanctioned ambient-clock exception; inject a fake to make timing
|
|
237
|
+
* assertions deterministic.
|
|
238
|
+
*
|
|
239
|
+
* **Must be a real monotonic clock when tools can run concurrently.** Each
|
|
240
|
+
* call records `now()` at dispatch and again when it settles, so a shared
|
|
241
|
+
* counter that only advances when some *other* call asks it to will charge
|
|
242
|
+
* one tool for another's time. A cooperatively-advanced fake is fine for a
|
|
243
|
+
* serial batch (`runsSerially`), and fine for the model-time figures always.
|
|
244
|
+
*
|
|
245
|
+
* Measured *around* `callModel`, so the number includes whatever that
|
|
246
|
+
* function does internally — a defect retry, a fallback provider, a routing
|
|
247
|
+
* hop. That is the honest figure for cost-and-latency accounting: it is what
|
|
248
|
+
* the turn actually took. A host that wants the successful attempt's
|
|
249
|
+
* generation time alone already has it, inside its own transport.
|
|
250
|
+
*/
|
|
251
|
+
now?: () => number;
|
|
149
252
|
/**
|
|
150
253
|
* Must this call run on its own, before the rest of its batch?
|
|
151
254
|
*
|
|
@@ -235,6 +338,20 @@ export interface ToolLoopParams {
|
|
|
235
338
|
userId: string;
|
|
236
339
|
content: string;
|
|
237
340
|
}) => Promise<void> | void;
|
|
341
|
+
/**
|
|
342
|
+
* Bounds the tool batch. Combine the run's deadline with any cancellation
|
|
343
|
+
* signal (`deadline.withExternal(cancelSignal)`) and pass the result.
|
|
344
|
+
*
|
|
345
|
+
* Without it, `throwIfTimedOut` and `ensureNotCancelled` are only consulted
|
|
346
|
+
* between iterations, so a deadline that fires while the model is being
|
|
347
|
+
* called still lets the whole batch execute its side effects, and a hung tool
|
|
348
|
+
* holds the run open for as long as it runs. Those are throw-based ports and
|
|
349
|
+
* cannot express "stop claiming new work" to a pool already in flight — only
|
|
350
|
+
* a signal can.
|
|
351
|
+
*
|
|
352
|
+
* Optional so an existing host is unchanged until it opts in.
|
|
353
|
+
*/
|
|
354
|
+
signal?: AbortSignal | undefined;
|
|
238
355
|
/** Decide whether the live context (token count) needs auto-compaction. */
|
|
239
356
|
needsCompaction?: (currentTokens: number) => boolean;
|
|
240
357
|
/**
|
|
@@ -256,5 +373,7 @@ export interface ToolLoopParams {
|
|
|
256
373
|
* Mutates `state` (messages + token accumulators) in place. That is deliberate
|
|
257
374
|
* rather than a return value: a caller's heartbeat reads live totals off it
|
|
258
375
|
* mid-loop, which a returned result could not provide until the run ended.
|
|
376
|
+
* The {@link ToolLoopResult} it *returns* is the complementary half — the
|
|
377
|
+
* run's conclusion, which only exists once the loop is over.
|
|
259
378
|
*/
|
|
260
|
-
export declare function runToolLoop(params: ToolLoopParams): Promise<
|
|
379
|
+
export declare function runToolLoop(params: ToolLoopParams): Promise<ToolLoopResult>;
|