@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.
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/memory-store.d.ts +38 -0
- package/dist/adapters/memory-store.js +73 -0
- package/dist/adapters/redis-memory.d.ts +13 -8
- package/dist/adapters/redis-memory.js +5 -0
- package/dist/brain/anthropic.js +50 -17
- package/dist/brain/gemini.js +50 -17
- package/dist/brain/ollama.js +68 -19
- package/dist/brain/openai.js +57 -23
- package/dist/brain/streaming.d.ts +315 -0
- package/dist/brain/streaming.js +439 -0
- package/dist/brain/tool-context.d.ts +50 -2
- package/dist/brain/tool-context.js +88 -0
- package/dist/bridge/pipeline.js +11 -8
- package/dist/core/context-builder.d.ts +11 -0
- package/dist/core/context-builder.js +11 -1
- package/dist/core/hooks.d.ts +10 -0
- package/dist/core/hooks.js +14 -0
- package/dist/core/loop.d.ts +43 -1
- package/dist/core/loop.js +749 -98
- package/dist/core/types.d.ts +261 -1
- package/dist/core/types.js +40 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -0
- package/dist/tools/delegate.d.ts +134 -0
- package/dist/tools/delegate.js +223 -0
- package/package.json +11 -11
package/dist/core/types.d.ts
CHANGED
|
@@ -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:
|
|
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,
|
package/dist/core/types.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|