@slopus/happy-agent-base 0.0.16 → 0.0.18
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 +27 -4
- package/dist/Agent.d.ts +6 -4
- package/dist/Agent.d.ts.map +1 -1
- package/dist/Agent.js +44 -41
- package/dist/Agent.js.map +1 -1
- package/dist/AgentBase.d.ts +13 -7
- package/dist/AgentBase.d.ts.map +1 -1
- package/dist/AgentBase.js +291 -149
- package/dist/AgentBase.js.map +1 -1
- package/dist/AgentBaseHooks.d.ts +21 -2
- package/dist/AgentBaseHooks.d.ts.map +1 -1
- package/dist/AgentDatabase.d.ts.map +1 -1
- package/dist/AgentDatabase.js +27 -14
- package/dist/AgentDatabase.js.map +1 -1
- package/dist/AgentDatabaseConnection.d.ts +39 -0
- package/dist/AgentDatabaseConnection.d.ts.map +1 -0
- package/dist/AgentDatabaseConnection.js +199 -0
- package/dist/AgentDatabaseConnection.js.map +1 -0
- package/dist/AgentModule.d.ts +40 -12
- package/dist/AgentModule.d.ts.map +1 -1
- package/dist/AgentPersistence.d.ts +4 -4
- package/dist/AgentRef.d.ts +1 -1
- package/dist/AgentRef.js +1 -1
- package/dist/AgentSpanAttributes.d.ts +11 -0
- package/dist/AgentSpanAttributes.d.ts.map +1 -0
- package/dist/AgentSpanAttributes.js +11 -0
- package/dist/AgentSpanAttributes.js.map +1 -0
- package/dist/AgentStorage.d.ts.map +1 -1
- package/dist/AgentStorage.js +2 -0
- package/dist/AgentStorage.js.map +1 -1
- package/dist/AgentSystemLocal.d.ts.map +1 -1
- package/dist/AgentSystemLocal.js +76 -43
- package/dist/AgentSystemLocal.js.map +1 -1
- package/dist/inTx.d.ts +0 -7
- package/dist/inTx.d.ts.map +1 -1
- package/dist/inTx.js +9 -16
- package/dist/inTx.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/openAgentPGliteDatabase.d.ts +10 -0
- package/dist/openAgentPGliteDatabase.d.ts.map +1 -0
- package/dist/openAgentPGliteDatabase.js +17 -0
- package/dist/openAgentPGliteDatabase.js.map +1 -0
- package/dist/openAgentPostgresDatabase.d.ts +13 -0
- package/dist/openAgentPostgresDatabase.d.ts.map +1 -0
- package/dist/openAgentPostgresDatabase.js +10 -0
- package/dist/openAgentPostgresDatabase.js.map +1 -0
- package/dist/openAgentSQLiteDatabase.d.ts +6 -0
- package/dist/openAgentSQLiteDatabase.d.ts.map +1 -0
- package/dist/openAgentSQLiteDatabase.js +48 -0
- package/dist/openAgentSQLiteDatabase.js.map +1 -0
- package/package.json +6 -4
package/dist/AgentBase.js
CHANGED
|
@@ -3,15 +3,17 @@ import { createId } from "@paralleldrive/cuid2";
|
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { Value } from "@sinclair/typebox/value";
|
|
6
|
-
import { afterCommit,
|
|
6
|
+
import { afterCommit, createContextNamespace, detach, deterministicStringify, withLifetime, } from "@steve.kite/stdlib";
|
|
7
7
|
import { agentDatabase, agentKV, agentStorageTransaction, withAgentContext, withAgentDatabase, withAgentHistoryKV, withAgentKV, withAgentPermissionMode, withAgentRunKV, } from "./AgentContexts.js";
|
|
8
8
|
import { agentConfig, ownAgentConfig, withAgentConfig } from "./AgentConfig.js";
|
|
9
|
+
import { outsideAgentDatabaseOperation } from "./AgentDatabaseConnection.js";
|
|
9
10
|
import { taskContextBeforeToolCall, withAgentTaskContext } from "./AgentTaskContext.js";
|
|
10
11
|
import { AgentKV } from "./AgentKV.js";
|
|
11
12
|
import { AGENT_BASE_PENDING_KEY, agentBasePendingStateOf, } from "./AgentBasePending.js";
|
|
12
13
|
import { cuid2Schema, ownAgentMessageMetadata, ownAgentMetadata, } from "./AgentMetadata.js";
|
|
13
14
|
import { DEFAULT_AGENT_PERMISSION_MODE, isAgentPermissionMode, } from "./AgentPermissionMode.js";
|
|
14
15
|
import { AgentProviders } from "./AgentProviders.js";
|
|
16
|
+
import { setAgentSpanAttributes } from "./AgentSpanAttributes.js";
|
|
15
17
|
/** Race winner when an abort interrupts a wait on the stream or a running tool. */
|
|
16
18
|
const ABORTED = Symbol("aborted");
|
|
17
19
|
/**
|
|
@@ -34,8 +36,6 @@ const insideTurn = createContextNamespace("agentInsideTurn", [], {
|
|
|
34
36
|
* its own and outlives whatever happened to start it.
|
|
35
37
|
*/
|
|
36
38
|
const insideLoops = new AsyncLocalStorage();
|
|
37
|
-
/** Persistence locks held by the current asynchronous call chain, independent of Context. */
|
|
38
|
-
const insidePersistenceLocks = new AsyncLocalStorage();
|
|
39
39
|
/**
|
|
40
40
|
* How long a close asked for from inside the agent's own run loop waits for the shutdown before
|
|
41
41
|
* telling its caller it cannot be waited for. Long enough that a caller which has already let go
|
|
@@ -76,10 +76,10 @@ const storedToolResultSchema = Type.Object({
|
|
|
76
76
|
*
|
|
77
77
|
* ## Serialization
|
|
78
78
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* Database transactions and ordered keys keep persistence changes aligned with their in-memory
|
|
80
|
+
* publication. Anything that decides from durable state resolves that state inside the committing
|
|
81
|
+
* step rather than capturing it beforehand; a reference taken before a wait can belong to a
|
|
82
|
+
* history that has since been replaced.
|
|
83
83
|
*
|
|
84
84
|
* ## Accepting a message
|
|
85
85
|
*
|
|
@@ -132,8 +132,8 @@ const storedToolResultSchema = Type.Object({
|
|
|
132
132
|
*
|
|
133
133
|
* An incompatible provider or model change resets the conversation; a compatible one keeps it.
|
|
134
134
|
* Either way the change lands on one side or the other, never the old history under the new
|
|
135
|
-
* model. `modelChanged` runs inside the
|
|
136
|
-
* capability released when the hook returns, so it cannot be retained to
|
|
135
|
+
* model. `modelChanged` runs inside the transaction and is lent a store bound to it — a
|
|
136
|
+
* capability released when the hook returns, so it cannot be retained to write after commit.
|
|
137
137
|
* A failing handoff rejects an incompatible switch outright rather than costing the history.
|
|
138
138
|
*
|
|
139
139
|
* ## Permission modes
|
|
@@ -256,12 +256,6 @@ export class AgentBase {
|
|
|
256
256
|
#permissionMode;
|
|
257
257
|
/** The single set of hooks the run is observed by and its configuration extended from. */
|
|
258
258
|
#hooks;
|
|
259
|
-
/**
|
|
260
|
-
* Serializes every persistence operation together with its in-memory effect, so storage
|
|
261
|
-
* order always matches history order and a load never overlaps an append. The lock belongs
|
|
262
|
-
* to the store, so an owner inspecting the same store sees only whole steps.
|
|
263
|
-
*/
|
|
264
|
-
#persistenceLock;
|
|
265
259
|
/** The session-scoped key-value store carried on every context the agent derives. */
|
|
266
260
|
#kv;
|
|
267
261
|
/** Durable state tied to the current history and invalidated when that history is replaced. */
|
|
@@ -348,6 +342,14 @@ export class AgentBase {
|
|
|
348
342
|
#inherited;
|
|
349
343
|
/** Whether that inherited record has been read; it can only be read before it is overwritten. */
|
|
350
344
|
#inheritedRead = false;
|
|
345
|
+
/**
|
|
346
|
+
* Whether the activation hook is still owed an announcement for inherited work: the store
|
|
347
|
+
* already said the agent was working when this process first looked, and no run of this
|
|
348
|
+
* process has announced taking that work up yet.
|
|
349
|
+
*/
|
|
350
|
+
#restoreActivationOwed = false;
|
|
351
|
+
/** Whether this process has announced the activation of the current active period. */
|
|
352
|
+
#activationAnnounced = false;
|
|
351
353
|
/**
|
|
352
354
|
* The compaction that has been asked for and not carried out yet, together with the promise
|
|
353
355
|
* every caller waiting for it shares. Requesting one while it is pending joins that promise
|
|
@@ -382,6 +384,8 @@ export class AgentBase {
|
|
|
382
384
|
#closing;
|
|
383
385
|
/** Operations accepted from a caller and not finished yet; a close waits for every one. */
|
|
384
386
|
#admitted = new Set();
|
|
387
|
+
/** Queue acceptances the run loop must publish before it may decide its queues are empty. */
|
|
388
|
+
#messageAdmissions = new Set();
|
|
385
389
|
/**
|
|
386
390
|
* Work from an earlier response that is still unwinding: a provider stream that has not
|
|
387
391
|
* finished closing, or a tool that was settled in the conversation by an abort and is still
|
|
@@ -415,10 +419,16 @@ export class AgentBase {
|
|
|
415
419
|
* they are needed by the first turn and by nothing before it, so an owner resuming a hundred
|
|
416
420
|
* identities at startup pays for a hundred small reads rather than a hundred transcripts.
|
|
417
421
|
* The rest loads on the way into the turn that actually needs it.
|
|
422
|
+
*
|
|
423
|
+
* The read normally runs on the agent's own context. A caller resolving the agent from
|
|
424
|
+
* inside an open storage transaction passes `loadCtx` to route this one read through that
|
|
425
|
+
* transaction's connection instead, because a single-connection driver cannot serve a read
|
|
426
|
+
* beside the transaction it is holding open. The agent's own lifetime context is detached
|
|
427
|
+
* either way and never carries the caller's transaction.
|
|
418
428
|
*/
|
|
419
|
-
static async load(ctx, options) {
|
|
429
|
+
static async load(ctx, options, loadCtx) {
|
|
420
430
|
const agent = new AgentBase(ctx, options);
|
|
421
|
-
await agent.#loadPendingState();
|
|
431
|
+
await agent.#loadPendingState(loadCtx ?? agent.#ctx);
|
|
422
432
|
return agent;
|
|
423
433
|
}
|
|
424
434
|
/**
|
|
@@ -440,14 +450,18 @@ export class AgentBase {
|
|
|
440
450
|
/**
|
|
441
451
|
* Read the outstanding work the store already holds, before this instance has written any of
|
|
442
452
|
* its own. It is both what `active` answers from and what an interrupted run is recognized
|
|
443
|
-
* by, so reading it here leaves the later stage writes nothing to learn from the store.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
* by, so reading it here leaves the later stage writes nothing to learn from the store. The
|
|
454
|
+
* context is the caller's choice: the agent's own for an ordinary load, or the resolving
|
|
455
|
+
* transaction's when the read has to ride that transaction's connection.
|
|
456
|
+
*/
|
|
457
|
+
async #loadPendingState(ctx) {
|
|
458
|
+
await this.#runPersistenceStep(ctx, async (operationCtx) => {
|
|
459
|
+
await this.#loadConfig(operationCtx);
|
|
460
|
+
const stored = await agentBasePendingStateOf(operationCtx, this.#persistence);
|
|
449
461
|
this.#inherited = stored;
|
|
450
462
|
this.#inheritedRead = true;
|
|
463
|
+
this.#restoreActivationOwed =
|
|
464
|
+
stored !== undefined && this.#hooks.afterAgentActivatedTransact !== undefined;
|
|
451
465
|
this.#pending = stored;
|
|
452
466
|
this.#loopId = stored?.loopId;
|
|
453
467
|
this.#turnId = stored?.turnId;
|
|
@@ -477,7 +491,6 @@ export class AgentBase {
|
|
|
477
491
|
this.#providers = options.providers;
|
|
478
492
|
this.#providerId = options.provider;
|
|
479
493
|
this.#persistence = options.persistence;
|
|
480
|
-
this.#persistenceLock = asyncLock({ reentry: "block" });
|
|
481
494
|
this.#hooks = options.hooks ?? {};
|
|
482
495
|
this.state = {
|
|
483
496
|
instructions: options.initialState?.instructions ?? "",
|
|
@@ -553,6 +566,27 @@ export class AgentBase {
|
|
|
553
566
|
permissionMode: this.#permissionMode,
|
|
554
567
|
};
|
|
555
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* Open agent work with the selection that governs it, so every operation in a trace can be
|
|
571
|
+
* attributed without recording prompts, arguments, or provider response content.
|
|
572
|
+
*/
|
|
573
|
+
#span(ctx, name, attributes, work) {
|
|
574
|
+
const selection = this.#selection();
|
|
575
|
+
return Promise.resolve(ctx.span(name, (spanCtx) => {
|
|
576
|
+
setAgentSpanAttributes(spanCtx, {
|
|
577
|
+
"agent.id": selection.id,
|
|
578
|
+
"agent.provider": selection.provider,
|
|
579
|
+
"agent.permission_mode": selection.permissionMode,
|
|
580
|
+
...(selection.model === undefined ? {} : { "agent.model": selection.model }),
|
|
581
|
+
...(selection.effort === undefined ? {} : { "agent.effort": selection.effort }),
|
|
582
|
+
...(selection.serviceTier === undefined
|
|
583
|
+
? {}
|
|
584
|
+
: { "agent.service_tier": selection.serviceTier }),
|
|
585
|
+
...attributes,
|
|
586
|
+
});
|
|
587
|
+
return work(spanCtx);
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
556
590
|
/**
|
|
557
591
|
* Whether the agent has anything left to do. This is the only thing about an agent's state
|
|
558
592
|
* anyone outside it may read: the queues and the stage behind this answer are the run's own
|
|
@@ -589,7 +623,7 @@ export class AgentBase {
|
|
|
589
623
|
};
|
|
590
624
|
}
|
|
591
625
|
/**
|
|
592
|
-
* Record the stage the run has reached
|
|
626
|
+
* Record the stage the run has reached through the store's transaction boundary.
|
|
593
627
|
*
|
|
594
628
|
* The first of these also reads what the store already held, before overwriting it. That
|
|
595
629
|
* reading is the only chance to see it: from this point the record says what this instance
|
|
@@ -599,27 +633,44 @@ export class AgentBase {
|
|
|
599
633
|
async #enterStage(ctx, stage, transact) {
|
|
600
634
|
const pending = this.#pendingState(stage);
|
|
601
635
|
if (transact === undefined &&
|
|
636
|
+
!this.#restoreActivationOwed &&
|
|
602
637
|
deterministicStringify(pending) === this.#pendingWritten &&
|
|
603
638
|
this.#inheritedRead) {
|
|
604
639
|
return undefined;
|
|
605
640
|
}
|
|
606
641
|
try {
|
|
607
|
-
return await this.#
|
|
642
|
+
return await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
608
643
|
if (!this.#inheritedRead) {
|
|
609
644
|
this.#inheritedRead = true;
|
|
610
645
|
this.#inherited = await agentBasePendingStateOf(lockCtx, this.#persistence);
|
|
646
|
+
this.#restoreActivationOwed =
|
|
647
|
+
this.#inherited !== undefined &&
|
|
648
|
+
!this.#activationAnnounced &&
|
|
649
|
+
this.#hooks.afterAgentActivatedTransact !== undefined;
|
|
611
650
|
}
|
|
612
|
-
|
|
651
|
+
const announceRestore = this.#restoreActivationOwed;
|
|
652
|
+
if (transact === undefined && !announceRestore) {
|
|
613
653
|
await this.#recordPending(lockCtx, stage);
|
|
614
654
|
return undefined;
|
|
615
655
|
}
|
|
616
|
-
|
|
656
|
+
const result = await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
617
657
|
// The state is staged first. The callback then writes against this exact
|
|
618
658
|
// transaction, so neither its conclusion nor the state it observed can land
|
|
619
659
|
// without the other.
|
|
620
660
|
await this.#recordPending(txCtx, stage, true);
|
|
621
|
-
|
|
661
|
+
// Taking up inherited work is what reactivates the agent after a restart,
|
|
662
|
+
// so the announcement commits with the resumed run's own stage record.
|
|
663
|
+
if (announceRestore)
|
|
664
|
+
await this.#announceActivation(txCtx, true);
|
|
665
|
+
return transact === undefined
|
|
666
|
+
? undefined
|
|
667
|
+
: await this.#withTransactionalContext(txCtx, transact);
|
|
622
668
|
});
|
|
669
|
+
if (announceRestore) {
|
|
670
|
+
this.#restoreActivationOwed = false;
|
|
671
|
+
this.#activationAnnounced = true;
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
623
674
|
});
|
|
624
675
|
}
|
|
625
676
|
catch (error) {
|
|
@@ -640,6 +691,41 @@ export class AgentBase {
|
|
|
640
691
|
this.#pending = undefined;
|
|
641
692
|
this.#pendingWritten = undefined;
|
|
642
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Tell the activation hook the agent stopped being settled, inside the very transaction that
|
|
696
|
+
* records the work it now owes. A failure propagates and rolls that transaction back, so a
|
|
697
|
+
* module never concludes the agent woke up from a wake-up that never became durable.
|
|
698
|
+
*/
|
|
699
|
+
async #announceActivation(txCtx, restored) {
|
|
700
|
+
const hook = this.#hooks.afterAgentActivatedTransact;
|
|
701
|
+
if (hook === undefined)
|
|
702
|
+
return;
|
|
703
|
+
await this.#withTransactionalContext(txCtx, (hookCtx) => hook(this.#workContext(hookCtx), { restored }));
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Record that scheduled work made an inference owed, and decide whether this transaction is
|
|
707
|
+
* the one that woke a settled agent. The decision is the store's own absent-only insert of
|
|
708
|
+
* the pending record, so transactions racing to wake the same agent are arbitrated by the
|
|
709
|
+
* database rather than by a read another writer could make stale: exactly one creates the
|
|
710
|
+
* record and activates, and the others find it already present. A record that is present
|
|
711
|
+
* but unreadable counts as no pending state, as it does everywhere else.
|
|
712
|
+
*/
|
|
713
|
+
async #claimPendingWork(txCtx) {
|
|
714
|
+
const pending = this.#pendingState("inference");
|
|
715
|
+
const serialized = deterministicStringify(pending);
|
|
716
|
+
// This instance already recorded the current run, so the work is already owed and
|
|
717
|
+
// nothing here can be an activation.
|
|
718
|
+
if (this.#pendingWritten === serialized)
|
|
719
|
+
return false;
|
|
720
|
+
let created = await this.#persistence.writeValueIfAbsent(txCtx, AGENT_BASE_PENDING_KEY, pending);
|
|
721
|
+
if (!created) {
|
|
722
|
+
created = (await agentBasePendingStateOf(txCtx, this.#persistence)) === undefined;
|
|
723
|
+
await this.#persistence.writeValue(txCtx, AGENT_BASE_PENDING_KEY, pending);
|
|
724
|
+
}
|
|
725
|
+
this.#pending = pending;
|
|
726
|
+
this.#pendingWritten = serialized;
|
|
727
|
+
return created;
|
|
728
|
+
}
|
|
643
729
|
/**
|
|
644
730
|
* Queue a user message that injects as soon as the current assistant response and its tool
|
|
645
731
|
* batch finish; steering always takes precedence over sent messages. Returns once the message
|
|
@@ -671,34 +757,37 @@ export class AgentBase {
|
|
|
671
757
|
throw new Error("The agent metadata is not valid.");
|
|
672
758
|
if (this.#closed)
|
|
673
759
|
throw new Error("The agent has been closed.");
|
|
674
|
-
if (insideTurn.get(ctx).includes(this.id) ||
|
|
675
|
-
this.#insideOwnLoop() ||
|
|
676
|
-
this.#insideOwnPersistenceLock()) {
|
|
760
|
+
if (insideTurn.get(ctx).includes(this.id) || this.#insideOwnLoop()) {
|
|
677
761
|
throw new Error("Updating metadata from inside this agent's current operation would wait for " +
|
|
678
762
|
"that same operation to finish. Update it after the hook or tool returns.");
|
|
679
763
|
}
|
|
680
764
|
let change;
|
|
681
765
|
let next;
|
|
682
|
-
await this.#
|
|
683
|
-
const previousMetadata = ownAgentMetadata(this.#config.metadata ?? {});
|
|
684
|
-
const metadata = ownAgentMetadata({ ...previousMetadata, ...ownedUpdate });
|
|
685
|
-
if (previousMetadata === undefined || metadata === undefined) {
|
|
686
|
-
throw new Error("The agent metadata is not valid.");
|
|
687
|
-
}
|
|
688
|
-
next = ownAgentConfig({ ...this.#config, metadata });
|
|
689
|
-
change = {
|
|
690
|
-
agentId: this.id,
|
|
691
|
-
previousMetadata,
|
|
692
|
-
update: ownedUpdate,
|
|
693
|
-
metadata,
|
|
694
|
-
};
|
|
766
|
+
await this.#runPersistenceStep(ctx, async (lockCtx) => {
|
|
695
767
|
await this.#persistence.transaction(lockCtx, async (txCtx) => {
|
|
768
|
+
const stored = await this.#persistence.readValues(txCtx, "agentConfig");
|
|
769
|
+
const exact = stored.find(({ key }) => key === "agentConfig")?.value;
|
|
770
|
+
const current = exact === undefined ? this.#config : ownAgentConfig(exact);
|
|
771
|
+
const previousMetadata = ownAgentMetadata(current.metadata ?? {});
|
|
772
|
+
const metadata = ownAgentMetadata({ ...previousMetadata, ...ownedUpdate });
|
|
773
|
+
if (previousMetadata === undefined || metadata === undefined) {
|
|
774
|
+
throw new Error("The agent metadata is not valid.");
|
|
775
|
+
}
|
|
776
|
+
next = ownAgentConfig({ ...current, metadata });
|
|
777
|
+
change = {
|
|
778
|
+
agentId: this.id,
|
|
779
|
+
previousMetadata,
|
|
780
|
+
update: ownedUpdate,
|
|
781
|
+
metadata,
|
|
782
|
+
};
|
|
696
783
|
await this.#persistence.writeValue(txCtx, "agentConfig", next);
|
|
697
784
|
await this.#withTransactionalContext(withAgentConfig(txCtx, next), async (hookCtx) => await this.#hooks.metadataChangedTransact?.(this.#hookContext(hookCtx), change));
|
|
785
|
+
afterCommit(txCtx, () => {
|
|
786
|
+
this.#config = next;
|
|
787
|
+
this.#baseCtx = withAgentConfig(this.#baseCtx, next);
|
|
788
|
+
this.#ctx = this.#deriveCtx();
|
|
789
|
+
});
|
|
698
790
|
});
|
|
699
|
-
this.#config = next;
|
|
700
|
-
this.#baseCtx = withAgentConfig(this.#baseCtx, next);
|
|
701
|
-
this.#ctx = this.#deriveCtx();
|
|
702
791
|
});
|
|
703
792
|
await this.#invokeHookOn(this.#hookContext(withAgentConfig(ctx, next)), this.#hooks.metadataChanged, change);
|
|
704
793
|
}
|
|
@@ -712,9 +801,6 @@ export class AgentBase {
|
|
|
712
801
|
if (outerTransaction?.lifetime.aborted === true) {
|
|
713
802
|
throw new Error("The agent storage transaction carried by this context has ended.");
|
|
714
803
|
}
|
|
715
|
-
if (outerTransaction !== undefined && this.#insideOwnPersistenceLock()) {
|
|
716
|
-
throw new Error("Agent message delivery cannot reenter its own persistence operation inside a transaction.");
|
|
717
|
-
}
|
|
718
804
|
const { await: requestedWait, id = createId(), metadata: suppliedMetadata, ...settings } = options ?? {};
|
|
719
805
|
if (!Value.Check(cuid2Schema, id)) {
|
|
720
806
|
throw new Error("The message ID must be a cuid2 identity.");
|
|
@@ -796,30 +882,20 @@ export class AgentBase {
|
|
|
796
882
|
#insideOwnLoop() {
|
|
797
883
|
return insideLoops.getStore()?.includes(this.id) === true;
|
|
798
884
|
}
|
|
799
|
-
/** Whether this call chain already holds this agent's persistence lock. */
|
|
800
|
-
#insideOwnPersistenceLock() {
|
|
801
|
-
return insidePersistenceLocks.getStore()?.includes(this.id) === true;
|
|
802
|
-
}
|
|
803
885
|
/** Live agent commands cannot be staged or undone by an enclosing database transaction. */
|
|
804
886
|
#assertOutsideStorageTransaction(ctx, operation) {
|
|
805
887
|
if (agentStorageTransaction(ctx) !== undefined) {
|
|
806
888
|
throw new Error(`Agent ${operation} cannot run from inside an outer storage transaction.`);
|
|
807
889
|
}
|
|
808
890
|
}
|
|
809
|
-
/**
|
|
810
|
-
async #
|
|
811
|
-
return await
|
|
812
|
-
const held = insidePersistenceLocks.getStore() ?? [];
|
|
813
|
-
return await insidePersistenceLocks.run([...held, this.id], async () => {
|
|
814
|
-
return await work(lockCtx);
|
|
815
|
-
});
|
|
816
|
-
});
|
|
891
|
+
/** Run one persistence step; its database statements and transactions own consistency. */
|
|
892
|
+
async #runPersistenceStep(ctx, work) {
|
|
893
|
+
return await work(ctx);
|
|
817
894
|
}
|
|
818
895
|
/**
|
|
819
|
-
* Accept a batch of messages as one durable
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
* failure admits none of them.
|
|
896
|
+
* Accept a batch of messages as one durable transaction, so a caller arriving while it is
|
|
897
|
+
* being written lands after the whole batch rather than in the middle of it, and a failure
|
|
898
|
+
* admits none of them.
|
|
823
899
|
*/
|
|
824
900
|
async #enqueue(ctx, batch) {
|
|
825
901
|
if (batch.length === 0)
|
|
@@ -832,19 +908,22 @@ export class AgentBase {
|
|
|
832
908
|
? this.#enqueueIndependently(ctx, batch)
|
|
833
909
|
: this.#enqueueInTransaction(ctx, batch);
|
|
834
910
|
this.#admitted.add(admitted);
|
|
911
|
+
this.#messageAdmissions.add(admitted);
|
|
835
912
|
try {
|
|
836
913
|
return await admitted;
|
|
837
914
|
}
|
|
838
915
|
finally {
|
|
839
916
|
this.#admitted.delete(admitted);
|
|
917
|
+
this.#messageAdmissions.delete(admitted);
|
|
840
918
|
}
|
|
841
919
|
}
|
|
842
|
-
/** Accept a batch
|
|
920
|
+
/** Accept a batch in a transaction of the acceptance's own. */
|
|
843
921
|
async #enqueueIndependently(ctx, batch) {
|
|
844
|
-
return await this.#
|
|
922
|
+
return await this.#runPersistenceStep(ctx, async (lockCtx) => {
|
|
845
923
|
const accepted = [];
|
|
846
924
|
const results = [];
|
|
847
925
|
await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
926
|
+
let activated = false;
|
|
848
927
|
for (const request of batch) {
|
|
849
928
|
const identityKey = `message.${request.id}`;
|
|
850
929
|
if (!(await this.#persistence.writeValueIfAbsent(txCtx, identityKey, true))) {
|
|
@@ -874,36 +953,41 @@ export class AgentBase {
|
|
|
874
953
|
// Accepting a message is what makes the work owed: the same transaction that
|
|
875
954
|
// admits it records that the agent owes an answer, so a process that dies right
|
|
876
955
|
// here is discovered still owing it rather than looking idle over a full queue.
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
956
|
+
// Creating that record where none existed is what makes a settled agent active,
|
|
957
|
+
// and the activation hook commits with the very transaction that admits the
|
|
958
|
+
// message.
|
|
959
|
+
if (await this.#claimPendingWork(txCtx)) {
|
|
960
|
+
await this.#announceActivation(txCtx, false);
|
|
961
|
+
activated = true;
|
|
962
|
+
}
|
|
963
|
+
afterCommit(txCtx, () => {
|
|
964
|
+
if (activated) {
|
|
965
|
+
this.#activationAnnounced = true;
|
|
966
|
+
this.#restoreActivationOwed = false;
|
|
967
|
+
}
|
|
968
|
+
for (const { key, request } of accepted) {
|
|
969
|
+
const queue = request.kind === "steering" ? this.#steering : this.#sends;
|
|
970
|
+
queue.push({
|
|
971
|
+
key,
|
|
972
|
+
id: request.id,
|
|
973
|
+
message: request.message,
|
|
974
|
+
...(request.metadata === undefined
|
|
975
|
+
? {}
|
|
976
|
+
: { metadata: request.metadata }),
|
|
977
|
+
options: request.options,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
this.#turnRequested = true;
|
|
981
|
+
this.#startRun();
|
|
890
982
|
});
|
|
891
|
-
}
|
|
892
|
-
if (accepted.length > 0) {
|
|
893
|
-
this.#turnRequested = true;
|
|
894
|
-
this.#startRun();
|
|
895
|
-
}
|
|
983
|
+
});
|
|
896
984
|
return results;
|
|
897
985
|
});
|
|
898
986
|
}
|
|
899
987
|
/**
|
|
900
|
-
* Accept a batch inside the caller's own open transaction.
|
|
901
|
-
*
|
|
902
|
-
*
|
|
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.
|
|
988
|
+
* Accept a batch inside the caller's own open transaction. The outer transaction supplies
|
|
989
|
+
* atomicity, queue keys are claimed with absent-only writes so a racing independent enqueue
|
|
990
|
+
* cannot be overwritten, and no heap state changes until the commit publishes the batch.
|
|
907
991
|
*/
|
|
908
992
|
async #enqueueInTransaction(ctx, batch) {
|
|
909
993
|
const results = [];
|
|
@@ -934,43 +1018,58 @@ export class AgentBase {
|
|
|
934
1018
|
// Accepting a message is what makes the work owed, so the same transaction that admits
|
|
935
1019
|
// it records that the agent owes an answer; here that record is staged durably and made
|
|
936
1020
|
// live only once the outermost commit publishes the whole batch.
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
1021
|
+
const staged = acceptedAny ? await this.#stagePendingMessageWork(ctx) : undefined;
|
|
1022
|
+
// Scheduling onto a settled agent is what makes it active; the announcement writes into
|
|
1023
|
+
// the caller's own transaction and is published or rolled back with the batch itself.
|
|
1024
|
+
if (staged?.activated === true)
|
|
1025
|
+
await this.#announceActivation(ctx, false);
|
|
940
1026
|
const offeredIds = batch.map(({ id }) => id);
|
|
941
1027
|
afterCommit(ctx, async () => {
|
|
942
|
-
await this.#activateCommittedMessages(offeredIds,
|
|
1028
|
+
await this.#activateCommittedMessages(offeredIds, staged);
|
|
943
1029
|
});
|
|
944
1030
|
return results;
|
|
945
1031
|
}
|
|
946
1032
|
/**
|
|
947
1033
|
* Persist the pending inference an outer transaction will make live after commit, without
|
|
948
|
-
* changing any heap state that would survive a rollback.
|
|
949
|
-
*
|
|
1034
|
+
* changing any heap state that would survive a rollback. Whether the agent was settled is
|
|
1035
|
+
* decided by the store's own absent-only insert of the pending record, so concurrent
|
|
1036
|
+
* transactions waking the same agent are arbitrated by the database itself: exactly one
|
|
1037
|
+
* creates the record and activates. Reading the transaction's current value on the taken
|
|
1038
|
+
* path also makes multiple sends in one outer transaction reuse the same lifecycle IDs.
|
|
950
1039
|
*/
|
|
951
1040
|
async #stagePendingMessageWork(ctx) {
|
|
1041
|
+
const fresh = { stage: "inference", loopId: createId() };
|
|
1042
|
+
if (await this.#persistence.writeValueIfAbsent(ctx, AGENT_BASE_PENDING_KEY, fresh)) {
|
|
1043
|
+
return { pending: fresh, activated: true };
|
|
1044
|
+
}
|
|
952
1045
|
const stored = await agentBasePendingStateOf(ctx, this.#persistence);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1046
|
+
// A record that is present but unreadable counts as no pending state, as everywhere
|
|
1047
|
+
// else, so this transaction still claims the activation when it replaces one.
|
|
1048
|
+
const pending = stored === undefined ? fresh : { ...stored, stage: "inference" };
|
|
956
1049
|
await this.#persistence.writeValue(ctx, AGENT_BASE_PENDING_KEY, pending);
|
|
957
|
-
return pending;
|
|
1050
|
+
return { pending, activated: stored === undefined };
|
|
958
1051
|
}
|
|
959
1052
|
/**
|
|
960
1053
|
* Publish a transactionally accepted message only after the outermost commit. The next turn
|
|
961
1054
|
* reloads the durable queue instead of copying staged entries into memory; the dirty marker
|
|
962
1055
|
* prevents a turn already in flight from clearing the request before that reload happens.
|
|
963
1056
|
*/
|
|
964
|
-
async #activateCommittedMessages(offeredIds,
|
|
1057
|
+
async #activateCommittedMessages(offeredIds, staged) {
|
|
965
1058
|
for (const id of offeredIds)
|
|
966
1059
|
this.#offeredMessageIds.add(id);
|
|
967
|
-
if (
|
|
1060
|
+
if (staged === undefined)
|
|
968
1061
|
return;
|
|
969
|
-
|
|
970
|
-
|
|
1062
|
+
// The commit published the staged activation announcement along with the batch, so the
|
|
1063
|
+
// current active period is now an announced one.
|
|
1064
|
+
if (staged.activated) {
|
|
1065
|
+
this.#activationAnnounced = true;
|
|
1066
|
+
this.#restoreActivationOwed = false;
|
|
1067
|
+
}
|
|
1068
|
+
await this.#runPersistenceStep(this.#ctx, async (lockCtx) => {
|
|
1069
|
+
let pending = staged.pending;
|
|
971
1070
|
try {
|
|
972
1071
|
pending =
|
|
973
|
-
(await agentBasePendingStateOf(lockCtx, this.#persistence)) ??
|
|
1072
|
+
(await agentBasePendingStateOf(lockCtx, this.#persistence)) ?? staged.pending;
|
|
974
1073
|
}
|
|
975
1074
|
catch {
|
|
976
1075
|
// The committed staged value is enough to start; a turn reload retries storage.
|
|
@@ -1002,22 +1101,31 @@ export class AgentBase {
|
|
|
1002
1101
|
return;
|
|
1003
1102
|
if (this.#closed)
|
|
1004
1103
|
throw new Error("The agent has been closed.");
|
|
1005
|
-
await this.#
|
|
1104
|
+
await this.#runPersistenceStep(ctx, async (lockCtx) => {
|
|
1006
1105
|
const accepted = [];
|
|
1007
1106
|
await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
1107
|
+
let activated = false;
|
|
1008
1108
|
for (const message of batch) {
|
|
1009
1109
|
const key = await this.#queueKey(txCtx, "inject.");
|
|
1010
1110
|
await this.#persistence.writeValue(txCtx, key, message);
|
|
1011
1111
|
accepted.push({ key, message });
|
|
1012
1112
|
}
|
|
1013
|
-
if (accepted.length
|
|
1014
|
-
|
|
1113
|
+
if (accepted.length === 0)
|
|
1114
|
+
return;
|
|
1115
|
+
if (await this.#claimPendingWork(txCtx)) {
|
|
1116
|
+
await this.#announceActivation(txCtx, false);
|
|
1117
|
+
activated = true;
|
|
1118
|
+
}
|
|
1119
|
+
afterCommit(txCtx, () => {
|
|
1120
|
+
if (activated) {
|
|
1121
|
+
this.#activationAnnounced = true;
|
|
1122
|
+
this.#restoreActivationOwed = false;
|
|
1123
|
+
}
|
|
1124
|
+
this.#injections.push(...accepted);
|
|
1125
|
+
this.#turnRequested = true;
|
|
1126
|
+
this.#startRun();
|
|
1127
|
+
});
|
|
1015
1128
|
});
|
|
1016
|
-
this.#injections.push(...accepted);
|
|
1017
|
-
if (accepted.length > 0) {
|
|
1018
|
-
this.#turnRequested = true;
|
|
1019
|
-
this.#startRun();
|
|
1020
|
-
}
|
|
1021
1129
|
});
|
|
1022
1130
|
}
|
|
1023
1131
|
/**
|
|
@@ -1303,7 +1411,7 @@ export class AgentBase {
|
|
|
1303
1411
|
return;
|
|
1304
1412
|
// The loop is a lifetime of its own, so it marks itself rather than inheriting whatever
|
|
1305
1413
|
// happened to start it — a tool of another agent, most often, which will be long gone.
|
|
1306
|
-
this.#runPromise = insideLoops
|
|
1414
|
+
this.#runPromise = outsideAgentDatabaseOperation(() => insideLoops
|
|
1307
1415
|
.run([this.id], () => this.#runLoop())
|
|
1308
1416
|
.finally(() => {
|
|
1309
1417
|
this.#runPromise = undefined;
|
|
@@ -1315,7 +1423,7 @@ export class AgentBase {
|
|
|
1315
1423
|
return;
|
|
1316
1424
|
}
|
|
1317
1425
|
this.#announceSettled();
|
|
1318
|
-
});
|
|
1426
|
+
}));
|
|
1319
1427
|
}
|
|
1320
1428
|
/**
|
|
1321
1429
|
* Answer turns until nothing is asked for any more. The inner loop is one turn each: reload
|
|
@@ -1328,13 +1436,14 @@ export class AgentBase {
|
|
|
1328
1436
|
// whole of it runs on. Everything below is handed that context rather than reading a
|
|
1329
1437
|
// scope back off the agent, so each turn, inference, tool call, and hook is placed in
|
|
1330
1438
|
// the run it actually belongs to even while another agent is running alongside it.
|
|
1331
|
-
await this.#
|
|
1439
|
+
await this.#span(this.#ctx, "agent.run", {}, (ctx) => this.#runTurns(ctx));
|
|
1332
1440
|
}
|
|
1333
1441
|
/** The run itself, on the context of the span the whole of it belongs to. */
|
|
1334
1442
|
async #runTurns(ctx) {
|
|
1335
1443
|
if (this.#pending?.stage === "settlement") {
|
|
1336
1444
|
this.#loopId ??= createId();
|
|
1337
1445
|
this.#settlementId ??= createId();
|
|
1446
|
+
setAgentSpanAttributes(ctx, { "agent.loop.id": this.#loopId });
|
|
1338
1447
|
await this.#settleDurably(ctx, {
|
|
1339
1448
|
loopId: this.#loopId,
|
|
1340
1449
|
settlementId: this.#settlementId,
|
|
@@ -1346,6 +1455,7 @@ export class AgentBase {
|
|
|
1346
1455
|
do {
|
|
1347
1456
|
this.#loopId ??= createId();
|
|
1348
1457
|
const loop = { loopId: this.#loopId };
|
|
1458
|
+
setAgentSpanAttributes(ctx, { "agent.loop.id": loop.loopId });
|
|
1349
1459
|
// The abort scope opens before the loop hook, not just before the turn. An abort
|
|
1350
1460
|
// owns everything the run does — its opening hook as much as its inference — so a
|
|
1351
1461
|
// run cancelled while it is still starting up never reaches the model at all.
|
|
@@ -1359,7 +1469,7 @@ export class AgentBase {
|
|
|
1359
1469
|
: (hookCtx) => this.#hooks.beforeAgentLoopTransact?.(hookCtx, loop));
|
|
1360
1470
|
await this.#invokeHook(ctx, this.#hooks.beforeAgentLoop, loop);
|
|
1361
1471
|
do {
|
|
1362
|
-
const outcome = await
|
|
1472
|
+
const outcome = await this.#span(ctx, "agent.turn", { "agent.loop.id": loop.loopId }, (turnCtx) => this.#runTurn(turnCtx, loop, abort));
|
|
1363
1473
|
if (outcome === "blocked")
|
|
1364
1474
|
return;
|
|
1365
1475
|
if (outcome === "stop")
|
|
@@ -1394,6 +1504,7 @@ export class AgentBase {
|
|
|
1394
1504
|
this.#turnAborted = false;
|
|
1395
1505
|
this.#durableWorkBlocked = false;
|
|
1396
1506
|
this.#turnId ??= createId();
|
|
1507
|
+
setAgentSpanAttributes(ctx, { "agent.turn.id": this.#turnId });
|
|
1397
1508
|
// Claimed before any awaiting, so a request raised while the turn is still starting up
|
|
1398
1509
|
// survives into another turn instead of being cleared by it. The redundant turn this can
|
|
1399
1510
|
// cost is cheap: an empty queue drains without any inference.
|
|
@@ -1459,12 +1570,15 @@ export class AgentBase {
|
|
|
1459
1570
|
* resumed at all.
|
|
1460
1571
|
*/
|
|
1461
1572
|
async #settleDurably(ctx, settlement) {
|
|
1462
|
-
await
|
|
1573
|
+
await this.#span(ctx, "agent.settle", {
|
|
1574
|
+
"agent.loop.id": settlement.loopId,
|
|
1575
|
+
"agent.settlement.id": settlement.settlementId,
|
|
1576
|
+
}, (settleCtx) => this.#settleRecord(settleCtx, settlement));
|
|
1463
1577
|
}
|
|
1464
1578
|
/** The settlement itself, on the context of the span it belongs to. */
|
|
1465
1579
|
async #settleRecord(ctx, settlement) {
|
|
1466
1580
|
try {
|
|
1467
|
-
await this.#
|
|
1581
|
+
await this.#runPersistenceStep(this.#workContext(ctx), (lockCtx) => this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
1468
1582
|
await this.#clearPending(txCtx);
|
|
1469
1583
|
await this.#invokeTransactionalSettle(txCtx, settlement);
|
|
1470
1584
|
// The run store is erased last, so a settling hook can still read what the
|
|
@@ -1478,6 +1592,10 @@ export class AgentBase {
|
|
|
1478
1592
|
this.#inferenceId = undefined;
|
|
1479
1593
|
this.#settlementId = undefined;
|
|
1480
1594
|
this.#settlement = settlement;
|
|
1595
|
+
// Settled means nothing is owed: the next scheduled message activates the agent
|
|
1596
|
+
// afresh, and nothing inherited remains to announce.
|
|
1597
|
+
this.#activationAnnounced = false;
|
|
1598
|
+
this.#restoreActivationOwed = false;
|
|
1481
1599
|
}
|
|
1482
1600
|
catch {
|
|
1483
1601
|
// The run itself is over and succeeded; only the record of its ending failed.
|
|
@@ -1721,6 +1839,7 @@ export class AgentBase {
|
|
|
1721
1839
|
await this.#emit(ctx, { type: "done", state: "cancelled" });
|
|
1722
1840
|
break;
|
|
1723
1841
|
}
|
|
1842
|
+
await this.#finishAdmittedQueueWrites();
|
|
1724
1843
|
let injected = await this.#consumeQueue(ctx, this.#steering, this.#steeringMode, "steering");
|
|
1725
1844
|
if (!injected && !needsInference) {
|
|
1726
1845
|
injected = await this.#consumeQueue(ctx, this.#sends, this.#sendMode, "send");
|
|
@@ -1731,7 +1850,7 @@ export class AgentBase {
|
|
|
1731
1850
|
// Nothing to answer — a start() on an idle history, or the queues ran dry.
|
|
1732
1851
|
if (!this.#noticeAwaitingResponse && !injected && !needsInference)
|
|
1733
1852
|
break;
|
|
1734
|
-
const response = await
|
|
1853
|
+
const response = await this.#span(ctx, "agent.inference", {}, (inferenceCtx) => this.#requestInference(inferenceCtx, abortPromise));
|
|
1735
1854
|
// A cancellation arrived before the request was made, so the turn cycles rather
|
|
1736
1855
|
// than talking to a session it may no longer own.
|
|
1737
1856
|
if (response === undefined)
|
|
@@ -1793,6 +1912,16 @@ export class AgentBase {
|
|
|
1793
1912
|
}
|
|
1794
1913
|
this.#turnAborted = abort.signal.aborted;
|
|
1795
1914
|
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Let queue writes already admitted by a re-entrant hook publish before deciding the queues
|
|
1917
|
+
* are empty. Their database transactions own ordering; this waits for those concrete writes
|
|
1918
|
+
* without holding another agent or database lock.
|
|
1919
|
+
*/
|
|
1920
|
+
async #finishAdmittedQueueWrites() {
|
|
1921
|
+
while (this.#messageAdmissions.size > 0) {
|
|
1922
|
+
await Promise.allSettled(this.#messageAdmissions);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1796
1925
|
/**
|
|
1797
1926
|
* Make one request of the provider and take its answer: the instructions and tools the turn
|
|
1798
1927
|
* is running with, the session they belong to, the inference brackets around the request, and
|
|
@@ -1817,6 +1946,11 @@ export class AgentBase {
|
|
|
1817
1946
|
};
|
|
1818
1947
|
this.#loopId = inferenceStart.loopId;
|
|
1819
1948
|
this.#turnId = inferenceStart.turnId;
|
|
1949
|
+
setAgentSpanAttributes(ctx, {
|
|
1950
|
+
"agent.loop.id": inferenceStart.loopId,
|
|
1951
|
+
"agent.turn.id": inferenceStart.turnId,
|
|
1952
|
+
"agent.inference.id": inferenceStart.inferenceId,
|
|
1953
|
+
});
|
|
1820
1954
|
await this.#enterStage(ctx, "inference", this.#hooks.beforeInferenceTransact === undefined
|
|
1821
1955
|
? undefined
|
|
1822
1956
|
: (hookCtx) => this.#hooks.beforeInferenceTransact?.(hookCtx, inferenceStart));
|
|
@@ -1908,7 +2042,7 @@ export class AgentBase {
|
|
|
1908
2042
|
const previousTokens = this.#contextTokens;
|
|
1909
2043
|
this.#contextTokens = tokens;
|
|
1910
2044
|
try {
|
|
1911
|
-
await this.#
|
|
2045
|
+
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
1912
2046
|
const write = (writeCtx) => tokens === undefined
|
|
1913
2047
|
? this.#persistence.deleteValue(writeCtx, "context")
|
|
1914
2048
|
: this.#persistence.writeValue(writeCtx, "context", { tokens });
|
|
@@ -1942,7 +2076,7 @@ export class AgentBase {
|
|
|
1942
2076
|
async #runCompaction(ctx, signal) {
|
|
1943
2077
|
if (this.#compaction === undefined)
|
|
1944
2078
|
return;
|
|
1945
|
-
await
|
|
2079
|
+
await this.#span(ctx, "agent.compaction", {}, (compactionCtx) => this.#compactHistory(compactionCtx, signal));
|
|
1946
2080
|
}
|
|
1947
2081
|
/** The compaction itself, on the context of the span it belongs to. */
|
|
1948
2082
|
async #compactHistory(ctx, signal) {
|
|
@@ -1963,6 +2097,11 @@ export class AgentBase {
|
|
|
1963
2097
|
};
|
|
1964
2098
|
this.#loopId = compactionStart.loopId;
|
|
1965
2099
|
this.#turnId = compactionStart.turnId;
|
|
2100
|
+
setAgentSpanAttributes(ctx, {
|
|
2101
|
+
"agent.loop.id": compactionStart.loopId,
|
|
2102
|
+
"agent.turn.id": compactionStart.turnId,
|
|
2103
|
+
"agent.compaction.id": compactionStart.compactionId,
|
|
2104
|
+
});
|
|
1966
2105
|
await this.#invokeHook(ctx, this.#hooks.beforeCompaction, compactionStart);
|
|
1967
2106
|
// Provider compaction is this turn's work, so it runs on this turn's lifetime: an
|
|
1968
2107
|
// abort reaches the provider operation itself rather than waiting for it to finish
|
|
@@ -1980,7 +2119,7 @@ export class AgentBase {
|
|
|
1980
2119
|
}
|
|
1981
2120
|
if (result.status === "completed") {
|
|
1982
2121
|
const completed = { ...compactionStart, result };
|
|
1983
|
-
await this.#
|
|
2122
|
+
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
1984
2123
|
// Physically delete the superseded records and write the replacement —
|
|
1985
2124
|
// which keeps the messages that stay — in one atomic step.
|
|
1986
2125
|
await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
@@ -2034,7 +2173,7 @@ export class AgentBase {
|
|
|
2034
2173
|
let settled = false;
|
|
2035
2174
|
let staged = false;
|
|
2036
2175
|
try {
|
|
2037
|
-
await this.#
|
|
2176
|
+
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
2038
2177
|
// A call the durable batch still holds belongs to the resume, which answers it
|
|
2039
2178
|
// properly — and re-executes it when the tool is durable. Settling it here as
|
|
2040
2179
|
// well would give the conversation two results for one call.
|
|
@@ -2142,7 +2281,7 @@ export class AgentBase {
|
|
|
2142
2281
|
content: [{ type: "text", text: `The last turn failed: ${message}` }],
|
|
2143
2282
|
};
|
|
2144
2283
|
try {
|
|
2145
|
-
await this.#
|
|
2284
|
+
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
2146
2285
|
await this.#appendRecord(lockCtx, { type: "system", message: failure });
|
|
2147
2286
|
this.#messages.push(failure);
|
|
2148
2287
|
});
|
|
@@ -2153,7 +2292,7 @@ export class AgentBase {
|
|
|
2153
2292
|
}
|
|
2154
2293
|
/** Move every pending hook notice into history as one atomic, ordered append batch. */
|
|
2155
2294
|
async #consumeInjections(ctx) {
|
|
2156
|
-
return await this.#
|
|
2295
|
+
return await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
2157
2296
|
if (this.#injections.length === 0)
|
|
2158
2297
|
return false;
|
|
2159
2298
|
const durable = new Set((await this.#persistence.readValues(lockCtx, "inject.")).map(({ key }) => key));
|
|
@@ -2202,16 +2341,16 @@ export class AgentBase {
|
|
|
2202
2341
|
* context store and the in-memory history. The moves run in one transaction, so a message
|
|
2203
2342
|
* is never durable in both stores or neither, and memory changes only after the commit.
|
|
2204
2343
|
*
|
|
2205
|
-
* What the consumption has to announce is announced once the
|
|
2206
|
-
* told a message has landed may perfectly well answer by sending another one
|
|
2207
|
-
*
|
|
2344
|
+
* What the consumption has to announce is announced once the transaction has committed. A
|
|
2345
|
+
* hook told a message has landed may perfectly well answer by sending another one without
|
|
2346
|
+
* re-entering the transaction that delivered the first message.
|
|
2208
2347
|
*/
|
|
2209
2348
|
async #consumeQueue(ctx, queue, mode, kind) {
|
|
2210
2349
|
const prefix = `${kind}.`;
|
|
2211
|
-
/** Filled in once the consumption has committed,
|
|
2350
|
+
/** Filled in once the consumption has committed, then reported to observers. */
|
|
2212
2351
|
const accepted = [];
|
|
2213
2352
|
let permissionChange;
|
|
2214
|
-
const consumed = await this.#
|
|
2353
|
+
const consumed = await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
2215
2354
|
if (queue.length === 0)
|
|
2216
2355
|
return false;
|
|
2217
2356
|
// The durable queue, not memory, decides what is left to consume after a restart.
|
|
@@ -2294,8 +2433,7 @@ export class AgentBase {
|
|
|
2294
2433
|
await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
2295
2434
|
if (selectionChanged) {
|
|
2296
2435
|
if (this.#hooks.modelChanged !== undefined && model !== undefined) {
|
|
2297
|
-
// The hook runs
|
|
2298
|
-
// transaction that commits the switch, so its store executes directly on
|
|
2436
|
+
// The hook runs inside the transaction that commits the switch, so its store executes directly on
|
|
2299
2437
|
// that transaction: what it writes lands and rolls back with the change
|
|
2300
2438
|
// it was told about, never on its own. The context it is given ends with
|
|
2301
2439
|
// the transaction, so a store it keeps cannot outlive the switch.
|
|
@@ -2393,16 +2531,14 @@ export class AgentBase {
|
|
|
2393
2531
|
id: entry.id,
|
|
2394
2532
|
kind,
|
|
2395
2533
|
message: entry.message,
|
|
2396
|
-
...(entry.metadata === undefined
|
|
2397
|
-
? {}
|
|
2398
|
-
: { metadata: entry.metadata }),
|
|
2534
|
+
...(entry.metadata === undefined ? {} : { metadata: entry.metadata }),
|
|
2399
2535
|
});
|
|
2400
2536
|
}
|
|
2401
2537
|
});
|
|
2402
2538
|
if (reset)
|
|
2403
2539
|
this.#rotateHistoryKV();
|
|
2404
2540
|
// Committed: from here the messages are part of the conversation, so what has to be
|
|
2405
|
-
// announced about them is decided now and reported once the
|
|
2541
|
+
// announced about them is decided now and reported once the transaction commits.
|
|
2406
2542
|
permissionChange = modeChange;
|
|
2407
2543
|
accepted.push(...batch.map((entry) => ({
|
|
2408
2544
|
id: entry.id,
|
|
@@ -2452,13 +2588,13 @@ export class AgentBase {
|
|
|
2452
2588
|
await this.#withTransactionalContext(hookCtx, (liveCtx) => hook(liveCtx, argument));
|
|
2453
2589
|
}
|
|
2454
2590
|
/**
|
|
2455
|
-
* Replace the in-memory state with the durable one.
|
|
2591
|
+
* Replace the in-memory state with the durable one. Durable queue acceptance guarantees every
|
|
2456
2592
|
* message already in memory reached storage first, so the load result supersedes memory
|
|
2457
2593
|
* entirely: the main store rebuilds the context, and the sorted queue keys rebuild the
|
|
2458
2594
|
* not-yet-consumed queues. Consecutive block records reassemble into one assistant message.
|
|
2459
2595
|
*/
|
|
2460
2596
|
async #loadHistory(ctx) {
|
|
2461
|
-
await this.#
|
|
2597
|
+
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
2462
2598
|
const records = await this.#persistence.load(lockCtx);
|
|
2463
2599
|
const last = records[records.length - 1];
|
|
2464
2600
|
this.#lastRecordType = last?.type;
|
|
@@ -2540,12 +2676,12 @@ export class AgentBase {
|
|
|
2540
2676
|
* complete context behind.
|
|
2541
2677
|
*/
|
|
2542
2678
|
async #runToolBatch(ctx, entries, resume, signal, abortPromise) {
|
|
2543
|
-
return await
|
|
2679
|
+
return await this.#span(ctx, "agent.tools", { "agent.tool.count": entries.length, "agent.tool.resume": resume }, (batchCtx) => this.#dispatchToolBatch(batchCtx, entries, resume, signal, abortPromise));
|
|
2544
2680
|
}
|
|
2545
2681
|
/** The batch itself, on the context of the span it belongs to. */
|
|
2546
2682
|
async #dispatchToolBatch(ctx, entries, resume, signal, abortPromise) {
|
|
2547
2683
|
if (!resume) {
|
|
2548
|
-
await this.#
|
|
2684
|
+
await this.#runPersistenceStep(this.#workContext(ctx), (lockCtx) => this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
2549
2685
|
for (const entry of entries) {
|
|
2550
2686
|
await this.#persistence.writeValue(txCtx, entry.key, this.#storedToolEntry(entry));
|
|
2551
2687
|
}
|
|
@@ -2577,7 +2713,7 @@ export class AgentBase {
|
|
|
2577
2713
|
if (commitFailed)
|
|
2578
2714
|
return;
|
|
2579
2715
|
try {
|
|
2580
|
-
await this.#
|
|
2716
|
+
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
2581
2717
|
while (committed < entries.length) {
|
|
2582
2718
|
const entry = entries[committed];
|
|
2583
2719
|
const proposed = results[committed];
|
|
@@ -2639,7 +2775,13 @@ export class AgentBase {
|
|
|
2639
2775
|
// The call's own span hangs off the batch's. Every call in the batch runs at
|
|
2640
2776
|
// the same time, so each opens its span from the batch's context and carries
|
|
2641
2777
|
// its own from there.
|
|
2642
|
-
const execution =
|
|
2778
|
+
const execution = this.#span(ctx, "agent.tool", {
|
|
2779
|
+
"agent.tool.id": entry.id,
|
|
2780
|
+
"agent.tool.name": entry.call.name,
|
|
2781
|
+
...(entry.call.namespace === undefined
|
|
2782
|
+
? {}
|
|
2783
|
+
: { "agent.tool.namespace": entry.call.namespace }),
|
|
2784
|
+
}, (toolCtx) => this.#executeToolCall(withLifetime(this.#workContext(toolCtx), toolLifetime), entry));
|
|
2643
2785
|
running.push(execution);
|
|
2644
2786
|
outcome = await Promise.race([execution, abortPromise, this.#closingTools()]);
|
|
2645
2787
|
}
|
|
@@ -3026,7 +3168,7 @@ export class AgentBase {
|
|
|
3026
3168
|
}
|
|
3027
3169
|
/**
|
|
3028
3170
|
* Write a queue entry under a key nothing else holds. An enqueue running outside the
|
|
3029
|
-
*
|
|
3171
|
+
* database transaction can race an independent one for the tail position, so the key is claimed
|
|
3030
3172
|
* with an absent-only write and a taken key moves one sequence further rather than
|
|
3031
3173
|
* overwriting whatever claimed it first.
|
|
3032
3174
|
*/
|
|
@@ -3055,7 +3197,7 @@ export class AgentBase {
|
|
|
3055
3197
|
const persist = async (event) => {
|
|
3056
3198
|
if (event === undefined)
|
|
3057
3199
|
return;
|
|
3058
|
-
await this.#
|
|
3200
|
+
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
3059
3201
|
if (this.#hooks.onEventTransact === undefined) {
|
|
3060
3202
|
await this.#appendRecord(lockCtx, { type: "block", block: event.block });
|
|
3061
3203
|
}
|