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