@m6d/cortex-server 1.3.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/index.d.ts +1 -1
  11. package/dist/src/ai/prompt.d.ts +6 -1
  12. package/dist/src/config.d.ts +4 -0
  13. package/dist/src/db/schema.d.ts +19 -1
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/routes/ws.d.ts +5 -1
  16. package/dist/src/types.d.ts +32 -14
  17. package/dist/src/ws/connections.d.ts +3 -3
  18. package/dist/src/ws/events.d.ts +28 -3
  19. package/dist/src/ws/index.d.ts +1 -1
  20. package/dist/src/ws/notify.d.ts +1 -1
  21. package/package.json +1 -1
  22. package/src/adapters/database.ts +3 -0
  23. package/src/adapters/mssql.ts +26 -6
  24. package/src/ai/active-streams.test.ts +21 -0
  25. package/src/ai/active-streams.ts +123 -0
  26. package/src/ai/context/builder.ts +94 -0
  27. package/src/ai/context/compressor.ts +47 -0
  28. package/src/ai/context/index.ts +75 -0
  29. package/src/ai/context/summarizer.ts +50 -0
  30. package/src/ai/context/token-estimator.ts +60 -0
  31. package/src/ai/context/types.ts +28 -0
  32. package/src/ai/index.ts +124 -29
  33. package/src/ai/prompt.ts +21 -15
  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 +47 -2
  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 +31 -3
  47. package/src/ws/index.ts +9 -1
  48. package/src/ws/notify.ts +2 -2
@@ -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,18 +24,60 @@ 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
 
33
- return stream(messages, thread, userId, token, config, c.req.raw.signal);
37
+ return stream(messages, thread, userId, token, config);
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
  }
package/src/types.ts CHANGED
@@ -1,24 +1,38 @@
1
1
  import type { ToolUIPart, UIMessage } from "ai";
2
2
  import type { ResolvedCortexAgentConfig } from "./config";
3
+ import type { InferSelectModel } from "drizzle-orm";
4
+ import type { messages, threads } from "./db/schema";
3
5
 
4
- export type Thread = {
6
+ export type Thread = InferSelectModel<typeof threads>;
7
+
8
+ export type StoredMessage = InferSelectModel<typeof messages>;
9
+
10
+ export type ThreadSummary = {
5
11
  id: string;
6
- userId: string;
7
- agentId: string;
8
- title: string | null;
9
- session: Record<string, unknown> | null;
12
+ title?: string;
10
13
  createdAt: Date;
11
14
  updatedAt: Date;
15
+ isRunning: boolean;
12
16
  };
13
17
 
14
- export type StoredMessage = {
15
- id: string;
16
- threadId: string;
17
- text: string | null;
18
- content: UIMessage;
19
- role: "system" | "user" | "assistant" | "tool";
20
- createdAt: Date;
21
- updatedAt: Date;
18
+ export type MessageMetadata = {
19
+ modelId: string;
20
+ providerMetadata: unknown;
21
+ isAborted?: boolean;
22
+ tokenUsage?: {
23
+ input: {
24
+ noCache: number;
25
+ cacheRead: number;
26
+ cacheWrite: number;
27
+ total: number;
28
+ };
29
+ output: {
30
+ reasoning: number;
31
+ text: number;
32
+ total: number;
33
+ };
34
+ total: number;
35
+ };
22
36
  };
23
37
 
24
38
  export type CapturedFileInput = {
@@ -48,3 +62,13 @@ export type CortexAppEnv = {
48
62
  agentId: string;
49
63
  };
50
64
  };
65
+
66
+ export function toThreadSummary(thread: Thread, isRunning: boolean) {
67
+ return {
68
+ id: thread.id,
69
+ title: thread.title ?? undefined,
70
+ createdAt: thread.createdAt,
71
+ updatedAt: thread.updatedAt,
72
+ isRunning,
73
+ } satisfies ThreadSummary;
74
+ }
@@ -2,22 +2,28 @@ import type { WSContext } from "hono/ws";
2
2
 
3
3
  const connections = new Map<string, WSContext[]>();
4
4
 
5
- export function addConnection(userId: string, ws: WSContext) {
6
- const sockets = connections.get(userId) ?? [];
5
+ function getConnectionKey(userId: string, agentId: string) {
6
+ return `${userId}:${agentId}`;
7
+ }
8
+
9
+ export function addConnection(userId: string, agentId: string, ws: WSContext) {
10
+ const key = getConnectionKey(userId, agentId);
11
+ const sockets = connections.get(key) ?? [];
7
12
  sockets.push(ws);
8
- connections.set(userId, sockets);
13
+ connections.set(key, sockets);
9
14
  }
10
15
 
11
- export function removeConnection(userId: string, ws: WSContext) {
12
- let sockets = connections.get(userId) ?? [];
16
+ export function removeConnection(userId: string, agentId: string, ws: WSContext) {
17
+ const key = getConnectionKey(userId, agentId);
18
+ let sockets = connections.get(key) ?? [];
13
19
  sockets = sockets.filter((x) => x !== ws);
14
20
  if (sockets.length) {
15
- connections.set(userId, sockets);
21
+ connections.set(key, sockets);
16
22
  } else {
17
- connections.delete(userId);
23
+ connections.delete(key);
18
24
  }
19
25
  }
20
26
 
21
- export function getConnections(userId: string) {
22
- return connections.get(userId) ?? [];
27
+ export function getConnections(userId: string, agentId: string) {
28
+ return connections.get(getConnectionKey(userId, agentId)) ?? [];
23
29
  }
package/src/ws/events.ts CHANGED
@@ -1,11 +1,39 @@
1
+ import type { ThreadSummary } from "../types.ts";
2
+
3
+ export type ThreadCreatedEvent = {
4
+ type: "thread:created";
5
+ payload: { thread: ThreadSummary };
6
+ };
7
+
8
+ export type ThreadDeletedEvent = {
9
+ type: "thread:deleted";
10
+ payload: { threadId: string };
11
+ };
12
+
1
13
  export type ThreadTitleUpdatedEvent = {
2
14
  type: "thread:title-updated";
3
- payload: { threadId: string; title: string };
15
+ payload: { thread: ThreadSummary };
16
+ };
17
+
18
+ export type ThreadRunStartedEvent = {
19
+ type: "thread:run-started";
20
+ payload: { thread: ThreadSummary };
21
+ };
22
+
23
+ export type ThreadRunFinishedEvent = {
24
+ type: "thread:run-finished";
25
+ payload: { thread: ThreadSummary };
4
26
  };
5
27
 
6
28
  export type ThreadMessagesUpdatedEvent = {
7
29
  type: "thread:messages-updated";
8
- payload: { threadId: string };
30
+ payload: { threadId: string; thread: ThreadSummary };
9
31
  };
10
32
 
11
- export type WsEvent = ThreadTitleUpdatedEvent | ThreadMessagesUpdatedEvent;
33
+ export type WsEvent =
34
+ | ThreadCreatedEvent
35
+ | ThreadDeletedEvent
36
+ | ThreadTitleUpdatedEvent
37
+ | ThreadRunStartedEvent
38
+ | ThreadRunFinishedEvent
39
+ | ThreadMessagesUpdatedEvent;
package/src/ws/index.ts CHANGED
@@ -1,3 +1,11 @@
1
1
  export { addConnection, removeConnection, getConnections } from "./connections.ts";
2
2
  export { notify } from "./notify.ts";
3
- export type { WsEvent, ThreadTitleUpdatedEvent, ThreadMessagesUpdatedEvent } from "./events.ts";
3
+ export type {
4
+ WsEvent,
5
+ ThreadCreatedEvent,
6
+ ThreadDeletedEvent,
7
+ ThreadTitleUpdatedEvent,
8
+ ThreadRunStartedEvent,
9
+ ThreadRunFinishedEvent,
10
+ ThreadMessagesUpdatedEvent,
11
+ } from "./events.ts";