@dudousxd/nestjs-agent-core 0.38.0 → 0.39.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
@@ -23,7 +23,7 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
23
23
  - `ToolHandler.describe?({ actor, threadId?, agentName? })` — **a per-turn description.** Called when the turn's tool list is built, after every gate; what it returns replaces the spec's `description` / `inputSchema` in what the model sees (`registry.definitionsFor(actor, policy, allowList, { threadId, agentName })`). The registry still validates calls against the registered schema, so a tool whose input varies per turn registers a permissive one and validates in `execute`.
24
24
  - `@dudousxd/nestjs-agent-core/genui` (+ `/genui/builtins`) — **the generative-UI catalog**, an isomorphic entry (no server-only import; a spec holds that line) a browser imports too: `defineComponent` / `defineCatalog` (Standard Schema or JSON Schema props), validation, `catalogToModelText`, `componentToText` fallbacks, tree helpers, and `genuiTools(catalog, { mode, terminal, showTool, resolveCatalog, … })` — the tools that push components, with an optional per-request catalog resolver consulted on every call and every turn's description. NestJS apps use `AgentGenuiModule` (`@dudousxd/nestjs-agent/genui`) instead of calling `genuiTools` themselves.
25
25
  - `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.
26
- - `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.
26
+ - `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 a run of its own — started from a journaled `detach:<toolCallId>` step, NOT a `ctx.startChild`, so a Stop on the delegating turn does not cascade to it — 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 (`patch:agent:detached-unlinked` + `detach:` — or `spawn:` on a run journaled by `@dudousxd/nestjs-agent` 1.19.2 or earlier — versus the awaited child's `signal:child:`) differ. A detached run outlives a Stop on the turn that started it; it is stopped by its own `runId`, and the card follows it to `delivered`, `failed` or `cancelled`. 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 (once — a thread already holding a message from that run is left alone, so the body and the runner may both call it), 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.
27
27
  - `RolesPolicy.can(actor, tool): boolean | Promise<boolean>` — the tool authorization seam
28
28
  - `HistoryPolicy` — the ceiling on how much of a thread rides into a turn. `select(messages, ctx)` must be pure; the loop runs it INSIDE `load:thread`, so the ceiling bounds the journal as well as the prompt — the checkpoint holds the selected messages rather than the whole `ThreadDetail`, and a run recorded before that keeps the old payload via `ctx.patched('agent:selected-history')`. The optional `summarize(dropped, ctx)` may call a model and runs in its own `history:summarize` checkpoint (which is also the only case where the dropped messages are journaled at all). `windowHistory`, `summarizeWithModel` and `estimateMessageTokens` are the built-ins. A policy MAY also declare `maxMessages` — the most messages `select` can ever keep — which the loop passes to `ThreadTurnReader` below as the store's read bound. Declaring it is a promise that `select` keeps at most that many, and that they are the NEWEST ones; a ceiling expressed only in tokens declares nothing, since one message can be four tokens or forty thousand and no row count follows from a budget.
29
29
  - `ThreadTurnReader.loadThreadForTurn({ threadId, messageLimit })` — **the read a turn actually needs.** `getThread` materializes a thread's whole transcript (every message row, every attachment, every tool output it ever recorded) to build a prompt bounded to its last few messages: on a 50-turn thread whose turns each ran a 50 KB tool that is ~2.6 MB per load, over 99% of it tool output. This read is bounded by the database (`order by created_at desc, id desc limit ?`, reversed for the prompt) and projected to the columns a model turn reads — `usage`, `follow_ups` and `run_id` stay in the table. The loop probes for it STRUCTURALLY, the same way `AgentService` probes `defaultAgentForThread`: a store that does not implement it keeps working through the full read, with no config change and no warning. `messageLimit` comes from `HistoryPolicy.maxMessages`, and is omitted — meaning read everything — for a policy that summarizes (`summarize` is handed what `select` DROPPED, and a read bounded to what it keeps drops nothing) or whose ceiling is only a token budget. Which branch ran is invisible to the journal by construction: both produce the same three answers, so the payload `load:thread` records is identical either way and a deployment's choice of store can never decide a run's checkpoints. `hasAssistantMessage` is answered over the WHOLE thread, never the page — it decides a `thread-start` intake, and a window holding only the user's last questions belongs to a conversation that has still been answered.
@@ -34,7 +34,7 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
34
34
  - `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.
35
35
  - `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.
36
36
  - `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.
37
- - `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.
37
+ - `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 — or one still waiting in a thread's queue — 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.
38
38
  - `agentFailureCode(error)` — the stream error code for a failed run (`cancelled`, `quota_exceeded`, `output_rejected`, `structured_output_invalid`, `replay_diverged` for the durable runtime refusing a checkpoint position, `model_no_output` for a model call that produced nothing, else `run_failed`), so a control doing its job never reads as the model breaking. `streamFailure(error)` is the `{ code, message }` a runner closes the stream with: for a crash the message is `RUN_FAILED_MESSAGE` in production and the raw text elsewhere (`exposeStreamErrorDetails` decides it outright) — the error itself belongs in the log and on the run row.
39
39
  - `settleDanglingToolCalls(messages, outcomes?)` / `danglingToolCallIds(messages)` — give every tool call in a history a result. A turn that dies mid-step leaves an assistant message asking for tools and answered by nothing, which a provider refuses, so every later turn on the thread fails too. The loop settles them inside `load:thread` (no new position; a replay reads the recorded payload) from the calls' own rows where the store implements the optional `AgentStore.toolCallOutcomes` — a tool that DID run hands the model its real output, so it is not run again — and otherwise with `UNFINISHED_TOOL_CALL`. `AgentStore.failUnsettledToolCalls(runId, error)` (optional) is what a failing run calls so no approval card waits on it; `settleDeadRun(store, { runId, threadId?, failure? })` is the same for a caller with no journal left to write to.
40
40
  - `AiToolCtx.idempotencyKey` (`<runId>:<toolCallId>`) / `AiToolCtx.toolCallId` — the same for every execution of one call, so a tool re-run after a worker died between its side effect and its checkpoint (or by a transient retry) can be made to land on the first attempt. `toolCallContext(ctx, toolCallId)` builds it for a runner's own dispatched step.
@@ -1,4 +1,4 @@
1
- import { A as AgentStreamEvent } from '../stream-events-CgWqAI-1.cjs';
1
+ import { A as AgentStreamEvent } from '../stream-events-CbVEowYb.cjs';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { A as AgentStreamEvent } from '../stream-events-CgWqAI-1.js';
1
+ import { A as AgentStreamEvent } from '../stream-events-CbVEowYb.js';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -1,8 +1,8 @@
1
1
  import { a as Catalog, J as JsonSchema, G as GenuiValidation, b as GenuiIssue } from '../catalog-CrzetM3_.cjs';
2
2
  export { A as AjvLike, c as COMPONENT_NAME, d as CatalogOptions, C as ComponentDefinition, e as JsonSchemaValidator, P as PropsSchema, f as ajvValidator, g as builtinJsonSchemaValidator, h as defineCatalog, i as defineComponent, j as formatIssues, k as isStandardSchema, t as toJsonSchema, l as toSnakeCase, m as toolNameFor, v as validateProps, n as validatePropsSync } from '../catalog-CrzetM3_.cjs';
3
3
  import { StandardSchemaV1, StandardJSONSchemaV1 } from '@standard-schema/spec';
4
- import { T as ToolHandler } from '../tool-DuJ-_qXM.cjs';
5
- import { a as Actor, T as ToolSpec, b as ToolPresentation } from '../stream-events-CgWqAI-1.cjs';
4
+ import { T as ToolHandler } from '../tool-DZYLEKnl.cjs';
5
+ import { a as Actor, T as ToolSpec, b as ToolPresentation } from '../stream-events-CbVEowYb.cjs';
6
6
 
7
7
  /**
8
8
  * One node of a composed UI: a catalog component, its props, and — for components declared with
@@ -1,8 +1,8 @@
1
1
  import { a as Catalog, J as JsonSchema, G as GenuiValidation, b as GenuiIssue } from '../catalog-CrzetM3_.js';
2
2
  export { A as AjvLike, c as COMPONENT_NAME, d as CatalogOptions, C as ComponentDefinition, e as JsonSchemaValidator, P as PropsSchema, f as ajvValidator, g as builtinJsonSchemaValidator, h as defineCatalog, i as defineComponent, j as formatIssues, k as isStandardSchema, t as toJsonSchema, l as toSnakeCase, m as toolNameFor, v as validateProps, n as validatePropsSync } from '../catalog-CrzetM3_.js';
3
3
  import { StandardSchemaV1, StandardJSONSchemaV1 } from '@standard-schema/spec';
4
- import { T as ToolHandler } from '../tool-DklsS3JX.js';
5
- import { a as Actor, T as ToolSpec, b as ToolPresentation } from '../stream-events-CgWqAI-1.js';
4
+ import { T as ToolHandler } from '../tool-_iq4xRyk.js';
5
+ import { a as Actor, T as ToolSpec, b as ToolPresentation } from '../stream-events-CbVEowYb.js';
6
6
 
7
7
  /**
8
8
  * One node of a composed UI: a catalog component, its props, and — for components declared with
@@ -1,6 +1,6 @@
1
- import { I as InputProcessor, O as OutputProcessor } from '../processors-C0snQzZJ.cjs';
2
- import { T as ToolHandler } from '../tool-DuJ-_qXM.cjs';
3
- import { a as Actor } from '../stream-events-CgWqAI-1.cjs';
1
+ import { I as InputProcessor, O as OutputProcessor } from '../processors-6p9nrqEi.cjs';
2
+ import { T as ToolHandler } from '../tool-DZYLEKnl.cjs';
3
+ import { a as Actor } from '../stream-events-CbVEowYb.cjs';
4
4
  import '@standard-schema/spec';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
- import { I as InputProcessor, O as OutputProcessor } from '../processors-D5110pit.js';
2
- import { T as ToolHandler } from '../tool-DklsS3JX.js';
3
- import { a as Actor } from '../stream-events-CgWqAI-1.js';
1
+ import { I as InputProcessor, O as OutputProcessor } from '../processors-Bp5XmF6V.js';
2
+ import { T as ToolHandler } from '../tool-_iq4xRyk.js';
3
+ import { a as Actor } from '../stream-events-CbVEowYb.js';
4
4
  import '@standard-schema/spec';
5
5
 
6
6
  /**
package/dist/index.cjs CHANGED
@@ -140,9 +140,11 @@ __export(src_exports, {
140
140
  filterToolsByEnabled: () => filterToolsByEnabled,
141
141
  filterToolsByRole: () => filterToolsByRole,
142
142
  findCatalogModel: () => findCatalogModel,
143
+ findPersona: () => findPersona,
143
144
  gateFollowUps: () => gateFollowUps,
144
145
  gateTail: () => gateTail,
145
146
  hashConfirmToken: () => hashConfirmToken,
147
+ intersectAllowLists: () => intersectAllowLists,
146
148
  invokeWithTransientRetry: () => invokeWithTransientRetry,
147
149
  isChatQueueStore: () => isChatQueueStore,
148
150
  isControlFlowSignal: () => isControlFlowSignal,
@@ -160,6 +162,7 @@ __export(src_exports, {
160
162
  observeTurnFrames: () => observeTurnFrames,
161
163
  offerMemories: () => offerMemories,
162
164
  offerSkills: () => offerSkills,
165
+ personaCatalogEntry: () => personaCatalogEntry,
163
166
  publishAgentDelegated: () => publishAgentDelegated,
164
167
  publishAgentMemoryResolved: () => publishAgentMemoryResolved,
165
168
  publishAgentMemoryWritten: () => publishAgentMemoryWritten,
@@ -189,6 +192,7 @@ __export(src_exports, {
189
192
  resolveGateLookback: () => resolveGateLookback,
190
193
  resolveMemoryDigest: () => resolveMemoryDigest,
191
194
  resolveOutputGateMode: () => resolveOutputGateMode,
195
+ resolvePersonaAlias: () => resolvePersonaAlias,
192
196
  resolveSkillCatalog: () => resolveSkillCatalog,
193
197
  resolveToolTransientRetryNumbers: () => resolveToolTransientRetryNumbers,
194
198
  rollupThreadUsage: () => rollupThreadUsage,
@@ -565,6 +569,9 @@ function queuedMessageView(message) {
565
569
  ...message.agentName !== void 0 ? {
566
570
  agentName: message.agentName
567
571
  } : {},
572
+ ...message.persona !== void 0 ? {
573
+ persona: message.persona
574
+ } : {},
568
575
  ...message.model !== void 0 ? {
569
576
  model: message.model
570
577
  } : {},
@@ -957,6 +964,54 @@ function filterToolsByAllowList(tools, allowedTools) {
957
964
  }
958
965
  __name(filterToolsByAllowList, "filterToolsByAllowList");
959
966
 
967
+ // src/personas.ts
968
+ function intersectAllowLists(first, second) {
969
+ if (first === void 0) {
970
+ return second === void 0 ? void 0 : [
971
+ ...second
972
+ ];
973
+ }
974
+ if (second === void 0) {
975
+ return [
976
+ ...first
977
+ ];
978
+ }
979
+ const allowed = new Set(second);
980
+ return first.filter((name) => allowed.has(name));
981
+ }
982
+ __name(intersectAllowLists, "intersectAllowLists");
983
+ function findPersona(definition, id) {
984
+ if (id === void 0) {
985
+ return void 0;
986
+ }
987
+ return definition?.personas?.find((persona) => persona.id === id);
988
+ }
989
+ __name(findPersona, "findPersona");
990
+ function personaCatalogEntry(persona) {
991
+ return {
992
+ id: persona.id,
993
+ label: persona.label,
994
+ ...persona.description !== void 0 ? {
995
+ description: persona.description
996
+ } : {}
997
+ };
998
+ }
999
+ __name(personaCatalogEntry, "personaCatalogEntry");
1000
+ function resolvePersonaAlias(definitions, name) {
1001
+ for (const definition of definitions) {
1002
+ for (const persona of definition.personas ?? []) {
1003
+ if (persona.aliases?.includes(name) === true) {
1004
+ return {
1005
+ agent: definition.name,
1006
+ persona: persona.id
1007
+ };
1008
+ }
1009
+ }
1010
+ }
1011
+ return void 0;
1012
+ }
1013
+ __name(resolvePersonaAlias, "resolvePersonaAlias");
1014
+
960
1015
  // src/history.ts
961
1016
  function estimateMessageTokens(message) {
962
1017
  const extras = (message.toolCalls !== void 0 ? JSON.stringify(message.toolCalls).length : 0) + (message.toolResults !== void 0 ? JSON.stringify(message.toolResults).length : 0);
@@ -2745,7 +2800,8 @@ function detachedUnsettled(args) {
2745
2800
  __name(detachedUnsettled, "detachedUnsettled");
2746
2801
  async function settleUnsettledDelegation(args) {
2747
2802
  const { store, delivery, agent, runId, status } = args;
2748
- if (await store.getThread(delivery.threadId) === null) {
2803
+ const thread = await store.getThread(delivery.threadId);
2804
+ if (thread === null || thread.messages.some((message) => message.runId === runId)) {
2749
2805
  return;
2750
2806
  }
2751
2807
  await store.appendMessage({
@@ -2895,7 +2951,7 @@ var ToolRegistry = class {
2895
2951
  * a call can reach here from a replayed durable step or an approval granted before the flag
2896
2952
  * moved, neither of which went through `definitionsFor` again) and re-parses the input via Zod.
2897
2953
  */
2898
- async invoke(name, input, ctx, policy) {
2954
+ async invoke(name, input, ctx, policy, options = {}) {
2899
2955
  const entry = this.entries.get(name);
2900
2956
  if (entry === void 0) {
2901
2957
  throw new ToolNotFoundError(name);
@@ -2909,6 +2965,9 @@ var ToolRegistry = class {
2909
2965
  if (!await canActorUseTool(ctx.actor, entry.handler)) {
2910
2966
  throw new ToolForbiddenError(name);
2911
2967
  }
2968
+ if (options.allowedTools !== void 0 && !options.allowedTools.includes(name)) {
2969
+ throw new ToolForbiddenError(name);
2970
+ }
2912
2971
  const validation = await entry.spec.inputSchema["~standard"].validate(input);
2913
2972
  if (validation.issues !== void 0) {
2914
2973
  throw new ToolInputInvalidError(name, validation.issues);
@@ -3260,16 +3319,10 @@ async function resolvePrompt(prompt, ctx) {
3260
3319
  return typeof prompt === "function" ? prompt(ctx) : prompt;
3261
3320
  }
3262
3321
  __name(resolvePrompt, "resolvePrompt");
3263
- async function resolveSystemPrompt(deps, input) {
3264
- const ctx = {
3265
- actor: input.actor,
3266
- agentName: input.agentName ?? "default",
3267
- ...input.pageContext !== void 0 ? {
3268
- pageContext: input.pageContext
3269
- } : {}
3270
- };
3322
+ async function resolveSystemPrompt(deps, input, persona) {
3323
+ const ctx = promptContext(input, persona);
3271
3324
  const sections = [
3272
- await resolvePrompt(deps.systemPrompt, ctx)
3325
+ persona?.prompt ?? await resolvePrompt(deps.systemPrompt, ctx)
3273
3326
  ];
3274
3327
  for (const contribute of deps.promptContributors ?? []) {
3275
3328
  const section = await contribute(ctx);
@@ -3280,6 +3333,67 @@ async function resolveSystemPrompt(deps, input) {
3280
3333
  return sections.join("\n\n");
3281
3334
  }
3282
3335
  __name(resolveSystemPrompt, "resolveSystemPrompt");
3336
+ function withoutPersona(input) {
3337
+ const { persona: _dropped, ...rest } = input;
3338
+ return rest;
3339
+ }
3340
+ __name(withoutPersona, "withoutPersona");
3341
+ function promptContext(input, persona) {
3342
+ return {
3343
+ actor: input.actor,
3344
+ agentName: input.agentName ?? "default",
3345
+ ...input.pageContext !== void 0 ? {
3346
+ pageContext: input.pageContext
3347
+ } : {},
3348
+ ...persona !== void 0 ? {
3349
+ persona: {
3350
+ id: persona.id,
3351
+ label: persona.label
3352
+ }
3353
+ } : {}
3354
+ };
3355
+ }
3356
+ __name(promptContext, "promptContext");
3357
+ async function resolveTurnPersona(deps, input) {
3358
+ const persona = findPersona(deps, input.persona);
3359
+ if (persona === void 0) {
3360
+ return null;
3361
+ }
3362
+ const ref = {
3363
+ id: persona.id,
3364
+ label: persona.label
3365
+ };
3366
+ let prompt;
3367
+ if (persona.systemPrompt !== void 0) {
3368
+ const ctx = promptContext(input, ref);
3369
+ const basePrompt = typeof persona.systemPrompt === "function" ? await resolvePrompt(deps.systemPrompt, ctx) : void 0;
3370
+ prompt = await resolvePrompt(persona.systemPrompt, {
3371
+ ...ctx,
3372
+ ...basePrompt !== void 0 ? {
3373
+ basePrompt
3374
+ } : {}
3375
+ });
3376
+ }
3377
+ return {
3378
+ ...ref,
3379
+ ...persona.allowedTools !== void 0 ? {
3380
+ allowedTools: [
3381
+ ...persona.allowedTools
3382
+ ]
3383
+ } : {},
3384
+ ...prompt !== void 0 ? {
3385
+ prompt
3386
+ } : {}
3387
+ };
3388
+ }
3389
+ __name(resolveTurnPersona, "resolveTurnPersona");
3390
+ function personaRefusal(persona, toolName) {
3391
+ if (persona?.allowedTools === void 0 || persona.allowedTools.includes(toolName)) {
3392
+ return null;
3393
+ }
3394
+ return `(tool "${toolName}" is not available to the "${persona.id}" persona)`;
3395
+ }
3396
+ __name(personaRefusal, "personaRefusal");
3283
3397
  function extractTask(input) {
3284
3398
  if (typeof input === "object" && input !== null && "task" in input) {
3285
3399
  const task = input.task;
@@ -3521,6 +3635,9 @@ function processorContext(input, step) {
3521
3635
  step,
3522
3636
  ...input.agentName !== void 0 ? {
3523
3637
  agentName: input.agentName
3638
+ } : {},
3639
+ ...input.persona !== void 0 ? {
3640
+ persona: input.persona
3524
3641
  } : {}
3525
3642
  };
3526
3643
  }
@@ -3684,6 +3801,9 @@ function toolContext(deps, input, hooks) {
3684
3801
  ...input.agentName !== void 0 ? {
3685
3802
  agentName: input.agentName
3686
3803
  } : {},
3804
+ ...input.persona !== void 0 ? {
3805
+ persona: input.persona
3806
+ } : {},
3687
3807
  ...input.pageContext !== void 0 ? {
3688
3808
  pageContext: input.pageContext
3689
3809
  } : {},
@@ -3731,6 +3851,9 @@ async function runIntake(intake, deps, input, hooks, writer, threadHasAssistant)
3731
3851
  ],
3732
3852
  ...input.agentName !== void 0 ? {
3733
3853
  agentName: input.agentName
3854
+ } : {},
3855
+ ...input.persona !== void 0 ? {
3856
+ persona: input.persona
3734
3857
  } : {}
3735
3858
  });
3736
3859
  await deps.store.recordToolCall({
@@ -4076,6 +4199,7 @@ async function invokeClaimedTool(turn, claimed) {
4076
4199
  if (claimed.toolType === "memory") {
4077
4200
  return await rememberIntoTurn(turn, claimed, startedAt);
4078
4201
  }
4202
+ const invokeAllowList = turn.persona?.allowedTools === void 0 ? void 0 : intersectAllowLists(deps.toolAllowList, turn.persona.allowedTools);
4079
4203
  let raw;
4080
4204
  if (hooks.dispatchTool) {
4081
4205
  const stepCtx = {
@@ -4086,6 +4210,9 @@ async function invokeClaimedTool(turn, claimed) {
4086
4210
  ...input.agentName !== void 0 ? {
4087
4211
  agentName: input.agentName
4088
4212
  } : {},
4213
+ ...input.persona !== void 0 ? {
4214
+ persona: input.persona
4215
+ } : {},
4089
4216
  ...input.pageContext !== void 0 ? {
4090
4217
  pageContext: input.pageContext
4091
4218
  } : {}
@@ -4094,6 +4221,9 @@ async function invokeClaimedTool(turn, claimed) {
4094
4221
  toolName: call.name,
4095
4222
  input: call.input,
4096
4223
  ctx: stepCtx,
4224
+ ...invokeAllowList !== void 0 ? {
4225
+ allowedTools: invokeAllowList
4226
+ } : {},
4097
4227
  ...deps.toolTimeoutMs !== void 0 ? {
4098
4228
  timeoutMs: deps.toolTimeoutMs
4099
4229
  } : {},
@@ -4116,7 +4246,9 @@ async function invokeClaimedTool(turn, claimed) {
4116
4246
  return deps.registry.invoke(call.name, call.input, {
4117
4247
  ...toolCallContext(ctx, call.id),
4118
4248
  emitUi: ui2.emit
4119
- }, deps.rolesPolicy);
4249
+ }, deps.rolesPolicy, invokeAllowList !== void 0 ? {
4250
+ allowedTools: invokeAllowList
4251
+ } : {});
4120
4252
  }, deps.toolTransientRetry ?? {}, {
4121
4253
  ...hooks.isControlFlowError !== void 0 ? {
4122
4254
  isControlFlowError: hooks.isControlFlowError
@@ -4247,7 +4379,7 @@ async function delegateToolCall(turn, claimed) {
4247
4379
  const { deps, input, hooks } = turn;
4248
4380
  const { call, targetAgent = call.name } = claimed;
4249
4381
  const task = extractTask(call.input);
4250
- const refusal = delegationRefusal({
4382
+ const refusal = personaRefusal(turn.persona, call.name) ?? delegationRefusal({
4251
4383
  deps,
4252
4384
  input,
4253
4385
  targetAgent
@@ -4543,14 +4675,16 @@ async function invokeClaimedToolsTogether(turn, parallel, claimed) {
4543
4675
  return results;
4544
4676
  }
4545
4677
  __name(invokeClaimedToolsTogether, "invokeClaimedToolsTogether");
4546
- async function runAgentLoop(boundDeps, input, hooks) {
4678
+ async function runAgentLoop(boundDeps, requested, hooks) {
4679
+ const persona = requested.persona === void 0 ? void 0 : await hooks.step("persona:resolve", () => resolveTurnPersona(boundDeps, requested)) ?? void 0;
4680
+ const input = persona === void 0 || persona.id !== requested.persona ? withoutPersona(requested) : requested;
4547
4681
  const deps = input.model === void 0 ? boundDeps : {
4548
4682
  ...boundDeps,
4549
4683
  model: withSelectedModel(boundDeps.model, input.model),
4550
4684
  modelId: input.model
4551
4685
  };
4552
4686
  const maxSteps = deps.maxSteps ?? 8;
4553
- let system = await resolveSystemPrompt(deps, input);
4687
+ let system = await resolveSystemPrompt(deps, input, persona);
4554
4688
  const inputProcessors = deps.inputProcessors ?? [];
4555
4689
  const outputProcessors = deps.outputProcessors ?? [];
4556
4690
  const gateMode = resolveOutputGateMode(outputProcessors);
@@ -4591,6 +4725,9 @@ async function runAgentLoop(boundDeps, input, hooks) {
4591
4725
  role: "user",
4592
4726
  content: input.userText,
4593
4727
  runId: hooks.runId,
4728
+ ...input.persona !== void 0 ? {
4729
+ persona: input.persona
4730
+ } : {},
4594
4731
  ...input.attachments !== void 0 ? {
4595
4732
  attachments: input.attachments
4596
4733
  } : {}
@@ -4613,6 +4750,9 @@ async function runAgentLoop(boundDeps, input, hooks) {
4613
4750
  actorId: input.actor.id,
4614
4751
  ...input.agentName !== void 0 ? {
4615
4752
  agentName: input.agentName
4753
+ } : {},
4754
+ ...input.persona !== void 0 ? {
4755
+ persona: input.persona
4616
4756
  } : {}
4617
4757
  });
4618
4758
  const stages = await resolvePromptStages(deps, hooks);
@@ -4749,13 +4889,16 @@ ${block}`;
4749
4889
  } : {},
4750
4890
  ...input.model !== void 0 ? {
4751
4891
  model: input.model
4892
+ } : {},
4893
+ ...persona?.allowedTools !== void 0 ? {
4894
+ personaAllowedTools: persona.allowedTools
4752
4895
  } : {}
4753
4896
  });
4754
4897
  } else {
4755
4898
  const tools = withMemoryTool({
4756
4899
  tools: withSkillTool({
4757
4900
  tools: withAskTool({
4758
- tools: await deps.registry.definitionsFor(input.actor, deps.rolesPolicy, deps.toolAllowList, {
4901
+ tools: await deps.registry.definitionsFor(input.actor, deps.rolesPolicy, intersectAllowLists(deps.toolAllowList, persona?.allowedTools), {
4759
4902
  threadId: input.threadId,
4760
4903
  ...input.agentName !== void 0 ? {
4761
4904
  agentName: input.agentName
@@ -4970,6 +5113,9 @@ ${block}`;
4970
5113
  ...input.agentName !== void 0 ? {
4971
5114
  agentName: input.agentName
4972
5115
  } : {},
5116
+ ...input.persona !== void 0 ? {
5117
+ persona: input.persona
5118
+ } : {},
4973
5119
  ...turn.reasoning !== void 0 ? {
4974
5120
  reasoning: turn.reasoning
4975
5121
  } : {},
@@ -5064,6 +5210,9 @@ ${block}`;
5064
5210
  } : {},
5065
5211
  ...memoryDigest !== void 0 ? {
5066
5212
  memory: memoryDigest
5213
+ } : {},
5214
+ ...persona !== void 0 ? {
5215
+ persona
5067
5216
  } : {}
5068
5217
  };
5069
5218
  const parallel = hooks.parallel;
@@ -5196,7 +5345,10 @@ var InMemoryAgentStore = class {
5196
5345
  transient: input.transient ?? false,
5197
5346
  createdAt: ts,
5198
5347
  updatedAt: ts,
5199
- messages: []
5348
+ messages: [],
5349
+ ...input.persona !== void 0 ? {
5350
+ persona: input.persona
5351
+ } : {}
5200
5352
  };
5201
5353
  this.threads.set(id, row);
5202
5354
  return this.toSummary(row);
@@ -5301,6 +5453,9 @@ var InMemoryAgentStore = class {
5301
5453
  } : {},
5302
5454
  ...source.model != null ? {
5303
5455
  model: source.model
5456
+ } : {},
5457
+ ...source.persona != null ? {
5458
+ persona: source.persona
5304
5459
  } : {}
5305
5460
  };
5306
5461
  this.threads.set(id, row);
@@ -5352,6 +5507,9 @@ var InMemoryAgentStore = class {
5352
5507
  if (patch.model !== void 0) {
5353
5508
  row.model = patch.model;
5354
5509
  }
5510
+ if (patch.persona !== void 0) {
5511
+ row.persona = patch.persona;
5512
+ }
5355
5513
  row.updatedAt = this.now();
5356
5514
  }
5357
5515
  async activeRunForThread(threadId) {
@@ -5365,6 +5523,10 @@ var InMemoryAgentStore = class {
5365
5523
  async defaultAgentForThread(threadId) {
5366
5524
  return this.threads.get(threadId)?.defaultAgent ?? null;
5367
5525
  }
5526
+ /** The thread's persona, projected like {@link defaultAgentForThread}. */
5527
+ async personaForThread(threadId) {
5528
+ return this.threads.get(threadId)?.persona ?? null;
5529
+ }
5368
5530
  /** The thread's pinned model, projected like {@link defaultAgentForThread}. */
5369
5531
  async modelForThread(threadId) {
5370
5532
  return this.threads.get(threadId)?.model ?? null;
@@ -5469,6 +5631,9 @@ var InMemoryAgentStore = class {
5469
5631
  ...input.agentName !== void 0 ? {
5470
5632
  agentName: input.agentName
5471
5633
  } : {},
5634
+ ...input.persona !== void 0 ? {
5635
+ persona: input.persona
5636
+ } : {},
5472
5637
  ...input.model !== void 0 ? {
5473
5638
  model: input.model
5474
5639
  } : {},
@@ -5607,6 +5772,9 @@ var InMemoryAgentStore = class {
5607
5772
  ...input.agentName !== void 0 ? {
5608
5773
  agentName: input.agentName
5609
5774
  } : {},
5775
+ ...input.persona !== void 0 ? {
5776
+ persona: input.persona
5777
+ } : {},
5610
5778
  ...input.runId !== void 0 ? {
5611
5779
  runId: input.runId
5612
5780
  } : {},
@@ -5675,7 +5843,8 @@ var InMemoryAgentStore = class {
5675
5843
  }
5676
5844
  }
5677
5845
  /**
5678
- * Of `mediaIds`, the ones a surviving message in one of this actor's threads still carries.
5846
+ * Of `mediaIds`, the ones a surviving message — or a message waiting in the queue — in one of
5847
+ * this actor's threads still carries.
5679
5848
  * Re-derived from the messages each call, so a media whose message was truncated away reads as
5680
5849
  * unreferenced again.
5681
5850
  */
@@ -5689,7 +5858,11 @@ var InMemoryAgentStore = class {
5689
5858
  if (thread.actorRef !== actorRef) {
5690
5859
  continue;
5691
5860
  }
5692
- for (const message of thread.messages) {
5861
+ const queued = this.queues.get(thread.id) ?? [];
5862
+ for (const message of [
5863
+ ...thread.messages,
5864
+ ...queued
5865
+ ]) {
5693
5866
  for (const attachment of message.attachments ?? []) {
5694
5867
  if (wanted.has(attachment.mediaId)) {
5695
5868
  found.add(attachment.mediaId);
@@ -6018,7 +6191,8 @@ var InMemoryAgentStore = class {
6018
6191
  defaultAgent: row.defaultAgent ?? null,
6019
6192
  ...row.model != null ? {
6020
6193
  model: row.model
6021
- } : {}
6194
+ } : {},
6195
+ persona: row.persona ?? null
6022
6196
  };
6023
6197
  }
6024
6198
  };
@@ -6691,9 +6865,11 @@ var SqlTokenStreamSink = class {
6691
6865
  filterToolsByEnabled,
6692
6866
  filterToolsByRole,
6693
6867
  findCatalogModel,
6868
+ findPersona,
6694
6869
  gateFollowUps,
6695
6870
  gateTail,
6696
6871
  hashConfirmToken,
6872
+ intersectAllowLists,
6697
6873
  invokeWithTransientRetry,
6698
6874
  isChatQueueStore,
6699
6875
  isControlFlowSignal,
@@ -6711,6 +6887,7 @@ var SqlTokenStreamSink = class {
6711
6887
  observeTurnFrames,
6712
6888
  offerMemories,
6713
6889
  offerSkills,
6890
+ personaCatalogEntry,
6714
6891
  publishAgentDelegated,
6715
6892
  publishAgentMemoryResolved,
6716
6893
  publishAgentMemoryWritten,
@@ -6740,6 +6917,7 @@ var SqlTokenStreamSink = class {
6740
6917
  resolveGateLookback,
6741
6918
  resolveMemoryDigest,
6742
6919
  resolveOutputGateMode,
6920
+ resolvePersonaAlias,
6743
6921
  resolveSkillCatalog,
6744
6922
  resolveToolTransientRetryNumbers,
6745
6923
  rollupThreadUsage,