@eggjs/agent-runtime 4.0.2-beta.3
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 -0
- package/dist/index.js +11 -0
- package/package.json +60 -0
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 };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { newRunId, newThreadId, nowUnix } from "./AgentStoreUtils.js";
|
|
2
|
+
import { AgentNotFoundError, AgentObjectType, RunStatus } from "@eggjs/tegg-types/agent-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/OSSAgentStore.ts
|
|
5
|
+
/**
|
|
6
|
+
* AgentStore implementation backed by an ObjectStorageClient (OSS, S3, etc.).
|
|
7
|
+
*
|
|
8
|
+
* ## Storage layout
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* {prefix}threads/{id}/meta.json — Thread metadata (JSON)
|
|
12
|
+
* {prefix}threads/{id}/messages.jsonl — Messages (JSONL, one JSON object per line)
|
|
13
|
+
* {prefix}runs/{id}.json — Run record (JSON)
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ### Why split threads into two keys?
|
|
17
|
+
*
|
|
18
|
+
* Thread messages are append-only: new messages are added at the end but never
|
|
19
|
+
* modified or deleted. Storing them as a JSONL file allows us to leverage the
|
|
20
|
+
* OSS AppendObject API (or similar) to write new messages without reading the
|
|
21
|
+
* entire thread first. This is much more efficient than read-modify-write for
|
|
22
|
+
* long conversations.
|
|
23
|
+
*
|
|
24
|
+
* If the underlying ObjectStorageClient provides an `append()` method, it will
|
|
25
|
+
* be used for O(1) message writes. Otherwise, the store falls back to
|
|
26
|
+
* get-concat-put (which is NOT atomic and may lose data under concurrent
|
|
27
|
+
* writers — acceptable for single-writer scenarios).
|
|
28
|
+
*
|
|
29
|
+
* ### Atomicity note
|
|
30
|
+
*
|
|
31
|
+
* Run updates still use read-modify-write because run fields are mutated
|
|
32
|
+
* (status, timestamps, output, etc.) — they cannot be modelled as append-only.
|
|
33
|
+
* For multi-writer safety, consider a database-backed AgentStore or ETag-based
|
|
34
|
+
* conditional writes with retry.
|
|
35
|
+
*/
|
|
36
|
+
var OSSAgentStore = class {
|
|
37
|
+
client;
|
|
38
|
+
prefix;
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.client = options.client;
|
|
41
|
+
const raw = options.prefix ?? "";
|
|
42
|
+
this.prefix = raw && !raw.endsWith("/") ? raw + "/" : raw;
|
|
43
|
+
}
|
|
44
|
+
/** Key for thread metadata (JSON). */
|
|
45
|
+
threadMetaKey(threadId) {
|
|
46
|
+
return `${this.prefix}threads/${threadId}/meta.json`;
|
|
47
|
+
}
|
|
48
|
+
/** Key for thread messages (JSONL, one message per line). */
|
|
49
|
+
threadMessagesKey(threadId) {
|
|
50
|
+
return `${this.prefix}threads/${threadId}/messages.jsonl`;
|
|
51
|
+
}
|
|
52
|
+
/** Key for run record (JSON). */
|
|
53
|
+
runKey(runId) {
|
|
54
|
+
return `${this.prefix}runs/${runId}.json`;
|
|
55
|
+
}
|
|
56
|
+
async init() {
|
|
57
|
+
await this.client.init?.();
|
|
58
|
+
}
|
|
59
|
+
async destroy() {
|
|
60
|
+
await this.client.destroy?.();
|
|
61
|
+
}
|
|
62
|
+
async createThread(metadata) {
|
|
63
|
+
const threadId = newThreadId();
|
|
64
|
+
const meta = {
|
|
65
|
+
id: threadId,
|
|
66
|
+
object: AgentObjectType.Thread,
|
|
67
|
+
metadata: metadata ?? {},
|
|
68
|
+
createdAt: nowUnix()
|
|
69
|
+
};
|
|
70
|
+
await this.client.put(this.threadMetaKey(threadId), JSON.stringify(meta));
|
|
71
|
+
return {
|
|
72
|
+
...meta,
|
|
73
|
+
messages: []
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async getThread(threadId) {
|
|
77
|
+
const [metaData, messagesData] = await Promise.all([this.client.get(this.threadMetaKey(threadId)), this.client.get(this.threadMessagesKey(threadId))]);
|
|
78
|
+
if (!metaData) throw new AgentNotFoundError(`Thread ${threadId} not found`);
|
|
79
|
+
const meta = JSON.parse(metaData);
|
|
80
|
+
const messages = messagesData ? messagesData.trim().split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line)) : [];
|
|
81
|
+
return {
|
|
82
|
+
...meta,
|
|
83
|
+
messages
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Append messages to a thread.
|
|
88
|
+
*
|
|
89
|
+
* Each message is serialized as a single JSON line (JSONL format).
|
|
90
|
+
* When the underlying client supports `append()`, this is a single
|
|
91
|
+
* O(1) write — no need to read the existing messages first.
|
|
92
|
+
*/
|
|
93
|
+
async appendMessages(threadId, messages) {
|
|
94
|
+
if (!await this.client.get(this.threadMetaKey(threadId))) throw new AgentNotFoundError(`Thread ${threadId} not found`);
|
|
95
|
+
if (messages.length === 0) return;
|
|
96
|
+
const lines = messages.map((m) => JSON.stringify(m)).join("\n") + "\n";
|
|
97
|
+
const messagesKey = this.threadMessagesKey(threadId);
|
|
98
|
+
if (this.client.append) await this.client.append(messagesKey, lines);
|
|
99
|
+
else {
|
|
100
|
+
const existing = await this.client.get(messagesKey) ?? "";
|
|
101
|
+
await this.client.put(messagesKey, existing + lines);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async createRun(input, threadId, config, metadata) {
|
|
105
|
+
const runId = newRunId();
|
|
106
|
+
const record = {
|
|
107
|
+
id: runId,
|
|
108
|
+
object: AgentObjectType.ThreadRun,
|
|
109
|
+
threadId,
|
|
110
|
+
status: RunStatus.Queued,
|
|
111
|
+
input,
|
|
112
|
+
config,
|
|
113
|
+
metadata,
|
|
114
|
+
createdAt: nowUnix()
|
|
115
|
+
};
|
|
116
|
+
await this.client.put(this.runKey(runId), JSON.stringify(record));
|
|
117
|
+
return record;
|
|
118
|
+
}
|
|
119
|
+
async getRun(runId) {
|
|
120
|
+
const data = await this.client.get(this.runKey(runId));
|
|
121
|
+
if (!data) throw new AgentNotFoundError(`Run ${runId} not found`);
|
|
122
|
+
return JSON.parse(data);
|
|
123
|
+
}
|
|
124
|
+
async updateRun(runId, updates) {
|
|
125
|
+
const run = await this.getRun(runId);
|
|
126
|
+
const { id: _, object: __, ...safeUpdates } = updates;
|
|
127
|
+
Object.assign(run, safeUpdates);
|
|
128
|
+
await this.client.put(this.runKey(runId), JSON.stringify(run));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
export { OSSAgentStore };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ObjectStorageClient } from "@eggjs/tegg-types/agent-runtime";
|
|
2
|
+
import { OSSObject } from "oss-client";
|
|
3
|
+
|
|
4
|
+
//#region src/OSSObjectStorageClient.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ObjectStorageClient backed by Alibaba Cloud OSS (via oss-client).
|
|
8
|
+
*
|
|
9
|
+
* Supports both `put`/`get` for normal objects and `append` for
|
|
10
|
+
* OSS Appendable Objects. The append path uses a local position cache
|
|
11
|
+
* to avoid extra HEAD requests; on position mismatch it falls back to
|
|
12
|
+
* HEAD + retry automatically.
|
|
13
|
+
*
|
|
14
|
+
* The OSSObject instance should be constructed and injected by the caller,
|
|
15
|
+
* following the IoC/DI principle.
|
|
16
|
+
*/
|
|
17
|
+
declare class OSSObjectStorageClient implements ObjectStorageClient {
|
|
18
|
+
private readonly client;
|
|
19
|
+
/**
|
|
20
|
+
* In-memory cache of next-append positions.
|
|
21
|
+
*
|
|
22
|
+
* After each successful `append()`, OSS returns `nextAppendPosition`.
|
|
23
|
+
* We cache it here so the next append can skip a HEAD round-trip.
|
|
24
|
+
* If the cached position is stale (e.g., process restarted or another
|
|
25
|
+
* writer appended), the append will fail with PositionNotEqualToLength
|
|
26
|
+
* and we fall back to HEAD + retry.
|
|
27
|
+
*/
|
|
28
|
+
private readonly appendPositions;
|
|
29
|
+
constructor(client: OSSObject);
|
|
30
|
+
put(key: string, value: string): Promise<void>;
|
|
31
|
+
get(key: string): Promise<string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Append data to an OSS Appendable Object.
|
|
34
|
+
*
|
|
35
|
+
* OSS AppendObject requires a `position` parameter that must equal the
|
|
36
|
+
* current object size. We use a three-step strategy:
|
|
37
|
+
*
|
|
38
|
+
* 1. Use the cached position (0 for new objects, or the value from the
|
|
39
|
+
* last successful append).
|
|
40
|
+
* 2. If OSS returns PositionNotEqualToLength (cache is stale), issue a
|
|
41
|
+
* HEAD request to learn the current object size, then retry once.
|
|
42
|
+
* 3. Update the cache with `nextAppendPosition` from the response.
|
|
43
|
+
*
|
|
44
|
+
* This gives us single-round-trip performance in the common case (single
|
|
45
|
+
* writer, no restarts) while still being self-healing when the cache is
|
|
46
|
+
* stale.
|
|
47
|
+
*/
|
|
48
|
+
append(key: string, value: string): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { OSSObjectStorageClient };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/OSSObjectStorageClient.ts
|
|
2
|
+
function isOSSError(err, code) {
|
|
3
|
+
return err != null && typeof err === "object" && "code" in err && err.code === code;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* ObjectStorageClient backed by Alibaba Cloud OSS (via oss-client).
|
|
7
|
+
*
|
|
8
|
+
* Supports both `put`/`get` for normal objects and `append` for
|
|
9
|
+
* OSS Appendable Objects. The append path uses a local position cache
|
|
10
|
+
* to avoid extra HEAD requests; on position mismatch it falls back to
|
|
11
|
+
* HEAD + retry automatically.
|
|
12
|
+
*
|
|
13
|
+
* The OSSObject instance should be constructed and injected by the caller,
|
|
14
|
+
* following the IoC/DI principle.
|
|
15
|
+
*/
|
|
16
|
+
var OSSObjectStorageClient = class {
|
|
17
|
+
client;
|
|
18
|
+
/**
|
|
19
|
+
* In-memory cache of next-append positions.
|
|
20
|
+
*
|
|
21
|
+
* After each successful `append()`, OSS returns `nextAppendPosition`.
|
|
22
|
+
* We cache it here so the next append can skip a HEAD round-trip.
|
|
23
|
+
* If the cached position is stale (e.g., process restarted or another
|
|
24
|
+
* writer appended), the append will fail with PositionNotEqualToLength
|
|
25
|
+
* and we fall back to HEAD + retry.
|
|
26
|
+
*/
|
|
27
|
+
appendPositions = /* @__PURE__ */ new Map();
|
|
28
|
+
constructor(client) {
|
|
29
|
+
this.client = client;
|
|
30
|
+
}
|
|
31
|
+
async put(key, value) {
|
|
32
|
+
await this.client.put(key, Buffer.from(value, "utf-8"));
|
|
33
|
+
}
|
|
34
|
+
async get(key) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await this.client.get(key);
|
|
37
|
+
if (result.content) return Buffer.isBuffer(result.content) ? result.content.toString("utf-8") : String(result.content);
|
|
38
|
+
return null;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (isOSSError(err, "NoSuchKey")) return null;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Append data to an OSS Appendable Object.
|
|
46
|
+
*
|
|
47
|
+
* OSS AppendObject requires a `position` parameter that must equal the
|
|
48
|
+
* current object size. We use a three-step strategy:
|
|
49
|
+
*
|
|
50
|
+
* 1. Use the cached position (0 for new objects, or the value from the
|
|
51
|
+
* last successful append).
|
|
52
|
+
* 2. If OSS returns PositionNotEqualToLength (cache is stale), issue a
|
|
53
|
+
* HEAD request to learn the current object size, then retry once.
|
|
54
|
+
* 3. Update the cache with `nextAppendPosition` from the response.
|
|
55
|
+
*
|
|
56
|
+
* This gives us single-round-trip performance in the common case (single
|
|
57
|
+
* writer, no restarts) while still being self-healing when the cache is
|
|
58
|
+
* stale.
|
|
59
|
+
*/
|
|
60
|
+
async append(key, value) {
|
|
61
|
+
const buf = Buffer.from(value, "utf-8");
|
|
62
|
+
const position = this.appendPositions.get(key) ?? 0;
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.client.append(key, buf, { position });
|
|
65
|
+
this.appendPositions.set(key, Number(result.nextAppendPosition));
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (isOSSError(err, "PositionNotEqualToLength")) {
|
|
68
|
+
const head = await this.client.head(key);
|
|
69
|
+
const currentPos = Number(head.res.headers["content-length"] ?? 0);
|
|
70
|
+
const result = await this.client.append(key, buf, { position: currentPos });
|
|
71
|
+
this.appendPositions.set(key, Number(result.nextAppendPosition));
|
|
72
|
+
} else throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { OSSObjectStorageClient };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MessageObject, RunObject, RunRecord } from "@eggjs/tegg-types/agent-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/RunBuilder.d.ts
|
|
4
|
+
/** Accumulated token usage — same shape as non-null RunRecord['usage']. */
|
|
5
|
+
type RunUsage = NonNullable<RunRecord["usage"]>;
|
|
6
|
+
/**
|
|
7
|
+
* Encapsulates run state transitions.
|
|
8
|
+
*
|
|
9
|
+
* Mutation methods (`start`, `complete`, `fail`, `cancel`) update internal
|
|
10
|
+
* state and return `Partial<RunRecord>` for the store.
|
|
11
|
+
*
|
|
12
|
+
* `snapshot()` produces a `RunObject` suitable for API responses and SSE events.
|
|
13
|
+
*/
|
|
14
|
+
declare class RunBuilder {
|
|
15
|
+
private readonly id;
|
|
16
|
+
private readonly threadId;
|
|
17
|
+
private readonly createdAt;
|
|
18
|
+
private readonly metadata?;
|
|
19
|
+
private readonly config?;
|
|
20
|
+
private status;
|
|
21
|
+
private startedAt?;
|
|
22
|
+
private completedAt?;
|
|
23
|
+
private cancelledAt?;
|
|
24
|
+
private failedAt?;
|
|
25
|
+
private lastError?;
|
|
26
|
+
private usage?;
|
|
27
|
+
private output?;
|
|
28
|
+
private constructor();
|
|
29
|
+
/** Create a RunBuilder from a store RunRecord, using its own threadId. */
|
|
30
|
+
static fromRecord(run: RunRecord): RunBuilder;
|
|
31
|
+
/** Create a RunBuilder from a store RunRecord, restoring all mutable state. */
|
|
32
|
+
static create(run: RunRecord, threadId: string): RunBuilder;
|
|
33
|
+
/** queued -> in_progress. Returns store update. */
|
|
34
|
+
start(): Partial<RunRecord>;
|
|
35
|
+
/** in_progress -> completed. Returns store update. */
|
|
36
|
+
complete(output: MessageObject[], usage?: RunUsage): Partial<RunRecord>;
|
|
37
|
+
/** queued/in_progress -> failed. Returns store update. */
|
|
38
|
+
fail(error: Error): Partial<RunRecord>;
|
|
39
|
+
/** in_progress/queued -> cancelling (idempotent if already cancelling). Returns store update. */
|
|
40
|
+
cancelling(): Partial<RunRecord>;
|
|
41
|
+
/** cancelling -> cancelled. Returns store update. */
|
|
42
|
+
cancel(): Partial<RunRecord>;
|
|
43
|
+
/** Produce a RunObject snapshot for API / SSE. */
|
|
44
|
+
snapshot(): RunObject;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { RunBuilder, RunUsage };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { nowUnix } from "./AgentStoreUtils.js";
|
|
2
|
+
import { AgentErrorCode, AgentObjectType, InvalidRunStateTransitionError, RunStatus } from "@eggjs/tegg-types/agent-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/RunBuilder.ts
|
|
5
|
+
/**
|
|
6
|
+
* Encapsulates run state transitions.
|
|
7
|
+
*
|
|
8
|
+
* Mutation methods (`start`, `complete`, `fail`, `cancel`) update internal
|
|
9
|
+
* state and return `Partial<RunRecord>` for the store.
|
|
10
|
+
*
|
|
11
|
+
* `snapshot()` produces a `RunObject` suitable for API responses and SSE events.
|
|
12
|
+
*/
|
|
13
|
+
var RunBuilder = class RunBuilder {
|
|
14
|
+
id;
|
|
15
|
+
threadId;
|
|
16
|
+
createdAt;
|
|
17
|
+
metadata;
|
|
18
|
+
config;
|
|
19
|
+
status;
|
|
20
|
+
startedAt;
|
|
21
|
+
completedAt;
|
|
22
|
+
cancelledAt;
|
|
23
|
+
failedAt;
|
|
24
|
+
lastError;
|
|
25
|
+
usage;
|
|
26
|
+
output;
|
|
27
|
+
constructor(id, threadId, createdAt, status, metadata, config) {
|
|
28
|
+
this.id = id;
|
|
29
|
+
this.threadId = threadId;
|
|
30
|
+
this.createdAt = createdAt;
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.metadata = metadata;
|
|
33
|
+
this.config = config;
|
|
34
|
+
}
|
|
35
|
+
/** Create a RunBuilder from a store RunRecord, using its own threadId. */
|
|
36
|
+
static fromRecord(run) {
|
|
37
|
+
return RunBuilder.create(run, run.threadId ?? "");
|
|
38
|
+
}
|
|
39
|
+
/** Create a RunBuilder from a store RunRecord, restoring all mutable state. */
|
|
40
|
+
static create(run, threadId) {
|
|
41
|
+
const rb = new RunBuilder(run.id, threadId, run.createdAt, run.status, run.metadata, run.config);
|
|
42
|
+
rb.startedAt = run.startedAt ?? void 0;
|
|
43
|
+
rb.completedAt = run.completedAt ?? void 0;
|
|
44
|
+
rb.cancelledAt = run.cancelledAt ?? void 0;
|
|
45
|
+
rb.failedAt = run.failedAt ?? void 0;
|
|
46
|
+
rb.lastError = run.lastError ?? void 0;
|
|
47
|
+
rb.output = run.output;
|
|
48
|
+
if (run.usage) rb.usage = { ...run.usage };
|
|
49
|
+
return rb;
|
|
50
|
+
}
|
|
51
|
+
/** queued -> in_progress. Returns store update. */
|
|
52
|
+
start() {
|
|
53
|
+
if (this.status !== RunStatus.Queued) throw new InvalidRunStateTransitionError(this.status, RunStatus.InProgress);
|
|
54
|
+
this.status = RunStatus.InProgress;
|
|
55
|
+
this.startedAt = nowUnix();
|
|
56
|
+
return {
|
|
57
|
+
status: this.status,
|
|
58
|
+
startedAt: this.startedAt
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** in_progress -> completed. Returns store update. */
|
|
62
|
+
complete(output, usage) {
|
|
63
|
+
if (this.status !== RunStatus.InProgress) throw new InvalidRunStateTransitionError(this.status, RunStatus.Completed);
|
|
64
|
+
this.status = RunStatus.Completed;
|
|
65
|
+
this.completedAt = nowUnix();
|
|
66
|
+
this.output = output;
|
|
67
|
+
this.usage = usage;
|
|
68
|
+
return {
|
|
69
|
+
status: this.status,
|
|
70
|
+
output,
|
|
71
|
+
usage,
|
|
72
|
+
completedAt: this.completedAt
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** queued/in_progress -> failed. Returns store update. */
|
|
76
|
+
fail(error) {
|
|
77
|
+
if (this.status !== RunStatus.InProgress && this.status !== RunStatus.Queued) throw new InvalidRunStateTransitionError(this.status, RunStatus.Failed);
|
|
78
|
+
this.status = RunStatus.Failed;
|
|
79
|
+
this.failedAt = nowUnix();
|
|
80
|
+
this.lastError = {
|
|
81
|
+
code: AgentErrorCode.ExecError,
|
|
82
|
+
message: error.message
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
status: this.status,
|
|
86
|
+
lastError: this.lastError,
|
|
87
|
+
failedAt: this.failedAt
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** in_progress/queued -> cancelling (idempotent if already cancelling). Returns store update. */
|
|
91
|
+
cancelling() {
|
|
92
|
+
if (this.status === RunStatus.Cancelling) return { status: this.status };
|
|
93
|
+
if (this.status !== RunStatus.InProgress && this.status !== RunStatus.Queued) throw new InvalidRunStateTransitionError(this.status, RunStatus.Cancelling);
|
|
94
|
+
this.status = RunStatus.Cancelling;
|
|
95
|
+
return { status: this.status };
|
|
96
|
+
}
|
|
97
|
+
/** cancelling -> cancelled. Returns store update. */
|
|
98
|
+
cancel() {
|
|
99
|
+
if (this.status !== RunStatus.Cancelling) throw new InvalidRunStateTransitionError(this.status, RunStatus.Cancelled);
|
|
100
|
+
this.status = RunStatus.Cancelled;
|
|
101
|
+
this.cancelledAt = nowUnix();
|
|
102
|
+
return {
|
|
103
|
+
status: this.status,
|
|
104
|
+
cancelledAt: this.cancelledAt
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Produce a RunObject snapshot for API / SSE. */
|
|
108
|
+
snapshot() {
|
|
109
|
+
return {
|
|
110
|
+
id: this.id,
|
|
111
|
+
object: AgentObjectType.ThreadRun,
|
|
112
|
+
createdAt: this.createdAt,
|
|
113
|
+
threadId: this.threadId,
|
|
114
|
+
status: this.status,
|
|
115
|
+
lastError: this.lastError,
|
|
116
|
+
startedAt: this.startedAt ?? null,
|
|
117
|
+
completedAt: this.completedAt ?? null,
|
|
118
|
+
cancelledAt: this.cancelledAt ?? null,
|
|
119
|
+
failedAt: this.failedAt ?? null,
|
|
120
|
+
usage: this.usage ?? null,
|
|
121
|
+
metadata: this.metadata,
|
|
122
|
+
output: this.output,
|
|
123
|
+
config: this.config
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
export { RunBuilder };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/SSEWriter.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Abstract interface for writing SSE events.
|
|
4
|
+
* Decouples AgentRuntime from HTTP transport details.
|
|
5
|
+
*/
|
|
6
|
+
interface SSEWriter {
|
|
7
|
+
/** Write an SSE event with the given name and JSON-serializable data. */
|
|
8
|
+
writeEvent(event: string, data: unknown): void;
|
|
9
|
+
/** Whether the underlying connection has been closed. */
|
|
10
|
+
readonly closed: boolean;
|
|
11
|
+
/** End the SSE stream. */
|
|
12
|
+
end(): void;
|
|
13
|
+
/** Register a callback for when the client disconnects. */
|
|
14
|
+
onClose(callback: () => void): void;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { SSEWriter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SSEWriter } from "./SSEWriter.js";
|
|
2
|
+
import { AGENT_RUNTIME, AgentExecutor, AgentRuntime, AgentRuntimeOptions } from "./AgentRuntime.js";
|
|
3
|
+
import { newMsgId, newRunId, newThreadId, nowUnix } from "./AgentStoreUtils.js";
|
|
4
|
+
import { HttpSSEWriter } from "./HttpSSEWriter.js";
|
|
5
|
+
import { RunBuilder, RunUsage } from "./RunBuilder.js";
|
|
6
|
+
import { MessageConverter } from "./MessageConverter.js";
|
|
7
|
+
import { OSSAgentStore, OSSAgentStoreOptions } from "./OSSAgentStore.js";
|
|
8
|
+
import { OSSObjectStorageClient } from "./OSSObjectStorageClient.js";
|
|
9
|
+
export * from "@eggjs/tegg-types/agent-runtime";
|
|
10
|
+
export { AGENT_RUNTIME, type AgentExecutor, AgentRuntime, type AgentRuntimeOptions, HttpSSEWriter, MessageConverter, OSSAgentStore, OSSAgentStoreOptions, OSSObjectStorageClient, RunBuilder, RunUsage, SSEWriter, newMsgId, newRunId, newThreadId, nowUnix };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { newMsgId, newRunId, newThreadId, nowUnix } from "./AgentStoreUtils.js";
|
|
2
|
+
import { MessageConverter } from "./MessageConverter.js";
|
|
3
|
+
import { RunBuilder } from "./RunBuilder.js";
|
|
4
|
+
import { AGENT_RUNTIME, AgentRuntime } from "./AgentRuntime.js";
|
|
5
|
+
import { HttpSSEWriter } from "./HttpSSEWriter.js";
|
|
6
|
+
import { OSSAgentStore } from "./OSSAgentStore.js";
|
|
7
|
+
import { OSSObjectStorageClient } from "./OSSObjectStorageClient.js";
|
|
8
|
+
|
|
9
|
+
export * from "@eggjs/tegg-types/agent-runtime"
|
|
10
|
+
|
|
11
|
+
export { AGENT_RUNTIME, AgentRuntime, HttpSSEWriter, MessageConverter, OSSAgentStore, OSSObjectStorageClient, RunBuilder, newMsgId, newRunId, newThreadId, nowUnix };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eggjs/agent-runtime",
|
|
3
|
+
"version": "4.0.2-beta.3",
|
|
4
|
+
"description": "Agent runtime with store abstraction for Egg.js tegg",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"egg",
|
|
8
|
+
"runtime",
|
|
9
|
+
"store",
|
|
10
|
+
"tegg"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/eggjs/egg/tree/next/tegg/core/agent-runtime",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/eggjs/egg/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/eggjs/egg.git",
|
|
20
|
+
"directory": "tegg/core/agent-runtime"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.js",
|
|
31
|
+
"./AgentRuntime": "./dist/AgentRuntime.js",
|
|
32
|
+
"./AgentStoreUtils": "./dist/AgentStoreUtils.js",
|
|
33
|
+
"./HttpSSEWriter": "./dist/HttpSSEWriter.js",
|
|
34
|
+
"./MessageConverter": "./dist/MessageConverter.js",
|
|
35
|
+
"./OSSAgentStore": "./dist/OSSAgentStore.js",
|
|
36
|
+
"./OSSObjectStorageClient": "./dist/OSSObjectStorageClient.js",
|
|
37
|
+
"./RunBuilder": "./dist/RunBuilder.js",
|
|
38
|
+
"./SSEWriter": "./dist/SSEWriter.js",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"egg-logger": "^3.5.0",
|
|
46
|
+
"oss-client": "^2.5.1",
|
|
47
|
+
"@eggjs/tegg-types": "4.0.2-beta.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^24.10.2",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=22.18.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"typecheck": "tsgo --noEmit",
|
|
58
|
+
"test": "vitest run"
|
|
59
|
+
}
|
|
60
|
+
}
|