@dudousxd/nestjs-agent-core 0.13.0 → 0.15.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 +1 -1
- package/dist/index.cjs +47 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -7
- package/dist/index.d.ts +22 -7
- package/dist/index.js +47 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +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
|
-
- `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:
|
|
30
|
+
- `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
31
|
- `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
32
|
- `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.
|
|
33
33
|
- `AttachmentStagingStore.list(input)` / `AgentStore.referencedMediaIds(actorRef, mediaIds)` — the two halves of attachment housekeeping, split the way ownership is. `stage()` writes bytes before any message exists, so an upload the user never sent leaves media nothing points at; the HOST can enumerate that media (it stored it) but cannot see a transcript, and this library sees every transcript but never holds bytes. `list` returns `StagedAttachment` metadata (`mediaId`, `name`, `contentType`, `sizeBytes`, `createdAt` — no `url`, since `resolve` mints those per turn precisely so they can be short-lived); `referencedMediaIds` answers which of a set of ids a message that still exists carries, scoped to one actor. The answer is DERIVED from the surviving message rows on every call, never latched: `truncateFrom` deletes messages — regenerating a turn does exactly that — so a reference disappears, and a flag set at send time would pin the bytes for ever. Both are optional; a caller that cannot get an answer must collect nothing rather than read silence as "unreferenced". `AgentService.collectableAttachments` composes them, and deletes nothing.
|
package/dist/index.cjs
CHANGED
|
@@ -1915,6 +1915,18 @@ var ToolRegistry = class {
|
|
|
1915
1915
|
has(name) {
|
|
1916
1916
|
return this.entries.has(name);
|
|
1917
1917
|
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Give a name back, and report whether it was held. For an importer that registered tools on
|
|
1920
|
+
* someone else's behalf — the MCP client is the one in this repo — and has to hand back the ones
|
|
1921
|
+
* its source stopped offering.
|
|
1922
|
+
*
|
|
1923
|
+
* The registry cannot tell whether a caller owns a name, so it does not try: whoever registered a
|
|
1924
|
+
* name is responsible for tracking that it did. Unregistering a name it does not own would
|
|
1925
|
+
* silently take a tool away from whoever does.
|
|
1926
|
+
*/
|
|
1927
|
+
unregister(name) {
|
|
1928
|
+
return this.entries.delete(name);
|
|
1929
|
+
}
|
|
1918
1930
|
spec(name) {
|
|
1919
1931
|
return this.entries.get(name)?.spec;
|
|
1920
1932
|
}
|
|
@@ -2228,7 +2240,12 @@ var MAX_DELEGATION_DEPTH = 5;
|
|
|
2228
2240
|
var DEFAULT_MAX_AGENT_APPEARANCES = 1;
|
|
2229
2241
|
function delegationRefusal(args) {
|
|
2230
2242
|
const { deps, input, targetAgent } = args;
|
|
2231
|
-
const ancestry =
|
|
2243
|
+
const ancestry = [
|
|
2244
|
+
...input.delegationPath ?? [],
|
|
2245
|
+
...input.agentName !== void 0 ? [
|
|
2246
|
+
input.agentName
|
|
2247
|
+
] : []
|
|
2248
|
+
];
|
|
2232
2249
|
const appearances = ancestry.filter((name) => name === targetAgent).length;
|
|
2233
2250
|
const maxAppearances = deps.maxAgentAppearances ?? DEFAULT_MAX_AGENT_APPEARANCES;
|
|
2234
2251
|
if (appearances >= maxAppearances) {
|
|
@@ -2763,6 +2780,27 @@ __name(runIntake, "runIntake");
|
|
|
2763
2780
|
var PARALLEL_TOOLS_PATCH = "agent:parallel-tools";
|
|
2764
2781
|
var CANCELLATION_PATCH = "agent:cancellation";
|
|
2765
2782
|
var SELECTED_HISTORY_PATCH = "agent:selected-history";
|
|
2783
|
+
var PROMPT_STAGES_PATCH = "agent:prompt-stages";
|
|
2784
|
+
async function resolvePromptStages(deps, hooks) {
|
|
2785
|
+
const configured = {
|
|
2786
|
+
memory: deps.memory !== void 0,
|
|
2787
|
+
retriever: deps.retriever !== void 0,
|
|
2788
|
+
skills: deps.skills !== void 0
|
|
2789
|
+
};
|
|
2790
|
+
return await (hooks.patched?.(PROMPT_STAGES_PATCH) ?? Promise.resolve(true)) ? hooks.step("run:prompt-stages", () => Promise.resolve(configured)) : configured;
|
|
2791
|
+
}
|
|
2792
|
+
__name(resolvePromptStages, "resolvePromptStages");
|
|
2793
|
+
var UNSERVED_MEMORY = {
|
|
2794
|
+
scopes: [],
|
|
2795
|
+
entries: [],
|
|
2796
|
+
omitted: 0,
|
|
2797
|
+
pinnedOmitted: 0
|
|
2798
|
+
};
|
|
2799
|
+
var UNSERVED_SKILLS = {
|
|
2800
|
+
scopes: [],
|
|
2801
|
+
entries: [],
|
|
2802
|
+
omitted: 0
|
|
2803
|
+
};
|
|
2766
2804
|
async function haltIfCancelled(hooks, cancellable, name) {
|
|
2767
2805
|
const observe = hooks.cancelled;
|
|
2768
2806
|
if (!cancellable || observe === void 0) {
|
|
@@ -3290,6 +3328,7 @@ async function runAgentLoop(deps, input, hooks) {
|
|
|
3290
3328
|
agentName: input.agentName
|
|
3291
3329
|
} : {}
|
|
3292
3330
|
});
|
|
3331
|
+
const stages = await resolvePromptStages(deps, hooks);
|
|
3293
3332
|
const startedAt = await hooks.step("run:started-at", () => Promise.resolve(Date.now()));
|
|
3294
3333
|
await hooks.step("persist:run:start", async () => {
|
|
3295
3334
|
const promptHash = (0, import_node_crypto.createHash)("sha256").update(system).digest("hex");
|
|
@@ -3307,9 +3346,9 @@ async function runAgentLoop(deps, input, hooks) {
|
|
|
3307
3346
|
});
|
|
3308
3347
|
});
|
|
3309
3348
|
let memoryDigest;
|
|
3310
|
-
if (
|
|
3349
|
+
if (stages.memory) {
|
|
3311
3350
|
const config = deps.memory;
|
|
3312
|
-
memoryDigest = await hooks.step("memory:digest", () => offerMemories({
|
|
3351
|
+
memoryDigest = await hooks.step("memory:digest", () => config === void 0 ? Promise.resolve(UNSERVED_MEMORY) : offerMemories({
|
|
3313
3352
|
config,
|
|
3314
3353
|
ctx: skillContext(input),
|
|
3315
3354
|
query: input.userText
|
|
@@ -3336,16 +3375,16 @@ ${block}`;
|
|
|
3336
3375
|
});
|
|
3337
3376
|
}
|
|
3338
3377
|
let injectedPassages;
|
|
3339
|
-
if (
|
|
3378
|
+
if (stages.retriever) {
|
|
3340
3379
|
const retriever = deps.retriever;
|
|
3341
3380
|
const topK = deps.retrievalTopK ?? 5;
|
|
3342
3381
|
const passages = await hooks.step("retrieve", () => spanned("retrieval", hooks.runId, {
|
|
3343
3382
|
runId: hooks.runId,
|
|
3344
3383
|
queryLength: input.userText.length,
|
|
3345
3384
|
topK
|
|
3346
|
-
}, () => retriever
|
|
3385
|
+
}, () => retriever?.retrieve(input.userText, {
|
|
3347
3386
|
topK
|
|
3348
|
-
}), (retrieved) => ({
|
|
3387
|
+
}) ?? Promise.resolve([]), (retrieved) => ({
|
|
3349
3388
|
count: retrieved.length
|
|
3350
3389
|
})));
|
|
3351
3390
|
if (passages.length > 0) {
|
|
@@ -3361,9 +3400,9 @@ ${buildContextBlock(passages)}`;
|
|
|
3361
3400
|
});
|
|
3362
3401
|
}
|
|
3363
3402
|
let skillOffer;
|
|
3364
|
-
if (
|
|
3403
|
+
if (stages.skills) {
|
|
3365
3404
|
const config = deps.skills;
|
|
3366
|
-
skillOffer = await hooks.step("skills:catalog", () => offerSkills(config, skillContext(input)));
|
|
3405
|
+
skillOffer = await hooks.step("skills:catalog", () => config === void 0 ? Promise.resolve(UNSERVED_SKILLS) : offerSkills(config, skillContext(input)));
|
|
3367
3406
|
const block = skillOffer.entries.length > 0 ? buildSkillsBlock(skillOffer.entries) : "";
|
|
3368
3407
|
if (block.length > 0) {
|
|
3369
3408
|
system = `${system}
|