@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.
Files changed (52) hide show
  1. package/README.md +1153 -60
  2. package/contracts/index.d.ts +1 -1
  3. package/contracts/index.js +1 -1
  4. package/contracts/turn.d.ts +31 -7
  5. package/contracts/turn.js +45 -0
  6. package/index.d.ts +16 -5
  7. package/index.js +16 -5
  8. package/loop/index.d.ts +1 -0
  9. package/loop/index.js +1 -0
  10. package/loop/tool-loop.d.ts +260 -0
  11. package/loop/tool-loop.js +276 -0
  12. package/package.json +22 -2
  13. package/plugins/activation.d.ts +67 -0
  14. package/plugins/activation.js +61 -0
  15. package/plugins/index.d.ts +3 -0
  16. package/plugins/index.js +3 -0
  17. package/plugins/registry.d.ts +52 -0
  18. package/plugins/registry.js +54 -0
  19. package/plugins/tool.d.ts +164 -0
  20. package/plugins/tool.js +9 -0
  21. package/routing/billing-basis.d.ts +48 -0
  22. package/routing/billing-basis.js +67 -0
  23. package/routing/circuit-breaker.d.ts +2 -2
  24. package/routing/errors.d.ts +1 -1
  25. package/routing/executor.d.ts +3 -3
  26. package/routing/executor.js +1 -1
  27. package/routing/index.d.ts +11 -9
  28. package/routing/index.js +11 -9
  29. package/routing/plan-degradation.d.ts +34 -0
  30. package/routing/plan-degradation.js +38 -0
  31. package/routing/plan.d.ts +2 -2
  32. package/routing/planner.d.ts +4 -4
  33. package/routing/planner.js +1 -1
  34. package/routing/policy.d.ts +1 -1
  35. package/routing/policy.js +1 -1
  36. package/routing/transport.d.ts +2 -2
  37. package/run/children.d.ts +204 -0
  38. package/run/children.js +226 -0
  39. package/run/harness.d.ts +94 -0
  40. package/run/harness.js +140 -0
  41. package/run/index.d.ts +3 -0
  42. package/run/index.js +3 -0
  43. package/run/tool-batch.d.ts +16 -0
  44. package/run/tool-batch.js +83 -0
  45. package/tools/index.d.ts +1 -0
  46. package/tools/index.js +1 -0
  47. package/tools/sanitize-schema.d.ts +150 -0
  48. package/tools/sanitize-schema.js +683 -0
  49. package/transcript/index.d.ts +1 -0
  50. package/transcript/index.js +1 -0
  51. package/transcript/validate.d.ts +54 -0
  52. package/transcript/validate.js +226 -0
@@ -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";
@@ -1 +1 @@
1
- export { emptyRunStats, accumulateTurn, accumulateToolCall, } from "./turn";
1
+ export { emptyRunStats, accumulateTurn, accumulateToolCall, accumulateRun, } from "./turn.js";
@@ -1,16 +1,16 @@
1
1
  import type OpenAI from "openai";
2
2
  /**
3
- * Turn vocabulary — the shared language between the turn kernel (arriving in
4
- * a later extraction phase), LLM transports, and hosts.
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
- * NOTE: `ToolPlugin` / tool-context contracts deliberately do NOT live here
12
- * yet they are being reshaped by the single-agent consolidation work in
13
- * the host repo and move here once that lands.
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 AA/StirrupJS harness reports (`speedStats`) so numbers are
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 deterministic LLM provider-routing core (see
9
- * `docs/bind.md` and the LLM Provider Routing PRD). The turn kernel and tool
10
- * contracts move here in later extraction phases.
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 deterministic LLM provider-routing core (see
9
- * `docs/bind.md` and the LLM Provider Routing PRD). The turn kernel and tool
10
- * contracts move here in later extraction phases.
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";
@@ -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>;