@m6d/cortex-server 1.2.0 → 1.4.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.
Files changed (48) hide show
  1. package/dist/src/adapters/database.d.ts +3 -0
  2. package/dist/src/ai/active-streams.d.ts +14 -0
  3. package/dist/src/ai/active-streams.test.d.ts +1 -0
  4. package/dist/src/ai/context/builder.d.ts +24 -0
  5. package/dist/src/ai/context/compressor.d.ts +7 -0
  6. package/dist/src/ai/context/index.d.ts +15 -0
  7. package/dist/src/ai/context/summarizer.d.ts +5 -0
  8. package/dist/src/ai/context/token-estimator.d.ts +20 -0
  9. package/dist/src/ai/context/types.d.ts +20 -0
  10. package/dist/src/ai/prompt.d.ts +6 -1
  11. package/dist/src/config.d.ts +4 -0
  12. package/dist/src/db/schema.d.ts +19 -1
  13. package/dist/src/index.d.ts +1 -0
  14. package/dist/src/routes/ws.d.ts +5 -1
  15. package/dist/src/types.d.ts +32 -14
  16. package/dist/src/ws/connections.d.ts +3 -3
  17. package/dist/src/ws/events.d.ts +33 -2
  18. package/dist/src/ws/index.d.ts +1 -1
  19. package/dist/src/ws/notify.d.ts +1 -1
  20. package/package.json +3 -2
  21. package/src/adapters/database.ts +3 -0
  22. package/src/adapters/mssql.ts +26 -6
  23. package/src/ai/active-streams.test.ts +21 -0
  24. package/src/ai/active-streams.ts +123 -0
  25. package/src/ai/context/builder.ts +94 -0
  26. package/src/ai/context/compressor.ts +47 -0
  27. package/src/ai/context/index.ts +75 -0
  28. package/src/ai/context/summarizer.ts +50 -0
  29. package/src/ai/context/token-estimator.ts +60 -0
  30. package/src/ai/context/types.ts +28 -0
  31. package/src/ai/index.ts +158 -22
  32. package/src/ai/prompt.ts +21 -15
  33. package/src/ai/tools/execute-code.tool.ts +79 -27
  34. package/src/ai/tools/query-graph.tool.ts +1 -1
  35. package/src/cli/extract-endpoints.ts +18 -18
  36. package/src/config.ts +4 -0
  37. package/src/db/migrations/20260315000000_add_context_meta/migration.sql +1 -0
  38. package/src/db/schema.ts +6 -1
  39. package/src/factory.ts +11 -1
  40. package/src/index.ts +2 -0
  41. package/src/routes/chat.ts +46 -1
  42. package/src/routes/threads.ts +46 -9
  43. package/src/routes/ws.ts +37 -23
  44. package/src/types.ts +37 -13
  45. package/src/ws/connections.ts +15 -9
  46. package/src/ws/events.ts +35 -2
  47. package/src/ws/index.ts +9 -1
  48. package/src/ws/notify.ts +2 -2
@@ -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);
@@ -24,7 +24,7 @@ ORDER BY score DESC;
24
24
  .string()
25
25
  .optional()
26
26
  .describe(
27
- 'Optional JSON-encoded string of query parameters. Example: `{"name": "LeaveBalance"}` if you know the exact name; for parameters that need to be embedded first prepend the name with `#`, e.g., `{"#paramName": "Text to be embedded before passed to query"}`',
27
+ 'Optional JSON-encoded string of query parameters. Example: `{"name": "LeaveBalance"}` if you know the exact name; for parameters that need to be embedded first prepend the name with `#`, e.g., `{"#embedding": "Text to be embedded before passed to query"}`',
28
28
  ),
29
29
  }),
30
30
  execute: async ({ query, parameters }) => {
@@ -39,7 +39,7 @@ const AUTO_START = "// @auto-generated-start";
39
39
  const AUTO_END = "// @auto-generated-end";
40
40
  const MAX_DEPTH = 8;
41
41
 
42
- const toCamelCase = (s: string): string =>
42
+ const toCamelCase = (s: string) =>
43
43
  s
44
44
  .split(".")
45
45
  .map((seg) => seg.replace(/^[A-Z]/, (c) => c.toLowerCase()))
@@ -54,13 +54,13 @@ const dedupe = (items: Prop[]): Prop[] => {
54
54
  return items.filter((x) => (seen.has(x.name) ? false : (seen.add(x.name), true)));
55
55
  };
56
56
 
57
- function normalizePath(raw: string): string {
57
+ function normalizePath(raw: string) {
58
58
  let p = raw.startsWith("/") ? raw : `/${raw}`;
59
59
  p = p.replace(/\{([^}:?]+)(?::[^}]+)?\??\}/g, "{$1}").replace(/^\/api(?=\/|$)/i, "") || "/";
60
60
  return p.length > 1 ? p.replace(/\/+$/, "") : p;
61
61
  }
62
62
 
63
- function walkFiles(dir: string, accept: (filePath: string) => boolean): string[] {
63
+ function walkFiles(dir: string, accept: (filePath: string) => boolean) {
64
64
  if (!fs.existsSync(dir)) return [];
65
65
  const out: string[] = [];
66
66
  function walk(d: string) {
@@ -74,7 +74,7 @@ function walkFiles(dir: string, accept: (filePath: string) => boolean): string[]
74
74
  return out;
75
75
  }
76
76
 
77
- function endpointFiles(root: string): Map<string, string> {
77
+ function endpointFiles(root: string) {
78
78
  const out = new Map<string, string>();
79
79
  for (const filePath of walkFiles(root, (p) => p.endsWith(".endpoint.ts"))) {
80
80
  const c = fs.readFileSync(filePath, "utf8");
@@ -85,7 +85,7 @@ function endpointFiles(root: string): Map<string, string> {
85
85
  return out;
86
86
  }
87
87
 
88
- function resolvePointer(doc: unknown, ref: string): unknown {
88
+ function resolvePointer(doc: unknown, ref: string) {
89
89
  if (!ref.startsWith("#/")) return null;
90
90
  let cur: unknown = doc;
91
91
  for (const part of ref.slice(2).split("/")) {
@@ -96,7 +96,7 @@ function resolvePointer(doc: unknown, ref: string): unknown {
96
96
  return cur;
97
97
  }
98
98
 
99
- function deref(doc: unknown, v: unknown): Obj {
99
+ function deref(doc: unknown, v: unknown) {
100
100
  if (!isObj(v)) return {};
101
101
  let x = v;
102
102
  const seen = new Set<string>();
@@ -108,7 +108,7 @@ function deref(doc: unknown, v: unknown): Obj {
108
108
  return x;
109
109
  }
110
110
 
111
- function schemaType(s: Obj): string {
111
+ function schemaType(s: Obj) {
112
112
  const t = Array.isArray(s.type)
113
113
  ? s.type.find((x) => typeof x === "string" && x !== "null")
114
114
  : s.type;
@@ -133,7 +133,7 @@ function isJsonNodeSchema(s: Obj) {
133
133
  );
134
134
  }
135
135
 
136
- function scalarType(s: Obj): string {
136
+ function scalarType(s: Obj) {
137
137
  if (Array.isArray(s.enum) && s.enum.every((v) => typeof v === "string")) {
138
138
  return s.enum.map((v) => `'${String(v).replace(/'/g, "\\'")}'`).join(" | ");
139
139
  }
@@ -149,7 +149,7 @@ function scalarType(s: Obj): string {
149
149
  return "unknown";
150
150
  }
151
151
 
152
- function toProps(doc: unknown, schemaIn: unknown, depth = 0): Prop[] {
152
+ function toProps(doc: unknown, schemaIn: unknown, depth = 0) {
153
153
  if (depth > MAX_DEPTH) return [];
154
154
  const s = deref(doc, schemaIn);
155
155
  if (isJsonNodeRef(schemaIn) || isJsonNodeSchema(s)) return [];
@@ -198,7 +198,7 @@ function toProp(doc: unknown, name: string, schemaIn: unknown, required: boolean
198
198
  return { name, required: req, type: scalarType(s) };
199
199
  }
200
200
 
201
- function pickContent(contentIn: unknown): { schema: unknown; mimes: string[] } | null {
201
+ function pickContent(contentIn: unknown) {
202
202
  const content = obj(contentIn);
203
203
  const mimes = Object.keys(content);
204
204
  if (!mimes.length) return null;
@@ -319,7 +319,7 @@ function extractEndpoint(
319
319
  method: HttpMethod,
320
320
  pathItemIn: unknown,
321
321
  operationIn: unknown,
322
- ): Endpoint {
322
+ ) {
323
323
  const pathItem = obj(pathItemIn);
324
324
  const operation = obj(operationIn);
325
325
 
@@ -393,10 +393,10 @@ function extractEndpoint(
393
393
  responseKind: parsed.responseKind,
394
394
  successStatus: parsed.successStatus,
395
395
  errorStatuses: parsed.errorStatuses,
396
- };
396
+ } satisfies Endpoint;
397
397
  }
398
398
 
399
- function parseSwaggerEndpoints(swagger: unknown): Map<string, Endpoint> {
399
+ function parseSwaggerEndpoints(swagger: unknown) {
400
400
  const out = new Map<string, Endpoint>();
401
401
  const doc = obj(swagger);
402
402
  for (const [route, pathItem] of Object.entries(obj(doc.paths))) {
@@ -410,7 +410,7 @@ function parseSwaggerEndpoints(swagger: unknown): Map<string, Endpoint> {
410
410
  return out;
411
411
  }
412
412
 
413
- function serializeProps(props: Prop[], depth: number): string {
413
+ function serializeProps(props: Prop[], depth: number) {
414
414
  return props
415
415
  .map((p) => {
416
416
  const indent = " ".repeat(depth);
@@ -431,7 +431,7 @@ function serializeProps(props: Prop[], depth: number): string {
431
431
  .join("\n");
432
432
  }
433
433
 
434
- function blockFor(endpoint: Endpoint): string {
434
+ function blockFor(endpoint: Endpoint) {
435
435
  return ` autoGenerated: {
436
436
  params: [
437
437
  ${serializeProps(endpoint.params, 2)}
@@ -461,7 +461,7 @@ function formatGeneratedFiles(cwd: string, filePaths: string[]) {
461
461
  }
462
462
  }
463
463
 
464
- async function fetchJson(url: string): Promise<unknown> {
464
+ async function fetchJson(url: string) {
465
465
  const response = await fetch(url, {
466
466
  headers: { Accept: "application/json" },
467
467
  });
@@ -471,7 +471,7 @@ async function fetchJson(url: string): Promise<unknown> {
471
471
  return response.json();
472
472
  }
473
473
 
474
- function resolveEndpoint(key: string, extracted: Map<string, Endpoint>): Endpoint | undefined {
474
+ function resolveEndpoint(key: string, extracted: Map<string, Endpoint>) {
475
475
  const [method, route] = key.split(":");
476
476
  if (!method || !route) return undefined;
477
477
  return (
@@ -483,7 +483,7 @@ function resolveEndpoint(key: string, extracted: Map<string, Endpoint>): Endpoin
483
483
  );
484
484
  }
485
485
 
486
- export async function extractEndpoints(options: ExtractEndpointsOptions): Promise<void> {
486
+ export async function extractEndpoints(options: ExtractEndpointsOptions) {
487
487
  const { swaggerUrl, domainsDir, write = false } = options;
488
488
 
489
489
  console.log(`Fetching Swagger from ${swaggerUrl}`);
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@ import type { DatabaseAdapter } from "./adapters/database";
3
3
  import type { StorageAdapter } from "./adapters/storage";
4
4
  import type { DomainDef } from "./graph/types.ts";
5
5
  import type { RequestInterceptorOptions } from "./ai/interceptors/request-interceptor.ts";
6
+ import type { ContextConfig } from "./ai/context/types.ts";
6
7
 
7
8
  export type KnowledgeConfig = {
8
9
  swagger?: { url: string };
@@ -64,6 +65,7 @@ export type CortexAgentDefinition = {
64
65
  url: string;
65
66
  apiKey: string;
66
67
  };
68
+ context?: Partial<ContextConfig>;
67
69
  knowledge?: KnowledgeConfig | null;
68
70
  };
69
71
 
@@ -110,6 +112,7 @@ export type ResolvedCortexAgentConfig = {
110
112
  args: Record<string, unknown>;
111
113
  }) => void;
112
114
  onStreamFinish?: (result: { messages: UIMessage[]; isAborted: boolean }) => void;
115
+ context: ContextConfig;
113
116
  knowledge?: KnowledgeConfig;
114
117
  };
115
118
 
@@ -144,6 +147,7 @@ export type CortexConfig = {
144
147
  url: string;
145
148
  apiKey: string;
146
149
  };
150
+ context?: Partial<ContextConfig>;
147
151
  knowledge?: KnowledgeConfig;
148
152
  agents: Record<string, CortexAgentDefinition>;
149
153
  };
@@ -0,0 +1 @@
1
+ ALTER TABLE [ai].[threads] ADD [context_meta] nvarchar(max);
package/src/db/schema.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { ToolUIPart, UIMessage } from "ai";
2
2
  import { sql } from "drizzle-orm";
3
3
  import { customType, datetime2, index, mssqlSchema, nvarchar } from "drizzle-orm/mssql-core";
4
+ import type { MessageMetadata } from "src/types";
5
+ import type { ThreadContextMeta } from "../ai/context/types.ts";
4
6
 
5
7
  const uniqueIdentifier = customType<{ data: string }>({
6
8
  dataType() {
@@ -28,6 +30,7 @@ export const threads = aiSchema.table("threads", {
28
30
  agentId: nvarchar({ length: 128 }).notNull().default("default"),
29
31
  title: nvarchar({ length: 256 }),
30
32
  session: nvarchar({ mode: "json", length: "max" }).$type<Record<string, unknown>>(),
33
+ contextMeta: nvarchar({ mode: "json", length: "max" }).$type<ThreadContextMeta>(),
31
34
  ...auditColumns,
32
35
  });
33
36
 
@@ -37,7 +40,9 @@ export const messages = aiSchema.table("messages", {
37
40
  .references(() => threads.id, { onDelete: "cascade" })
38
41
  .notNull(),
39
42
  text: nvarchar({ length: "max" }),
40
- content: nvarchar({ mode: "json", length: "max" }).notNull().$type<UIMessage>(),
43
+ content: nvarchar({ mode: "json", length: "max" })
44
+ .notNull()
45
+ .$type<UIMessage<MessageMetadata>>(),
41
46
  role: nvarchar({
42
47
  enum: ["system", "user", "assistant", "tool"],
43
48
  length: 16,
package/src/factory.ts CHANGED
@@ -2,6 +2,8 @@ import { Hono } from "hono";
2
2
  import { websocket } from "hono/bun";
3
3
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
4
4
  import type { CortexConfig, ResolvedCortexAgentConfig } from "./config.ts";
5
+ import { DEFAULT_CONTEXT_CONFIG } from "./ai/context/types.ts";
6
+ import type { ContextConfig } from "./ai/context/types.ts";
5
7
  import type { AppEnv, CortexAppEnv } from "./types.ts";
6
8
  import type { DomainDef } from "./graph/types.ts";
7
9
  import { createUserLoaderMiddleware } from "./auth/middleware.ts";
@@ -61,7 +63,7 @@ export function createCortex(config: CortexConfig) {
61
63
  const userLoader = createUserLoaderMiddleware(config.auth);
62
64
  app.use("*", userLoader);
63
65
 
64
- // WebSocket route (global, not agent-scoped)
66
+ // Compatibility WebSocket route
65
67
  app.route("/", createWsRoute());
66
68
 
67
69
  // Agent-scoped routes
@@ -72,6 +74,12 @@ export function createCortex(config: CortexConfig) {
72
74
  const agentDef = config.agents[agentId];
73
75
  if (!agentDef) return c.json({ error: "Agent not found" }, 404);
74
76
 
77
+ const resolvedContext: ContextConfig = {
78
+ ...DEFAULT_CONTEXT_CONFIG,
79
+ ...config.context,
80
+ ...agentDef.context,
81
+ };
82
+
75
83
  const resolvedConfig: ResolvedCortexAgentConfig = {
76
84
  db,
77
85
  storage,
@@ -89,6 +97,7 @@ export function createCortex(config: CortexConfig) {
89
97
  loadSessionData: agentDef.loadSessionData,
90
98
  onToolCall: agentDef.onToolCall,
91
99
  onStreamFinish: agentDef.onStreamFinish,
100
+ context: resolvedContext,
92
101
  };
93
102
 
94
103
  c.set("agentConfig", resolvedConfig);
@@ -99,6 +108,7 @@ export function createCortex(config: CortexConfig) {
99
108
  agentApp.route("/", createThreadRoutes());
100
109
  agentApp.route("/", createChatRoutes());
101
110
  agentApp.route("/", createFileRoutes());
111
+ agentApp.route("/", createWsRoute({ useAgentParam: true }));
102
112
 
103
113
  app.route("/agents/:agentId", agentApp);
104
114
 
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export type {
7
7
  StorageConfig,
8
8
  } from "./config";
9
9
 
10
+ export type { ContextConfig, ThreadContextMeta } from "./ai/context/types";
11
+
10
12
  // Types consumers may need
11
13
  export type { Thread, AppEnv } from "./types";
12
14
 
@@ -5,6 +5,9 @@ import { HTTPException } from "hono/http-exception";
5
5
  import type { CortexAppEnv } from "../types.ts";
6
6
  import { requireAuth } from "../auth/middleware.ts";
7
7
  import { stream } from "../ai/index.ts";
8
+ import { subscribe, abortStream } from "../ai/active-streams.ts";
9
+ import { notify } from "../ws/index.ts";
10
+ import { toThreadSummary } from "../types.ts";
8
11
 
9
12
  export function createChatRoutes() {
10
13
  const app = new Hono<CortexAppEnv>();
@@ -21,12 +24,13 @@ export function createChatRoutes() {
21
24
  ),
22
25
  async function (c) {
23
26
  const config = c.get("agentConfig");
27
+ const agentId = c.get("agentId");
24
28
  const { id: userId, token } = c.get("user");
25
29
  const { messages, id: threadId } = c.req.valid("json");
26
30
 
27
31
  const thread = await config.db.threads.getById(userId, threadId);
28
32
 
29
- if (!thread) {
33
+ if (!thread || thread.agentId !== agentId) {
30
34
  throw new HTTPException(404, { message: "Not found" });
31
35
  }
32
36
 
@@ -34,5 +38,46 @@ export function createChatRoutes() {
34
38
  },
35
39
  );
36
40
 
41
+ app.get("/chat/:chatId/stream", requireAuth, async function (c) {
42
+ const config = c.get("agentConfig");
43
+ const agentId = c.get("agentId");
44
+ const chatId = c.req.param("chatId");
45
+ const thread = await config.db.threads.getById(c.get("user").id, chatId);
46
+ if (!thread || thread.agentId !== agentId) {
47
+ throw new HTTPException(404, { message: "Not found" });
48
+ }
49
+
50
+ const subscriberStream = subscribe(chatId);
51
+ if (!subscriberStream) {
52
+ return c.body(null, 204);
53
+ }
54
+
55
+ return new Response(subscriberStream.pipeThrough(new TextEncoderStream()), {
56
+ headers: {
57
+ "Content-Type": "text/event-stream",
58
+ "Cache-Control": "no-cache",
59
+ Connection: "keep-alive",
60
+ },
61
+ });
62
+ });
63
+
64
+ app.post("/chat/:chatId/abort", requireAuth, async function (c) {
65
+ const config = c.get("agentConfig");
66
+ const agentId = c.get("agentId");
67
+ const userId = c.get("user").id;
68
+ const chatId = c.req.param("chatId");
69
+ const thread = await config.db.threads.getById(userId, chatId);
70
+ if (!thread || thread.agentId !== agentId) {
71
+ throw new HTTPException(404, { message: "Not found" });
72
+ }
73
+
74
+ abortStream(chatId);
75
+ notify(userId, agentId, {
76
+ type: "thread:run-finished",
77
+ payload: { thread: toThreadSummary(thread, false) },
78
+ });
79
+ return c.body(null, 204);
80
+ });
81
+
37
82
  return app;
38
83
  }
@@ -4,6 +4,9 @@ import z from "zod";
4
4
  import type { CortexAppEnv } from "../types.ts";
5
5
  import { requireAuth } from "../auth/middleware.ts";
6
6
  import { generateTitle } from "../ai/index.ts";
7
+ import { abortStream, isStreamRunning } from "../ai/active-streams.ts";
8
+ import { notify } from "../ws/index.ts";
9
+ import { toThreadSummary } from "../types.ts";
7
10
 
8
11
  export function createThreadRoutes() {
9
12
  const app = new Hono<CortexAppEnv>();
@@ -13,7 +16,7 @@ export function createThreadRoutes() {
13
16
  const agentId = c.get("agentId");
14
17
  const threads = await config.db.threads
15
18
  .list(c.get("user").id, agentId)
16
- .then((x) => x.map((y) => ({ id: y.id, title: y.title, createdAt: y.createdAt })));
19
+ .then((x) => x.map((y) => toThreadSummary(y, isStreamRunning(y.id))));
17
20
  return c.json(threads);
18
21
  });
19
22
 
@@ -24,28 +27,55 @@ export function createThreadRoutes() {
24
27
  async function (c) {
25
28
  const config = c.get("agentConfig");
26
29
  const agentId = c.get("agentId");
30
+ const userId = c.get("user").id;
27
31
  const { prompt } = c.req.valid("json");
28
- const { id, createdAt, title } = await config.db.threads.create(
29
- c.get("user").id,
30
- agentId,
31
- );
32
- generateTitle(id, prompt, c.get("user").id, config);
33
- return c.json({ id, title, createdAt });
32
+ const thread = await config.db.threads.create(userId, agentId);
33
+
34
+ notify(userId, agentId, {
35
+ type: "thread:created",
36
+ payload: { thread: toThreadSummary(thread, false) },
37
+ });
38
+
39
+ generateTitle(thread.id, prompt, userId, config);
40
+
41
+ return c.json(toThreadSummary(thread, false));
34
42
  },
35
43
  );
36
44
 
37
45
  app.delete("/threads/:threadId", requireAuth, async function (c) {
38
46
  const config = c.get("agentConfig");
47
+ const agentId = c.get("agentId");
48
+ const userId = c.get("user").id;
39
49
  const threadId = c.req.param("threadId");
40
- await config.db.threads.delete(c.get("user").id, threadId);
50
+
51
+ const thread = await config.db.threads.getById(userId, threadId);
52
+ if (!thread || thread.agentId !== agentId) {
53
+ return c.body(null, 404);
54
+ }
55
+
56
+ abortStream(threadId);
57
+ await config.db.threads.delete(userId, threadId);
58
+
59
+ notify(userId, agentId, {
60
+ type: "thread:deleted",
61
+ payload: { threadId },
62
+ });
63
+
41
64
  return c.body(null, 204);
42
65
  });
43
66
 
44
67
  app.get("/threads/:threadId/messages", requireAuth, async function (c) {
45
68
  const config = c.get("agentConfig");
69
+ const agentId = c.get("agentId");
70
+ const userId = c.get("user").id;
46
71
  const threadId = c.req.param("threadId");
72
+ const thread = await config.db.threads.getById(userId, threadId);
73
+ if (!thread || thread.agentId !== agentId) {
74
+ return c.body(null, 404);
75
+ }
76
+
47
77
  const messages = await config.db.messages
48
- .list(c.get("user").id, threadId)
78
+ .list(userId, threadId)
49
79
  .then((x) => x.map((y) => y.content));
50
80
  return c.json(messages);
51
81
  });
@@ -56,8 +86,15 @@ export function createThreadRoutes() {
56
86
  zValidator("json", z.object({ session: z.record(z.unknown()) })),
57
87
  async function (c) {
58
88
  const config = c.get("agentConfig");
89
+ const agentId = c.get("agentId");
90
+ const userId = c.get("user").id;
59
91
  const threadId = c.req.param("threadId");
60
92
  const { session } = c.req.valid("json");
93
+ const thread = await config.db.threads.getById(userId, threadId);
94
+ if (!thread || thread.agentId !== agentId) {
95
+ return c.body(null, 404);
96
+ }
97
+
61
98
  await config.db.threads.updateSession(threadId, session);
62
99
  return c.body(null, 204);
63
100
  },
package/src/routes/ws.ts CHANGED
@@ -3,31 +3,45 @@ import { upgradeWebSocket } from "hono/bun";
3
3
  import type { AppEnv } from "../types.ts";
4
4
  import { addConnection, removeConnection } from "../ws/index.ts";
5
5
 
6
- export function createWsRoute() {
6
+ type WsRouteOptions = {
7
+ useAgentParam?: boolean;
8
+ };
9
+
10
+ export function createWsRoute(options?: WsRouteOptions) {
7
11
  const app = new Hono<AppEnv>();
12
+ const handleUpgrade = upgradeWebSocket(function (c) {
13
+ const userId = c.get("user")?.id;
14
+ const agentId =
15
+ options?.useAgentParam === true ? c.req.param("agentId") : c.req.query("agentId");
16
+
17
+ return {
18
+ onOpen(_, ws) {
19
+ if (userId && agentId) {
20
+ addConnection(userId, agentId, ws);
21
+ }
22
+ },
23
+
24
+ onClose(_, ws) {
25
+ if (userId && agentId) {
26
+ removeConnection(userId, agentId, ws);
27
+ }
28
+ },
29
+ };
30
+ });
31
+
32
+ app.get("/ws", function (c, next) {
33
+ if (!c.get("user")) {
34
+ return c.json({ error: "Unauthorized" }, 401);
35
+ }
36
+
37
+ const agentId =
38
+ options?.useAgentParam === true ? c.req.param("agentId") : c.req.query("agentId");
39
+ if (!agentId) {
40
+ return c.json({ error: "agentId is required" }, 400);
41
+ }
8
42
 
9
- app.get(
10
- "/ws",
11
- upgradeWebSocket(function (c) {
12
- const user = c.get("user");
13
- const userId = user?.id;
14
-
15
- return {
16
- onOpen(_, ws) {
17
- if (userId) {
18
- addConnection(userId, ws);
19
- }
20
- ws.send("Connected!");
21
- },
22
-
23
- onClose(_, ws) {
24
- if (userId) {
25
- removeConnection(userId, ws);
26
- }
27
- },
28
- };
29
- }),
30
- );
43
+ return handleUpgrade(c, next) as unknown as Response;
44
+ });
31
45
 
32
46
  return app;
33
47
  }