@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.
- package/dist/src/adapters/database.d.ts +3 -0
- package/dist/src/ai/active-streams.d.ts +14 -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/graph/expand-domains.d.ts +2 -0
- package/dist/src/graph/helpers.d.ts +5 -0
- package/dist/src/graph/resolver.d.ts +2 -0
- package/dist/src/graph/types.d.ts +6 -0
- 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.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 +27 -18
- 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/graph/expand-domains.ts +276 -0
- package/src/graph/generate-cypher.ts +18 -5
- package/src/graph/helpers.ts +1 -0
- package/src/graph/resolver.ts +10 -0
- package/src/graph/seed.ts +5 -2
- package/src/graph/types.ts +6 -0
- 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
package/src/ai/index.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type UIMessage,
|
|
3
3
|
type ToolSet,
|
|
4
|
-
consumeStream,
|
|
5
4
|
convertToModelMessages,
|
|
6
5
|
generateId,
|
|
7
6
|
generateText,
|
|
8
7
|
safeValidateUIMessages,
|
|
8
|
+
stepCountIs,
|
|
9
9
|
streamText,
|
|
10
10
|
} from "ai";
|
|
11
11
|
import { HTTPException } from "hono/http-exception";
|
|
12
12
|
import type { ResolvedCortexAgentConfig } from "../config.ts";
|
|
13
|
-
import type { Thread } from "../types.ts";
|
|
13
|
+
import type { MessageMetadata, Thread } from "../types.ts";
|
|
14
14
|
import { createModel, createEmbeddingModel } from "./helpers.ts";
|
|
15
|
-
import { buildSystemPrompt } from "./prompt.ts";
|
|
15
|
+
import { buildSystemPrompt, resolveSession } from "./prompt.ts";
|
|
16
16
|
import { createQueryGraphTool } from "./tools/query-graph.tool.ts";
|
|
17
17
|
import { createCallEndpointTool } from "./tools/call-endpoint.tool.ts";
|
|
18
18
|
import { createExecuteCodeTool } from "./tools/execute-code.tool.ts";
|
|
@@ -21,6 +21,15 @@ import { createRequestInterceptor } from "./interceptors/request-interceptor.ts"
|
|
|
21
21
|
import { createNeo4jClient } from "../graph/neo4j.ts";
|
|
22
22
|
import { resolveFromGraph } from "../graph/resolver.ts";
|
|
23
23
|
import { notify } from "../ws/index.ts";
|
|
24
|
+
import { buildContextMessages } from "./context/builder.ts";
|
|
25
|
+
import { optimizeThreadContext, estimateTokens, trimMessagesToFit } from "./context/index.ts";
|
|
26
|
+
import {
|
|
27
|
+
registerStream,
|
|
28
|
+
attachSseStream,
|
|
29
|
+
removeStream,
|
|
30
|
+
isStreamRunning,
|
|
31
|
+
} from "./active-streams.ts";
|
|
32
|
+
import { toThreadSummary } from "../types.ts";
|
|
24
33
|
|
|
25
34
|
export async function stream(
|
|
26
35
|
messages: unknown[],
|
|
@@ -28,8 +37,9 @@ export async function stream(
|
|
|
28
37
|
userId: string,
|
|
29
38
|
token: string,
|
|
30
39
|
config: ResolvedCortexAgentConfig,
|
|
31
|
-
abortSignal?: AbortSignal,
|
|
32
40
|
) {
|
|
41
|
+
const abortController = new AbortController();
|
|
42
|
+
|
|
33
43
|
const validationResult = await safeValidateUIMessages({ messages });
|
|
34
44
|
if (!validationResult.success) {
|
|
35
45
|
throw new HTTPException(423, { message: "Invalid messages format" });
|
|
@@ -37,15 +47,19 @@ export async function stream(
|
|
|
37
47
|
|
|
38
48
|
const validatedMessages = validationResult.data;
|
|
39
49
|
await config.db.messages.upsert(thread.id, validatedMessages);
|
|
50
|
+
const updatedThread = await config.db.threads.touch(thread.id);
|
|
40
51
|
|
|
41
|
-
const
|
|
42
|
-
.list(userId, thread.id, { limit: 20 })
|
|
43
|
-
.then((x) => x.map((y) => y.content));
|
|
52
|
+
const activeStream = registerStream(thread.id, abortController);
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
notify(userId, thread.agentId, {
|
|
55
|
+
type: "thread:run-started",
|
|
56
|
+
payload: { thread: toThreadSummary(updatedThread, true) },
|
|
57
|
+
});
|
|
46
58
|
|
|
59
|
+
// Extract prompt from the just-upserted messages (last user message)
|
|
60
|
+
// so we can start graph resolution without waiting for history fetch
|
|
47
61
|
const prompt =
|
|
48
|
-
|
|
62
|
+
validatedMessages
|
|
49
63
|
.filter((x) => x.role === "user")
|
|
50
64
|
.at(-1)
|
|
51
65
|
?.parts.find((x) => x.type === "text")?.text ?? "";
|
|
@@ -54,15 +68,24 @@ export async function stream(
|
|
|
54
68
|
const embeddingModel = createEmbeddingModel(config.embedding);
|
|
55
69
|
const neo4j = createNeo4jClient(config.neo4j, embeddingModel);
|
|
56
70
|
|
|
57
|
-
//
|
|
58
|
-
const resolved = await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
// Run independent operations in parallel
|
|
72
|
+
const [contextResult, resolved, session] = await Promise.all([
|
|
73
|
+
// Branch A: Load messages + build token-aware context window
|
|
74
|
+
buildContextMessages(userId, thread, config.db, config.context),
|
|
75
|
+
// Branch B: Resolve graph context (400-2000ms, the bottleneck)
|
|
76
|
+
resolveFromGraph(prompt, {
|
|
77
|
+
neo4j,
|
|
78
|
+
embeddingModel,
|
|
79
|
+
reranker: config.reranker,
|
|
80
|
+
}),
|
|
81
|
+
// Branch C: Resolve session data
|
|
82
|
+
resolveSession(config, thread, token),
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const { messages: contextMessages, allMessages: originalMessages } = contextResult;
|
|
63
86
|
|
|
64
87
|
// Build tools
|
|
65
|
-
const builtInTools:
|
|
88
|
+
const builtInTools: ToolSet = {
|
|
66
89
|
captureFiles: captureFilesTool,
|
|
67
90
|
queryGraph: createQueryGraphTool(neo4j),
|
|
68
91
|
};
|
|
@@ -88,36 +111,105 @@ export async function stream(
|
|
|
88
111
|
...config.tools,
|
|
89
112
|
} as ToolSet;
|
|
90
113
|
|
|
91
|
-
const systemPrompt = await buildSystemPrompt(config,
|
|
114
|
+
const systemPrompt = await buildSystemPrompt(config, resolved, session);
|
|
115
|
+
|
|
116
|
+
// The context builder reserved a static token budget for the system prompt + tools.
|
|
117
|
+
// Now that we have the actual values, verify the reserve was sufficient and trim
|
|
118
|
+
// the oldest messages if it wasn't.
|
|
119
|
+
const actualFixedCost = estimateTokens(systemPrompt) + estimateTokens(JSON.stringify(tools));
|
|
120
|
+
const { reservedTokenBudget, maxContextTokens } = config.context;
|
|
121
|
+
const trimmedMessages =
|
|
122
|
+
actualFixedCost > reservedTokenBudget
|
|
123
|
+
? trimMessagesToFit(contextMessages, maxContextTokens - actualFixedCost)
|
|
124
|
+
: contextMessages;
|
|
125
|
+
|
|
126
|
+
const recentMessages = await convertToModelMessages(trimmedMessages);
|
|
92
127
|
|
|
93
128
|
const result = streamText({
|
|
94
129
|
model,
|
|
95
130
|
system: systemPrompt,
|
|
96
131
|
tools,
|
|
97
132
|
messages: recentMessages,
|
|
98
|
-
abortSignal,
|
|
133
|
+
abortSignal: abortController.signal,
|
|
134
|
+
stopWhen: stepCountIs(50),
|
|
99
135
|
});
|
|
100
136
|
|
|
101
|
-
return result.toUIMessageStreamResponse({
|
|
137
|
+
return result.toUIMessageStreamResponse<UIMessage<MessageMetadata>>({
|
|
102
138
|
originalMessages,
|
|
103
139
|
generateMessageId: generateId,
|
|
104
|
-
consumeSseStream:
|
|
140
|
+
consumeSseStream: ({ stream: sseStream }) => {
|
|
141
|
+
attachSseStream(thread.id, sseStream);
|
|
142
|
+
},
|
|
105
143
|
onFinish: async ({ messages: finishedMessages, isAborted }) => {
|
|
106
144
|
if (isAborted) {
|
|
107
145
|
finalizeAbortedMessages(finishedMessages);
|
|
108
146
|
}
|
|
147
|
+
|
|
148
|
+
// Record token usage (result promises reject on abort, so skip)
|
|
149
|
+
const lastAssistantMessage = finishedMessages
|
|
150
|
+
.filter((x) => x.role === "assistant")
|
|
151
|
+
.at(-1);
|
|
152
|
+
if (lastAssistantMessage && !isAborted) {
|
|
153
|
+
const providerMetadata = await result.providerMetadata;
|
|
154
|
+
const response = await result.response;
|
|
155
|
+
const usage = await result.totalUsage;
|
|
156
|
+
let metadata: MessageMetadata = {
|
|
157
|
+
isAborted,
|
|
158
|
+
providerMetadata,
|
|
159
|
+
modelId: response.modelId,
|
|
160
|
+
tokenUsage: {
|
|
161
|
+
input: {
|
|
162
|
+
noCache: usage.inputTokenDetails.noCacheTokens ?? 0,
|
|
163
|
+
cacheRead: usage.inputTokenDetails.cacheReadTokens ?? 0,
|
|
164
|
+
cacheWrite: usage.inputTokenDetails.cacheWriteTokens ?? 0,
|
|
165
|
+
total: usage.inputTokens ?? 0,
|
|
166
|
+
},
|
|
167
|
+
output: {
|
|
168
|
+
reasoning: usage.outputTokenDetails.reasoningTokens ?? 0,
|
|
169
|
+
text: usage.outputTokenDetails.textTokens ?? 0,
|
|
170
|
+
total: usage.outputTokens ?? 0,
|
|
171
|
+
},
|
|
172
|
+
total: usage.totalTokens ?? 0,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
lastAssistantMessage.metadata = metadata;
|
|
177
|
+
} else if (lastAssistantMessage) {
|
|
178
|
+
lastAssistantMessage.metadata = {
|
|
179
|
+
isAborted,
|
|
180
|
+
modelId: "",
|
|
181
|
+
providerMetadata: undefined,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const persistedThread = await config.db.threads.getById(userId, thread.id);
|
|
186
|
+
if (!persistedThread) {
|
|
187
|
+
removeStream(thread.id);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
109
191
|
await config.db.messages.upsert(thread.id, finishedMessages);
|
|
110
192
|
config.onStreamFinish?.({ messages: finishedMessages, isAborted });
|
|
111
193
|
|
|
112
194
|
// XXX: we need to notify the user so that the client can
|
|
113
195
|
// fetch new messages. The client can't fetch messages
|
|
114
|
-
// immediately
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
196
|
+
// immediately because messages may not have been saved yet.
|
|
197
|
+
notify(userId, persistedThread.agentId, {
|
|
198
|
+
type: "thread:messages-updated",
|
|
199
|
+
payload: {
|
|
200
|
+
threadId: thread.id,
|
|
201
|
+
thread: toThreadSummary(persistedThread, false),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
setTimeout(() => removeStream(thread.id, activeStream.id), 10_000);
|
|
206
|
+
|
|
207
|
+
// Fire-and-forget: optimize context for next request
|
|
208
|
+
// Runs after response is delivered — no perceived latency
|
|
209
|
+
try {
|
|
210
|
+
optimizeThreadContext(thread, finishedMessages, config);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error("[cortex-server] Context optimization failed:", err);
|
|
121
213
|
}
|
|
122
214
|
},
|
|
123
215
|
});
|
|
@@ -143,9 +235,12 @@ going to do so or any other speech. Spit out only the title.`,
|
|
|
143
235
|
|
|
144
236
|
await config.db.threads.updateTitle(threadId, output ?? "");
|
|
145
237
|
|
|
146
|
-
|
|
238
|
+
const thread = await config.db.threads.getById(userId, threadId);
|
|
239
|
+
if (!thread) return;
|
|
240
|
+
|
|
241
|
+
notify(userId, thread.agentId, {
|
|
147
242
|
type: "thread:title-updated",
|
|
148
|
-
payload: {
|
|
243
|
+
payload: { thread: toThreadSummary(thread, isStreamRunning(thread.id)) },
|
|
149
244
|
});
|
|
150
245
|
}
|
|
151
246
|
|
package/src/ai/prompt.ts
CHANGED
|
@@ -2,29 +2,35 @@ import type { ResolvedContext } from "../graph/resolver.ts";
|
|
|
2
2
|
import type { ResolvedCortexAgentConfig } from "../config.ts";
|
|
3
3
|
import type { Thread } from "../types.ts";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Resolves session data for the thread, loading from the configured
|
|
7
|
+
* session loader if not already cached on the thread.
|
|
8
|
+
*/
|
|
9
|
+
export async function resolveSession(
|
|
6
10
|
config: ResolvedCortexAgentConfig,
|
|
7
|
-
prompt: string,
|
|
8
11
|
thread: Thread,
|
|
9
12
|
token: string,
|
|
13
|
+
) {
|
|
14
|
+
let session = thread.session;
|
|
15
|
+
|
|
16
|
+
if (!session && config.loadSessionData) {
|
|
17
|
+
session = await config.loadSessionData(token);
|
|
18
|
+
// Persist to DB for future cache hits
|
|
19
|
+
await config.db.threads.updateSession(thread.id, session);
|
|
20
|
+
thread.session = session;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return session;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function buildSystemPrompt(
|
|
27
|
+
config: ResolvedCortexAgentConfig,
|
|
10
28
|
resolved: ResolvedContext | null,
|
|
29
|
+
session: Record<string, unknown> | null,
|
|
11
30
|
) {
|
|
12
31
|
// Resolve the consumer's base system prompt
|
|
13
32
|
let basePrompt: string;
|
|
14
33
|
if (typeof config.systemPrompt === "function") {
|
|
15
|
-
// Resolve session data with caching
|
|
16
|
-
let session: Record<string, unknown> | null = thread.session as Record<
|
|
17
|
-
string,
|
|
18
|
-
unknown
|
|
19
|
-
> | null;
|
|
20
|
-
|
|
21
|
-
if (!session && config.loadSessionData) {
|
|
22
|
-
session = await config.loadSessionData(token);
|
|
23
|
-
// Persist to DB for future cache hits
|
|
24
|
-
await config.db.threads.updateSession(thread.id, session);
|
|
25
|
-
thread.session = session;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
34
|
basePrompt = await config.systemPrompt(session);
|
|
29
35
|
} else {
|
|
30
36
|
basePrompt = config.systemPrompt;
|
|
@@ -59,6 +65,7 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
59
65
|
)
|
|
60
66
|
.join("\n")
|
|
61
67
|
: "";
|
|
68
|
+
const meta = ep.metadata !== "{}" ? `\n- Metadata: ${ep.metadata}` : "";
|
|
62
69
|
parts.push(
|
|
63
70
|
`
|
|
64
71
|
### ${ep.concept} (read)
|
|
@@ -66,7 +73,7 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
66
73
|
- ${ep.method} ${ep.path}
|
|
67
74
|
- Params: ${ep.params}
|
|
68
75
|
- Body: ${ep.body}
|
|
69
|
-
- Response: ${ep.response}${rules}${deps}`,
|
|
76
|
+
- Response: ${ep.response}${rules}${deps}${meta}`,
|
|
70
77
|
);
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -82,6 +89,7 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
82
89
|
)
|
|
83
90
|
.join("\n")
|
|
84
91
|
: "";
|
|
92
|
+
const meta = ep.metadata !== "{}" ? `\n- Metadata: ${ep.metadata}` : "";
|
|
85
93
|
parts.push(
|
|
86
94
|
`
|
|
87
95
|
### ${ep.concept} (write)
|
|
@@ -89,17 +97,18 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
89
97
|
- ${ep.method} ${ep.path}
|
|
90
98
|
- Params: ${ep.params}
|
|
91
99
|
- Body: ${ep.body}
|
|
92
|
-
- Response: ${ep.response}${rules}${deps}`,
|
|
100
|
+
- Response: ${ep.response}${rules}${deps}${meta}`,
|
|
93
101
|
);
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
for (const svc of resolved.services) {
|
|
97
105
|
const rules = svc.rules.length > 0 ? `\n Rules: ${svc.rules.join("; ")}` : "";
|
|
106
|
+
const meta = svc.metadata !== "{}" ? `\n- Metadata: ${svc.metadata}` : "";
|
|
98
107
|
parts.push(
|
|
99
108
|
`
|
|
100
109
|
### ${svc.concept} via ${svc.serviceName} (service)
|
|
101
110
|
- Built-in ID: ${svc.builtInId}
|
|
102
|
-
- Description: ${svc.description || "N/A"}${rules}`,
|
|
111
|
+
- Description: ${svc.description || "N/A"}${rules}${meta}`,
|
|
103
112
|
);
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -24,7 +24,7 @@ ORDER BY score DESC;
|
|
|
24
24
|
.string()
|
|
25
25
|
.optional()
|
|
26
26
|
.describe(
|
|
27
|
-
'Optional JSON-encoded string of query parameters. Example: `{"name": "LeaveBalance"}` if you know the exact name; for parameters that need to be embedded first prepend the name with `#`, e.g., `{"#
|
|
27
|
+
'Optional JSON-encoded string of query parameters. Example: `{"name": "LeaveBalance"}` if you know the exact name; for parameters that need to be embedded first prepend the name with `#`, e.g., `{"#embedding": "Text to be embedded before passed to query"}`',
|
|
28
28
|
),
|
|
29
29
|
}),
|
|
30
30
|
execute: async ({ query, parameters }) => {
|
|
@@ -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
|
|