@slopus/happy-agent-base 0.0.13 → 0.0.15

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,17 @@ 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
+ requires a target that is already live; loading an idle target remains a separate lifetime
37
+ operation. Other live `Agent` and `AgentSystem` lifetime commands—creating, resolving, mutating,
38
+ archiving, or closing—remain rejected inside an outer transaction.
32
39
 
33
40
  An agent runs in one of four permission modes — `read_only`, `workspace_write`, `auto`, and
34
41
  `full_access` — carried on every context it derives and read back with `agentPermissionMode`. A
@@ -75,9 +82,11 @@ Messages receive a generated cuid2 identity, or accept one through `{ id }` for
75
82
  delivery. A repeated ID is an ignored persistence conflict while its message remains in the
76
83
  durable conversation; deliberate conversation replacement releases identities for the records it
77
84
  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.
85
+ created the identity or found it already present. Inside an outer transaction, even an operation
86
+ requested with `await: false` completes its durable queue write before returning, because work may
87
+ not retain a transaction context after the transaction body ends. Optional immutable metadata
88
+ travels beside the provider message and reaches both message-accepted hooks; module-generated send
89
+ and steer actions accept the same fields.
81
90
 
82
91
  Base allocates cuid2 identities for every settled-to-settled loop, turn, inference, and settlement.
83
92
  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":"AAAA,OAAO,KAAK,EAMR,sBAAsB,EACtB,kBAAkB,EAKlB,kBAAkB,EACrB,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EAQH,KAAK,OAAO,EACf,MAAM,oBAAoB,CAAC;AAsB5B,OAAO,KAAK,EAIR,cAAc,EAQjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,gBAAgB,EAAe,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAIH,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAErB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAGH,KAAK,mBAAmB,EAC3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAsCrD,mGAAmG;AACnG,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG,KAAK,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IACzC,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,iFAAiF;IACjF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACjD;AACD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC5B;AAkED,iGAAiG;AACjG,MAAM,WAAW,gBAAgB;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC;IAChC,mDAAmD;IACnD,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,mBAAmB,CAAC;IAC9C,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAC3C,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2JG;AACH,qBAAa,SAAS;;IAClB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAoL/B;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAEzE;IAED;;;;;;;;;OASG;IACH,OAAa,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAI7E;IAED;;;;;;;;OAQG;IACH,OAAa,UAAU,CACnB,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,gBAAgB,GAC1B,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAKhC;IAuBD;;;;;OAKG;IACH,OAAO,eAsCN;IA8ED;;;;;OAKG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAuFD;;;;;;OAMG;IACG,KAAK,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,kBAAkB,EAC3B,OAAO,CAAC,EAAE,uBAAuB,GAAG,qBAAqB,GAC1D,OAAO,CAAC,sBAAsB,CAAC,CAEjC;IAED;;;;;;OAMG;IACG,IAAI,CACN,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,kBAAkB,EAC3B,OAAO,CAAC,EAAE,uBAAuB,GAAG,qBAAqB,GAC1D,OAAO,CAAC,sBAAsB,CAAC,CAEjC;IAED;;;OAGG;IACG,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDvE;IAuND;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAGZ;IAED;;;;;OAKG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjC;IAED;;;;;;;;;OASG;IACG,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB1E;IAmGD;;;;;;;;;;;OAWG;IACG,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBxE;IAgBD;;;;;;;;;;;;;;;;OAgBG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAuD3B;IAED;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAInC;CAurEJ"}
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
  /**
@@ -2675,11 +2803,19 @@ export class AgentBase {
2675
2803
  return failure("The tool call was incomplete and was not executed.");
2676
2804
  }
2677
2805
  let args;
2678
- try {
2679
- args = call.arguments.trim().length === 0 ? {} : JSON.parse(call.arguments);
2806
+ if (tool.grammar !== undefined) {
2807
+ // A grammar tool is freeform: its own syntax constrained the model, so what arrives is
2808
+ // the text that grammar produced and never JSON. Reading it as JSON would refuse every
2809
+ // call a correct patch or query makes, so it is handed over as written.
2810
+ args = { input: call.arguments };
2680
2811
  }
2681
- catch {
2682
- return failure(`The arguments for "${call.name}" were not valid JSON.`);
2812
+ else {
2813
+ try {
2814
+ args = call.arguments.trim().length === 0 ? {} : JSON.parse(call.arguments);
2815
+ }
2816
+ catch {
2817
+ return failure(`The arguments for "${call.name}" were not valid JSON.`);
2818
+ }
2683
2819
  }
2684
2820
  if (tool.parameters !== undefined && !Value.Check(tool.parameters, args)) {
2685
2821
  return failure(`The arguments for "${call.name}" did not match its schema.`);
@@ -2888,6 +3024,22 @@ export class AgentBase {
2888
3024
  // a clock that went backwards: continue the sequence rather than starting it again.
2889
3025
  return key(lastSlot, Number(lastSequence) + 1);
2890
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
+ }
2891
3043
  /**
2892
3044
  * Consume one response stream into the assistant message it spells out, appending each block
2893
3045
  * to the store as it finishes and reporting every event to the hooks. What comes back is what