@slopus/happy-agent-base 0.0.30 → 0.0.32

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
@@ -83,8 +83,11 @@ transaction that appends the result. Nothing outside the loop ever executes a to
83
83
  drove execution would be deciding inside machinery that also commits results, resumes interrupted
84
84
  batches, and settles cancelled ones.
85
85
 
86
- Every executable call receives an internally generated cuid2 `id`, the provider's separate opaque
87
- `providerCallId`, and a call-bound `kv`. Calling `call.commit(ctx, result)` inside a transaction
86
+ Every tool call receives a Base-generated cuid2 `id`; executable calls also receive a call-bound
87
+ `kv`. Events, hooks, task context, modules, results, and execution use that ID. The provider-native
88
+ ID remains only in the raw stream and private provider-context records so replay and server-tool
89
+ results can still be correlated after a restart; each context record pairs it with the Base ID as
90
+ a separate sidecar, never in `vendor`. Calling `call.commit(ctx, result)` inside a transaction
88
91
  atomically saves that result with the tool's writes. The first successful commit wins; later
89
92
  commits and the tool's eventual return or throw are ignored. Committed results survive a crash,
90
93
  remain ordered with their batch, and the call-bound KV is erased in the result transaction.
@@ -1 +1 @@
1
- {"version":3,"file":"AgentBase.d.ts","sourceRoot":"","sources":["../sources/AgentBase.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AgentBase.d.ts","sourceRoot":"","sources":["../sources/AgentBase.ts"],"names":[],"mappings":"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"}
package/dist/AgentBase.js CHANGED
@@ -10,6 +10,7 @@ import { outsideAgentDatabaseOperation } from "./AgentDatabaseConnection.js";
10
10
  import { taskContextBeforeToolCall, withAgentTaskContext } from "./AgentTaskContext.js";
11
11
  import { AgentKV } from "./AgentKV.js";
12
12
  import { AGENT_BASE_PENDING_KEY, agentBasePendingStateOf, } from "./AgentBasePending.js";
13
+ import { assistantContextRecord, baseContextMessages, baseSessionEvent, contextFromRecords, providerContextMessages, storedContextToolIds, toolContextRecord, } from "./AgentProviderContext.js";
13
14
  import { cuid2Schema, ownAgentMessageMetadata, ownAgentMetadata, } from "./AgentMetadata.js";
14
15
  import { DEFAULT_AGENT_PERMISSION_MODE, isAgentPermissionMode, } from "./AgentPermissionMode.js";
15
16
  import { AgentProviders } from "./AgentProviders.js";
@@ -47,7 +48,6 @@ const insideLoops = new AsyncLocalStorage();
47
48
  const INSIDE_CLOSE_REPORT_MS = 15;
48
49
  const storedToolCallSchema = Type.Object({
49
50
  id: cuid2Schema,
50
- providerCallId: Type.String(),
51
51
  call: Type.Object({
52
52
  type: Type.Literal("tool_call"),
53
53
  name: Type.String(),
@@ -61,7 +61,7 @@ const storedToolCallSchema = Type.Object({
61
61
  /** A durable first-writer-wins claim on the result one call will append. */
62
62
  const storedToolResultSchema = Type.Object({
63
63
  role: Type.Literal("tool"),
64
- callId: Type.String(),
64
+ callId: cuid2Schema,
65
65
  content: Type.Array(Type.Unknown()),
66
66
  isError: Type.Optional(Type.Boolean()),
67
67
  vendor: Type.Optional(Type.Unknown()),
@@ -271,8 +271,10 @@ export class AgentBase {
271
271
  #session;
272
272
  /** The provider-facing configuration the current session was created with. */
273
273
  #sessionConfig;
274
- /** The conversation as this instance last knew it, reloaded from the store every turn. */
274
+ /** The conversation under Base-owned identities, reloaded from the store every turn. */
275
275
  #messages = [];
276
+ /** Provider-native tool IDs used only when serializing the private provider context. */
277
+ #providerToolIds = new Map();
276
278
  /** The durable steering queue, in the order its keys sort. */
277
279
  #steering = [];
278
280
  /** The durable send queue, in the order its keys sort. */
@@ -2266,6 +2268,15 @@ export class AgentBase {
2266
2268
  context: { instructions, messages: snapshot },
2267
2269
  ...(this.#model === undefined ? {} : { model: this.#model }),
2268
2270
  });
2271
+ const cancelledContext = result.status === "cancelled"
2272
+ ? baseContextMessages(result.context.messages, this.#providerToolIds)
2273
+ : undefined;
2274
+ let hookResult = result.status === "cancelled" && cancelledContext !== undefined
2275
+ ? {
2276
+ ...result,
2277
+ context: { ...result.context, messages: cancelledContext.messages },
2278
+ }
2279
+ : result;
2269
2280
  if (result.status === "failed") {
2270
2281
  await this.#preserveCompactionContinuation(ctx, continueWithInference && !signal.aborted);
2271
2282
  await this.#invokeHook(ctx, this.#hooks.afterCompaction, {
@@ -2275,7 +2286,18 @@ export class AgentBase {
2275
2286
  throw new Error(result.message);
2276
2287
  }
2277
2288
  if (result.status === "completed") {
2278
- const completed = { ...compactionStart, result };
2289
+ const replacement = baseContextMessages(result.context.messages, this.#providerToolIds);
2290
+ const preserved = baseContextMessages(result.preservedMessages, this.#providerToolIds);
2291
+ const publicResult = {
2292
+ ...result,
2293
+ preservedMessages: preserved.messages,
2294
+ context: { ...result.context, messages: replacement.messages },
2295
+ };
2296
+ hookResult = publicResult;
2297
+ const completed = {
2298
+ ...compactionStart,
2299
+ result: publicResult,
2300
+ };
2279
2301
  await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
2280
2302
  // Physically delete the superseded records and write the replacement —
2281
2303
  // which keeps the messages that stay — in one atomic step.
@@ -2288,7 +2310,8 @@ export class AgentBase {
2288
2310
  }
2289
2311
  await this.#persistence.append(txCtx, {
2290
2312
  type: "compaction",
2291
- messages: result.context.messages,
2313
+ contextToolIds: storedContextToolIds(replacement.toolIds),
2314
+ messages: replacement.messages,
2292
2315
  });
2293
2316
  if (continueWithInference) {
2294
2317
  // The replacement and the response it still owes are one durable
@@ -2300,7 +2323,8 @@ export class AgentBase {
2300
2323
  }
2301
2324
  });
2302
2325
  this.#rotateHistoryKV();
2303
- this.#messages = [...result.context.messages];
2326
+ this.#messages = replacement.messages;
2327
+ this.#providerToolIds = replacement.toolIds;
2304
2328
  this.#lastRecordType = "compaction";
2305
2329
  });
2306
2330
  // The conversation the measurement described is gone; its size is unknown
@@ -2312,7 +2336,7 @@ export class AgentBase {
2312
2336
  }
2313
2337
  await this.#invokeHook(ctx, this.#hooks.afterCompaction, {
2314
2338
  ...compactionStart,
2315
- result,
2339
+ result: hookResult,
2316
2340
  });
2317
2341
  this.#compaction = undefined;
2318
2342
  pending.resolve();
@@ -2355,7 +2379,7 @@ export class AgentBase {
2355
2379
  // it independently without racing that batch, and it cannot settle the agent
2356
2380
  // while the call remains open, so leave the durable stage untouched for resume.
2357
2381
  const pending = await this.#persistence.readValues(lockCtx, "tool.");
2358
- const dispatched = new Set(pending.map(({ key, value }) => this.#restoreToolEntry(key, value).providerCallId));
2382
+ const dispatched = new Set(pending.map(({ key, value }) => this.#restoreToolEntry(key, value).id));
2359
2383
  if (owed.some((call) => dispatched.has(call.callId))) {
2360
2384
  this.#durableWorkBlocked = true;
2361
2385
  return;
@@ -2364,7 +2388,7 @@ export class AgentBase {
2364
2388
  const entry = this.#newToolEntry(index, call);
2365
2389
  return {
2366
2390
  ...entry,
2367
- committed: toolFailure(entry.providerCallId, reason),
2391
+ committed: toolFailure(entry.id, reason),
2368
2392
  };
2369
2393
  });
2370
2394
  // Give every otherwise-undispatched call a durable internal identity and staged
@@ -2380,12 +2404,12 @@ export class AgentBase {
2380
2404
  await this.#recordTransaction(lockCtx, async (txCtx) => {
2381
2405
  for (const entry of entries) {
2382
2406
  const result = entry.committed;
2383
- await this.#appendRecord(txCtx, { type: "tool", message: result });
2407
+ await this.#appendRecord(txCtx, toolContextRecord(result, this.#providerToolIds));
2384
2408
  await this.#persistence.deleteValue(txCtx, entry.key);
2385
2409
  // A result the conversation records is a result the hook sees, however
2386
2410
  // little of a run produced it. A hook that fails here leaves the calls
2387
2411
  // unsettled, which is what lets a later attempt answer them properly.
2388
- await this.#invokeToolTransactHook(txCtx, entry.id, entry.providerCallId, this.#hooks.afterToolCallTransact, result);
2412
+ await this.#invokeToolTransactHook(txCtx, entry.id, this.#hooks.afterToolCallTransact, result);
2389
2413
  await this.#kv.scoped("call", entry.id).clear(txCtx);
2390
2414
  }
2391
2415
  await this.#recordPending(txCtx, "inference");
@@ -2739,6 +2763,7 @@ export class AgentBase {
2739
2763
  queue.splice(0, count);
2740
2764
  if (reset) {
2741
2765
  this.#messages = injected === undefined ? [] : [injected];
2766
+ this.#providerToolIds.clear();
2742
2767
  this.#contextTokens = undefined;
2743
2768
  }
2744
2769
  this.#messages.push(...batch.map((entry) => entry.message));
@@ -2838,14 +2863,15 @@ export class AgentBase {
2838
2863
  const records = await this.#persistence.load(lockCtx);
2839
2864
  const last = records[records.length - 1];
2840
2865
  this.#lastRecordType = last?.type;
2841
- let restored = messagesFromRecords(records);
2866
+ const restored = contextFromRecords(records);
2842
2867
  const steering = await this.#persistence.readValues(lockCtx, "steering.");
2843
2868
  const sends = await this.#persistence.readValues(lockCtx, "send.");
2844
2869
  const injections = await this.#persistence.readValues(lockCtx, "inject.");
2845
2870
  const pendingTools = await this.#persistence.readValues(lockCtx, "tool.");
2846
2871
  const settings = await this.#persistence.readValues(lockCtx, "settings");
2847
2872
  const context = await this.#persistence.readValues(lockCtx, "context");
2848
- this.#messages = restored;
2873
+ this.#messages = restored.messages;
2874
+ this.#providerToolIds = restored.toolIds;
2849
2875
  // The measured size of the restored context, so the first turn after a reload can
2850
2876
  // still decide whether it needs a compaction.
2851
2877
  const measured = context[0]?.value;
@@ -2882,7 +2908,7 @@ export class AgentBase {
2882
2908
  // context would keep an unanswered tool call for ever, which most providers
2883
2909
  // reject outright. They are recovered as the batch that was about to be
2884
2910
  // dispatched.
2885
- const owed = this.#unansweredCalls(restored);
2911
+ const owed = this.#unansweredCalls(restored.messages);
2886
2912
  if (owed.length > 0) {
2887
2913
  this.#pendingTools = owed.map((call, index) => this.#newToolEntry(index, call));
2888
2914
  this.#pendingToolsUndispatched = true;
@@ -2918,7 +2944,8 @@ export class AgentBase {
2918
2944
  // Last, so a hook noting a call about to happen sees a transaction holding
2919
2945
  // the whole batch it belongs to.
2920
2946
  for (const entry of entries) {
2921
- await this.#invokeToolTransactHook(txCtx, entry.id, entry.providerCallId, this.#hooks.beforeToolCallTransact, entry.call);
2947
+ const { vendor: _vendor, ...call } = entry.call;
2948
+ await this.#invokeToolTransactHook(txCtx, entry.id, this.#hooks.beforeToolCallTransact, call);
2922
2949
  }
2923
2950
  }));
2924
2951
  }
@@ -2962,20 +2989,17 @@ export class AgentBase {
2962
2989
  throw new Error(`The committed result claim for tool "${entry.id}" is not valid.`);
2963
2990
  }
2964
2991
  const result = stored;
2965
- if (result.callId !== entry.providerCallId) {
2966
- throw new Error(`The committed result claim for tool "${entry.id}" has the wrong provider call ID.`);
2992
+ if (result.callId !== entry.id) {
2993
+ throw new Error(`The committed result claim for tool "${entry.id}" has the wrong identity.`);
2967
2994
  }
2968
2995
  winner = result;
2969
2996
  results[committed] = result;
2970
- await this.#appendRecord(txCtx, {
2971
- type: "tool",
2972
- message: result,
2973
- });
2997
+ await this.#appendRecord(txCtx, toolContextRecord(result, this.#providerToolIds));
2974
2998
  // The call is answered, so both its retry record and its temporary
2975
2999
  // invocation store disappear in this same result transaction.
2976
3000
  await this.#persistence.deleteValue(txCtx, entry.key);
2977
3001
  await this.#persistence.deleteValue(txCtx, resultKey);
2978
- await this.#invokeToolTransactHook(txCtx, entry.id, entry.providerCallId, this.#hooks.afterToolCallTransact, result);
3002
+ await this.#invokeToolTransactHook(txCtx, entry.id, this.#hooks.afterToolCallTransact, result);
2979
3003
  await this.#kv.scoped("call", entry.id).clear(txCtx);
2980
3004
  });
2981
3005
  this.#messages.push(winner);
@@ -3004,7 +3028,7 @@ export class AgentBase {
3004
3028
  outcome = entry.committed;
3005
3029
  }
3006
3030
  else if (resume && !(await this.#isDurable(ctx, entry.call))) {
3007
- outcome = toolFailure(entry.call.callId, "The tool call was interrupted by a restart and was not retried.");
3031
+ outcome = toolFailure(entry.id, "The tool call was interrupted by a restart and was not retried.");
3008
3032
  }
3009
3033
  else {
3010
3034
  const toolLifetime = AbortSignal.any([signal, this.#closeController.signal]);
@@ -3027,7 +3051,7 @@ export class AgentBase {
3027
3051
  outcome === ABORTED
3028
3052
  ? {
3029
3053
  role: "tool",
3030
- callId: entry.call.callId,
3054
+ callId: entry.id,
3031
3055
  content: [
3032
3056
  {
3033
3057
  type: "text",
@@ -3131,19 +3155,20 @@ export class AgentBase {
3131
3155
  ordered.push(result);
3132
3156
  }
3133
3157
  }
3134
- return ordered;
3158
+ return providerContextMessages(ordered, this.#providerToolIds);
3135
3159
  }
3136
- /** Allocate one internal identity before the call becomes durable or executable. */
3160
+ /** Turn one canonical call into its durable Base-owned batch entry. */
3137
3161
  #newToolEntry(index, call) {
3138
- const id = createId();
3162
+ if (!Value.Check(cuid2Schema, call.callId)) {
3163
+ throw new Error(`Tool call "${call.callId}" is not a Base-generated ID.`);
3164
+ }
3139
3165
  return {
3140
- key: this.#toolKey(index, id),
3141
- id,
3142
- providerCallId: call.callId,
3166
+ key: this.#toolKey(index, call.callId),
3167
+ id: call.callId,
3143
3168
  call,
3144
3169
  };
3145
3170
  }
3146
- /** Restore the exact internal/provider identity pair that was dispatched before a restart. */
3171
+ /** Restore the Base/provider context identity pair dispatched before a restart. */
3147
3172
  #restoreToolEntry(key, value) {
3148
3173
  if (!Value.Check(storedToolCallSchema, value)) {
3149
3174
  throw new Error(`The pending tool call under "${key}" is not valid.`);
@@ -3153,26 +3178,27 @@ export class AgentBase {
3153
3178
  !Value.Check(storedToolResultSchema, stored.committed)) {
3154
3179
  throw new Error(`The committed result under "${key}" is not valid.`);
3155
3180
  }
3156
- const call = {
3157
- ...stored.call,
3158
- callId: stored.providerCallId,
3159
- };
3181
+ const call = { ...stored.call, callId: stored.id };
3160
3182
  return {
3161
3183
  key,
3162
3184
  id: stored.id,
3163
- providerCallId: stored.providerCallId,
3164
3185
  call,
3165
3186
  ...(stored.committed === undefined
3166
3187
  ? {}
3167
3188
  : { committed: stored.committed }),
3168
3189
  };
3169
3190
  }
3170
- /** The explicit durable representation keeps provider and internal identities separate. */
3191
+ /** The durable representation pairs one Base identity with provider-context replay data. */
3171
3192
  #storedToolEntry(entry) {
3172
- const { callId: _providerCallId, server: _server, ...call } = entry.call;
3193
+ if (entry.call.callId !== entry.id) {
3194
+ throw new Error(`Pending tool "${entry.id}" has a mismatched Base identity.`);
3195
+ }
3196
+ if (entry.committed !== undefined && entry.committed.callId !== entry.id) {
3197
+ throw new Error(`Pending tool "${entry.id}" has a mismatched committed result ID.`);
3198
+ }
3199
+ const { callId: _callId, server: _server, ...call } = entry.call;
3173
3200
  return {
3174
3201
  id: entry.id,
3175
- providerCallId: entry.providerCallId,
3176
3202
  call,
3177
3203
  ...(entry.committed === undefined ? {} : { committed: entry.committed }),
3178
3204
  };
@@ -3186,24 +3212,23 @@ export class AgentBase {
3186
3212
  return `toolResult.${id}`;
3187
3213
  }
3188
3214
  /**
3189
- * The scope one call owns: state lives under its internal ID, never under the provider's
3190
- * opaque ID or another call's scope, and is erased when the call commits. The task context
3191
- * still locates the provider call in conversation history.
3215
+ * The scope one call owns: state lives under its Base ID and is erased when the call commits.
3216
+ * The same ID locates the call in conversation history.
3192
3217
  */
3193
- #callScoped(ctx, id, providerCallId) {
3194
- return withAgentTaskContext(withAgentRunKV(withAgentKV(ctx, this.#kv.scoped("call", id)), this.#runKV.scoped("call", id)), taskContextBeforeToolCall(this.#messages, providerCallId));
3218
+ #callScoped(ctx, id) {
3219
+ return withAgentTaskContext(withAgentRunKV(withAgentKV(ctx, this.#kv.scoped("call", id)), this.#runKV.scoped("call", id)), taskContextBeforeToolCall(this.#messages, id));
3195
3220
  }
3196
3221
  /**
3197
3222
  * Call a tool hook that writes inside a transaction of its own call's. The lifetime ends with
3198
3223
  * the callback, so a context kept afterwards cannot outlive the transaction it belongs to.
3199
3224
  * Its failure is not contained: it rolls that transaction back.
3200
3225
  */
3201
- async #invokeToolTransactHook(txCtx, id, providerCallId, hook, argument) {
3226
+ async #invokeToolTransactHook(txCtx, id, hook, argument) {
3202
3227
  if (hook === undefined)
3203
3228
  return;
3204
3229
  const lifetime = new AbortController();
3205
3230
  try {
3206
- await hook(this.#callScoped(withLifetime(txCtx, lifetime.signal), id, providerCallId), argument);
3231
+ await hook(this.#callScoped(withLifetime(txCtx, lifetime.signal), id), argument);
3207
3232
  }
3208
3233
  finally {
3209
3234
  lifetime.abort();
@@ -3218,7 +3243,7 @@ export class AgentBase {
3218
3243
  const { call } = entry;
3219
3244
  const failure = (text) => ({
3220
3245
  role: "tool",
3221
- callId: call.callId,
3246
+ callId: entry.id,
3222
3247
  content: [{ type: "text", text }],
3223
3248
  isError: true,
3224
3249
  });
@@ -3259,7 +3284,7 @@ export class AgentBase {
3259
3284
  const callLifetime = new AbortController();
3260
3285
  let committing = false;
3261
3286
  const boundedCallKV = callKV.until(callLifetime.signal, () => !committing);
3262
- const callCtx = withAgentKV(this.#callScoped(ctx, entry.id, entry.providerCallId), boundedCallKV);
3287
+ const callCtx = withAgentKV(this.#callScoped(ctx, entry.id), boundedCallKV);
3263
3288
  // From here the call is one the two tool hooks bracket: a tool that exists, a call that
3264
3289
  // finished, and arguments its schema accepts. A call refused before that reaches neither
3265
3290
  // hook, because there is nothing yet to decide about or to report.
@@ -3273,7 +3298,7 @@ export class AgentBase {
3273
3298
  resolveCommitted = resolve;
3274
3299
  });
3275
3300
  const outcomeFor = (result) => ({
3276
- callId: call.callId,
3301
+ callId: entry.id,
3277
3302
  tool: ran,
3278
3303
  arguments: ranArguments,
3279
3304
  content: [...ran.toLLM(result)],
@@ -3292,7 +3317,7 @@ export class AgentBase {
3292
3317
  const candidate = outcomeFor(result);
3293
3318
  const message = {
3294
3319
  role: "tool",
3295
- callId: entry.providerCallId,
3320
+ callId: entry.id,
3296
3321
  content: candidate.content,
3297
3322
  ...(candidate.isError ? { isError: true } : {}),
3298
3323
  };
@@ -3341,7 +3366,7 @@ export class AgentBase {
3341
3366
  };
3342
3367
  try {
3343
3368
  const decision = await this.#hooks.beforeToolCall?.(callCtx, {
3344
- callId: call.callId,
3369
+ callId: entry.id,
3345
3370
  tool,
3346
3371
  arguments: args,
3347
3372
  });
@@ -3349,7 +3374,7 @@ export class AgentBase {
3349
3374
  // The hook answered the model itself, so the tool never runs and there is no
3350
3375
  // structured result — only what the model is told.
3351
3376
  outcome = {
3352
- callId: call.callId,
3377
+ callId: entry.id,
3353
3378
  tool,
3354
3379
  arguments: args,
3355
3380
  content: [...decision.content],
@@ -3376,7 +3401,6 @@ export class AgentBase {
3376
3401
  : [runCtx.lifetime, callLifetime.signal]));
3377
3402
  const toolCall = {
3378
3403
  id: entry.id,
3379
- providerCallId: entry.providerCallId,
3380
3404
  kv: boundedCallKV,
3381
3405
  commit,
3382
3406
  };
@@ -3415,7 +3439,7 @@ export class AgentBase {
3415
3439
  outcome =
3416
3440
  committedOutcome ??
3417
3441
  {
3418
- callId: call.callId,
3442
+ callId: entry.id,
3419
3443
  tool: ran,
3420
3444
  arguments: ranArguments,
3421
3445
  content: [
@@ -3431,7 +3455,7 @@ export class AgentBase {
3431
3455
  await this.#invokeHookOn(callCtx, this.#hooks.afterToolCall, outcome);
3432
3456
  return {
3433
3457
  role: "tool",
3434
- callId: call.callId,
3458
+ callId: entry.id,
3435
3459
  content: outcome.content,
3436
3460
  ...(outcome.isError ? { isError: true } : {}),
3437
3461
  };
@@ -3487,21 +3511,26 @@ export class AgentBase {
3487
3511
  // in-memory assistant message never diverges from what a reload would rebuild.
3488
3512
  const persisted = [];
3489
3513
  const toolCallIndexes = new Map();
3490
- const persist = async (event) => {
3491
- if (event === undefined)
3514
+ const toolResultIndexes = new Map();
3515
+ // Provider IDs remain only in the private context blocks. Every event and transactional
3516
+ // projection leaving this method uses the generated Base ID instead.
3517
+ const responseToolIds = new Map();
3518
+ const persist = async (block, event) => {
3519
+ if (block === undefined)
3492
3520
  return;
3521
+ const record = assistantContextRecord(block, this.#providerToolIds);
3493
3522
  await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
3494
- if (this.#hooks.onEventTransact === undefined) {
3495
- await this.#appendRecord(lockCtx, { type: "block", block: event.block });
3523
+ if (event === undefined || this.#hooks.onEventTransact === undefined) {
3524
+ await this.#appendRecord(lockCtx, record);
3496
3525
  }
3497
3526
  else {
3498
3527
  await this.#recordTransaction(lockCtx, async (txCtx) => {
3499
- await this.#appendRecord(txCtx, { type: "block", block: event.block });
3528
+ await this.#appendRecord(txCtx, record);
3500
3529
  await this.#withTransactionalContext(txCtx, (hookCtx) => this.#hooks.onEventTransact?.(hookCtx, event));
3501
3530
  });
3502
3531
  }
3503
3532
  });
3504
- persisted.push(event.block);
3533
+ persisted.push(block);
3505
3534
  };
3506
3535
  const iterator = stream[Symbol.asyncIterator]();
3507
3536
  // A response usually ends before its stream does — at the done event, or at an abort —
@@ -3522,7 +3551,8 @@ export class AgentBase {
3522
3551
  exhausted = true;
3523
3552
  break;
3524
3553
  }
3525
- const event = next.value;
3554
+ const providerEvent = next.value;
3555
+ const event = baseSessionEvent(providerEvent, responseToolIds, this.#providerToolIds);
3526
3556
  await this.#emit(ctx, event);
3527
3557
  switch (event.type) {
3528
3558
  case "text_start":
@@ -3540,7 +3570,7 @@ export class AgentBase {
3540
3570
  }
3541
3571
  case "text_end": {
3542
3572
  const last = content[content.length - 1];
3543
- await persist(last?.type === "text" ? { ...event, block: last } : undefined);
3573
+ await persist(last?.type === "text" ? last : undefined, last?.type === "text" ? { ...event, block: last } : undefined);
3544
3574
  break;
3545
3575
  }
3546
3576
  case "reasoning_start":
@@ -3566,11 +3596,14 @@ export class AgentBase {
3566
3596
  : { reasoning: event.reasoning }),
3567
3597
  };
3568
3598
  content[content.length - 1] = finished;
3569
- await persist({ ...event, block: finished });
3599
+ await persist(finished, { ...event, block: finished });
3570
3600
  }
3571
3601
  break;
3572
3602
  }
3573
- case "toolcall_start":
3603
+ case "toolcall_start": {
3604
+ if (providerEvent.type !== "toolcall_start") {
3605
+ throw new Error("The provider tool-call start changed event type.");
3606
+ }
3574
3607
  toolCallIndexes.set(event.callId, content.length);
3575
3608
  content.push({
3576
3609
  type: "tool_call",
@@ -3581,9 +3614,12 @@ export class AgentBase {
3581
3614
  ? {}
3582
3615
  : { namespace: event.namespace }),
3583
3616
  ...(event.server === undefined ? {} : { server: event.server }),
3584
- ...(event.vendor === undefined ? {} : { vendor: event.vendor }),
3617
+ ...(providerEvent.vendor === undefined
3618
+ ? {}
3619
+ : { vendor: providerEvent.vendor }),
3585
3620
  });
3586
3621
  break;
3622
+ }
3587
3623
  case "toolcall_end": {
3588
3624
  const index = toolCallIndexes.get(event.callId);
3589
3625
  const block = index === undefined ? undefined : content[index];
@@ -3596,17 +3632,59 @@ export class AgentBase {
3596
3632
  : { incomplete: event.incomplete }),
3597
3633
  };
3598
3634
  content[index] = finished;
3599
- await persist({ ...event, block: finished });
3635
+ const { vendor: _vendor, ...publicBlock } = finished;
3636
+ await persist(finished, {
3637
+ ...event,
3638
+ block: publicBlock,
3639
+ });
3640
+ }
3641
+ break;
3642
+ }
3643
+ case "toolcall_result_start": {
3644
+ if (providerEvent.type !== "toolcall_result_start") {
3645
+ throw new Error("The provider tool-result start changed event type.");
3646
+ }
3647
+ const callIndex = toolCallIndexes.get(event.callId);
3648
+ const call = callIndex === undefined ? undefined : content[callIndex];
3649
+ if (call?.type !== "tool_call") {
3650
+ throw new Error("A provider tool result has no matching call.");
3600
3651
  }
3652
+ toolResultIndexes.set(event.callId, content.length);
3653
+ content.push({
3654
+ type: "tool_result",
3655
+ callId: event.callId,
3656
+ content: [],
3657
+ ...(providerEvent.vendor === undefined
3658
+ ? {}
3659
+ : { vendor: providerEvent.vendor }),
3660
+ });
3601
3661
  break;
3602
3662
  }
3603
- // The provider settled a server tool call on its own backend and streams the
3604
- // result here. The agent simply ignores it: nothing to execute, nothing to
3605
- // store — the events still reach the hooks like every other event.
3606
- case "toolcall_result_start":
3607
3663
  case "toolcall_result_delta":
3608
- case "toolcall_result_end":
3609
3664
  break;
3665
+ case "toolcall_result_end": {
3666
+ const callIndex = toolCallIndexes.get(event.callId);
3667
+ const call = callIndex === undefined ? undefined : content[callIndex];
3668
+ const resultIndex = toolResultIndexes.get(event.callId);
3669
+ if (resultIndex === undefined || call?.type !== "tool_call") {
3670
+ throw new Error("A provider tool-result end has no matching start.");
3671
+ }
3672
+ const result = content[resultIndex];
3673
+ if (result?.type !== "tool_result") {
3674
+ throw new Error("A provider tool-result end has no matching start.");
3675
+ }
3676
+ const finished = {
3677
+ ...result,
3678
+ content: event.content,
3679
+ ...(event.isError === undefined ? {} : { isError: event.isError }),
3680
+ ...(event.incomplete === undefined
3681
+ ? {}
3682
+ : { incomplete: event.incomplete }),
3683
+ };
3684
+ content[resultIndex] = finished;
3685
+ await persist(finished, undefined);
3686
+ break;
3687
+ }
3610
3688
  case "done":
3611
3689
  return {
3612
3690
  // Only blocks that finished, which are exactly the blocks that were
@@ -3683,41 +3761,11 @@ export class AgentBase {
3683
3761
  }
3684
3762
  }
3685
3763
  }
3686
- /**
3687
- * The conversation a run of records spells out. A compaction record carries the complete
3688
- * replacement context and supersedes everything before it; consecutive blocks belong to one
3689
- * response. Used both to restore the whole history and to read the tail of it, which is why it
3690
- * takes any run of records rather than the store itself.
3691
- */
3692
- function messagesFromRecords(records) {
3693
- let messages = [];
3694
- for (const record of records) {
3695
- if (record.type === "compaction") {
3696
- messages = [...record.messages];
3697
- continue;
3698
- }
3699
- if (record.type === "user" || record.type === "tool" || record.type === "system") {
3700
- messages.push(record.message);
3701
- continue;
3702
- }
3703
- const last = messages[messages.length - 1];
3704
- if (last?.role === "assistant") {
3705
- messages[messages.length - 1] = {
3706
- role: "assistant",
3707
- content: [...last.content, record.block],
3708
- };
3709
- }
3710
- else {
3711
- messages.push({ role: "assistant", content: [record.block] });
3712
- }
3713
- }
3714
- return messages;
3715
- }
3716
3764
  /** The result that stands in for a call the agent could not, or must not, carry out. */
3717
- function toolFailure(callId, reason) {
3765
+ function toolFailure(id, reason) {
3718
3766
  return {
3719
3767
  role: "tool",
3720
- callId,
3768
+ callId: id,
3721
3769
  content: [{ type: "text", text: reason }],
3722
3770
  isError: true,
3723
3771
  };