@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 +18 -7
- package/dist/AgentBase.d.ts.map +1 -1
- package/dist/AgentBase.js +153 -9
- package/dist/AgentBase.js.map +1 -1
- package/dist/AgentConfig.d.ts +36 -4
- package/dist/AgentConfig.d.ts.map +1 -1
- package/dist/AgentConfig.js +36 -5
- package/dist/AgentConfig.js.map +1 -1
- package/dist/AgentPersistenceDrizzle.d.ts.map +1 -1
- package/dist/AgentPersistenceDrizzle.js +12 -4
- package/dist/AgentPersistenceDrizzle.js.map +1 -1
- package/dist/AgentSystemLocal.d.ts.map +1 -1
- package/dist/AgentSystemLocal.js +48 -7
- package/dist/AgentSystemLocal.js.map +1 -1
- package/dist/inTx.d.ts +13 -0
- package/dist/inTx.d.ts.map +1 -1
- package/dist/inTx.js +18 -0
- package/dist/inTx.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
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,
|
package/dist/AgentBase.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|