@dudousxd/nestjs-agent-core 0.19.0 → 0.21.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
@@ -27,6 +27,7 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
27
27
  - `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
28
  - `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
29
  - `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.
30
+ - `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
31
  - `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
32
  - `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
33
  - `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.
@@ -35,7 +36,8 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
35
36
  - `RunCancelledError` / `AgentLoopHooks.cancelled()` — real cancellation. The loop asks `hooks.cancelled()` at the points where stopping is safe and cheap (between steps, before the next model call, before a turn's tools are dispatched), ALWAYS from inside a checkpoint, so the answer is journaled and a cancel arriving between two replays can never change a branch a replayed position already took; the positions themselves sit behind `hooks.patched('agent:cancellation')`, so a run already in flight keeps its recorded shape. A tool already executing is never interrupted — there is no un-executing a side effect, and abandoning a dispatched step leaves a journal holding a dispatch whose result never lands. Observing a cancel throws `RunCancelledError`, which unwinds through the path a suspend already uses; the RUNNER settles it, recording the run `cancelled` (`AgentStore.recordRunEnd`'s third terminal, never `failed`) and ending the stream on a `{ kind: 'cancelled' }` frame rather than failing it, so a user pressing Stop is not in anybody's error rate.
36
37
  - `isControlFlowSignal(error)` / `isReplayIntegrityError(error)` — recognize a durable suspend and a checkpoint refusal without importing the durable packages (a `Symbol.for` marker and a class name, respectively). Rethrow both untouched from any `catch` in a workflow body.
37
38
  - `settleAll(tasks)` / `SettledTask<T>` — the implementation behind the optional `AgentLoopHooks.parallel`, which is how a turn's `read` tool calls run concurrently. It invokes every task synchronously, in list order, before awaiting any of them (so a runner that takes checkpoint positions on the call keeps them in call order), and resolves only once all of them have settled (so a runner that unwinds a turn by throwing never abandons a sibling mid-dispatch). A runner that assigns positions anywhere else simply omits the hook and the loop stays sequential.
38
- - `AgentStreamEvent` / `encodeStreamEvent` / `decodeStreamEvent` — the live-stream vocabulary (text, reasoning, tool calls with optional `parentId` nesting, `elicitation`, `approval-requested` with `approver`/`expiresAt`, server-pushed `ui` components, `title`, `cancelled`). It is also the contract a runner that is not this loop writes to get the React client for free: see [docs/stream-protocol.md](../../docs/stream-protocol.md). `AgentUiComponent` and `AgentApprovalRequest` are the payload types.
39
+ - `AgentStreamEvent` / `encodeStreamEvent` / `decodeStreamEvent` — the live-stream vocabulary (text, reasoning, tool calls with optional `parentId` nesting, `elicitation`, `approval-requested` with `approver`/`expiresAt`, server-pushed `ui` components, `title`, `cancelled`). It is also the contract a runner that is not this loop writes to get the React client for free: see [docs/stream-protocol.md](../../docs/stream-protocol.md). `AgentUiComponent`, `AgentApprovalRequest` and `AgentApprovalSettlement` (the `approval-settled` frame) are the payload types.
40
+ - `ApprovalPolicy` / `DefaultApprovalPolicy` / `mayDecideApproval` — **who approves an `action` call, and for how long.** `requirementFor(tool, actor, thread)` answers `{ required, approver, ttlMs? }`: `required: false` runs the call straight away, `approver` is `'requester'` (the thread's own actor — the default) or a role, `ttlMs` bounds the wait. The answer is taken INSIDE the call's `persist:toolcall` checkpoint (together with the thread's remembered approvals, `AgentStore.rememberedApprovals`), so a replay reads the branch back instead of re-asking a policy that changed while the run was parked. The loop streams `approval-requested` with the approver and `expiresAt`, hands the ttl to `AgentLoopHooks.awaitApproval(call, ctx, { timeoutMs })`, and settles a lapsed wait (`Decision.expired`) as `ToolCallStatus 'expired'` — denied to the model with "the approval expired", streamed as `tool-output-denied` + `approval-settled { status: 'expired' }`. `Decision.remember` approves later calls of the same tool in the same thread; `Decision.decidedVia` records the surface. `StoredMessage.approvals` carries the persisted record on a reloaded thread. A run claimed before this existed keeps waiting on the requester with no timeout.
39
41
  - `observeTurnFrames` / `withTurnFrames` — what a model turn streamed that its result does not carry: the model's thinking (`reasoning` text, and `reasoningMs` summed over each burst of consecutive reasoning frames) and pushed `ui` components (first-seen order, last props per id). The loop and the dispatched `llm` step wrap the provider's sink with it inside the model checkpoint, so every provider that streams the vocabulary gets reasoning persisted (`StoredMessage.reasoning` / `reasoningMs` / `ui`) and a replay reads the journaled numbers back. A provider may set `ModelTurnResult.reasoning` / `reasoningMs` / `ui` itself; its values win. The step's `reasoningMs` also rides the `step-finish` frame.
40
42
  - `ToolPresentation` / `ToolResultView` / `ToolCatalogEntry` — how a person-facing surface talks about a tool without naming it (`ToolSpec.presentation`, set by `@AiTool({ presentation })`; never shown to the model). `ToolRegistry.visibleSpecs(actor, policy, allowList)` returns the whole specs behind the same gates `definitionsFor` applies, which is what `GET /agent/tools` serves.
41
43
  - `runAgentLoop(deps, input, hooks)` — the loop; the NestJS package drives it from both runners
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-Q2rmGeaG.cjs';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CwXibnce.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-Q2rmGeaG.js';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CwXibnce.js';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**