@eggjs/agent-runtime 3.73.0-beta.0 → 4.0.2-beta.4
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/LICENSE +21 -0
- package/dist/AgentRuntime.d.ts +46 -0
- package/dist/AgentRuntime.js +298 -0
- package/dist/AgentStoreUtils.d.ts +7 -0
- package/dist/AgentStoreUtils.js +18 -0
- package/dist/HttpSSEWriter.d.ts +20 -0
- package/dist/HttpSSEWriter.js +49 -0
- package/dist/MessageConverter.d.ts +36 -0
- package/dist/MessageConverter.js +123 -0
- package/dist/OSSAgentStore.d.ts +66 -0
- package/dist/OSSAgentStore.js +133 -0
- package/dist/OSSObjectStorageClient.d.ts +51 -0
- package/dist/OSSObjectStorageClient.js +78 -0
- package/dist/RunBuilder.d.ts +47 -0
- package/dist/RunBuilder.js +129 -0
- package/dist/SSEWriter.d.ts +17 -0
- package/dist/SSEWriter.js +1 -0
- package/dist/index.d.ts +10 -10
- package/dist/index.js +11 -31
- package/package.json +35 -25
- package/dist/src/AgentRuntime.d.ts +0 -42
- package/dist/src/AgentRuntime.js +0 -376
- package/dist/src/AgentStore.d.ts +0 -42
- package/dist/src/AgentStore.js +0 -3
- package/dist/src/AgentStoreUtils.d.ts +0 -4
- package/dist/src/AgentStoreUtils.js +0 -23
- package/dist/src/FileAgentStore.d.ts +0 -21
- package/dist/src/FileAgentStore.js +0 -104
- package/dist/src/HttpSSEWriter.d.ts +0 -16
- package/dist/src/HttpSSEWriter.js +0 -51
- package/dist/src/MessageConverter.d.ts +0 -32
- package/dist/src/MessageConverter.js +0 -108
- package/dist/src/OSSAgentStore.d.ts +0 -62
- package/dist/src/OSSAgentStore.js +0 -165
- package/dist/src/OSSObjectStorageClient.d.ts +0 -46
- package/dist/src/OSSObjectStorageClient.js +0 -90
- package/dist/src/RunBuilder.d.ts +0 -43
- package/dist/src/RunBuilder.js +0 -127
- package/dist/src/SSEWriter.d.ts +0 -14
- package/dist/src/SSEWriter.js +0 -3
- package/dist/src/agentDefaults.d.ts +0 -1
- package/dist/src/agentDefaults.js +0 -447
- package/dist/src/enhanceAgentController.d.ts +0 -2
- package/dist/src/enhanceAgentController.js +0 -90
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SSEWriter } from "./SSEWriter.js";
|
|
2
|
+
import { AgentStore, AgentStreamMessage, CreateRunInput, RunObject, ThreadObject, ThreadObjectWithMessages } from "@eggjs/tegg-types/agent-runtime";
|
|
3
|
+
import { EggLogger } from "egg-logger";
|
|
4
|
+
|
|
5
|
+
//#region src/AgentRuntime.d.ts
|
|
6
|
+
declare const AGENT_RUNTIME: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* The executor interface — only requires execRun so the runtime can delegate
|
|
9
|
+
* execution back through the controller's prototype chain (AOP/mock friendly).
|
|
10
|
+
*/
|
|
11
|
+
interface AgentExecutor {
|
|
12
|
+
execRun(input: CreateRunInput, signal?: AbortSignal): AsyncGenerator<AgentStreamMessage>;
|
|
13
|
+
}
|
|
14
|
+
interface AgentRuntimeOptions {
|
|
15
|
+
executor: AgentExecutor;
|
|
16
|
+
store: AgentStore;
|
|
17
|
+
logger: EggLogger;
|
|
18
|
+
}
|
|
19
|
+
declare class AgentRuntime {
|
|
20
|
+
private static readonly TERMINAL_RUN_STATUSES;
|
|
21
|
+
private store;
|
|
22
|
+
private runningTasks;
|
|
23
|
+
private executor;
|
|
24
|
+
private logger;
|
|
25
|
+
constructor(options: AgentRuntimeOptions);
|
|
26
|
+
createThread(): Promise<ThreadObject>;
|
|
27
|
+
getThread(threadId: string): Promise<ThreadObjectWithMessages>;
|
|
28
|
+
private ensureThread;
|
|
29
|
+
syncRun(input: CreateRunInput, signal?: AbortSignal): Promise<RunObject>;
|
|
30
|
+
asyncRun(input: CreateRunInput): Promise<RunObject>;
|
|
31
|
+
streamRun(input: CreateRunInput, writer: SSEWriter): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Consume the execRun async generator, emitting SSE message.delta events
|
|
34
|
+
* for each chunk and accumulating content blocks and token usage.
|
|
35
|
+
*/
|
|
36
|
+
private consumeStreamMessages;
|
|
37
|
+
getRun(runId: string): Promise<RunObject>;
|
|
38
|
+
cancelRun(runId: string): Promise<RunObject>;
|
|
39
|
+
/** Wait for all in-flight background tasks to complete naturally (without aborting). */
|
|
40
|
+
waitForPendingTasks(): Promise<void>;
|
|
41
|
+
destroy(): Promise<void>;
|
|
42
|
+
/** Factory method — avoids the spread-arg type issue with dynamic delegation. */
|
|
43
|
+
static create(options: AgentRuntimeOptions): AgentRuntime;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { AGENT_RUNTIME, AgentExecutor, AgentRuntime, AgentRuntimeOptions };
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { newMsgId } from "./AgentStoreUtils.js";
|
|
2
|
+
import { MessageConverter } from "./MessageConverter.js";
|
|
3
|
+
import { RunBuilder } from "./RunBuilder.js";
|
|
4
|
+
import { AgentConflictError, AgentObjectType, AgentSSEEvent, RunStatus } from "@eggjs/tegg-types/agent-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/AgentRuntime.ts
|
|
7
|
+
const AGENT_RUNTIME = Symbol("agentRuntime");
|
|
8
|
+
var AgentRuntime = class AgentRuntime {
|
|
9
|
+
static TERMINAL_RUN_STATUSES = new Set([
|
|
10
|
+
RunStatus.Completed,
|
|
11
|
+
RunStatus.Failed,
|
|
12
|
+
RunStatus.Cancelled,
|
|
13
|
+
RunStatus.Expired
|
|
14
|
+
]);
|
|
15
|
+
store;
|
|
16
|
+
runningTasks;
|
|
17
|
+
executor;
|
|
18
|
+
logger;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.executor = options.executor;
|
|
21
|
+
this.store = options.store;
|
|
22
|
+
if (!options.logger) throw new Error("AgentRuntimeOptions.logger is required");
|
|
23
|
+
this.logger = options.logger;
|
|
24
|
+
this.runningTasks = /* @__PURE__ */ new Map();
|
|
25
|
+
}
|
|
26
|
+
async createThread() {
|
|
27
|
+
const thread = await this.store.createThread();
|
|
28
|
+
return {
|
|
29
|
+
id: thread.id,
|
|
30
|
+
object: AgentObjectType.Thread,
|
|
31
|
+
createdAt: thread.createdAt,
|
|
32
|
+
metadata: thread.metadata ?? {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async getThread(threadId) {
|
|
36
|
+
const thread = await this.store.getThread(threadId);
|
|
37
|
+
return {
|
|
38
|
+
id: thread.id,
|
|
39
|
+
object: AgentObjectType.Thread,
|
|
40
|
+
createdAt: thread.createdAt,
|
|
41
|
+
metadata: thread.metadata ?? {},
|
|
42
|
+
messages: thread.messages
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async ensureThread(input) {
|
|
46
|
+
if (input.threadId) return {
|
|
47
|
+
threadId: input.threadId,
|
|
48
|
+
input
|
|
49
|
+
};
|
|
50
|
+
const thread = await this.store.createThread();
|
|
51
|
+
return {
|
|
52
|
+
threadId: thread.id,
|
|
53
|
+
input: {
|
|
54
|
+
...input,
|
|
55
|
+
threadId: thread.id
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async syncRun(input, signal) {
|
|
60
|
+
const { threadId, input: resolvedInput } = await this.ensureThread(input);
|
|
61
|
+
input = resolvedInput;
|
|
62
|
+
const run = await this.store.createRun(input.input.messages, threadId, input.config, input.metadata);
|
|
63
|
+
const rb = RunBuilder.create(run, threadId);
|
|
64
|
+
const abortController = new AbortController();
|
|
65
|
+
if (signal) if (signal.aborted) abortController.abort();
|
|
66
|
+
else signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
67
|
+
let resolveTask;
|
|
68
|
+
const taskPromise = new Promise((r) => {
|
|
69
|
+
resolveTask = r;
|
|
70
|
+
});
|
|
71
|
+
this.runningTasks.set(run.id, {
|
|
72
|
+
promise: taskPromise,
|
|
73
|
+
abortController
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
await this.store.updateRun(run.id, rb.start());
|
|
77
|
+
const streamMessages = [];
|
|
78
|
+
for await (const msg of this.executor.execRun(input, abortController.signal)) {
|
|
79
|
+
if (abortController.signal.aborted) {
|
|
80
|
+
const latest = await this.store.getRun(run.id);
|
|
81
|
+
return RunBuilder.fromRecord(latest).snapshot();
|
|
82
|
+
}
|
|
83
|
+
streamMessages.push(msg);
|
|
84
|
+
}
|
|
85
|
+
const { output, usage } = MessageConverter.extractFromStreamMessages(streamMessages, run.id);
|
|
86
|
+
await this.store.appendMessages(threadId, [...MessageConverter.toInputMessageObjects(input.input.messages, threadId), ...output]);
|
|
87
|
+
await this.store.updateRun(run.id, rb.complete(output, usage));
|
|
88
|
+
return rb.snapshot();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (abortController.signal.aborted) {
|
|
91
|
+
const latest = await this.store.getRun(run.id);
|
|
92
|
+
return RunBuilder.fromRecord(latest).snapshot();
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await this.store.updateRun(run.id, rb.fail(err));
|
|
96
|
+
} catch (storeErr) {
|
|
97
|
+
this.logger.error("[AgentRuntime] failed to update run status after syncRun error:", storeErr);
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
} finally {
|
|
101
|
+
resolveTask();
|
|
102
|
+
this.runningTasks.delete(run.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async asyncRun(input) {
|
|
106
|
+
const { threadId, input: resolvedInput } = await this.ensureThread(input);
|
|
107
|
+
input = resolvedInput;
|
|
108
|
+
const run = await this.store.createRun(input.input.messages, threadId, input.config, input.metadata);
|
|
109
|
+
const rb = RunBuilder.create(run, threadId);
|
|
110
|
+
const abortController = new AbortController();
|
|
111
|
+
const queuedSnapshot = rb.snapshot();
|
|
112
|
+
let resolveTask;
|
|
113
|
+
const taskPromise = new Promise((r) => {
|
|
114
|
+
resolveTask = r;
|
|
115
|
+
});
|
|
116
|
+
this.runningTasks.set(run.id, {
|
|
117
|
+
promise: taskPromise,
|
|
118
|
+
abortController
|
|
119
|
+
});
|
|
120
|
+
(async () => {
|
|
121
|
+
try {
|
|
122
|
+
await this.store.updateRun(run.id, rb.start());
|
|
123
|
+
const streamMessages = [];
|
|
124
|
+
for await (const msg of this.executor.execRun(input, abortController.signal)) {
|
|
125
|
+
if (abortController.signal.aborted) return;
|
|
126
|
+
streamMessages.push(msg);
|
|
127
|
+
}
|
|
128
|
+
const currentRun = await this.store.getRun(run.id);
|
|
129
|
+
if (currentRun.status === RunStatus.Cancelling || currentRun.status === RunStatus.Cancelled) return;
|
|
130
|
+
const { output, usage } = MessageConverter.extractFromStreamMessages(streamMessages, run.id);
|
|
131
|
+
await this.store.appendMessages(threadId, [...MessageConverter.toInputMessageObjects(input.input.messages, threadId), ...output]);
|
|
132
|
+
await this.store.updateRun(run.id, rb.complete(output, usage));
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (!abortController.signal.aborted) try {
|
|
135
|
+
const currentRun = await this.store.getRun(run.id);
|
|
136
|
+
if (currentRun.status !== RunStatus.Cancelling && currentRun.status !== RunStatus.Cancelled) await this.store.updateRun(run.id, rb.fail(err));
|
|
137
|
+
} catch (storeErr) {
|
|
138
|
+
this.logger.error("[AgentRuntime] failed to update run status after error:", storeErr);
|
|
139
|
+
}
|
|
140
|
+
else this.logger.error("[AgentRuntime] execRun error during abort:", err);
|
|
141
|
+
} finally {
|
|
142
|
+
resolveTask();
|
|
143
|
+
this.runningTasks.delete(run.id);
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
return queuedSnapshot;
|
|
147
|
+
}
|
|
148
|
+
async streamRun(input, writer) {
|
|
149
|
+
const abortController = new AbortController();
|
|
150
|
+
writer.onClose(() => abortController.abort());
|
|
151
|
+
const { threadId, input: resolvedInput } = await this.ensureThread(input);
|
|
152
|
+
input = resolvedInput;
|
|
153
|
+
const run = await this.store.createRun(input.input.messages, threadId, input.config, input.metadata);
|
|
154
|
+
const rb = RunBuilder.create(run, threadId);
|
|
155
|
+
let resolveTask;
|
|
156
|
+
const taskPromise = new Promise((r) => {
|
|
157
|
+
resolveTask = r;
|
|
158
|
+
});
|
|
159
|
+
this.runningTasks.set(run.id, {
|
|
160
|
+
promise: taskPromise,
|
|
161
|
+
abortController
|
|
162
|
+
});
|
|
163
|
+
writer.writeEvent(AgentSSEEvent.ThreadRunCreated, rb.snapshot());
|
|
164
|
+
await this.store.updateRun(run.id, rb.start());
|
|
165
|
+
writer.writeEvent(AgentSSEEvent.ThreadRunInProgress, rb.snapshot());
|
|
166
|
+
const msgId = newMsgId();
|
|
167
|
+
const msgObj = MessageConverter.createStreamMessage(msgId, run.id);
|
|
168
|
+
writer.writeEvent(AgentSSEEvent.ThreadMessageCreated, msgObj);
|
|
169
|
+
try {
|
|
170
|
+
const { content, usage, aborted } = await this.consumeStreamMessages(input, abortController.signal, writer, msgId);
|
|
171
|
+
if (aborted) {
|
|
172
|
+
rb.cancelling();
|
|
173
|
+
try {
|
|
174
|
+
await this.store.updateRun(run.id, rb.cancel());
|
|
175
|
+
} catch (storeErr) {
|
|
176
|
+
this.logger.error("[AgentRuntime] failed to write cancelled status during stream abort:", storeErr);
|
|
177
|
+
}
|
|
178
|
+
if (!writer.closed) writer.writeEvent(AgentSSEEvent.ThreadRunCancelled, rb.snapshot());
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const completedMsg = MessageConverter.completeMessage(msgObj, content);
|
|
182
|
+
writer.writeEvent(AgentSSEEvent.ThreadMessageCompleted, completedMsg);
|
|
183
|
+
const output = content.length > 0 ? [completedMsg] : [];
|
|
184
|
+
await this.store.appendMessages(threadId, [...MessageConverter.toInputMessageObjects(input.input.messages, threadId), ...output]);
|
|
185
|
+
await this.store.updateRun(run.id, rb.complete(output, usage));
|
|
186
|
+
writer.writeEvent(AgentSSEEvent.ThreadRunCompleted, rb.snapshot());
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (abortController.signal.aborted) {
|
|
189
|
+
rb.cancelling();
|
|
190
|
+
try {
|
|
191
|
+
await this.store.updateRun(run.id, rb.cancel());
|
|
192
|
+
} catch (storeErr) {
|
|
193
|
+
this.logger.error("[AgentRuntime] failed to write cancelled status during stream error:", storeErr);
|
|
194
|
+
}
|
|
195
|
+
if (!writer.closed) writer.writeEvent(AgentSSEEvent.ThreadRunCancelled, rb.snapshot());
|
|
196
|
+
} else {
|
|
197
|
+
try {
|
|
198
|
+
await this.store.updateRun(run.id, rb.fail(err));
|
|
199
|
+
} catch (storeErr) {
|
|
200
|
+
this.logger.error("[AgentRuntime] failed to update run status after error:", storeErr);
|
|
201
|
+
}
|
|
202
|
+
if (!writer.closed) writer.writeEvent(AgentSSEEvent.ThreadRunFailed, rb.snapshot());
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
resolveTask();
|
|
206
|
+
this.runningTasks.delete(run.id);
|
|
207
|
+
if (!writer.closed) {
|
|
208
|
+
writer.writeEvent(AgentSSEEvent.Done, "[DONE]");
|
|
209
|
+
writer.end();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Consume the execRun async generator, emitting SSE message.delta events
|
|
215
|
+
* for each chunk and accumulating content blocks and token usage.
|
|
216
|
+
*/
|
|
217
|
+
async consumeStreamMessages(input, signal, writer, msgId) {
|
|
218
|
+
const content = [];
|
|
219
|
+
let promptTokens = 0;
|
|
220
|
+
let completionTokens = 0;
|
|
221
|
+
let hasUsage = false;
|
|
222
|
+
for await (const msg of this.executor.execRun(input, signal)) {
|
|
223
|
+
if (signal.aborted) return {
|
|
224
|
+
content,
|
|
225
|
+
usage: void 0,
|
|
226
|
+
aborted: true
|
|
227
|
+
};
|
|
228
|
+
if (msg.message) {
|
|
229
|
+
const contentBlocks = MessageConverter.toContentBlocks(msg.message);
|
|
230
|
+
content.push(...contentBlocks);
|
|
231
|
+
const delta = {
|
|
232
|
+
id: msgId,
|
|
233
|
+
object: AgentObjectType.ThreadMessageDelta,
|
|
234
|
+
delta: { content: contentBlocks }
|
|
235
|
+
};
|
|
236
|
+
writer.writeEvent(AgentSSEEvent.ThreadMessageDelta, delta);
|
|
237
|
+
}
|
|
238
|
+
if (msg.usage) {
|
|
239
|
+
hasUsage = true;
|
|
240
|
+
promptTokens += msg.usage.promptTokens ?? 0;
|
|
241
|
+
completionTokens += msg.usage.completionTokens ?? 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
content,
|
|
246
|
+
usage: hasUsage ? {
|
|
247
|
+
promptTokens,
|
|
248
|
+
completionTokens,
|
|
249
|
+
totalTokens: promptTokens + completionTokens
|
|
250
|
+
} : void 0,
|
|
251
|
+
aborted: false
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async getRun(runId) {
|
|
255
|
+
const run = await this.store.getRun(runId);
|
|
256
|
+
return RunBuilder.fromRecord(run).snapshot();
|
|
257
|
+
}
|
|
258
|
+
async cancelRun(runId) {
|
|
259
|
+
const run = await this.store.getRun(runId);
|
|
260
|
+
if (AgentRuntime.TERMINAL_RUN_STATUSES.has(run.status)) throw new AgentConflictError(`Cannot cancel run with status '${run.status}'`);
|
|
261
|
+
const rb = RunBuilder.fromRecord(run);
|
|
262
|
+
await this.store.updateRun(runId, rb.cancelling());
|
|
263
|
+
const task = this.runningTasks.get(runId);
|
|
264
|
+
if (task) {
|
|
265
|
+
task.abortController.abort();
|
|
266
|
+
await task.promise.catch(() => {});
|
|
267
|
+
}
|
|
268
|
+
const freshRun = await this.store.getRun(runId);
|
|
269
|
+
if (AgentRuntime.TERMINAL_RUN_STATUSES.has(freshRun.status)) return RunBuilder.fromRecord(freshRun).snapshot();
|
|
270
|
+
try {
|
|
271
|
+
await this.store.updateRun(runId, rb.cancel());
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.logger.error("[AgentRuntime] failed to write cancelled state after cancelling:", err);
|
|
274
|
+
const fallback = await this.store.getRun(runId);
|
|
275
|
+
return RunBuilder.fromRecord(fallback).snapshot();
|
|
276
|
+
}
|
|
277
|
+
return rb.snapshot();
|
|
278
|
+
}
|
|
279
|
+
/** Wait for all in-flight background tasks to complete naturally (without aborting). */
|
|
280
|
+
async waitForPendingTasks() {
|
|
281
|
+
if (this.runningTasks.size) {
|
|
282
|
+
const pending = Array.from(this.runningTasks.values()).map((t) => t.promise);
|
|
283
|
+
await Promise.allSettled(pending);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async destroy() {
|
|
287
|
+
for (const task of this.runningTasks.values()) task.abortController.abort();
|
|
288
|
+
await this.waitForPendingTasks();
|
|
289
|
+
if (this.store.destroy) await this.store.destroy();
|
|
290
|
+
}
|
|
291
|
+
/** Factory method — avoids the spread-arg type issue with dynamic delegation. */
|
|
292
|
+
static create(options) {
|
|
293
|
+
return new AgentRuntime(options);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
export { AGENT_RUNTIME, AgentRuntime };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
//#region src/AgentStoreUtils.ts
|
|
4
|
+
function nowUnix() {
|
|
5
|
+
return Math.floor(Date.now() / 1e3);
|
|
6
|
+
}
|
|
7
|
+
function newMsgId() {
|
|
8
|
+
return `msg_${crypto.randomUUID()}`;
|
|
9
|
+
}
|
|
10
|
+
function newThreadId() {
|
|
11
|
+
return `thread_${crypto.randomUUID()}`;
|
|
12
|
+
}
|
|
13
|
+
function newRunId() {
|
|
14
|
+
return `run_${crypto.randomUUID()}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { newMsgId, newRunId, newThreadId, nowUnix };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SSEWriter } from "./SSEWriter.js";
|
|
2
|
+
import { ServerResponse } from "node:http";
|
|
3
|
+
|
|
4
|
+
//#region src/HttpSSEWriter.d.ts
|
|
5
|
+
declare class HttpSSEWriter implements SSEWriter {
|
|
6
|
+
private res;
|
|
7
|
+
private _closed;
|
|
8
|
+
private closeCallbacks;
|
|
9
|
+
private headersSent;
|
|
10
|
+
private readonly onResClose;
|
|
11
|
+
constructor(res: ServerResponse);
|
|
12
|
+
/** Lazily write headers on first event — avoids sending corrupt headers if constructor throws. */
|
|
13
|
+
private ensureHeaders;
|
|
14
|
+
writeEvent(event: string, data: unknown): void;
|
|
15
|
+
get closed(): boolean;
|
|
16
|
+
end(): void;
|
|
17
|
+
onClose(callback: () => void): void;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { HttpSSEWriter };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/HttpSSEWriter.ts
|
|
2
|
+
var HttpSSEWriter = class {
|
|
3
|
+
res;
|
|
4
|
+
_closed = false;
|
|
5
|
+
closeCallbacks = [];
|
|
6
|
+
headersSent = false;
|
|
7
|
+
onResClose;
|
|
8
|
+
constructor(res) {
|
|
9
|
+
this.res = res;
|
|
10
|
+
this.onResClose = () => {
|
|
11
|
+
this._closed = true;
|
|
12
|
+
for (const cb of this.closeCallbacks) cb();
|
|
13
|
+
this.closeCallbacks.length = 0;
|
|
14
|
+
};
|
|
15
|
+
res.on("close", this.onResClose);
|
|
16
|
+
}
|
|
17
|
+
/** Lazily write headers on first event — avoids sending corrupt headers if constructor throws. */
|
|
18
|
+
ensureHeaders() {
|
|
19
|
+
if (this.headersSent) return;
|
|
20
|
+
this.headersSent = true;
|
|
21
|
+
this.res.writeHead(200, {
|
|
22
|
+
"content-type": "text/event-stream",
|
|
23
|
+
"cache-control": "no-cache",
|
|
24
|
+
connection: "keep-alive"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
writeEvent(event, data) {
|
|
28
|
+
if (this._closed) return;
|
|
29
|
+
this.ensureHeaders();
|
|
30
|
+
this.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
31
|
+
}
|
|
32
|
+
get closed() {
|
|
33
|
+
return this._closed;
|
|
34
|
+
}
|
|
35
|
+
end() {
|
|
36
|
+
if (!this._closed) {
|
|
37
|
+
this._closed = true;
|
|
38
|
+
this.res.off("close", this.onResClose);
|
|
39
|
+
this.closeCallbacks.length = 0;
|
|
40
|
+
this.res.end();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
onClose(callback) {
|
|
44
|
+
this.closeCallbacks.push(callback);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { HttpSSEWriter };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { RunUsage } from "./RunBuilder.js";
|
|
2
|
+
import { AgentStreamMessage, AgentStreamMessagePayload, CreateRunInput, MessageContentBlock, MessageObject } from "@eggjs/tegg-types/agent-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/MessageConverter.d.ts
|
|
5
|
+
declare class MessageConverter {
|
|
6
|
+
/**
|
|
7
|
+
* Convert an AgentStreamMessage's message payload into OpenAI MessageContentBlock[].
|
|
8
|
+
*/
|
|
9
|
+
static toContentBlocks(msg: AgentStreamMessagePayload): MessageContentBlock[];
|
|
10
|
+
/**
|
|
11
|
+
* Build a completed MessageObject from an AgentStreamMessage payload.
|
|
12
|
+
*/
|
|
13
|
+
static toMessageObject(msg: AgentStreamMessagePayload, runId?: string): MessageObject;
|
|
14
|
+
/**
|
|
15
|
+
* Extract MessageObjects and accumulated usage from AgentStreamMessage objects.
|
|
16
|
+
*/
|
|
17
|
+
static extractFromStreamMessages(messages: AgentStreamMessage[], runId?: string): {
|
|
18
|
+
output: MessageObject[];
|
|
19
|
+
usage?: RunUsage;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Produce a completed copy of a streaming MessageObject with final content.
|
|
23
|
+
*/
|
|
24
|
+
static completeMessage(msg: MessageObject, content: MessageContentBlock[]): MessageObject;
|
|
25
|
+
/**
|
|
26
|
+
* Create an in-progress MessageObject for streaming (before content is known).
|
|
27
|
+
*/
|
|
28
|
+
static createStreamMessage(msgId: string, runId: string): MessageObject;
|
|
29
|
+
/**
|
|
30
|
+
* Convert input messages to MessageObjects for thread history.
|
|
31
|
+
* System messages are filtered out — they are transient instructions, not conversation history.
|
|
32
|
+
*/
|
|
33
|
+
static toInputMessageObjects(messages: CreateRunInput["input"]["messages"], threadId?: string): MessageObject[];
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { MessageConverter };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { newMsgId, nowUnix } from "./AgentStoreUtils.js";
|
|
2
|
+
import { AgentObjectType, ContentBlockType, MessageRole, MessageStatus } from "@eggjs/tegg-types/agent-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/MessageConverter.ts
|
|
5
|
+
var MessageConverter = class MessageConverter {
|
|
6
|
+
/**
|
|
7
|
+
* Convert an AgentStreamMessage's message payload into OpenAI MessageContentBlock[].
|
|
8
|
+
*/
|
|
9
|
+
static toContentBlocks(msg) {
|
|
10
|
+
if (!msg) return [];
|
|
11
|
+
const content = msg.content;
|
|
12
|
+
if (typeof content === "string") return [{
|
|
13
|
+
type: ContentBlockType.Text,
|
|
14
|
+
text: {
|
|
15
|
+
value: content,
|
|
16
|
+
annotations: []
|
|
17
|
+
}
|
|
18
|
+
}];
|
|
19
|
+
if (Array.isArray(content)) return content.filter((part) => part.type === ContentBlockType.Text).map((part) => ({
|
|
20
|
+
type: ContentBlockType.Text,
|
|
21
|
+
text: {
|
|
22
|
+
value: part.text,
|
|
23
|
+
annotations: []
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a completed MessageObject from an AgentStreamMessage payload.
|
|
30
|
+
*/
|
|
31
|
+
static toMessageObject(msg, runId) {
|
|
32
|
+
return {
|
|
33
|
+
id: newMsgId(),
|
|
34
|
+
object: AgentObjectType.ThreadMessage,
|
|
35
|
+
createdAt: nowUnix(),
|
|
36
|
+
runId,
|
|
37
|
+
role: MessageRole.Assistant,
|
|
38
|
+
status: MessageStatus.Completed,
|
|
39
|
+
content: MessageConverter.toContentBlocks(msg)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Extract MessageObjects and accumulated usage from AgentStreamMessage objects.
|
|
44
|
+
*/
|
|
45
|
+
static extractFromStreamMessages(messages, runId) {
|
|
46
|
+
const output = [];
|
|
47
|
+
let promptTokens = 0;
|
|
48
|
+
let completionTokens = 0;
|
|
49
|
+
let hasUsage = false;
|
|
50
|
+
for (const msg of messages) {
|
|
51
|
+
if (msg.message) output.push(MessageConverter.toMessageObject(msg.message, runId));
|
|
52
|
+
if (msg.usage) {
|
|
53
|
+
hasUsage = true;
|
|
54
|
+
promptTokens += msg.usage.promptTokens ?? 0;
|
|
55
|
+
completionTokens += msg.usage.completionTokens ?? 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let usage;
|
|
59
|
+
if (hasUsage) usage = {
|
|
60
|
+
promptTokens,
|
|
61
|
+
completionTokens,
|
|
62
|
+
totalTokens: promptTokens + completionTokens
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
output,
|
|
66
|
+
usage
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Produce a completed copy of a streaming MessageObject with final content.
|
|
71
|
+
*/
|
|
72
|
+
static completeMessage(msg, content) {
|
|
73
|
+
return {
|
|
74
|
+
...msg,
|
|
75
|
+
status: MessageStatus.Completed,
|
|
76
|
+
content
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create an in-progress MessageObject for streaming (before content is known).
|
|
81
|
+
*/
|
|
82
|
+
static createStreamMessage(msgId, runId) {
|
|
83
|
+
return {
|
|
84
|
+
id: msgId,
|
|
85
|
+
object: AgentObjectType.ThreadMessage,
|
|
86
|
+
createdAt: nowUnix(),
|
|
87
|
+
runId,
|
|
88
|
+
role: MessageRole.Assistant,
|
|
89
|
+
status: MessageStatus.InProgress,
|
|
90
|
+
content: []
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Convert input messages to MessageObjects for thread history.
|
|
95
|
+
* System messages are filtered out — they are transient instructions, not conversation history.
|
|
96
|
+
*/
|
|
97
|
+
static toInputMessageObjects(messages, threadId) {
|
|
98
|
+
return messages.filter((m) => m.role !== MessageRole.System).map((m) => ({
|
|
99
|
+
id: newMsgId(),
|
|
100
|
+
object: AgentObjectType.ThreadMessage,
|
|
101
|
+
createdAt: nowUnix(),
|
|
102
|
+
threadId,
|
|
103
|
+
role: m.role,
|
|
104
|
+
status: MessageStatus.Completed,
|
|
105
|
+
content: typeof m.content === "string" ? [{
|
|
106
|
+
type: ContentBlockType.Text,
|
|
107
|
+
text: {
|
|
108
|
+
value: m.content,
|
|
109
|
+
annotations: []
|
|
110
|
+
}
|
|
111
|
+
}] : m.content.map((p) => ({
|
|
112
|
+
type: ContentBlockType.Text,
|
|
113
|
+
text: {
|
|
114
|
+
value: p.text,
|
|
115
|
+
annotations: []
|
|
116
|
+
}
|
|
117
|
+
}))
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
//#endregion
|
|
123
|
+
export { MessageConverter };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AgentRunConfig, AgentStore, InputMessage, MessageObject, ObjectStorageClient, RunRecord, ThreadRecord } from "@eggjs/tegg-types/agent-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/OSSAgentStore.d.ts
|
|
4
|
+
interface OSSAgentStoreOptions {
|
|
5
|
+
client: ObjectStorageClient;
|
|
6
|
+
prefix?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* AgentStore implementation backed by an ObjectStorageClient (OSS, S3, etc.).
|
|
10
|
+
*
|
|
11
|
+
* ## Storage layout
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* {prefix}threads/{id}/meta.json — Thread metadata (JSON)
|
|
15
|
+
* {prefix}threads/{id}/messages.jsonl — Messages (JSONL, one JSON object per line)
|
|
16
|
+
* {prefix}runs/{id}.json — Run record (JSON)
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* ### Why split threads into two keys?
|
|
20
|
+
*
|
|
21
|
+
* Thread messages are append-only: new messages are added at the end but never
|
|
22
|
+
* modified or deleted. Storing them as a JSONL file allows us to leverage the
|
|
23
|
+
* OSS AppendObject API (or similar) to write new messages without reading the
|
|
24
|
+
* entire thread first. This is much more efficient than read-modify-write for
|
|
25
|
+
* long conversations.
|
|
26
|
+
*
|
|
27
|
+
* If the underlying ObjectStorageClient provides an `append()` method, it will
|
|
28
|
+
* be used for O(1) message writes. Otherwise, the store falls back to
|
|
29
|
+
* get-concat-put (which is NOT atomic and may lose data under concurrent
|
|
30
|
+
* writers — acceptable for single-writer scenarios).
|
|
31
|
+
*
|
|
32
|
+
* ### Atomicity note
|
|
33
|
+
*
|
|
34
|
+
* Run updates still use read-modify-write because run fields are mutated
|
|
35
|
+
* (status, timestamps, output, etc.) — they cannot be modelled as append-only.
|
|
36
|
+
* For multi-writer safety, consider a database-backed AgentStore or ETag-based
|
|
37
|
+
* conditional writes with retry.
|
|
38
|
+
*/
|
|
39
|
+
declare class OSSAgentStore implements AgentStore {
|
|
40
|
+
private readonly client;
|
|
41
|
+
private readonly prefix;
|
|
42
|
+
constructor(options: OSSAgentStoreOptions);
|
|
43
|
+
/** Key for thread metadata (JSON). */
|
|
44
|
+
private threadMetaKey;
|
|
45
|
+
/** Key for thread messages (JSONL, one message per line). */
|
|
46
|
+
private threadMessagesKey;
|
|
47
|
+
/** Key for run record (JSON). */
|
|
48
|
+
private runKey;
|
|
49
|
+
init(): Promise<void>;
|
|
50
|
+
destroy(): Promise<void>;
|
|
51
|
+
createThread(metadata?: Record<string, unknown>): Promise<ThreadRecord>;
|
|
52
|
+
getThread(threadId: string): Promise<ThreadRecord>;
|
|
53
|
+
/**
|
|
54
|
+
* Append messages to a thread.
|
|
55
|
+
*
|
|
56
|
+
* Each message is serialized as a single JSON line (JSONL format).
|
|
57
|
+
* When the underlying client supports `append()`, this is a single
|
|
58
|
+
* O(1) write — no need to read the existing messages first.
|
|
59
|
+
*/
|
|
60
|
+
appendMessages(threadId: string, messages: MessageObject[]): Promise<void>;
|
|
61
|
+
createRun(input: InputMessage[], threadId?: string, config?: AgentRunConfig, metadata?: Record<string, unknown>): Promise<RunRecord>;
|
|
62
|
+
getRun(runId: string): Promise<RunRecord>;
|
|
63
|
+
updateRun(runId: string, updates: Partial<RunRecord>): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { OSSAgentStore, OSSAgentStoreOptions };
|