@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,95 @@
1
+ import { ATTACHMENT_STATUSES } from "@cortex/contracts/wire";
2
+ import {
3
+ index,
4
+ integer,
5
+ jsonb,
6
+ pgSchema,
7
+ text,
8
+ timestamp,
9
+ uuid,
10
+ varchar,
11
+ } from "drizzle-orm/pg-core";
12
+ import type { ChatMessage, TokenUsage } from "@/types";
13
+ import type { ThreadContextMeta } from "@/ai/context/types";
14
+
15
+ /**
16
+ * The Postgres twin of `schema.mssql.ts`. Drizzle types its schemas per dialect,
17
+ * so the two cannot be shared — `schema.parity.test.ts` asserts at compile time
18
+ * that they still infer the same rows, which is what the rest of the code sees.
19
+ */
20
+
21
+ export const aiSchema = pgSchema("ai");
22
+
23
+ const auditColumns = {
24
+ createdAt: timestamp({ mode: "date", withTimezone: true })
25
+ .$default(() => new Date())
26
+ .notNull(),
27
+ updatedAt: timestamp({ mode: "date", withTimezone: true })
28
+ .$default(() => new Date())
29
+ .$onUpdate(() => new Date())
30
+ .notNull(),
31
+ };
32
+
33
+ export const threads = aiSchema.table("threads", {
34
+ id: uuid().defaultRandom().primaryKey(),
35
+ userId: varchar({ length: 255 }).notNull(),
36
+ agentId: varchar({ length: 128 }).notNull().default("default"),
37
+ title: varchar({ length: 256 }),
38
+ session: jsonb().$type<Record<string, unknown>>(),
39
+ contextMeta: jsonb().$type<ThreadContextMeta>(),
40
+ ...auditColumns,
41
+ });
42
+
43
+ export const messages = aiSchema.table("messages", {
44
+ id: varchar({ length: 128 }).primaryKey(),
45
+ threadId: uuid()
46
+ .references(() => threads.id, { onDelete: "cascade" })
47
+ .notNull(),
48
+ text: text(),
49
+ content: jsonb().notNull().$type<ChatMessage>(),
50
+ /**
51
+ * Position within the insert batch that wrote the row. A turn commits
52
+ * several rows at once, so they share a `createdAt`; the transcript orders
53
+ * on (createdAt, ordinal) to keep them in the order the turn produced them.
54
+ */
55
+ ordinal: integer().notNull().default(0),
56
+ role: varchar({
57
+ enum: ["system", "user", "assistant", "tool"],
58
+ length: 16,
59
+ }).notNull(),
60
+ ...auditColumns,
61
+ });
62
+
63
+ export const llmRequests = aiSchema.table("llm_requests", {
64
+ id: uuid().defaultRandom().primaryKey(),
65
+ messageId: varchar({ length: 128 })
66
+ .references(() => messages.id, { onDelete: "cascade" })
67
+ .notNull(),
68
+ stepNumber: integer().notNull(),
69
+ prompt: text().notNull(),
70
+ output: text(),
71
+ tokenUsage: jsonb().$type<TokenUsage>(),
72
+ });
73
+
74
+ export const attachments = aiSchema.table(
75
+ "attachments",
76
+ {
77
+ id: uuid().defaultRandom().primaryKey(),
78
+ threadId: uuid()
79
+ .references(() => threads.id, { onDelete: "cascade" })
80
+ .notNull(),
81
+ userId: varchar({ length: 255 }).notNull(),
82
+ messageId: varchar({ length: 128 }).references(() => messages.id),
83
+ filename: varchar({ length: 2048 }).notNull(),
84
+ contentType: varchar({ length: 256 }).notNull(),
85
+ sizeBytes: integer().notNull(),
86
+ storageKey: varchar({ length: 512 }).notNull(),
87
+ status: varchar({
88
+ enum: ATTACHMENT_STATUSES,
89
+ length: 16,
90
+ }).notNull(),
91
+ description: text(),
92
+ ...auditColumns,
93
+ },
94
+ (table) => [index("attachments_thread_id_index").on(table.threadId)],
95
+ );
@@ -0,0 +1,127 @@
1
+ import { Hono } from "hono";
2
+ import { websocket } from "hono/bun";
3
+ import type { CortexConfig, ResolvedCortexAgentConfig } from "./config";
4
+ import { DEFAULT_CONTEXT_CONFIG } from "./ai/context/types";
5
+ import type { ContextConfig } from "./ai/context/types";
6
+ import type { AppEnv, CortexAppEnv } from "./types";
7
+ import { createUserLoaderMiddleware } from "./auth/middleware";
8
+ import { createThreadRoutes } from "./routes/threads";
9
+ import { createChatRoutes } from "./routes/chat";
10
+ import { createFileRoutes } from "./routes/files";
11
+ import { createWsRoute } from "./routes/ws";
12
+ import { runMigrations } from "./db/migrate";
13
+ import { configureRunCoordination } from "./ai/active-runs";
14
+ import { createRedisRuns } from "./ai/redis-runs";
15
+ import { getRedisConnections } from "./redis";
16
+ import { createMssqlAdapter } from "./adapters/database/mssql/index";
17
+ import { createPostgresAdapter } from "./adapters/database/postgres/index";
18
+ import { createMinioAdapter } from "./adapters/storage/minio";
19
+ import { ControlCenterClient } from "./cc/client";
20
+ import { getControlCenterConfig } from "./cc/config-cache";
21
+ import { AGENT_SLUG_PATTERN } from "./cc/types";
22
+
23
+ export type CortexInstance = {
24
+ serve(): Promise<{
25
+ app: Hono<AppEnv>;
26
+ port: number;
27
+ fetch: Hono<AppEnv>["fetch"];
28
+ websocket: typeof websocket;
29
+ }>;
30
+ };
31
+
32
+ export function createCortex(config: CortexConfig) {
33
+ return {
34
+ async serve() {
35
+ const storage = config.storage ? createMinioAdapter(config.storage) : undefined;
36
+ const createAdapter =
37
+ config.database.type === "postgres" ? createPostgresAdapter : createMssqlAdapter;
38
+ const db = createAdapter(config.database.connectionString, storage);
39
+ const ccClient = config.controlCenter
40
+ ? new ControlCenterClient(config.controlCenter)
41
+ : null;
42
+
43
+ if (config.redis) {
44
+ configureRunCoordination(
45
+ createRedisRuns(getRedisConnections(config.redis.url), config.redis),
46
+ );
47
+ }
48
+
49
+ await runMigrations(config.database);
50
+
51
+ const app = new Hono<AppEnv>();
52
+
53
+ const userLoader = createUserLoaderMiddleware(config.auth);
54
+ app.use("*", userLoader);
55
+
56
+ // Compatibility WebSocket route
57
+ app.route("/", createWsRoute());
58
+
59
+ // Agent-scoped routes
60
+ const agentApp = new Hono<CortexAppEnv>();
61
+
62
+ agentApp.use("*", async function (c, next) {
63
+ const agentId = c.req.param("agentId") as string;
64
+ // An id with no local entry may still be an agent published on the
65
+ // Control Center: existence is checked against it (ETag-cached, so
66
+ // usually a 304), which makes publishing there the only allowlist.
67
+ let agentDef = config.agents?.[agentId];
68
+ if (!agentDef && ccClient && AGENT_SLUG_PATTERN.test(agentId)) {
69
+ const ccConfig = await getControlCenterConfig(ccClient, agentId);
70
+ if (ccConfig) agentDef = {};
71
+ }
72
+ if (!agentDef) return c.json({ error: "Agent not found" }, 404);
73
+
74
+ const resolvedContext: ContextConfig = {
75
+ ...DEFAULT_CONTEXT_CONFIG,
76
+ ...config.context,
77
+ ...agentDef.context,
78
+ };
79
+
80
+ const resolvedConfig: ResolvedCortexAgentConfig = {
81
+ agentId,
82
+ db,
83
+ storage,
84
+ model: agentDef.model ?? config.model,
85
+ fastModel: agentDef.fastModel ?? config.fastModel,
86
+ vision: agentDef.vision ?? config.vision,
87
+ embedding: agentDef.embedding ?? config.embedding,
88
+ neo4j: agentDef.neo4j ?? config.neo4j,
89
+ reranker: agentDef.reranker ?? config.reranker,
90
+ controlCenter: config.controlCenter,
91
+ knowledge:
92
+ agentDef.knowledge === null
93
+ ? undefined
94
+ : (agentDef.knowledge ?? config.knowledge),
95
+ systemPrompt: agentDef.systemPrompt,
96
+ tools: agentDef.tools,
97
+ backendFetch: agentDef.backendFetch,
98
+ loadSessionData: agentDef.loadSessionData,
99
+ resolveRequestContext: agentDef.resolveRequestContext,
100
+ onToolCall: agentDef.onToolCall,
101
+ onStreamFinish: agentDef.onStreamFinish,
102
+ context: resolvedContext,
103
+ };
104
+
105
+ c.set("agentConfig", resolvedConfig);
106
+ c.set("agentId", agentId);
107
+ await next();
108
+ });
109
+
110
+ agentApp.route("/", createThreadRoutes());
111
+ agentApp.route("/", createChatRoutes());
112
+ agentApp.route("/", createFileRoutes());
113
+ agentApp.route("/", createWsRoute({ useAgentParam: true }));
114
+
115
+ app.route("/agents/:agentId", agentApp);
116
+
117
+ const port = config.port ?? 3331;
118
+
119
+ return {
120
+ app,
121
+ port,
122
+ fetch: app.fetch,
123
+ websocket,
124
+ };
125
+ },
126
+ } satisfies CortexInstance;
127
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The graph contract lives in `@cortex/contracts/graph` (internal/contracts),
3
+ * because `@m6d/cortex-cli` writes the graph this package reads and the two are
4
+ * installed separately (each vendors its own copy at pack time). It is
5
+ * re-exported here so authoring a project's domains still needs exactly one
6
+ * import path — nothing about the public surface moved.
7
+ */
8
+ export * from "@cortex/contracts/graph";
9
+
10
+ // Graph resolver — the reader, and the one part of the graph that is this
11
+ // package's own.
12
+ export type {
13
+ RerankerConfig,
14
+ ResolverConfig,
15
+ ResolvedEndpoint,
16
+ ResolvedService,
17
+ ResolvedContext,
18
+ } from "./resolver";
19
+ export { resolveFromGraph } from "./resolver";
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Pre-resolves relevant API endpoints from the Neo4j knowledge graph
3
+ * using vector similarity search on concept descriptions.
4
+ *
5
+ * All configuration is passed explicitly — no hardcoded model references
6
+ * or environment variables.
7
+ */
8
+
9
+ import {
10
+ GRAPH_SCHEMA,
11
+ GRAPH_SCHEMA_VERSION,
12
+ type ConceptDef,
13
+ type EmbedFn,
14
+ type Neo4jClient,
15
+ } from "@cortex/contracts/graph";
16
+
17
+ // The graph's vocabulary, aliased short so the Cypher below still reads as Cypher.
18
+ const { label: L, rel: R, prop: P } = GRAPH_SCHEMA;
19
+
20
+ /**
21
+ * Seed data, not schema: the concept a project names to expose its generic
22
+ * service-request endpoints, which is why it stays here rather than in
23
+ * `schema.ts` — the seeder writes whatever concepts a domain declares.
24
+ */
25
+ const SERVICE_REQUEST = "ServiceRequest";
26
+
27
+ export type RerankerConfig = {
28
+ url: string;
29
+ apiKey: string;
30
+ };
31
+
32
+ export type ResolverConfig = {
33
+ neo4j: Neo4jClient;
34
+ embed: EmbedFn;
35
+ reranker?: RerankerConfig;
36
+ };
37
+
38
+ type EndpointDependency = {
39
+ depPath: string;
40
+ depMethod: string;
41
+ paramName: string;
42
+ fromField: string;
43
+ };
44
+
45
+ type EndpointRelation = "read" | "write";
46
+
47
+ type RelationType = typeof R.queriedVia | typeof R.mutatedVia;
48
+
49
+ export type ResolvedEndpoint = {
50
+ concept: string;
51
+ relation: EndpointRelation;
52
+ name: string;
53
+ path: string;
54
+ method: string;
55
+ params: string;
56
+ body: string;
57
+ response: string;
58
+ dependencies: EndpointDependency[];
59
+ rules: string[];
60
+ metadata: string;
61
+ };
62
+
63
+ export type ResolvedService = {
64
+ concept: string;
65
+ serviceName: string;
66
+ builtInId: string;
67
+ description: string;
68
+ rules: string[];
69
+ metadata: string;
70
+ };
71
+
72
+ export type ResolvedContext = {
73
+ readEndpoints: ResolvedEndpoint[];
74
+ writeEndpoints: ResolvedEndpoint[];
75
+ services: ResolvedService[];
76
+ };
77
+
78
+ type EndpointRow = {
79
+ concept: string;
80
+ relationType: RelationType;
81
+ name: string;
82
+ path: string;
83
+ method: string;
84
+ params: string | null;
85
+ body: string | null;
86
+ response: string | null;
87
+ metadata: string | null;
88
+ dependencies: (EndpointDependency | { depPath: null })[];
89
+ rules: (string | null)[];
90
+ };
91
+
92
+ type ServiceRow = {
93
+ concept: string;
94
+ serviceName: string;
95
+ builtInId: string;
96
+ description: string | null;
97
+ metadata: string | null;
98
+ rules: (string | null)[];
99
+ };
100
+
101
+ /**
102
+ * Cleared only by a restart, which is also the only thing that can change which
103
+ * schema this process reads with. A re-seed under a running server is the case
104
+ * this misses, and re-seeding at the same version is the overwhelmingly common one.
105
+ */
106
+ let versionChecked = false;
107
+
108
+ /**
109
+ * `@m6d/cortex-cli` stamps the schema version it seeded onto the graph; this reads
110
+ * it back before any Cypher below runs. Lazily, on the first resolve rather than at
111
+ * boot, so a Neo4j that happens to be down cannot stop `serve()`.
112
+ */
113
+ async function assertGraphVersion(neo4j: Neo4jClient) {
114
+ if (versionChecked) return;
115
+
116
+ const rows: unknown = await neo4j
117
+ .query(
118
+ `OPTIONAL MATCH (g:${L.marker}) WITH g LIMIT 1
119
+ OPTIONAL MATCH (c:${L.concept})
120
+ RETURN g.${P.version} AS version, count(c) AS concepts`,
121
+ )
122
+ .then(JSON.parse)
123
+ .catch(() => undefined);
124
+
125
+ // An unreachable graph answers with an error object rather than a row array.
126
+ // That is the resolve's own failure to report, not schema skew.
127
+ if (!Array.isArray(rows)) return;
128
+
129
+ // OPTIONAL MATCH answers a missing marker with `null`, not with a missing row.
130
+ const { version: stamped, concepts } =
131
+ (rows as { version?: number | null; concepts?: number }[])[0] ?? {};
132
+ if (stamped === GRAPH_SCHEMA_VERSION) {
133
+ versionChecked = true;
134
+ return;
135
+ }
136
+
137
+ // No stamp over an empty graph is a project mid-setup (Neo4j configured, seed
138
+ // not yet run), not skew: resolve as the empty context it is. Not cached, so
139
+ // the stamp is validated as soon as a seed lands.
140
+ if (stamped == null && (concepts ?? 0) === 0) return;
141
+
142
+ throw new Error(
143
+ [
144
+ stamped == null
145
+ ? `The Neo4j graph holds data but no schema stamp, and this server reads graph schema v${GRAPH_SCHEMA_VERSION}.`
146
+ : `The Neo4j graph was written with graph schema v${stamped}, and this server reads v${GRAPH_SCHEMA_VERSION}.`,
147
+ "Writer and reader disagree on the labels, relationships and property names, so retrieval would return empty or wrong results instead of failing.",
148
+ "Re-seed the graph with `bunx @m6d/cortex-cli graph seed`, or install the @m6d/cortex-server that matches the graph you have.",
149
+ ].join(" "),
150
+ );
151
+ }
152
+
153
+ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
154
+ // Before the first query and outside the catch below: skew is invisible to
155
+ // every check further down, which is the whole reason it is checked here.
156
+ await assertGraphVersion(config.neo4j);
157
+
158
+ const empty: ResolvedContext = {
159
+ readEndpoints: [],
160
+ writeEndpoints: [],
161
+ services: [],
162
+ };
163
+
164
+ try {
165
+ const embedding = (await config.embed([prompt]))[0]!;
166
+
167
+ const conceptsRaw = await config.neo4j.query(
168
+ `CALL db.index.vector.queryNodes('${GRAPH_SCHEMA.conceptEmbeddingsIndex}', 10, $embedding)
169
+ YIELD node, score
170
+ WHERE score > 0.3
171
+ RETURN node.${P.name} AS name, node.${P.description} AS description, score
172
+ ORDER BY score DESC`,
173
+ { embedding },
174
+ );
175
+
176
+ let concepts = JSON.parse(conceptsRaw) as (Pick<ConceptDef, "name" | "description"> & {
177
+ score: number;
178
+ })[];
179
+
180
+ if (!Array.isArray(concepts) || concepts.length === 0) {
181
+ return empty;
182
+ }
183
+
184
+ if (config.reranker) {
185
+ concepts = await computeRelevanceScores(prompt, concepts, config.reranker);
186
+ } else {
187
+ concepts = concepts.slice(0, 3);
188
+ }
189
+
190
+ const conceptNames = concepts.map((c) => c.name);
191
+
192
+ const endpointsRaw = await config.neo4j.query(
193
+ `UNWIND $names AS conceptName
194
+ MATCH (c:${L.concept} {${P.name}: conceptName})-[:${R.specializes}*0..3]->(ancestor:${L.concept})
195
+ WITH conceptName, collect(DISTINCT ancestor) AS family
196
+ UNWIND family AS related
197
+ MATCH (related)-[rel:${R.queriedVia}|${R.mutatedVia}]->(e:${L.endpoint})
198
+ WITH conceptName, related, rel, e
199
+ OPTIONAL MATCH (e)-[d:${R.dependsOn}]->(dep:${L.endpoint})
200
+ WITH conceptName, related, rel, e,
201
+ collect(DISTINCT {
202
+ depPath: dep.${P.path},
203
+ depMethod: dep.${P.method},
204
+ paramName: d.${P.paramName},
205
+ fromField: d.${P.fromField}
206
+ }) AS dependencies
207
+ OPTIONAL MATCH (rEndpoint:${L.rule})-[:${R.governs}]->(e)
208
+ OPTIONAL MATCH (rConcept:${L.rule})-[:${R.governs}]->(related)
209
+ RETURN conceptName AS concept,
210
+ type(rel) AS relationType,
211
+ e.${P.name} AS name,
212
+ e.${P.path} AS path,
213
+ e.${P.method} AS method,
214
+ e.${P.params} AS params,
215
+ e.${P.body} AS body,
216
+ e.${P.response} AS response,
217
+ e.${P.metadata} AS metadata,
218
+ dependencies,
219
+ collect(DISTINCT rEndpoint.${P.description}) +
220
+ collect(DISTINCT rConcept.${P.description}) AS rules`,
221
+ { names: conceptNames },
222
+ );
223
+
224
+ const endpointRows = asArray<EndpointRow>(JSON.parse(endpointsRaw));
225
+ const allEndpoints = dedupeEndpoints(endpointRows.map(toResolvedEndpoint));
226
+
227
+ const servicesRaw = await config.neo4j.query(
228
+ `UNWIND $names AS conceptName
229
+ MATCH (c:${L.concept} {${P.name}: conceptName})-[:${R.specializes}*0..3]->(ancestor:${L.concept})
230
+ WITH conceptName, collect(DISTINCT ancestor) AS family
231
+ UNWIND family AS related
232
+ MATCH (related)-[:${R.requestedVia}]->(svc:${L.service})
233
+ WITH conceptName, related, svc
234
+ OPTIONAL MATCH (rService:${L.rule})-[:${R.governs}]->(svc)
235
+ OPTIONAL MATCH (rConcept:${L.rule})-[:${R.governs}]->(related)
236
+ RETURN conceptName AS concept,
237
+ svc.${P.name} AS serviceName,
238
+ svc.${P.builtInId} AS builtInId,
239
+ svc.${P.description} AS description,
240
+ svc.${P.metadata} AS metadata,
241
+ collect(DISTINCT rService.${P.description}) +
242
+ collect(DISTINCT rConcept.${P.description}) AS rules`,
243
+ { names: conceptNames },
244
+ );
245
+
246
+ const services = dedupeServices(
247
+ asArray<ServiceRow>(JSON.parse(servicesRaw)).map(function (row) {
248
+ return {
249
+ concept: row.concept,
250
+ serviceName: row.serviceName,
251
+ builtInId: row.builtInId,
252
+ description: row.description ?? "",
253
+ metadata: row.metadata ?? "{}",
254
+ rules: row.rules.filter((rule): rule is string => Boolean(rule)),
255
+ };
256
+ }),
257
+ );
258
+
259
+ // Service-to-Servicing bridge: if any services were resolved, pull in
260
+ // the generic Servicing endpoints
261
+ let servicingEndpoints: ResolvedEndpoint[] = [];
262
+ if (services.length > 0) {
263
+ const servicingRaw = await config.neo4j.query(
264
+ `MATCH (c:${L.concept} {${P.name}: '${SERVICE_REQUEST}'})-[rel:${R.queriedVia}|${R.mutatedVia}]->(e:${L.endpoint})
265
+ OPTIONAL MATCH (e)-[d:${R.dependsOn}]->(dep:${L.endpoint})
266
+ WITH c, rel, e,
267
+ collect(DISTINCT {
268
+ depPath: dep.${P.path},
269
+ depMethod: dep.${P.method},
270
+ paramName: d.${P.paramName},
271
+ fromField: d.${P.fromField}
272
+ }) AS dependencies
273
+ OPTIONAL MATCH (rEndpoint:${L.rule})-[:${R.governs}]->(e)
274
+ OPTIONAL MATCH (rConcept:${L.rule})-[:${R.governs}]->(c)
275
+ RETURN '${SERVICE_REQUEST}' AS concept,
276
+ type(rel) AS relationType,
277
+ e.${P.name} AS name,
278
+ e.${P.path} AS path,
279
+ e.${P.method} AS method,
280
+ e.${P.params} AS params,
281
+ e.${P.body} AS body,
282
+ e.${P.response} AS response,
283
+ e.${P.metadata} AS metadata,
284
+ dependencies,
285
+ collect(DISTINCT rEndpoint.${P.description}) +
286
+ collect(DISTINCT rConcept.${P.description}) AS rules`,
287
+ {},
288
+ );
289
+
290
+ servicingEndpoints = dedupeEndpoints(
291
+ asArray<EndpointRow>(JSON.parse(servicingRaw)).map(toResolvedEndpoint),
292
+ );
293
+ }
294
+
295
+ const combinedEndpoints = dedupeEndpoints([...allEndpoints, ...servicingEndpoints]);
296
+
297
+ return {
298
+ readEndpoints: combinedEndpoints.filter((ep) => ep.relation === "read"),
299
+ writeEndpoints: combinedEndpoints.filter((ep) => ep.relation === "write"),
300
+ services,
301
+ } satisfies ResolvedContext;
302
+ } catch (error) {
303
+ console.error("Graph resolution failed:", error);
304
+ return empty;
305
+ }
306
+ }
307
+
308
+ function asArray<T>(value: unknown) {
309
+ return Array.isArray(value) ? (value as T[]) : ([] as T[]);
310
+ }
311
+
312
+ /** Maps a Cypher row to an endpoint. The string defaults stand in for columns
313
+ * Neo4j returns as null, which downstream JSON.parse calls would choke on. */
314
+ function toResolvedEndpoint(row: EndpointRow) {
315
+ return {
316
+ concept: row.concept,
317
+ relation: row.relationType === R.mutatedVia ? ("write" as const) : ("read" as const),
318
+ name: row.name,
319
+ path: row.path,
320
+ method: row.method,
321
+ params: row.params ?? "[]",
322
+ body: row.body ?? "[]",
323
+ response: row.response ?? "[]",
324
+ metadata: row.metadata ?? "{}",
325
+ dependencies: row.dependencies.filter(
326
+ (dep): dep is EndpointDependency => dep.depPath != null,
327
+ ),
328
+ rules: row.rules.filter((rule): rule is string => Boolean(rule)),
329
+ } satisfies ResolvedEndpoint;
330
+ }
331
+
332
+ function dedupeEndpoints(endpoints: ResolvedEndpoint[]) {
333
+ const seen = new Set<string>();
334
+ const deduped: ResolvedEndpoint[] = [];
335
+
336
+ for (const ep of endpoints) {
337
+ const key = `${ep.relation}|${ep.concept}|${ep.method}|${ep.path}`;
338
+ if (seen.has(key)) continue;
339
+ seen.add(key);
340
+ deduped.push(ep);
341
+ }
342
+
343
+ return deduped;
344
+ }
345
+
346
+ function dedupeServices(services: ResolvedService[]) {
347
+ const seen = new Set<string>();
348
+ const deduped: ResolvedService[] = [];
349
+
350
+ for (const svc of services) {
351
+ const key = `${svc.concept}|${svc.builtInId}`;
352
+ if (seen.has(key)) continue;
353
+ seen.add(key);
354
+ deduped.push(svc);
355
+ }
356
+
357
+ return deduped;
358
+ }
359
+
360
+ async function computeRelevanceScores(
361
+ prompt: string,
362
+ concepts: (Pick<ConceptDef, "name" | "description"> & { score: number })[],
363
+ reranker: RerankerConfig,
364
+ ) {
365
+ const response = await fetch(reranker.url, {
366
+ method: "POST",
367
+ headers: {
368
+ Authorization: `Bearer ${reranker.apiKey}`,
369
+ "Content-Type": "application/json",
370
+ },
371
+ body: JSON.stringify({
372
+ queries: [prompt],
373
+ documents: concepts.map((x) => x.description),
374
+ }),
375
+ });
376
+
377
+ const data = (await response.json()) as { scores: number[] };
378
+ const scores = data.scores;
379
+
380
+ const scoredConcepts = concepts.map((c, idx) => ({
381
+ ...c,
382
+ score: scores[idx]!,
383
+ }));
384
+
385
+ scoredConcepts.sort((a, b) => b.score - a.score);
386
+ return scoredConcepts.slice(0, 3);
387
+ }
@@ -2,13 +2,21 @@
2
2
  export type {
3
3
  CortexConfig,
4
4
  CortexAgentDefinition,
5
+ ControlCenterConfig,
5
6
  KnowledgeConfig,
6
7
  DatabaseConfig,
8
+ RedisConfig,
7
9
  StorageConfig,
8
10
  PromptContext,
11
+ ToolContext,
12
+ ToolSet,
13
+ ToolSetFactory,
9
14
  } from "./config";
10
15
  export { defineAgent } from "./config";
11
16
 
17
+ // Provider shape, so a consumer can name what it passes to the model helpers.
18
+ export type { ProviderConfig } from "./ai/helpers";
19
+
12
20
  export type { ContextConfig, ThreadContextMeta } from "./ai/context/types";
13
21
 
14
22
  // Types consumers may need
@@ -26,13 +34,13 @@ export type {
26
34
  export { createRequestInterceptor } from "./ai/interceptors/request-interceptor";
27
35
 
28
36
  // Tools (consumers may register custom tools or use built-in ones)
29
- export { captureFilesTool } from "./ai/tools/capture-files.tool";
30
37
  export { createQueryGraphTool } from "./ai/tools/query-graph.tool";
31
- export { createCallEndpointTool } from "./ai/tools/call-endpoint.tool";
32
38
  export { createExecuteCodeTool } from "./ai/tools/execute-code.tool";
33
39
 
34
- // Graph (consumers may use independently)
40
+ // Graph (consumers may use independently). The contract itself lives in
41
+ // @cortex/contracts/graph — see ./graph/index — and is re-exported through here.
35
42
  export * from "./graph/index";
36
43
 
37
- // WebSocket event types
38
- export type { WsEvent, ThreadTitleUpdatedEvent } from "./ws/index";
44
+ // The wire contract, re-exported so consumers get it without a second import path.
45
+ // Its home is @cortex/contracts/wire, which the client SDKs compile in too.
46
+ export * from "@cortex/contracts/wire";