@slopus/happy-agent-base 0.0.31 → 0.0.32
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 +5 -2
- package/dist/AgentBase.d.ts.map +1 -1
- package/dist/AgentBase.js +152 -104
- package/dist/AgentBase.js.map +1 -1
- package/dist/AgentBaseHooks.d.ts +10 -8
- package/dist/AgentBaseHooks.d.ts.map +1 -1
- package/dist/AgentKV.d.ts +1 -1
- package/dist/AgentKV.js +1 -1
- package/dist/AgentPersistence.d.ts +6 -0
- package/dist/AgentPersistence.d.ts.map +1 -1
- package/dist/AgentProviderContext.d.ts +28 -0
- package/dist/AgentProviderContext.d.ts.map +1 -0
- package/dist/AgentProviderContext.js +215 -0
- package/dist/AgentProviderContext.js.map +1 -0
- package/dist/AgentSystemLocal.d.ts.map +1 -1
- package/dist/AgentSystemLocal.js +4 -1
- package/dist/AgentSystemLocal.js.map +1 -1
- package/dist/AgentTaskContext.js +23 -2
- package/dist/AgentTaskContext.js.map +1 -1
- package/dist/AgentTool.d.ts +1 -3
- package/dist/AgentTool.d.ts.map +1 -1
- package/dist/AgentTool.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -83,8 +83,11 @@ transaction that appends the result. Nothing outside the loop ever executes a to
|
|
|
83
83
|
drove execution would be deciding inside machinery that also commits results, resumes interrupted
|
|
84
84
|
batches, and settles cancelled ones.
|
|
85
85
|
|
|
86
|
-
Every
|
|
87
|
-
`
|
|
86
|
+
Every tool call receives a Base-generated cuid2 `id`; executable calls also receive a call-bound
|
|
87
|
+
`kv`. Events, hooks, task context, modules, results, and execution use that ID. The provider-native
|
|
88
|
+
ID remains only in the raw stream and private provider-context records so replay and server-tool
|
|
89
|
+
results can still be correlated after a restart; each context record pairs it with the Base ID as
|
|
90
|
+
a separate sidecar, never in `vendor`. Calling `call.commit(ctx, result)` inside a transaction
|
|
88
91
|
atomically saves that result with the tool's writes. The first successful commit wins; later
|
|
89
92
|
commits and the tool's eventual return or throw are ignored. Committed results survive a crash,
|
|
90
93
|
remain ordered with their batch, and the call-bound KV is erased in the result transaction.
|
package/dist/AgentBase.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentBase.d.ts","sourceRoot":"","sources":["../sources/AgentBase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAMR,sBAAsB,EACtB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"AgentBase.d.ts","sourceRoot":"","sources":["../sources/AgentBase.ts"],"names":[],"mappings":"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"}
|
package/dist/AgentBase.js
CHANGED
|
@@ -10,6 +10,7 @@ import { outsideAgentDatabaseOperation } from "./AgentDatabaseConnection.js";
|
|
|
10
10
|
import { taskContextBeforeToolCall, withAgentTaskContext } from "./AgentTaskContext.js";
|
|
11
11
|
import { AgentKV } from "./AgentKV.js";
|
|
12
12
|
import { AGENT_BASE_PENDING_KEY, agentBasePendingStateOf, } from "./AgentBasePending.js";
|
|
13
|
+
import { assistantContextRecord, baseContextMessages, baseSessionEvent, contextFromRecords, providerContextMessages, storedContextToolIds, toolContextRecord, } from "./AgentProviderContext.js";
|
|
13
14
|
import { cuid2Schema, ownAgentMessageMetadata, ownAgentMetadata, } from "./AgentMetadata.js";
|
|
14
15
|
import { DEFAULT_AGENT_PERMISSION_MODE, isAgentPermissionMode, } from "./AgentPermissionMode.js";
|
|
15
16
|
import { AgentProviders } from "./AgentProviders.js";
|
|
@@ -47,7 +48,6 @@ const insideLoops = new AsyncLocalStorage();
|
|
|
47
48
|
const INSIDE_CLOSE_REPORT_MS = 15;
|
|
48
49
|
const storedToolCallSchema = Type.Object({
|
|
49
50
|
id: cuid2Schema,
|
|
50
|
-
providerCallId: Type.String(),
|
|
51
51
|
call: Type.Object({
|
|
52
52
|
type: Type.Literal("tool_call"),
|
|
53
53
|
name: Type.String(),
|
|
@@ -61,7 +61,7 @@ const storedToolCallSchema = Type.Object({
|
|
|
61
61
|
/** A durable first-writer-wins claim on the result one call will append. */
|
|
62
62
|
const storedToolResultSchema = Type.Object({
|
|
63
63
|
role: Type.Literal("tool"),
|
|
64
|
-
callId:
|
|
64
|
+
callId: cuid2Schema,
|
|
65
65
|
content: Type.Array(Type.Unknown()),
|
|
66
66
|
isError: Type.Optional(Type.Boolean()),
|
|
67
67
|
vendor: Type.Optional(Type.Unknown()),
|
|
@@ -271,8 +271,10 @@ export class AgentBase {
|
|
|
271
271
|
#session;
|
|
272
272
|
/** The provider-facing configuration the current session was created with. */
|
|
273
273
|
#sessionConfig;
|
|
274
|
-
/** The conversation
|
|
274
|
+
/** The conversation under Base-owned identities, reloaded from the store every turn. */
|
|
275
275
|
#messages = [];
|
|
276
|
+
/** Provider-native tool IDs used only when serializing the private provider context. */
|
|
277
|
+
#providerToolIds = new Map();
|
|
276
278
|
/** The durable steering queue, in the order its keys sort. */
|
|
277
279
|
#steering = [];
|
|
278
280
|
/** The durable send queue, in the order its keys sort. */
|
|
@@ -2266,6 +2268,15 @@ export class AgentBase {
|
|
|
2266
2268
|
context: { instructions, messages: snapshot },
|
|
2267
2269
|
...(this.#model === undefined ? {} : { model: this.#model }),
|
|
2268
2270
|
});
|
|
2271
|
+
const cancelledContext = result.status === "cancelled"
|
|
2272
|
+
? baseContextMessages(result.context.messages, this.#providerToolIds)
|
|
2273
|
+
: undefined;
|
|
2274
|
+
let hookResult = result.status === "cancelled" && cancelledContext !== undefined
|
|
2275
|
+
? {
|
|
2276
|
+
...result,
|
|
2277
|
+
context: { ...result.context, messages: cancelledContext.messages },
|
|
2278
|
+
}
|
|
2279
|
+
: result;
|
|
2269
2280
|
if (result.status === "failed") {
|
|
2270
2281
|
await this.#preserveCompactionContinuation(ctx, continueWithInference && !signal.aborted);
|
|
2271
2282
|
await this.#invokeHook(ctx, this.#hooks.afterCompaction, {
|
|
@@ -2275,7 +2286,18 @@ export class AgentBase {
|
|
|
2275
2286
|
throw new Error(result.message);
|
|
2276
2287
|
}
|
|
2277
2288
|
if (result.status === "completed") {
|
|
2278
|
-
const
|
|
2289
|
+
const replacement = baseContextMessages(result.context.messages, this.#providerToolIds);
|
|
2290
|
+
const preserved = baseContextMessages(result.preservedMessages, this.#providerToolIds);
|
|
2291
|
+
const publicResult = {
|
|
2292
|
+
...result,
|
|
2293
|
+
preservedMessages: preserved.messages,
|
|
2294
|
+
context: { ...result.context, messages: replacement.messages },
|
|
2295
|
+
};
|
|
2296
|
+
hookResult = publicResult;
|
|
2297
|
+
const completed = {
|
|
2298
|
+
...compactionStart,
|
|
2299
|
+
result: publicResult,
|
|
2300
|
+
};
|
|
2279
2301
|
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
2280
2302
|
// Physically delete the superseded records and write the replacement —
|
|
2281
2303
|
// which keeps the messages that stay — in one atomic step.
|
|
@@ -2288,7 +2310,8 @@ export class AgentBase {
|
|
|
2288
2310
|
}
|
|
2289
2311
|
await this.#persistence.append(txCtx, {
|
|
2290
2312
|
type: "compaction",
|
|
2291
|
-
|
|
2313
|
+
contextToolIds: storedContextToolIds(replacement.toolIds),
|
|
2314
|
+
messages: replacement.messages,
|
|
2292
2315
|
});
|
|
2293
2316
|
if (continueWithInference) {
|
|
2294
2317
|
// The replacement and the response it still owes are one durable
|
|
@@ -2300,7 +2323,8 @@ export class AgentBase {
|
|
|
2300
2323
|
}
|
|
2301
2324
|
});
|
|
2302
2325
|
this.#rotateHistoryKV();
|
|
2303
|
-
this.#messages =
|
|
2326
|
+
this.#messages = replacement.messages;
|
|
2327
|
+
this.#providerToolIds = replacement.toolIds;
|
|
2304
2328
|
this.#lastRecordType = "compaction";
|
|
2305
2329
|
});
|
|
2306
2330
|
// The conversation the measurement described is gone; its size is unknown
|
|
@@ -2312,7 +2336,7 @@ export class AgentBase {
|
|
|
2312
2336
|
}
|
|
2313
2337
|
await this.#invokeHook(ctx, this.#hooks.afterCompaction, {
|
|
2314
2338
|
...compactionStart,
|
|
2315
|
-
result,
|
|
2339
|
+
result: hookResult,
|
|
2316
2340
|
});
|
|
2317
2341
|
this.#compaction = undefined;
|
|
2318
2342
|
pending.resolve();
|
|
@@ -2355,7 +2379,7 @@ export class AgentBase {
|
|
|
2355
2379
|
// it independently without racing that batch, and it cannot settle the agent
|
|
2356
2380
|
// while the call remains open, so leave the durable stage untouched for resume.
|
|
2357
2381
|
const pending = await this.#persistence.readValues(lockCtx, "tool.");
|
|
2358
|
-
const dispatched = new Set(pending.map(({ key, value }) => this.#restoreToolEntry(key, value).
|
|
2382
|
+
const dispatched = new Set(pending.map(({ key, value }) => this.#restoreToolEntry(key, value).id));
|
|
2359
2383
|
if (owed.some((call) => dispatched.has(call.callId))) {
|
|
2360
2384
|
this.#durableWorkBlocked = true;
|
|
2361
2385
|
return;
|
|
@@ -2364,7 +2388,7 @@ export class AgentBase {
|
|
|
2364
2388
|
const entry = this.#newToolEntry(index, call);
|
|
2365
2389
|
return {
|
|
2366
2390
|
...entry,
|
|
2367
|
-
committed: toolFailure(entry.
|
|
2391
|
+
committed: toolFailure(entry.id, reason),
|
|
2368
2392
|
};
|
|
2369
2393
|
});
|
|
2370
2394
|
// Give every otherwise-undispatched call a durable internal identity and staged
|
|
@@ -2380,12 +2404,12 @@ export class AgentBase {
|
|
|
2380
2404
|
await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
2381
2405
|
for (const entry of entries) {
|
|
2382
2406
|
const result = entry.committed;
|
|
2383
|
-
await this.#appendRecord(txCtx,
|
|
2407
|
+
await this.#appendRecord(txCtx, toolContextRecord(result, this.#providerToolIds));
|
|
2384
2408
|
await this.#persistence.deleteValue(txCtx, entry.key);
|
|
2385
2409
|
// A result the conversation records is a result the hook sees, however
|
|
2386
2410
|
// little of a run produced it. A hook that fails here leaves the calls
|
|
2387
2411
|
// unsettled, which is what lets a later attempt answer them properly.
|
|
2388
|
-
await this.#invokeToolTransactHook(txCtx, entry.id,
|
|
2412
|
+
await this.#invokeToolTransactHook(txCtx, entry.id, this.#hooks.afterToolCallTransact, result);
|
|
2389
2413
|
await this.#kv.scoped("call", entry.id).clear(txCtx);
|
|
2390
2414
|
}
|
|
2391
2415
|
await this.#recordPending(txCtx, "inference");
|
|
@@ -2739,6 +2763,7 @@ export class AgentBase {
|
|
|
2739
2763
|
queue.splice(0, count);
|
|
2740
2764
|
if (reset) {
|
|
2741
2765
|
this.#messages = injected === undefined ? [] : [injected];
|
|
2766
|
+
this.#providerToolIds.clear();
|
|
2742
2767
|
this.#contextTokens = undefined;
|
|
2743
2768
|
}
|
|
2744
2769
|
this.#messages.push(...batch.map((entry) => entry.message));
|
|
@@ -2838,14 +2863,15 @@ export class AgentBase {
|
|
|
2838
2863
|
const records = await this.#persistence.load(lockCtx);
|
|
2839
2864
|
const last = records[records.length - 1];
|
|
2840
2865
|
this.#lastRecordType = last?.type;
|
|
2841
|
-
|
|
2866
|
+
const restored = contextFromRecords(records);
|
|
2842
2867
|
const steering = await this.#persistence.readValues(lockCtx, "steering.");
|
|
2843
2868
|
const sends = await this.#persistence.readValues(lockCtx, "send.");
|
|
2844
2869
|
const injections = await this.#persistence.readValues(lockCtx, "inject.");
|
|
2845
2870
|
const pendingTools = await this.#persistence.readValues(lockCtx, "tool.");
|
|
2846
2871
|
const settings = await this.#persistence.readValues(lockCtx, "settings");
|
|
2847
2872
|
const context = await this.#persistence.readValues(lockCtx, "context");
|
|
2848
|
-
this.#messages = restored;
|
|
2873
|
+
this.#messages = restored.messages;
|
|
2874
|
+
this.#providerToolIds = restored.toolIds;
|
|
2849
2875
|
// The measured size of the restored context, so the first turn after a reload can
|
|
2850
2876
|
// still decide whether it needs a compaction.
|
|
2851
2877
|
const measured = context[0]?.value;
|
|
@@ -2882,7 +2908,7 @@ export class AgentBase {
|
|
|
2882
2908
|
// context would keep an unanswered tool call for ever, which most providers
|
|
2883
2909
|
// reject outright. They are recovered as the batch that was about to be
|
|
2884
2910
|
// dispatched.
|
|
2885
|
-
const owed = this.#unansweredCalls(restored);
|
|
2911
|
+
const owed = this.#unansweredCalls(restored.messages);
|
|
2886
2912
|
if (owed.length > 0) {
|
|
2887
2913
|
this.#pendingTools = owed.map((call, index) => this.#newToolEntry(index, call));
|
|
2888
2914
|
this.#pendingToolsUndispatched = true;
|
|
@@ -2918,7 +2944,8 @@ export class AgentBase {
|
|
|
2918
2944
|
// Last, so a hook noting a call about to happen sees a transaction holding
|
|
2919
2945
|
// the whole batch it belongs to.
|
|
2920
2946
|
for (const entry of entries) {
|
|
2921
|
-
|
|
2947
|
+
const { vendor: _vendor, ...call } = entry.call;
|
|
2948
|
+
await this.#invokeToolTransactHook(txCtx, entry.id, this.#hooks.beforeToolCallTransact, call);
|
|
2922
2949
|
}
|
|
2923
2950
|
}));
|
|
2924
2951
|
}
|
|
@@ -2962,20 +2989,17 @@ export class AgentBase {
|
|
|
2962
2989
|
throw new Error(`The committed result claim for tool "${entry.id}" is not valid.`);
|
|
2963
2990
|
}
|
|
2964
2991
|
const result = stored;
|
|
2965
|
-
if (result.callId !== entry.
|
|
2966
|
-
throw new Error(`The committed result claim for tool "${entry.id}" has the wrong
|
|
2992
|
+
if (result.callId !== entry.id) {
|
|
2993
|
+
throw new Error(`The committed result claim for tool "${entry.id}" has the wrong identity.`);
|
|
2967
2994
|
}
|
|
2968
2995
|
winner = result;
|
|
2969
2996
|
results[committed] = result;
|
|
2970
|
-
await this.#appendRecord(txCtx,
|
|
2971
|
-
type: "tool",
|
|
2972
|
-
message: result,
|
|
2973
|
-
});
|
|
2997
|
+
await this.#appendRecord(txCtx, toolContextRecord(result, this.#providerToolIds));
|
|
2974
2998
|
// The call is answered, so both its retry record and its temporary
|
|
2975
2999
|
// invocation store disappear in this same result transaction.
|
|
2976
3000
|
await this.#persistence.deleteValue(txCtx, entry.key);
|
|
2977
3001
|
await this.#persistence.deleteValue(txCtx, resultKey);
|
|
2978
|
-
await this.#invokeToolTransactHook(txCtx, entry.id,
|
|
3002
|
+
await this.#invokeToolTransactHook(txCtx, entry.id, this.#hooks.afterToolCallTransact, result);
|
|
2979
3003
|
await this.#kv.scoped("call", entry.id).clear(txCtx);
|
|
2980
3004
|
});
|
|
2981
3005
|
this.#messages.push(winner);
|
|
@@ -3004,7 +3028,7 @@ export class AgentBase {
|
|
|
3004
3028
|
outcome = entry.committed;
|
|
3005
3029
|
}
|
|
3006
3030
|
else if (resume && !(await this.#isDurable(ctx, entry.call))) {
|
|
3007
|
-
outcome = toolFailure(entry.
|
|
3031
|
+
outcome = toolFailure(entry.id, "The tool call was interrupted by a restart and was not retried.");
|
|
3008
3032
|
}
|
|
3009
3033
|
else {
|
|
3010
3034
|
const toolLifetime = AbortSignal.any([signal, this.#closeController.signal]);
|
|
@@ -3027,7 +3051,7 @@ export class AgentBase {
|
|
|
3027
3051
|
outcome === ABORTED
|
|
3028
3052
|
? {
|
|
3029
3053
|
role: "tool",
|
|
3030
|
-
callId: entry.
|
|
3054
|
+
callId: entry.id,
|
|
3031
3055
|
content: [
|
|
3032
3056
|
{
|
|
3033
3057
|
type: "text",
|
|
@@ -3131,19 +3155,20 @@ export class AgentBase {
|
|
|
3131
3155
|
ordered.push(result);
|
|
3132
3156
|
}
|
|
3133
3157
|
}
|
|
3134
|
-
return ordered;
|
|
3158
|
+
return providerContextMessages(ordered, this.#providerToolIds);
|
|
3135
3159
|
}
|
|
3136
|
-
/**
|
|
3160
|
+
/** Turn one canonical call into its durable Base-owned batch entry. */
|
|
3137
3161
|
#newToolEntry(index, call) {
|
|
3138
|
-
|
|
3162
|
+
if (!Value.Check(cuid2Schema, call.callId)) {
|
|
3163
|
+
throw new Error(`Tool call "${call.callId}" is not a Base-generated ID.`);
|
|
3164
|
+
}
|
|
3139
3165
|
return {
|
|
3140
|
-
key: this.#toolKey(index,
|
|
3141
|
-
id,
|
|
3142
|
-
providerCallId: call.callId,
|
|
3166
|
+
key: this.#toolKey(index, call.callId),
|
|
3167
|
+
id: call.callId,
|
|
3143
3168
|
call,
|
|
3144
3169
|
};
|
|
3145
3170
|
}
|
|
3146
|
-
/** Restore the
|
|
3171
|
+
/** Restore the Base/provider context identity pair dispatched before a restart. */
|
|
3147
3172
|
#restoreToolEntry(key, value) {
|
|
3148
3173
|
if (!Value.Check(storedToolCallSchema, value)) {
|
|
3149
3174
|
throw new Error(`The pending tool call under "${key}" is not valid.`);
|
|
@@ -3153,26 +3178,27 @@ export class AgentBase {
|
|
|
3153
3178
|
!Value.Check(storedToolResultSchema, stored.committed)) {
|
|
3154
3179
|
throw new Error(`The committed result under "${key}" is not valid.`);
|
|
3155
3180
|
}
|
|
3156
|
-
const call = {
|
|
3157
|
-
...stored.call,
|
|
3158
|
-
callId: stored.providerCallId,
|
|
3159
|
-
};
|
|
3181
|
+
const call = { ...stored.call, callId: stored.id };
|
|
3160
3182
|
return {
|
|
3161
3183
|
key,
|
|
3162
3184
|
id: stored.id,
|
|
3163
|
-
providerCallId: stored.providerCallId,
|
|
3164
3185
|
call,
|
|
3165
3186
|
...(stored.committed === undefined
|
|
3166
3187
|
? {}
|
|
3167
3188
|
: { committed: stored.committed }),
|
|
3168
3189
|
};
|
|
3169
3190
|
}
|
|
3170
|
-
/** The
|
|
3191
|
+
/** The durable representation pairs one Base identity with provider-context replay data. */
|
|
3171
3192
|
#storedToolEntry(entry) {
|
|
3172
|
-
|
|
3193
|
+
if (entry.call.callId !== entry.id) {
|
|
3194
|
+
throw new Error(`Pending tool "${entry.id}" has a mismatched Base identity.`);
|
|
3195
|
+
}
|
|
3196
|
+
if (entry.committed !== undefined && entry.committed.callId !== entry.id) {
|
|
3197
|
+
throw new Error(`Pending tool "${entry.id}" has a mismatched committed result ID.`);
|
|
3198
|
+
}
|
|
3199
|
+
const { callId: _callId, server: _server, ...call } = entry.call;
|
|
3173
3200
|
return {
|
|
3174
3201
|
id: entry.id,
|
|
3175
|
-
providerCallId: entry.providerCallId,
|
|
3176
3202
|
call,
|
|
3177
3203
|
...(entry.committed === undefined ? {} : { committed: entry.committed }),
|
|
3178
3204
|
};
|
|
@@ -3186,24 +3212,23 @@ export class AgentBase {
|
|
|
3186
3212
|
return `toolResult.${id}`;
|
|
3187
3213
|
}
|
|
3188
3214
|
/**
|
|
3189
|
-
* The scope one call owns: state lives under its
|
|
3190
|
-
*
|
|
3191
|
-
* still locates the provider call in conversation history.
|
|
3215
|
+
* The scope one call owns: state lives under its Base ID and is erased when the call commits.
|
|
3216
|
+
* The same ID locates the call in conversation history.
|
|
3192
3217
|
*/
|
|
3193
|
-
#callScoped(ctx, id
|
|
3194
|
-
return withAgentTaskContext(withAgentRunKV(withAgentKV(ctx, this.#kv.scoped("call", id)), this.#runKV.scoped("call", id)), taskContextBeforeToolCall(this.#messages,
|
|
3218
|
+
#callScoped(ctx, id) {
|
|
3219
|
+
return withAgentTaskContext(withAgentRunKV(withAgentKV(ctx, this.#kv.scoped("call", id)), this.#runKV.scoped("call", id)), taskContextBeforeToolCall(this.#messages, id));
|
|
3195
3220
|
}
|
|
3196
3221
|
/**
|
|
3197
3222
|
* Call a tool hook that writes inside a transaction of its own call's. The lifetime ends with
|
|
3198
3223
|
* the callback, so a context kept afterwards cannot outlive the transaction it belongs to.
|
|
3199
3224
|
* Its failure is not contained: it rolls that transaction back.
|
|
3200
3225
|
*/
|
|
3201
|
-
async #invokeToolTransactHook(txCtx, id,
|
|
3226
|
+
async #invokeToolTransactHook(txCtx, id, hook, argument) {
|
|
3202
3227
|
if (hook === undefined)
|
|
3203
3228
|
return;
|
|
3204
3229
|
const lifetime = new AbortController();
|
|
3205
3230
|
try {
|
|
3206
|
-
await hook(this.#callScoped(withLifetime(txCtx, lifetime.signal), id
|
|
3231
|
+
await hook(this.#callScoped(withLifetime(txCtx, lifetime.signal), id), argument);
|
|
3207
3232
|
}
|
|
3208
3233
|
finally {
|
|
3209
3234
|
lifetime.abort();
|
|
@@ -3218,7 +3243,7 @@ export class AgentBase {
|
|
|
3218
3243
|
const { call } = entry;
|
|
3219
3244
|
const failure = (text) => ({
|
|
3220
3245
|
role: "tool",
|
|
3221
|
-
callId:
|
|
3246
|
+
callId: entry.id,
|
|
3222
3247
|
content: [{ type: "text", text }],
|
|
3223
3248
|
isError: true,
|
|
3224
3249
|
});
|
|
@@ -3259,7 +3284,7 @@ export class AgentBase {
|
|
|
3259
3284
|
const callLifetime = new AbortController();
|
|
3260
3285
|
let committing = false;
|
|
3261
3286
|
const boundedCallKV = callKV.until(callLifetime.signal, () => !committing);
|
|
3262
|
-
const callCtx = withAgentKV(this.#callScoped(ctx, entry.id
|
|
3287
|
+
const callCtx = withAgentKV(this.#callScoped(ctx, entry.id), boundedCallKV);
|
|
3263
3288
|
// From here the call is one the two tool hooks bracket: a tool that exists, a call that
|
|
3264
3289
|
// finished, and arguments its schema accepts. A call refused before that reaches neither
|
|
3265
3290
|
// hook, because there is nothing yet to decide about or to report.
|
|
@@ -3273,7 +3298,7 @@ export class AgentBase {
|
|
|
3273
3298
|
resolveCommitted = resolve;
|
|
3274
3299
|
});
|
|
3275
3300
|
const outcomeFor = (result) => ({
|
|
3276
|
-
callId:
|
|
3301
|
+
callId: entry.id,
|
|
3277
3302
|
tool: ran,
|
|
3278
3303
|
arguments: ranArguments,
|
|
3279
3304
|
content: [...ran.toLLM(result)],
|
|
@@ -3292,7 +3317,7 @@ export class AgentBase {
|
|
|
3292
3317
|
const candidate = outcomeFor(result);
|
|
3293
3318
|
const message = {
|
|
3294
3319
|
role: "tool",
|
|
3295
|
-
callId: entry.
|
|
3320
|
+
callId: entry.id,
|
|
3296
3321
|
content: candidate.content,
|
|
3297
3322
|
...(candidate.isError ? { isError: true } : {}),
|
|
3298
3323
|
};
|
|
@@ -3341,7 +3366,7 @@ export class AgentBase {
|
|
|
3341
3366
|
};
|
|
3342
3367
|
try {
|
|
3343
3368
|
const decision = await this.#hooks.beforeToolCall?.(callCtx, {
|
|
3344
|
-
callId:
|
|
3369
|
+
callId: entry.id,
|
|
3345
3370
|
tool,
|
|
3346
3371
|
arguments: args,
|
|
3347
3372
|
});
|
|
@@ -3349,7 +3374,7 @@ export class AgentBase {
|
|
|
3349
3374
|
// The hook answered the model itself, so the tool never runs and there is no
|
|
3350
3375
|
// structured result — only what the model is told.
|
|
3351
3376
|
outcome = {
|
|
3352
|
-
callId:
|
|
3377
|
+
callId: entry.id,
|
|
3353
3378
|
tool,
|
|
3354
3379
|
arguments: args,
|
|
3355
3380
|
content: [...decision.content],
|
|
@@ -3376,7 +3401,6 @@ export class AgentBase {
|
|
|
3376
3401
|
: [runCtx.lifetime, callLifetime.signal]));
|
|
3377
3402
|
const toolCall = {
|
|
3378
3403
|
id: entry.id,
|
|
3379
|
-
providerCallId: entry.providerCallId,
|
|
3380
3404
|
kv: boundedCallKV,
|
|
3381
3405
|
commit,
|
|
3382
3406
|
};
|
|
@@ -3415,7 +3439,7 @@ export class AgentBase {
|
|
|
3415
3439
|
outcome =
|
|
3416
3440
|
committedOutcome ??
|
|
3417
3441
|
{
|
|
3418
|
-
callId:
|
|
3442
|
+
callId: entry.id,
|
|
3419
3443
|
tool: ran,
|
|
3420
3444
|
arguments: ranArguments,
|
|
3421
3445
|
content: [
|
|
@@ -3431,7 +3455,7 @@ export class AgentBase {
|
|
|
3431
3455
|
await this.#invokeHookOn(callCtx, this.#hooks.afterToolCall, outcome);
|
|
3432
3456
|
return {
|
|
3433
3457
|
role: "tool",
|
|
3434
|
-
callId:
|
|
3458
|
+
callId: entry.id,
|
|
3435
3459
|
content: outcome.content,
|
|
3436
3460
|
...(outcome.isError ? { isError: true } : {}),
|
|
3437
3461
|
};
|
|
@@ -3487,21 +3511,26 @@ export class AgentBase {
|
|
|
3487
3511
|
// in-memory assistant message never diverges from what a reload would rebuild.
|
|
3488
3512
|
const persisted = [];
|
|
3489
3513
|
const toolCallIndexes = new Map();
|
|
3490
|
-
const
|
|
3491
|
-
|
|
3514
|
+
const toolResultIndexes = new Map();
|
|
3515
|
+
// Provider IDs remain only in the private context blocks. Every event and transactional
|
|
3516
|
+
// projection leaving this method uses the generated Base ID instead.
|
|
3517
|
+
const responseToolIds = new Map();
|
|
3518
|
+
const persist = async (block, event) => {
|
|
3519
|
+
if (block === undefined)
|
|
3492
3520
|
return;
|
|
3521
|
+
const record = assistantContextRecord(block, this.#providerToolIds);
|
|
3493
3522
|
await this.#runPersistenceStep(this.#workContext(ctx), async (lockCtx) => {
|
|
3494
|
-
if (this.#hooks.onEventTransact === undefined) {
|
|
3495
|
-
await this.#appendRecord(lockCtx,
|
|
3523
|
+
if (event === undefined || this.#hooks.onEventTransact === undefined) {
|
|
3524
|
+
await this.#appendRecord(lockCtx, record);
|
|
3496
3525
|
}
|
|
3497
3526
|
else {
|
|
3498
3527
|
await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
3499
|
-
await this.#appendRecord(txCtx,
|
|
3528
|
+
await this.#appendRecord(txCtx, record);
|
|
3500
3529
|
await this.#withTransactionalContext(txCtx, (hookCtx) => this.#hooks.onEventTransact?.(hookCtx, event));
|
|
3501
3530
|
});
|
|
3502
3531
|
}
|
|
3503
3532
|
});
|
|
3504
|
-
persisted.push(
|
|
3533
|
+
persisted.push(block);
|
|
3505
3534
|
};
|
|
3506
3535
|
const iterator = stream[Symbol.asyncIterator]();
|
|
3507
3536
|
// A response usually ends before its stream does — at the done event, or at an abort —
|
|
@@ -3522,7 +3551,8 @@ export class AgentBase {
|
|
|
3522
3551
|
exhausted = true;
|
|
3523
3552
|
break;
|
|
3524
3553
|
}
|
|
3525
|
-
const
|
|
3554
|
+
const providerEvent = next.value;
|
|
3555
|
+
const event = baseSessionEvent(providerEvent, responseToolIds, this.#providerToolIds);
|
|
3526
3556
|
await this.#emit(ctx, event);
|
|
3527
3557
|
switch (event.type) {
|
|
3528
3558
|
case "text_start":
|
|
@@ -3540,7 +3570,7 @@ export class AgentBase {
|
|
|
3540
3570
|
}
|
|
3541
3571
|
case "text_end": {
|
|
3542
3572
|
const last = content[content.length - 1];
|
|
3543
|
-
await persist(last?.type === "text" ? { ...event, block: last } : undefined);
|
|
3573
|
+
await persist(last?.type === "text" ? last : undefined, last?.type === "text" ? { ...event, block: last } : undefined);
|
|
3544
3574
|
break;
|
|
3545
3575
|
}
|
|
3546
3576
|
case "reasoning_start":
|
|
@@ -3566,11 +3596,14 @@ export class AgentBase {
|
|
|
3566
3596
|
: { reasoning: event.reasoning }),
|
|
3567
3597
|
};
|
|
3568
3598
|
content[content.length - 1] = finished;
|
|
3569
|
-
await persist({ ...event, block: finished });
|
|
3599
|
+
await persist(finished, { ...event, block: finished });
|
|
3570
3600
|
}
|
|
3571
3601
|
break;
|
|
3572
3602
|
}
|
|
3573
|
-
case "toolcall_start":
|
|
3603
|
+
case "toolcall_start": {
|
|
3604
|
+
if (providerEvent.type !== "toolcall_start") {
|
|
3605
|
+
throw new Error("The provider tool-call start changed event type.");
|
|
3606
|
+
}
|
|
3574
3607
|
toolCallIndexes.set(event.callId, content.length);
|
|
3575
3608
|
content.push({
|
|
3576
3609
|
type: "tool_call",
|
|
@@ -3581,9 +3614,12 @@ export class AgentBase {
|
|
|
3581
3614
|
? {}
|
|
3582
3615
|
: { namespace: event.namespace }),
|
|
3583
3616
|
...(event.server === undefined ? {} : { server: event.server }),
|
|
3584
|
-
...(
|
|
3617
|
+
...(providerEvent.vendor === undefined
|
|
3618
|
+
? {}
|
|
3619
|
+
: { vendor: providerEvent.vendor }),
|
|
3585
3620
|
});
|
|
3586
3621
|
break;
|
|
3622
|
+
}
|
|
3587
3623
|
case "toolcall_end": {
|
|
3588
3624
|
const index = toolCallIndexes.get(event.callId);
|
|
3589
3625
|
const block = index === undefined ? undefined : content[index];
|
|
@@ -3596,17 +3632,59 @@ export class AgentBase {
|
|
|
3596
3632
|
: { incomplete: event.incomplete }),
|
|
3597
3633
|
};
|
|
3598
3634
|
content[index] = finished;
|
|
3599
|
-
|
|
3635
|
+
const { vendor: _vendor, ...publicBlock } = finished;
|
|
3636
|
+
await persist(finished, {
|
|
3637
|
+
...event,
|
|
3638
|
+
block: publicBlock,
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
break;
|
|
3642
|
+
}
|
|
3643
|
+
case "toolcall_result_start": {
|
|
3644
|
+
if (providerEvent.type !== "toolcall_result_start") {
|
|
3645
|
+
throw new Error("The provider tool-result start changed event type.");
|
|
3646
|
+
}
|
|
3647
|
+
const callIndex = toolCallIndexes.get(event.callId);
|
|
3648
|
+
const call = callIndex === undefined ? undefined : content[callIndex];
|
|
3649
|
+
if (call?.type !== "tool_call") {
|
|
3650
|
+
throw new Error("A provider tool result has no matching call.");
|
|
3600
3651
|
}
|
|
3652
|
+
toolResultIndexes.set(event.callId, content.length);
|
|
3653
|
+
content.push({
|
|
3654
|
+
type: "tool_result",
|
|
3655
|
+
callId: event.callId,
|
|
3656
|
+
content: [],
|
|
3657
|
+
...(providerEvent.vendor === undefined
|
|
3658
|
+
? {}
|
|
3659
|
+
: { vendor: providerEvent.vendor }),
|
|
3660
|
+
});
|
|
3601
3661
|
break;
|
|
3602
3662
|
}
|
|
3603
|
-
// The provider settled a server tool call on its own backend and streams the
|
|
3604
|
-
// result here. The agent simply ignores it: nothing to execute, nothing to
|
|
3605
|
-
// store — the events still reach the hooks like every other event.
|
|
3606
|
-
case "toolcall_result_start":
|
|
3607
3663
|
case "toolcall_result_delta":
|
|
3608
|
-
case "toolcall_result_end":
|
|
3609
3664
|
break;
|
|
3665
|
+
case "toolcall_result_end": {
|
|
3666
|
+
const callIndex = toolCallIndexes.get(event.callId);
|
|
3667
|
+
const call = callIndex === undefined ? undefined : content[callIndex];
|
|
3668
|
+
const resultIndex = toolResultIndexes.get(event.callId);
|
|
3669
|
+
if (resultIndex === undefined || call?.type !== "tool_call") {
|
|
3670
|
+
throw new Error("A provider tool-result end has no matching start.");
|
|
3671
|
+
}
|
|
3672
|
+
const result = content[resultIndex];
|
|
3673
|
+
if (result?.type !== "tool_result") {
|
|
3674
|
+
throw new Error("A provider tool-result end has no matching start.");
|
|
3675
|
+
}
|
|
3676
|
+
const finished = {
|
|
3677
|
+
...result,
|
|
3678
|
+
content: event.content,
|
|
3679
|
+
...(event.isError === undefined ? {} : { isError: event.isError }),
|
|
3680
|
+
...(event.incomplete === undefined
|
|
3681
|
+
? {}
|
|
3682
|
+
: { incomplete: event.incomplete }),
|
|
3683
|
+
};
|
|
3684
|
+
content[resultIndex] = finished;
|
|
3685
|
+
await persist(finished, undefined);
|
|
3686
|
+
break;
|
|
3687
|
+
}
|
|
3610
3688
|
case "done":
|
|
3611
3689
|
return {
|
|
3612
3690
|
// Only blocks that finished, which are exactly the blocks that were
|
|
@@ -3683,41 +3761,11 @@ export class AgentBase {
|
|
|
3683
3761
|
}
|
|
3684
3762
|
}
|
|
3685
3763
|
}
|
|
3686
|
-
/**
|
|
3687
|
-
* The conversation a run of records spells out. A compaction record carries the complete
|
|
3688
|
-
* replacement context and supersedes everything before it; consecutive blocks belong to one
|
|
3689
|
-
* response. Used both to restore the whole history and to read the tail of it, which is why it
|
|
3690
|
-
* takes any run of records rather than the store itself.
|
|
3691
|
-
*/
|
|
3692
|
-
function messagesFromRecords(records) {
|
|
3693
|
-
let messages = [];
|
|
3694
|
-
for (const record of records) {
|
|
3695
|
-
if (record.type === "compaction") {
|
|
3696
|
-
messages = [...record.messages];
|
|
3697
|
-
continue;
|
|
3698
|
-
}
|
|
3699
|
-
if (record.type === "user" || record.type === "tool" || record.type === "system") {
|
|
3700
|
-
messages.push(record.message);
|
|
3701
|
-
continue;
|
|
3702
|
-
}
|
|
3703
|
-
const last = messages[messages.length - 1];
|
|
3704
|
-
if (last?.role === "assistant") {
|
|
3705
|
-
messages[messages.length - 1] = {
|
|
3706
|
-
role: "assistant",
|
|
3707
|
-
content: [...last.content, record.block],
|
|
3708
|
-
};
|
|
3709
|
-
}
|
|
3710
|
-
else {
|
|
3711
|
-
messages.push({ role: "assistant", content: [record.block] });
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
return messages;
|
|
3715
|
-
}
|
|
3716
3764
|
/** The result that stands in for a call the agent could not, or must not, carry out. */
|
|
3717
|
-
function toolFailure(
|
|
3765
|
+
function toolFailure(id, reason) {
|
|
3718
3766
|
return {
|
|
3719
3767
|
role: "tool",
|
|
3720
|
-
callId,
|
|
3768
|
+
callId: id,
|
|
3721
3769
|
content: [{ type: "text", text: reason }],
|
|
3722
3770
|
isError: true,
|
|
3723
3771
|
};
|