@dudousxd/nestjs-agent-core 0.15.5 → 0.17.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
@@ -35,8 +35,76 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
35
35
  - `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
36
  - `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
37
  - `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.
38
39
  - `runAgentLoop(deps, input, hooks)` — the loop; the NestJS package drives it from both runners
39
40
 
41
+ ## Guardrails — `@dudousxd/nestjs-agent-core/guardrails`
42
+
43
+ PII, secret, prompt-injection and tool-poisoning detection, a rule engine with reversible redaction,
44
+ and the adapter onto the processor seams above. A separate entry point with **no imports at all** —
45
+ no NestJS, no agent loop, no Node-only API — so a process that never runs the loop (a gateway
46
+ proxying raw OpenAI / Anthropic / MCP traffic) uses the same detectors and engine.
47
+
48
+ ```ts
49
+ import { createGuardrails } from '@dudousxd/nestjs-agent-core/guardrails';
50
+
51
+ const guardrails = createGuardrails({
52
+ pii: 'redact', // reversible: the model sees [EMAIL_1], the reader the address
53
+ secrets: 'block',
54
+ injection: { threshold: 0.6 }, // default action: block
55
+ toolPoisoning: true, // for guardrails.screenTool (see the MCP package's `screen`)
56
+ rules: (ctx) => rulesForTenant(ctx.actor?.tenantRef), // more rules, resolved per scan
57
+ onEvent: (event) => audit.write(event), // hits carry fingerprints, never values
58
+ fingerprint: (value) => hmac(value),
59
+ });
60
+
61
+ AgentModule.forRoot({
62
+ inputProcessors: [guardrails.input],
63
+ outputProcessors: [guardrails.output],
64
+ // …
65
+ });
66
+ ```
67
+
68
+ Where each stage runs, and what each action does there:
69
+
70
+ | Stage | Seen by | `redact` | `block` / `approve` |
71
+ |---|---|---|---|
72
+ | `llm_request` — system, messages, earlier tool-call arguments | `guardrails.input`, every step | placeholders, restorable | the run fails: `ProcessorFailedError` whose `cause` is a `GuardrailBlockedError` |
73
+ | `tool_result` — every tool result riding in the prompt | `guardrails.input`, every step, one result at a time (`match.tools` applies) | placeholders, restorable | the result is **withheld**: the model is told why and the turn goes on |
74
+ | `llm_response` — each step's answer | `guardrails.output` | one-way placeholders; the thread's own placeholders are then restored | `reject` → `OutputRejectedError` |
75
+ | `tool_args` — the calls a step asks for | `guardrails.output` (read-only there) and `guardrails.wrapTool` | only through `wrapTool` | `reject` the step, or `wrapTool` throws and the model reads the tool's failure |
76
+ | `tool_description` | `guardrails.screenTool(tool)` | — | `{ allowed: false, reason }` |
77
+
78
+ - **Streaming stays on.** The output processor declares `incremental` with a 256-character lookback
79
+ (`incremental: { lookbackChars }` widens it, `false` gates the whole answer). One-way placeholders
80
+ are numbered per pass, so a prefix and its extension agree, which is the promise `incremental`
81
+ makes.
82
+ - **Placeholders live per thread** in a `VaultStore` (default `InMemoryVaultStore`, bounded). A run
83
+ resumed in another process finds no vault and its placeholders reach the reader unrestored — the
84
+ values still never reach the model. `Vault.toJSON` / `Vault.fromJSON` back a shared store, which
85
+ then holds the raw values.
86
+ - **`wrapTool(name, handler)`** is the one place a tool's arguments can be rewritten: it restores the
87
+ thread's placeholders (the model only ever saw `[EMAIL_1]`, the tool needs the address) and runs
88
+ the `tool_args` rules on what the tool is about to receive. PII is deliberately not a `tool_args`
89
+ stage of the `pii` shorthand for that reason; secrets are.
90
+ - **Re-sent history is not re-refused.** Content only in earlier turns is redacted one-way instead
91
+ of blocked, and each hit is reported once per turn — every step re-sends the prompt and an
92
+ incremental gate re-reads a growing answer.
93
+ - **A rule** is `{ id, stages, detectors, action, match?: { sources?, tools? }, when?, options? }`.
94
+ `when(ctx)` carries anything scope-like (tenant, roles, model); a `{ kind: 'custom', name, detect }`
95
+ detector carries anything the built-ins do not (NER, a classifier, a moderation endpoint, an LLM
96
+ judge). A detector that throws follows the rule's `failMode` (`open` by default).
97
+
98
+ Standalone, without the loop: `detectPii` (Luhn + card brands, CPF/CNPJ incl. the 2026 alphanumeric
99
+ CNPJ, SSN, IBAN mod 97, phones, IPv4), `detectSecrets` (provider key formats, JWT, PEM, entropy-gated
100
+ generic assignments), `scoreInjection` (EN / PT-BR / ES, chat-template spoofing, hidden Unicode tag
101
+ characters, base64 payloads, markdown exfiltration), `scoreToolText` + `toolText`, `detectRegex`,
102
+ `detectKeywords`, then `scan(rules, ctx, segments, vault)` to combine them. For provider wire formats:
103
+ `requestSlots` / `responseSlots` / `toolResultSlots` (OpenAI chat, Anthropic Messages, MCP results —
104
+ each a text plus a setter that writes back in place), `refuseResponse`, and `StreamGuard`, which
105
+ redacts, restores and blocks an OpenAI or Anthropic SSE stream in windows that never cut through a
106
+ finding or a placeholder.
107
+
40
108
  ## License
41
109
 
42
110
  MIT © Davide Carvalho