@juno-ai/bind 2.0.0 → 4.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 +1153 -60
- package/contracts/index.d.ts +1 -1
- package/contracts/index.js +1 -1
- package/contracts/turn.d.ts +31 -7
- package/contracts/turn.js +45 -0
- package/index.d.ts +16 -5
- package/index.js +16 -5
- package/loop/index.d.ts +1 -0
- package/loop/index.js +1 -0
- package/loop/tool-loop.d.ts +260 -0
- package/loop/tool-loop.js +276 -0
- package/package.json +22 -2
- package/plugins/activation.d.ts +67 -0
- package/plugins/activation.js +61 -0
- package/plugins/index.d.ts +3 -0
- package/plugins/index.js +3 -0
- package/plugins/registry.d.ts +52 -0
- package/plugins/registry.js +54 -0
- package/plugins/tool.d.ts +164 -0
- package/plugins/tool.js +9 -0
- package/routing/billing-basis.d.ts +48 -0
- package/routing/billing-basis.js +67 -0
- package/routing/circuit-breaker.d.ts +2 -2
- package/routing/errors.d.ts +1 -1
- package/routing/executor.d.ts +3 -3
- package/routing/executor.js +1 -1
- package/routing/index.d.ts +11 -9
- package/routing/index.js +11 -9
- package/routing/plan-degradation.d.ts +34 -0
- package/routing/plan-degradation.js +38 -0
- package/routing/plan.d.ts +2 -2
- package/routing/planner.d.ts +4 -4
- package/routing/planner.js +1 -1
- package/routing/policy.d.ts +1 -1
- package/routing/policy.js +1 -1
- package/routing/transport.d.ts +2 -2
- package/run/children.d.ts +204 -0
- package/run/children.js +226 -0
- package/run/harness.d.ts +94 -0
- package/run/harness.js +140 -0
- package/run/index.d.ts +3 -0
- package/run/index.js +3 -0
- package/run/tool-batch.d.ts +16 -0
- package/run/tool-batch.js +83 -0
- package/tools/index.d.ts +1 -0
- package/tools/index.js +1 -0
- package/tools/sanitize-schema.d.ts +150 -0
- package/tools/sanitize-schema.js +683 -0
- package/transcript/index.d.ts +1 -0
- package/transcript/index.js +1 -0
- package/transcript/validate.d.ts +54 -0
- package/transcript/validate.js +226 -0
package/contracts/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { emptyRunStats, accumulateTurn, accumulateToolCall, type TranscriptMessage, type AssistantTurnMessage, type WireToolDefinition, type WireToolCall, type TurnTimings, type TurnUsage, type ModelTurnResult, type TurnFn, type StopReason, type RunStats, } from "./turn";
|
|
1
|
+
export { emptyRunStats, accumulateTurn, accumulateToolCall, accumulateRun, type TranscriptMessage, type AssistantTurnMessage, type WireToolDefinition, type WireToolCall, type TurnTimings, type TurnUsage, type ModelTurnResult, type TurnFn, type StopReason, type RunStats, } from "./turn.js";
|
package/contracts/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { emptyRunStats, accumulateTurn, accumulateToolCall, } from "./turn";
|
|
1
|
+
export { emptyRunStats, accumulateTurn, accumulateToolCall, accumulateRun, } from "./turn.js";
|
package/contracts/turn.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
2
|
/**
|
|
3
|
-
* Turn vocabulary — the shared language between the turn kernel
|
|
4
|
-
*
|
|
3
|
+
* Turn vocabulary — the shared language between the turn kernel
|
|
4
|
+
* (`@juno-ai/bind/loop`), LLM transports, and hosts.
|
|
5
5
|
*
|
|
6
6
|
* The declared wire format is the OpenAI chat-completions message shape,
|
|
7
7
|
* consumed as **types only** (`openai` is a peer used purely for its type
|
|
8
8
|
* declarations here; no runtime import). Hosts on other client stacks (e.g.
|
|
9
9
|
* the Vercel AI SDK) adapt at the turn-function boundary.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The tool/plugin vocabulary is a separate contract and lives in `plugins/`,
|
|
12
|
+
* not here: this module is about what a turn *is* on the wire, while that one
|
|
13
|
+
* is about what a host can register and how the harness discloses it.
|
|
14
14
|
*/
|
|
15
15
|
export type TranscriptMessage = OpenAI.ChatCompletionMessageParam;
|
|
16
16
|
export type AssistantTurnMessage = OpenAI.ChatCompletionMessage;
|
|
@@ -18,8 +18,8 @@ export type WireToolDefinition = OpenAI.ChatCompletionTool;
|
|
|
18
18
|
export type WireToolCall = OpenAI.ChatCompletionMessageToolCall;
|
|
19
19
|
/**
|
|
20
20
|
* Per-turn latency/throughput measurements. Field shapes deliberately match
|
|
21
|
-
* the metrics the
|
|
22
|
-
* directly comparable with published benchmark methodology:
|
|
21
|
+
* the metrics the StirrupJS benchmark harness reports (`speedStats`), so
|
|
22
|
+
* numbers are directly comparable with published benchmark methodology:
|
|
23
23
|
* time-to-first-token, generation wall time, and output tokens/second —
|
|
24
24
|
* plus the model-time vs tool-time split that per-task wall-clock hides.
|
|
25
25
|
*/
|
|
@@ -77,3 +77,27 @@ export declare function emptyRunStats(): RunStats;
|
|
|
77
77
|
export declare function accumulateTurn(stats: RunStats, turn: ModelTurnResult): RunStats;
|
|
78
78
|
/** Fold one dispatched tool call's duration into cumulative run stats. */
|
|
79
79
|
export declare function accumulateToolCall(stats: RunStats, toolName: string, durationMs: number): RunStats;
|
|
80
|
+
/**
|
|
81
|
+
* Fold a completed run's totals into another run's — the roll-up for a chain
|
|
82
|
+
* that spawned child runs (`@juno-ai/bind/run`).
|
|
83
|
+
*
|
|
84
|
+
* Two things follow from summing across runs rather than within one, and both
|
|
85
|
+
* are correct rather than artifacts:
|
|
86
|
+
*
|
|
87
|
+
* - **`modelTimeMs` can exceed the chain's wall-clock**, because children that
|
|
88
|
+
* ran concurrently each contribute their own. That is precisely why model
|
|
89
|
+
* time and wall-clock are separate numbers; a chain's *cost* is the sum, its
|
|
90
|
+
* *latency* is not.
|
|
91
|
+
* - **`outputTokensPerSecond` is recomputed from the merged totals**, not
|
|
92
|
+
* averaged from the parts. An average of two rates weights a 10-token run
|
|
93
|
+
* the same as a 10,000-token one and reports a throughput neither run
|
|
94
|
+
* achieved.
|
|
95
|
+
*
|
|
96
|
+
* The fold is associative and order-independent, so a chain reduces cleanly in
|
|
97
|
+
* whatever order its children finish:
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* const chainTotals = childStats.reduce(accumulateRun, parentStats);
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function accumulateRun(stats: RunStats, run: RunStats): RunStats;
|
package/contracts/turn.js
CHANGED
|
@@ -37,3 +37,48 @@ export function accumulateToolCall(stats, toolName, durationMs) {
|
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Fold a completed run's totals into another run's — the roll-up for a chain
|
|
42
|
+
* that spawned child runs (`@juno-ai/bind/run`).
|
|
43
|
+
*
|
|
44
|
+
* Two things follow from summing across runs rather than within one, and both
|
|
45
|
+
* are correct rather than artifacts:
|
|
46
|
+
*
|
|
47
|
+
* - **`modelTimeMs` can exceed the chain's wall-clock**, because children that
|
|
48
|
+
* ran concurrently each contribute their own. That is precisely why model
|
|
49
|
+
* time and wall-clock are separate numbers; a chain's *cost* is the sum, its
|
|
50
|
+
* *latency* is not.
|
|
51
|
+
* - **`outputTokensPerSecond` is recomputed from the merged totals**, not
|
|
52
|
+
* averaged from the parts. An average of two rates weights a 10-token run
|
|
53
|
+
* the same as a 10,000-token one and reports a throughput neither run
|
|
54
|
+
* achieved.
|
|
55
|
+
*
|
|
56
|
+
* The fold is associative and order-independent, so a chain reduces cleanly in
|
|
57
|
+
* whatever order its children finish:
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* const chainTotals = childStats.reduce(accumulateRun, parentStats);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function accumulateRun(stats, run) {
|
|
64
|
+
const outputTokens = stats.outputTokens + run.outputTokens;
|
|
65
|
+
const modelTimeMs = stats.modelTimeMs + run.modelTimeMs;
|
|
66
|
+
const toolTimeBreakdownMs = {
|
|
67
|
+
...stats.toolTimeBreakdownMs,
|
|
68
|
+
};
|
|
69
|
+
for (const [toolName, durationMs] of Object.entries(run.toolTimeBreakdownMs)) {
|
|
70
|
+
toolTimeBreakdownMs[toolName] =
|
|
71
|
+
(toolTimeBreakdownMs[toolName] ?? 0) + durationMs;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
turns: stats.turns + run.turns,
|
|
75
|
+
toolCalls: stats.toolCalls + run.toolCalls,
|
|
76
|
+
inputTokens: stats.inputTokens + run.inputTokens,
|
|
77
|
+
outputTokens,
|
|
78
|
+
costCents: stats.costCents + run.costCents,
|
|
79
|
+
modelTimeMs,
|
|
80
|
+
toolTimeMs: stats.toolTimeMs + run.toolTimeMs,
|
|
81
|
+
outputTokensPerSecond: modelTimeMs > 0 ? (outputTokens / modelTimeMs) * 1000 : null,
|
|
82
|
+
toolTimeBreakdownMs,
|
|
83
|
+
};
|
|
84
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
* completion into tool effects into the next turn's context. This package is
|
|
6
6
|
* the harness that runs that chain.
|
|
7
7
|
*
|
|
8
|
-
* Current surface: the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Current surface: the tool-calling turn kernel (`src/loop/` — the iteration
|
|
9
|
+
* engine itself), the deterministic LLM provider-routing core, the turn
|
|
10
|
+
* vocabulary, the run mechanics (deadline, coalesced heartbeat, failure
|
|
11
|
+
* classification, tool-batch pooling, child-run lineage and admission),
|
|
12
|
+
* transcript validation/healing, provider tool-schema sanitization, and the
|
|
13
|
+
* plugin/tool vocabulary with its registry and progressive-disclosure
|
|
14
|
+
* activation — generic over the host's invocation context. What is NOT here is
|
|
15
|
+
* the run driver: starting a run, recording what it did, and delivering its
|
|
16
|
+
* output. See the README for the rest of what is deliberately absent.
|
|
11
17
|
*/
|
|
12
|
-
export * from "./routing/index";
|
|
13
|
-
export * from "./contracts/index";
|
|
18
|
+
export * from "./routing/index.js";
|
|
19
|
+
export * from "./contracts/index.js";
|
|
20
|
+
export * from "./run/index.js";
|
|
21
|
+
export * from "./transcript/index.js";
|
|
22
|
+
export * from "./tools/index.js";
|
|
23
|
+
export * from "./plugins/index.js";
|
|
24
|
+
export * from "./loop/index.js";
|
package/index.js
CHANGED
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
* completion into tool effects into the next turn's context. This package is
|
|
6
6
|
* the harness that runs that chain.
|
|
7
7
|
*
|
|
8
|
-
* Current surface: the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Current surface: the tool-calling turn kernel (`src/loop/` — the iteration
|
|
9
|
+
* engine itself), the deterministic LLM provider-routing core, the turn
|
|
10
|
+
* vocabulary, the run mechanics (deadline, coalesced heartbeat, failure
|
|
11
|
+
* classification, tool-batch pooling, child-run lineage and admission),
|
|
12
|
+
* transcript validation/healing, provider tool-schema sanitization, and the
|
|
13
|
+
* plugin/tool vocabulary with its registry and progressive-disclosure
|
|
14
|
+
* activation — generic over the host's invocation context. What is NOT here is
|
|
15
|
+
* the run driver: starting a run, recording what it did, and delivering its
|
|
16
|
+
* output. See the README for the rest of what is deliberately absent.
|
|
11
17
|
*/
|
|
12
|
-
export * from "./routing/index";
|
|
13
|
-
export * from "./contracts/index";
|
|
18
|
+
export * from "./routing/index.js";
|
|
19
|
+
export * from "./contracts/index.js";
|
|
20
|
+
export * from "./run/index.js";
|
|
21
|
+
export * from "./transcript/index.js";
|
|
22
|
+
export * from "./tools/index.js";
|
|
23
|
+
export * from "./plugins/index.js";
|
|
24
|
+
export * from "./loop/index.js";
|
package/loop/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runToolLoop, type ToolLoopParams, type ToolLoopState, type ToolLoopTurn, type ToolCallOutcome, type CompactionApplied, type RunStatus, } from "./tool-loop.js";
|
package/loop/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runToolLoop, } from "./tool-loop.js";
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
/**
|
|
3
|
+
* The agent iteration engine: call the model, run the tools it asked for,
|
|
4
|
+
* repeat until it stops asking. Everything that *happens* as a result — status
|
|
5
|
+
* updates, heartbeats, activity rows, persistence, cancellation — is injected,
|
|
6
|
+
* so the loop itself does no I/O and holds no host vocabulary.
|
|
7
|
+
*
|
|
8
|
+
* Three ports are worth understanding before wiring this up, because each
|
|
9
|
+
* replaced something the loop previously hardcoded:
|
|
10
|
+
*
|
|
11
|
+
* - **`runsSerially`** decides which calls in a batch must run one at a time,
|
|
12
|
+
* ahead of the rest. Not a performance knob: a call that changes what tools
|
|
13
|
+
* exist has to take effect before a later call in the same batch tries to
|
|
14
|
+
* use them.
|
|
15
|
+
* - **`isFatalToolError`** decides which thrown errors abort the run instead
|
|
16
|
+
* of becoming a tool error the model can read. Cancellation and "we failed
|
|
17
|
+
* to record the result" belong here; a tool that simply failed does not.
|
|
18
|
+
* - **`onToolCallRejected`** observes the errors that were synthesized rather
|
|
19
|
+
* than thrown. Without it they are invisible — the model sees them, your
|
|
20
|
+
* logs do not.
|
|
21
|
+
*
|
|
22
|
+
* Turn accounting is deliberately the flat usage the loop needs to run
|
|
23
|
+
* (`ToolLoopTurn`), not the richer `ModelTurnResult` in `@juno-ai/bind/contracts`
|
|
24
|
+
* with its timings. The two describe the same event at different resolutions
|
|
25
|
+
* and converge when the loop learns to accumulate `RunStats` directly; until
|
|
26
|
+
* then a host that wants throughput metrics folds them alongside.
|
|
27
|
+
*/
|
|
28
|
+
/** One model completion's message + the provider usage the loop accounts for. */
|
|
29
|
+
export interface ToolLoopTurn {
|
|
30
|
+
message: OpenAI.ChatCompletionMessage;
|
|
31
|
+
inputTokens: number;
|
|
32
|
+
outputTokens: number;
|
|
33
|
+
costCents: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Outcome of running one tool call inside an assistant `tool_calls` batch.
|
|
37
|
+
*
|
|
38
|
+
* Every optional field is a **control signal**: the loop branches on it. They
|
|
39
|
+
* are not host payload passing through — `loadedPluginName` grows the active
|
|
40
|
+
* set, `requestCompaction` triggers a compaction at the batch boundary, and
|
|
41
|
+
* `suspend` ends the run. A host's own per-call data belongs inside
|
|
42
|
+
* `toolMessage`, which the loop only appends.
|
|
43
|
+
*/
|
|
44
|
+
export type ToolCallOutcome = {
|
|
45
|
+
toolMessage: OpenAI.ChatCompletionToolMessageParam;
|
|
46
|
+
/**
|
|
47
|
+
* A plugin this call activated; the loop adds it to the active set at once —
|
|
48
|
+
* but ONLY from the serial phase. A call that reaches the concurrent phase
|
|
49
|
+
* has already missed its window (a later call in the same batch could
|
|
50
|
+
* already be running), so this field is ignored there rather than applied
|
|
51
|
+
* late. Return it from a call your `runsSerially` selects, or it is dropped.
|
|
52
|
+
*/
|
|
53
|
+
loadedPluginName?: string;
|
|
54
|
+
/** An instruction module this call loaded, passed to `activateSkills`. Serial phase only, as above. */
|
|
55
|
+
loadedSkillRef?: string;
|
|
56
|
+
/** Plugins the loaded module needs, activated before it. Serial phase only, as above. */
|
|
57
|
+
autoLoadedPlugins?: string[];
|
|
58
|
+
/** Compact the transcript at this batch's boundary. */
|
|
59
|
+
requestCompaction?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* End the run. `answer` records the open call and WITHHOLDS its tool message
|
|
62
|
+
* — the result is a human's future answer, threaded back on resume — so at
|
|
63
|
+
* most one may be open at a time; a second in the same batch is answered with
|
|
64
|
+
* an error rather than left unpaired. `wake` keeps its tool message and
|
|
65
|
+
* re-enters through a fresh prompt.
|
|
66
|
+
*/
|
|
67
|
+
suspend?: {
|
|
68
|
+
toolCallId: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
resumeKind: "answer" | "wake";
|
|
71
|
+
request?: unknown;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Result of compacting: the new transcript + the compaction call's own usage,
|
|
76
|
+
* plus a deferred `persist` step. The loop applies accounting + swaps the
|
|
77
|
+
* transcript BEFORE calling `persist`, so a persistence failure can't drop the
|
|
78
|
+
* tokens the compaction LLM call already consumed.
|
|
79
|
+
*/
|
|
80
|
+
export interface CompactionApplied {
|
|
81
|
+
messages: OpenAI.ChatCompletionMessageParam[];
|
|
82
|
+
inputTokens: number;
|
|
83
|
+
outputTokens: number;
|
|
84
|
+
costCents: number;
|
|
85
|
+
/** Persist the compacted session + activity row. Runs after accounting. */
|
|
86
|
+
persist: () => Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Mutable accumulator threaded through the loop. The caller seeds it and reads
|
|
90
|
+
* it back after; a shared object (rather than return values) lets the caller's
|
|
91
|
+
* coalesced heartbeat read live token totals mid-loop. `messages` is mutated in
|
|
92
|
+
* place (assistant + tool turns appended; replaced wholesale on compaction).
|
|
93
|
+
*/
|
|
94
|
+
export interface ToolLoopState {
|
|
95
|
+
messages: OpenAI.ChatCompletionMessageParam[];
|
|
96
|
+
inputTokens: number;
|
|
97
|
+
outputTokens: number;
|
|
98
|
+
costCents: number;
|
|
99
|
+
/** Provider prompt/completion tokens from the most recent turn (0 right after a compaction). */
|
|
100
|
+
lastPromptTokens: number;
|
|
101
|
+
lastOutputTokens: number;
|
|
102
|
+
/** True once any turn has produced a real provider count this run. */
|
|
103
|
+
hasFreshTokenCount: boolean;
|
|
104
|
+
/** Cumulative tool calls dispatched this run — the count only, never names or
|
|
105
|
+
* results. Feeds a live progress indicator via `onProgressUpdate`. */
|
|
106
|
+
toolCalls: number;
|
|
107
|
+
/** True when the loop ended because a tool suspended the run (asked a human a
|
|
108
|
+
* question, or scheduled its own resume). Distinguishes an intentional pause
|
|
109
|
+
* from an iteration-limit cutoff — without it a caller reports "couldn't
|
|
110
|
+
* finish" over a run that stopped exactly where it meant to. */
|
|
111
|
+
endedTurnViaTool?: boolean;
|
|
112
|
+
/** Set when a tool suspended the run awaiting an answer: the open tool-call is
|
|
113
|
+
* waiting on a human or an external system. The loop withholds that call's
|
|
114
|
+
* `tool` message — its result IS the future answer — and ends the run; a
|
|
115
|
+
* resume threads the answer back as the matching `role:"tool"` result. At
|
|
116
|
+
* most one may be open at a time; extras in the same batch are answered with
|
|
117
|
+
* a synthesized error so no second slot is left unpaired. */
|
|
118
|
+
suspended?: {
|
|
119
|
+
toolCallId: string;
|
|
120
|
+
reason: string;
|
|
121
|
+
resumeKind: "answer";
|
|
122
|
+
request?: unknown;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export type RunStatus = "thinking" | "thinking_with_tools" | "executing_tools";
|
|
126
|
+
export interface ToolLoopParams {
|
|
127
|
+
state: ToolLoopState;
|
|
128
|
+
/** Active plugin set — read to build tool defs, grown by `activatePlugins`. */
|
|
129
|
+
activePlugins: Set<string>;
|
|
130
|
+
maxIterations: number;
|
|
131
|
+
/** Call the model with the current transcript + tool defs. `onOutputProgress`
|
|
132
|
+
* (optional) receives a running estimate of THIS call's output tokens as the
|
|
133
|
+
* stream flows (throttled ~1 Hz inside the model call); the loop adds the
|
|
134
|
+
* prior cumulative before forwarding to `onProgressUpdate`. */
|
|
135
|
+
callModel: (messages: OpenAI.ChatCompletionMessageParam[], tools: OpenAI.ChatCompletionTool[] | undefined, onOutputProgress?: (estimatedOutputTokens: number) => void) => Promise<ToolLoopTurn>;
|
|
136
|
+
/** Build the tool definitions for the current active-plugin set (+ MCP). */
|
|
137
|
+
buildTools: () => OpenAI.ChatCompletionTool[];
|
|
138
|
+
/** Execute one tool call → the `tool` message + control signals. */
|
|
139
|
+
runToolCall: (toolCall: OpenAI.ChatCompletionMessageToolCall) => Promise<ToolCallOutcome>;
|
|
140
|
+
/** Activate newly loaded plugins (mutate the catalog/active set). */
|
|
141
|
+
activatePlugins: (pluginNames: string[]) => void;
|
|
142
|
+
/**
|
|
143
|
+
* Activate newly loaded skills: inject their bodies into the system prompt's
|
|
144
|
+
* instructions section and refresh the catalog. Async because a host may
|
|
145
|
+
* 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.
|
|
147
|
+
*/
|
|
148
|
+
activateSkills: (skillRefs: string[]) => Promise<void> | void;
|
|
149
|
+
/**
|
|
150
|
+
* Must this call run on its own, before the rest of its batch?
|
|
151
|
+
*
|
|
152
|
+
* True for any call that changes what the later calls can do — activating a
|
|
153
|
+
* plugin, loading an instruction module. The loop runs those one at a time
|
|
154
|
+
* and applies each outcome immediately, so a dependent call in the SAME batch
|
|
155
|
+
* sees the effect. Everything else fans out concurrently, pooled per tool
|
|
156
|
+
* name.
|
|
157
|
+
*
|
|
158
|
+
* Unwired, nothing is serial: every call runs in the concurrent phase, which
|
|
159
|
+
* is correct for a host whose tools do not reshape the tool surface.
|
|
160
|
+
*/
|
|
161
|
+
runsSerially?: (toolCall: OpenAI.ChatCompletionMessageToolCall) => boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Should this thrown error abort the run rather than become a tool error the
|
|
164
|
+
* model reads?
|
|
165
|
+
*
|
|
166
|
+
* Two things belong here: **cancellation**, which must propagate even when no
|
|
167
|
+
* `ensureNotCancelled` observer is wired, and a failure to *record* an
|
|
168
|
+
* outcome — synthesizing "the tool failed" over a persistence failure tells
|
|
169
|
+
* the model a lie about work that may well have happened.
|
|
170
|
+
*
|
|
171
|
+
* Unwired, nothing is fatal: every failure is synthesized into a tool error
|
|
172
|
+
* and the run continues. That is the safe default for an isolated tool, and
|
|
173
|
+
* the wrong one as soon as your tools write anything.
|
|
174
|
+
*/
|
|
175
|
+
isFatalToolError?: (error: unknown) => boolean;
|
|
176
|
+
/**
|
|
177
|
+
* A tool call failed and was answered with a synthesized error instead of
|
|
178
|
+
* throwing. The model sees it either way; without this observer nothing else
|
|
179
|
+
* does.
|
|
180
|
+
*/
|
|
181
|
+
onToolCallRejected?: (toolCallId: string, error: unknown) => void;
|
|
182
|
+
/** Throw to abort (run cancelled). Checked at the top of each iteration and after each batch. */
|
|
183
|
+
ensureNotCancelled?: () => Promise<void> | void;
|
|
184
|
+
/** Throw a timeout error if the wall-clock budget is exhausted (checked first each iteration). */
|
|
185
|
+
throwIfTimedOut?: () => void;
|
|
186
|
+
/** Phase status for rich client feedback. */
|
|
187
|
+
onStatus?: (status: RunStatus) => void;
|
|
188
|
+
/** Intermediate assistant text emitted alongside tool calls. NOTE: when
|
|
189
|
+
* `onAssistantMessage` is also wired, a text-with-tools message fires to BOTH
|
|
190
|
+
* observers (they have different contracts — see below). Wire only one per
|
|
191
|
+
* output surface so the same text isn't delivered twice. */
|
|
192
|
+
onThinking?: (content: string) => void;
|
|
193
|
+
/**
|
|
194
|
+
* Every assistant message that carries non-empty text content, fired the
|
|
195
|
+
* moment it's produced — whether or not it also requested tools, and including
|
|
196
|
+
* the final tool-less reply. Unlike `onThinking` (text-with-tools only), this
|
|
197
|
+
* sees ALL of a turn's assistant text, so a caller can stream each message to
|
|
198
|
+
* the client as it lands rather than batching them at turn end. Receives the
|
|
199
|
+
* already-trimmed content.
|
|
200
|
+
*/
|
|
201
|
+
onAssistantMessage?: (content: string) => void;
|
|
202
|
+
/** Flush accumulated progress (heartbeat). Called force-true after each turn. */
|
|
203
|
+
flushProgress?: () => Promise<void> | void;
|
|
204
|
+
/** Cumulative live progress: the running output-token count (updated
|
|
205
|
+
* mid-stream and after each model turn) and the number of tool calls made so
|
|
206
|
+
* far this run (the count only — never a name or a result). Called with the
|
|
207
|
+
* estimate during streaming and with real totals at each iteration boundary,
|
|
208
|
+
* and again right after a batch is dispatched so the count rises promptly. */
|
|
209
|
+
onProgressUpdate?: (outputTokens: number, toolCalls: number) => void;
|
|
210
|
+
/** Return true to stop the loop after the current turn (e.g. agent disabled mid-run). */
|
|
211
|
+
shouldStop?: () => Promise<boolean> | boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Called when the model produced a turn with NO tool calls — i.e. the loop is
|
|
214
|
+
* about to stop. Return a non-empty string to inject it as a synthetic `user`
|
|
215
|
+
* message and CONTINUE the loop instead of stopping; return null/empty to stop
|
|
216
|
+
* as normal (the default behavior when unwired). Use it to push a stalled turn
|
|
217
|
+
* forward — nudging an agent that ended a turn having made no progress at all.
|
|
218
|
+
* Receives the cumulative tool-call count so the hook can detect exactly that.
|
|
219
|
+
* MUST be self-bounding (eventually return null); `maxIterations` bounds it
|
|
220
|
+
* regardless, and the injected `user` message is model-only — a caller that
|
|
221
|
+
* persists a transcript should ignore `user` turns it didn't originate.
|
|
222
|
+
*
|
|
223
|
+
* `cumulativeToolCalls` is the running total across ALL iterations of this run
|
|
224
|
+
* (not just the current one), so a hook can detect "made zero tool calls the
|
|
225
|
+
* whole run" — a tool call in any earlier iteration makes it non-zero.
|
|
226
|
+
*/
|
|
227
|
+
onTurnWouldEnd?: (assistantMessage: OpenAI.ChatCompletionMessage, cumulativeToolCalls: number) => string | null | Promise<string | null>;
|
|
228
|
+
/** Drain queued human interrupts for this run. */
|
|
229
|
+
drainInterrupts?: () => Array<{
|
|
230
|
+
userId: string;
|
|
231
|
+
content: string;
|
|
232
|
+
}>;
|
|
233
|
+
/** Report that an interrupt was received (activity row). */
|
|
234
|
+
onInterruptReceived?: (interrupt: {
|
|
235
|
+
userId: string;
|
|
236
|
+
content: string;
|
|
237
|
+
}) => Promise<void> | void;
|
|
238
|
+
/** Decide whether the live context (token count) needs auto-compaction. */
|
|
239
|
+
needsCompaction?: (currentTokens: number) => boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Perform + persist a compaction and return the new transcript + usage. The
|
|
242
|
+
* loop owns swapping `state.messages` and resetting the live counts; this
|
|
243
|
+
* callback owns the LLM passes and persistence (session + activity row).
|
|
244
|
+
*/
|
|
245
|
+
applyCompaction?: (trigger: "manual" | "auto", messages: OpenAI.ChatCompletionMessageParam[]) => Promise<CompactionApplied>;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Repeatedly call the model and execute the tools it requests, until it stops
|
|
249
|
+
* requesting them, a caller stops the loop, a tool suspends the run, or
|
|
250
|
+
* `maxIterations` is reached.
|
|
251
|
+
*
|
|
252
|
+
* Intrinsic: plugin/module activation, two-phase tool batching, compaction
|
|
253
|
+
* (manual + auto), interrupt draining, and the suspend protocol. Injected:
|
|
254
|
+
* every side effect — status, heartbeat, activity, persistence, cancellation.
|
|
255
|
+
*
|
|
256
|
+
* Mutates `state` (messages + token accumulators) in place. That is deliberate
|
|
257
|
+
* rather than a return value: a caller's heartbeat reads live totals off it
|
|
258
|
+
* mid-loop, which a returned result could not provide until the run ended.
|
|
259
|
+
*/
|
|
260
|
+
export declare function runToolLoop(params: ToolLoopParams): Promise<void>;
|