@m6d/cortex-server 1.3.0 → 1.5.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 (56) 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/context/builder.d.ts +24 -0
  4. package/dist/src/ai/context/compressor.d.ts +7 -0
  5. package/dist/src/ai/context/index.d.ts +15 -0
  6. package/dist/src/ai/context/summarizer.d.ts +5 -0
  7. package/dist/src/ai/context/token-estimator.d.ts +20 -0
  8. package/dist/src/ai/context/types.d.ts +20 -0
  9. package/dist/src/ai/index.d.ts +1 -1
  10. package/dist/src/ai/prompt.d.ts +6 -1
  11. package/dist/src/config.d.ts +4 -0
  12. package/dist/src/db/schema.d.ts +19 -1
  13. package/dist/src/graph/expand-domains.d.ts +2 -0
  14. package/dist/src/graph/helpers.d.ts +5 -0
  15. package/dist/src/graph/resolver.d.ts +2 -0
  16. package/dist/src/graph/types.d.ts +6 -0
  17. package/dist/src/index.d.ts +1 -0
  18. package/dist/src/routes/ws.d.ts +5 -1
  19. package/dist/src/types.d.ts +32 -14
  20. package/dist/src/ws/connections.d.ts +3 -3
  21. package/dist/src/ws/events.d.ts +28 -3
  22. package/dist/src/ws/index.d.ts +1 -1
  23. package/dist/src/ws/notify.d.ts +1 -1
  24. package/package.json +1 -1
  25. package/src/adapters/database.ts +3 -0
  26. package/src/adapters/mssql.ts +26 -6
  27. package/src/ai/active-streams.ts +123 -0
  28. package/src/ai/context/builder.ts +94 -0
  29. package/src/ai/context/compressor.ts +47 -0
  30. package/src/ai/context/index.ts +75 -0
  31. package/src/ai/context/summarizer.ts +50 -0
  32. package/src/ai/context/token-estimator.ts +60 -0
  33. package/src/ai/context/types.ts +28 -0
  34. package/src/ai/index.ts +124 -29
  35. package/src/ai/prompt.ts +27 -18
  36. package/src/ai/tools/query-graph.tool.ts +1 -1
  37. package/src/cli/extract-endpoints.ts +18 -18
  38. package/src/config.ts +4 -0
  39. package/src/db/migrations/20260315000000_add_context_meta/migration.sql +1 -0
  40. package/src/db/schema.ts +6 -1
  41. package/src/factory.ts +11 -1
  42. package/src/graph/expand-domains.ts +276 -0
  43. package/src/graph/generate-cypher.ts +18 -5
  44. package/src/graph/helpers.ts +1 -0
  45. package/src/graph/resolver.ts +10 -0
  46. package/src/graph/seed.ts +5 -2
  47. package/src/graph/types.ts +6 -0
  48. package/src/index.ts +2 -0
  49. package/src/routes/chat.ts +47 -2
  50. package/src/routes/threads.ts +46 -9
  51. package/src/routes/ws.ts +37 -23
  52. package/src/types.ts +37 -13
  53. package/src/ws/connections.ts +15 -9
  54. package/src/ws/events.ts +31 -3
  55. package/src/ws/index.ts +9 -1
  56. package/src/ws/notify.ts +2 -2
@@ -0,0 +1,276 @@
1
+ import type { ConceptDef, DomainDef, EndpointDef, RuleDef, ServiceDef } from "./types.ts";
2
+
3
+ type DomainCollections = {
4
+ concepts: ConceptDef[];
5
+ endpoints: EndpointDef[];
6
+ services: ServiceDef[];
7
+ rules: RuleDef[];
8
+ };
9
+
10
+ type OwnershipMaps = {
11
+ concepts: WeakMap<ConceptDef, string>;
12
+ endpoints: WeakMap<EndpointDef, string>;
13
+ services: WeakMap<ServiceDef, string>;
14
+ rules: WeakMap<RuleDef, string>;
15
+ };
16
+
17
+ export function expandDomains(domains: Record<string, DomainDef>) {
18
+ const ownership = createOwnershipMaps(domains);
19
+ const expanded: Record<string, DomainDef> = {};
20
+
21
+ for (const [domainKey, domain] of Object.entries(domains)) {
22
+ const collections = cloneCollections(domain);
23
+ const pendingConcepts = [...collections.concepts];
24
+ const pendingEndpoints = [...collections.endpoints];
25
+ const pendingServices = [...collections.services];
26
+
27
+ while (
28
+ pendingConcepts.length > 0 ||
29
+ pendingEndpoints.length > 0 ||
30
+ pendingServices.length > 0
31
+ ) {
32
+ const concept = pendingConcepts.pop();
33
+ if (concept) {
34
+ includeConcept({
35
+ domainKey,
36
+ concept,
37
+ ownership,
38
+ collections,
39
+ pendingConcepts,
40
+ pendingEndpoints,
41
+ });
42
+ }
43
+
44
+ const endpoint = pendingEndpoints.pop();
45
+ if (endpoint) {
46
+ includeEndpoint({
47
+ domainKey,
48
+ endpoint,
49
+ ownership,
50
+ collections,
51
+ pendingConcepts,
52
+ pendingEndpoints,
53
+ });
54
+ }
55
+
56
+ const service = pendingServices.pop();
57
+ if (service) {
58
+ includeService({
59
+ domainKey,
60
+ service,
61
+ ownership,
62
+ collections,
63
+ pendingConcepts,
64
+ });
65
+ }
66
+ }
67
+
68
+ expanded[domainKey] = {
69
+ ...domain,
70
+ concepts: collections.concepts,
71
+ endpoints: collections.endpoints,
72
+ services: collections.services,
73
+ rules: collections.rules,
74
+ };
75
+ }
76
+
77
+ return expanded;
78
+ }
79
+
80
+ function createOwnershipMaps(domains: Record<string, DomainDef>) {
81
+ const ownership: OwnershipMaps = {
82
+ concepts: new WeakMap<ConceptDef, string>(),
83
+ endpoints: new WeakMap<EndpointDef, string>(),
84
+ services: new WeakMap<ServiceDef, string>(),
85
+ rules: new WeakMap<RuleDef, string>(),
86
+ };
87
+
88
+ for (const [domainKey, domain] of Object.entries(domains)) {
89
+ for (const concept of domain.concepts ?? []) {
90
+ assignOwner(ownership.concepts, concept, domainKey);
91
+ }
92
+
93
+ for (const endpoint of domain.endpoints ?? []) {
94
+ assignOwner(ownership.endpoints, endpoint, domainKey);
95
+ }
96
+
97
+ for (const service of domain.services ?? []) {
98
+ assignOwner(ownership.services, service, domainKey);
99
+ }
100
+
101
+ for (const rule of domain.rules ?? []) {
102
+ assignOwner(ownership.rules, rule, domainKey);
103
+ }
104
+ }
105
+
106
+ return ownership;
107
+ }
108
+
109
+ function assignOwner<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
110
+ if (!owners.has(value)) {
111
+ owners.set(value, domainKey);
112
+ }
113
+ }
114
+
115
+ function cloneCollections(domain: DomainDef) {
116
+ return {
117
+ concepts: [...(domain.concepts ?? [])],
118
+ endpoints: [...(domain.endpoints ?? [])],
119
+ services: [...(domain.services ?? [])],
120
+ rules: [...(domain.rules ?? [])],
121
+ } satisfies DomainCollections;
122
+ }
123
+
124
+ function includeConcept(options: {
125
+ domainKey: string;
126
+ concept: ConceptDef;
127
+ ownership: OwnershipMaps;
128
+ collections: DomainCollections;
129
+ pendingConcepts: ConceptDef[];
130
+ pendingEndpoints: EndpointDef[];
131
+ }) {
132
+ const { domainKey, concept, ownership, collections, pendingConcepts, pendingEndpoints } =
133
+ options;
134
+
135
+ includeRule(domainKey, concept.governedBy ?? [], ownership, collections);
136
+
137
+ if (shouldAttach(ownership.concepts, concept, domainKey)) {
138
+ pushUnique(collections.concepts, concept);
139
+ }
140
+
141
+ if (
142
+ concept.parentConcept &&
143
+ shouldAttach(ownership.concepts, concept.parentConcept, domainKey)
144
+ ) {
145
+ if (pushUnique(collections.concepts, concept.parentConcept)) {
146
+ pendingConcepts.push(concept.parentConcept);
147
+ }
148
+ }
149
+
150
+ for (const endpoint of collections.endpoints) {
151
+ if (referencesConcept(endpoint, concept)) {
152
+ includeEndpoint({
153
+ domainKey,
154
+ endpoint,
155
+ ownership,
156
+ collections,
157
+ pendingConcepts,
158
+ pendingEndpoints,
159
+ });
160
+ }
161
+ }
162
+ }
163
+
164
+ function includeEndpoint(options: {
165
+ domainKey: string;
166
+ endpoint: EndpointDef;
167
+ ownership: OwnershipMaps;
168
+ collections: DomainCollections;
169
+ pendingConcepts: ConceptDef[];
170
+ pendingEndpoints: EndpointDef[];
171
+ }) {
172
+ const { domainKey, endpoint, ownership, collections, pendingConcepts, pendingEndpoints } =
173
+ options;
174
+
175
+ if (shouldAttach(ownership.endpoints, endpoint, domainKey)) {
176
+ pushUnique(collections.endpoints, endpoint);
177
+ }
178
+
179
+ includeRule(domainKey, endpoint.governedBy ?? [], ownership, collections);
180
+
181
+ for (const concept of endpoint.queries ?? []) {
182
+ if (
183
+ shouldAttach(ownership.concepts, concept, domainKey) &&
184
+ pushUnique(collections.concepts, concept)
185
+ ) {
186
+ pendingConcepts.push(concept);
187
+ }
188
+ }
189
+
190
+ for (const concept of endpoint.mutates ?? []) {
191
+ if (
192
+ shouldAttach(ownership.concepts, concept, domainKey) &&
193
+ pushUnique(collections.concepts, concept)
194
+ ) {
195
+ pendingConcepts.push(concept);
196
+ }
197
+ }
198
+
199
+ for (const returned of endpoint.returns ?? []) {
200
+ if (
201
+ shouldAttach(ownership.concepts, returned.concept, domainKey) &&
202
+ pushUnique(collections.concepts, returned.concept)
203
+ ) {
204
+ pendingConcepts.push(returned.concept);
205
+ }
206
+ }
207
+
208
+ for (const dependency of endpoint.dependsOn ?? []) {
209
+ if (
210
+ shouldAttach(ownership.endpoints, dependency.endpoint, domainKey) &&
211
+ pushUnique(collections.endpoints, dependency.endpoint)
212
+ ) {
213
+ pendingEndpoints.push(dependency.endpoint);
214
+ }
215
+ }
216
+ }
217
+
218
+ function includeService(options: {
219
+ domainKey: string;
220
+ service: ServiceDef;
221
+ ownership: OwnershipMaps;
222
+ collections: DomainCollections;
223
+ pendingConcepts: ConceptDef[];
224
+ }) {
225
+ const { domainKey, service, ownership, collections, pendingConcepts } = options;
226
+
227
+ if (shouldAttach(ownership.services, service, domainKey)) {
228
+ pushUnique(collections.services, service);
229
+ }
230
+
231
+ includeRule(domainKey, service.governedBy ?? [], ownership, collections);
232
+
233
+ if (
234
+ shouldAttach(ownership.concepts, service.belongsTo, domainKey) &&
235
+ pushUnique(collections.concepts, service.belongsTo)
236
+ ) {
237
+ pendingConcepts.push(service.belongsTo);
238
+ }
239
+ }
240
+
241
+ function includeRule(
242
+ domainKey: string,
243
+ rules: readonly RuleDef[],
244
+ ownership: OwnershipMaps,
245
+ collections: DomainCollections,
246
+ ) {
247
+ for (const rule of rules) {
248
+ if (shouldAttach(ownership.rules, rule, domainKey)) {
249
+ pushUnique(collections.rules, rule);
250
+ }
251
+ }
252
+ }
253
+
254
+ function shouldAttach<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
255
+ const owner = owners.get(value);
256
+ return owner == null || owner === domainKey;
257
+ }
258
+
259
+ function pushUnique<T extends object>(values: T[], value: T) {
260
+ if (values.includes(value)) {
261
+ return false;
262
+ }
263
+
264
+ values.push(value);
265
+ return true;
266
+ }
267
+
268
+ function referencesConcept(endpoint: EndpointDef, concept: ConceptDef) {
269
+ return (
270
+ endpoint.queries?.includes(concept) ||
271
+ endpoint.mutates?.includes(concept) ||
272
+ endpoint.returns?.some(function (returned) {
273
+ return returned.concept === concept;
274
+ })
275
+ );
276
+ }
@@ -31,9 +31,12 @@ export function generateCypher(data: DomainDef) {
31
31
  const edges: string[] = [];
32
32
 
33
33
  // -- Domain node --
34
+ const domainMeta = data.metadata
35
+ ? `,\n d.metadata = '${jsonStr(data.metadata)}'`
36
+ : "";
34
37
  nodes.push(
35
38
  `MERGE (d:Domain {name: '${esc(data.name)}'})
36
- SET d.description = '${esc(data.description)}'`,
39
+ SET d.description = '${esc(data.description)}'${domainMeta}`,
37
40
  );
38
41
 
39
42
  // -- Concept nodes --
@@ -42,10 +45,11 @@ export function generateCypher(data: DomainDef) {
42
45
  c.aliases && c.aliases.length > 0
43
46
  ? `, c.aliases = [${c.aliases.map((a) => `'${esc(a)}'`).join(", ")}]`
44
47
  : "";
48
+ const conceptMeta = c.metadata ? `, c.metadata = '${jsonStr(c.metadata)}'` : "";
45
49
 
46
50
  nodes.push(
47
51
  `MERGE (c:Concept {name: '${esc(c.name)}'})
48
- SET c.description = '${esc(c.description)}'${aliasClause}`,
52
+ SET c.description = '${esc(c.description)}'${aliasClause}${conceptMeta}`,
49
53
  );
50
54
 
51
55
  edges.push(
@@ -73,6 +77,9 @@ export function generateCypher(data: DomainDef) {
73
77
 
74
78
  // -- Endpoint nodes --
75
79
  for (const ep of data.endpoints ?? []) {
80
+ const endpointMeta = ep.metadata
81
+ ? `,\n e.metadata = '${jsonStr(ep.metadata)}'`
82
+ : "";
76
83
  nodes.push(
77
84
  `MERGE (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
78
85
  SET e.name = '${esc(ep.name)}',
@@ -82,7 +89,7 @@ export function generateCypher(data: DomainDef) {
82
89
  e.response = '${jsonStr(ep.response)}',
83
90
  e.propertiesDescriptions = '${jsonStr(ep.propertiesDescriptions)}',
84
91
  e.successStatus = ${ep.successStatus},
85
- e.errorStatuses = [${ep.errorStatuses.join(", ")}]`,
92
+ e.errorStatuses = [${ep.errorStatuses.join(", ")}]${endpointMeta}`,
86
93
  );
87
94
 
88
95
  for (const concept of ep.queries ?? []) {
@@ -131,10 +138,13 @@ export function generateCypher(data: DomainDef) {
131
138
 
132
139
  // -- Service nodes --
133
140
  for (const svc of data.services ?? []) {
141
+ const serviceMeta = svc.metadata
142
+ ? `,\n s.metadata = '${jsonStr(svc.metadata)}'`
143
+ : "";
134
144
  nodes.push(
135
145
  `MERGE (s:Service {builtInId: '${esc(svc.builtInId)}'})
136
146
  SET s.name = '${esc(svc.name)}',
137
- s.description = '${esc(svc.description)}'`,
147
+ s.description = '${esc(svc.description)}'${serviceMeta}`,
138
148
  );
139
149
 
140
150
  edges.push(
@@ -154,9 +164,12 @@ export function generateCypher(data: DomainDef) {
154
164
 
155
165
  // -- Rule nodes --
156
166
  for (const rule of data.rules ?? []) {
167
+ const ruleMeta = rule.metadata
168
+ ? `,\n r.metadata = '${jsonStr(rule.metadata)}'`
169
+ : "";
157
170
  nodes.push(
158
171
  `MERGE (r:Rule {name: '${esc(rule.name)}'})
159
- SET r.description = '${esc(rule.description)}'`,
172
+ SET r.description = '${esc(rule.description)}'${ruleMeta}`,
160
173
  );
161
174
  }
162
175
 
@@ -64,5 +64,6 @@ export function defineEndpoint(input: EndpointInput) {
64
64
  returns: input.returns,
65
65
  dependsOn: input.dependsOn,
66
66
  governedBy: input.governedBy,
67
+ metadata: input.metadata,
67
68
  } satisfies EndpointDef;
68
69
  }
@@ -50,6 +50,7 @@ export type ResolvedEndpoint = {
50
50
  response: string;
51
51
  dependencies: EndpointDependency[];
52
52
  rules: string[];
53
+ metadata: string;
53
54
  };
54
55
 
55
56
  export type ResolvedService = {
@@ -58,6 +59,7 @@ export type ResolvedService = {
58
59
  builtInId: string;
59
60
  description: string;
60
61
  rules: string[];
62
+ metadata: string;
61
63
  };
62
64
 
63
65
  export type ResolvedContext = {
@@ -79,6 +81,7 @@ type EndpointRow = {
79
81
  params: string | null;
80
82
  body: string | null;
81
83
  response: string | null;
84
+ metadata: string | null;
82
85
  dependencies: (EndpointDependency | { depPath: null })[];
83
86
  rules: (string | null)[];
84
87
  };
@@ -88,6 +91,7 @@ type ServiceRow = {
88
91
  serviceName: string;
89
92
  builtInId: string;
90
93
  description: string | null;
94
+ metadata: string | null;
91
95
  rules: (string | null)[];
92
96
  };
93
97
 
@@ -158,6 +162,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
158
162
  e.params AS params,
159
163
  e.body AS body,
160
164
  e.response AS response,
165
+ e.metadata AS metadata,
161
166
  dependencies,
162
167
  collect(DISTINCT rEndpoint.description) +
163
168
  collect(DISTINCT rConcept.description) AS rules`,
@@ -177,6 +182,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
177
182
  params: row.params ?? "[]",
178
183
  body: row.body ?? "[]",
179
184
  response: row.response ?? "[]",
185
+ metadata: row.metadata ?? "{}",
180
186
  dependencies: row.dependencies.filter(
181
187
  (dep): dep is EndpointDependency => dep.depPath != null,
182
188
  ),
@@ -198,6 +204,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
198
204
  svc.name AS serviceName,
199
205
  svc.builtInId AS builtInId,
200
206
  svc.description AS description,
207
+ svc.metadata AS metadata,
201
208
  collect(DISTINCT rService.description) +
202
209
  collect(DISTINCT rConcept.description) AS rules`,
203
210
  { names: conceptNames },
@@ -210,6 +217,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
210
217
  serviceName: row.serviceName,
211
218
  builtInId: row.builtInId,
212
219
  description: row.description ?? "",
220
+ metadata: row.metadata ?? "{}",
213
221
  rules: row.rules.filter((rule): rule is string => Boolean(rule)),
214
222
  };
215
223
  }),
@@ -239,6 +247,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
239
247
  e.params AS params,
240
248
  e.body AS body,
241
249
  e.response AS response,
250
+ e.metadata AS metadata,
242
251
  dependencies,
243
252
  collect(DISTINCT rEndpoint.description) +
244
253
  collect(DISTINCT rConcept.description) AS rules`,
@@ -259,6 +268,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
259
268
  params: row.params ?? "[]",
260
269
  body: row.body ?? "[]",
261
270
  response: row.response ?? "[]",
271
+ metadata: row.metadata ?? "{}",
262
272
  dependencies: row.dependencies.filter(
263
273
  (dep): dep is EndpointDependency => dep.depPath != null,
264
274
  ),
package/src/graph/seed.ts CHANGED
@@ -10,6 +10,7 @@ import type { EmbeddingModel } from "ai";
10
10
  import { embed } from "ai";
11
11
  import type { Neo4jClient, Neo4jConfig } from "./neo4j.ts";
12
12
  import { createNeo4jClient } from "./neo4j.ts";
13
+ import { expandDomains } from "./expand-domains.ts";
13
14
  import { generateCypher, type GeneratedCypher } from "./generate-cypher.ts";
14
15
  import type { DomainDef } from "./types.ts";
15
16
  import { validateDomain } from "./validate.ts";
@@ -122,8 +123,10 @@ async function generateEmbeddings(client: Neo4jClient, embeddingConfig: Embeddin
122
123
  // ---------------------------------------------------------------------------
123
124
 
124
125
  export async function seedGraph(config: SeedGraphConfig, domains: Record<string, DomainDef>) {
126
+ const expandedDomains = expandDomains(domains);
127
+
125
128
  // Validate all domains
126
- const validationErrors = Object.entries(domains).flatMap(([name, domain]) =>
129
+ const validationErrors = Object.entries(expandedDomains).flatMap(([name, domain]) =>
127
130
  validateDomain(domain).map((err) => `[${name}] ${err}`),
128
131
  );
129
132
 
@@ -133,7 +136,7 @@ export async function seedGraph(config: SeedGraphConfig, domains: Record<string,
133
136
  }
134
137
 
135
138
  const client = createNeo4jClient(config.neo4j, config.embedding.model);
136
- const targets = Object.entries(domains);
139
+ const targets = Object.entries(expandedDomains);
137
140
 
138
141
  console.log(`Seeding ${targets.length} domain(s)...`);
139
142
 
@@ -52,6 +52,7 @@ export type ConceptDef = {
52
52
  aliases?: string[];
53
53
  parentConcept?: ConceptDef;
54
54
  governedBy?: RuleDef[];
55
+ metadata?: Record<string, unknown>;
55
56
  };
56
57
 
57
58
  // ---------------------------------------------------------------------------
@@ -84,6 +85,7 @@ export type EndpointDef = {
84
85
  }[];
85
86
 
86
87
  governedBy?: RuleDef[];
88
+ metadata?: Record<string, unknown>;
87
89
  };
88
90
 
89
91
  // ---------------------------------------------------------------------------
@@ -112,6 +114,7 @@ export type EndpointInput = {
112
114
  }[];
113
115
 
114
116
  governedBy?: RuleDef[];
117
+ metadata?: Record<string, unknown>;
115
118
  };
116
119
 
117
120
  // ---------------------------------------------------------------------------
@@ -125,6 +128,7 @@ export type ServiceDef = {
125
128
  builtInId: string;
126
129
  belongsTo: ConceptDef;
127
130
  governedBy?: RuleDef[];
131
+ metadata?: Record<string, unknown>;
128
132
  };
129
133
 
130
134
  // ---------------------------------------------------------------------------
@@ -135,6 +139,7 @@ export type RuleDef = {
135
139
  readonly __brand: "rule";
136
140
  name: string;
137
141
  description: string;
142
+ metadata?: Record<string, unknown>;
138
143
  };
139
144
 
140
145
  // ---------------------------------------------------------------------------
@@ -149,4 +154,5 @@ export type DomainDef = {
149
154
  endpoints?: EndpointDef[];
150
155
  services?: ServiceDef[];
151
156
  rules?: RuleDef[];
157
+ metadata?: Record<string, unknown>;
152
158
  };
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
  },