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