@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
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ToolUIPart, UIMessage } from "ai";
|
|
2
2
|
import type { Thread, StoredMessage, CapturedFileInput } from "../types";
|
|
3
|
+
import type { ThreadContextMeta } from "../ai/context/types.ts";
|
|
3
4
|
export type DatabaseAdapter = {
|
|
4
5
|
threads: {
|
|
5
6
|
list(userId: string, agentId: string): Promise<Thread[]>;
|
|
6
7
|
getById(userId: string, threadId: string): Promise<Thread | null>;
|
|
7
8
|
create(userId: string, agentId: string): Promise<Thread>;
|
|
8
9
|
delete(userId: string, threadId: string): Promise<void>;
|
|
10
|
+
touch(threadId: string): Promise<Thread>;
|
|
9
11
|
updateTitle(threadId: string, title: string): Promise<void>;
|
|
10
12
|
updateSession(threadId: string, session: Record<string, unknown>): Promise<void>;
|
|
13
|
+
updateContextMeta(threadId: string, meta: ThreadContextMeta): Promise<void>;
|
|
11
14
|
};
|
|
12
15
|
messages: {
|
|
13
16
|
list(userId: string, threadId: string, opts?: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type ActiveStream = {
|
|
2
|
+
id: string;
|
|
3
|
+
abortController: AbortController;
|
|
4
|
+
buffer: string[];
|
|
5
|
+
subscribers: Set<ReadableStreamDefaultController<string>>;
|
|
6
|
+
isComplete: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function registerStream(threadId: string, abortController: AbortController): ActiveStream;
|
|
9
|
+
export declare function attachSseStream(threadId: string, sseStream: ReadableStream<string>): void;
|
|
10
|
+
export declare function subscribe(threadId: string): ReadableStream<string> | null;
|
|
11
|
+
export declare function abortStream(threadId: string): boolean;
|
|
12
|
+
export declare function isStreamRunning(threadId: string): boolean;
|
|
13
|
+
export declare function removeStream(threadId: string, streamId?: string): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import type { DatabaseAdapter } from "../../adapters/database.ts";
|
|
3
|
+
import type { MessageMetadata, Thread } from "../../types.ts";
|
|
4
|
+
import type { ContextConfig } from "./types.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Builds a token-aware context window from stored messages.
|
|
7
|
+
*
|
|
8
|
+
* 1. Loads messages from DB with generous limit
|
|
9
|
+
* 2. Reads existing summary from thread.contextMeta
|
|
10
|
+
* 3. Compresses large tool results
|
|
11
|
+
* 4. Walks messages newest-to-oldest, accumulating token estimates
|
|
12
|
+
* 5. Stops when adding the next message would exceed the budget
|
|
13
|
+
* 6. Prepends summary as synthetic message if older messages were trimmed
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildContextMessages(userId: string, thread: Thread, db: DatabaseAdapter, contextConfig: ContextConfig): Promise<{
|
|
16
|
+
messages: UIMessage<MessageMetadata, import("ai").UIDataTypes, import("ai").UITools>[];
|
|
17
|
+
allMessages: UIMessage<MessageMetadata, import("ai").UIDataTypes, import("ai").UITools>[];
|
|
18
|
+
summary: string | null;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Drops the oldest messages until the total estimated tokens fit within `budget`.
|
|
22
|
+
* Always keeps at least the most recent message.
|
|
23
|
+
*/
|
|
24
|
+
export declare function trimMessagesToFit(messages: UIMessage<MessageMetadata>[], budget: number): UIMessage<MessageMetadata, import("ai").UIDataTypes, import("ai").UITools>[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import type { MessageMetadata } from "src/types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Returns a new array of messages with large tool outputs truncated
|
|
5
|
+
* to `maxTokensPerResult`. Does not mutate the input messages.
|
|
6
|
+
*/
|
|
7
|
+
export declare function compressToolResults(messages: UIMessage<MessageMetadata>[], maxTokensPerResult: number): UIMessage<MessageMetadata, import("ai").UIDataTypes, import("ai").UITools>[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type { ContextConfig, ThreadContextMeta } from "./types.ts";
|
|
2
|
+
export { DEFAULT_CONTEXT_CONFIG } from "./types.ts";
|
|
3
|
+
export { CHARS_PER_TOKEN, estimateTokens, estimateMessageTokens, estimateMessagesTokens, } from "./token-estimator.ts";
|
|
4
|
+
export { compressToolResults } from "./compressor.ts";
|
|
5
|
+
export { summarizeMessages } from "./summarizer.ts";
|
|
6
|
+
export { buildContextMessages, trimMessagesToFit } from "./builder.ts";
|
|
7
|
+
import type { UIMessage } from "ai";
|
|
8
|
+
import type { ResolvedCortexAgentConfig } from "../../config.ts";
|
|
9
|
+
import type { Thread } from "../../types.ts";
|
|
10
|
+
/**
|
|
11
|
+
* Post-response context optimization.
|
|
12
|
+
* Called fire-and-forget from onFinish — summarizes older messages
|
|
13
|
+
* when token usage exceeds the configured threshold.
|
|
14
|
+
*/
|
|
15
|
+
export declare function optimizeThreadContext(thread: Thread, messages: UIMessage[], config: ResolvedCortexAgentConfig): Promise<void>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import type { ContextConfig } from "./types.ts";
|
|
3
|
+
type SummarizationModelConfig = NonNullable<ContextConfig["summarizationModel"]>;
|
|
4
|
+
export declare function summarizeMessages(messages: UIMessage[], existingSummary: string | null, modelConfig: SummarizationModelConfig): Promise<string>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
/** Average characters per token for English text. Used by the heuristic estimator. */
|
|
3
|
+
export declare const CHARS_PER_TOKEN = 4;
|
|
4
|
+
/**
|
|
5
|
+
* Estimates token count for a string using the chars/4 heuristic.
|
|
6
|
+
* ~10% accuracy for English text — good enough for budget decisions.
|
|
7
|
+
*/
|
|
8
|
+
export declare function estimateTokens(text: string): number;
|
|
9
|
+
/**
|
|
10
|
+
* Estimates token count for a single UIMessage by walking its parts.
|
|
11
|
+
*/
|
|
12
|
+
export declare function estimateMessageTokens(message: UIMessage): number;
|
|
13
|
+
/**
|
|
14
|
+
* Estimates token counts for an array of UIMessages.
|
|
15
|
+
* Returns per-message estimates in the same order.
|
|
16
|
+
*/
|
|
17
|
+
export declare function estimateMessagesTokens(messages: UIMessage[]): {
|
|
18
|
+
message: UIMessage<unknown, import("ai").UIDataTypes, import("ai").UITools>;
|
|
19
|
+
tokens: number;
|
|
20
|
+
}[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ContextConfig = {
|
|
2
|
+
maxContextTokens: number;
|
|
3
|
+
reservedTokenBudget: number;
|
|
4
|
+
summarizationThreshold: number;
|
|
5
|
+
summarizationModel?: {
|
|
6
|
+
baseURL: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
modelName: string;
|
|
9
|
+
providerName?: string;
|
|
10
|
+
};
|
|
11
|
+
toolResultMaxTokens: number;
|
|
12
|
+
recentMessagesToKeep: number;
|
|
13
|
+
};
|
|
14
|
+
export type ThreadContextMeta = {
|
|
15
|
+
summary: string | null;
|
|
16
|
+
summaryUpToMessageId: string | null;
|
|
17
|
+
totalEstimatedTokens: number;
|
|
18
|
+
lastOptimizedAt: string | null;
|
|
19
|
+
};
|
|
20
|
+
export declare const DEFAULT_CONTEXT_CONFIG: ContextConfig;
|
package/dist/src/ai/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ResolvedCortexAgentConfig } from "../config.ts";
|
|
2
2
|
import type { Thread } from "../types.ts";
|
|
3
|
-
export declare function stream(messages: unknown[], thread: Thread, userId: string, token: string, config: ResolvedCortexAgentConfig
|
|
3
|
+
export declare function stream(messages: unknown[], thread: Thread, userId: string, token: string, config: ResolvedCortexAgentConfig): Promise<Response>;
|
|
4
4
|
export declare function generateTitle(threadId: string, prompt: string, userId: string, config: ResolvedCortexAgentConfig): Promise<void>;
|
package/dist/src/ai/prompt.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
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
|
+
* Resolves session data for the thread, loading from the configured
|
|
6
|
+
* session loader if not already cached on the thread.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveSession(config: ResolvedCortexAgentConfig, thread: Thread, token: string): Promise<Record<string, unknown> | null>;
|
|
9
|
+
export declare function buildSystemPrompt(config: ResolvedCortexAgentConfig, resolved: ResolvedContext | null, session: Record<string, unknown> | null): Promise<string>;
|
package/dist/src/config.d.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
|
export type KnowledgeConfig = {
|
|
7
8
|
swagger?: {
|
|
8
9
|
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
|
export type ResolvedCortexAgentConfig = {
|
|
@@ -111,6 +113,7 @@ export type ResolvedCortexAgentConfig = {
|
|
|
111
113
|
messages: UIMessage[];
|
|
112
114
|
isAborted: boolean;
|
|
113
115
|
}) => void;
|
|
116
|
+
context: ContextConfig;
|
|
114
117
|
knowledge?: KnowledgeConfig;
|
|
115
118
|
};
|
|
116
119
|
export type CortexConfig = {
|
|
@@ -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
|
};
|
package/dist/src/db/schema.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
+
import type { MessageMetadata } from "src/types";
|
|
3
|
+
import type { ThreadContextMeta } from "../ai/context/types.ts";
|
|
2
4
|
export declare const aiSchema: import("drizzle-orm/mssql-core").MsSqlSchema<"ai">;
|
|
3
5
|
export declare const threads: import("drizzle-orm/mssql-core").MsSqlTableWithColumns<{
|
|
4
6
|
name: "threads";
|
|
@@ -116,6 +118,22 @@ export declare const threads: import("drizzle-orm/mssql-core").MsSqlTableWithCol
|
|
|
116
118
|
identity: undefined;
|
|
117
119
|
generated: undefined;
|
|
118
120
|
}, {}>;
|
|
121
|
+
contextMeta: import("drizzle-orm/mssql-core").MsSqlColumn<{
|
|
122
|
+
name: string;
|
|
123
|
+
tableName: "threads";
|
|
124
|
+
dataType: "object json";
|
|
125
|
+
data: ThreadContextMeta;
|
|
126
|
+
driverParam: string;
|
|
127
|
+
notNull: false;
|
|
128
|
+
hasDefault: false;
|
|
129
|
+
isPrimaryKey: false;
|
|
130
|
+
isAutoincrement: false;
|
|
131
|
+
hasRuntimeDefault: false;
|
|
132
|
+
enumValues: undefined;
|
|
133
|
+
baseColumn: never;
|
|
134
|
+
identity: undefined;
|
|
135
|
+
generated: undefined;
|
|
136
|
+
}, {}>;
|
|
119
137
|
};
|
|
120
138
|
dialect: "mssql";
|
|
121
139
|
}>;
|
|
@@ -207,7 +225,7 @@ export declare const messages: import("drizzle-orm/mssql-core").MsSqlTableWithCo
|
|
|
207
225
|
name: string;
|
|
208
226
|
tableName: "messages";
|
|
209
227
|
dataType: "object json";
|
|
210
|
-
data: UIMessage<
|
|
228
|
+
data: UIMessage<MessageMetadata, import("ai").UIDataTypes, import("ai").UITools>;
|
|
211
229
|
driverParam: string;
|
|
212
230
|
notNull: true;
|
|
213
231
|
hasDefault: false;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { CortexConfig, CortexAgentDefinition, KnowledgeConfig, DatabaseConfig, StorageConfig, } from "./config";
|
|
2
|
+
export type { ContextConfig, ThreadContextMeta } from "./ai/context/types";
|
|
2
3
|
export type { Thread, AppEnv } from "./types";
|
|
3
4
|
export type { CortexInstance } from "./factory";
|
|
4
5
|
export { createCortex } from "./factory";
|
package/dist/src/routes/ws.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { AppEnv } from "../types.ts";
|
|
3
|
-
|
|
3
|
+
type WsRouteOptions = {
|
|
4
|
+
useAgentParam?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function createWsRoute(options?: WsRouteOptions): Hono<AppEnv, import("hono/types").BlankSchema, "/">;
|
|
7
|
+
export {};
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import type { UIMessage } from "ai";
|
|
2
1
|
import type { ResolvedCortexAgentConfig } from "./config";
|
|
3
|
-
|
|
2
|
+
import type { InferSelectModel } from "drizzle-orm";
|
|
3
|
+
import type { messages, threads } from "./db/schema";
|
|
4
|
+
export type Thread = InferSelectModel<typeof threads>;
|
|
5
|
+
export type StoredMessage = InferSelectModel<typeof messages>;
|
|
6
|
+
export type ThreadSummary = {
|
|
4
7
|
id: string;
|
|
5
|
-
|
|
6
|
-
agentId: string;
|
|
7
|
-
title: string | null;
|
|
8
|
-
session: Record<string, unknown> | null;
|
|
8
|
+
title?: string;
|
|
9
9
|
createdAt: Date;
|
|
10
10
|
updatedAt: Date;
|
|
11
|
+
isRunning: boolean;
|
|
11
12
|
};
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
export type MessageMetadata = {
|
|
14
|
+
modelId: string;
|
|
15
|
+
providerMetadata: unknown;
|
|
16
|
+
isAborted?: boolean;
|
|
17
|
+
tokenUsage?: {
|
|
18
|
+
input: {
|
|
19
|
+
noCache: number;
|
|
20
|
+
cacheRead: number;
|
|
21
|
+
cacheWrite: number;
|
|
22
|
+
total: number;
|
|
23
|
+
};
|
|
24
|
+
output: {
|
|
25
|
+
reasoning: number;
|
|
26
|
+
text: number;
|
|
27
|
+
total: number;
|
|
28
|
+
};
|
|
29
|
+
total: number;
|
|
30
|
+
};
|
|
20
31
|
};
|
|
21
32
|
export type CapturedFileInput = {
|
|
22
33
|
id: string;
|
|
@@ -54,3 +65,10 @@ export type CortexAppEnv = {
|
|
|
54
65
|
agentId: string;
|
|
55
66
|
};
|
|
56
67
|
};
|
|
68
|
+
export declare function toThreadSummary(thread: Thread, isRunning: boolean): {
|
|
69
|
+
id: string;
|
|
70
|
+
title: string | undefined;
|
|
71
|
+
createdAt: Date;
|
|
72
|
+
updatedAt: Date;
|
|
73
|
+
isRunning: boolean;
|
|
74
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { WSContext } from "hono/ws";
|
|
2
|
-
export declare function addConnection(userId: string, ws: WSContext): void;
|
|
3
|
-
export declare function removeConnection(userId: string, ws: WSContext): void;
|
|
4
|
-
export declare function getConnections(userId: string): WSContext<unknown>[];
|
|
2
|
+
export declare function addConnection(userId: string, agentId: string, ws: WSContext): void;
|
|
3
|
+
export declare function removeConnection(userId: string, agentId: string, ws: WSContext): void;
|
|
4
|
+
export declare function getConnections(userId: string, agentId: string): WSContext<unknown>[];
|
package/dist/src/ws/events.d.ts
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
|
+
import type { ThreadSummary } from "../types.ts";
|
|
2
|
+
export type ThreadCreatedEvent = {
|
|
3
|
+
type: "thread:created";
|
|
4
|
+
payload: {
|
|
5
|
+
thread: ThreadSummary;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export type ThreadDeletedEvent = {
|
|
9
|
+
type: "thread:deleted";
|
|
10
|
+
payload: {
|
|
11
|
+
threadId: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
1
14
|
export type ThreadTitleUpdatedEvent = {
|
|
2
15
|
type: "thread:title-updated";
|
|
3
16
|
payload: {
|
|
4
|
-
|
|
5
|
-
|
|
17
|
+
thread: ThreadSummary;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type ThreadRunStartedEvent = {
|
|
21
|
+
type: "thread:run-started";
|
|
22
|
+
payload: {
|
|
23
|
+
thread: ThreadSummary;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type ThreadRunFinishedEvent = {
|
|
27
|
+
type: "thread:run-finished";
|
|
28
|
+
payload: {
|
|
29
|
+
thread: ThreadSummary;
|
|
6
30
|
};
|
|
7
31
|
};
|
|
8
32
|
export type ThreadMessagesUpdatedEvent = {
|
|
9
33
|
type: "thread:messages-updated";
|
|
10
34
|
payload: {
|
|
11
35
|
threadId: string;
|
|
36
|
+
thread: ThreadSummary;
|
|
12
37
|
};
|
|
13
38
|
};
|
|
14
|
-
export type WsEvent = ThreadTitleUpdatedEvent | ThreadMessagesUpdatedEvent;
|
|
39
|
+
export type WsEvent = ThreadCreatedEvent | ThreadDeletedEvent | ThreadTitleUpdatedEvent | ThreadRunStartedEvent | ThreadRunFinishedEvent | ThreadMessagesUpdatedEvent;
|
package/dist/src/ws/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { addConnection, removeConnection, getConnections } from "./connections.ts";
|
|
2
2
|
export { notify } from "./notify.ts";
|
|
3
|
-
export type { WsEvent, ThreadTitleUpdatedEvent, ThreadMessagesUpdatedEvent } from "./events.ts";
|
|
3
|
+
export type { WsEvent, ThreadCreatedEvent, ThreadDeletedEvent, ThreadTitleUpdatedEvent, ThreadRunStartedEvent, ThreadRunFinishedEvent, ThreadMessagesUpdatedEvent, } from "./events.ts";
|
package/dist/src/ws/notify.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { WsEvent } from "./events.ts";
|
|
2
|
-
export declare function notify(userId: string, event: WsEvent): void;
|
|
2
|
+
export declare function notify(userId: string, agentId: string, event: WsEvent): void;
|
package/package.json
CHANGED
package/src/adapters/database.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolUIPart, UIMessage } from "ai";
|
|
2
2
|
import type { Thread, StoredMessage, CapturedFileInput } from "../types";
|
|
3
|
+
import type { ThreadContextMeta } from "../ai/context/types.ts";
|
|
3
4
|
|
|
4
5
|
export type DatabaseAdapter = {
|
|
5
6
|
threads: {
|
|
@@ -7,8 +8,10 @@ export type DatabaseAdapter = {
|
|
|
7
8
|
getById(userId: string, threadId: string): Promise<Thread | null>;
|
|
8
9
|
create(userId: string, agentId: string): Promise<Thread>;
|
|
9
10
|
delete(userId: string, threadId: string): Promise<void>;
|
|
11
|
+
touch(threadId: string): Promise<Thread>;
|
|
10
12
|
updateTitle(threadId: string, title: string): Promise<void>;
|
|
11
13
|
updateSession(threadId: string, session: Record<string, unknown>): Promise<void>;
|
|
14
|
+
updateContextMeta(threadId: string, meta: ThreadContextMeta): Promise<void>;
|
|
12
15
|
};
|
|
13
16
|
messages: {
|
|
14
17
|
list(userId: string, threadId: string, opts?: { limit?: number }): Promise<StoredMessage[]>;
|
package/src/adapters/mssql.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { drizzle } from "drizzle-orm/node-mssql";
|
|
|
4
4
|
import { threads, messages, capturedFiles } from "../db/schema";
|
|
5
5
|
import type { DatabaseAdapter } from "./database";
|
|
6
6
|
import type { StorageAdapter } from "./storage";
|
|
7
|
-
import type { Thread, CapturedFileInput } from "../types";
|
|
7
|
+
import type { Thread, CapturedFileInput, MessageMetadata } from "../types";
|
|
8
|
+
import type { ThreadContextMeta } from "../ai/context/types.ts";
|
|
8
9
|
|
|
9
10
|
export function createMssqlAdapter(connectionString: string, storage: StorageAdapter) {
|
|
10
11
|
const db = drizzle(connectionString, { casing: "snake_case" });
|
|
@@ -16,7 +17,7 @@ export function createMssqlAdapter(connectionString: string, storage: StorageAda
|
|
|
16
17
|
.select()
|
|
17
18
|
.from(threads)
|
|
18
19
|
.where(and(eq(threads.userId, userId), eq(threads.agentId, agentId)))
|
|
19
|
-
.orderBy(desc(threads.createdAt))) as Thread[];
|
|
20
|
+
.orderBy(desc(threads.updatedAt), desc(threads.createdAt))) as Thread[];
|
|
20
21
|
},
|
|
21
22
|
|
|
22
23
|
async getById(userId: string, threadId: string) {
|
|
@@ -56,6 +57,17 @@ export function createMssqlAdapter(connectionString: string, storage: StorageAda
|
|
|
56
57
|
.execute();
|
|
57
58
|
},
|
|
58
59
|
|
|
60
|
+
async touch(threadId: string) {
|
|
61
|
+
const result = await db
|
|
62
|
+
.update(threads)
|
|
63
|
+
.set({ updatedAt: new Date() })
|
|
64
|
+
.where(eq(threads.id, threadId))
|
|
65
|
+
.output()
|
|
66
|
+
.execute();
|
|
67
|
+
|
|
68
|
+
return result[0] as Thread;
|
|
69
|
+
},
|
|
70
|
+
|
|
59
71
|
async updateTitle(threadId: string, title: string) {
|
|
60
72
|
await db.update(threads).set({ title }).where(eq(threads.id, threadId)).execute();
|
|
61
73
|
},
|
|
@@ -63,6 +75,14 @@ export function createMssqlAdapter(connectionString: string, storage: StorageAda
|
|
|
63
75
|
async updateSession(threadId: string, session: Record<string, unknown>) {
|
|
64
76
|
await db.update(threads).set({ session }).where(eq(threads.id, threadId)).execute();
|
|
65
77
|
},
|
|
78
|
+
|
|
79
|
+
async updateContextMeta(threadId: string, meta: ThreadContextMeta) {
|
|
80
|
+
await db
|
|
81
|
+
.update(threads)
|
|
82
|
+
.set({ contextMeta: meta })
|
|
83
|
+
.where(eq(threads.id, threadId))
|
|
84
|
+
.execute();
|
|
85
|
+
},
|
|
66
86
|
},
|
|
67
87
|
|
|
68
88
|
messages: {
|
|
@@ -78,8 +98,8 @@ export function createMssqlAdapter(connectionString: string, storage: StorageAda
|
|
|
78
98
|
.then((x) => x.reverse());
|
|
79
99
|
},
|
|
80
100
|
|
|
81
|
-
async upsert(threadId: string,
|
|
82
|
-
const ids =
|
|
101
|
+
async upsert(threadId: string, messagesToUpsert: UIMessage<MessageMetadata>[]) {
|
|
102
|
+
const ids = messagesToUpsert.map((x) => x.id);
|
|
83
103
|
|
|
84
104
|
const existingIds = await db
|
|
85
105
|
.select({ id: messages.id })
|
|
@@ -88,7 +108,7 @@ export function createMssqlAdapter(connectionString: string, storage: StorageAda
|
|
|
88
108
|
.execute()
|
|
89
109
|
.then((x) => x.map((y) => y.id));
|
|
90
110
|
|
|
91
|
-
const newMessages =
|
|
111
|
+
const newMessages = messagesToUpsert.filter((x) => !existingIds.includes(x.id));
|
|
92
112
|
|
|
93
113
|
if (newMessages.length) {
|
|
94
114
|
await db
|
|
@@ -108,7 +128,7 @@ export function createMssqlAdapter(connectionString: string, storage: StorageAda
|
|
|
108
128
|
.execute();
|
|
109
129
|
}
|
|
110
130
|
|
|
111
|
-
const existingMessages =
|
|
131
|
+
const existingMessages = messagesToUpsert.filter((x) => existingIds.includes(x.id));
|
|
112
132
|
for (const message of existingMessages) {
|
|
113
133
|
await db
|
|
114
134
|
.update(messages)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { registerStream, removeStream, subscribe } from "./active-streams.ts";
|
|
3
|
+
|
|
4
|
+
describe("active-streams", function () {
|
|
5
|
+
afterEach(function () {
|
|
6
|
+
removeStream("thread-under-test");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("does not remove a newer stream when cleaning up an older one", function () {
|
|
10
|
+
const firstStream = registerStream("thread-under-test", new AbortController());
|
|
11
|
+
const secondStream = registerStream("thread-under-test", new AbortController());
|
|
12
|
+
|
|
13
|
+
removeStream("thread-under-test", firstStream.id);
|
|
14
|
+
|
|
15
|
+
expect(subscribe("thread-under-test")).not.toBeNull();
|
|
16
|
+
|
|
17
|
+
removeStream("thread-under-test", secondStream.id);
|
|
18
|
+
|
|
19
|
+
expect(subscribe("thread-under-test")).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
type ActiveStream = {
|
|
2
|
+
id: string;
|
|
3
|
+
abortController: AbortController;
|
|
4
|
+
buffer: string[];
|
|
5
|
+
subscribers: Set<ReadableStreamDefaultController<string>>;
|
|
6
|
+
isComplete: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const streams = new Map<string, ActiveStream>();
|
|
10
|
+
|
|
11
|
+
export function registerStream(threadId: string, abortController: AbortController) {
|
|
12
|
+
const existing = streams.get(threadId);
|
|
13
|
+
if (existing) {
|
|
14
|
+
existing.abortController.abort();
|
|
15
|
+
for (const controller of existing.subscribers) {
|
|
16
|
+
try {
|
|
17
|
+
controller.close();
|
|
18
|
+
} catch {
|
|
19
|
+
/* already closed */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
streams.delete(threadId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entry: ActiveStream = {
|
|
26
|
+
id: crypto.randomUUID(),
|
|
27
|
+
abortController,
|
|
28
|
+
buffer: [],
|
|
29
|
+
subscribers: new Set(),
|
|
30
|
+
isComplete: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
streams.set(threadId, entry);
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function attachSseStream(threadId: string, sseStream: ReadableStream<string>) {
|
|
38
|
+
const entry = streams.get(threadId);
|
|
39
|
+
if (!entry) return;
|
|
40
|
+
|
|
41
|
+
const reader = sseStream.getReader();
|
|
42
|
+
(async function consume() {
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done) break;
|
|
47
|
+
entry.buffer.push(value);
|
|
48
|
+
for (const controller of entry.subscribers) {
|
|
49
|
+
try {
|
|
50
|
+
controller.enqueue(value);
|
|
51
|
+
} catch {
|
|
52
|
+
/* subscriber cancelled */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
/* stream aborted or errored */
|
|
58
|
+
} finally {
|
|
59
|
+
entry.isComplete = true;
|
|
60
|
+
for (const controller of entry.subscribers) {
|
|
61
|
+
try {
|
|
62
|
+
controller.close();
|
|
63
|
+
} catch {
|
|
64
|
+
/* already closed */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function subscribe(threadId: string) {
|
|
72
|
+
const entry = streams.get(threadId);
|
|
73
|
+
if (!entry) return null;
|
|
74
|
+
|
|
75
|
+
let savedController: ReadableStreamDefaultController<string>;
|
|
76
|
+
|
|
77
|
+
return new ReadableStream<string>({
|
|
78
|
+
start(controller) {
|
|
79
|
+
savedController = controller;
|
|
80
|
+
controller.enqueue("[START]"); // XXX: we need to add this flag for an immediate response for the subscriber, otherwise we'd have to wait for the first token from the llm.
|
|
81
|
+
for (const chunk of entry.buffer) {
|
|
82
|
+
controller.enqueue(chunk);
|
|
83
|
+
}
|
|
84
|
+
if (entry.isComplete) {
|
|
85
|
+
controller.close();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
entry.subscribers.add(controller);
|
|
89
|
+
},
|
|
90
|
+
cancel() {
|
|
91
|
+
entry.subscribers.delete(savedController);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function abortStream(threadId: string) {
|
|
97
|
+
const entry = streams.get(threadId);
|
|
98
|
+
if (!entry) return false;
|
|
99
|
+
queueMicrotask(() => entry.abortController.abort());
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isStreamRunning(threadId: string) {
|
|
104
|
+
const entry = streams.get(threadId);
|
|
105
|
+
return entry ? !entry.isComplete : false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function removeStream(threadId: string, streamId?: string) {
|
|
109
|
+
const entry = streams.get(threadId);
|
|
110
|
+
if (!entry) return;
|
|
111
|
+
if (streamId && entry.id !== streamId) return;
|
|
112
|
+
|
|
113
|
+
if (entry) {
|
|
114
|
+
for (const controller of entry.subscribers) {
|
|
115
|
+
try {
|
|
116
|
+
controller.close();
|
|
117
|
+
} catch {
|
|
118
|
+
/* already closed */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
streams.delete(threadId);
|
|
123
|
+
}
|