@dudousxd/nestjs-agent-core 0.20.0 → 0.22.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 CHANGED
@@ -18,7 +18,8 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
18
18
 
19
19
  ## Key types
20
20
 
21
- - `ToolSpec` — `{ name, kind: 'read' | 'action' | 'agent', description, inputSchema, roles?, ability?, targetAgent?, detached? }`. (`ToolKind` has three further members — `'ask'`, `'skill'` and `'memory'` — which no `ToolSpec` carries: they belong to the built-in tools the loop serves itself rather than from a handler. `ask` settles against a person, `skill` against the turn's journaled catalog, `remember` against the turn's journaled memory digest.) `inputSchema` is a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType); the loop validates via `~standard.validate`, throwing `ToolInputInvalidError` on failure.
21
+ - `ToolSpec` — `{ name, kind: 'read' | 'action' | 'agent', description, inputSchema, roles?, ability?, targetAgent?, detached?, terminal? }`. (`ToolKind` has three further members — `'ask'`, `'skill'` and `'memory'` — which no `ToolSpec` carries: they belong to the built-in tools the loop serves itself rather than from a handler. `ask` settles against a person, `skill` against the turn's journaled catalog, `remember` against the turn's journaled memory digest.) `inputSchema` is a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType); the loop validates via `~standard.validate`, throwing `ToolInputInvalidError` on failure.
22
+ - `AiToolCtx.emitUi(component, props, { id?, version? })` — **a tool pushes generative UI.** The `ui` frame streams live (inline, durable, and from a dispatched tool step's worker) and the component is persisted on the assistant message through the optional `AgentStore.setMessageUi`, once per step, after the step's tools settle. `id` defaults to `<toolCallId>:ui:<n>`, so a retried call replaces what it pushed. The pushes ride the tool step's journaled result (`wrapToolStepOutput` / `unwrapToolStepOutput`; the wrapper is only used when something was pushed), so a replay neither re-streams nor re-persists them. `ToolSpec.terminal` ends the turn after a successful call, settled in `persist:toolcall` like `detached`.
22
23
  - `AgentDefinition` — a named agent (`systemPrompt` string | `PromptBuilder`, `tools`, `delegatesTo`, `personas`, …) for multi-agent setups. `delegatesTo` holds `AgentDelegation` entries: a bare target name for the delegation that waits, `{ agent, detached: true }` for one that does not.
23
24
  - `detachedStarted` / `detachedDelivered` / `settleUnsettledDelegation` / `AgentLoopHooks.startAgent` / `AgentRunInput.deliverTo` — **delegation that does not block the chat.** An `agent`-kind call whose spec says `detached` is STARTED rather than awaited (`hooks.startAgent`, which the durable runner maps to `ctx.startChild` and the inline one to a loop nobody awaits), and the turn ends with a `DetachedDelegationReceipt` as the call's result instead of an answer. The started run carries a `deliverTo` address — the delegating thread and the call that started it — and posts its answer there as a message of its own, stamped with its own `runId` and `agentName`, so a client renders "the research agent finished" rather than the assistant's next reply. Whether a call detaches is settled INSIDE `persist:toolcall` alongside its kind and target, never from a live registry lookup, so a replay reads the branch back rather than re-deciding it; the loop writes the same checkpoint names either way, and only the runner's own positions (`spawn:` versus the awaited child's `signal:child:`) differ. A detached run streams into its OWN sink and its `action` tools park on its OWN run, so its approval reaches the pending-approvals surface instead of an inline card in whatever turn happens to be open. `settleUnsettledDelegation` is the runner's half: a run that crashed or was stopped posts a message saying so, because "started" is the one state a reader can neither wait on nor act on. `AgentRunInput.parentRunId` / `RecordRunStartInput.parentRunId` record the edge, so a delegation is not a run row with nothing pointing at it.
24
25
  - `RolesPolicy.can(actor, tool): boolean | Promise<boolean>` — the tool authorization seam
@@ -27,6 +28,7 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
27
28
  - `InputProcessor` / `OutputProcessor` — the seams on either side of the model call. Input rewrites `{ system, messages }` before every model call; output returns `pass` / `replace` / `reject` on each step's answer. Both run inside a checkpoint (so a processor may call a model), and they own TRANSFORMATION only — `HistoryPolicy` owns which messages are there to transform. Registering an output processor takes the turn's model call off the run's sink; what that costs the reader depends on what the chain declares (`resolveOutputGateMode`). An undeclared processor holds the whole answer — `createFrameBuffer` + `releaseGatedFrames`. A chain where EVERY processor sets `incremental` gates the growing prefix instead and keeps streaming — `createIncrementalGate` + `gateTail`, with the widest `lookbackChars` in the chain (`resolveGateLookback`, default `DEFAULT_INCREMENTAL_LOOKBACK_CHARS`) held back. The whole-answer pass stays authoritative either way, and `gateTail` raises `ProcessorFailedError` when the settled answer is not an extension of the released prefix. The output chain covers every model-written value that reaches a reader, not only the streamed answer: the `outputSchema` formatting pass is gated before its text is validated (`process:output:structured:<step>:<attempt>`), and each follow-up suggestion is gated on its own (`gateFollowUps`, `process:output:followups:<step>`) — where a refused suggestion is DROPPED rather than failing a run whose answer already passed the same chain. Neither is streamed, so an `incremental` declaration does not apply to them. The folded history summary is deliberately NOT gated here: it never reaches a reader, it re-enters the next prompt as a leading `system` message, and `inputProcessors` — the seam that owns prompt text — already see it on every step.
28
29
  - `outputSchema` (on `AgentLoopDeps`) / `StructuredOutputError` — constrain the final answer to a Standard Schema via one journaled formatting pass after the turn's last model step, with bounded repair. The pass is a translation, so it is shown the question (as the input chain left it) and the gated answer, and nothing else off the transcript — `outputFromTranscript` opts an agent back into the whole turn. `validateStructured` and `extractJson` are the pieces; `ModelTurnArgs.outputSchema` and `ModelTurnResult.object` are how a provider opts into constraining generation itself.
29
30
  - `ElicitationRequest` / `ElicitationReply` / `settleElicitation` — putting a structured question set to the USER and waiting for the answer, from either surface (`AgentLoopDeps.intake`, authored on the agent; `AgentLoopDeps.ask`, the model-callable tool). Both persist as one pending tool-call row and park on the `tool:<runId>:<callId>` signal a HITL approval already uses. `settleElicitation` is pure: it fills every unanswered question from the request's own `defaults` and renders the result by option LABEL, so the same values are reached on every replay without a checkpoint of its own. Because the row is parked as a `pending_approval` action, the reply that comes back may be a `Decision` an operator pressed Approve/Reject on rather than answers — `normalizeElicitationReply` reduces one to the other where the reply is CONSUMED (Approve = confirm every pre-picked default, Reject = skip), so the reduction holds on every path rather than only on a host that implemented no `awaitAnswers`. `askToolDefinition()` is the tool as the model sees it — never registered, so its kind can never be decided by a process-local registry lookup.
31
+ - `ElicitationInput` / `validateElicitationValue` / `validateElicitationAnswer` / `readElicitationQuestions` — **typed questions.** `ElicitationQuestion` gains `description?` and `input?: { type: 'text' | 'textarea' | 'number' | 'boolean' | 'date' | 'email' | 'url' | 'select', placeholder?, required?, min?, max?, pattern? }`; `options` becomes optional when `input` asks for a typed value (a `select` still picks from them), and a typed question may omit `defaults`. Answers stay `string[]` on the wire in one canonical form per type (decimal, `"true"`/`"false"`, `YYYY-MM-DD`, …). The validators are the one rule set the loop (which drops values it cannot settle), the answer route (which refuses them with `400`) and the React model (which reports them before sending) share.
30
32
  - `Skill` / `SkillProvider` / `ScopeResolver` / `offerSkills` — authored procedures the model pulls in when a task calls for one, instead of every instruction living in the system prompt. A skill is not an agent: an `@Agent` is WHO answers, a skill is HOW one task is done, and any agent may load one. Scoping is by an opaque TOKEN (`actor:u1`, `tenant:berlin`, `global`, or a host's own `depot:north`), and which tokens apply is a host-supplied `ScopeResolver` returning them most-specific-first — so precedence falls out of the order and a new axis is a resolver change, not a schema change. `defaultScopeResolver` covers the tokens derivable from `Actor` alone (actor / tenant / global); `actorScope`, `tenantScope` and `GLOBAL_SCOPE` mint them. This package owns NO skill table: the host owns the rows behind `SkillProvider` (`list(scopes, ctx)` for the catalog, `load(name, scope, ctx)` for one body), so a consumer can relate its own `Sector` entity against the token values in its own read model without writing migrations into a schema the boot-time heal also edits. `staticSkillProvider` and `compositeSkillProvider` are the built-ins. `resolveSkillCatalog` is the pure precedence pass: most specific wins, and the loser's scope is recorded on `shadows` rather than discarded, so the model can say "your setting differs from the org default" instead of choosing silently. What enters the SYSTEM prompt is the catalog only — one line per skill, bounded by `maxSkills` (`DEFAULT_MAX_SKILLS`) — while a BODY arrives as a `skill` tool result on the transcript, where the `HistoryPolicy` ceiling already governs it. The loop spends ONE checkpoint on all of it (`skills:catalog`) holding the whole offer, and serves each load inside the ordinary `tool:<callId>` checkpoint, so both the scopes that applied and the body that entered the prompt are facts the journal holds rather than answers a replaying process's provider would give afresh. `loadSkill` refuses any name the turn's own catalog does not carry, which makes the journaled catalog the authorization boundary as well as the menu. `skillWriteVerdict` is the write rule: your own scope is yours, a wider one needs an elevated HUMAN author, and nothing but a human may ever write above its own scope — an agent that could write a `tenant:` skill is an agent whose prompt anyone in the tenant can edit by talking to it.
31
33
  - `MemoryRecord` / `MemoryProvider` / `offerMemories` / `writeMemory` — what the assistant concluded about a person or an organisation, carried across turns and threads. Scoped by the SAME opaque tokens and the same `ScopeResolver` skills use, so a deployment has one answer to "which scopes does this actor have". A memory is a keyed fact: `{ key, text, scope, origin, updatedAt }`, and the key is what makes a conflict mechanically detectable — two memories sharing a key at different scopes are one question answered twice, and `resolveMemoryDigest` lets the narrower win. Where it does, the entry's `overrides` carries the beaten **text** and its **author**, not merely its scope (a skill's `shadows`): the model is following one procedure either way, but a memory is a VALUE, and an agent that knew only that a wider one existed could tell the user nothing except which it picked. **Not retrieval:** a passage is a document someone authored and can fix at its source, a memory is the agent's own inference about someone who never saw it written — hence `MemoryOrigin` on every record, a block that tells the model these are its own fallible notes, and `forget` being REQUIRED on the provider while `write` is optional. Every provider method and every multi-argument export here takes ONE named object (`ListMemoriesInput`, `StoreMemoryInput`, `ResolveMemoryDigestInput`, …): `key`, `text` and `scope` are all strings, and transposed positional arguments would compile clean and write a fact whose key is its value. `memoryWriteVerdict` carries the same four rules as `skillWriteVerdict`; rule three (nothing but a human may write above its own scope, whatever elevation a host grants) is enforced by SHAPE as well as by check, since `rememberToolDefinition()` takes no scope parameter. `memoryForgetVerdict` is narrower still and takes no `elevated` flag: deleting what the assistant believes about YOU needs nobody's permission. What enters the system prompt is one line per memory bounded by `maxMemories` (`DEFAULT_MAX_MEMORIES`), each capped at `maxFactChars` (`DEFAULT_MAX_FACT_CHARS`) when it is WRITTEN — so the block's ceiling is the product of two numbers an operator set, and there is no body/catalog split because a fact that cannot be stated in a line is a document. **The prompt budget is bounded; the store is not.** Once the applicable set outgrows the block, WHICH memories it carries is a decision, and making it by scope starves the widest scopes first — one person's twentieth note would end every chance their organisation's facts had, leaving only a non-zero `omitted` behind. So a provider MAY implement `search({ scopes, query, limit, ctx })` and the block is filled by relevance to the turn instead; omit it and every turn is served by `list`, selecting narrowest-then-newest as before. Scope remains a hard FILTER that gates before ranking (a record returned outside `scopes` is dropped, so a host's filter bug costs throughput rather than privacy), and `search` must return every record sharing a returned key or precedence inverts. `MemoryRecord.pinned` is the categorical always-on marker — present whatever the turn is about, spending the same budget, never set by the agent (`StoreMemoryInput` has no such field, and the `remember` tool has no such parameter), with `MemoryDigest.pinnedOmitted` naming the one omission that is a misconfiguration rather than a budget. `buildMemoryBlock` frames entries by `origin.author`: what the agent CONCLUDED is hedged ("your own notes … prefer what the user says now"), what a person STATED is not, because telling a model to prefer the user over an organisation's published policy hands any user an override of it by assertion. A `partial` block says so, so the model does not read an absence as evidence. The loop spends ONE checkpoint (`memory:digest`) holding the whole digest — the search included, since a ranking is the most re-derivable decision here — which is both what the block is rendered from and what a later `remember` call is authorized against; the write itself happens inside the ordinary `tool:<callId>` checkpoint, which is what makes it idempotent under replay. The query is the user's own turn text and nothing else: the only thing available before the first model call, already a journaled input to the run, and it fails at a turn with no topic — which is what `pinned` is for.
32
34
  - `AgentStore.setMessageToolResults(messageId, results)` — a message's tool CALLS are known when it is appended and their outputs are not, so the loop settles them afterwards with one write of the turn's complete result list (its synthetic `retrieve` / `structured_output` calls included). Both halves live on the message because that is where a thread reader pairs them; a call whose output only ever reaches the `agent_tool_call` table renders as a tool still running. Required, not optional — a store that silently declines it breaks a client with nothing logged.
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-B2Dq9ZwF.cjs';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-BQqFt06V.cjs';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-B2Dq9ZwF.js';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-BQqFt06V.js';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**