@slopus/happy-agent-base 0.0.5 → 0.0.6

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.
Files changed (44) hide show
  1. package/README.md +16 -1
  2. package/dist/Agent.d.ts +3 -0
  3. package/dist/Agent.d.ts.map +1 -1
  4. package/dist/Agent.js +10 -2
  5. package/dist/Agent.js.map +1 -1
  6. package/dist/AgentBase.d.ts +10 -0
  7. package/dist/AgentBase.d.ts.map +1 -1
  8. package/dist/AgentBase.js +186 -34
  9. package/dist/AgentBase.js.map +1 -1
  10. package/dist/AgentBaseHooks.d.ts +18 -5
  11. package/dist/AgentBaseHooks.d.ts.map +1 -1
  12. package/dist/AgentConfig.d.ts +12 -5
  13. package/dist/AgentConfig.d.ts.map +1 -1
  14. package/dist/AgentConfig.js +25 -5
  15. package/dist/AgentConfig.js.map +1 -1
  16. package/dist/AgentFeature.d.ts +12 -5
  17. package/dist/AgentFeature.d.ts.map +1 -1
  18. package/dist/AgentFeatureAction.d.ts +5 -0
  19. package/dist/AgentFeatureAction.d.ts.map +1 -1
  20. package/dist/AgentMetadata.d.ts +37 -0
  21. package/dist/AgentMetadata.d.ts.map +1 -0
  22. package/dist/AgentMetadata.js +74 -0
  23. package/dist/AgentMetadata.js.map +1 -0
  24. package/dist/AgentPersistence.d.ts +14 -4
  25. package/dist/AgentPersistence.d.ts.map +1 -1
  26. package/dist/AgentRef.d.ts +6 -1
  27. package/dist/AgentRef.d.ts.map +1 -1
  28. package/dist/AgentRef.js +8 -1
  29. package/dist/AgentRef.js.map +1 -1
  30. package/dist/AgentSystem.d.ts +26 -7
  31. package/dist/AgentSystem.d.ts.map +1 -1
  32. package/dist/AgentSystemLocal.d.ts +14 -8
  33. package/dist/AgentSystemLocal.d.ts.map +1 -1
  34. package/dist/AgentSystemLocal.js +108 -32
  35. package/dist/AgentSystemLocal.js.map +1 -1
  36. package/dist/AgentSystemRef.d.ts +14 -5
  37. package/dist/AgentSystemRef.d.ts.map +1 -1
  38. package/dist/AgentSystemRef.js +25 -6
  39. package/dist/AgentSystemRef.js.map +1 -1
  40. package/dist/index.d.ts +3 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -2
  43. package/dist/index.js.map +1 -1
  44. package/package.json +1 -1
package/dist/AgentBase.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { areProviderModelsCompatible } from "@slopus/happy-providers";
2
+ import { createId } from "@paralleldrive/cuid2";
2
3
  import { AsyncLocalStorage } from "node:async_hooks";
3
4
  import { Value } from "@sinclair/typebox/value";
4
5
  import { asyncLock, createContextNamespace, deterministicStringify, withLifetime, } from "@steve.kite/stdlib";
5
6
  import { withAgentContext, withAgentKV, withAgentPermissionMode, withAgentRunKV, } from "./AgentContexts.js";
7
+ import { agentConfig, ownAgentConfig, withAgentConfig } from "./AgentConfig.js";
6
8
  import { taskContextBeforeToolCall, withAgentTaskContext } from "./AgentTaskContext.js";
7
9
  import { AgentKV } from "./AgentKV.js";
8
10
  import { AGENT_BASE_PENDING_KEY, agentBasePendingStateOf, } from "./AgentBasePending.js";
11
+ import { cuid2Schema, ownAgentMessageMetadata, ownAgentMetadata, } from "./AgentMetadata.js";
9
12
  import { DEFAULT_AGENT_PERMISSION_MODE, isAgentPermissionMode, } from "./AgentPermissionMode.js";
10
13
  import { AgentProviders } from "./AgentProviders.js";
11
14
  /** Race winner when an abort interrupts a wait on the stream or a running tool. */
@@ -25,6 +28,8 @@ const insideTurn = createContextNamespace("agentInsideTurn", []);
25
28
  * its own and outlives whatever happened to start it.
26
29
  */
27
30
  const insideLoops = new AsyncLocalStorage();
31
+ /** Persistence locks held by the current asynchronous call chain, independent of Context. */
32
+ const insidePersistenceLocks = new AsyncLocalStorage();
28
33
  /**
29
34
  * How long a close asked for from inside the agent's own run loop waits for the shutdown before
30
35
  * telling its caller it cannot be waited for. Long enough that a caller which has already let go
@@ -203,6 +208,8 @@ export class AgentBase {
203
208
  #baseCtx;
204
209
  /** The base context extended with the effective selection and the agent's key-value store. */
205
210
  #ctx;
211
+ /** The immutable configuration snapshot carried by every hook context. */
212
+ #config;
206
213
  /** The registry the provider ID is resolved through, each time a session is created. */
207
214
  #providers;
208
215
  /** The registry ID of the provider in force; durable, so a restart resumes on the same one. */
@@ -388,7 +395,8 @@ export class AgentBase {
388
395
  * by, so reading it here leaves the later stage writes nothing to learn from the store.
389
396
  */
390
397
  async #loadPendingState() {
391
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
398
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
399
+ await this.#loadConfig(lockCtx);
392
400
  const stored = await agentBasePendingStateOf(lockCtx, this.#persistence);
393
401
  this.#inherited = stored;
394
402
  this.#inheritedRead = true;
@@ -405,10 +413,11 @@ export class AgentBase {
405
413
  */
406
414
  constructor(ctx, options) {
407
415
  this.id = options.id;
416
+ this.#config = ownAgentConfig(agentConfig(ctx) ?? {});
408
417
  // An agent is its own lifetime. Whatever call happened to construct it — a tool of
409
418
  // another agent, most often — is not a loop this one runs inside, so an inherited
410
419
  // marker is dropped rather than carried into work that outlives that call.
411
- this.#baseCtx = insideTurn.set(ctx, [options.id]);
420
+ this.#baseCtx = withAgentConfig(insideTurn.set(ctx, [options.id]), this.#config);
412
421
  this.#providers = options.providers;
413
422
  this.#providerId = options.provider;
414
423
  this.#persistence = options.persistence;
@@ -436,8 +445,23 @@ export class AgentBase {
436
445
  * the selection changes.
437
446
  */
438
447
  #deriveCtx() {
439
- const ctx = withAgentContext(this.#baseCtx, this.#selection());
440
- return withAgentRunKV(withAgentKV(ctx, this.#kv), this.#runKV);
448
+ return this.#hookContext(this.#baseCtx);
449
+ }
450
+ /** Add this agent's selection and stores to a caller context without losing its transaction. */
451
+ #hookContext(ctx) {
452
+ const selected = withAgentContext(ctx, this.#selection());
453
+ return withAgentRunKV(withAgentKV(selected, this.#kv), this.#runKV);
454
+ }
455
+ /** Load a directly owned configuration written by `updateMetadata`, when one exists. */
456
+ async #loadConfig(ctx) {
457
+ const stored = await this.#persistence.readValues(ctx, "agentConfig");
458
+ const exact = stored.find(({ key }) => key === "agentConfig")?.value;
459
+ if (exact === undefined)
460
+ return;
461
+ const config = ownAgentConfig(exact);
462
+ this.#config = config;
463
+ this.#baseCtx = withAgentConfig(this.#baseCtx, config);
464
+ this.#ctx = this.#deriveCtx();
441
465
  }
442
466
  /** Everything about what the agent is currently running on, as one value to carry. */
443
467
  #selection() {
@@ -489,7 +513,7 @@ export class AgentBase {
489
513
  return undefined;
490
514
  }
491
515
  try {
492
- return await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
516
+ return await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
493
517
  if (!this.#inheritedRead) {
494
518
  this.#inheritedRead = true;
495
519
  this.#inherited = await agentBasePendingStateOf(lockCtx, this.#persistence);
@@ -545,19 +569,72 @@ export class AgentBase {
545
569
  async send(ctx, message, options) {
546
570
  await this.#offer(ctx, "send", message, options);
547
571
  }
572
+ /**
573
+ * Shallow-merge fields into this agent's immutable metadata. The complete AgentConfig and
574
+ * transactional hook writes commit together; observing hooks run only after that commit.
575
+ */
576
+ async updateMetadata(ctx, update) {
577
+ const ownedUpdate = ownAgentMetadata(update);
578
+ if (ownedUpdate === undefined)
579
+ throw new Error("The agent metadata is not valid.");
580
+ if (this.#closed)
581
+ throw new Error("The agent has been closed.");
582
+ if (insideTurn.get(ctx).includes(this.id) ||
583
+ this.#insideOwnLoop() ||
584
+ this.#insideOwnPersistenceLock()) {
585
+ throw new Error("Updating metadata from inside this agent's current operation would wait for " +
586
+ "that same operation to finish. Update it after the hook or tool returns.");
587
+ }
588
+ let change;
589
+ let next;
590
+ await this.#runInPersistenceLock(ctx, async (lockCtx) => {
591
+ const previousMetadata = ownAgentMetadata(this.#config.metadata ?? {});
592
+ const metadata = ownAgentMetadata({ ...previousMetadata, ...ownedUpdate });
593
+ if (previousMetadata === undefined || metadata === undefined) {
594
+ throw new Error("The agent metadata is not valid.");
595
+ }
596
+ next = ownAgentConfig({ ...this.#config, metadata });
597
+ change = {
598
+ agentId: this.id,
599
+ previousMetadata,
600
+ update: ownedUpdate,
601
+ metadata,
602
+ };
603
+ await this.#persistence.transaction(lockCtx, async (txCtx) => {
604
+ await this.#persistence.writeValue(txCtx, "agentConfig", next);
605
+ await this.#withTransactionalContext(withAgentConfig(txCtx, next), async (hookCtx) => await this.#hooks.metadataChangedTransact?.(this.#hookContext(hookCtx), change));
606
+ });
607
+ this.#config = next;
608
+ this.#baseCtx = withAgentConfig(this.#baseCtx, next);
609
+ this.#ctx = this.#deriveCtx();
610
+ });
611
+ await this.#invokeHookOn(this.#hookContext(withAgentConfig(ctx, next)), this.#hooks.metadataChanged, change);
612
+ }
548
613
  /**
549
614
  * Hand one message to a durable queue. The acceptance runs whether or not the caller waits
550
615
  * for it — an unwaited failure is still a message that never entered the conversation, and
551
616
  * the agent's own close still drains it, so nothing is dropped by not looking.
552
617
  */
553
618
  async #offer(ctx, kind, message, options) {
554
- const { await: wait = false, ...settings } = options ?? {};
619
+ const { await: wait = false, id = createId(), metadata: suppliedMetadata, ...settings } = options ?? {};
620
+ if (!Value.Check(cuid2Schema, id)) {
621
+ throw new Error("The message ID must be a cuid2 identity.");
622
+ }
623
+ const metadata = ownAgentMessageMetadata(suppliedMetadata);
555
624
  // Refusing the flag rather than the operation: a closed agent and a re-entrant wait are
556
625
  // both caller mistakes, and both are reported before any work is started.
557
626
  this.#assertCanWait(ctx, wait, kind === "steering" ? "a steered message" : "a sent message");
558
627
  if (this.#closed)
559
628
  throw new Error("The agent has been closed.");
560
- const accepted = this.#enqueue(ctx, [{ kind, message, options: settings }]);
629
+ const accepted = this.#enqueue(ctx, [
630
+ {
631
+ kind,
632
+ id,
633
+ message: structuredClone(message),
634
+ ...(metadata === undefined ? {} : { metadata }),
635
+ options: settings,
636
+ },
637
+ ]);
561
638
  if (wait)
562
639
  return accepted;
563
640
  accepted.catch(() => undefined);
@@ -585,6 +662,19 @@ export class AgentBase {
585
662
  #insideOwnLoop() {
586
663
  return insideLoops.getStore()?.includes(this.id) === true;
587
664
  }
665
+ /** Whether this call chain already holds this agent's persistence lock. */
666
+ #insideOwnPersistenceLock() {
667
+ return insidePersistenceLocks.getStore()?.includes(this.id) === true;
668
+ }
669
+ /** Hold the persistence lock while marking it independently of the caller's Context. */
670
+ async #runInPersistenceLock(ctx, work) {
671
+ return await this.#persistenceLock.runInLock(ctx, async (lockCtx) => {
672
+ const held = insidePersistenceLocks.getStore() ?? [];
673
+ return await insidePersistenceLocks.run([...held, this.id], async () => {
674
+ return await work(lockCtx);
675
+ });
676
+ });
677
+ }
588
678
  /**
589
679
  * Accept a batch of messages as one durable step. Every message is written under the same
590
680
  * hold of the persistence lock and inside one transaction, so a caller arriving while a
@@ -598,17 +688,25 @@ export class AgentBase {
598
688
  throw new Error("The agent has been closed.");
599
689
  // Admitted: from here on the messages are the agent's responsibility, and a close that
600
690
  // begins now waits for them rather than resolving over the top of them.
601
- const admitted = this.#persistenceLock.runInLock(ctx, async (lockCtx) => {
691
+ const admitted = this.#runInPersistenceLock(ctx, async (lockCtx) => {
602
692
  const accepted = [];
603
693
  await this.#persistence.transaction(lockCtx, async (txCtx) => {
604
694
  for (const request of batch) {
695
+ const identityKey = `message.${request.id}`;
696
+ if (!(await this.#persistence.writeValueIfAbsent(txCtx, identityKey, true))) {
697
+ continue;
698
+ }
605
699
  const key = await this.#queueKey(txCtx, `${request.kind}.`);
606
700
  await this.#persistence.writeValue(txCtx, key, {
701
+ id: request.id,
607
702
  message: request.message,
703
+ ...(request.metadata === undefined ? {} : { metadata: request.metadata }),
608
704
  options: request.options,
609
705
  });
610
706
  accepted.push({ key, request });
611
707
  }
708
+ if (accepted.length === 0)
709
+ return;
612
710
  // Accepting a message is what makes the work owed: the same transaction that
613
711
  // admits it records that the agent owes an answer, so a process that dies right
614
712
  // here is discovered still owing it rather than looking idle over a full queue.
@@ -619,8 +717,16 @@ export class AgentBase {
619
717
  // one replaces the queue arrays wholesale, and a reference taken before the wait
620
718
  // would push the message into an array nobody reads again.
621
719
  const queue = request.kind === "steering" ? this.#steering : this.#sends;
622
- queue.push({ key, message: request.message, options: request.options });
720
+ queue.push({
721
+ key,
722
+ id: request.id,
723
+ message: request.message,
724
+ ...(request.metadata === undefined ? {} : { metadata: request.metadata }),
725
+ options: request.options,
726
+ });
623
727
  }
728
+ if (accepted.length === 0)
729
+ return;
624
730
  this.#turnRequested = true;
625
731
  this.#startRun();
626
732
  });
@@ -962,7 +1068,7 @@ export class AgentBase {
962
1068
  // durable exactly as it was for the next attempt.
963
1069
  const loadFailure = await this.#ensureLoaded().then(() => undefined, (error) => error);
964
1070
  if (loadFailure !== undefined) {
965
- this.#emit({
1071
+ await this.#emit({
966
1072
  type: "done",
967
1073
  state: "error",
968
1074
  kind: "internal_error",
@@ -1011,7 +1117,7 @@ export class AgentBase {
1011
1117
  */
1012
1118
  async #settleDurably() {
1013
1119
  try {
1014
- await this.#persistenceLock.runInLock(this.#ctx, (lockCtx) => this.#recordTransaction(lockCtx, async (txCtx) => {
1120
+ await this.#runInPersistenceLock(this.#ctx, (lockCtx) => this.#recordTransaction(lockCtx, async (txCtx) => {
1015
1121
  await this.#clearPending(txCtx);
1016
1122
  await this.#invokeTransactionalSettle(txCtx);
1017
1123
  // The run store is erased last, so a settling hook can still read what the
@@ -1164,9 +1270,21 @@ export class AgentBase {
1164
1270
  this.#ensureCompaction().catch(() => undefined);
1165
1271
  continue;
1166
1272
  }
1273
+ const id = action.id ?? createId();
1274
+ if (!Value.Check(cuid2Schema, id))
1275
+ continue;
1276
+ let metadata;
1277
+ try {
1278
+ metadata = ownAgentMessageMetadata(action.metadata);
1279
+ }
1280
+ catch {
1281
+ continue;
1282
+ }
1167
1283
  batch.push({
1168
1284
  kind: action.type === "steer" ? "steering" : "send",
1169
- message: action.message,
1285
+ id,
1286
+ message: structuredClone(action.message),
1287
+ ...(metadata === undefined ? {} : { metadata }),
1170
1288
  options: {},
1171
1289
  });
1172
1290
  }
@@ -1213,7 +1331,8 @@ export class AgentBase {
1213
1331
  let needsInference = resumed.length > 0;
1214
1332
  if (!this.#recoveryChecked) {
1215
1333
  this.#recoveryChecked = true;
1216
- needsInference ||= this.#resumesInterruptedRun();
1334
+ if (await this.#resumesInterruptedRun())
1335
+ needsInference = true;
1217
1336
  }
1218
1337
  // Each cycle first drains the queues, then runs one inference. Steering injects at
1219
1338
  // every stop between responses and always outranks sends; sent messages inject
@@ -1232,7 +1351,7 @@ export class AgentBase {
1232
1351
  if (abort.signal.aborted) {
1233
1352
  const hasPendingWork = needsInference || this.#steering.length > 0 || this.#sends.length > 0;
1234
1353
  if (hasPendingWork)
1235
- this.#emit({ type: "done", state: "cancelled" });
1354
+ await this.#emit({ type: "done", state: "cancelled" });
1236
1355
  break;
1237
1356
  }
1238
1357
  let injected = await this.#consumeQueue(this.#steering, this.#steeringMode, "steering");
@@ -1323,7 +1442,7 @@ export class AgentBase {
1323
1442
  }
1324
1443
  }
1325
1444
  catch (error) {
1326
- this.#emit({
1445
+ await this.#emit({
1327
1446
  type: "done",
1328
1447
  state: "error",
1329
1448
  kind: "internal_error",
@@ -1350,12 +1469,13 @@ export class AgentBase {
1350
1469
  * beginning of a block that will now never arrive is told to drop it. Only finished blocks
1351
1470
  * are persisted, so the conversation is intact and it is the view being corrected.
1352
1471
  */
1353
- #resumesInterruptedRun() {
1472
+ async #resumesInterruptedRun() {
1354
1473
  const owed = this.#lastRecordType === "user" ||
1355
1474
  this.#lastRecordType === "tool" ||
1356
1475
  this.#lastRecordType === "system";
1357
- if (owed && this.#inherited?.stage === "inference")
1358
- this.#emit({ type: "block_reset" });
1476
+ if (owed && this.#inherited?.stage === "inference") {
1477
+ await this.#emit({ type: "block_reset" });
1478
+ }
1359
1479
  return owed;
1360
1480
  }
1361
1481
  /** Load the durable state once. A failed load is not sticky: the next turn retries it. */
@@ -1375,7 +1495,7 @@ export class AgentBase {
1375
1495
  const previousTokens = this.#contextTokens;
1376
1496
  this.#contextTokens = tokens;
1377
1497
  try {
1378
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
1498
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
1379
1499
  const write = (writeCtx) => tokens === undefined
1380
1500
  ? this.#persistence.deleteValue(writeCtx, "context")
1381
1501
  : this.#persistence.writeValue(writeCtx, "context", { tokens });
@@ -1426,10 +1546,11 @@ export class AgentBase {
1426
1546
  throw new Error(result.message);
1427
1547
  }
1428
1548
  if (result.status === "completed") {
1429
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
1549
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
1430
1550
  // Physically delete the superseded records and write the replacement —
1431
1551
  // which keeps the messages that stay — in one atomic step.
1432
1552
  await this.#recordTransaction(lockCtx, async (txCtx) => {
1553
+ await this.#deleteMessageIdentities(txCtx, await this.#persistence.load(txCtx));
1433
1554
  await this.#persistence.clearRecords(txCtx);
1434
1555
  await this.#persistence.append(txCtx, {
1435
1556
  type: "compaction",
@@ -1469,7 +1590,7 @@ export class AgentBase {
1469
1590
  return true;
1470
1591
  let settled = false;
1471
1592
  try {
1472
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
1593
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
1473
1594
  // A call the durable batch still holds belongs to the resume, which answers it
1474
1595
  // properly — and re-executes it when the tool is durable. Settling it here as
1475
1596
  // well would give the conversation two results for one call.
@@ -1505,6 +1626,14 @@ export class AgentBase {
1505
1626
  async #appendRecord(ctx, record) {
1506
1627
  await this.#persistence.append(ctx, record);
1507
1628
  }
1629
+ /** Remove deduplication identities for user records a history replacement is deleting. */
1630
+ async #deleteMessageIdentities(ctx, records) {
1631
+ for (const record of records) {
1632
+ if (record.type === "user") {
1633
+ await this.#persistence.deleteValue(ctx, `message.${record.id}`);
1634
+ }
1635
+ }
1636
+ }
1508
1637
  /**
1509
1638
  * A transaction whose pending-state cache unwinds with it.
1510
1639
  */
@@ -1538,7 +1667,7 @@ export class AgentBase {
1538
1667
  content: [{ type: "text", text: `The last turn failed: ${message}` }],
1539
1668
  };
1540
1669
  try {
1541
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
1670
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
1542
1671
  await this.#appendRecord(lockCtx, { type: "system", message: failure });
1543
1672
  this.#messages.push(failure);
1544
1673
  });
@@ -1561,7 +1690,7 @@ export class AgentBase {
1561
1690
  /** Filled in once the consumption has committed, and reported after the lock is released. */
1562
1691
  const accepted = [];
1563
1692
  let permissionChange;
1564
- const consumed = await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
1693
+ const consumed = await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
1565
1694
  if (queue.length === 0)
1566
1695
  return false;
1567
1696
  // The durable queue, not memory, decides what is left to consume after a restart.
@@ -1698,6 +1827,7 @@ export class AgentBase {
1698
1827
  await this.#persistence.deleteValue(txCtx, entry.key);
1699
1828
  }
1700
1829
  if (reset) {
1830
+ await this.#deleteMessageIdentities(txCtx, await this.#persistence.load(txCtx));
1701
1831
  await this.#persistence.clearRecords(txCtx);
1702
1832
  // The erased conversation is what the measurement described.
1703
1833
  await this.#persistence.deleteValue(txCtx, "context");
@@ -1711,7 +1841,9 @@ export class AgentBase {
1711
1841
  for (const entry of batch) {
1712
1842
  await this.#appendRecord(txCtx, {
1713
1843
  type: "user",
1844
+ id: entry.id,
1714
1845
  message: entry.message,
1846
+ ...(entry.metadata === undefined ? {} : { metadata: entry.metadata }),
1715
1847
  });
1716
1848
  }
1717
1849
  if (changed) {
@@ -1735,13 +1867,23 @@ export class AgentBase {
1735
1867
  await this.#invokeTransactHook(txCtx, selection, this.#hooks.permissionModeChangedTransact, modeChange);
1736
1868
  }
1737
1869
  for (const entry of batch) {
1738
- await this.#invokeTransactHook(txCtx, selection, this.#hooks.messageAcceptedTransact, { kind, message: entry.message });
1870
+ await this.#invokeTransactHook(txCtx, selection, this.#hooks.messageAcceptedTransact, {
1871
+ id: entry.id,
1872
+ kind,
1873
+ message: entry.message,
1874
+ ...(entry.metadata === undefined ? {} : { metadata: entry.metadata }),
1875
+ });
1739
1876
  }
1740
1877
  });
1741
1878
  // Committed: from here the messages are part of the conversation, so what has to be
1742
1879
  // announced about them is decided now and reported once the lock is released.
1743
1880
  permissionChange = modeChange;
1744
- accepted.push(...batch.map((entry) => ({ kind, message: entry.message })));
1881
+ accepted.push(...batch.map((entry) => ({
1882
+ id: entry.id,
1883
+ kind,
1884
+ message: entry.message,
1885
+ ...(entry.metadata === undefined ? {} : { metadata: entry.metadata }),
1886
+ })));
1745
1887
  queue.splice(0, count);
1746
1888
  if (reset) {
1747
1889
  this.#messages = injected === undefined ? [] : [injected];
@@ -1794,7 +1936,7 @@ export class AgentBase {
1794
1936
  * not-yet-consumed queues. Consecutive block records reassemble into one assistant message.
1795
1937
  */
1796
1938
  async #loadHistory() {
1797
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
1939
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
1798
1940
  const records = await this.#persistence.load(lockCtx);
1799
1941
  const last = records[records.length - 1];
1800
1942
  this.#lastRecordType = last?.type;
@@ -1811,7 +1953,17 @@ export class AgentBase {
1811
1953
  this.#contextTokens = measured?.tokens;
1812
1954
  const entry = (key, value) => {
1813
1955
  const envelope = value;
1814
- return { key, message: envelope.message, options: envelope.options ?? {} };
1956
+ if (!Value.Check(cuid2Schema, envelope.id)) {
1957
+ throw new Error(`The queued message under "${key}" has an invalid ID.`);
1958
+ }
1959
+ const metadata = ownAgentMessageMetadata(envelope.metadata);
1960
+ return {
1961
+ key,
1962
+ id: envelope.id,
1963
+ message: envelope.message,
1964
+ ...(metadata === undefined ? {} : { metadata }),
1965
+ options: envelope.options ?? {},
1966
+ };
1815
1967
  };
1816
1968
  this.#steering = steering.map(({ key, value }) => entry(key, value));
1817
1969
  this.#sends = sends.map(({ key, value }) => entry(key, value));
@@ -1867,7 +2019,7 @@ export class AgentBase {
1867
2019
  */
1868
2020
  async #runToolBatch(entries, resume, signal, abortPromise) {
1869
2021
  if (!resume) {
1870
- await this.#persistenceLock.runInLock(this.#ctx, (lockCtx) => this.#recordTransaction(lockCtx, async (txCtx) => {
2022
+ await this.#runInPersistenceLock(this.#ctx, (lockCtx) => this.#recordTransaction(lockCtx, async (txCtx) => {
1871
2023
  for (const entry of entries) {
1872
2024
  await this.#persistence.writeValue(txCtx, entry.key, entry.call);
1873
2025
  }
@@ -1899,7 +2051,7 @@ export class AgentBase {
1899
2051
  if (commitFailed)
1900
2052
  return;
1901
2053
  try {
1902
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
2054
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
1903
2055
  while (committed < entries.length) {
1904
2056
  const entry = entries[committed];
1905
2057
  const result = results[committed];
@@ -2171,7 +2323,7 @@ export class AgentBase {
2171
2323
  const persist = async (event) => {
2172
2324
  if (event === undefined)
2173
2325
  return;
2174
- await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
2326
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
2175
2327
  if (this.#hooks.onEventTransact === undefined) {
2176
2328
  await this.#appendRecord(lockCtx, { type: "block", block: event.block });
2177
2329
  }
@@ -2196,7 +2348,7 @@ export class AgentBase {
2196
2348
  const next = await Promise.race([iterator.next(), abortPromise]);
2197
2349
  if (next === ABORTED) {
2198
2350
  // Drop the unfinished block and end the turn.
2199
- this.#emit({ type: "done", state: "cancelled" });
2351
+ await this.#emit({ type: "done", state: "cancelled" });
2200
2352
  return { content: persisted, state: "cancelled" };
2201
2353
  }
2202
2354
  if (next.done === true) {
@@ -2204,7 +2356,7 @@ export class AgentBase {
2204
2356
  break;
2205
2357
  }
2206
2358
  const event = next.value;
2207
- this.#emit(event);
2359
+ await this.#emit(event);
2208
2360
  switch (event.type) {
2209
2361
  case "text_start":
2210
2362
  content.push({ type: "text", text: "" });
@@ -2355,9 +2507,9 @@ export class AgentBase {
2355
2507
  return this.#session;
2356
2508
  }
2357
2509
  /** Report one stream event to the hooks. Hooks observe the stream; they never fail a run. */
2358
- #emit(event) {
2510
+ async #emit(event) {
2359
2511
  try {
2360
- this.#hooks.onEvent?.(this.#ctx, event);
2512
+ await this.#hooks.onEvent?.(this.#ctx, event);
2361
2513
  }
2362
2514
  catch {
2363
2515
  // Hooks observe the stream; they never fail a run.