@msm-core/mini 0.5.2 → 0.9.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.
@@ -4,6 +4,8 @@
4
4
  * All contracts for the lite agent runtime. Brain-agnostic, zero embedded
5
5
  * databases. Application layer owns persistence; agent owns the loop.
6
6
  */
7
+ import type { SessionLogPort } from "@msm-core/session";
8
+ import type { ContextBudget } from "./context-builder.js";
7
9
  /**
8
10
  * A single memory entry — assembled by msm-context or by the app directly.
9
11
  * The agent injects these into every brain call in priority order.
@@ -114,12 +116,104 @@ export interface BrainRunInput {
114
116
  * safe — the loop still stops waiting via its timeout race.
115
117
  */
116
118
  signal?: AbortSignal;
119
+ /**
120
+ * Present ONLY when a consumer asked to see the answer as it is produced
121
+ * (ب١). Its absence is the normal case and means "do not stream" — every
122
+ * built-in brain then walks the exact non-streaming path it always walked.
123
+ *
124
+ * **Display, not truth.** What the loop records, gates, returns and replays
125
+ * is still the completed `BrainPayload`. A chunk is never logged as an event,
126
+ * never enters a `replay` fingerprint, and never changes the session-log
127
+ * invariant. Drop every chunk on the floor and the run is bit-identical.
128
+ *
129
+ * A brain that ignores this field is CORRECT, not broken — it simply does not
130
+ * stream. Third-party brains written before ب١ keep working untouched.
131
+ */
132
+ onChunk?: (chunk: BrainChunk) => void;
133
+ }
134
+ /**
135
+ * One piece of a model answer as it arrives.
136
+ *
137
+ * **Text only in v1.** Streaming tool CALLS is deliberately out of scope: the
138
+ * brain still accumulates them and returns them in the payload exactly as it
139
+ * does today, so a half-arrived tool call is never observable and never
140
+ * executable. The type is an object rather than a bare `string` so that day —
141
+ * if it comes — adds a field instead of breaking every consumer's signature.
142
+ */
143
+ export interface BrainChunk {
144
+ text: string;
145
+ }
146
+ /**
147
+ * One tool invocation a model asked for, in the model's own order.
148
+ *
149
+ * The unit is the CALL, not the step: modern models emit several in one
150
+ * response, and a step-shaped type would force the loop back into one
151
+ * round-trip per tool — the cost this exists to remove.
152
+ */
153
+ export interface BrainToolCall {
154
+ name: string;
155
+ params: Record<string, unknown>;
117
156
  }
157
+ /**
158
+ * Every action the loop DISPATCHES ON — declared as data, and the only list.
159
+ *
160
+ * ── Why this is a `const` array and not an inline union (ر٢) ────────────────
161
+ *
162
+ * There were two lists. This union, and a second copy in `bridge/pipeline.ts`
163
+ * that normalises whatever a duck-typed pipeline hands back. Both carried a
164
+ * fifth member the loop never dispatched on — a routing action that was
165
+ * declared at the beginning, implemented nowhere, and used by nobody (measured:
166
+ * zero occurrences in the live consumer). It survived precisely because
167
+ * deleting it meant finding and agreeing two places, and س١'s lesson is that
168
+ * two lists of the same thing drift apart silently and are only noticed by the
169
+ * damage.
170
+ *
171
+ * So there is now one list. The union below is derived from it and the bridge
172
+ * validates against it, which means a member cannot be added to one and
173
+ * forgotten in the other, and a member cannot be REMOVED from one and left
174
+ * standing in the other. `BrainActionsAreTheFourLiveOnes` then makes growing
175
+ * this array a decision someone has to write down rather than one that happens
176
+ * by omission.
177
+ *
178
+ * **What the loop actually does with each** (`core/loop.ts`):
179
+ * - `use_tool` → the step's tool calls run, then back to the model.
180
+ * - `clarify` / `escalate` → terminal, and the outcome carries that type.
181
+ * - `respond` → terminal, the text is delivered.
182
+ *
183
+ * And an action that is NONE of these — a rogue model emitting a string at
184
+ * runtime, where no type can stop it — takes the `respond` path: terminal,
185
+ * one iteration, whatever text the payload carried. That is the existing
186
+ * behaviour, it is fail-safe (a nonsense action never loops and never runs a
187
+ * tool), and it is pinned by a guard in `tests/delegate.test.ts` rather than
188
+ * left as an accident. The bridge's `respond` fallback for an unrecognised
189
+ * string is the same rule stated one layer earlier.
190
+ */
191
+ export declare const BRAIN_ACTIONS: readonly ["use_tool", "respond", "clarify", "escalate"];
192
+ /** What a model may ask the loop to do. Derived from `BRAIN_ACTIONS`. */
193
+ export type BrainAction = (typeof BRAIN_ACTIONS)[number];
118
194
  export interface BrainOrchestration {
119
- action: "use_tool" | "respond" | "clarify" | "escalate" | "delegate";
195
+ action: BrainAction;
120
196
  confidence: number;
197
+ /**
198
+ * The FIRST call of the step. Always filled whenever `tool_calls` is —
199
+ * never left behind as `tool_calls` grows. Every consumer written against
200
+ * one-tool-per-step keeps reading exactly what it always read.
201
+ */
121
202
  tool_name?: string;
203
+ /** The first call's arguments. Same covenant as `tool_name`. */
122
204
  tool_params?: Record<string, unknown>;
205
+ /**
206
+ * ALL the calls of this step, in the order the model emitted them.
207
+ *
208
+ * **A pure addition.** A brain that fills only `tool_name`/`tool_params`
209
+ * (every third-party brain written before this field existed, and every
210
+ * scripted brain in the suite) is normalized to a single-call step and takes
211
+ * the path it always took, character for character. A brain that fills this
212
+ * field fills the two above with `tool_calls[0]` as well — the covenant is
213
+ * one-directional: new readers may read the array, old readers never see a
214
+ * hole where the first call used to be.
215
+ */
216
+ tool_calls?: BrainToolCall[];
123
217
  reasoning?: string;
124
218
  [key: string]: unknown;
125
219
  }
@@ -253,6 +347,89 @@ export interface DocumentState {
253
347
  startedAt: number;
254
348
  updatedAt: number;
255
349
  }
350
+ /**
351
+ * Final run metadata persisted after every handle() — for ops dashboards.
352
+ *
353
+ * Declared here, with the port it belongs to, rather than inside one adapter:
354
+ * it is part of the contract every store implements, not a Redis detail. The
355
+ * Redis adapter re-exports it, so existing imports keep working unchanged.
356
+ */
357
+ export interface SessionMetadata {
358
+ iterationCount: number;
359
+ startedAt: number;
360
+ totalCostUsd: number;
361
+ status: "running" | "completed" | "failed" | "killed";
362
+ }
363
+ /**
364
+ * The session-memory PORT — conversation history, run metadata, document state.
365
+ *
366
+ * The loop talks to this interface and never to a concrete store. Inject an
367
+ * implementation via `AgentConfig.memory`; when it is omitted the loop builds
368
+ * the bundled `RedisMemory` from `AgentConfig.redis` exactly as it always has.
369
+ *
370
+ * Six functions — the surface `RedisMemory` already had, at its current
371
+ * signatures. Nothing is added speculatively: an append-only event log is a
372
+ * different contract and does not belong here.
373
+ */
374
+ export interface SessionStore {
375
+ /** Append one message to this session's conversation history. */
376
+ appendHistory(sessionId: string, entry: Message): Promise<void>;
377
+ /** The last `limit` messages, oldest-first. Default limit: 50. */
378
+ getHistory(sessionId: string, limit?: number): Promise<Message[]>;
379
+ getMetadata(sessionId: string): Promise<SessionMetadata | null>;
380
+ setMetadata(sessionId: string, meta: SessionMetadata): Promise<void>;
381
+ getDocumentState(sessionId: string): Promise<DocumentState | null>;
382
+ setDocumentState(sessionId: string, state: DocumentState): Promise<void>;
383
+ }
384
+ /**
385
+ * What a compactor decides when it decides to compact.
386
+ *
387
+ * `keepTurns` is a REQUEST, not a command: the loop hands it to
388
+ * `compactionBoundary`, which cuts only at whole turns and refuses to sever a
389
+ * tool call from its result. A compactor asking to keep zero turns of a session
390
+ * whose last turn is still unpaired gets a smaller range than it asked for, or
391
+ * none at all. That is the point of having two defences.
392
+ */
393
+ export interface CompactionDecision {
394
+ /** The text the compacted range will read as. Empty means "no compaction". */
395
+ summary: string;
396
+ /** How many trailing turns to leave verbatim. Clamped to what is safe. */
397
+ keepTurns: number;
398
+ }
399
+ /**
400
+ * The compaction seat: a port the loop consults before it derives the
401
+ * conversation, and never a trick it plays on the history array.
402
+ *
403
+ * Today a long session is trimmed by `history.slice(-maxHistoryMessages)`. That
404
+ * cut lands wherever it lands — through the middle of a tool call and its
405
+ * result, through the middle of a turn — and it is silent. A long legal case in
406
+ * nisus loses its own beginning and nothing anywhere says so. This port is the
407
+ * seat where that stops being true.
408
+ *
409
+ * Three properties are contractual:
410
+ *
411
+ * 1. **It only works with `sessionLog`.** Compaction records a `compaction`
412
+ * event; without a log there is nowhere to record it, and a summary that is
413
+ * not recorded is exactly the silent rewrite this replaces. With `memory`
414
+ * alone the port is inert and the old `slice` path runs untouched — declared,
415
+ * not an oversight.
416
+ *
417
+ * 2. **`null` means no compaction.** The overwhelmingly common answer. A port
418
+ * that returns `null` costs one call and changes nothing.
419
+ *
420
+ * 3. **A throwing port fails the turn.** It is not swallowed. A compactor that
421
+ * has quietly been failing for a month is a session that has quietly been
422
+ * losing its head for a month — the failure this exists to end, wearing a
423
+ * different hat.
424
+ *
425
+ * @param messages the conversation DERIVED from the log so far, in order.
426
+ * @param budget the budget the derived history will actually be trimmed to —
427
+ * `maxHistoryMessages` is the effective cap, already reconciled
428
+ * with the agent's iteration-derived history limit.
429
+ */
430
+ export interface CompactionPort {
431
+ maybeCompact(messages: readonly Message[], budget: ContextBudget): Promise<CompactionDecision | null>;
432
+ }
256
433
  export interface RedisConfig {
257
434
  /**
258
435
  * Redis connection URL — mini creates its own connection.
@@ -304,6 +481,68 @@ export interface AgentConfig {
304
481
  definition: string | AgentDefinition;
305
482
  brain: Brain;
306
483
  redis: RedisConfig;
484
+ /**
485
+ * Optional session-memory port. When injected, the loop routes ALL history /
486
+ * metadata / document-state access through it and never constructs a
487
+ * Redis-backed store. When omitted, the loop builds `RedisMemory` from
488
+ * `redis` exactly as before — same prefix, same TTLs, same tenant scoping —
489
+ * so every existing consumer is unaffected.
490
+ *
491
+ * `redis` stays required either way: the control bus, the session lock and
492
+ * tool dedup are separate ports and still ride on it.
493
+ */
494
+ memory?: SessionStore;
495
+ /**
496
+ * Optional session **event log** (`@msm-core/session`). Injecting it inverts
497
+ * where conversation context comes from:
498
+ *
499
+ * - **Omitted (default)** — nothing changes. Not one event is written, the
500
+ * history handed to the brain is `memory.getHistory()` exactly as before,
501
+ * and the invariant below never runs. Every existing consumer is
502
+ * bit-for-bit unaffected.
503
+ * - **Injected** — the loop writes every event as it happens (the user
504
+ * message, each step, each model request/response, every tool call and
505
+ * its result, guards, the delivered reply), and the conversation handed
506
+ * to the model is DERIVED from the log (`deriveMessages`) instead of read
507
+ * from the history array. `memory.appendHistory` keeps being written too:
508
+ * one transitional phase of dual writing, log as truth, array as
509
+ * compatibility.
510
+ *
511
+ * Two consequences worth knowing before you inject one:
512
+ *
513
+ * 1. **The derived conversation is richer.** It carries the
514
+ * `assistant`/`tool` pairs of previous turns, which the history array
515
+ * never held. That is the point of the log, not a side effect — but it
516
+ * is a real change in what the model sees.
517
+ * 2. **It is the truth, so its failures are loud.** A rejected append
518
+ * (e.g. `SeqGapError`) fails the turn rather than being swallowed: a log
519
+ * with a hole in it cannot back an audit, a resume or a replay.
520
+ *
521
+ * Isolation is the injector's, exactly as with `memory`: this port takes a
522
+ * `sessionId` and knows nothing of tenants. Whoever injects a log injects its
523
+ * tenant scoping with it.
524
+ */
525
+ sessionLog?: SessionLogPort;
526
+ /**
527
+ * Optional **compaction port** (ض١). Consulted once per turn, right before
528
+ * the conversation is derived, and only when `sessionLog` is also injected.
529
+ *
530
+ * - **Omitted (default)** — not one extra call, not one extra event. The
531
+ * conversation is derived and trimmed exactly as it was before.
532
+ * - **Omitted `sessionLog`** — the seat is inert even when this is set:
533
+ * compaction records an event, and without a log there is nothing to
534
+ * record it in. The legacy history path keeps its `slice`. Declared.
535
+ * - **Both injected** — when the port returns a summary, the loop writes a
536
+ * `compaction` event covering a safe range (`compactionBoundary`) and
537
+ * re-derives. The model then reads one summary plus the recent turns
538
+ * verbatim, and the invariant still proves that what it read came out of
539
+ * the log — because the compaction is IN the log.
540
+ *
541
+ * `createBrainCompactor` is a reference implementation that summarises with an
542
+ * injected `Brain`. The port does not require it; anything with a
543
+ * `maybeCompact` will do, including one that never calls a model at all.
544
+ */
545
+ compaction?: CompactionPort;
307
546
  /** App-provided tools — agent can only call tools listed here */
308
547
  tools: Tool[];
309
548
  guards?: GuardConfig;
@@ -343,6 +582,15 @@ export interface GuardFiredInfo {
343
582
  signal: GuardSignal;
344
583
  iteration: number;
345
584
  }
585
+ /** One streamed piece of the model's answer, with the step it belongs to (ب١). */
586
+ export interface ChunkInfo {
587
+ sessionId: string;
588
+ /** The loop iteration whose brain call produced this text. */
589
+ iteration: number;
590
+ /** The delta — not the running total. Concatenating every chunk of a step
591
+ * yields the text that step's payload carries. */
592
+ text: string;
593
+ }
346
594
  /**
347
595
  * Return value from onBeforeTool hook:
348
596
  * "proceed" — execute the tool normally
@@ -365,6 +613,18 @@ export interface AgentHooks {
365
613
  onToolCall?: (info: ToolCallInfo) => void;
366
614
  onGuard?: (info: GuardFiredInfo) => void;
367
615
  onFatalError?: (error: Error, sessionId: string) => void;
616
+ /**
617
+ * Called for each piece of model text as it arrives (ب١). Wire it to an SSE
618
+ * channel and the user stops watching a silent screen.
619
+ *
620
+ * **Setting this is what turns streaming on.** The loop passes `onChunk` down
621
+ * to the brain only when this hook exists; with no hook, `BrainRunInput`
622
+ * carries no `onChunk` and every brain takes its ordinary non-streaming path.
623
+ *
624
+ * Fire-and-forget like its siblings: a throwing hook is swallowed and the
625
+ * stream continues — a broken display must not break the run.
626
+ */
627
+ onChunk?: (info: ChunkInfo) => void;
368
628
  /**
369
629
  * Called before every tool execution (BEFORE the dedup check, so cached
370
630
  * results can never bypass the gate). Return "proceed" to execute normally,
@@ -4,4 +4,43 @@
4
4
  * All contracts for the lite agent runtime. Brain-agnostic, zero embedded
5
5
  * databases. Application layer owns persistence; agent owns the loop.
6
6
  */
7
- export {};
7
+ /**
8
+ * Every action the loop DISPATCHES ON — declared as data, and the only list.
9
+ *
10
+ * ── Why this is a `const` array and not an inline union (ر٢) ────────────────
11
+ *
12
+ * There were two lists. This union, and a second copy in `bridge/pipeline.ts`
13
+ * that normalises whatever a duck-typed pipeline hands back. Both carried a
14
+ * fifth member the loop never dispatched on — a routing action that was
15
+ * declared at the beginning, implemented nowhere, and used by nobody (measured:
16
+ * zero occurrences in the live consumer). It survived precisely because
17
+ * deleting it meant finding and agreeing two places, and س١'s lesson is that
18
+ * two lists of the same thing drift apart silently and are only noticed by the
19
+ * damage.
20
+ *
21
+ * So there is now one list. The union below is derived from it and the bridge
22
+ * validates against it, which means a member cannot be added to one and
23
+ * forgotten in the other, and a member cannot be REMOVED from one and left
24
+ * standing in the other. `BrainActionsAreTheFourLiveOnes` then makes growing
25
+ * this array a decision someone has to write down rather than one that happens
26
+ * by omission.
27
+ *
28
+ * **What the loop actually does with each** (`core/loop.ts`):
29
+ * - `use_tool` → the step's tool calls run, then back to the model.
30
+ * - `clarify` / `escalate` → terminal, and the outcome carries that type.
31
+ * - `respond` → terminal, the text is delivered.
32
+ *
33
+ * And an action that is NONE of these — a rogue model emitting a string at
34
+ * runtime, where no type can stop it — takes the `respond` path: terminal,
35
+ * one iteration, whatever text the payload carried. That is the existing
36
+ * behaviour, it is fail-safe (a nonsense action never loops and never runs a
37
+ * tool), and it is pinned by a guard in `tests/delegate.test.ts` rather than
38
+ * left as an accident. The bridge's `respond` fallback for an unrecognised
39
+ * string is the same rule stated one layer earlier.
40
+ */
41
+ export const BRAIN_ACTIONS = [
42
+ "use_tool",
43
+ "respond",
44
+ "clarify",
45
+ "escalate",
46
+ ];
package/dist/index.d.ts CHANGED
@@ -7,13 +7,18 @@
7
7
  * import { RedisMemory, RedisControlBus } from 'msm-mini/adapters'
8
8
  */
9
9
  export { createAgent } from "./core/loop.js";
10
+ export { createBrainCompactor } from "./core/loop.js";
11
+ export type { BrainCompactorOptions } from "./core/loop.js";
10
12
  export { createGeminiBrain } from "./brain/gemini.js";
11
13
  export { createOpenAIBrain } from "./brain/openai.js";
12
14
  export { createAnthropicBrain } from "./brain/anthropic.js";
13
15
  export { createOllamaBrain } from "./brain/ollama.js";
14
16
  export { buildBrain } from "./brain/factory.js";
15
17
  export { parseDefinition } from "./definition/parser.js";
16
- export type { Agent, AgentConfig, AgentEvent, AgentContext, AgentDefinition, AgentHooks, BeforeToolHookResult, Brain, BrainPayload, BrainRunInput, DocumentState, GateConfig, GuardConfig, GuardSignal, GuardSignalType, IterationInfo, LoopOutcome, MemoryEntry, Message, OutcomeType, OutputValidation, OutputValidator, RedisConfig, RunState, SectionInfo, TenantContext, Tool, ToolCallInfo, ToolDefinition, ToolMeta, ToolParameter, ToolResult, } from "./core/types.js";
18
+ export { createDelegateTool, DELEGATE_TOOL_NAME } from "./tools/delegate.js";
19
+ export type { DelegateTool, DelegateToolOptions } from "./tools/delegate.js";
20
+ export type { Agent, AgentConfig, AgentEvent, AgentContext, AgentDefinition, AgentHooks, BeforeToolHookResult, Brain, BrainChunk, BrainOrchestration, BrainPayload, BrainRunInput, BrainToolCall, ChunkInfo, CompactionDecision, CompactionPort, DocumentState, GateConfig, GuardConfig, GuardSignal, GuardSignalType, IterationInfo, LoopOutcome, MemoryEntry, Message, OutcomeType, OutputValidation, OutputValidator, RedisConfig, RunState, SectionInfo, SessionMetadata, SessionStore, TenantContext, Tool, ToolCallInfo, ToolDefinition, ToolMeta, ToolParameter, ToolResult, } from "./core/types.js";
21
+ export type { ContextBudget } from "./core/context-builder.js";
17
22
  export { resolveGuards, DEFAULT_GUARDS } from "./core/guards.js";
18
23
  export { scoreOutcome } from "./quality/scorer.js";
19
24
  export type { QualityScore, QualityFlag } from "./quality/scorer.js";
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@
8
8
  */
9
9
  // ── Main factory ────────────────────────────────────────────────────────────
10
10
  export { createAgent } from "./core/loop.js";
11
+ // ── Compaction (ض١) — the seat, and one reference occupant ──────────────────
12
+ export { createBrainCompactor } from "./core/loop.js";
11
13
  // ── Brain factories ─────────────────────────────────────────────────────────
12
14
  export { createGeminiBrain } from "./brain/gemini.js";
13
15
  export { createOpenAIBrain } from "./brain/openai.js";
@@ -16,6 +18,8 @@ export { createOllamaBrain } from "./brain/ollama.js";
16
18
  export { buildBrain } from "./brain/factory.js";
17
19
  // ── Definition parser ───────────────────────────────────────────────────────
18
20
  export { parseDefinition } from "./definition/parser.js";
21
+ // ── Delegation (ر٢) — one agent asks another, as an ordinary tool ───────────
22
+ export { createDelegateTool, DELEGATE_TOOL_NAME } from "./tools/delegate.js";
19
23
  // ── Guard utilities ─────────────────────────────────────────────────────────
20
24
  export { resolveGuards, DEFAULT_GUARDS } from "./core/guards.js";
21
25
  // ── Quality scorer ──────────────────────────────────────────────────────────
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Delegation — one agent asks another, as an ORDINARY TOOL (ر٢).
3
+ *
4
+ * ── Why a tool and not a loop action ────────────────────────────────────────
5
+ *
6
+ * `BrainOrchestration.action` used to carry a fifth member for routing work to
7
+ * another agent. It was declared at the beginning, dispatched on by nothing,
8
+ * and used by nobody — measured, not assumed: zero occurrences in the live
9
+ * consumer. Meanwhile that same consumer HAS been delegating in production for
10
+ * months, and it does it the other way round: a `call_agent` tool over an
11
+ * injected handle. The measurement settled the design argument, so the action
12
+ * was buried (`core/types.ts`) and this is the thing that replaces it.
13
+ *
14
+ * The consequence is the point of the whole exercise: **there is not one line
15
+ * of loop code here.** A delegation is a tool call, so it already goes through
16
+ * the parameter validation, the `onBeforeTool` approval gate, the dedup cache,
17
+ * the control-bus disable, the per-call `tool_call`/`tool_result` pair in the
18
+ * session log, the tool-call budget and the consecutive-failure counter —
19
+ * every one of them, for free, because it is not special. An action in the loop
20
+ * would have had to re-earn each of those, one `if` at a time, in the file
21
+ * every agent rides on.
22
+ *
23
+ * ── What this improves on the lifted original ───────────────────────────────
24
+ *
25
+ * Three things, each of them a rule this repo already pays for elsewhere:
26
+ *
27
+ * 1. **The child's session id is DERIVED, not random.** `parent.d.child`,
28
+ * built from the parent's own id. The original minted a fresh UUID per
29
+ * delegation, which is exactly the thing س٥ named the enemy of replay: an
30
+ * arbitrary identifier kills every comparison that crosses a run — a tape
31
+ * fingerprint, a golden log, a diff of two sessions that should be
32
+ * identical. Derived, the same delegation twice is the same id twice, the
33
+ * log reads `s-42.d.researcher` and says what it is, and the depth is
34
+ * legible in the id instead of held in a side channel.
35
+ *
36
+ * 2. **The agent name is an `enum`, so the model cannot invent one.** The
37
+ * brains forward `enum` into the provider schema (`toolParamsToJsonSchema`),
38
+ * so this is a real constraint at the provider and not a hint. It is still
39
+ * checked at execution — `validateParams` enforces presence and type, never
40
+ * membership — and an unknown name is a NAMED failure the model can read
41
+ * and correct, not a crash.
42
+ *
43
+ * 3. **The cost is visible.** The original returned the child's text and threw
44
+ * the rest away, so a delegating agent's real spend was invisible to
45
+ * everything that watched it. `totalCostUsd` rides back in the result.
46
+ *
47
+ * ── What is deliberately NOT here ───────────────────────────────────────────
48
+ *
49
+ * • **The child's cost is not added to the parent's `totalCostUsd`.** It is
50
+ * reported and left there. Both agents run their own budgets, and a number
51
+ * counted in two places is worse than a number counted in one: it would
52
+ * make the parent's cost cap fire on spend the child already paid for, and
53
+ * the pair would then disagree about what the run cost. Raised for
54
+ * management, not decided here.
55
+ *
56
+ * • **No parent context is forwarded.** The child gets the message and its
57
+ * tenant, and assembles its own persona, memories and tools. Handing it the
58
+ * parent's `AgentContext` would make it a continuation of the parent rather
59
+ * than a second agent — and `ToolMeta` does not carry one anyway.
60
+ */
61
+ import type { Agent, Tool } from "../core/types.js";
62
+ /**
63
+ * The tool's name, exported because consumers dispatch on it.
64
+ *
65
+ * An approval hook, an audit trail or a UI badge all need to recognise a
66
+ * delegation by name, and a hand-typed string in each of them is the drift س١
67
+ * was paid for. It is `call_agent` and not something new so that the consumer
68
+ * already running this pattern in production can swap its hand-rolled tool for
69
+ * this factory without changing a prompt, a manifest, or an approval rule.
70
+ */
71
+ export declare const DELEGATE_TOOL_NAME = "call_agent";
72
+ /** Tuning for `createDelegateTool`. Every field has a working default. */
73
+ export interface DelegateToolOptions {
74
+ /**
75
+ * How many hops deep delegation may go. Default **1**: the agent you build
76
+ * may ask a delegate, and that delegate may not ask anyone.
77
+ *
78
+ * Measured off the CALLER's session id (`.d.` counted), not off a counter
79
+ * threaded through the call — so it cannot be lost, reset or lied about by a
80
+ * path that forgot to pass it on.
81
+ *
82
+ * `0` is a legitimate value and means "wired but switched off". Anything that
83
+ * is not a finite number ≥ 0 falls back to the default, in the spirit of
84
+ * `resolveGuards`: a typo must not silently remove a cap.
85
+ */
86
+ maxDepth?: number;
87
+ /**
88
+ * Stamped onto the tool, exactly as the MCP adapter stamps an MCP tool (ر١).
89
+ *
90
+ * That is the entire mechanism — there is no delegation-specific approval
91
+ * code, because `requiresApproval` is already a contract the executor fails
92
+ * closed on: set it with no `onBeforeTool` hook configured and the call is
93
+ * blocked before `execute` runs, which means blocked before the child agent
94
+ * is reached at all.
95
+ */
96
+ requiresApproval?: boolean;
97
+ /** Advisory metadata, mirroring `ToolDefinition`. See the note on `DelegateTool`. */
98
+ destructive?: boolean;
99
+ /** Advisory metadata, mirroring `ToolDefinition`. See the note on `DelegateTool`. */
100
+ category?: string;
101
+ }
102
+ /**
103
+ * A `Tool`, plus the two advisory fields mini's `Tool` does not carry.
104
+ *
105
+ * Shaped exactly like `McpTool` (ر١) and for the same reason: `ToolDefinition`
106
+ * declares `destructive` and `category`, `Tool` does not, and
107
+ * `toToolDefinitions` copies neither — so today they are carried and not yet
108
+ * read. That gap is already raised for management as the `toToolDefinitions`
109
+ * debt; this type is written to mirror the MCP stamp so that the day the debt
110
+ * is paid, both stamps start being read by the same change.
111
+ */
112
+ export interface DelegateTool extends Tool {
113
+ destructive?: boolean;
114
+ category?: string;
115
+ }
116
+ /**
117
+ * Build the delegation tool for a set of named agents.
118
+ *
119
+ * ```ts
120
+ * const agent = createAgent({
121
+ * definition, brain, redis,
122
+ * tools: [searchTool, createDelegateTool({ researcher, drafter })],
123
+ * });
124
+ * ```
125
+ *
126
+ * The delegates are `mini` agents themselves — whatever `createAgent` returned,
127
+ * or anything else satisfying `Agent`. Nothing is constructed here and no
128
+ * connection is opened: this is a port like every other in the package, handed
129
+ * in at composition time.
130
+ *
131
+ * @param delegates agents this tool may call, by the name the model will use.
132
+ * @param opts depth cap and the approval/metadata stamp.
133
+ */
134
+ export declare function createDelegateTool(delegates: Record<string, Agent>, opts?: DelegateToolOptions): DelegateTool;