experimental-a2 0.13.0 → 0.14.1
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/CHANGELOG.md +43 -0
- package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
- package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +2 -2
- package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
- package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/ai-Cai-lCbj.d.ts +580 -0
- package/dist/ai-Cai-lCbj.d.ts.map +1 -0
- package/dist/ai-control-CcD4hh3y.js +119 -0
- package/dist/ai-control-CcD4hh3y.js.map +1 -0
- package/dist/ai-server.d.ts +6 -6
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +1053 -513
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -365
- package/dist/ai.js +801 -80
- package/dist/ai.js.map +1 -1
- package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
- package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
- package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
- package/dist/client-BYzHjkwU.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
- package/dist/contract-CKRg_E4q.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
- package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
- package/dist/reducer-DEMjEY_O.js +29 -0
- package/dist/reducer-DEMjEY_O.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
- package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
- package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
- package/dist/server-CjJSGcF7.js.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
- package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +19 -0
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
- package/dist/store-redis-notify-D2EI6gwX.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/docs/guides/06-ai-agents.mdx +287 -65
- package/docs/reference/01-api.mdx +122 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
- package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
- package/examples/playground/app/agent/model.ts +11 -2
- package/examples/playground/app/agent/server.ts +8 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
- package/examples/playground/app/chat/model.ts +2 -2
- package/examples/playground/app/chat/server.ts +24 -17
- package/examples/playground/app/globals.css +179 -0
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +185 -0
- package/src/ai-control-server.ts +829 -0
- package/src/ai-control-state.ts +152 -0
- package/src/ai-control.ts +139 -0
- package/src/ai-coordinator.ts +99 -32
- package/src/ai-progress-batches.ts +68 -0
- package/src/ai-projector.ts +76 -15
- package/src/ai-sdk-step.ts +0 -1
- package/src/ai-server.ts +429 -619
- package/src/ai.ts +553 -108
- package/src/client.ts +31 -9
- package/src/licenses/Apache-2.0.txt +55 -0
- package/src/parse-partial-json.ts +441 -0
- package/src/reducer.ts +6 -0
- package/src/server.ts +8 -4
- package/src/store-postgres.ts +27 -0
- package/src/store-redis-core.ts +53 -1
- package/src/store-redis-notify.ts +1 -0
- package/src/store.ts +6 -0
- package/dist/ai.d.ts.map +0 -1
- package/dist/client-Bf6uSEAk.js.map +0 -1
- package/dist/contract-48bUMgcL.js.map +0 -1
- package/dist/server-BeNADlCI.js.map +0 -1
- package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
package/dist/ai-server.js
CHANGED
|
@@ -1,150 +1,11 @@
|
|
|
1
|
+
import { n as validateSync } from "./validate-XKT4FSNn.js";
|
|
1
2
|
import { t as A2Error } from "./errors-DCk6ch5n.js";
|
|
2
3
|
import { f as consumeSchedulerSendFailure, p as installAmbientToolScopeStorage } from "./internal-Dq2qYxou.js";
|
|
3
|
-
import { i as setServerFetchHooks, t as createServer } from "./server-
|
|
4
|
+
import { i as setServerFetchHooks, t as createServer } from "./server-CjJSGcF7.js";
|
|
5
|
+
import { n as applyControlChanges, r as controlCommit, t as initialControlState } from "./ai-control-CcD4hh3y.js";
|
|
4
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
5
8
|
import { asSchema, convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
|
|
6
|
-
//#region src/ai-sdk-step.ts
|
|
7
|
-
const CONTROLLED_SETTINGS = [
|
|
8
|
-
"_internal",
|
|
9
|
-
"abortSignal",
|
|
10
|
-
"instructions",
|
|
11
|
-
"messages",
|
|
12
|
-
"model",
|
|
13
|
-
"onToolExecutionEnd",
|
|
14
|
-
"onToolExecutionStart",
|
|
15
|
-
"experimental_onToolCallFinish",
|
|
16
|
-
"experimental_onToolCallStart",
|
|
17
|
-
"experimental_sandbox",
|
|
18
|
-
"experimental_toolCallers",
|
|
19
|
-
"experimental_toolApprovalSecret",
|
|
20
|
-
"prompt",
|
|
21
|
-
"prepareStep",
|
|
22
|
-
"stopWhen",
|
|
23
|
-
"system",
|
|
24
|
-
"tools"
|
|
25
|
-
];
|
|
26
|
-
const safeSettings = (settings) => {
|
|
27
|
-
const result = { ...settings };
|
|
28
|
-
for (const key of CONTROLLED_SETTINGS) Reflect.deleteProperty(result, key);
|
|
29
|
-
if (typeof result.timeout === "object" && result.timeout !== null) {
|
|
30
|
-
const timeout = { ...result.timeout };
|
|
31
|
-
Reflect.deleteProperty(timeout, "toolMs");
|
|
32
|
-
Reflect.deleteProperty(timeout, "tools");
|
|
33
|
-
result.timeout = timeout;
|
|
34
|
-
}
|
|
35
|
-
return result;
|
|
36
|
-
};
|
|
37
|
-
const modelToolSet = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
|
|
38
|
-
if (tool.type === "provider" && tool.isProviderExecuted === true) return [name, tool];
|
|
39
|
-
const definition = { ...tool };
|
|
40
|
-
Reflect.deleteProperty(definition, "execute");
|
|
41
|
-
return [name, definition];
|
|
42
|
-
}));
|
|
43
|
-
const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
44
|
-
/** Run exactly one AI SDK model step while leaving local tool execution to A2. */
|
|
45
|
-
async function generateAISDKStep(input) {
|
|
46
|
-
const settings = safeSettings(input.settings);
|
|
47
|
-
const tools = modelToolSet(input.tools);
|
|
48
|
-
const messages = input.modelMessages ?? await convertToModelMessages(input.messages, { tools });
|
|
49
|
-
const streamOptions = {
|
|
50
|
-
...settings,
|
|
51
|
-
model: input.model,
|
|
52
|
-
tools,
|
|
53
|
-
messages,
|
|
54
|
-
abortSignal: input.abortSignal,
|
|
55
|
-
...input.instructions === void 0 ? {} : { instructions: input.instructions },
|
|
56
|
-
stopWhen: stepCountIs(1),
|
|
57
|
-
onError: settings.onError ?? (() => {})
|
|
58
|
-
};
|
|
59
|
-
const result = streamText(streamOptions);
|
|
60
|
-
let completion;
|
|
61
|
-
let fatalError;
|
|
62
|
-
const observed = result.stream.pipeThrough(new TransformStream({ transform(part, controller) {
|
|
63
|
-
if (part.type === "error" && fatalError === void 0) fatalError = { value: part.error };
|
|
64
|
-
else if (part.type === "finish") completion = {
|
|
65
|
-
finishReason: part.finishReason,
|
|
66
|
-
usage: part.totalUsage
|
|
67
|
-
};
|
|
68
|
-
controller.enqueue(part);
|
|
69
|
-
} }));
|
|
70
|
-
return {
|
|
71
|
-
stream: toUIMessageStream({
|
|
72
|
-
stream: observed,
|
|
73
|
-
tools,
|
|
74
|
-
originalMessages: input.messages,
|
|
75
|
-
generateMessageId: () => input.responseMessageId,
|
|
76
|
-
onError: errorMessage$1,
|
|
77
|
-
...input.messageMetadata === void 0 ? {} : { messageMetadata: input.messageMetadata }
|
|
78
|
-
}),
|
|
79
|
-
completion() {
|
|
80
|
-
if (fatalError !== void 0) throw fatalError.value;
|
|
81
|
-
if (completion === void 0) throw new Error("AI SDK step ended without a finish part");
|
|
82
|
-
if (completion.finishReason === "error") throw new Error("AI SDK step finished with an error");
|
|
83
|
-
return completion;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
//#endregion
|
|
88
|
-
//#region src/ai-model-metadata.ts
|
|
89
|
-
const catalogUrl = "https://ai-gateway.vercel.sh/v1/models";
|
|
90
|
-
const catalogLifetimeMs = 36e5;
|
|
91
|
-
const catalogTimeoutMs = 5e3;
|
|
92
|
-
let catalog;
|
|
93
|
-
const gatewayModelId = (model) => {
|
|
94
|
-
if (typeof model === "string") return globalThis.AI_SDK_DEFAULT_PROVIDER === void 0 ? model : void 0;
|
|
95
|
-
return model.provider === "gateway" ? model.modelId : void 0;
|
|
96
|
-
};
|
|
97
|
-
const readCatalog = () => {
|
|
98
|
-
if (catalog && catalog.expiresAt > Date.now()) return catalog.promise;
|
|
99
|
-
const entry = {
|
|
100
|
-
expiresAt: Number.POSITIVE_INFINITY,
|
|
101
|
-
promise: (async () => {
|
|
102
|
-
const controller = new AbortController();
|
|
103
|
-
const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("model catalog request timed out")), catalogTimeoutMs);
|
|
104
|
-
timeout.unref?.();
|
|
105
|
-
try {
|
|
106
|
-
const response = await fetch(catalogUrl, { signal: controller.signal });
|
|
107
|
-
if (!response.ok) throw new Error(`model catalog request failed: HTTP ${response.status}`);
|
|
108
|
-
const body = await response.json();
|
|
109
|
-
if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) throw new Error("model catalog response must contain a data array");
|
|
110
|
-
const models = /* @__PURE__ */ new Map();
|
|
111
|
-
for (const row of body.data) {
|
|
112
|
-
if (typeof row !== "object" || row === null || !("type" in row) || !("id" in row) || !("context_window" in row) || !("max_tokens" in row) || row.type !== "language" || typeof row.id !== "string" || typeof row.context_window !== "number" || typeof row.max_tokens !== "number" || !Number.isSafeInteger(row.context_window) || row.context_window <= 0 || !Number.isSafeInteger(row.max_tokens) || row.max_tokens <= 0) continue;
|
|
113
|
-
models.set(row.id, {
|
|
114
|
-
contextWindow: row.context_window,
|
|
115
|
-
maxOutputTokens: row.max_tokens
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
return models;
|
|
119
|
-
} finally {
|
|
120
|
-
clearTimeout(timeout);
|
|
121
|
-
}
|
|
122
|
-
})()
|
|
123
|
-
};
|
|
124
|
-
catalog = entry;
|
|
125
|
-
entry.promise = entry.promise.then((models) => {
|
|
126
|
-
entry.expiresAt = Date.now() + catalogLifetimeMs;
|
|
127
|
-
return models;
|
|
128
|
-
}, (error) => {
|
|
129
|
-
if (catalog === entry) catalog = void 0;
|
|
130
|
-
throw error;
|
|
131
|
-
});
|
|
132
|
-
return entry.promise;
|
|
133
|
-
};
|
|
134
|
-
const readModelLimits = async (options) => {
|
|
135
|
-
options.signal.throwIfAborted();
|
|
136
|
-
let onAbort;
|
|
137
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
138
|
-
onAbort = () => reject(options.signal.reason);
|
|
139
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
140
|
-
});
|
|
141
|
-
try {
|
|
142
|
-
return (await Promise.race([readCatalog(), aborted])).get(options.modelId) ?? null;
|
|
143
|
-
} finally {
|
|
144
|
-
options.signal.removeEventListener("abort", onAbort);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
//#endregion
|
|
148
9
|
//#region src/ai-coordinator.ts
|
|
149
10
|
const updateResponse = (state, update) => state.response === void 0 ? state : {
|
|
150
11
|
...state,
|
|
@@ -156,6 +17,11 @@ const foldCoordinator = (state, event) => {
|
|
|
156
17
|
closed: true,
|
|
157
18
|
queued: []
|
|
158
19
|
};
|
|
20
|
+
case "ai.retry.requested":
|
|
21
|
+
case "ai.compaction.completed": {
|
|
22
|
+
const { calibration: _, ...rest } = state;
|
|
23
|
+
return rest;
|
|
24
|
+
}
|
|
159
25
|
case "ai.message.created": {
|
|
160
26
|
const payload = event.payload;
|
|
161
27
|
const message = payload.message;
|
|
@@ -163,24 +29,24 @@ const foldCoordinator = (state, event) => {
|
|
|
163
29
|
return {
|
|
164
30
|
...state,
|
|
165
31
|
queued: [...state.queued.filter((item) => item.messageId !== message.id), {
|
|
166
|
-
index: event.index,
|
|
167
32
|
messageId: message.id,
|
|
168
33
|
generate: payload.generate !== false
|
|
169
|
-
}]
|
|
34
|
+
}]
|
|
170
35
|
};
|
|
171
36
|
}
|
|
172
37
|
case "ai.generation.requested": {
|
|
173
38
|
const request = event.payload;
|
|
174
39
|
if (request.reason === "message") {
|
|
175
40
|
const responseMessageId = request.responseMessageId ?? `${request.messageId}:assistant`;
|
|
176
|
-
const requested = state.queued.
|
|
41
|
+
const requested = state.queued.findIndex((item) => item.messageId === request.messageId);
|
|
177
42
|
return {
|
|
178
43
|
...state,
|
|
179
|
-
queued: requested
|
|
44
|
+
queued: requested < 0 ? state.queued.filter((item) => item.messageId !== request.messageId) : state.queued.slice(requested + 1),
|
|
180
45
|
response: {
|
|
181
46
|
rootMessageId: request.messageId,
|
|
182
47
|
responseMessageId,
|
|
183
48
|
status: "requested",
|
|
49
|
+
stepCount: 0,
|
|
184
50
|
activeRequestId: event.id,
|
|
185
51
|
calls: [],
|
|
186
52
|
inputs: []
|
|
@@ -189,14 +55,25 @@ const foldCoordinator = (state, event) => {
|
|
|
189
55
|
}
|
|
190
56
|
if (state.response === void 0 || request.responseMessageId !== state.response.responseMessageId) return state;
|
|
191
57
|
if (request.reason === "tool" && (!continuationReady(state) || event.id !== `ai.generate:tools:${state.response.generation?.generationId}`) || request.reason === "retry" && state.response.status !== "failed" || request.reason === "input" && state.response.inputResponse === void 0) return state;
|
|
192
|
-
return updateResponse(state, (response) =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
58
|
+
return updateResponse(state, (response) => {
|
|
59
|
+
const generation = response.generation ?? response.source?.generation;
|
|
60
|
+
const promptThroughIndex = response.promptThroughIndex ?? response.source?.promptThroughIndex;
|
|
61
|
+
return {
|
|
62
|
+
rootMessageId: response.rootMessageId,
|
|
63
|
+
responseMessageId: response.responseMessageId,
|
|
64
|
+
status: "requested",
|
|
65
|
+
stepCount: response.stepCount,
|
|
66
|
+
activeRequestId: event.id,
|
|
67
|
+
...generation === void 0 || promptThroughIndex === void 0 ? {} : { source: {
|
|
68
|
+
generation,
|
|
69
|
+
promptThroughIndex,
|
|
70
|
+
failed: response.failure?.generationId === generation.generationId,
|
|
71
|
+
canCompact: response.calls.every((call) => call.terminal)
|
|
72
|
+
} },
|
|
73
|
+
calls: [],
|
|
74
|
+
inputs: []
|
|
75
|
+
};
|
|
76
|
+
});
|
|
200
77
|
}
|
|
201
78
|
case "ai.generation.started": {
|
|
202
79
|
const generation = event.payload;
|
|
@@ -205,7 +82,10 @@ const foldCoordinator = (state, event) => {
|
|
|
205
82
|
rootMessageId: response.rootMessageId,
|
|
206
83
|
responseMessageId: response.responseMessageId,
|
|
207
84
|
status: "generating",
|
|
85
|
+
stepCount: response.stepCount,
|
|
208
86
|
generation,
|
|
87
|
+
...generation.promptThroughIndex === void 0 ? {} : { promptThroughIndex: generation.promptThroughIndex },
|
|
88
|
+
...response.source === void 0 ? {} : { source: response.source },
|
|
209
89
|
activeRequestId: generation.requestId,
|
|
210
90
|
calls: [],
|
|
211
91
|
inputs: []
|
|
@@ -214,11 +94,22 @@ const foldCoordinator = (state, event) => {
|
|
|
214
94
|
case "ai.generation.completed": {
|
|
215
95
|
const completion = event.payload;
|
|
216
96
|
if (state.response?.generation?.generationId !== completion.generationId) return state;
|
|
217
|
-
|
|
97
|
+
const { calibration: _, ...uncalibrated } = updateResponse(state, (response) => ({
|
|
218
98
|
...response,
|
|
219
99
|
status: "waiting",
|
|
220
|
-
completion
|
|
100
|
+
completion,
|
|
101
|
+
stepCount: response.stepCount + (response.completion === void 0 ? 1 : 0)
|
|
221
102
|
}));
|
|
103
|
+
const inputTokens = completion.usage?.inputTokens;
|
|
104
|
+
const estimate = completion.inputTokenEstimate;
|
|
105
|
+
return inputTokens !== void 0 && Number.isFinite(inputTokens) && inputTokens >= 0 && estimate !== void 0 ? {
|
|
106
|
+
...uncalibrated,
|
|
107
|
+
calibration: {
|
|
108
|
+
model: state.response.generation.model,
|
|
109
|
+
inputTokens,
|
|
110
|
+
estimate
|
|
111
|
+
}
|
|
112
|
+
} : uncalibrated;
|
|
222
113
|
}
|
|
223
114
|
case "ai.generation.failed": {
|
|
224
115
|
const failure = event.payload;
|
|
@@ -234,18 +125,16 @@ const foldCoordinator = (state, event) => {
|
|
|
234
125
|
}
|
|
235
126
|
case "ai.message.completed": {
|
|
236
127
|
const messageId = event.payload.messageId;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} : state;
|
|
128
|
+
if (state.response?.responseMessageId !== messageId) return state;
|
|
129
|
+
const { response: _, ...rest } = state;
|
|
130
|
+
return rest;
|
|
241
131
|
}
|
|
242
132
|
case "ai.message.interrupted": {
|
|
243
133
|
const interruption = event.payload;
|
|
244
134
|
const response = state.response;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
} : state;
|
|
135
|
+
if (response?.responseMessageId !== interruption.messageId || interruption.generationId !== response.generation?.generationId && interruption.requestId !== response.activeRequestId) return state;
|
|
136
|
+
const { response: _, ...rest } = state;
|
|
137
|
+
return rest;
|
|
249
138
|
}
|
|
250
139
|
case "ai.tool.called": {
|
|
251
140
|
const call = event.payload;
|
|
@@ -253,7 +142,6 @@ const foldCoordinator = (state, event) => {
|
|
|
253
142
|
return updateResponse(state, (response) => ({
|
|
254
143
|
...response,
|
|
255
144
|
calls: [...response.calls.filter((candidate) => candidate.call.toolCallId !== call.toolCallId), {
|
|
256
|
-
index: event.index,
|
|
257
145
|
call,
|
|
258
146
|
terminal: false
|
|
259
147
|
}]
|
|
@@ -277,8 +165,7 @@ const foldCoordinator = (state, event) => {
|
|
|
277
165
|
...response,
|
|
278
166
|
calls: response.calls.map((candidate) => candidate.approval?.approvalId === approval.approvalId ? {
|
|
279
167
|
...candidate,
|
|
280
|
-
response: approval
|
|
281
|
-
responseIndex: event.index
|
|
168
|
+
response: approval
|
|
282
169
|
} : candidate)
|
|
283
170
|
}));
|
|
284
171
|
}
|
|
@@ -301,34 +188,701 @@ const foldCoordinator = (state, event) => {
|
|
|
301
188
|
inputs: [...response.inputs.filter((candidate) => candidate.messageId !== input.messageId || candidate.generationId !== input.generationId || candidate.inputId !== input.inputId), input]
|
|
302
189
|
}));
|
|
303
190
|
}
|
|
304
|
-
case "ai.input.responded": {
|
|
305
|
-
const input = event.payload;
|
|
306
|
-
const requested = state.response?.inputs.find((candidate) => candidate.messageId === input.messageId && candidate.generationId === input.generationId && candidate.inputId === input.inputId && candidate.name === input.name);
|
|
307
|
-
if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId || requested === void 0) return state;
|
|
308
|
-
return updateResponse(state, (response) => ({
|
|
309
|
-
...response,
|
|
310
|
-
inputs: response.inputs.filter((candidate) => candidate.inputId !== input.inputId),
|
|
311
|
-
inputResponse: {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
191
|
+
case "ai.input.responded": {
|
|
192
|
+
const input = event.payload;
|
|
193
|
+
const requested = state.response?.inputs.find((candidate) => candidate.messageId === input.messageId && candidate.generationId === input.generationId && candidate.inputId === input.inputId && candidate.name === input.name);
|
|
194
|
+
if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId || requested === void 0) return state;
|
|
195
|
+
return updateResponse(state, (response) => ({
|
|
196
|
+
...response,
|
|
197
|
+
inputs: response.inputs.filter((candidate) => candidate.inputId !== input.inputId),
|
|
198
|
+
inputResponse: {
|
|
199
|
+
generationId: input.generationId,
|
|
200
|
+
inputId: input.inputId,
|
|
201
|
+
name: requested.name
|
|
202
|
+
}
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
default: return state;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const aiCoordinatorReducer = (contract) => contract.reducer({
|
|
209
|
+
name: "a2.ai.coordinator.v5",
|
|
210
|
+
initialState: {
|
|
211
|
+
closed: false,
|
|
212
|
+
queued: []
|
|
213
|
+
}
|
|
214
|
+
}).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
|
|
215
|
+
state,
|
|
216
|
+
changes: event.payload.changes.filter((change) => change.path[0] === "coordinator").map((change) => Object.assign({}, change, { path: change.path.slice(1) }))
|
|
217
|
+
}) : state);
|
|
218
|
+
const continuationReady = (state) => {
|
|
219
|
+
const response = state.response;
|
|
220
|
+
return !state.closed && response?.completion?.finishReason === "tool-calls" && response.generation?.generationId === response.completion.generationId && response.failure === void 0 && response.calls.length > 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0);
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/ai-control-server.ts
|
|
224
|
+
var ControlCancelled = class extends Error {};
|
|
225
|
+
const callReadyForModel = (call) => call.terminal || call.call.providerExecuted === true && !call.call.supportsDeferredResults && call.response !== void 0;
|
|
226
|
+
const approvalBlocksExecution = (call) => call.approval !== void 0 && call.response?.approved !== true;
|
|
227
|
+
const createControlRuntime = (options) => {
|
|
228
|
+
const reducer = options.agent.contract.reducer({
|
|
229
|
+
name: "a2.ai.control.v1",
|
|
230
|
+
initialState: initialControlState()
|
|
231
|
+
}).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
|
|
232
|
+
state,
|
|
233
|
+
changes: event.payload.changes
|
|
234
|
+
}) : state);
|
|
235
|
+
const coordinator = aiCoordinatorReducer(options.agent.contract);
|
|
236
|
+
const handler = async (ctx) => {
|
|
237
|
+
const snapshot = await ctx.session.state(reducer, { through: "latest" });
|
|
238
|
+
const before = structuredClone(snapshot.state);
|
|
239
|
+
const moves = [];
|
|
240
|
+
const state = structuredClone(snapshot.state);
|
|
241
|
+
const facts = [];
|
|
242
|
+
const jobs = [];
|
|
243
|
+
const emit = ({ type, payload, id = `${ctx.event.id}:fact:${facts.length}` }) => {
|
|
244
|
+
const fact = {
|
|
245
|
+
type,
|
|
246
|
+
payload,
|
|
247
|
+
id
|
|
248
|
+
};
|
|
249
|
+
facts.push(fact);
|
|
250
|
+
state.coordinator = foldCoordinator(state.coordinator, {
|
|
251
|
+
...ctx.event,
|
|
252
|
+
...fact
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
let reason;
|
|
256
|
+
const end = ({ lastSeenIndex, reason: interruptionReason }) => {
|
|
257
|
+
const response = state.coordinator.response;
|
|
258
|
+
if (response === void 0) return;
|
|
259
|
+
const owner = response.generation === void 0 ? { requestId: response.activeRequestId } : { generationId: response.generation.generationId };
|
|
260
|
+
emit({
|
|
261
|
+
type: "ai.message.interrupted",
|
|
262
|
+
payload: {
|
|
263
|
+
messageId: response.responseMessageId,
|
|
264
|
+
...owner,
|
|
265
|
+
...interruptionReason === void 0 ? {} : { reason: interruptionReason },
|
|
266
|
+
...lastSeenIndex === void 0 ? {} : { lastSeenIndex }
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
state.active = null;
|
|
270
|
+
};
|
|
271
|
+
const knownInput = (id) => state.inbox.items.some((candidate) => candidate.id === id) || Object.hasOwn(state.reservedIds, id);
|
|
272
|
+
const missingInputReason = (id) => state.reservedIds[id] === "removed" || !knownInput(id) ? "not-found" : "already-active";
|
|
273
|
+
const inputTaken = ({ message, generate = true }) => knownInput(message.id) || generate && message.role === "user" && knownInput(`${message.id}:assistant`);
|
|
274
|
+
const insert = ({ message, generate = true, first = false, afterStepOf }) => {
|
|
275
|
+
const candidate = {
|
|
276
|
+
id: message.id,
|
|
277
|
+
revision: 0,
|
|
278
|
+
message,
|
|
279
|
+
generate: generate && message.role === "user",
|
|
280
|
+
...afterStepOf === void 0 ? {} : { afterStepOf }
|
|
281
|
+
};
|
|
282
|
+
state.reservedIds = {
|
|
283
|
+
...state.reservedIds,
|
|
284
|
+
[candidate.id]: true,
|
|
285
|
+
...candidate.generate ? { [`${candidate.id}:assistant`]: true } : {}
|
|
286
|
+
};
|
|
287
|
+
if (first) state.inbox.items.unshift(candidate);
|
|
288
|
+
else state.inbox.items.push(candidate);
|
|
289
|
+
};
|
|
290
|
+
const moveInput = ({ from, to }) => {
|
|
291
|
+
if (from === to) return;
|
|
292
|
+
const [moved] = state.inbox.items.splice(from, 1);
|
|
293
|
+
state.inbox.items.splice(to, 0, moved);
|
|
294
|
+
const [previous] = before.inbox.items.splice(from, 1);
|
|
295
|
+
before.inbox.items.splice(to, 0, previous);
|
|
296
|
+
moves.push({
|
|
297
|
+
path: ["inbox", "items"],
|
|
298
|
+
move: {
|
|
299
|
+
from,
|
|
300
|
+
to
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
const requestModel = ({ request, id }) => {
|
|
305
|
+
const active = state.active;
|
|
306
|
+
const payload = {
|
|
307
|
+
...request,
|
|
308
|
+
control: {
|
|
309
|
+
turnId: active.turnId,
|
|
310
|
+
version: active.version
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
state.coordinator = foldCoordinator(state.coordinator, {
|
|
314
|
+
...ctx.event,
|
|
315
|
+
type: "ai.generation.requested",
|
|
316
|
+
id,
|
|
317
|
+
payload
|
|
318
|
+
});
|
|
319
|
+
active.model = {
|
|
320
|
+
id,
|
|
321
|
+
version: active.version,
|
|
322
|
+
settled: false
|
|
323
|
+
};
|
|
324
|
+
active.phase = "starting";
|
|
325
|
+
jobs.push({
|
|
326
|
+
type: "ai.generation.requested",
|
|
327
|
+
id,
|
|
328
|
+
payload
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
if (ctx.event.type === "ai.session.closed") {
|
|
332
|
+
state.coordinator = {
|
|
333
|
+
closed: true,
|
|
334
|
+
queued: []
|
|
335
|
+
};
|
|
336
|
+
state.active = null;
|
|
337
|
+
state.inbox.items = [];
|
|
338
|
+
} else if (ctx.event.type === "ai.control.requested") {
|
|
339
|
+
const command = ctx.event.payload;
|
|
340
|
+
if (state.coordinator.closed) reason = "closed";
|
|
341
|
+
else switch (command.action) {
|
|
342
|
+
case "send":
|
|
343
|
+
if (inputTaken({
|
|
344
|
+
message: command.message,
|
|
345
|
+
generate: command.generate !== false
|
|
346
|
+
})) reason = "duplicate-input";
|
|
347
|
+
else insert({
|
|
348
|
+
message: command.message,
|
|
349
|
+
generate: command.generate !== false
|
|
350
|
+
});
|
|
351
|
+
break;
|
|
352
|
+
case "edit": {
|
|
353
|
+
const item = state.inbox.items.find((candidate) => candidate.id === command.message.id);
|
|
354
|
+
if (item === void 0) reason = missingInputReason(command.message.id);
|
|
355
|
+
else if (item.revision !== command.expectedRevision) reason = "revision-conflict";
|
|
356
|
+
else if (item.message.role !== command.message.role) reason = "revision-conflict";
|
|
357
|
+
else {
|
|
358
|
+
item.message = command.message;
|
|
359
|
+
item.revision += 1;
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case "remove": {
|
|
364
|
+
const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
|
|
365
|
+
if (position < 0) reason = missingInputReason(command.inputId);
|
|
366
|
+
else {
|
|
367
|
+
state.inbox.items.splice(position, 1);
|
|
368
|
+
state.reservedIds = {
|
|
369
|
+
...state.reservedIds,
|
|
370
|
+
[command.inputId]: "removed"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "move": {
|
|
376
|
+
const index = state.inbox.items.findIndex((candidate) => candidate.id === command.inputId);
|
|
377
|
+
if (index < 0) reason = missingInputReason(command.inputId);
|
|
378
|
+
else if (command.beforeId !== null && !state.inbox.items.some((candidate) => candidate.id === command.beforeId)) reason = "not-found";
|
|
379
|
+
else {
|
|
380
|
+
const destination = command.beforeId === null ? state.inbox.items.length : state.inbox.items.findIndex((item) => item.id === command.beforeId);
|
|
381
|
+
moveInput({
|
|
382
|
+
from: index,
|
|
383
|
+
to: destination > index ? destination - 1 : destination
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
case "send-now": {
|
|
389
|
+
const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
|
|
390
|
+
const selected = state.inbox.items[position];
|
|
391
|
+
if (!selected) reason = missingInputReason(command.inputId);
|
|
392
|
+
else if ((state.active?.turnId ?? null) !== command.turnId) reason = "stale-turn";
|
|
393
|
+
else if (selected.message.role !== "user") reason = "not-user-input";
|
|
394
|
+
else if (!selected.generate && knownInput(`${selected.id}:assistant`)) reason = "duplicate-input";
|
|
395
|
+
else {
|
|
396
|
+
if (!selected.generate) {
|
|
397
|
+
state.reservedIds = {
|
|
398
|
+
...state.reservedIds,
|
|
399
|
+
[`${selected.id}:assistant`]: true
|
|
400
|
+
};
|
|
401
|
+
selected.generate = true;
|
|
402
|
+
}
|
|
403
|
+
if (state.active) selected.afterStepOf = state.active.turnId;
|
|
404
|
+
else delete selected.afterStepOf;
|
|
405
|
+
moveInput({
|
|
406
|
+
from: position,
|
|
407
|
+
to: 0
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "stop":
|
|
413
|
+
if (state.active?.turnId !== command.turnId) reason = "stale-turn";
|
|
414
|
+
else end({
|
|
415
|
+
lastSeenIndex: command.lastSeenIndex,
|
|
416
|
+
reason: void 0
|
|
417
|
+
});
|
|
418
|
+
break;
|
|
419
|
+
case "steer":
|
|
420
|
+
if (inputTaken({ message: command.message })) reason = "duplicate-input";
|
|
421
|
+
else insert({
|
|
422
|
+
message: command.message,
|
|
423
|
+
generate: true,
|
|
424
|
+
first: true,
|
|
425
|
+
afterStepOf: command.turnId
|
|
426
|
+
});
|
|
427
|
+
break;
|
|
428
|
+
case "interrupt": {
|
|
429
|
+
const response = state.coordinator.response;
|
|
430
|
+
if (!response || response.responseMessageId !== command.request.messageId || command.request.generationId !== response.generation?.generationId && command.request.requestId !== response.activeRequestId) reason = "stale-turn";
|
|
431
|
+
else end({
|
|
432
|
+
lastSeenIndex: command.request.lastSeenIndex,
|
|
433
|
+
reason: command.request.reason
|
|
434
|
+
});
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case "pause": {
|
|
438
|
+
state.inbox.paused = true;
|
|
439
|
+
const active = state.active;
|
|
440
|
+
if (command.when === "now" && active !== null && !active.suspended) {
|
|
441
|
+
active.suspended = true;
|
|
442
|
+
active.version += 1;
|
|
443
|
+
const generation = state.coordinator.response?.generation;
|
|
444
|
+
if (generation) emit({
|
|
445
|
+
type: "ai.generation.paused",
|
|
446
|
+
payload: {
|
|
447
|
+
generationId: generation.generationId,
|
|
448
|
+
messageId: generation.responseMessageId
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case "resume":
|
|
455
|
+
state.inbox.paused = false;
|
|
456
|
+
break;
|
|
457
|
+
case "tool-result":
|
|
458
|
+
if (!state.coordinator.response?.calls.find((candidate) => candidate.call.generationId === command.result.generationId && candidate.call.messageId === command.result.messageId && candidate.call.toolCallId === command.result.toolCallId && candidate.call.providerExecuted === true && !candidate.terminal)) reason = "not-found";
|
|
459
|
+
else emit({
|
|
460
|
+
type: "ai.tool.result",
|
|
461
|
+
payload: command.result
|
|
462
|
+
});
|
|
463
|
+
break;
|
|
464
|
+
case "approval": {
|
|
465
|
+
const response = state.coordinator.response;
|
|
466
|
+
const call = response?.calls.find((candidate) => candidate.approval?.approvalId === command.response.approvalId && candidate.approval.generationId === command.response.generationId && candidate.approval.messageId === command.response.messageId);
|
|
467
|
+
if (response?.failure) reason = "stale-turn";
|
|
468
|
+
else if (!call || call.response || call.terminal) reason = "not-found";
|
|
469
|
+
else emit({
|
|
470
|
+
type: "ai.approval.responded",
|
|
471
|
+
payload: command.response
|
|
472
|
+
});
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case "request-input":
|
|
476
|
+
case "input": {
|
|
477
|
+
const payload = command.action === "input" ? command.response : command.request;
|
|
478
|
+
const response = state.coordinator.response;
|
|
479
|
+
if (response?.failure !== void 0 || response?.generation?.generationId !== payload.generationId || response.responseMessageId !== payload.messageId || command.action === "input" && !response.inputs.some((input) => input.inputId === payload.inputId && input.name === payload.name)) reason = "not-found";
|
|
480
|
+
else emit({
|
|
481
|
+
type: command.action === "input" ? "ai.input.responded" : "ai.input.requested",
|
|
482
|
+
payload
|
|
483
|
+
});
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case "retry": {
|
|
487
|
+
const response = state.coordinator.response;
|
|
488
|
+
if (!state.active || response?.status !== "failed" || response.rootMessageId !== command.request.messageId || response.responseMessageId !== command.request.responseMessageId) reason = "stale-turn";
|
|
489
|
+
else if (!state.inbox.paused && !state.active.suspended) {
|
|
490
|
+
emit({
|
|
491
|
+
type: "ai.retry.requested",
|
|
492
|
+
payload: command.request
|
|
493
|
+
});
|
|
494
|
+
requestModel({
|
|
495
|
+
request: {
|
|
496
|
+
messageId: response.rootMessageId,
|
|
497
|
+
responseMessageId: response.responseMessageId,
|
|
498
|
+
reason: "retry"
|
|
499
|
+
},
|
|
500
|
+
id: `ai.generate:retry:${command.request.retryId}`
|
|
501
|
+
});
|
|
502
|
+
} else reason = "stale-turn";
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
const report = ctx.event.payload;
|
|
508
|
+
const active = state.active;
|
|
509
|
+
const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === report.workId);
|
|
510
|
+
const work = report.kind === "model" ? active?.model : call?.work;
|
|
511
|
+
if (!(active?.turnId === report.turnId && work?.id === report.workId && work.version === report.version && !work.settled && report.attempt >= (work.attempt ?? 0))) reason = "stale-work";
|
|
512
|
+
else {
|
|
513
|
+
work.attempt = report.attempt;
|
|
514
|
+
const authorized = !active.suspended && active.version === report.version;
|
|
515
|
+
for (const event of report.events) {
|
|
516
|
+
if (work.facts?.includes(event.id)) continue;
|
|
517
|
+
if (authorized || report.kind === "tool" && event.type === "ai.tool.result") {
|
|
518
|
+
emit({
|
|
519
|
+
type: event.type,
|
|
520
|
+
payload: event.payload,
|
|
521
|
+
id: event.id
|
|
522
|
+
});
|
|
523
|
+
work.facts = [...work.facts ?? [], event.id];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (report.settled) work.settled = true;
|
|
527
|
+
if (!authorized) reason = "stale-work";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
let active = state.active;
|
|
531
|
+
let response = state.coordinator.response;
|
|
532
|
+
if (active && response === void 0) {
|
|
533
|
+
state.active = null;
|
|
534
|
+
active = null;
|
|
535
|
+
}
|
|
536
|
+
if (active && response) {
|
|
537
|
+
const handoff = state.inbox.items.some((item) => item.afterStepOf === active.turnId);
|
|
538
|
+
const outstanding = active.model !== null && !active.model.settled || response.calls.some((candidate) => candidate.work !== void 0 && !candidate.work.settled);
|
|
539
|
+
if (active.suspended) {
|
|
540
|
+
active.phase = outstanding ? "pausing" : "paused";
|
|
541
|
+
if (!state.inbox.paused && !outstanding && !handoff) active.suspended = false;
|
|
542
|
+
}
|
|
543
|
+
if (!active.suspended && response.failure) active.phase = "failed";
|
|
544
|
+
if (!active.suspended && response.failure === void 0) for (const call of response.calls) {
|
|
545
|
+
if (call.terminal || call.work !== void 0 && (!call.work.settled || call.work.version === active.version) || call.call.providerExecuted === true || call.approval && !call.response) continue;
|
|
546
|
+
if (call.response?.approved === false) {
|
|
547
|
+
emit({
|
|
548
|
+
type: "ai.tool.result",
|
|
549
|
+
payload: {
|
|
550
|
+
...call.call,
|
|
551
|
+
denied: true,
|
|
552
|
+
phase: "execution"
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const id = `${ctx.event.id}:tool:${call.call.toolCallId}`;
|
|
558
|
+
call.work = {
|
|
559
|
+
id,
|
|
560
|
+
version: active.version,
|
|
561
|
+
settled: false
|
|
562
|
+
};
|
|
563
|
+
jobs.push({
|
|
564
|
+
type: "ai.tool.execution.requested",
|
|
565
|
+
id,
|
|
566
|
+
payload: {
|
|
567
|
+
turnId: active.turnId,
|
|
568
|
+
version: active.version,
|
|
569
|
+
call: call.call,
|
|
570
|
+
generation: response.generation
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
response = state.coordinator.response;
|
|
575
|
+
const callsDone = response.calls.every(callReadyForModel);
|
|
576
|
+
const modelDone = active.model === null || active.model.settled;
|
|
577
|
+
const workSettled = modelDone && response.calls.every((call) => call.work === void 0 || call.work.settled);
|
|
578
|
+
const externalPending = response.calls.some((call) => call.call.providerExecuted && call.call.supportsDeferredResults && !call.terminal && !approvalBlocksExecution(call));
|
|
579
|
+
const handoffReady = handoff && workSettled && !externalPending && (active.suspended || response.failure !== void 0 || response.calls.every((call) => callReadyForModel(call) || approvalBlocksExecution(call)));
|
|
580
|
+
const readyForModel = !active.suspended && response.failure === void 0 && modelDone && callsDone && response.inputs.length === 0;
|
|
581
|
+
const naturalCompletion = readyForModel && response.completion !== void 0 && response.completion.finishReason !== "tool-calls";
|
|
582
|
+
if (handoffReady && !naturalCompletion) end({
|
|
583
|
+
lastSeenIndex: void 0,
|
|
584
|
+
reason: "steered after step"
|
|
585
|
+
});
|
|
586
|
+
else if (readyForModel && (!handoff || handoffReady)) {
|
|
587
|
+
const resume = active.model !== null && active.model.version !== active.version;
|
|
588
|
+
const input = response.inputResponse;
|
|
589
|
+
if (!handoff && (resume || continuationReady(state.coordinator) || response.completion && input)) {
|
|
590
|
+
const nextReason = resume ? "resume" : input ? "input" : "tool";
|
|
591
|
+
const id = nextReason === "tool" ? `ai.generate:tools:${response.generation.generationId}` : `${ctx.event.id}:generate:${nextReason}`;
|
|
592
|
+
requestModel({
|
|
593
|
+
request: {
|
|
594
|
+
messageId: response.responseMessageId,
|
|
595
|
+
responseMessageId: response.responseMessageId,
|
|
596
|
+
reason: nextReason
|
|
597
|
+
},
|
|
598
|
+
id
|
|
599
|
+
});
|
|
600
|
+
} else if (naturalCompletion) {
|
|
601
|
+
emit({
|
|
602
|
+
type: "ai.message.completed",
|
|
603
|
+
payload: { messageId: response.responseMessageId }
|
|
604
|
+
});
|
|
605
|
+
state.active = null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (!active.suspended && state.active && jobs.every((job) => job.type !== "ai.generation.requested")) state.active.phase = response.failure ? "failed" : !modelDone ? "generating" : !callsDone ? "tools" : "waiting";
|
|
609
|
+
}
|
|
610
|
+
if (state.active === null && !state.inbox.paused && !state.coordinator.closed) while (state.inbox.items.length > 0) {
|
|
611
|
+
const item = state.inbox.items.shift();
|
|
612
|
+
delete item.afterStepOf;
|
|
613
|
+
emit({
|
|
614
|
+
type: "ai.message.created",
|
|
615
|
+
payload: {
|
|
616
|
+
message: item.message,
|
|
617
|
+
generate: item.generate
|
|
618
|
+
},
|
|
619
|
+
id: `ai.admitted:${item.id}`
|
|
620
|
+
});
|
|
621
|
+
if (!item.generate) continue;
|
|
622
|
+
state.active = {
|
|
623
|
+
turnId: item.id,
|
|
624
|
+
input: item,
|
|
625
|
+
phase: "starting",
|
|
626
|
+
version: 0,
|
|
627
|
+
suspended: false,
|
|
628
|
+
model: null
|
|
629
|
+
};
|
|
630
|
+
requestModel({
|
|
631
|
+
request: {
|
|
632
|
+
messageId: item.id,
|
|
633
|
+
reason: "message"
|
|
634
|
+
},
|
|
635
|
+
id: `ai.generate:message:${item.id}`
|
|
636
|
+
});
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
const receipt = {
|
|
640
|
+
commandId: ctx.event.id,
|
|
641
|
+
...ctx.event.type === "ai.work.reported" ? { work: true } : {},
|
|
642
|
+
outcome: reason === void 0 ? "applied" : "rejected",
|
|
643
|
+
...reason === void 0 ? {} : { reason }
|
|
644
|
+
};
|
|
645
|
+
return [
|
|
646
|
+
...facts,
|
|
647
|
+
{
|
|
648
|
+
type: "ai.control.committed",
|
|
649
|
+
payload: controlCommit({
|
|
650
|
+
before,
|
|
651
|
+
after: state,
|
|
652
|
+
moves
|
|
653
|
+
})
|
|
654
|
+
},
|
|
655
|
+
...jobs,
|
|
656
|
+
{
|
|
657
|
+
type: "ai.control.decided",
|
|
658
|
+
payload: receipt
|
|
659
|
+
}
|
|
660
|
+
];
|
|
661
|
+
};
|
|
662
|
+
const report = ({ ctx, events, settled, name }) => {
|
|
663
|
+
const payload = ctx.event.payload;
|
|
664
|
+
const { turnId, version } = "turnId" in payload ? payload : payload.control;
|
|
665
|
+
for (const event of events) {
|
|
666
|
+
const schema = options.agent.contract.events[event.type];
|
|
667
|
+
const result = validateSync(schema, event.payload, `AI worker ${event.type}`);
|
|
668
|
+
if (result.issues) throw new TypeError(result.issues.map((issue) => issue.message).join("; "));
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
type: "ai.work.reported",
|
|
672
|
+
id: `${ctx.event.id}:report:${ctx.attempt}:${name}`,
|
|
673
|
+
payload: {
|
|
674
|
+
workId: ctx.event.id,
|
|
675
|
+
attempt: ctx.attempt,
|
|
676
|
+
turnId,
|
|
677
|
+
version,
|
|
678
|
+
kind: ctx.event.type === "ai.generation.requested" ? "model" : "tool",
|
|
679
|
+
settled,
|
|
680
|
+
events: events.filter((event) => event.type !== "ai.message.completed" && event.type !== "ai.message.interrupted").map((event, index) => ({
|
|
681
|
+
type: event.type,
|
|
682
|
+
payload: event.payload,
|
|
683
|
+
id: event.id ?? `${ctx.event.id}:fact:${ctx.attempt}:${name}:${index}`
|
|
684
|
+
}))
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
};
|
|
688
|
+
const append = async ({ ctx, name, events }) => {
|
|
689
|
+
const progress = events.filter((event) => event.type === "ai.generation.progress");
|
|
690
|
+
const lifecycle = events.filter((event) => event.type !== "ai.generation.progress");
|
|
691
|
+
if (lifecycle.length === 0) return ctx.session.append(name, ...progress);
|
|
692
|
+
const input = report({
|
|
693
|
+
ctx,
|
|
694
|
+
events: lifecycle,
|
|
695
|
+
settled: false,
|
|
696
|
+
name
|
|
697
|
+
});
|
|
698
|
+
const persisted = await ctx.session.append(name, ...progress, input);
|
|
699
|
+
const index = persisted.at(-1).index;
|
|
700
|
+
const stream = ctx.session.stream({ startAfter: index })[Symbol.asyncIterator]();
|
|
701
|
+
const canonical = [];
|
|
702
|
+
const wanted = new Set(lifecycle.map((event) => event.id));
|
|
703
|
+
let rejectAbort;
|
|
704
|
+
const aborted = new Promise((_, reject) => {
|
|
705
|
+
rejectAbort = reject;
|
|
706
|
+
});
|
|
707
|
+
const onAbort = () => {
|
|
708
|
+
rejectAbort(ctx.signal.reason);
|
|
709
|
+
};
|
|
710
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
711
|
+
try {
|
|
712
|
+
ctx.signal.throwIfAborted();
|
|
713
|
+
for (;;) {
|
|
714
|
+
const item = await Promise.race([stream.next(), aborted]);
|
|
715
|
+
if (item.done) throw new Error("AI control acknowledgement stream ended");
|
|
716
|
+
if (wanted.has(item.value.id)) canonical.push(item.value);
|
|
717
|
+
if (item.value.type === "ai.control.decided" && item.value.payload.commandId === input.id) {
|
|
718
|
+
if (item.value.payload.outcome === "rejected") throw new ControlCancelled("stale AI work");
|
|
719
|
+
return [...persisted.filter((event) => event.type !== "ai.work.reported"), ...canonical];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} finally {
|
|
723
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
724
|
+
await stream.return?.();
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const settled = ({ ctx, events }) => report({
|
|
728
|
+
ctx,
|
|
729
|
+
events: events === void 0 ? [] : Array.isArray(events) ? events : [events],
|
|
730
|
+
settled: true,
|
|
731
|
+
name: "settled"
|
|
732
|
+
});
|
|
733
|
+
const cancelled = ({ error, signal }) => {
|
|
734
|
+
if (signal.reason instanceof A2Error) throw signal.reason;
|
|
735
|
+
return error instanceof ControlCancelled || signal.aborted;
|
|
736
|
+
};
|
|
737
|
+
return {
|
|
738
|
+
handler,
|
|
739
|
+
append,
|
|
740
|
+
settled,
|
|
741
|
+
cancelled,
|
|
742
|
+
reducer,
|
|
743
|
+
coordinator
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/ai-sdk-step.ts
|
|
748
|
+
const CONTROLLED_SETTINGS = [
|
|
749
|
+
"_internal",
|
|
750
|
+
"abortSignal",
|
|
751
|
+
"instructions",
|
|
752
|
+
"messages",
|
|
753
|
+
"model",
|
|
754
|
+
"onToolExecutionEnd",
|
|
755
|
+
"onToolExecutionStart",
|
|
756
|
+
"experimental_onToolCallFinish",
|
|
757
|
+
"experimental_onToolCallStart",
|
|
758
|
+
"experimental_sandbox",
|
|
759
|
+
"experimental_toolCallers",
|
|
760
|
+
"experimental_toolApprovalSecret",
|
|
761
|
+
"prompt",
|
|
762
|
+
"prepareStep",
|
|
763
|
+
"stopWhen",
|
|
764
|
+
"system",
|
|
765
|
+
"tools"
|
|
766
|
+
];
|
|
767
|
+
const safeSettings = (settings) => {
|
|
768
|
+
const result = { ...settings };
|
|
769
|
+
for (const key of CONTROLLED_SETTINGS) Reflect.deleteProperty(result, key);
|
|
770
|
+
if (typeof result.timeout === "object" && result.timeout !== null) {
|
|
771
|
+
const timeout = { ...result.timeout };
|
|
772
|
+
Reflect.deleteProperty(timeout, "toolMs");
|
|
773
|
+
Reflect.deleteProperty(timeout, "tools");
|
|
774
|
+
result.timeout = timeout;
|
|
775
|
+
}
|
|
776
|
+
return result;
|
|
777
|
+
};
|
|
778
|
+
const modelToolSet = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
|
|
779
|
+
if (tool.type === "provider" && tool.isProviderExecuted === true) return [name, tool];
|
|
780
|
+
const definition = { ...tool };
|
|
781
|
+
Reflect.deleteProperty(definition, "execute");
|
|
782
|
+
return [name, definition];
|
|
783
|
+
}));
|
|
784
|
+
const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
785
|
+
/** Run exactly one AI SDK model step while leaving local tool execution to A2. */
|
|
786
|
+
async function generateAISDKStep(input) {
|
|
787
|
+
const settings = safeSettings(input.settings);
|
|
788
|
+
const tools = modelToolSet(input.tools);
|
|
789
|
+
const messages = input.modelMessages ?? await convertToModelMessages(input.messages, { tools });
|
|
790
|
+
const streamOptions = {
|
|
791
|
+
...settings,
|
|
792
|
+
model: input.model,
|
|
793
|
+
tools,
|
|
794
|
+
messages,
|
|
795
|
+
abortSignal: input.abortSignal,
|
|
796
|
+
...input.instructions === void 0 ? {} : { instructions: input.instructions },
|
|
797
|
+
stopWhen: stepCountIs(1),
|
|
798
|
+
onError: settings.onError ?? (() => {})
|
|
799
|
+
};
|
|
800
|
+
const result = streamText(streamOptions);
|
|
801
|
+
let completion;
|
|
802
|
+
let fatalError;
|
|
803
|
+
const observed = result.stream.pipeThrough(new TransformStream({ transform(part, controller) {
|
|
804
|
+
if (part.type === "error" && fatalError === void 0) fatalError = { value: part.error };
|
|
805
|
+
else if (part.type === "finish") completion = {
|
|
806
|
+
finishReason: part.finishReason,
|
|
807
|
+
usage: part.totalUsage
|
|
808
|
+
};
|
|
809
|
+
controller.enqueue(part);
|
|
810
|
+
} }));
|
|
811
|
+
return {
|
|
812
|
+
stream: toUIMessageStream({
|
|
813
|
+
stream: observed,
|
|
814
|
+
tools,
|
|
815
|
+
generateMessageId: () => input.responseMessageId,
|
|
816
|
+
onError: errorMessage$1,
|
|
817
|
+
...input.messageMetadata === void 0 ? {} : { messageMetadata: input.messageMetadata }
|
|
818
|
+
}),
|
|
819
|
+
completion() {
|
|
820
|
+
if (fatalError !== void 0) throw fatalError.value;
|
|
821
|
+
if (completion === void 0) throw new Error("AI SDK step ended without a finish part");
|
|
822
|
+
if (completion.finishReason === "error") throw new Error("AI SDK step finished with an error");
|
|
823
|
+
return completion;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/ai-model-metadata.ts
|
|
829
|
+
const catalogUrl = "https://ai-gateway.vercel.sh/v1/models";
|
|
830
|
+
const catalogLifetimeMs = 36e5;
|
|
831
|
+
const catalogTimeoutMs = 5e3;
|
|
832
|
+
let catalog;
|
|
833
|
+
const gatewayModelId = (model) => {
|
|
834
|
+
if (typeof model === "string") return globalThis.AI_SDK_DEFAULT_PROVIDER === void 0 ? model : void 0;
|
|
835
|
+
return model.provider === "gateway" ? model.modelId : void 0;
|
|
836
|
+
};
|
|
837
|
+
const readCatalog = () => {
|
|
838
|
+
if (catalog && catalog.expiresAt > Date.now()) return catalog.promise;
|
|
839
|
+
const entry = {
|
|
840
|
+
expiresAt: Number.POSITIVE_INFINITY,
|
|
841
|
+
promise: (async () => {
|
|
842
|
+
const controller = new AbortController();
|
|
843
|
+
const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("model catalog request timed out")), catalogTimeoutMs);
|
|
844
|
+
timeout.unref?.();
|
|
845
|
+
try {
|
|
846
|
+
const response = await fetch(catalogUrl, { signal: controller.signal });
|
|
847
|
+
if (!response.ok) throw new Error(`model catalog request failed: HTTP ${response.status}`);
|
|
848
|
+
const body = await response.json();
|
|
849
|
+
if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) throw new Error("model catalog response must contain a data array");
|
|
850
|
+
const models = /* @__PURE__ */ new Map();
|
|
851
|
+
for (const row of body.data) {
|
|
852
|
+
if (typeof row !== "object" || row === null || !("type" in row) || !("id" in row) || !("context_window" in row) || !("max_tokens" in row) || row.type !== "language" || typeof row.id !== "string" || typeof row.context_window !== "number" || typeof row.max_tokens !== "number" || !Number.isSafeInteger(row.context_window) || row.context_window <= 0 || !Number.isSafeInteger(row.max_tokens) || row.max_tokens <= 0) continue;
|
|
853
|
+
models.set(row.id, {
|
|
854
|
+
contextWindow: row.context_window,
|
|
855
|
+
maxOutputTokens: row.max_tokens
|
|
856
|
+
});
|
|
316
857
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
858
|
+
return models;
|
|
859
|
+
} finally {
|
|
860
|
+
clearTimeout(timeout);
|
|
861
|
+
}
|
|
862
|
+
})()
|
|
863
|
+
};
|
|
864
|
+
catalog = entry;
|
|
865
|
+
entry.promise = entry.promise.then((models) => {
|
|
866
|
+
entry.expiresAt = Date.now() + catalogLifetimeMs;
|
|
867
|
+
return models;
|
|
868
|
+
}, (error) => {
|
|
869
|
+
if (catalog === entry) catalog = void 0;
|
|
870
|
+
throw error;
|
|
871
|
+
});
|
|
872
|
+
return entry.promise;
|
|
321
873
|
};
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
874
|
+
const readModelLimits = async (options) => {
|
|
875
|
+
options.signal.throwIfAborted();
|
|
876
|
+
let onAbort;
|
|
877
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
878
|
+
onAbort = () => reject(options.signal.reason);
|
|
879
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
880
|
+
});
|
|
881
|
+
try {
|
|
882
|
+
return (await Promise.race([readCatalog(), aborted])).get(options.modelId) ?? null;
|
|
883
|
+
} finally {
|
|
884
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
327
885
|
}
|
|
328
|
-
}).fold((state, event) => foldCoordinator(state, event));
|
|
329
|
-
const continuationReady = (state) => {
|
|
330
|
-
const response = state.response;
|
|
331
|
-
return !state.closed && response?.completion?.finishReason === "tool-calls" && response.generation?.generationId === response.completion.generationId && response.failure === void 0 && response.calls.length > 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0);
|
|
332
886
|
};
|
|
333
887
|
//#endregion
|
|
334
888
|
//#region src/ai-server.ts
|
|
@@ -340,9 +894,11 @@ const continuationReady = (state) => {
|
|
|
340
894
|
*/
|
|
341
895
|
function validateAgentIngress(context) {
|
|
342
896
|
const rejected = context.events.find((event) => {
|
|
343
|
-
if (event.type !== "ai.
|
|
344
|
-
const
|
|
345
|
-
|
|
897
|
+
if (event.type !== "ai.control.requested") return true;
|
|
898
|
+
const command = event.payload;
|
|
899
|
+
if (command["action"] === "request-input" || command["action"] === "tool-result") return true;
|
|
900
|
+
if (command["action"] === "send" || command["action"] === "edit" || command["action"] === "steer") return command["message"]?.role !== "user";
|
|
901
|
+
return false;
|
|
346
902
|
});
|
|
347
903
|
if (rejected !== void 0) throw new A2Error("INVALID_PAYLOAD", `event '${rejected.type}' is server-only for an AI agent`);
|
|
348
904
|
}
|
|
@@ -396,7 +952,23 @@ const nextOrTimer = async (next, delayMs) => {
|
|
|
396
952
|
if (timer !== void 0) clearTimeout(timer);
|
|
397
953
|
}
|
|
398
954
|
};
|
|
399
|
-
async function generateWithAISDK(
|
|
955
|
+
async function generateWithAISDK(options) {
|
|
956
|
+
const { agentName, context, messageMetadata } = options;
|
|
957
|
+
const headers = context.generation.headers ?? {};
|
|
958
|
+
const generation = gatewayModelId(context.model) === void 0 ? context.generation : {
|
|
959
|
+
...context.generation,
|
|
960
|
+
headers: Object.keys(headers).some((key) => key.toLowerCase() === "x-session-affinity") ? headers : {
|
|
961
|
+
...headers,
|
|
962
|
+
"x-session-affinity": createHash("sha256").update(JSON.stringify([agentName, context.request.sessionId])).digest("hex")
|
|
963
|
+
},
|
|
964
|
+
providerOptions: {
|
|
965
|
+
...context.generation.providerOptions,
|
|
966
|
+
gateway: {
|
|
967
|
+
caching: "auto",
|
|
968
|
+
...context.generation.providerOptions?.["gateway"]
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
};
|
|
400
972
|
const step = await generateAISDKStep({
|
|
401
973
|
model: context.model,
|
|
402
974
|
tools: context.tools,
|
|
@@ -405,9 +977,10 @@ async function generateWithAISDK(context, messageMetadata) {
|
|
|
405
977
|
responseMessageId: context.responseMessageId,
|
|
406
978
|
abortSignal: context.signal,
|
|
407
979
|
...context.instructions === void 0 ? {} : { instructions: context.instructions },
|
|
408
|
-
settings:
|
|
980
|
+
settings: generation,
|
|
409
981
|
...messageMetadata === void 0 ? {} : { messageMetadata: ({ part }) => messageMetadata({
|
|
410
982
|
...context,
|
|
983
|
+
generation,
|
|
411
984
|
part
|
|
412
985
|
}) }
|
|
413
986
|
});
|
|
@@ -491,9 +1064,9 @@ async function* consumeGeneration(options) {
|
|
|
491
1064
|
...completion?.usage === void 0 ? {} : { usage: completion.usage }
|
|
492
1065
|
};
|
|
493
1066
|
}
|
|
494
|
-
const
|
|
495
|
-
let state =
|
|
496
|
-
for (const event of
|
|
1067
|
+
const foldAIEvents = (options) => {
|
|
1068
|
+
let state = options.state;
|
|
1069
|
+
for (const event of options.events) state = options.agent.reducer.fold(state, event);
|
|
497
1070
|
return state;
|
|
498
1071
|
};
|
|
499
1072
|
const summarize = async (options) => {
|
|
@@ -525,15 +1098,19 @@ const summarize = async (options) => {
|
|
|
525
1098
|
return [name, definition];
|
|
526
1099
|
}));
|
|
527
1100
|
const source = await generateWithAISDK({
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1101
|
+
agentName: options.agentName,
|
|
1102
|
+
context: {
|
|
1103
|
+
...options.context,
|
|
1104
|
+
generation,
|
|
1105
|
+
tools,
|
|
1106
|
+
modelMessages: [...options.context.modelMessages, {
|
|
1107
|
+
role: "user",
|
|
1108
|
+
content: instruction
|
|
1109
|
+
}],
|
|
1110
|
+
responseMessageId: `${options.context.generationId}:summary`
|
|
1111
|
+
},
|
|
1112
|
+
messageMetadata: void 0
|
|
1113
|
+
});
|
|
537
1114
|
let summary = "";
|
|
538
1115
|
let calledTool = false;
|
|
539
1116
|
let finish;
|
|
@@ -558,35 +1135,50 @@ const activeCompaction = (state) => {
|
|
|
558
1135
|
const compaction = state.compaction;
|
|
559
1136
|
return compaction?.status === "completed" && compaction.messages !== void 0 && state.messages.some((message) => message.id === compaction.throughMessageId) ? compaction : null;
|
|
560
1137
|
};
|
|
561
|
-
const contextMessages = (options) => {
|
|
562
|
-
const { agent,
|
|
1138
|
+
const contextMessages = async (options) => {
|
|
1139
|
+
const { agent, session, snapshot } = options;
|
|
1140
|
+
const appended = options.appended ?? [];
|
|
1141
|
+
const state = foldAIEvents({
|
|
1142
|
+
agent,
|
|
1143
|
+
events: appended,
|
|
1144
|
+
state: snapshot.state
|
|
1145
|
+
});
|
|
563
1146
|
const compaction = activeCompaction(state);
|
|
564
|
-
|
|
1147
|
+
const queued = new Set(options.coordinator.queued.map((item) => item.messageId));
|
|
1148
|
+
const visible = (messages) => messages.filter((message) => !queued.has(message.id));
|
|
1149
|
+
if (compaction === null) return visible(state.messages);
|
|
565
1150
|
const retained = new Set(compaction.retainedMessageIds ?? []);
|
|
566
|
-
const
|
|
567
|
-
const throughIndex = compaction.throughIndex ?? (startedIndex === void 0 ? void 0 : startedIndex - 1);
|
|
1151
|
+
const throughIndex = compaction.throughIndex;
|
|
568
1152
|
if (throughIndex !== void 0) {
|
|
569
|
-
const prefix =
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
for (const
|
|
578
|
-
|
|
1153
|
+
const [prefix, priorCoordinator, tail] = await Promise.all([
|
|
1154
|
+
throughIndex === snapshot.index ? Promise.resolve(snapshot) : session.state(agent.reducer, { through: throughIndex }),
|
|
1155
|
+
session.state(aiCoordinatorReducer(agent.contract), { through: throughIndex }),
|
|
1156
|
+
throughIndex >= snapshot.index ? Promise.resolve([]) : session.history({
|
|
1157
|
+
gte: throughIndex + 1,
|
|
1158
|
+
lte: snapshot.index
|
|
1159
|
+
})
|
|
1160
|
+
]);
|
|
1161
|
+
for (const item of priorCoordinator.state.queued) retained.add(item.messageId);
|
|
1162
|
+
const events = options.excluded === void 0 ? tail : withoutGenerationLifecycle(tail, options.excluded);
|
|
1163
|
+
const context = foldAIEvents({
|
|
1164
|
+
agent,
|
|
1165
|
+
events: [...events, ...appended],
|
|
1166
|
+
state: {
|
|
1167
|
+
...prefix.state,
|
|
1168
|
+
messages: [...compaction.messages, ...prefix.state.messages.filter((message) => retained.has(message.id))],
|
|
1169
|
+
activeProjection: null
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1173
|
+
for (const message of [...compaction.messages, ...state.messages]) if (!positions.has(message.id)) positions.set(message.id, positions.size);
|
|
1174
|
+
return visible(context.messages.toSorted((left, right) => (positions.get(left.id) ?? positions.size) - (positions.get(right.id) ?? positions.size)));
|
|
579
1175
|
}
|
|
580
1176
|
const boundary = state.messages.findIndex((message) => message.id === compaction.throughMessageId);
|
|
581
|
-
return [
|
|
1177
|
+
return visible([
|
|
582
1178
|
...compaction.messages,
|
|
583
1179
|
...state.messages.slice(0, boundary + 1).filter((message) => retained.has(message.id)),
|
|
584
1180
|
...state.messages.slice(boundary + 1)
|
|
585
|
-
];
|
|
586
|
-
};
|
|
587
|
-
const activeContextMessages = (options) => {
|
|
588
|
-
const queued = new Set(options.coordinator.queued.map((item) => item.messageId));
|
|
589
|
-
return contextMessages(options).filter((message) => !queued.has(message.id));
|
|
1181
|
+
]);
|
|
590
1182
|
};
|
|
591
1183
|
const modelContext = async (options) => {
|
|
592
1184
|
const messages = await convertToModelMessages(options.messages, { tools: options.tools });
|
|
@@ -609,22 +1201,8 @@ const estimateInputTokens = async (options) => {
|
|
|
609
1201
|
})).byteLength / 4);
|
|
610
1202
|
};
|
|
611
1203
|
const measuredInputTokens = (options) => {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (event.type !== "ai.generation.completed") continue;
|
|
615
|
-
const completed = event.payload;
|
|
616
|
-
const started = options.history.find((candidate) => candidate.type === "ai.generation.started" && candidate.payload.generationId === completed.generationId);
|
|
617
|
-
const input = completed.usage?.inputTokens;
|
|
618
|
-
const estimate = completed.inputTokenEstimate;
|
|
619
|
-
if ((started?.payload)?.model !== options.model) break;
|
|
620
|
-
if (input === void 0 || estimate === void 0 || !Number.isFinite(input) || input < 0 || options.estimate < estimate) break;
|
|
621
|
-
return Math.max(options.estimate, Math.ceil(input + options.estimate - estimate));
|
|
622
|
-
}
|
|
623
|
-
return options.estimate;
|
|
624
|
-
};
|
|
625
|
-
const generationRequestId = (generationId) => {
|
|
626
|
-
const markerIndex = generationId.lastIndexOf(":generation:");
|
|
627
|
-
return markerIndex === -1 ? void 0 : generationId.slice(0, markerIndex);
|
|
1204
|
+
const previous = options.calibration;
|
|
1205
|
+
return previous !== void 0 && previous.model === options.model && options.estimate >= previous.estimate ? Math.max(options.estimate, Math.ceil(previous.inputTokens + options.estimate - previous.estimate)) : options.estimate;
|
|
628
1206
|
};
|
|
629
1207
|
const toolCalledEvent = (payload) => ({
|
|
630
1208
|
type: "ai.tool.called",
|
|
@@ -793,29 +1371,6 @@ const lifecycleEvents = (options) => {
|
|
|
793
1371
|
pending
|
|
794
1372
|
};
|
|
795
1373
|
};
|
|
796
|
-
function queuedMessagesAt(history) {
|
|
797
|
-
const queued = [];
|
|
798
|
-
for (const event of history) {
|
|
799
|
-
if (event.type === "ai.message.created") {
|
|
800
|
-
const payload = event.payload;
|
|
801
|
-
if (payload.message.role !== "user") continue;
|
|
802
|
-
const duplicate = queued.findIndex((item) => item.messageId === payload.message.id);
|
|
803
|
-
if (duplicate !== -1) queued.splice(duplicate, 1);
|
|
804
|
-
queued.push({
|
|
805
|
-
index: event.index,
|
|
806
|
-
messageId: payload.message.id,
|
|
807
|
-
generate: payload.generate !== false
|
|
808
|
-
});
|
|
809
|
-
continue;
|
|
810
|
-
}
|
|
811
|
-
if (event.type !== "ai.generation.requested") continue;
|
|
812
|
-
const request = event.payload;
|
|
813
|
-
if (request.reason !== "message") continue;
|
|
814
|
-
const requested = queued.findIndex((item) => item.messageId === request.messageId);
|
|
815
|
-
if (requested !== -1) queued.splice(0, requested + 1);
|
|
816
|
-
}
|
|
817
|
-
return queued;
|
|
818
|
-
}
|
|
819
1374
|
function withoutGenerationLifecycle(history, generationIds) {
|
|
820
1375
|
if (generationIds.size === 0) return history;
|
|
821
1376
|
const approvalKeys = /* @__PURE__ */ new Set();
|
|
@@ -839,7 +1394,7 @@ const validateCompaction = (options) => {
|
|
|
839
1394
|
if (compaction !== false && (typeof compaction !== "object" || compaction === null)) throw new TypeError("compaction must resolve to false or a policy object");
|
|
840
1395
|
if (compaction !== false && "then" in compaction) {
|
|
841
1396
|
Promise.resolve(compaction).catch(() => {});
|
|
842
|
-
throw new TypeError("compaction options must
|
|
1397
|
+
throw new TypeError("compaction options must be a policy or a resolver");
|
|
843
1398
|
}
|
|
844
1399
|
if (compaction !== false && !("shouldCompact" in compaction)) {
|
|
845
1400
|
if (compaction.thresholdTokens !== void 0 && (!Number.isSafeInteger(compaction.thresholdTokens) || compaction.thresholdTokens < 1)) throw new TypeError("compaction.thresholdTokens must be a positive safe integer");
|
|
@@ -871,58 +1426,9 @@ function createHandlers(options) {
|
|
|
871
1426
|
const tools = options.tools ?? {};
|
|
872
1427
|
const generation = options.generation ?? {};
|
|
873
1428
|
const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
|
|
874
|
-
const
|
|
1429
|
+
const control = createControlRuntime({ agent: options.agent });
|
|
1430
|
+
const coordinator = control.coordinator;
|
|
875
1431
|
const promptCache = /* @__PURE__ */ new Map();
|
|
876
|
-
const coordinatorStateAt = (history, frontier = Number.POSITIVE_INFINITY) => {
|
|
877
|
-
let state = coordinator.initialState;
|
|
878
|
-
for (const event of history) {
|
|
879
|
-
if (event.index >= frontier) break;
|
|
880
|
-
state = coordinator.fold(state, event);
|
|
881
|
-
}
|
|
882
|
-
return state;
|
|
883
|
-
};
|
|
884
|
-
const requestForNextMessage = (state) => {
|
|
885
|
-
if (state.closed || state.response !== void 0) return void 0;
|
|
886
|
-
const next = state.queued.find((item) => item.generate !== false);
|
|
887
|
-
if (next === void 0) return void 0;
|
|
888
|
-
return {
|
|
889
|
-
type: "ai.generation.requested",
|
|
890
|
-
id: `ai.generate:message:${next.messageId}`,
|
|
891
|
-
payload: {
|
|
892
|
-
messageId: next.messageId,
|
|
893
|
-
reason: "message"
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
};
|
|
897
|
-
const scheduleNext = async (ctx) => requestForNextMessage((await ctx.session.state(coordinator, { through: "latest" })).state);
|
|
898
|
-
const continueIfReady = async (ctx, generationId) => {
|
|
899
|
-
const state = (await ctx.session.state(coordinator, { through: "latest" })).state;
|
|
900
|
-
const response = state.response;
|
|
901
|
-
if (response?.generation?.generationId !== generationId) return;
|
|
902
|
-
const input = response.inputResponse;
|
|
903
|
-
if (response.completion?.generationId === generationId && response.failure === void 0 && input !== void 0 && response.inputs.length === 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0)) {
|
|
904
|
-
await ctx.session.append("continue-after-input", {
|
|
905
|
-
type: "ai.generation.requested",
|
|
906
|
-
id: `ai.generate:input:${encodeURIComponent(response.responseMessageId)}:${encodeURIComponent(input.generationId)}:${encodeURIComponent(input.inputId)}`,
|
|
907
|
-
payload: {
|
|
908
|
-
messageId: response.responseMessageId,
|
|
909
|
-
responseMessageId: response.responseMessageId,
|
|
910
|
-
reason: "input"
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (!continuationReady(state)) return;
|
|
916
|
-
await ctx.session.append("continue-after-tools", {
|
|
917
|
-
type: "ai.generation.requested",
|
|
918
|
-
id: `ai.generate:tools:${generationId}`,
|
|
919
|
-
payload: {
|
|
920
|
-
messageId: response.responseMessageId,
|
|
921
|
-
responseMessageId: response.responseMessageId,
|
|
922
|
-
reason: "tool"
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
};
|
|
926
1432
|
const resultEvent = (call, suffix, result) => ({
|
|
927
1433
|
type: "ai.tool.result",
|
|
928
1434
|
id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
|
|
@@ -943,22 +1449,35 @@ function createHandlers(options) {
|
|
|
943
1449
|
...call.toolMetadata === void 0 ? {} : { toolMetadata: call.toolMetadata }
|
|
944
1450
|
}
|
|
945
1451
|
});
|
|
946
|
-
const promptMessages = (
|
|
947
|
-
const
|
|
1452
|
+
const promptMessages = (input) => {
|
|
1453
|
+
const { ctx, call, generation: owner } = input;
|
|
1454
|
+
const key = promptCacheKey(ctx.event.sessionId, call.generationId);
|
|
948
1455
|
const cached = promptCache.get(key);
|
|
949
1456
|
if (cached) return cached;
|
|
950
1457
|
const computation = (async () => {
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
1458
|
+
const frontier = owner.promptThroughIndex;
|
|
1459
|
+
if (frontier === void 0) throw new TypeError("AI work requires a prompt checkpoint");
|
|
1460
|
+
const [snapshot, atPrompt, tail] = await Promise.all([
|
|
1461
|
+
ctx.session.state(options.agent.reducer, { through: frontier }),
|
|
1462
|
+
ctx.session.state(coordinator, { through: frontier }),
|
|
1463
|
+
ctx.session.history({
|
|
1464
|
+
gte: frontier + 1,
|
|
1465
|
+
lte: ctx.event.index
|
|
1466
|
+
})
|
|
1467
|
+
]);
|
|
1468
|
+
const appended = tail.filter((event) => (event.type === "ai.generation.started" || event.type === "ai.compaction.requested" || event.type === "ai.compaction.completed") && event.payload.generationId === call.generationId);
|
|
1469
|
+
const state = foldAIEvents({
|
|
1470
|
+
agent: options.agent,
|
|
1471
|
+
events: appended,
|
|
1472
|
+
state: snapshot.state
|
|
1473
|
+
});
|
|
956
1474
|
return modelContext({
|
|
957
|
-
messages:
|
|
1475
|
+
messages: await contextMessages({
|
|
958
1476
|
agent: options.agent,
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
coordinator:
|
|
1477
|
+
session: ctx.session,
|
|
1478
|
+
snapshot,
|
|
1479
|
+
coordinator: atPrompt.state,
|
|
1480
|
+
appended
|
|
962
1481
|
}),
|
|
963
1482
|
state,
|
|
964
1483
|
tools
|
|
@@ -974,7 +1493,7 @@ function createHandlers(options) {
|
|
|
974
1493
|
const schedulerFailure = consumeSchedulerSendFailure(error);
|
|
975
1494
|
if (checkAbort(ctx.signal)) return;
|
|
976
1495
|
if (schedulerFailure === "retryable") throw error;
|
|
977
|
-
return resultEvent(call,
|
|
1496
|
+
return resultEvent(call, `${ctx.event.id}:execution:error`, { error: errorMessage(error) });
|
|
978
1497
|
};
|
|
979
1498
|
const runToolExecution = async (ctx, call, execute, messages) => {
|
|
980
1499
|
let output;
|
|
@@ -993,7 +1512,7 @@ function createHandlers(options) {
|
|
|
993
1512
|
} catch (error) {
|
|
994
1513
|
return toolExecutionFailure(ctx, call, error);
|
|
995
1514
|
}
|
|
996
|
-
if (iterator === void 0) return resultEvent(call,
|
|
1515
|
+
if (iterator === void 0) return resultEvent(call, `${ctx.event.id}:execution:0`, { output });
|
|
997
1516
|
let last;
|
|
998
1517
|
let sequence = 0;
|
|
999
1518
|
let done = false;
|
|
@@ -1005,16 +1524,21 @@ function createHandlers(options) {
|
|
|
1005
1524
|
} catch (error) {
|
|
1006
1525
|
return toolExecutionFailure(ctx, call, error);
|
|
1007
1526
|
}
|
|
1008
|
-
if (checkAbort(ctx.signal)) return;
|
|
1009
1527
|
if (result.done) {
|
|
1528
|
+
if (ctx.signal.reason instanceof A2Error) throw ctx.signal.reason;
|
|
1010
1529
|
done = true;
|
|
1011
1530
|
break;
|
|
1012
1531
|
}
|
|
1532
|
+
if (checkAbort(ctx.signal)) return;
|
|
1013
1533
|
last = result.value;
|
|
1014
|
-
await
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1534
|
+
await control.append({
|
|
1535
|
+
ctx,
|
|
1536
|
+
name: `tool:${call.toolCallId}:preliminary:${sequence}`,
|
|
1537
|
+
events: [resultEvent(call, `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`, {
|
|
1538
|
+
output: result.value,
|
|
1539
|
+
preliminary: true
|
|
1540
|
+
})]
|
|
1541
|
+
});
|
|
1018
1542
|
sequence += 1;
|
|
1019
1543
|
}
|
|
1020
1544
|
} finally {
|
|
@@ -1022,12 +1546,13 @@ function createHandlers(options) {
|
|
|
1022
1546
|
await iterator.return?.();
|
|
1023
1547
|
} catch {}
|
|
1024
1548
|
}
|
|
1025
|
-
return resultEvent(call,
|
|
1549
|
+
return resultEvent(call, `${ctx.event.id}:execution:${sequence}:final`, sequence === 0 ? {} : { output: last });
|
|
1026
1550
|
};
|
|
1027
|
-
const executeTool = async (
|
|
1551
|
+
const executeTool = async (input) => {
|
|
1552
|
+
const { ctx, call } = input;
|
|
1028
1553
|
const execute = tools[call.toolName]?.execute;
|
|
1029
|
-
if (execute === void 0) return resultEvent(call,
|
|
1030
|
-
const messages = await promptMessages(
|
|
1554
|
+
if (execute === void 0) return resultEvent(call, `${ctx.event.id}:execution:error`, { error: `Tool '${call.toolName}' has no server executor` });
|
|
1555
|
+
const messages = await promptMessages(input);
|
|
1031
1556
|
if (checkAbort(ctx.signal)) return;
|
|
1032
1557
|
const scope = {
|
|
1033
1558
|
contract: options.agent.contract,
|
|
@@ -1035,62 +1560,82 @@ function createHandlers(options) {
|
|
|
1035
1560
|
};
|
|
1036
1561
|
return toolScopeStorage.run(scope, () => runToolExecution(ctx, call, execute, messages));
|
|
1037
1562
|
};
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1563
|
+
const handleToolExecution = async (ctx) => {
|
|
1564
|
+
try {
|
|
1565
|
+
const state = (await ctx.session.state(control.reducer, { through: "latest" })).state;
|
|
1566
|
+
const request = ctx.event.payload;
|
|
1567
|
+
const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === ctx.event.id);
|
|
1568
|
+
if (state.active?.turnId !== request.turnId || state.active.suspended || state.active.version !== request.version || state.coordinator.response?.failure !== void 0 || !call || call.terminal || call.work?.settled) return control.settled({
|
|
1569
|
+
ctx,
|
|
1570
|
+
events: void 0
|
|
1571
|
+
});
|
|
1572
|
+
return control.settled({
|
|
1573
|
+
ctx,
|
|
1574
|
+
events: await executeTool({
|
|
1575
|
+
ctx,
|
|
1576
|
+
call: request.call,
|
|
1577
|
+
generation: request.generation
|
|
1578
|
+
})
|
|
1579
|
+
});
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
if (control.cancelled({
|
|
1582
|
+
error,
|
|
1583
|
+
signal: ctx.signal
|
|
1584
|
+
})) return control.settled({
|
|
1585
|
+
ctx,
|
|
1586
|
+
events: void 0
|
|
1587
|
+
});
|
|
1588
|
+
throw error;
|
|
1055
1589
|
}
|
|
1056
|
-
if (!ctx.event.payload.approved) return resultEvent(current.call, "execution:denied", { denied: true });
|
|
1057
|
-
return executeTool(ctx, current.call);
|
|
1058
1590
|
};
|
|
1059
1591
|
const generationHandler = async (ctx) => {
|
|
1060
|
-
|
|
1592
|
+
if (checkAbort(ctx.signal)) return;
|
|
1061
1593
|
const requestId = ctx.event.id;
|
|
1062
1594
|
const request = ctx.event.payload;
|
|
1063
|
-
const
|
|
1595
|
+
const snapshot = await ctx.session.state(options.agent.reducer, { through: "latest" });
|
|
1596
|
+
if (snapshot.state.active?.phase === "paused" || snapshot.state.active?.phase === "pausing") return;
|
|
1597
|
+
const coordinatorState = (await ctx.session.state(coordinator, { through: snapshot.index })).state;
|
|
1064
1598
|
if (coordinatorState.closed || coordinatorState.response?.activeRequestId !== requestId) return;
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
return payload.requestId === requestId && payload.superseded !== true;
|
|
1070
|
-
})) return;
|
|
1071
|
-
const previousStarts = history.filter((event) => event.type === "ai.generation.started" && event.payload.requestId === requestId);
|
|
1072
|
-
if (previousStarts.map((event) => event.payload.attempt).some((priorAttempt) => priorAttempt >= ctx.attempt)) return;
|
|
1599
|
+
const response = coordinatorState.response;
|
|
1600
|
+
const current = response.generation?.requestId === requestId ? response.generation : void 0;
|
|
1601
|
+
if (current !== void 0 && (response.completion !== void 0 || response.failure !== void 0 && response.failure.superseded !== true)) return;
|
|
1602
|
+
if (current !== void 0 && current.attempt >= ctx.attempt) return;
|
|
1073
1603
|
const attempt = ctx.attempt;
|
|
1074
1604
|
const generationId = `${requestId}:generation:${attempt}`;
|
|
1075
1605
|
const responseMessageId = request.responseMessageId ?? (request.reason === "message" ? `${request.messageId}:assistant` : request.messageId);
|
|
1076
|
-
const responseStepCount =
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1606
|
+
const responseStepCount = response.stepCount;
|
|
1607
|
+
if (request.reason === "tool" && (requestId !== `ai.generate:tools:${response.source?.generation.generationId}` || response.responseMessageId !== request.messageId || response.responseMessageId !== request.responseMessageId)) return;
|
|
1608
|
+
const previous = current;
|
|
1609
|
+
const replaced = previous === void 0 ? [] : [{
|
|
1610
|
+
generation: previous,
|
|
1611
|
+
frontier: response.promptThroughIndex
|
|
1612
|
+
}];
|
|
1613
|
+
if (request.reason === "retry" && response.source?.failed) replaced.push({
|
|
1614
|
+
generation: response.source.generation,
|
|
1615
|
+
frontier: response.source.promptThroughIndex
|
|
1616
|
+
});
|
|
1617
|
+
const replacedGenerationIds = new Set(replaced.map(({ generation: owner }) => owner.generationId));
|
|
1618
|
+
let state = snapshot.state;
|
|
1619
|
+
if (replaced.length > 0) {
|
|
1620
|
+
const through = Math.min(...replaced.map(({ frontier }) => frontier));
|
|
1621
|
+
const baseline = await ctx.session.state(options.agent.reducer, { through });
|
|
1622
|
+
const tail = await ctx.session.history({
|
|
1623
|
+
gte: through + 1,
|
|
1624
|
+
lte: snapshot.index
|
|
1625
|
+
});
|
|
1626
|
+
state = foldAIEvents({
|
|
1627
|
+
agent: options.agent,
|
|
1628
|
+
events: withoutGenerationLifecycle(tail, replacedGenerationIds),
|
|
1629
|
+
state: baseline.state
|
|
1630
|
+
});
|
|
1082
1631
|
}
|
|
1083
|
-
const previous = previousStarts.filter((event) => event.payload.attempt < attempt).toSorted((left, right) => right.payload.attempt - left.payload.attempt)[0];
|
|
1084
|
-
const incompleteId = previous ? previous.payload.generationId : void 0;
|
|
1085
|
-
const replacedGenerationIds = /* @__PURE__ */ new Set();
|
|
1086
|
-
if (incompleteId !== void 0) replacedGenerationIds.add(incompleteId);
|
|
1087
|
-
if (request.reason === "retry" && sourceCoordinatorState.response?.failure?.generationId !== void 0) replacedGenerationIds.add(sourceCoordinatorState.response.failure.generationId);
|
|
1088
|
-
const promptHistory = withoutGenerationLifecycle(history, replacedGenerationIds);
|
|
1089
|
-
const state = replay(options.agent, promptHistory);
|
|
1090
1632
|
const resolverContext = {
|
|
1091
1633
|
event: ctx.event,
|
|
1092
1634
|
state,
|
|
1093
|
-
|
|
1635
|
+
session: { state: (reducer, readOptions) => ctx.session.state(reducer, {
|
|
1636
|
+
...readOptions,
|
|
1637
|
+
through: readOptions?.through ?? snapshot.index
|
|
1638
|
+
}) },
|
|
1094
1639
|
signal: ctx.signal
|
|
1095
1640
|
};
|
|
1096
1641
|
let generationStarted = false;
|
|
@@ -1101,14 +1646,14 @@ function createHandlers(options) {
|
|
|
1101
1646
|
const resolvedInstructions = options.instructions === void 0 ? void 0 : await resolve(options.instructions, resolverContext);
|
|
1102
1647
|
if (checkAbort(ctx.signal)) return;
|
|
1103
1648
|
const compaction = typeof configuredCompaction === "function" ? validateCompaction({
|
|
1104
|
-
compaction: configuredCompaction
|
|
1649
|
+
compaction: await resolve(configuredCompaction, resolverContext),
|
|
1105
1650
|
explicit: true,
|
|
1106
1651
|
generation: options.generation,
|
|
1107
1652
|
tools: options.tools
|
|
1108
1653
|
}) : staticCompaction;
|
|
1109
1654
|
if (checkAbort(ctx.signal)) return;
|
|
1110
1655
|
if (request.reason === "tool" && responseStepCount >= maxSteps) {
|
|
1111
|
-
const source =
|
|
1656
|
+
const source = response.source?.generation;
|
|
1112
1657
|
if (source === void 0) return;
|
|
1113
1658
|
return {
|
|
1114
1659
|
type: "ai.generation.failed",
|
|
@@ -1130,7 +1675,7 @@ function createHandlers(options) {
|
|
|
1130
1675
|
responseMessageId,
|
|
1131
1676
|
attempt,
|
|
1132
1677
|
model: modelName(resolvedModel),
|
|
1133
|
-
promptThroughIndex:
|
|
1678
|
+
promptThroughIndex: snapshot.index
|
|
1134
1679
|
};
|
|
1135
1680
|
const catalogModelId = gatewayModelId(resolvedModel);
|
|
1136
1681
|
const gatewayOptions = options.generation?.providerOptions?.["gateway"];
|
|
@@ -1143,7 +1688,7 @@ function createHandlers(options) {
|
|
|
1143
1688
|
payload: { modelId: catalogModelId }
|
|
1144
1689
|
});
|
|
1145
1690
|
if (previous) {
|
|
1146
|
-
const payload = previous
|
|
1691
|
+
const payload = previous;
|
|
1147
1692
|
const superseded = {
|
|
1148
1693
|
requestId,
|
|
1149
1694
|
messageId: payload.messageId,
|
|
@@ -1163,20 +1708,25 @@ function createHandlers(options) {
|
|
|
1163
1708
|
id: generationId,
|
|
1164
1709
|
payload: started
|
|
1165
1710
|
});
|
|
1166
|
-
const
|
|
1711
|
+
const generationEvents = await control.append({
|
|
1712
|
+
ctx,
|
|
1713
|
+
name: "generation-start",
|
|
1714
|
+
events: startEvents
|
|
1715
|
+
});
|
|
1167
1716
|
generationStarted = true;
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const
|
|
1171
|
-
...coordinatorStateAt(history),
|
|
1172
|
-
queued: queuedMessagesAt(history)
|
|
1173
|
-
};
|
|
1174
|
-
const messages = activeContextMessages({
|
|
1717
|
+
if (checkAbort(ctx.signal)) return;
|
|
1718
|
+
const promptCoordinatorState = coordinatorState;
|
|
1719
|
+
const messages = await contextMessages({
|
|
1175
1720
|
agent: options.agent,
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1721
|
+
session: ctx.session,
|
|
1722
|
+
snapshot: {
|
|
1723
|
+
state,
|
|
1724
|
+
index: snapshot.index
|
|
1725
|
+
},
|
|
1726
|
+
coordinator: promptCoordinatorState,
|
|
1727
|
+
excluded: replacedGenerationIds
|
|
1179
1728
|
});
|
|
1729
|
+
let generationMessages = messages;
|
|
1180
1730
|
let modelMessages = await modelContext({
|
|
1181
1731
|
messages,
|
|
1182
1732
|
state,
|
|
@@ -1190,7 +1740,7 @@ function createHandlers(options) {
|
|
|
1190
1740
|
messages,
|
|
1191
1741
|
modelMessages,
|
|
1192
1742
|
state,
|
|
1193
|
-
|
|
1743
|
+
session: resolverContext.session,
|
|
1194
1744
|
signal: ctx.signal,
|
|
1195
1745
|
model: resolvedModel,
|
|
1196
1746
|
tools,
|
|
@@ -1203,7 +1753,7 @@ function createHandlers(options) {
|
|
|
1203
1753
|
const limits = metadata?.status === "resolved" && !usesFallbackModels ? metadata.limits : void 0;
|
|
1204
1754
|
let inputTokenEstimate;
|
|
1205
1755
|
let compacted = false;
|
|
1206
|
-
const canCompact =
|
|
1756
|
+
const canCompact = response.source?.canCompact ?? true;
|
|
1207
1757
|
if (policy && canCompact) {
|
|
1208
1758
|
const compactionContext = {
|
|
1209
1759
|
...resolverContext,
|
|
@@ -1221,7 +1771,7 @@ function createHandlers(options) {
|
|
|
1221
1771
|
const inputTokens = measuredInputTokens({
|
|
1222
1772
|
estimate: inputTokenEstimate,
|
|
1223
1773
|
model: modelName(resolvedModel),
|
|
1224
|
-
|
|
1774
|
+
calibration: coordinatorState.calibration
|
|
1225
1775
|
});
|
|
1226
1776
|
const threshold = policy.thresholdTokens ?? (limits === void 0 ? void 0 : Math.floor(Math.min(limits.contextWindow * .75, limits.contextWindow - (generation.maxOutputTokens ?? 0))));
|
|
1227
1777
|
if (limits !== void 0 && threshold !== void 0 && threshold <= 0) throw new Error(`output allowance exhausts the ${limits.contextWindow}-token context window`);
|
|
@@ -1230,19 +1780,24 @@ function createHandlers(options) {
|
|
|
1230
1780
|
if (checkAbort(ctx.signal)) return;
|
|
1231
1781
|
if (shouldCompact) {
|
|
1232
1782
|
const throughMessageId = state.messages.findLast((message) => !promptCoordinatorState.queued.some((queued) => queued.messageId === message.id))?.id ?? request.messageId;
|
|
1233
|
-
const throughIndex =
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1783
|
+
const throughIndex = snapshot.index;
|
|
1784
|
+
generationEvents.push(...await control.append({
|
|
1785
|
+
ctx,
|
|
1786
|
+
name: "compaction-requested",
|
|
1787
|
+
events: [{
|
|
1788
|
+
type: "ai.compaction.requested",
|
|
1789
|
+
id: `${generationId}:compaction:requested`,
|
|
1790
|
+
payload: {
|
|
1791
|
+
generationId,
|
|
1792
|
+
throughMessageId,
|
|
1793
|
+
throughIndex
|
|
1794
|
+
}
|
|
1795
|
+
}]
|
|
1242
1796
|
}));
|
|
1243
1797
|
const result = !("shouldCompact" in policy) ? {
|
|
1244
1798
|
messages: [],
|
|
1245
1799
|
...await summarize({
|
|
1800
|
+
agentName: options.agent.contract.name,
|
|
1246
1801
|
context: baseContext,
|
|
1247
1802
|
instructions: policy.instructions
|
|
1248
1803
|
})
|
|
@@ -1255,23 +1810,33 @@ function createHandlers(options) {
|
|
|
1255
1810
|
...result,
|
|
1256
1811
|
...promptCoordinatorState.queued.length === 0 ? {} : { retainedMessageIds: promptCoordinatorState.queued.map((item) => item.messageId) }
|
|
1257
1812
|
};
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1813
|
+
generationEvents.push(...await control.append({
|
|
1814
|
+
ctx,
|
|
1815
|
+
name: "compaction-completed",
|
|
1816
|
+
events: [{
|
|
1817
|
+
type: "ai.compaction.completed",
|
|
1818
|
+
id: `${generationId}:compaction:completed`,
|
|
1819
|
+
payload: completed
|
|
1820
|
+
}]
|
|
1262
1821
|
}));
|
|
1822
|
+
const queued = new Set(promptCoordinatorState.queued.map((item) => item.messageId));
|
|
1823
|
+
generationMessages = result.messages.filter((message) => !queued.has(message.id));
|
|
1263
1824
|
compacted = true;
|
|
1264
1825
|
}
|
|
1265
1826
|
}
|
|
1266
|
-
const currentState =
|
|
1267
|
-
const currentCoordinatorState = coordinatorStateAt(generationHistory);
|
|
1268
|
-
const generationMessages = activeContextMessages({
|
|
1827
|
+
const currentState = foldAIEvents({
|
|
1269
1828
|
agent: options.agent,
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
coordinator: currentCoordinatorState
|
|
1829
|
+
events: generationEvents,
|
|
1830
|
+
state: snapshot.state
|
|
1273
1831
|
});
|
|
1274
|
-
|
|
1832
|
+
if (request.reason === "retry" || previous !== void 0) generationMessages = await contextMessages({
|
|
1833
|
+
agent: options.agent,
|
|
1834
|
+
session: ctx.session,
|
|
1835
|
+
snapshot,
|
|
1836
|
+
coordinator: promptCoordinatorState,
|
|
1837
|
+
appended: generationEvents
|
|
1838
|
+
});
|
|
1839
|
+
if (policy && "shouldCompact" in policy || generationMessages !== messages || activeCompaction(currentState)?.summary !== activeCompaction(state)?.summary) modelMessages = await modelContext({
|
|
1275
1840
|
messages: generationMessages,
|
|
1276
1841
|
state: currentState,
|
|
1277
1842
|
tools
|
|
@@ -1292,7 +1857,7 @@ function createHandlers(options) {
|
|
|
1292
1857
|
messages: generationMessages,
|
|
1293
1858
|
modelMessages,
|
|
1294
1859
|
state: currentState,
|
|
1295
|
-
|
|
1860
|
+
session: resolverContext.session,
|
|
1296
1861
|
signal: ctx.signal,
|
|
1297
1862
|
model: resolvedModel,
|
|
1298
1863
|
tools,
|
|
@@ -1301,7 +1866,11 @@ function createHandlers(options) {
|
|
|
1301
1866
|
};
|
|
1302
1867
|
const custom = options.generate !== void 0;
|
|
1303
1868
|
const updates = consumeGeneration({
|
|
1304
|
-
source: options.generate === void 0 ? await generateWithAISDK(
|
|
1869
|
+
source: options.generate === void 0 ? await generateWithAISDK({
|
|
1870
|
+
agentName: options.agent.contract.name,
|
|
1871
|
+
context: generateContext,
|
|
1872
|
+
messageMetadata: options.messageMetadata
|
|
1873
|
+
}) : { stream: await options.generate(generateContext) },
|
|
1305
1874
|
...options.progress === void 0 ? {} : { progress: options.progress }
|
|
1306
1875
|
});
|
|
1307
1876
|
let sequence = 0;
|
|
@@ -1332,11 +1901,15 @@ function createHandlers(options) {
|
|
|
1332
1901
|
custom
|
|
1333
1902
|
});
|
|
1334
1903
|
pendingToolCalls = lifecycle.pending;
|
|
1335
|
-
await
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1904
|
+
await control.append({
|
|
1905
|
+
ctx,
|
|
1906
|
+
name: `generation-progress:${sequence}`,
|
|
1907
|
+
events: [{
|
|
1908
|
+
type: "ai.generation.progress",
|
|
1909
|
+
id: `${generationId}:progress:${sequence}`,
|
|
1910
|
+
payload: progress
|
|
1911
|
+
}, ...lifecycle.events]
|
|
1912
|
+
});
|
|
1340
1913
|
sequence += 1;
|
|
1341
1914
|
}
|
|
1342
1915
|
if (!finish) throw new Error("agent generation finished without output");
|
|
@@ -1368,22 +1941,12 @@ function createHandlers(options) {
|
|
|
1368
1941
|
}
|
|
1369
1942
|
];
|
|
1370
1943
|
} catch (error) {
|
|
1944
|
+
if (error instanceof ControlCancelled) throw error;
|
|
1371
1945
|
if (error instanceof A2Error) {
|
|
1372
1946
|
checkAbort(ctx.signal);
|
|
1373
1947
|
throw error;
|
|
1374
1948
|
}
|
|
1375
|
-
if (checkAbort(ctx.signal))
|
|
1376
|
-
const interrupted = {
|
|
1377
|
-
messageId: responseMessageId,
|
|
1378
|
-
generationId,
|
|
1379
|
-
reason: "aborted"
|
|
1380
|
-
};
|
|
1381
|
-
return {
|
|
1382
|
-
type: "ai.message.interrupted",
|
|
1383
|
-
id: `${generationId}:interrupted`,
|
|
1384
|
-
payload: interrupted
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1949
|
+
if (checkAbort(ctx.signal)) return;
|
|
1387
1950
|
if (!generationStarted) throw error;
|
|
1388
1951
|
const failed = {
|
|
1389
1952
|
requestId,
|
|
@@ -1399,36 +1962,10 @@ function createHandlers(options) {
|
|
|
1399
1962
|
};
|
|
1400
1963
|
}
|
|
1401
1964
|
};
|
|
1402
|
-
const handleMessageCreated = async (ctx) => {
|
|
1403
|
-
if (ctx.event.payload.message.role !== "user" || ctx.event.payload.generate === false) return;
|
|
1404
|
-
return scheduleNext(ctx);
|
|
1405
|
-
};
|
|
1406
|
-
const handleRetry = async (ctx) => {
|
|
1407
|
-
const response = (await ctx.session.state(coordinator, { through: "latest" })).state.response;
|
|
1408
|
-
if (response?.status !== "failed" || response.rootMessageId !== ctx.event.payload.messageId || response.responseMessageId !== ctx.event.payload.responseMessageId) return;
|
|
1409
|
-
return {
|
|
1410
|
-
type: "ai.generation.requested",
|
|
1411
|
-
id: `ai.generate:retry:${ctx.event.payload.retryId}`,
|
|
1412
|
-
payload: {
|
|
1413
|
-
messageId: response.rootMessageId,
|
|
1414
|
-
responseMessageId: response.responseMessageId,
|
|
1415
|
-
reason: "retry"
|
|
1416
|
-
}
|
|
1417
|
-
};
|
|
1418
|
-
};
|
|
1419
|
-
const handleInputResponse = async (ctx) => {
|
|
1420
|
-
const response = (await ctx.session.state(coordinator, { through: "latest" })).state.response;
|
|
1421
|
-
if (response?.responseMessageId !== ctx.event.payload.messageId || response.inputResponse?.index !== ctx.event.index || response.inputResponse.generationId !== ctx.event.payload.generationId || response.inputResponse.inputId !== ctx.event.payload.inputId) return;
|
|
1422
|
-
await continueIfReady(ctx, ctx.event.payload.generationId);
|
|
1423
|
-
};
|
|
1424
1965
|
const clearPromptCache = (sessionId) => {
|
|
1425
1966
|
const prefix = `${sessionId}\u001f`;
|
|
1426
1967
|
for (const key of promptCache.keys()) if (key.startsWith(prefix)) promptCache.delete(key);
|
|
1427
1968
|
};
|
|
1428
|
-
const handleResponseEnded = async (ctx) => {
|
|
1429
|
-
clearPromptCache(ctx.event.sessionId);
|
|
1430
|
-
return scheduleNext(ctx);
|
|
1431
|
-
};
|
|
1432
1969
|
return {
|
|
1433
1970
|
"ai.model.metadata.requested": { handler: async (ctx) => {
|
|
1434
1971
|
const limits = await readModelLimits({
|
|
@@ -1445,66 +1982,69 @@ function createHandlers(options) {
|
|
|
1445
1982
|
}
|
|
1446
1983
|
};
|
|
1447
1984
|
} },
|
|
1448
|
-
"ai.
|
|
1449
|
-
lane: "a2.ai.
|
|
1450
|
-
handler:
|
|
1451
|
-
},
|
|
1452
|
-
"ai.retry.requested": {
|
|
1453
|
-
lane: "a2.ai.turn",
|
|
1454
|
-
handler: handleRetry
|
|
1985
|
+
"ai.control.requested": {
|
|
1986
|
+
lane: "a2.ai.control",
|
|
1987
|
+
handler: control.handler
|
|
1455
1988
|
},
|
|
1456
|
-
"ai.
|
|
1457
|
-
lane: "a2.ai.
|
|
1458
|
-
handler:
|
|
1989
|
+
"ai.work.reported": {
|
|
1990
|
+
lane: "a2.ai.control",
|
|
1991
|
+
handler: control.handler
|
|
1459
1992
|
},
|
|
1460
|
-
"ai.message.completed": {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
"ai.message.interrupted": {
|
|
1465
|
-
lane: "a2.ai.turn",
|
|
1466
|
-
handler: handleResponseEnded
|
|
1467
|
-
},
|
|
1468
|
-
"ai.session.closed": { handler: (ctx) => {
|
|
1993
|
+
"ai.message.completed": { handler: async (ctx) => {
|
|
1994
|
+
clearPromptCache(ctx.event.sessionId);
|
|
1995
|
+
} },
|
|
1996
|
+
"ai.message.interrupted": { handler: async (ctx) => {
|
|
1469
1997
|
clearPromptCache(ctx.event.sessionId);
|
|
1470
|
-
return Promise.resolve();
|
|
1471
1998
|
} },
|
|
1999
|
+
"ai.session.closed": {
|
|
2000
|
+
lane: "a2.ai.control",
|
|
2001
|
+
handler: async (ctx) => {
|
|
2002
|
+
clearPromptCache(ctx.event.sessionId);
|
|
2003
|
+
return control.handler(ctx);
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
1472
2006
|
"ai.generation.failed": { handler: (ctx) => {
|
|
1473
2007
|
promptCache.delete(promptCacheKey(ctx.event.sessionId, ctx.event.payload.generationId));
|
|
1474
2008
|
return Promise.resolve();
|
|
1475
2009
|
} },
|
|
1476
2010
|
"ai.generation.requested": {
|
|
1477
|
-
lane: "a2.ai.
|
|
2011
|
+
lane: "a2.ai.model",
|
|
1478
2012
|
abortOn: {
|
|
1479
|
-
"ai.
|
|
1480
|
-
const
|
|
1481
|
-
return
|
|
2013
|
+
"ai.control.committed": (event, trigger) => {
|
|
2014
|
+
const active = event.payload.view;
|
|
2015
|
+
return active?.turnId !== trigger.payload.control?.turnId || active?.version !== trigger.payload.control?.version;
|
|
1482
2016
|
},
|
|
1483
2017
|
"ai.session.closed": true
|
|
1484
2018
|
},
|
|
1485
|
-
handler:
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
2019
|
+
handler: async (ctx) => {
|
|
2020
|
+
try {
|
|
2021
|
+
return control.settled({
|
|
2022
|
+
ctx,
|
|
2023
|
+
events: await generationHandler(ctx)
|
|
2024
|
+
});
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
if (control.cancelled({
|
|
2027
|
+
error,
|
|
2028
|
+
signal: ctx.signal
|
|
2029
|
+
})) return control.settled({
|
|
2030
|
+
ctx,
|
|
2031
|
+
events: void 0
|
|
2032
|
+
});
|
|
2033
|
+
throw error;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1495
2036
|
},
|
|
1496
|
-
"ai.
|
|
2037
|
+
"ai.tool.execution.requested": {
|
|
1497
2038
|
abortOn: {
|
|
1498
|
-
"ai.generation.failed": (event, trigger) => event.payload.
|
|
1499
|
-
"ai.
|
|
2039
|
+
"ai.generation.failed": (event, trigger) => event.payload.generationId === trigger.payload.call.generationId,
|
|
2040
|
+
"ai.control.committed": (event, trigger) => {
|
|
2041
|
+
const active = event.payload.view;
|
|
2042
|
+
return active?.turnId !== trigger.payload.turnId || active?.version !== trigger.payload.version;
|
|
2043
|
+
},
|
|
1500
2044
|
"ai.session.closed": true
|
|
1501
2045
|
},
|
|
1502
|
-
handler:
|
|
1503
|
-
}
|
|
1504
|
-
"ai.tool.result": { handler: async (ctx) => {
|
|
1505
|
-
if (ctx.event.payload.preliminary === true) return;
|
|
1506
|
-
await continueIfReady(ctx, ctx.event.payload.generationId);
|
|
1507
|
-
} }
|
|
2046
|
+
handler: handleToolExecution
|
|
2047
|
+
}
|
|
1508
2048
|
};
|
|
1509
2049
|
}
|
|
1510
2050
|
/** Assemble an A2 server with the built-in agent handlers and app extensions. */
|