@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.
@@ -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>;
@@ -5,4 +5,10 @@ export type ThreadTitleUpdatedEvent = {
5
5
  title: string;
6
6
  };
7
7
  };
8
- export type WsEvent = ThreadTitleUpdatedEvent;
8
+ export type ThreadMessagesUpdatedEvent = {
9
+ type: "thread:messages-updated";
10
+ payload: {
11
+ threadId: string;
12
+ };
13
+ };
14
+ export type WsEvent = ThreadTitleUpdatedEvent | ThreadMessagesUpdatedEvent;
@@ -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.2.0",
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
- onAbort: () => {
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
- console.log("Stream was aborted");
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 createApiHelper(
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
- get: (path, queryParams) => {
58
- const fullPath = queryParams ? path + buildQueryString(queryParams) : path;
59
- return request("GET", fullPath);
60
- },
61
- post: (path, body) => request("POST", path, body),
62
- put: (path, body) => request("PUT", path, body),
63
- del: (path) => request("DELETE", path),
64
- } satisfies ApiHelper;
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
- const AsyncFunction: new (
68
- ...args: [...paramNames: string[], body: string]
69
- ) => (...args: unknown[]) => Promise<unknown> = Object.getPrototypeOf(
70
- async function () {},
71
- ).constructor;
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 fn = new AsyncFunction("api", code);
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);
@@ -30,7 +30,7 @@ export function createChatRoutes() {
30
30
  throw new HTTPException(404, { message: "Not found" });
31
31
  }
32
32
 
33
- return stream(messages, thread, userId, token, config);
33
+ return stream(messages, thread, userId, token, config, c.req.raw.signal);
34
34
  },
35
35
  );
36
36
 
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 WsEvent = ThreadTitleUpdatedEvent;
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";