@m6d/cortex-server 1.7.0 → 2.0.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 (239) hide show
  1. package/README.md +8 -3
  2. package/contracts/README.md +23 -0
  3. package/contracts/graph/embed.ts +50 -0
  4. package/{src → contracts}/graph/helpers.ts +4 -10
  5. package/{src → contracts}/graph/neo4j.ts +14 -27
  6. package/contracts/graph/schema.ts +65 -0
  7. package/{src → contracts}/graph/types.ts +2 -29
  8. package/contracts/graph.ts +36 -0
  9. package/contracts/runtime.ts +208 -0
  10. package/contracts/wire.ts +143 -0
  11. package/dist/contracts/graph/embed.d.ts +22 -0
  12. package/dist/{src → contracts}/graph/helpers.d.ts +14 -12
  13. package/dist/{src → contracts}/graph/neo4j.d.ts +5 -5
  14. package/dist/contracts/graph/schema.d.ts +62 -0
  15. package/dist/{src → contracts}/graph/types.d.ts +1 -1
  16. package/dist/contracts/graph.d.ts +14 -0
  17. package/dist/contracts/runtime.d.ts +278 -0
  18. package/dist/contracts/wire.d.ts +120 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/src/lib/adapters/database/index.d.ts +61 -0
  21. package/dist/src/lib/adapters/database/message-content.d.ts +30 -0
  22. package/dist/src/lib/adapters/database/mssql/attachments.d.ts +100 -0
  23. package/dist/src/lib/adapters/database/mssql/client.d.ts +8 -0
  24. package/dist/src/lib/adapters/database/mssql/index.d.ts +194 -0
  25. package/dist/src/lib/adapters/database/mssql/llm-requests.d.ts +17 -0
  26. package/dist/src/lib/adapters/database/mssql/messages.d.ts +37 -0
  27. package/dist/src/lib/adapters/database/mssql/threads.d.ts +50 -0
  28. package/dist/src/lib/adapters/database/postgres/attachments.d.ts +113 -0
  29. package/dist/src/lib/adapters/database/postgres/client.d.ts +8 -0
  30. package/dist/src/lib/adapters/database/postgres/index.d.ts +207 -0
  31. package/dist/src/lib/adapters/database/postgres/llm-requests.d.ts +17 -0
  32. package/dist/src/lib/adapters/database/postgres/messages.d.ts +37 -0
  33. package/dist/src/lib/adapters/database/postgres/threads.d.ts +50 -0
  34. package/{src/adapters/storage.ts → dist/src/lib/adapters/storage/index.d.ts} +1 -1
  35. package/dist/src/{adapters → lib/adapters/storage}/minio.d.ts +1 -1
  36. package/dist/src/lib/ai/active-runs.d.ts +63 -0
  37. package/dist/src/lib/ai/attachments.d.ts +30 -0
  38. package/dist/src/lib/ai/backend-url.d.ts +16 -0
  39. package/dist/src/lib/ai/cc-runtime.d.ts +61 -0
  40. package/dist/src/lib/ai/commit-gate.d.ts +21 -0
  41. package/dist/src/{ai → lib/ai}/context/builder.d.ts +6 -7
  42. package/dist/src/lib/ai/context/compression-middleware.d.ts +15 -0
  43. package/dist/src/lib/ai/context/compressor.d.ts +7 -0
  44. package/dist/src/lib/ai/context/intra-turn-compressor.d.ts +24 -0
  45. package/dist/src/lib/ai/context/optimize-thread-context.d.ts +9 -0
  46. package/dist/src/lib/ai/context/summarizer.d.ts +5 -0
  47. package/dist/src/{ai → lib/ai}/context/token-estimator.d.ts +5 -8
  48. package/dist/src/{ai → lib/ai}/context/types.d.ts +2 -0
  49. package/dist/src/{ai → lib/ai}/fetch.d.ts +1 -1
  50. package/dist/src/lib/ai/finish-turn.d.ts +23 -0
  51. package/dist/src/lib/ai/helpers.d.ts +38 -0
  52. package/dist/src/lib/ai/index.d.ts +7 -0
  53. package/dist/src/lib/ai/inspector.d.ts +41 -0
  54. package/dist/src/lib/ai/interceptors/request-interceptor.d.ts +13 -0
  55. package/dist/src/{ai → lib/ai}/prompt.d.ts +5 -4
  56. package/dist/src/lib/ai/redis-runs.d.ts +41 -0
  57. package/dist/src/lib/ai/redis-stream-log.d.ts +39 -0
  58. package/dist/src/lib/ai/tools/execute-code.tool.d.ts +18 -0
  59. package/dist/src/lib/ai/tools/query-graph.tool.d.ts +13 -0
  60. package/dist/src/lib/ai/tools/read-attachment.tool.d.ts +21 -0
  61. package/dist/src/lib/ai/tools/search-common.d.ts +17 -0
  62. package/dist/src/lib/ai/tools/search-knowledge.tool.d.ts +15 -0
  63. package/dist/src/lib/ai/tools/search-services.tool.d.ts +12 -0
  64. package/dist/src/lib/ai/tools/search-tools.tool.d.ts +12 -0
  65. package/dist/src/lib/ai/turn-tools.d.ts +39 -0
  66. package/dist/src/lib/ai/vision/rasterize-pdf.d.ts +26 -0
  67. package/dist/src/lib/ai/vision/rasterize-pdf.worker.d.ts +1 -0
  68. package/dist/src/lib/ai/vision/vision-reader.d.ts +45 -0
  69. package/dist/src/lib/cc/client.d.ts +183 -0
  70. package/dist/src/lib/cc/config-cache.d.ts +20 -0
  71. package/dist/src/lib/cc/format.d.ts +23 -0
  72. package/dist/src/lib/cc/registry.d.ts +34 -0
  73. package/dist/src/lib/cc/types.d.ts +27 -0
  74. package/dist/src/lib/config.d.ts +170 -0
  75. package/dist/src/lib/db/drizzle.config.mssql.d.ts +2 -0
  76. package/dist/src/lib/db/drizzle.config.pg.d.ts +2 -0
  77. package/dist/src/lib/db/migrate.d.ts +7 -0
  78. package/dist/src/{db/schema.d.ts → lib/db/schema.mssql.d.ts} +236 -24
  79. package/dist/src/lib/db/schema.pg.d.ts +546 -0
  80. package/dist/src/{factory.d.ts → lib/factory.d.ts} +2 -12
  81. package/dist/src/lib/graph/index.d.ts +10 -0
  82. package/dist/src/{graph → lib/graph}/resolver.d.ts +2 -3
  83. package/dist/src/{index.d.ts → lib/index.d.ts} +3 -4
  84. package/dist/src/lib/redis.d.ts +19 -0
  85. package/dist/src/{routes → lib/routes}/chat.d.ts +1 -1
  86. package/dist/src/{routes → lib/routes}/files.d.ts +1 -1
  87. package/dist/src/lib/routes/owned-thread.d.ts +17 -0
  88. package/dist/src/{routes → lib/routes}/threads.d.ts +1 -1
  89. package/dist/src/{routes → lib/routes}/ws.d.ts +1 -1
  90. package/dist/src/lib/types.d.ts +58 -0
  91. package/dist/src/{ws → lib/ws}/connections.d.ts +2 -1
  92. package/index.ts +1 -1
  93. package/package.json +35 -15
  94. package/src/lib/adapters/database/index.ts +78 -0
  95. package/src/lib/adapters/database/message-content.ts +39 -0
  96. package/src/lib/adapters/database/mssql/attachments.ts +90 -0
  97. package/src/lib/adapters/database/mssql/client.ts +11 -0
  98. package/src/lib/adapters/database/mssql/index.ts +23 -0
  99. package/src/lib/adapters/database/mssql/llm-requests.ts +43 -0
  100. package/src/lib/adapters/database/mssql/messages.ts +92 -0
  101. package/src/lib/adapters/database/mssql/threads.ts +83 -0
  102. package/src/lib/adapters/database/postgres/attachments.ts +88 -0
  103. package/src/lib/adapters/database/postgres/client.ts +11 -0
  104. package/src/lib/adapters/database/postgres/index.ts +23 -0
  105. package/src/lib/adapters/database/postgres/llm-requests.ts +45 -0
  106. package/src/lib/adapters/database/postgres/messages.ts +92 -0
  107. package/src/lib/adapters/database/postgres/threads.ts +84 -0
  108. package/{dist/src/adapters/storage.d.ts → src/lib/adapters/storage/index.ts} +1 -1
  109. package/src/{adapters → lib/adapters/storage}/minio.ts +7 -6
  110. package/src/lib/ai/active-runs.ts +157 -0
  111. package/src/lib/ai/attachments.ts +144 -0
  112. package/src/lib/ai/backend-url.ts +27 -0
  113. package/src/lib/ai/cc-runtime.ts +63 -0
  114. package/src/lib/ai/commit-gate.ts +116 -0
  115. package/src/{ai → lib/ai}/context/builder.ts +12 -14
  116. package/src/lib/ai/context/compression-middleware.ts +44 -0
  117. package/src/lib/ai/context/compressor.ts +267 -0
  118. package/src/lib/ai/context/intra-turn-compressor.ts +181 -0
  119. package/src/lib/ai/context/optimize-thread-context.ts +50 -0
  120. package/src/lib/ai/context/summarizer.ts +48 -0
  121. package/src/lib/ai/context/token-estimator.ts +65 -0
  122. package/src/{ai → lib/ai}/context/types.ts +4 -1
  123. package/src/{ai → lib/ai}/fetch.ts +10 -5
  124. package/src/lib/ai/finish-turn.ts +75 -0
  125. package/src/lib/ai/helpers.ts +100 -0
  126. package/src/lib/ai/index.ts +408 -0
  127. package/src/lib/ai/inspector.ts +112 -0
  128. package/src/lib/ai/interceptors/request-interceptor.ts +89 -0
  129. package/src/lib/ai/prompt.ts +355 -0
  130. package/src/lib/ai/redis-runs.ts +167 -0
  131. package/src/lib/ai/redis-stream-log.ts +219 -0
  132. package/src/lib/ai/tools/execute-code.tool.ts +267 -0
  133. package/src/{ai → lib/ai}/tools/query-graph.tool.ts +7 -8
  134. package/src/lib/ai/tools/read-attachment.tool.ts +59 -0
  135. package/src/lib/ai/tools/search-common.ts +15 -0
  136. package/src/lib/ai/tools/search-knowledge.tool.ts +33 -0
  137. package/src/lib/ai/tools/search-services.tool.ts +31 -0
  138. package/src/lib/ai/tools/search-tools.tool.ts +29 -0
  139. package/src/lib/ai/turn-tools.ts +154 -0
  140. package/src/lib/ai/vision/rasterize-pdf.ts +69 -0
  141. package/src/lib/ai/vision/rasterize-pdf.worker.ts +81 -0
  142. package/src/lib/ai/vision/vision-reader.ts +130 -0
  143. package/src/{auth → lib/auth}/middleware.ts +12 -3
  144. package/src/lib/cc/client.ts +273 -0
  145. package/src/lib/cc/config-cache.ts +32 -0
  146. package/src/lib/cc/format.ts +96 -0
  147. package/src/lib/cc/registry.ts +62 -0
  148. package/src/lib/cc/types.ts +44 -0
  149. package/src/lib/config.ts +212 -0
  150. package/src/lib/db/drizzle.config.mssql.ts +9 -0
  151. package/src/lib/db/drizzle.config.pg.ts +9 -0
  152. package/src/lib/db/migrate.ts +56 -0
  153. package/src/{db/migrations/20260309012148_cloudy_maria_hill → lib/db/migrations/mssql/20260326231647_nice_speedball}/migration.sql +12 -0
  154. package/src/{db/migrations/20260309012148_cloudy_maria_hill → lib/db/migrations/mssql/20260326231647_nice_speedball}/snapshot.json +106 -1
  155. package/src/lib/db/migrations/mssql/20260729092726_talented_wilson_fisk/migration.sql +20 -0
  156. package/src/lib/db/migrations/mssql/20260729092726_talented_wilson_fisk/snapshot.json +497 -0
  157. package/src/lib/db/migrations/mssql/20260801003652_perpetual_blue_shield/migration.sql +1 -0
  158. package/src/lib/db/migrations/mssql/20260801003652_perpetual_blue_shield/snapshot.json +516 -0
  159. package/src/lib/db/migrations/mssql/20260812174642_user_id_text/migration.sql +2 -0
  160. package/src/lib/db/migrations/mssql/20260812174642_user_id_text/snapshot.json +516 -0
  161. package/src/lib/db/migrations/pg/20260801084117_thick_ben_grimm/migration.sql +53 -0
  162. package/src/lib/db/migrations/pg/20260801084117_thick_ben_grimm/snapshot.json +584 -0
  163. package/src/lib/db/migrations/pg/20260812174622_user_id_text/migration.sql +2 -0
  164. package/src/lib/db/migrations/pg/20260812174622_user_id_text/snapshot.json +584 -0
  165. package/src/lib/db/schema.mssql.ts +94 -0
  166. package/src/lib/db/schema.pg.ts +95 -0
  167. package/src/lib/factory.ts +127 -0
  168. package/src/lib/graph/index.ts +19 -0
  169. package/src/lib/graph/resolver.ts +387 -0
  170. package/src/{index.ts → lib/index.ts} +13 -5
  171. package/src/lib/redis.ts +44 -0
  172. package/src/lib/routes/chat.ts +115 -0
  173. package/src/lib/routes/files.ts +129 -0
  174. package/src/lib/routes/owned-thread.ts +17 -0
  175. package/src/{routes → lib/routes}/threads.ts +22 -31
  176. package/src/{routes → lib/routes}/ws.ts +9 -3
  177. package/src/lib/types.ts +76 -0
  178. package/src/{ws → lib/ws}/connections.ts +9 -1
  179. package/tsconfig.json +48 -0
  180. package/dist/src/adapters/database.d.ts +0 -30
  181. package/dist/src/adapters/mssql.d.ts +0 -3
  182. package/dist/src/ai/active-streams.d.ts +0 -14
  183. package/dist/src/ai/context/compressor.d.ts +0 -7
  184. package/dist/src/ai/context/index.d.ts +0 -15
  185. package/dist/src/ai/context/summarizer.d.ts +0 -5
  186. package/dist/src/ai/helpers.d.ts +0 -5
  187. package/dist/src/ai/index.d.ts +0 -4
  188. package/dist/src/ai/interceptors/request-interceptor.d.ts +0 -12
  189. package/dist/src/ai/tools/call-endpoint.tool.d.ts +0 -7
  190. package/dist/src/ai/tools/capture-files.tool.d.ts +0 -6
  191. package/dist/src/ai/tools/execute-code.tool.d.ts +0 -4
  192. package/dist/src/ai/tools/query-graph.tool.d.ts +0 -5
  193. package/dist/src/cli/extract-endpoints.d.ts +0 -6
  194. package/dist/src/config.d.ts +0 -165
  195. package/dist/src/db/migrate.d.ts +0 -1
  196. package/dist/src/graph/expand-domains.d.ts +0 -2
  197. package/dist/src/graph/generate-cypher.d.ts +0 -22
  198. package/dist/src/graph/index.d.ts +0 -11
  199. package/dist/src/graph/seed.d.ts +0 -19
  200. package/dist/src/graph/validate.d.ts +0 -2
  201. package/dist/src/routes/index.d.ts +0 -4
  202. package/dist/src/types.d.ts +0 -74
  203. package/dist/src/ws/events.d.ts +0 -39
  204. package/dist/src/ws/index.d.ts +0 -3
  205. package/dist/src/ws/notify.d.ts +0 -2
  206. package/src/adapters/database.ts +0 -29
  207. package/src/adapters/mssql.ts +0 -195
  208. package/src/ai/active-streams.ts +0 -123
  209. package/src/ai/context/compressor.ts +0 -47
  210. package/src/ai/context/index.ts +0 -75
  211. package/src/ai/context/summarizer.ts +0 -50
  212. package/src/ai/context/token-estimator.ts +0 -60
  213. package/src/ai/helpers.ts +0 -32
  214. package/src/ai/index.ts +0 -277
  215. package/src/ai/interceptors/request-interceptor.ts +0 -61
  216. package/src/ai/prompt.ts +0 -126
  217. package/src/ai/tools/call-endpoint.tool.ts +0 -89
  218. package/src/ai/tools/capture-files.tool.ts +0 -20
  219. package/src/ai/tools/execute-code.tool.ts +0 -153
  220. package/src/cli/extract-endpoints.ts +0 -550
  221. package/src/config.ts +0 -182
  222. package/src/db/migrate.ts +0 -21
  223. package/src/db/migrations/20260315000000_add_context_meta/migration.sql +0 -1
  224. package/src/db/schema.ts +0 -70
  225. package/src/factory.ts +0 -171
  226. package/src/graph/expand-domains.ts +0 -276
  227. package/src/graph/generate-cypher.ts +0 -192
  228. package/src/graph/index.ts +0 -47
  229. package/src/graph/resolver.ts +0 -357
  230. package/src/graph/seed.ts +0 -162
  231. package/src/graph/validate.ts +0 -78
  232. package/src/routes/chat.ts +0 -86
  233. package/src/routes/files.ts +0 -100
  234. package/src/routes/index.ts +0 -4
  235. package/src/types.ts +0 -74
  236. package/src/ws/events.ts +0 -39
  237. package/src/ws/index.ts +0 -11
  238. package/src/ws/notify.ts +0 -9
  239. /package/dist/src/{auth → lib/auth}/middleware.d.ts +0 -0
@@ -0,0 +1,44 @@
1
+ import { Redis } from "ioredis";
2
+
3
+ /**
4
+ * The two long-lived connections everything Redis-backed shares. A subscriber
5
+ * connection cannot issue normal commands, so pub/sub listening gets its own;
6
+ * the publisher carries everything else (claims, stream appends, PUBLISH).
7
+ * Blocking stream reads never run on either — each reader duplicates its own
8
+ * connection, or one blocked XREAD would stall every claim and append behind it.
9
+ */
10
+ export type RedisConnections = {
11
+ publisher: Redis;
12
+ subscriber: Redis;
13
+ };
14
+
15
+ /**
16
+ * Without an `error` listener ioredis raises unhandled-error events, which can
17
+ * crash the process — with one, a flapping or unreachable Redis becomes a log
18
+ * line while ioredis reconnects underneath.
19
+ */
20
+ export function withRedisLogging(client: Redis, role: string) {
21
+ client.on("error", (error: unknown) => console.error(`[cortex-server] redis ${role}:`, error));
22
+ return client;
23
+ }
24
+
25
+ /**
26
+ * Cached per URL on `globalThis` so hot reloads and repeated `createCortex`
27
+ * calls reuse the connections instead of opening new ones each time.
28
+ */
29
+ const store = globalThis as typeof globalThis & {
30
+ __cortexRedis?: Map<string, RedisConnections>;
31
+ };
32
+
33
+ export function getRedisConnections(url: string) {
34
+ store.__cortexRedis ??= new Map();
35
+ let connections = store.__cortexRedis.get(url);
36
+ if (!connections) {
37
+ connections = {
38
+ publisher: withRedisLogging(new Redis(url), "publisher"),
39
+ subscriber: withRedisLogging(new Redis(url), "subscriber"),
40
+ };
41
+ store.__cortexRedis.set(url, connections);
42
+ }
43
+ return connections;
44
+ }
@@ -0,0 +1,115 @@
1
+ import { EventType, chatParamsFromRequest, resumeServerSentEventsResponse } from "@tanstack/ai";
2
+ import type { AbortRunResult } from "@cortex/contracts/wire";
3
+ import { Hono } from "hono";
4
+ import { bodyLimit } from "hono/body-limit";
5
+ import { HTTPException } from "hono/http-exception";
6
+ import type { CortexAppEnv } from "@/types";
7
+ import { requireAuth } from "@/auth/middleware";
8
+ import { startTurn } from "@/ai/index";
9
+ import { abortRun, getRunId, joinLog, offsetBelongsToRun } from "@/ai/active-runs";
10
+ import { notify } from "@/ws/connections";
11
+ import { requireOwnedThread } from "./owned-thread";
12
+ import { toThreadSummary } from "@/types";
13
+
14
+ const CHAT_BODY_MAX_BYTES = 1024 * 1024;
15
+
16
+ /**
17
+ * The answer to "attach me to the run on this thread" when there is no longer
18
+ * one. An empty body would be the obvious reply, but the client reads every
19
+ * attach as a stream and treats a missing one as a transport failure — so the
20
+ * run is reported over, which is what actually happened, and the client settles
21
+ * the turn from the database like any other ending.
22
+ */
23
+ function runEnded() {
24
+ const finished = { type: EventType.RUN_FINISHED, timestamp: Date.now() };
25
+
26
+ return new Response(`data: ${JSON.stringify(finished)}\n\n`, {
27
+ headers: {
28
+ "Content-Type": "text/event-stream",
29
+ "Cache-Control": "no-cache",
30
+ Connection: "keep-alive",
31
+ },
32
+ });
33
+ }
34
+
35
+ export function createChatRoutes() {
36
+ const app = new Hono<CortexAppEnv>();
37
+
38
+ app.post("/chat", requireAuth, bodyLimit({ maxSize: CHAT_BODY_MAX_BYTES }), async function (c) {
39
+ const config = c.get("agentConfig");
40
+ const { id: userId, token } = c.get("user");
41
+
42
+ let params: Awaited<ReturnType<typeof chatParamsFromRequest>>;
43
+ try {
44
+ params = await chatParamsFromRequest(c.req.raw);
45
+ } catch (thrown) {
46
+ // The AG-UI validator rejects a malformed body by throwing the
47
+ // 400 it wants sent; Hono has no such convention, so return it.
48
+ if (thrown instanceof Response) return thrown;
49
+ throw thrown;
50
+ }
51
+
52
+ const thread = await requireOwnedThread(c, params.threadId);
53
+
54
+ // A dropped connection re-POSTs the same body with Last-Event-ID.
55
+ // The run is already producing into its log, so this replays from
56
+ // where the client left off rather than starting a second turn. A
57
+ // run that has since finished sends the client back to the messages
58
+ // endpoint, which by then holds everything the log would have.
59
+ const resumeOffset = c.req.header("Last-Event-ID");
60
+ if (resumeOffset) {
61
+ const runId = await getRunId(thread.id);
62
+ if (!runId) return runEnded();
63
+ if (!offsetBelongsToRun(resumeOffset, runId)) {
64
+ throw new HTTPException(400, { message: "Invalid resume offset" });
65
+ }
66
+
67
+ return resumeServerSentEventsResponse({
68
+ adapter: joinLog(thread.id, runId, resumeOffset),
69
+ });
70
+ }
71
+
72
+ const requestContext = config.resolveRequestContext
73
+ ? await config.resolveRequestContext(c.req.raw)
74
+ : {};
75
+
76
+ // Null means a stop beat the run to its own announcement; the run is
77
+ // over, which is what actually happened.
78
+ return (
79
+ (await startTurn(params, thread, userId, token, requestContext, config)) ?? runEnded()
80
+ );
81
+ });
82
+
83
+ /** Attaches a reload, a thread switch, or a second tab to the thread's live run. */
84
+ app.get("/chat/:chatId/stream", requireAuth, async function (c) {
85
+ const chatId = c.req.param("chatId");
86
+ await requireOwnedThread(c, chatId);
87
+
88
+ const runId = await getRunId(chatId);
89
+ if (!runId) return runEnded();
90
+
91
+ return resumeServerSentEventsResponse({ adapter: joinLog(chatId, runId) });
92
+ });
93
+
94
+ app.post("/chat/:chatId/abort", requireAuth, async function (c) {
95
+ const agentId = c.get("agentId");
96
+ const userId = c.get("user").id;
97
+ const chatId = c.req.param("chatId");
98
+ const thread = await requireOwnedThread(c, chatId);
99
+
100
+ // Only announce an ending that happened: the client's stop button can
101
+ // outlive the server run (it drains the durability log after the
102
+ // producer commits), and a run-finished for a run that already ended
103
+ // can land before its own run-started and pin the thread as running.
104
+ const aborted = await abortRun(chatId);
105
+ if (aborted) {
106
+ notify(userId, agentId, {
107
+ type: "thread:run-finished",
108
+ payload: { thread: toThreadSummary(thread, false) },
109
+ });
110
+ }
111
+ return c.json({ aborted } satisfies AbortRunResult);
112
+ });
113
+
114
+ return app;
115
+ }
@@ -0,0 +1,129 @@
1
+ import {
2
+ ATTACHMENT_MAX_BYTES,
3
+ MAX_ATTACHMENTS_PER_MESSAGE,
4
+ checkAttachmentPolicy,
5
+ } from "@cortex/contracts/wire";
6
+ import { Hono } from "hono";
7
+ import { bodyLimit } from "hono/body-limit";
8
+ import { HTTPException } from "hono/http-exception";
9
+ import { toAttachmentSummary, type CortexAppEnv } from "@/types";
10
+ import { requireAuth } from "@/auth/middleware";
11
+ import type { ResolvedCortexAgentConfig } from "@/config";
12
+ import { requireOwnedThread } from "./owned-thread";
13
+
14
+ export function createFileRoutes() {
15
+ const app = new Hono<CortexAppEnv>();
16
+
17
+ app.post(
18
+ "/threads/:threadId/files",
19
+ requireAuth,
20
+ bodyLimit({ maxSize: ATTACHMENT_MAX_BYTES + 64 * 1024 }),
21
+ async function (c) {
22
+ const config = c.get("agentConfig");
23
+ const storage = requireStorage(config);
24
+
25
+ const threadId = c.req.param("threadId");
26
+ const userId = c.get("user").id;
27
+ await requireOwnedThread(c, threadId);
28
+
29
+ const file = (await c.req.parseBody())["file"];
30
+ if (!(file instanceof File)) {
31
+ throw new HTTPException(400, { message: "File is required" });
32
+ }
33
+
34
+ const policy = checkAttachmentPolicy(file.type, file.size);
35
+ if (policy !== "ok") {
36
+ throw new HTTPException(422, { message: `Attachment rejected: ${policy}` });
37
+ }
38
+
39
+ const storageKey = `attachments/${crypto.randomUUID()}`;
40
+ const bytes = Buffer.from(await file.arrayBuffer()).toString("base64");
41
+ if (!(await storage.put(storageKey, bytes))) {
42
+ throw new HTTPException(500, { message: "File upload failed" });
43
+ }
44
+
45
+ const attachment = await config.db.attachments
46
+ .createPending(
47
+ {
48
+ threadId,
49
+ userId,
50
+ messageId: null,
51
+ filename: file.name,
52
+ contentType: file.type,
53
+ sizeBytes: file.size,
54
+ storageKey,
55
+ status: "pending",
56
+ },
57
+ MAX_ATTACHMENTS_PER_MESSAGE,
58
+ )
59
+ .catch(async function (error: unknown) {
60
+ await storage.delete([storageKey]);
61
+ throw error;
62
+ });
63
+ if (!attachment) {
64
+ await storage.delete([storageKey]);
65
+ throw new HTTPException(422, { message: "Attachment limit reached" });
66
+ }
67
+
68
+ return c.json(toAttachmentSummary(attachment));
69
+ },
70
+ );
71
+
72
+ app.get("/files/:id", requireAuth, async function (c) {
73
+ const config = c.get("agentConfig");
74
+ const storage = requireStorage(config);
75
+
76
+ const attachment = await config.db.attachments.getById(c.req.param("id"), c.get("user").id);
77
+ if (!attachment) {
78
+ throw new HTTPException(404, { message: "File not found" });
79
+ }
80
+ await requireOwnedThread(c, attachment.threadId);
81
+
82
+ const filename = attachment.filename
83
+ .replace(/[\r\n"\\]/g, "")
84
+ .replace(/[^\u0020-\u007e]/g, "_");
85
+
86
+ return new Response(await storage.stream(attachment.storageKey), {
87
+ headers: {
88
+ "Content-Type": attachment.contentType,
89
+ "Content-Disposition": `attachment; filename="${filename}"; filename*=UTF-8''${encodeURIComponent(attachment.filename)}`,
90
+ "X-Content-Type-Options": "nosniff",
91
+ },
92
+ });
93
+ });
94
+
95
+ app.delete("/files/:id", requireAuth, async function (c) {
96
+ const config = c.get("agentConfig");
97
+ const storage = requireStorage(config);
98
+
99
+ const id = c.req.param("id");
100
+ const userId = c.get("user").id;
101
+ const attachment = await config.db.attachments.getById(id, userId);
102
+ if (!attachment) {
103
+ throw new HTTPException(404, { message: "File not found" });
104
+ }
105
+ await requireOwnedThread(c, attachment.threadId);
106
+ if (attachment.messageId !== null) {
107
+ throw new HTTPException(409, { message: "File is attached to a message" });
108
+ }
109
+
110
+ if (!(await storage.delete([attachment.storageKey]))) {
111
+ throw new HTTPException(502, { message: "File deletion failed" });
112
+ }
113
+
114
+ if (!(await config.db.attachments.remove(id, userId))) {
115
+ throw new HTTPException(404, { message: "File not found" });
116
+ }
117
+
118
+ return c.body(null, 204);
119
+ });
120
+
121
+ return app;
122
+ }
123
+
124
+ function requireStorage(config: ResolvedCortexAgentConfig) {
125
+ if (!config.storage) {
126
+ throw new HTTPException(503, { message: "File storage is not configured" });
127
+ }
128
+ return config.storage;
129
+ }
@@ -0,0 +1,17 @@
1
+ import type { Context } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import type { CortexAppEnv } from "@/types";
4
+
5
+ /**
6
+ * Loads a thread and asserts it belongs to both the caller and the agent in the
7
+ * path. A thread owned by another agent 404s rather than 403 on purpose — a 403
8
+ * would confirm the thread exists, making cross-agent ids probeable.
9
+ */
10
+ export async function requireOwnedThread(c: Context<CortexAppEnv>, threadId: string) {
11
+ const thread = await c.get("agentConfig").db.threads.getById(c.get("user").id, threadId);
12
+ if (!thread || thread.agentId !== c.get("agentId")) {
13
+ throw new HTTPException(404, { message: "Not found" });
14
+ }
15
+
16
+ return thread;
17
+ }
@@ -1,12 +1,12 @@
1
1
  import { Hono } from "hono";
2
2
  import { zValidator } from "@hono/zod-validator";
3
3
  import z from "zod";
4
- import type { CortexAppEnv } from "../types.ts";
5
- import { requireAuth } from "../auth/middleware.ts";
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";
4
+ import type { CortexAppEnv } from "@/types";
5
+ import { requireAuth } from "@/auth/middleware";
6
+ import { abortRun, runningThreadIds } from "@/ai/active-runs";
7
+ import { notify } from "@/ws/connections";
8
+ import { requireOwnedThread } from "./owned-thread";
9
+ import { toThreadSummary } from "@/types";
10
10
 
11
11
  export function createThreadRoutes() {
12
12
  const app = new Hono<CortexAppEnv>();
@@ -14,21 +14,19 @@ export function createThreadRoutes() {
14
14
  app.get("/threads", requireAuth, async function (c) {
15
15
  const config = c.get("agentConfig");
16
16
  const agentId = c.get("agentId");
17
- const threads = await config.db.threads
18
- .list(c.get("user").id, agentId)
19
- .then((x) => x.map((y) => toThreadSummary(y, isStreamRunning(y.id))));
20
- return c.json(threads);
17
+ const threads = await config.db.threads.list(c.get("user").id, agentId);
18
+ const running = await runningThreadIds(threads.map((thread) => thread.id));
19
+ return c.json(threads.map((thread) => toThreadSummary(thread, running.has(thread.id))));
21
20
  });
22
21
 
23
22
  app.post(
24
23
  "/threads",
25
24
  requireAuth,
26
- zValidator("json", z.object({ prompt: z.string() })),
25
+ zValidator("json", z.object({ prompt: z.string().optional() })),
27
26
  async function (c) {
28
27
  const config = c.get("agentConfig");
29
28
  const agentId = c.get("agentId");
30
29
  const userId = c.get("user").id;
31
- const { prompt } = c.req.valid("json");
32
30
  const thread = await config.db.threads.create(userId, agentId);
33
31
 
34
32
  notify(userId, agentId, {
@@ -36,8 +34,6 @@ export function createThreadRoutes() {
36
34
  payload: { thread: toThreadSummary(thread, false) },
37
35
  });
38
36
 
39
- generateTitle(thread.id, prompt, userId, config);
40
-
41
37
  return c.json(toThreadSummary(thread, false));
42
38
  },
43
39
  );
@@ -48,12 +44,9 @@ export function createThreadRoutes() {
48
44
  const userId = c.get("user").id;
49
45
  const threadId = c.req.param("threadId");
50
46
 
51
- const thread = await config.db.threads.getById(userId, threadId);
52
- if (!thread || thread.agentId !== agentId) {
53
- return c.body(null, 404);
54
- }
47
+ await requireOwnedThread(c, threadId);
55
48
 
56
- abortStream(threadId);
49
+ await abortRun(threadId);
57
50
  await config.db.threads.delete(userId, threadId);
58
51
 
59
52
  notify(userId, agentId, {
@@ -66,13 +59,9 @@ export function createThreadRoutes() {
66
59
 
67
60
  app.get("/threads/:threadId/messages", requireAuth, async function (c) {
68
61
  const config = c.get("agentConfig");
69
- const agentId = c.get("agentId");
70
62
  const userId = c.get("user").id;
71
63
  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
- }
64
+ await requireOwnedThread(c, threadId);
76
65
 
77
66
  const messages = await config.db.messages
78
67
  .list(userId, threadId)
@@ -80,20 +69,22 @@ export function createThreadRoutes() {
80
69
  return c.json(messages);
81
70
  });
82
71
 
72
+ app.get("/messages/:messageId/llm-requests", requireAuth, async function (c) {
73
+ const config = c.get("agentConfig");
74
+ const messageId = c.req.param("messageId");
75
+ const requests = await config.db.llmRequests.listByMessageId(messageId);
76
+ return c.json(requests);
77
+ });
78
+
83
79
  app.post(
84
80
  "/threads/:threadId/session",
85
81
  requireAuth,
86
- zValidator("json", z.object({ session: z.record(z.unknown()) })),
82
+ zValidator("json", z.object({ session: z.record(z.string(), z.unknown()) })),
87
83
  async function (c) {
88
84
  const config = c.get("agentConfig");
89
- const agentId = c.get("agentId");
90
- const userId = c.get("user").id;
91
85
  const threadId = c.req.param("threadId");
92
86
  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
- }
87
+ await requireOwnedThread(c, threadId);
97
88
 
98
89
  await config.db.threads.updateSession(threadId, session);
99
90
  return c.body(null, 204);
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { upgradeWebSocket } from "hono/bun";
3
- import type { AppEnv } from "../types.ts";
4
- import { addConnection, removeConnection } from "../ws/index.ts";
3
+ import type { AppEnv } from "@/types";
4
+ import { addConnection, removeConnection } from "@/ws/connections";
5
5
 
6
6
  type WsRouteOptions = {
7
7
  useAgentParam?: boolean;
@@ -10,7 +10,9 @@ type WsRouteOptions = {
10
10
  export function createWsRoute(options?: WsRouteOptions) {
11
11
  const app = new Hono<AppEnv>();
12
12
  const handleUpgrade = upgradeWebSocket(function (c) {
13
- const userId = c.get("user")?.id;
13
+ // upgradeWebSocket erases the app's env generic, so the context reads as any.
14
+ const user = c.get("user") as AppEnv["Variables"]["user"];
15
+ const userId = user?.id;
14
16
  const agentId =
15
17
  options?.useAgentParam === true ? c.req.param("agentId") : c.req.query("agentId");
16
18
 
@@ -40,6 +42,10 @@ export function createWsRoute(options?: WsRouteOptions) {
40
42
  return c.json({ error: "agentId is required" }, 400);
41
43
  }
42
44
 
45
+ // upgradeWebSocket types as a void middleware while tsc requires the route
46
+ // to produce a Response; the lint checker resolves the generics differently
47
+ // and would auto-strip the bridge cast, so pin it.
48
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
43
49
  return handleUpgrade(c, next) as unknown as Response;
44
50
  });
45
51
 
@@ -0,0 +1,76 @@
1
+ import type { MessagePart } from "@tanstack/ai";
2
+ import type { ResolvedCortexAgentConfig } from "./config";
3
+ import type { InferSelectModel } from "drizzle-orm";
4
+ import type { attachments, messages, threads } from "./db/schema.mssql";
5
+
6
+ export type {
7
+ CortexMessage,
8
+ MessageMetadata,
9
+ ThreadSummary,
10
+ TokenUsage,
11
+ } from "@cortex/contracts/wire";
12
+
13
+ import type { AttachmentSummary, CortexMessage, ThreadSummary } from "@cortex/contracts/wire";
14
+
15
+ /** A message as cortex persists and serves it: SDK parts plus cortex's envelope. */
16
+ export type ChatMessage = CortexMessage<MessagePart>;
17
+
18
+ export type Thread = InferSelectModel<typeof threads>;
19
+
20
+ export type StoredMessage = InferSelectModel<typeof messages>;
21
+
22
+ export type Attachment = InferSelectModel<typeof attachments>;
23
+
24
+ export type NewAttachment = typeof attachments.$inferInsert;
25
+
26
+ export type AppEnv = {
27
+ // Hono's env generics only stay assignable across helpers with the {} idiom.
28
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
29
+ Bindings: {};
30
+ Variables: {
31
+ user: { id: string; token: string } | undefined;
32
+ };
33
+ };
34
+
35
+ export type AuthedAppEnv = {
36
+ // Hono's env generics only stay assignable across helpers with the {} idiom.
37
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
38
+ Bindings: {};
39
+ Variables: {
40
+ user: { id: string; token: string };
41
+ };
42
+ };
43
+
44
+ export type CortexAppEnv = {
45
+ // Hono's env generics only stay assignable across helpers with the {} idiom.
46
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
47
+ Bindings: {};
48
+ Variables: {
49
+ user: { id: string; token: string };
50
+ agentConfig: ResolvedCortexAgentConfig;
51
+ agentId: string;
52
+ };
53
+ };
54
+
55
+ /** Maps a stored row to the wire shape. Dates become ISO strings — the same bytes
56
+ * `JSON.stringify` would have produced, but now the type says so. */
57
+ export function toThreadSummary(thread: Thread, isRunning: boolean) {
58
+ return {
59
+ id: thread.id,
60
+ title: thread.title ?? undefined,
61
+ createdAt: thread.createdAt.toISOString(),
62
+ updatedAt: thread.updatedAt.toISOString(),
63
+ isRunning,
64
+ } satisfies ThreadSummary;
65
+ }
66
+
67
+ export function toAttachmentSummary(attachment: Attachment) {
68
+ return {
69
+ id: attachment.id,
70
+ filename: attachment.filename,
71
+ contentType: attachment.contentType,
72
+ sizeBytes: attachment.sizeBytes,
73
+ status: attachment.status,
74
+ description: attachment.description ?? undefined,
75
+ } satisfies AttachmentSummary;
76
+ }
@@ -1,4 +1,5 @@
1
1
  import type { WSContext } from "hono/ws";
2
+ import type { WsEvent } from "@cortex/contracts/wire";
2
3
 
3
4
  const connections = new Map<string, WSContext[]>();
4
5
 
@@ -24,6 +25,13 @@ export function removeConnection(userId: string, agentId: string, ws: WSContext)
24
25
  }
25
26
  }
26
27
 
27
- export function getConnections(userId: string, agentId: string) {
28
+ function getConnections(userId: string, agentId: string) {
28
29
  return connections.get(getConnectionKey(userId, agentId)) ?? [];
29
30
  }
31
+
32
+ export function notify(userId: string, agentId: string, event: WsEvent) {
33
+ const message = JSON.stringify(event);
34
+ for (const ws of getConnections(userId, agentId)) {
35
+ ws.send(message);
36
+ }
37
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ // Named explicitly: auto-inclusion of @types/* is an editor-dependent
6
+ // heuristic, and the `Bun` global comes from here.
7
+ "types": ["bun"],
8
+ "target": "ESNext",
9
+ "module": "Preserve",
10
+ "moduleDetection": "force",
11
+ "jsx": "react-jsx",
12
+ "allowJs": true,
13
+ "baseUrl": ".",
14
+
15
+ // Bundler mode
16
+ "moduleResolution": "bundler",
17
+ "verbatimModuleSyntax": true,
18
+ "noEmit": true,
19
+ "paths": {
20
+ "@m6d/cortex-server": ["./index.ts"],
21
+ // The API contracts live in internal/contracts, a private workspace
22
+ // package that never publishes; this package ships raw source, so
23
+ // `vendor` copies them into ./contracts for build and pack. The first
24
+ // candidate exists only then (and in the published tarball) — inside
25
+ // the repo resolution falls through to the workspace source.
26
+ "@cortex/contracts/*": ["./contracts/*", "../../internal/contracts/*"],
27
+ // Internal-only. tsc does not rewrite aliases on emit, so `build` runs
28
+ // tsc-alias to turn these back into relative paths in dist/**/*.d.ts —
29
+ // a consumer's tsc knows nothing about our `@/`. The published raw src
30
+ // keeps `@/`, which resolves because Bun reads this file (hence its
31
+ // presence in package.json `files`).
32
+ "@/*": ["./src/lib/*"]
33
+ },
34
+
35
+ // Best practices
36
+ "strict": true,
37
+ "skipLibCheck": true,
38
+ "noFallthroughCasesInSwitch": true,
39
+ "noUncheckedIndexedAccess": true,
40
+ "noImplicitOverride": true,
41
+
42
+ // Some stricter flags (disabled by default)
43
+ "noUnusedLocals": false,
44
+ "noUnusedParameters": false,
45
+ "noPropertyAccessFromIndexSignature": false
46
+ },
47
+ "include": ["src/lib/**/*.ts", "scripts/**/*.ts", "index.ts"]
48
+ }
@@ -1,30 +0,0 @@
1
- import type { ToolUIPart, UIMessage } from "ai";
2
- import type { Thread, StoredMessage, CapturedFileInput } from "../types";
3
- import type { ThreadContextMeta } from "../ai/context/types.ts";
4
- export type DatabaseAdapter = {
5
- threads: {
6
- list(userId: string, agentId: string): Promise<Thread[]>;
7
- getById(userId: string, threadId: string): Promise<Thread | null>;
8
- create(userId: string, agentId: string): Promise<Thread>;
9
- delete(userId: string, threadId: string): Promise<void>;
10
- touch(threadId: string): Promise<Thread>;
11
- updateTitle(threadId: string, title: string): Promise<void>;
12
- updateSession(threadId: string, session: Record<string, unknown>): Promise<void>;
13
- updateContextMeta(threadId: string, meta: ThreadContextMeta): Promise<void>;
14
- };
15
- messages: {
16
- list(userId: string, threadId: string, opts?: {
17
- limit?: number;
18
- }): Promise<StoredMessage[]>;
19
- upsert(threadId: string, messages: UIMessage[]): Promise<void>;
20
- };
21
- capturedFiles: {
22
- create(userId: string, messageId: string, toolPart: ToolUIPart, files: CapturedFileInput[]): Promise<{
23
- id: string;
24
- uploadId: string;
25
- }[]>;
26
- getById(id: string, userId: string): Promise<{
27
- name: string;
28
- } | null>;
29
- };
30
- };
@@ -1,3 +0,0 @@
1
- import type { DatabaseAdapter } from "./database";
2
- import type { StorageAdapter } from "./storage";
3
- export declare function createMssqlAdapter(connectionString: string, storage: StorageAdapter): DatabaseAdapter;
@@ -1,14 +0,0 @@
1
- type ActiveStream = {
2
- id: string;
3
- abortController: AbortController;
4
- buffer: string[];
5
- subscribers: Set<ReadableStreamDefaultController<string>>;
6
- isComplete: boolean;
7
- };
8
- export declare function registerStream(threadId: string, abortController: AbortController): ActiveStream;
9
- export declare function attachSseStream(threadId: string, sseStream: ReadableStream<string>): void;
10
- export declare function subscribe(threadId: string): ReadableStream<string> | null;
11
- export declare function abortStream(threadId: string): boolean;
12
- export declare function isStreamRunning(threadId: string): boolean;
13
- export declare function removeStream(threadId: string, streamId?: string): void;
14
- export {};
@@ -1,7 +0,0 @@
1
- import type { UIMessage } from "ai";
2
- import type { MessageMetadata } from "src/types.ts";
3
- /**
4
- * Returns a new array of messages with large tool outputs truncated
5
- * to `maxTokensPerResult`. Does not mutate the input messages.
6
- */
7
- export declare function compressToolResults(messages: UIMessage<MessageMetadata>[], maxTokensPerResult: number): UIMessage<MessageMetadata, import("ai").UIDataTypes, import("ai").UITools>[];