@m6d/cortex-server 1.2.0 → 1.3.0
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/dist/src/ai/index.d.ts +1 -1
- package/dist/src/ws/events.d.ts +7 -1
- package/dist/src/ws/index.d.ts +1 -1
- package/package.json +3 -2
- package/src/ai/index.ts +45 -4
- package/src/ai/tools/execute-code.tool.ts +79 -27
- package/src/routes/chat.ts +1 -1
- package/src/ws/events.ts +6 -1
- package/src/ws/index.ts +1 -1
package/dist/src/ai/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ResolvedCortexAgentConfig } from "../config.ts";
|
|
2
2
|
import type { Thread } from "../types.ts";
|
|
3
|
-
export declare function stream(messages: unknown[], thread: Thread, userId: string, token: string, config: ResolvedCortexAgentConfig): Promise<Response>;
|
|
3
|
+
export declare function stream(messages: unknown[], thread: Thread, userId: string, token: string, config: ResolvedCortexAgentConfig, abortSignal?: AbortSignal): Promise<Response>;
|
|
4
4
|
export declare function generateTitle(threadId: string, prompt: string, userId: string, config: ResolvedCortexAgentConfig): Promise<void>;
|
package/dist/src/ws/events.d.ts
CHANGED
|
@@ -5,4 +5,10 @@ export type ThreadTitleUpdatedEvent = {
|
|
|
5
5
|
title: string;
|
|
6
6
|
};
|
|
7
7
|
};
|
|
8
|
-
export type
|
|
8
|
+
export type ThreadMessagesUpdatedEvent = {
|
|
9
|
+
type: "thread:messages-updated";
|
|
10
|
+
payload: {
|
|
11
|
+
threadId: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type WsEvent = ThreadTitleUpdatedEvent | ThreadMessagesUpdatedEvent;
|
package/dist/src/ws/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { addConnection, removeConnection, getConnections } from "./connections.ts";
|
|
2
2
|
export { notify } from "./notify.ts";
|
|
3
|
-
export type { WsEvent, ThreadTitleUpdatedEvent } from "./events.ts";
|
|
3
|
+
export type { WsEvent, ThreadTitleUpdatedEvent, ThreadMessagesUpdatedEvent } from "./events.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m6d/cortex-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Reusable AI agent chat server library for Hono + Bun",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"@hono/zod-validator": "^0.5.0",
|
|
30
30
|
"drizzle-orm": "^1.0.0-beta.15-859cf75",
|
|
31
31
|
"minio": "^8.0.7",
|
|
32
|
-
"mssql": "^12.2.0"
|
|
32
|
+
"mssql": "^12.2.0",
|
|
33
|
+
"quickjs-emscripten": "^0.32.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@ai-sdk/openai-compatible": "^2.0.0",
|
package/src/ai/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type UIMessage,
|
|
2
3
|
type ToolSet,
|
|
4
|
+
consumeStream,
|
|
3
5
|
convertToModelMessages,
|
|
4
6
|
generateId,
|
|
5
7
|
generateText,
|
|
@@ -26,6 +28,7 @@ export async function stream(
|
|
|
26
28
|
userId: string,
|
|
27
29
|
token: string,
|
|
28
30
|
config: ResolvedCortexAgentConfig,
|
|
31
|
+
abortSignal?: AbortSignal,
|
|
29
32
|
) {
|
|
30
33
|
const validationResult = await safeValidateUIMessages({ messages });
|
|
31
34
|
if (!validationResult.success) {
|
|
@@ -92,20 +95,30 @@ export async function stream(
|
|
|
92
95
|
system: systemPrompt,
|
|
93
96
|
tools,
|
|
94
97
|
messages: recentMessages,
|
|
95
|
-
|
|
96
|
-
console.log("Stream aborted");
|
|
97
|
-
},
|
|
98
|
+
abortSignal,
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
return result.toUIMessageStreamResponse({
|
|
101
102
|
originalMessages,
|
|
102
103
|
generateMessageId: generateId,
|
|
104
|
+
consumeSseStream: consumeStream,
|
|
103
105
|
onFinish: async ({ messages: finishedMessages, isAborted }) => {
|
|
104
106
|
if (isAborted) {
|
|
105
|
-
|
|
107
|
+
finalizeAbortedMessages(finishedMessages);
|
|
106
108
|
}
|
|
107
109
|
await config.db.messages.upsert(thread.id, finishedMessages);
|
|
108
110
|
config.onStreamFinish?.({ messages: finishedMessages, isAborted });
|
|
111
|
+
|
|
112
|
+
// XXX: we need to notify the user so that the client can
|
|
113
|
+
// fetch new messages. The client can't fetch messages
|
|
114
|
+
// immediately after abort because messages may not have been
|
|
115
|
+
// saved yet.
|
|
116
|
+
if (isAborted) {
|
|
117
|
+
notify(userId, {
|
|
118
|
+
type: "thread:messages-updated",
|
|
119
|
+
payload: { threadId: thread.id },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
109
122
|
},
|
|
110
123
|
});
|
|
111
124
|
}
|
|
@@ -135,3 +148,31 @@ going to do so or any other speech. Spit out only the title.`,
|
|
|
135
148
|
payload: { threadId, title: output ?? "" },
|
|
136
149
|
});
|
|
137
150
|
}
|
|
151
|
+
|
|
152
|
+
const TERMINAL_TOOL_STATES = new Set(["output-available", "output-error", "output-denied"]);
|
|
153
|
+
|
|
154
|
+
function finalizeAbortedMessages(messages: UIMessage[]) {
|
|
155
|
+
const lastMessage = messages.at(-1);
|
|
156
|
+
if (!lastMessage || lastMessage.role !== "assistant") return;
|
|
157
|
+
|
|
158
|
+
lastMessage.parts = lastMessage.parts.map((part) => {
|
|
159
|
+
if ((part.type === "text" || part.type === "reasoning") && part.state === "streaming") {
|
|
160
|
+
return { ...part, state: "done" as const };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if ("toolCallId" in part && "state" in part) {
|
|
164
|
+
const toolState = part.state;
|
|
165
|
+
if (!TERMINAL_TOOL_STATES.has(toolState)) {
|
|
166
|
+
const { approval: _, ...rest } = part;
|
|
167
|
+
return {
|
|
168
|
+
...rest,
|
|
169
|
+
state: "output-error" as const,
|
|
170
|
+
errorText: "Generation was aborted",
|
|
171
|
+
output: undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return part;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -2,13 +2,7 @@ import { tool } from "ai";
|
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import type { ResolvedCortexAgentConfig } from "../../config.ts";
|
|
4
4
|
import { fetchBackend } from "../fetch.ts";
|
|
5
|
-
|
|
6
|
-
type ApiHelper = {
|
|
7
|
-
get: (path: string, queryParams?: Record<string, unknown>) => Promise<unknown>;
|
|
8
|
-
post: (path: string, body?: unknown) => Promise<unknown>;
|
|
9
|
-
put: (path: string, body?: unknown) => Promise<unknown>;
|
|
10
|
-
del: (path: string) => Promise<unknown>;
|
|
11
|
-
};
|
|
5
|
+
import { getQuickJS, Scope, shouldInterruptAfterDeadline } from "quickjs-emscripten";
|
|
12
6
|
|
|
13
7
|
function buildQueryString(params: Record<string, unknown>) {
|
|
14
8
|
const searchParams = new URLSearchParams();
|
|
@@ -26,7 +20,8 @@ function buildQueryString(params: Record<string, unknown>) {
|
|
|
26
20
|
return qs ? `?${qs}` : "";
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
function
|
|
23
|
+
async function runInSandbox(
|
|
24
|
+
code: string,
|
|
30
25
|
backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
|
|
31
26
|
token: string,
|
|
32
27
|
) {
|
|
@@ -53,22 +48,82 @@ function createApiHelper(
|
|
|
53
48
|
return response.json();
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
return await Scope.withScopeAsync(async (scope) => {
|
|
52
|
+
const QuickJS = await getQuickJS();
|
|
53
|
+
|
|
54
|
+
const runtime = scope.manage(QuickJS.newRuntime());
|
|
55
|
+
runtime.setMemoryLimit(1024 * 1024 * 20); // 20MB
|
|
56
|
+
runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + 10_000)); // 60 seconds;
|
|
57
|
+
|
|
58
|
+
const vm = scope.manage(runtime.newContext());
|
|
59
|
+
|
|
60
|
+
function drainJobs() {
|
|
61
|
+
vm.runtime.executePendingJobs();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeApiFunction(fn: (...args: unknown[]) => Promise<unknown>) {
|
|
65
|
+
return scope.manage(
|
|
66
|
+
vm.newFunction(fn.name, (...argHandles) => {
|
|
67
|
+
const args = argHandles.map(vm.dump.bind(vm));
|
|
68
|
+
const promise = scope.manage(
|
|
69
|
+
vm.newPromise(
|
|
70
|
+
fn(...args).then((result) =>
|
|
71
|
+
scope.manage(
|
|
72
|
+
vm.unwrapResult(vm.evalCode(`(${JSON.stringify(result)})`)),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
promise.settled.finally(() => {
|
|
79
|
+
drainJobs();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return promise.handle;
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const apiHandle = scope.manage(vm.newObject());
|
|
88
|
+
vm.setProp(
|
|
89
|
+
apiHandle,
|
|
90
|
+
"get",
|
|
91
|
+
makeApiFunction(async (path, queryParams?) => {
|
|
92
|
+
const fullPath = queryParams
|
|
93
|
+
? path + buildQueryString(queryParams as Record<string, unknown>)
|
|
94
|
+
: path;
|
|
95
|
+
return await request("GET", fullPath as string);
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
vm.setProp(
|
|
99
|
+
apiHandle,
|
|
100
|
+
"post",
|
|
101
|
+
makeApiFunction(async (path, body) => await request("POST", path as string, body)),
|
|
102
|
+
);
|
|
103
|
+
vm.setProp(
|
|
104
|
+
apiHandle,
|
|
105
|
+
"put",
|
|
106
|
+
makeApiFunction(async (path, body) => await request("PUT", path as string, body)),
|
|
107
|
+
);
|
|
108
|
+
vm.setProp(
|
|
109
|
+
apiHandle,
|
|
110
|
+
"del",
|
|
111
|
+
makeApiFunction(async (path) => await request("DELETE", path as string)),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
vm.setProp(vm.global, "api", apiHandle);
|
|
115
|
+
|
|
116
|
+
const runResult = vm.evalCode(`(async () => {
|
|
117
|
+
${code}
|
|
118
|
+
})()`);
|
|
119
|
+
const resultHandle = scope.manage(vm.unwrapResult(runResult));
|
|
120
|
+
const pending = vm.resolvePromise(resultHandle);
|
|
121
|
+
drainJobs();
|
|
122
|
+
const resolvedResult = await pending;
|
|
123
|
+
const result = scope.manage(vm.unwrapResult(resolvedResult));
|
|
124
|
+
return vm.dump(result);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
72
127
|
|
|
73
128
|
export function createExecuteCodeTool(
|
|
74
129
|
backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
|
|
@@ -86,11 +141,8 @@ export function createExecuteCodeTool(
|
|
|
86
141
|
),
|
|
87
142
|
}),
|
|
88
143
|
execute: async ({ code }) => {
|
|
89
|
-
const apiHelper = createApiHelper(backendFetch, token);
|
|
90
|
-
|
|
91
144
|
try {
|
|
92
|
-
const
|
|
93
|
-
const result = await fn(apiHelper);
|
|
145
|
+
const result = await runInSandbox(code, backendFetch, token);
|
|
94
146
|
return JSON.stringify(result);
|
|
95
147
|
} catch (e) {
|
|
96
148
|
const message = e instanceof Error ? e.message : String(e);
|
package/src/routes/chat.ts
CHANGED
package/src/ws/events.ts
CHANGED
|
@@ -3,4 +3,9 @@ export type ThreadTitleUpdatedEvent = {
|
|
|
3
3
|
payload: { threadId: string; title: string };
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type ThreadMessagesUpdatedEvent = {
|
|
7
|
+
type: "thread:messages-updated";
|
|
8
|
+
payload: { threadId: string };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type WsEvent = ThreadTitleUpdatedEvent | ThreadMessagesUpdatedEvent;
|
package/src/ws/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { addConnection, removeConnection, getConnections } from "./connections.ts";
|
|
2
2
|
export { notify } from "./notify.ts";
|
|
3
|
-
export type { WsEvent, ThreadTitleUpdatedEvent } from "./events.ts";
|
|
3
|
+
export type { WsEvent, ThreadTitleUpdatedEvent, ThreadMessagesUpdatedEvent } from "./events.ts";
|