@slopus/happy-agent-base 0.0.14 → 0.0.16

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
@@ -25,10 +25,19 @@ active Drizzle facade as `ctx.db`; `ctx.inTx(work)` and the exported `inTx(ctx,
25
25
  an outer transaction or reuse the one already carried by the context. Outside a transaction,
26
26
  stdlib starts post-commit callbacks on the next microtask.
27
27
 
28
- Storage, KV, migrations, and transactional module hooks compose with a context's outer transaction.
29
- Live `Agent` and `AgentSystem` commands do not: creating, resolving, messaging,
30
- mutating, archiving, or closing a live agent from inside an outer storage transaction is rejected
31
- because the corresponding in-memory lifetime cannot be published until that transaction commits.
28
+ Storage, KV, migrations, transactional module hooks, and message delivery compose with a context's
29
+ outer transaction. `send` and `steer` persist their queue entry and pending-work marker inside that
30
+ transaction, then publish the in-memory queue and start the run only through `afterCommit`; rollback
31
+ therefore leaves no live effect. Delivery inside a transaction never waits on the agent's internal
32
+ persistence lock — the caller holds the database writer while its transaction stays open, and a
33
+ running turn takes that lock before touching the database, so waiting here could deadlock against a
34
+ live turn; the open transaction supplies the atomicity the lock otherwise guarantees.
35
+ Transactional routing through `AgentSystem.send` or `steer`
36
+ loads an idle target on the way: instantiation reads only committed state and builds memory, so a
37
+ rolled-back delivery leaves nothing but an idle live object with no durable work to pick up, and
38
+ the target's run starts only when the commit publishes the message. Other live `Agent` and
39
+ `AgentSystem` lifetime commands—creating, resolving, mutating, archiving, or closing—remain
40
+ rejected inside an outer transaction.
32
41
 
33
42
  An agent runs in one of four permission modes — `read_only`, `workspace_write`, `auto`, and
34
43
  `full_access` — carried on every context it derives and read back with `agentPermissionMode`. A
@@ -75,9 +84,11 @@ Messages receive a generated cuid2 identity, or accept one through `{ id }` for
75
84
  delivery. A repeated ID is an ignored persistence conflict while its message remains in the
76
85
  durable conversation; deliberate conversation replacement releases identities for the records it
77
86
  removes. `send` and `steer` return the effective ID, delivery mode, and whether durable acceptance
78
- created the identity or found it already present. Optional immutable metadata travels beside the
79
- provider message and reaches both message-accepted hooks; module-generated send and steer actions
80
- accept the same fields.
87
+ created the identity or found it already present. Inside an outer transaction, even an operation
88
+ requested with `await: false` completes its durable queue write before returning, because work may
89
+ not retain a transaction context after the transaction body ends. Optional immutable metadata
90
+ travels beside the provider message and reaches both message-accepted hooks; module-generated send
91
+ and steer actions accept the same fields.
81
92
 
82
93
  Base allocates cuid2 identities for every settled-to-settled loop, turn, inference, and settlement.
83
94
  The IDs are persisted with outstanding work before their first lifecycle hook, survive restart,
@@ -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
@@ -289,6 +289,8 @@ export class AgentBase {
289
289
  #steering = [];
290
290
  /** The durable send queue, in the order its keys sort. */
291
291
  #sends = [];
292
+ /** A committed queue write that the next turn still has to reload from storage. */
293
+ #committedQueueDirty = false;
292
294
  /** Hook notices waiting to join history after tool settlement and compaction. */
293
295
  #injections = [];
294
296
  /** A consumed notice that still needs one provider response. */
@@ -706,7 +708,13 @@ export class AgentBase {
706
708
  * the agent's own close still drains it, so nothing is dropped by not looking.
707
709
  */
708
710
  async #offer(ctx, kind, message, options) {
709
- this.#assertOutsideStorageTransaction(ctx, "message delivery");
711
+ const outerTransaction = agentStorageTransaction(ctx);
712
+ if (outerTransaction?.lifetime.aborted === true) {
713
+ throw new Error("The agent storage transaction carried by this context has ended.");
714
+ }
715
+ if (outerTransaction !== undefined && this.#insideOwnPersistenceLock()) {
716
+ throw new Error("Agent message delivery cannot reenter its own persistence operation inside a transaction.");
717
+ }
710
718
  const { await: requestedWait, id = createId(), metadata: suppliedMetadata, ...settings } = options ?? {};
711
719
  if (!Value.Check(cuid2Schema, id)) {
712
720
  throw new Error("The message ID must be a cuid2 identity.");
@@ -719,7 +727,9 @@ export class AgentBase {
719
727
  if (this.#closed)
720
728
  throw new Error("The agent has been closed.");
721
729
  const knownInProcess = this.#offeredMessageIds.has(id);
722
- this.#offeredMessageIds.add(id);
730
+ if (outerTransaction === undefined) {
731
+ this.#offeredMessageIds.add(id);
732
+ }
723
733
  const accepted = this.#enqueue(ctx, [
724
734
  {
725
735
  kind,
@@ -729,6 +739,16 @@ export class AgentBase {
729
739
  options: settings,
730
740
  },
731
741
  ]);
742
+ // Work using an outer transaction must finish its durable writes before that transaction
743
+ // body can return. The caller may still choose not to wait for an ordinary independent
744
+ // acceptance, but a carried transaction cannot safely outlive unfinished work using it.
745
+ if (outerTransaction !== undefined) {
746
+ const [result] = await accepted;
747
+ if (result === undefined) {
748
+ throw new Error("The message acceptance result was lost.");
749
+ }
750
+ return result;
751
+ }
732
752
  if (wait) {
733
753
  try {
734
754
  const [result] = await accepted;
@@ -808,7 +828,20 @@ export class AgentBase {
808
828
  throw new Error("The agent has been closed.");
809
829
  // Admitted: from here on the messages are the agent's responsibility, and a close that
810
830
  // begins now waits for them rather than resolving over the top of them.
811
- const admitted = this.#runInPersistenceLock(ctx, async (lockCtx) => {
831
+ const admitted = agentStorageTransaction(ctx) === undefined
832
+ ? this.#enqueueIndependently(ctx, batch)
833
+ : this.#enqueueInTransaction(ctx, batch);
834
+ this.#admitted.add(admitted);
835
+ try {
836
+ return await admitted;
837
+ }
838
+ finally {
839
+ this.#admitted.delete(admitted);
840
+ }
841
+ }
842
+ /** Accept a batch under the persistence lock, in a transaction of the acceptance's own. */
843
+ async #enqueueIndependently(ctx, batch) {
844
+ return await this.#runInPersistenceLock(ctx, async (lockCtx) => {
812
845
  const accepted = [];
813
846
  const results = [];
814
847
  await this.#recordTransaction(lockCtx, async (txCtx) => {
@@ -862,13 +895,106 @@ export class AgentBase {
862
895
  }
863
896
  return results;
864
897
  });
865
- this.#admitted.add(admitted);
866
- try {
867
- return await admitted;
868
- }
869
- finally {
870
- this.#admitted.delete(admitted);
898
+ }
899
+ /**
900
+ * Accept a batch inside the caller's own open transaction. This path must not take the
901
+ * agent's persistence lock: the caller holds the database writer for as long as its
902
+ * transaction stays open, while a running turn takes the lock first and the database second,
903
+ * so waiting for the lock here would close a cycle nothing could break. The outer transaction
904
+ * supplies the atomicity the lock otherwise guarantees, queue keys are claimed with
905
+ * absent-only writes so a racing independent enqueue cannot be overwritten, and no heap
906
+ * state changes until the commit publishes the batch.
907
+ */
908
+ async #enqueueInTransaction(ctx, batch) {
909
+ const results = [];
910
+ let acceptedAny = false;
911
+ for (const request of batch) {
912
+ const identityKey = `message.${request.id}`;
913
+ if (!(await this.#persistence.writeValueIfAbsent(ctx, identityKey, true))) {
914
+ results.push({
915
+ id: request.id,
916
+ delivery: request.kind === "steering" ? "steer" : "send",
917
+ accepted: "existing",
918
+ });
919
+ continue;
920
+ }
921
+ await this.#claimQueueKey(ctx, `${request.kind}.`, {
922
+ id: request.id,
923
+ message: request.message,
924
+ ...(request.metadata === undefined ? {} : { metadata: request.metadata }),
925
+ options: request.options,
926
+ });
927
+ acceptedAny = true;
928
+ results.push({
929
+ id: request.id,
930
+ delivery: request.kind === "steering" ? "steer" : "send",
931
+ accepted: "created",
932
+ });
871
933
  }
934
+ // Accepting a message is what makes the work owed, so the same transaction that admits
935
+ // it records that the agent owes an answer; here that record is staged durably and made
936
+ // live only once the outermost commit publishes the whole batch.
937
+ const stagedPending = acceptedAny
938
+ ? await this.#stagePendingMessageWork(ctx)
939
+ : undefined;
940
+ const offeredIds = batch.map(({ id }) => id);
941
+ afterCommit(ctx, async () => {
942
+ await this.#activateCommittedMessages(offeredIds, stagedPending);
943
+ });
944
+ return results;
945
+ }
946
+ /**
947
+ * Persist the pending inference an outer transaction will make live after commit, without
948
+ * changing any heap state that would survive a rollback. Reading the transaction's current
949
+ * value also makes multiple sends in one outer transaction reuse the same lifecycle IDs.
950
+ */
951
+ async #stagePendingMessageWork(ctx) {
952
+ const stored = await agentBasePendingStateOf(ctx, this.#persistence);
953
+ const pending = stored === undefined
954
+ ? { stage: "inference", loopId: createId() }
955
+ : { ...stored, stage: "inference" };
956
+ await this.#persistence.writeValue(ctx, AGENT_BASE_PENDING_KEY, pending);
957
+ return pending;
958
+ }
959
+ /**
960
+ * Publish a transactionally accepted message only after the outermost commit. The next turn
961
+ * reloads the durable queue instead of copying staged entries into memory; the dirty marker
962
+ * prevents a turn already in flight from clearing the request before that reload happens.
963
+ */
964
+ async #activateCommittedMessages(offeredIds, stagedPending) {
965
+ for (const id of offeredIds)
966
+ this.#offeredMessageIds.add(id);
967
+ if (stagedPending === undefined)
968
+ return;
969
+ await this.#runInPersistenceLock(this.#ctx, async (lockCtx) => {
970
+ let pending = stagedPending;
971
+ try {
972
+ pending =
973
+ (await agentBasePendingStateOf(lockCtx, this.#persistence)) ?? stagedPending;
974
+ }
975
+ catch {
976
+ // The committed staged value is enough to start; a turn reload retries storage.
977
+ }
978
+ // A run in flight holds its lifecycle IDs across many lock acquisitions, so adopting
979
+ // over it would tear the identities out from under a live turn. That run reaches the
980
+ // committed batch through the dirty marker instead; adoption is for an idle agent
981
+ // whose next run should continue exactly what the commit recorded.
982
+ if (this.#runPromise === undefined) {
983
+ this.#adoptPendingState(pending);
984
+ }
985
+ this.#committedQueueDirty = true;
986
+ });
987
+ this.#turnRequested = true;
988
+ this.#startRun();
989
+ }
990
+ /** Make one committed pending record the exact in-memory lifecycle state. */
991
+ #adoptPendingState(pending) {
992
+ this.#pending = pending;
993
+ this.#pendingWritten = deterministicStringify(pending);
994
+ this.#loopId = pending.loopId;
995
+ this.#turnId = pending.turnId;
996
+ this.#inferenceId = pending.inferenceId;
997
+ this.#settlementId = pending.settlementId;
872
998
  }
873
999
  /** Durably queue hook notices until history reaches a safe append boundary. */
874
1000
  async #enqueueInjections(ctx, batch) {
@@ -2060,6 +2186,7 @@ export class AgentBase {
2060
2186
  #clearTurnRequestIfNoPendingInput() {
2061
2187
  if (this.#steering.length === 0 &&
2062
2188
  this.#sends.length === 0 &&
2189
+ !this.#committedQueueDirty &&
2063
2190
  this.#injections.length === 0 &&
2064
2191
  !this.#noticeAwaitingResponse &&
2065
2192
  this.#compaction === undefined) {
@@ -2399,6 +2526,7 @@ export class AgentBase {
2399
2526
  this.#pendingToolsUndispatched = true;
2400
2527
  }
2401
2528
  }
2529
+ this.#committedQueueDirty = false;
2402
2530
  });
2403
2531
  }
2404
2532
  /**
@@ -2896,6 +3024,22 @@ export class AgentBase {
2896
3024
  // a clock that went backwards: continue the sequence rather than starting it again.
2897
3025
  return key(lastSlot, Number(lastSequence) + 1);
2898
3026
  }
3027
+ /**
3028
+ * Write a queue entry under a key nothing else holds. An enqueue running outside the
3029
+ * persistence lock can race an independent one for the tail position, so the key is claimed
3030
+ * with an absent-only write and a taken key moves one sequence further rather than
3031
+ * overwriting whatever claimed it first.
3032
+ */
3033
+ async #claimQueueKey(ctx, prefix, envelope) {
3034
+ let key = await this.#queueKey(ctx, prefix);
3035
+ for (let attempt = 0; attempt < 1000; attempt += 1) {
3036
+ if (await this.#persistence.writeValueIfAbsent(ctx, key, envelope))
3037
+ return key;
3038
+ const [slot, sequence] = key.slice(prefix.length).split(".");
3039
+ key = `${prefix}${slot}.${String(Number(sequence) + 1).padStart(6, "0")}`;
3040
+ }
3041
+ throw new Error("A queue key could not be claimed after repeated collisions.");
3042
+ }
2899
3043
  /**
2900
3044
  * Consume one response stream into the assistant message it spells out, appending each block
2901
3045
  * to the store as it finishes and reporting every event to the hooks. What comes back is what