@dudousxd/nestjs-agent-core 0.33.0 → 0.34.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,7 +35,9 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
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
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.
38
- - `agentFailureCode(error)` — the stream error code for a failed run (`cancelled`, `quota_exceeded`, `output_rejected`, `structured_output_invalid`, else `run_failed`), so a control doing its job never reads as the model breaking.
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
+ - `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
+ - `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.
39
41
  - `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.
40
42
  - `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.
41
43
  - `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.
@@ -1,7 +1,7 @@
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 { A as Actor, T as ToolSpec, a as ToolHandler, b as ToolPresentation } from '../tool-CMTuoJ2v.cjs';
4
+ import { A as Actor, T as ToolSpec, a as ToolHandler, b as ToolPresentation } from '../tool-C128TFWw.cjs';
5
5
 
6
6
  /**
7
7
  * One node of a composed UI: a catalog component, its props, and — for components declared with
@@ -1,7 +1,7 @@
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 { A as Actor, T as ToolSpec, a as ToolHandler, b as ToolPresentation } from '../tool-CMTuoJ2v.js';
4
+ import { A as Actor, T as ToolSpec, a as ToolHandler, b as ToolPresentation } from '../tool-C128TFWw.js';
5
5
 
6
6
  /**
7
7
  * One node of a composed UI: a catalog component, its props, and — for components declared with
@@ -1,5 +1,5 @@
1
- import { I as InputProcessor, O as OutputProcessor } from '../processors-DvZDW5zS.cjs';
2
- import { A as Actor, a as ToolHandler } from '../tool-CMTuoJ2v.cjs';
1
+ import { I as InputProcessor, O as OutputProcessor } from '../processors-Bm2DniQ8.cjs';
2
+ import { A as Actor, a as ToolHandler } from '../tool-C128TFWw.cjs';
3
3
  import '@standard-schema/spec';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { I as InputProcessor, O as OutputProcessor } from '../processors-C1jir8eB.js';
2
- import { A as Actor, a as ToolHandler } from '../tool-CMTuoJ2v.js';
1
+ import { I as InputProcessor, O as OutputProcessor } from '../processors-CtrfAA0B.js';
2
+ import { A as Actor, a as ToolHandler } from '../tool-C128TFWw.js';
3
3
  import '@standard-schema/spec';
4
4
 
5
5
  /**
package/dist/index.cjs CHANGED
@@ -76,6 +76,11 @@ __export(src_exports, {
76
76
  REMEMBER_TOOL_DESCRIPTION: () => REMEMBER_TOOL_DESCRIPTION,
77
77
  REMEMBER_TOOL_NAME: () => REMEMBER_TOOL_NAME,
78
78
  REQUESTER_APPROVER: () => REQUESTER_APPROVER,
79
+ RUN_ENDED_BEFORE_TOOL_CALL: () => RUN_ENDED_BEFORE_TOOL_CALL,
80
+ RUN_FAILED_MESSAGE: () => RUN_FAILED_MESSAGE,
81
+ RUN_NOT_ACTIVE_CODE: () => RUN_NOT_ACTIVE_CODE,
82
+ RUN_NOT_ACTIVE_MESSAGE: () => RUN_NOT_ACTIVE_MESSAGE,
83
+ RUN_NO_LONGER_RUNNING: () => RUN_NO_LONGER_RUNNING,
79
84
  RunCancelledError: () => RunCancelledError,
80
85
  SKILL_TOOL_DESCRIPTION: () => SKILL_TOOL_DESCRIPTION,
81
86
  SKILL_TOOL_NAME: () => SKILL_TOOL_NAME,
@@ -86,6 +91,7 @@ __export(src_exports, {
86
91
  ToolInputInvalidError: () => ToolInputInvalidError,
87
92
  ToolNotFoundError: () => ToolNotFoundError,
88
93
  ToolRegistry: () => ToolRegistry,
94
+ UNFINISHED_TOOL_CALL: () => UNFINISHED_TOOL_CALL,
89
95
  actorScope: () => actorScope,
90
96
  agentDiagnosticKey: () => agentDiagnosticKey,
91
97
  agentFailureCode: () => agentFailureCode,
@@ -103,6 +109,7 @@ __export(src_exports, {
103
109
  createIncrementalGate: () => createIncrementalGate,
104
110
  createNoopEmitUi: () => createNoopEmitUi,
105
111
  createUiCollector: () => createUiCollector,
112
+ danglingToolCallIds: () => danglingToolCallIds,
106
113
  dayBoundsUtc: () => dayBoundsUtc,
107
114
  decodeStreamEvent: () => decodeStreamEvent,
108
115
  defaultCanDecide: () => defaultCanDecide,
@@ -114,6 +121,7 @@ __export(src_exports, {
114
121
  estimateCost: () => estimateCost,
115
122
  estimateMessageTokens: () => estimateMessageTokens,
116
123
  exhaustedWindow: () => exhaustedWindow,
124
+ exposeStreamErrorDetails: () => exposeStreamErrorDetails,
117
125
  extractJson: () => extractJson,
118
126
  filterToolsByAllowList: () => filterToolsByAllowList,
119
127
  filterToolsByCanUse: () => filterToolsByCanUse,
@@ -176,6 +184,8 @@ __export(src_exports, {
176
184
  runOutputProcessors: () => runOutputProcessors,
177
185
  seedModelPrices: () => seedModelPrices,
178
186
  settleAll: () => settleAll,
187
+ settleDanglingToolCalls: () => settleDanglingToolCalls,
188
+ settleDeadRun: () => settleDeadRun,
179
189
  settleElicitation: () => settleElicitation,
180
190
  settleUnsettledDelegation: () => settleUnsettledDelegation,
181
191
  skillInputSchema: () => skillInputSchema,
@@ -184,9 +194,11 @@ __export(src_exports, {
184
194
  stampToolKinds: () => stampToolKinds,
185
195
  staticModelCatalog: () => staticModelCatalog,
186
196
  staticSkillProvider: () => staticSkillProvider,
197
+ streamFailure: () => streamFailure,
187
198
  summarizeWithModel: () => summarizeWithModel,
188
199
  tenantScope: () => tenantScope,
189
200
  toolCallApprovalFromRow: () => toolCallApprovalFromRow,
201
+ toolCallContext: () => toolCallContext,
190
202
  traceLlmTurn: () => traceLlmTurn,
191
203
  traceToolExecution: () => traceToolExecution,
192
204
  truncateDetailContent: () => truncateDetailContent,
@@ -2553,6 +2565,123 @@ function isControlFlowSignal(error) {
2553
2565
  }
2554
2566
  __name(isControlFlowSignal, "isControlFlowSignal");
2555
2567
 
2568
+ // src/dangling-tool-calls.ts
2569
+ var UNFINISHED_TOOL_CALL = "This tool call was never completed: the turn it belonged to ended unexpectedly before it was settled. Do not assume it ran, and do not assume it did not \u2014 check the current state with a read tool if one exists, tell the person plainly what is and is not confirmed, and do it again only if they still want it.";
2570
+ var RUN_ENDED_BEFORE_TOOL_CALL = "the run ended before this tool call was settled";
2571
+ var DECLINED = "The person was asked to approve this action and declined it. Nothing ran and nothing changed.";
2572
+ var EXPIRED = "This action needed approval, and the request expired before anyone decided. Nothing ran.";
2573
+ function danglingToolCallIds(messages) {
2574
+ const ids = [];
2575
+ for (const message of messages) {
2576
+ const calls = message.toolCalls ?? [];
2577
+ if (message.role !== "assistant" || calls.length === 0) {
2578
+ continue;
2579
+ }
2580
+ const settled = new Set((message.toolResults ?? []).map((result) => result.id));
2581
+ for (const call of calls) {
2582
+ if (!settled.has(call.id)) {
2583
+ ids.push(call.id);
2584
+ }
2585
+ }
2586
+ }
2587
+ return ids;
2588
+ }
2589
+ __name(danglingToolCallIds, "danglingToolCallIds");
2590
+ function resultFor(call, outcome) {
2591
+ const base = {
2592
+ id: call.id,
2593
+ name: call.name
2594
+ };
2595
+ switch (outcome?.status) {
2596
+ case "executed":
2597
+ case "auto_executed":
2598
+ return {
2599
+ ...base,
2600
+ output: outcome.output ?? null
2601
+ };
2602
+ case "failed":
2603
+ return {
2604
+ ...base,
2605
+ output: null,
2606
+ error: outcome.error ?? UNFINISHED_TOOL_CALL
2607
+ };
2608
+ case "rejected":
2609
+ return {
2610
+ ...base,
2611
+ output: {
2612
+ rejected: true
2613
+ },
2614
+ denied: true,
2615
+ error: DECLINED
2616
+ };
2617
+ case "expired":
2618
+ return {
2619
+ ...base,
2620
+ output: {
2621
+ rejected: true,
2622
+ expired: true
2623
+ },
2624
+ denied: true,
2625
+ expired: true,
2626
+ error: EXPIRED
2627
+ };
2628
+ default:
2629
+ return {
2630
+ ...base,
2631
+ output: null,
2632
+ error: UNFINISHED_TOOL_CALL
2633
+ };
2634
+ }
2635
+ }
2636
+ __name(resultFor, "resultFor");
2637
+ function settleDanglingToolCalls(messages, outcomes = []) {
2638
+ const known = new Map(outcomes.map((outcome) => [
2639
+ outcome.id,
2640
+ outcome
2641
+ ]));
2642
+ return messages.map((message) => {
2643
+ const calls = message.toolCalls ?? [];
2644
+ if (message.role !== "assistant" || calls.length === 0) {
2645
+ return message;
2646
+ }
2647
+ const results = message.toolResults ?? [];
2648
+ const settled = new Set(results.map((result) => result.id));
2649
+ const missing = calls.filter((call) => !settled.has(call.id));
2650
+ if (missing.length === 0) {
2651
+ return message;
2652
+ }
2653
+ return {
2654
+ ...message,
2655
+ toolResults: [
2656
+ ...results,
2657
+ ...missing.map((call) => resultFor(call, known.get(call.id)))
2658
+ ]
2659
+ };
2660
+ });
2661
+ }
2662
+ __name(settleDanglingToolCalls, "settleDanglingToolCalls");
2663
+
2664
+ // src/dead-run.ts
2665
+ var RUN_NOT_ACTIVE_CODE = "run_not_active";
2666
+ var RUN_NOT_ACTIVE_MESSAGE = "This request is no longer waiting for an answer: the turn it belonged to has ended. Send the message again.";
2667
+ var RUN_NO_LONGER_RUNNING = "the run is no longer running";
2668
+ async function settleDeadRun(store, input) {
2669
+ const failure = input.failure;
2670
+ if (failure !== void 0) {
2671
+ await Promise.resolve(store.recordRunEnd?.({
2672
+ runId: input.runId,
2673
+ status: "failed",
2674
+ errorCode: failure.code,
2675
+ errorMessage: failure.message
2676
+ })).catch(() => void 0);
2677
+ }
2678
+ await Promise.resolve(store.failUnsettledToolCalls?.(input.runId, RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
2679
+ if (input.threadId !== void 0) {
2680
+ await releaseThreadRun(store, input.threadId, input.runId).catch(() => void 0);
2681
+ }
2682
+ }
2683
+ __name(settleDeadRun, "settleDeadRun");
2684
+
2556
2685
  // src/delegation.ts
2557
2686
  function normalizeDelegation(entry) {
2558
2687
  return typeof entry === "string" ? {
@@ -3017,9 +3146,46 @@ function agentFailureCode(error) {
3017
3146
  if (error instanceof StructuredOutputError) {
3018
3147
  return "structured_output_invalid";
3019
3148
  }
3020
- return "run_failed";
3149
+ if (isReplayIntegrityError(error)) {
3150
+ return "replay_diverged";
3151
+ }
3152
+ return isNoOutputError(error) ? "model_no_output" : "run_failed";
3021
3153
  }
3022
3154
  __name(agentFailureCode, "agentFailureCode");
3155
+ function isNoOutputError(error) {
3156
+ if (!(error instanceof Error)) {
3157
+ return false;
3158
+ }
3159
+ return error.name === "AI_NoOutputGeneratedError" || error.name === "NoOutputGeneratedError" || error.message.startsWith("No output generated");
3160
+ }
3161
+ __name(isNoOutputError, "isNoOutputError");
3162
+ var RUN_FAILED_MESSAGE = "The assistant could not finish this answer. Please try again.";
3163
+ var streamErrorDetails;
3164
+ function exposeStreamErrorDetails(expose) {
3165
+ streamErrorDetails = expose;
3166
+ }
3167
+ __name(exposeStreamErrorDetails, "exposeStreamErrorDetails");
3168
+ var WORDED_CODES = /* @__PURE__ */ new Set([
3169
+ "quota_exceeded",
3170
+ "output_rejected",
3171
+ "structured_output_invalid"
3172
+ ]);
3173
+ function streamFailure(error) {
3174
+ const detail = error instanceof Error ? error.message : String(error);
3175
+ const code = agentFailureCode(error);
3176
+ if (WORDED_CODES.has(code)) {
3177
+ return {
3178
+ code,
3179
+ message: detail
3180
+ };
3181
+ }
3182
+ const exposed = streamErrorDetails ?? process.env.NODE_ENV !== "production";
3183
+ return {
3184
+ code,
3185
+ message: exposed ? detail : RUN_FAILED_MESSAGE
3186
+ };
3187
+ }
3188
+ __name(streamFailure, "streamFailure");
3023
3189
  var MAX_DELEGATION_DEPTH = 5;
3024
3190
  var DEFAULT_MAX_AGENT_APPEARANCES = 1;
3025
3191
  function delegationRefusal(args) {
@@ -3211,9 +3377,9 @@ function splitHistory(policy, input, messages) {
3211
3377
  }
3212
3378
  __name(splitHistory, "splitHistory");
3213
3379
  async function loadSelectedHistory(deps, input, hooks) {
3214
- return hooks.step("load:thread", async () => {
3380
+ const recorded = await hooks.step("load:thread", async () => {
3215
3381
  const thread = await readThreadForTurn(deps, input.threadId);
3216
- const { keep, drop } = splitHistory(deps.historyPolicy, input, toModelMessages(thread.messages));
3382
+ const { keep, drop } = splitHistory(deps.historyPolicy, input, await settleHistory(deps, toModelMessages(thread.messages)));
3217
3383
  return {
3218
3384
  messages: keep,
3219
3385
  dropped: deps.historyPolicy?.summarize === void 0 ? [] : drop,
@@ -3221,8 +3387,21 @@ async function loadSelectedHistory(deps, input, hooks) {
3221
3387
  hasAssistantMessage: thread.hasAssistantMessage
3222
3388
  };
3223
3389
  });
3390
+ return {
3391
+ ...recorded,
3392
+ messages: settleDanglingToolCalls(recorded.messages)
3393
+ };
3224
3394
  }
3225
3395
  __name(loadSelectedHistory, "loadSelectedHistory");
3396
+ async function settleHistory(deps, messages) {
3397
+ const dangling = danglingToolCallIds(messages);
3398
+ if (dangling.length === 0) {
3399
+ return messages;
3400
+ }
3401
+ const outcomes = await Promise.resolve(deps.store.toolCallOutcomes?.(dangling) ?? []).catch(() => []);
3402
+ return settleDanglingToolCalls(messages, outcomes);
3403
+ }
3404
+ __name(settleHistory, "settleHistory");
3226
3405
  async function readThreadForTurn(deps, threadId) {
3227
3406
  const windowing = deps.store;
3228
3407
  if (typeof windowing.loadThreadForTurn === "function") {
@@ -3262,7 +3441,7 @@ __name(turnMessageLimit, "turnMessageLimit");
3262
3441
  async function loadWholeThread(deps, input, hooks) {
3263
3442
  const thread = await hooks.step("load:thread", () => deps.store.getThread(input.threadId));
3264
3443
  const stored = thread?.messages ?? [];
3265
- const { keep, drop } = splitHistory(deps.historyPolicy, input, toModelMessages(stored));
3444
+ const { keep, drop } = splitHistory(deps.historyPolicy, input, settleDanglingToolCalls(toModelMessages(stored)));
3266
3445
  return {
3267
3446
  messages: keep,
3268
3447
  dropped: drop,
@@ -3445,6 +3624,14 @@ async function awaitElicitation(hooks, request, ctx) {
3445
3624
  }, ctx));
3446
3625
  }
3447
3626
  __name(awaitElicitation, "awaitElicitation");
3627
+ function toolCallContext(ctx, toolCallId) {
3628
+ return {
3629
+ ...ctx,
3630
+ toolCallId,
3631
+ idempotencyKey: `${ctx.runId}:${toolCallId}`
3632
+ };
3633
+ }
3634
+ __name(toolCallContext, "toolCallContext");
3448
3635
  function toolContext(deps, input, hooks) {
3449
3636
  return {
3450
3637
  actor: input.actor,
@@ -3884,7 +4071,7 @@ async function invokeClaimedTool(turn, claimed) {
3884
4071
  }, () => invokeWithTransientRetry(() => {
3885
4072
  ui2.restart();
3886
4073
  return deps.registry.invoke(call.name, call.input, {
3887
- ...ctx,
4074
+ ...toolCallContext(ctx, call.id),
3888
4075
  emitUi: ui2.emit
3889
4076
  }, deps.rolesPolicy);
3890
4077
  }, deps.toolTransientRetry ?? {}, {
@@ -5482,6 +5669,36 @@ var InMemoryAgentStore = class {
5482
5669
  } : {}
5483
5670
  });
5484
5671
  }
5672
+ async toolCallOutcomes(toolCallIds) {
5673
+ const outcomes = [];
5674
+ for (const id of toolCallIds) {
5675
+ const row = this.toolCalls.get(id);
5676
+ if (row !== void 0) {
5677
+ outcomes.push({
5678
+ id,
5679
+ status: row.status,
5680
+ ...row.output !== void 0 ? {
5681
+ output: row.output
5682
+ } : {},
5683
+ ...row.error !== void 0 ? {
5684
+ error: row.error
5685
+ } : {}
5686
+ });
5687
+ }
5688
+ }
5689
+ return outcomes;
5690
+ }
5691
+ async failUnsettledToolCalls(runId, error) {
5692
+ let settled = 0;
5693
+ for (const row of this.toolCalls.values()) {
5694
+ if (row.runId === runId && row.status === "pending_approval") {
5695
+ row.status = "failed";
5696
+ row.error = error;
5697
+ settled += 1;
5698
+ }
5699
+ }
5700
+ return settled;
5701
+ }
5485
5702
  async updateToolCall(input) {
5486
5703
  const row = this.toolCalls.get(input.toolCallId);
5487
5704
  if (row === void 0) {
@@ -5809,6 +6026,11 @@ var InMemoryAgentStore = class {
5809
6026
  REMEMBER_TOOL_DESCRIPTION,
5810
6027
  REMEMBER_TOOL_NAME,
5811
6028
  REQUESTER_APPROVER,
6029
+ RUN_ENDED_BEFORE_TOOL_CALL,
6030
+ RUN_FAILED_MESSAGE,
6031
+ RUN_NOT_ACTIVE_CODE,
6032
+ RUN_NOT_ACTIVE_MESSAGE,
6033
+ RUN_NO_LONGER_RUNNING,
5812
6034
  RunCancelledError,
5813
6035
  SKILL_TOOL_DESCRIPTION,
5814
6036
  SKILL_TOOL_NAME,
@@ -5819,6 +6041,7 @@ var InMemoryAgentStore = class {
5819
6041
  ToolInputInvalidError,
5820
6042
  ToolNotFoundError,
5821
6043
  ToolRegistry,
6044
+ UNFINISHED_TOOL_CALL,
5822
6045
  actorScope,
5823
6046
  agentDiagnosticKey,
5824
6047
  agentFailureCode,
@@ -5836,6 +6059,7 @@ var InMemoryAgentStore = class {
5836
6059
  createIncrementalGate,
5837
6060
  createNoopEmitUi,
5838
6061
  createUiCollector,
6062
+ danglingToolCallIds,
5839
6063
  dayBoundsUtc,
5840
6064
  decodeStreamEvent,
5841
6065
  defaultCanDecide,
@@ -5847,6 +6071,7 @@ var InMemoryAgentStore = class {
5847
6071
  estimateCost,
5848
6072
  estimateMessageTokens,
5849
6073
  exhaustedWindow,
6074
+ exposeStreamErrorDetails,
5850
6075
  extractJson,
5851
6076
  filterToolsByAllowList,
5852
6077
  filterToolsByCanUse,
@@ -5909,6 +6134,8 @@ var InMemoryAgentStore = class {
5909
6134
  runOutputProcessors,
5910
6135
  seedModelPrices,
5911
6136
  settleAll,
6137
+ settleDanglingToolCalls,
6138
+ settleDeadRun,
5912
6139
  settleElicitation,
5913
6140
  settleUnsettledDelegation,
5914
6141
  skillInputSchema,
@@ -5917,9 +6144,11 @@ var InMemoryAgentStore = class {
5917
6144
  stampToolKinds,
5918
6145
  staticModelCatalog,
5919
6146
  staticSkillProvider,
6147
+ streamFailure,
5920
6148
  summarizeWithModel,
5921
6149
  tenantScope,
5922
6150
  toolCallApprovalFromRow,
6151
+ toolCallContext,
5923
6152
  traceLlmTurn,
5924
6153
  traceToolExecution,
5925
6154
  truncateDetailContent,