@m6d/cortex-server 1.7.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/contracts/README.md +23 -0
- package/contracts/graph/embed.ts +50 -0
- package/{src → contracts}/graph/helpers.ts +4 -10
- package/{src → contracts}/graph/neo4j.ts +14 -27
- package/contracts/graph/schema.ts +65 -0
- package/{src → contracts}/graph/types.ts +2 -29
- package/contracts/graph.ts +36 -0
- package/contracts/runtime.ts +208 -0
- package/contracts/wire.ts +143 -0
- package/dist/contracts/graph/embed.d.ts +22 -0
- package/dist/{src → contracts}/graph/helpers.d.ts +14 -12
- package/dist/{src → contracts}/graph/neo4j.d.ts +5 -5
- package/dist/contracts/graph/schema.d.ts +62 -0
- package/dist/{src → contracts}/graph/types.d.ts +1 -1
- package/dist/contracts/graph.d.ts +14 -0
- package/dist/contracts/runtime.d.ts +278 -0
- package/dist/contracts/wire.d.ts +120 -0
- package/dist/index.d.ts +1 -1
- package/dist/src/lib/adapters/database/index.d.ts +61 -0
- package/dist/src/lib/adapters/database/message-content.d.ts +30 -0
- package/dist/src/lib/adapters/database/mssql/attachments.d.ts +100 -0
- package/dist/src/lib/adapters/database/mssql/client.d.ts +8 -0
- package/dist/src/lib/adapters/database/mssql/index.d.ts +194 -0
- package/dist/src/lib/adapters/database/mssql/llm-requests.d.ts +17 -0
- package/dist/src/lib/adapters/database/mssql/messages.d.ts +37 -0
- package/dist/src/lib/adapters/database/mssql/threads.d.ts +50 -0
- package/dist/src/lib/adapters/database/postgres/attachments.d.ts +113 -0
- package/dist/src/lib/adapters/database/postgres/client.d.ts +8 -0
- package/dist/src/lib/adapters/database/postgres/index.d.ts +207 -0
- package/dist/src/lib/adapters/database/postgres/llm-requests.d.ts +17 -0
- package/dist/src/lib/adapters/database/postgres/messages.d.ts +37 -0
- package/dist/src/lib/adapters/database/postgres/threads.d.ts +50 -0
- package/{src/adapters/storage.ts → dist/src/lib/adapters/storage/index.d.ts} +1 -1
- package/dist/src/{adapters → lib/adapters/storage}/minio.d.ts +1 -1
- package/dist/src/lib/ai/active-runs.d.ts +63 -0
- package/dist/src/lib/ai/attachments.d.ts +30 -0
- package/dist/src/lib/ai/backend-url.d.ts +16 -0
- package/dist/src/lib/ai/cc-runtime.d.ts +61 -0
- package/dist/src/lib/ai/commit-gate.d.ts +21 -0
- package/dist/src/{ai → lib/ai}/context/builder.d.ts +6 -7
- package/dist/src/lib/ai/context/compression-middleware.d.ts +15 -0
- package/dist/src/lib/ai/context/compressor.d.ts +7 -0
- package/dist/src/lib/ai/context/intra-turn-compressor.d.ts +24 -0
- package/dist/src/lib/ai/context/optimize-thread-context.d.ts +9 -0
- package/dist/src/lib/ai/context/summarizer.d.ts +5 -0
- package/dist/src/{ai → lib/ai}/context/token-estimator.d.ts +5 -8
- package/dist/src/{ai → lib/ai}/context/types.d.ts +2 -0
- package/dist/src/{ai → lib/ai}/fetch.d.ts +1 -1
- package/dist/src/lib/ai/finish-turn.d.ts +23 -0
- package/dist/src/lib/ai/helpers.d.ts +38 -0
- package/dist/src/lib/ai/index.d.ts +7 -0
- package/dist/src/lib/ai/inspector.d.ts +41 -0
- package/dist/src/lib/ai/interceptors/request-interceptor.d.ts +13 -0
- package/dist/src/{ai → lib/ai}/prompt.d.ts +5 -4
- package/dist/src/lib/ai/redis-runs.d.ts +41 -0
- package/dist/src/lib/ai/redis-stream-log.d.ts +39 -0
- package/dist/src/lib/ai/tools/execute-code.tool.d.ts +18 -0
- package/dist/src/lib/ai/tools/query-graph.tool.d.ts +13 -0
- package/dist/src/lib/ai/tools/read-attachment.tool.d.ts +21 -0
- package/dist/src/lib/ai/tools/search-common.d.ts +17 -0
- package/dist/src/lib/ai/tools/search-knowledge.tool.d.ts +15 -0
- package/dist/src/lib/ai/tools/search-services.tool.d.ts +12 -0
- package/dist/src/lib/ai/tools/search-tools.tool.d.ts +12 -0
- package/dist/src/lib/ai/turn-tools.d.ts +39 -0
- package/dist/src/lib/ai/vision/rasterize-pdf.d.ts +26 -0
- package/dist/src/lib/ai/vision/rasterize-pdf.worker.d.ts +1 -0
- package/dist/src/lib/ai/vision/vision-reader.d.ts +45 -0
- package/dist/src/lib/cc/client.d.ts +183 -0
- package/dist/src/lib/cc/config-cache.d.ts +20 -0
- package/dist/src/lib/cc/format.d.ts +23 -0
- package/dist/src/lib/cc/registry.d.ts +34 -0
- package/dist/src/lib/cc/types.d.ts +27 -0
- package/dist/src/lib/config.d.ts +170 -0
- package/dist/src/lib/db/drizzle.config.mssql.d.ts +2 -0
- package/dist/src/lib/db/drizzle.config.pg.d.ts +2 -0
- package/dist/src/lib/db/migrate.d.ts +7 -0
- package/dist/src/{db/schema.d.ts → lib/db/schema.mssql.d.ts} +236 -24
- package/dist/src/lib/db/schema.pg.d.ts +546 -0
- package/dist/src/{factory.d.ts → lib/factory.d.ts} +2 -12
- package/dist/src/lib/graph/index.d.ts +10 -0
- package/dist/src/{graph → lib/graph}/resolver.d.ts +2 -3
- package/dist/src/{index.d.ts → lib/index.d.ts} +3 -4
- package/dist/src/lib/redis.d.ts +19 -0
- package/dist/src/{routes → lib/routes}/chat.d.ts +1 -1
- package/dist/src/{routes → lib/routes}/files.d.ts +1 -1
- package/dist/src/lib/routes/owned-thread.d.ts +17 -0
- package/dist/src/{routes → lib/routes}/threads.d.ts +1 -1
- package/dist/src/{routes → lib/routes}/ws.d.ts +1 -1
- package/dist/src/lib/types.d.ts +58 -0
- package/dist/src/{ws → lib/ws}/connections.d.ts +2 -1
- package/index.ts +1 -1
- package/package.json +35 -15
- package/src/lib/adapters/database/index.ts +78 -0
- package/src/lib/adapters/database/message-content.ts +39 -0
- package/src/lib/adapters/database/mssql/attachments.ts +90 -0
- package/src/lib/adapters/database/mssql/client.ts +11 -0
- package/src/lib/adapters/database/mssql/index.ts +23 -0
- package/src/lib/adapters/database/mssql/llm-requests.ts +43 -0
- package/src/lib/adapters/database/mssql/messages.ts +92 -0
- package/src/lib/adapters/database/mssql/threads.ts +83 -0
- package/src/lib/adapters/database/postgres/attachments.ts +88 -0
- package/src/lib/adapters/database/postgres/client.ts +11 -0
- package/src/lib/adapters/database/postgres/index.ts +23 -0
- package/src/lib/adapters/database/postgres/llm-requests.ts +45 -0
- package/src/lib/adapters/database/postgres/messages.ts +92 -0
- package/src/lib/adapters/database/postgres/threads.ts +84 -0
- package/{dist/src/adapters/storage.d.ts → src/lib/adapters/storage/index.ts} +1 -1
- package/src/{adapters → lib/adapters/storage}/minio.ts +7 -6
- package/src/lib/ai/active-runs.ts +157 -0
- package/src/lib/ai/attachments.ts +144 -0
- package/src/lib/ai/backend-url.ts +27 -0
- package/src/lib/ai/cc-runtime.ts +63 -0
- package/src/lib/ai/commit-gate.ts +116 -0
- package/src/{ai → lib/ai}/context/builder.ts +12 -14
- package/src/lib/ai/context/compression-middleware.ts +44 -0
- package/src/lib/ai/context/compressor.ts +267 -0
- package/src/lib/ai/context/intra-turn-compressor.ts +181 -0
- package/src/lib/ai/context/optimize-thread-context.ts +50 -0
- package/src/lib/ai/context/summarizer.ts +48 -0
- package/src/lib/ai/context/token-estimator.ts +65 -0
- package/src/{ai → lib/ai}/context/types.ts +4 -1
- package/src/{ai → lib/ai}/fetch.ts +10 -5
- package/src/lib/ai/finish-turn.ts +75 -0
- package/src/lib/ai/helpers.ts +100 -0
- package/src/lib/ai/index.ts +408 -0
- package/src/lib/ai/inspector.ts +112 -0
- package/src/lib/ai/interceptors/request-interceptor.ts +89 -0
- package/src/lib/ai/prompt.ts +355 -0
- package/src/lib/ai/redis-runs.ts +167 -0
- package/src/lib/ai/redis-stream-log.ts +219 -0
- package/src/lib/ai/tools/execute-code.tool.ts +267 -0
- package/src/{ai → lib/ai}/tools/query-graph.tool.ts +7 -8
- package/src/lib/ai/tools/read-attachment.tool.ts +59 -0
- package/src/lib/ai/tools/search-common.ts +15 -0
- package/src/lib/ai/tools/search-knowledge.tool.ts +33 -0
- package/src/lib/ai/tools/search-services.tool.ts +31 -0
- package/src/lib/ai/tools/search-tools.tool.ts +29 -0
- package/src/lib/ai/turn-tools.ts +154 -0
- package/src/lib/ai/vision/rasterize-pdf.ts +69 -0
- package/src/lib/ai/vision/rasterize-pdf.worker.ts +81 -0
- package/src/lib/ai/vision/vision-reader.ts +130 -0
- package/src/{auth → lib/auth}/middleware.ts +12 -3
- package/src/lib/cc/client.ts +273 -0
- package/src/lib/cc/config-cache.ts +32 -0
- package/src/lib/cc/format.ts +96 -0
- package/src/lib/cc/registry.ts +62 -0
- package/src/lib/cc/types.ts +44 -0
- package/src/lib/config.ts +212 -0
- package/src/lib/db/drizzle.config.mssql.ts +9 -0
- package/src/lib/db/drizzle.config.pg.ts +9 -0
- package/src/lib/db/migrate.ts +56 -0
- package/src/{db/migrations/20260309012148_cloudy_maria_hill → lib/db/migrations/mssql/20260326231647_nice_speedball}/migration.sql +12 -0
- package/src/{db/migrations/20260309012148_cloudy_maria_hill → lib/db/migrations/mssql/20260326231647_nice_speedball}/snapshot.json +106 -1
- package/src/lib/db/migrations/mssql/20260729092726_talented_wilson_fisk/migration.sql +20 -0
- package/src/lib/db/migrations/mssql/20260729092726_talented_wilson_fisk/snapshot.json +497 -0
- package/src/lib/db/migrations/mssql/20260801003652_perpetual_blue_shield/migration.sql +1 -0
- package/src/lib/db/migrations/mssql/20260801003652_perpetual_blue_shield/snapshot.json +516 -0
- package/src/lib/db/migrations/mssql/20260812174642_user_id_text/migration.sql +2 -0
- package/src/lib/db/migrations/mssql/20260812174642_user_id_text/snapshot.json +516 -0
- package/src/lib/db/migrations/pg/20260801084117_thick_ben_grimm/migration.sql +53 -0
- package/src/lib/db/migrations/pg/20260801084117_thick_ben_grimm/snapshot.json +584 -0
- package/src/lib/db/migrations/pg/20260812174622_user_id_text/migration.sql +2 -0
- package/src/lib/db/migrations/pg/20260812174622_user_id_text/snapshot.json +584 -0
- package/src/lib/db/schema.mssql.ts +94 -0
- package/src/lib/db/schema.pg.ts +95 -0
- package/src/lib/factory.ts +127 -0
- package/src/lib/graph/index.ts +19 -0
- package/src/lib/graph/resolver.ts +387 -0
- package/src/{index.ts → lib/index.ts} +13 -5
- package/src/lib/redis.ts +44 -0
- package/src/lib/routes/chat.ts +115 -0
- package/src/lib/routes/files.ts +129 -0
- package/src/lib/routes/owned-thread.ts +17 -0
- package/src/{routes → lib/routes}/threads.ts +22 -31
- package/src/{routes → lib/routes}/ws.ts +9 -3
- package/src/lib/types.ts +76 -0
- package/src/{ws → lib/ws}/connections.ts +9 -1
- package/tsconfig.json +48 -0
- package/dist/src/adapters/database.d.ts +0 -30
- package/dist/src/adapters/mssql.d.ts +0 -3
- package/dist/src/ai/active-streams.d.ts +0 -14
- package/dist/src/ai/context/compressor.d.ts +0 -7
- package/dist/src/ai/context/index.d.ts +0 -15
- package/dist/src/ai/context/summarizer.d.ts +0 -5
- package/dist/src/ai/helpers.d.ts +0 -5
- package/dist/src/ai/index.d.ts +0 -4
- package/dist/src/ai/interceptors/request-interceptor.d.ts +0 -12
- package/dist/src/ai/tools/call-endpoint.tool.d.ts +0 -7
- package/dist/src/ai/tools/capture-files.tool.d.ts +0 -6
- package/dist/src/ai/tools/execute-code.tool.d.ts +0 -4
- package/dist/src/ai/tools/query-graph.tool.d.ts +0 -5
- package/dist/src/cli/extract-endpoints.d.ts +0 -6
- package/dist/src/config.d.ts +0 -165
- package/dist/src/db/migrate.d.ts +0 -1
- package/dist/src/graph/expand-domains.d.ts +0 -2
- package/dist/src/graph/generate-cypher.d.ts +0 -22
- package/dist/src/graph/index.d.ts +0 -11
- package/dist/src/graph/seed.d.ts +0 -19
- package/dist/src/graph/validate.d.ts +0 -2
- package/dist/src/routes/index.d.ts +0 -4
- package/dist/src/types.d.ts +0 -74
- package/dist/src/ws/events.d.ts +0 -39
- package/dist/src/ws/index.d.ts +0 -3
- package/dist/src/ws/notify.d.ts +0 -2
- package/src/adapters/database.ts +0 -29
- package/src/adapters/mssql.ts +0 -195
- package/src/ai/active-streams.ts +0 -123
- package/src/ai/context/compressor.ts +0 -47
- package/src/ai/context/index.ts +0 -75
- package/src/ai/context/summarizer.ts +0 -50
- package/src/ai/context/token-estimator.ts +0 -60
- package/src/ai/helpers.ts +0 -32
- package/src/ai/index.ts +0 -277
- package/src/ai/interceptors/request-interceptor.ts +0 -61
- package/src/ai/prompt.ts +0 -126
- package/src/ai/tools/call-endpoint.tool.ts +0 -89
- package/src/ai/tools/capture-files.tool.ts +0 -20
- package/src/ai/tools/execute-code.tool.ts +0 -153
- package/src/cli/extract-endpoints.ts +0 -550
- package/src/config.ts +0 -182
- package/src/db/migrate.ts +0 -21
- package/src/db/migrations/20260315000000_add_context_meta/migration.sql +0 -1
- package/src/db/schema.ts +0 -70
- package/src/factory.ts +0 -171
- package/src/graph/expand-domains.ts +0 -276
- package/src/graph/generate-cypher.ts +0 -192
- package/src/graph/index.ts +0 -47
- package/src/graph/resolver.ts +0 -357
- package/src/graph/seed.ts +0 -162
- package/src/graph/validate.ts +0 -78
- package/src/routes/chat.ts +0 -86
- package/src/routes/files.ts +0 -100
- package/src/routes/index.ts +0 -4
- package/src/types.ts +0 -74
- package/src/ws/events.ts +0 -39
- package/src/ws/index.ts +0 -11
- package/src/ws/notify.ts +0 -9
- /package/dist/src/{auth → lib/auth}/middleware.d.ts +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { MessagePart } from "@tanstack/ai";
|
|
2
|
+
import { estimateTokens, CHARS_PER_TOKEN } from "./token-estimator";
|
|
3
|
+
import type { ChatMessage } from "@/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a new array of messages with large tool outputs truncated
|
|
7
|
+
* to `maxTokensPerResult`. Does not mutate the input messages.
|
|
8
|
+
*/
|
|
9
|
+
export function compressToolResults(messages: ChatMessage[], maxTokensPerResult: number) {
|
|
10
|
+
return messages.map((message) => {
|
|
11
|
+
const parts = message.parts.map((part) => compressPart(part, maxTokensPerResult));
|
|
12
|
+
const changed = parts.some((part, index) => part !== message.parts[index]);
|
|
13
|
+
|
|
14
|
+
return changed ? { ...message, parts } : message;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Both halves of a tool call can carry a payload big enough to crowd the window:
|
|
20
|
+
* the result the tool returned, and the output stashed on the call itself when the
|
|
21
|
+
* client executed it.
|
|
22
|
+
*/
|
|
23
|
+
function compressPart(part: MessagePart, maxTokensPerResult: number) {
|
|
24
|
+
if (part.type === "tool-result") {
|
|
25
|
+
const compressed = compressPayload(part.content, maxTokensPerResult);
|
|
26
|
+
return compressed === null ? part : { ...part, content: compressed };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (part.type === "tool-call" && part.output !== undefined) {
|
|
30
|
+
const compressed = compressPayload(part.output, maxTokensPerResult);
|
|
31
|
+
return compressed === null ? part : { ...part, output: compressed };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return part;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Returns the compressed form, or null when the payload already fits. */
|
|
38
|
+
function compressPayload(value: unknown, maxTokens: number) {
|
|
39
|
+
if (estimateTokens(JSON.stringify(value)) <= maxTokens) return null;
|
|
40
|
+
|
|
41
|
+
return smartStructuralCompress(value, maxTokens * CHARS_PER_TOKEN);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structurally compresses a value to fit within a character budget.
|
|
46
|
+
* Understands JSON arrays, error objects, and nested objects — preserving
|
|
47
|
+
* semantic meaning better than naive string truncation.
|
|
48
|
+
*/
|
|
49
|
+
// eslint-disable-next-line m6d/no-explicit-return-type -- mutually recursive with compressString, so the return type cannot be inferred
|
|
50
|
+
function smartStructuralCompress(value: unknown, charBudget: number): string {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
return compressString(value, charBudget);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return compressArray(value, charBudget);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof value === "object" && value !== null) {
|
|
60
|
+
return compressObject(value as Record<string, unknown>, charBudget);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Primitives
|
|
64
|
+
const str = String(value);
|
|
65
|
+
if (str.length <= charBudget) return str;
|
|
66
|
+
return str.slice(0, charBudget - 20) + "\n[...truncated]";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compressString(value: string, charBudget: number) {
|
|
70
|
+
if (value.length <= charBudget) return value;
|
|
71
|
+
|
|
72
|
+
// Try to parse as JSON first — if it's a stringified structure, compress structurally
|
|
73
|
+
try {
|
|
74
|
+
const parsed: unknown = JSON.parse(value);
|
|
75
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
76
|
+
const result = smartStructuralCompress(parsed, charBudget);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Not JSON, just truncate
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return value.slice(0, charBudget - 20) + "\n[...truncated]";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compressArray(value: unknown[], charBudget: number) {
|
|
87
|
+
const totalCount = value.length;
|
|
88
|
+
|
|
89
|
+
if (totalCount === 0) return "[]";
|
|
90
|
+
|
|
91
|
+
// Infer schema from first object element (if items are objects)
|
|
92
|
+
const firstObj = value.find(
|
|
93
|
+
(item): item is Record<string, unknown> =>
|
|
94
|
+
typeof item === "object" && item !== null && !Array.isArray(item),
|
|
95
|
+
);
|
|
96
|
+
const schemaKeys = firstObj ? Object.keys(firstObj) : null;
|
|
97
|
+
|
|
98
|
+
// Keep as many items as fit within ~60% of budget, leaving room for the summary footer
|
|
99
|
+
const itemBudget = Math.floor(charBudget * 0.6);
|
|
100
|
+
const items: unknown[] = [];
|
|
101
|
+
let accumulated = 0;
|
|
102
|
+
|
|
103
|
+
for (const item of value) {
|
|
104
|
+
const itemStr = JSON.stringify(item);
|
|
105
|
+
if (accumulated + itemStr.length > itemBudget && items.length > 0) break;
|
|
106
|
+
accumulated += itemStr.length;
|
|
107
|
+
items.push(item);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const shownStr = JSON.stringify(items, null, 2);
|
|
111
|
+
const remaining = totalCount - items.length;
|
|
112
|
+
|
|
113
|
+
if (remaining === 0 && shownStr.length <= charBudget) return shownStr;
|
|
114
|
+
|
|
115
|
+
const schemaHint = schemaKeys ? ` Schema: {${schemaKeys.join(", ")}}` : "";
|
|
116
|
+
const footer = `\n[COMPRESSED: ${remaining} more items of ${totalCount} total not shown.${schemaHint} Re-execute with filters or pagination to access specific items.]`;
|
|
117
|
+
|
|
118
|
+
const result = shownStr + footer;
|
|
119
|
+
if (result.length <= charBudget) return result;
|
|
120
|
+
|
|
121
|
+
// If still too large, fall back to a tighter representation
|
|
122
|
+
const tightStr = JSON.stringify(items);
|
|
123
|
+
return tightStr.slice(0, charBudget - footer.length - 20) + footer;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compressObject(value: Record<string, unknown>, charBudget: number) {
|
|
127
|
+
// Check if it looks like an error — keep message, drop stack
|
|
128
|
+
if (isErrorLike(value)) {
|
|
129
|
+
return compressErrorObject(value, charBudget);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fullStr = JSON.stringify(value, null, 2);
|
|
133
|
+
if (fullStr.length <= charBudget) return fullStr;
|
|
134
|
+
|
|
135
|
+
const allKeys = Object.keys(value);
|
|
136
|
+
|
|
137
|
+
// Phase 1: Partition keys into scalar (cheap) and complex (expensive)
|
|
138
|
+
const scalarEntries: string[] = [];
|
|
139
|
+
const complexKeys: string[] = [];
|
|
140
|
+
let scalarCharsUsed = 0;
|
|
141
|
+
|
|
142
|
+
for (const key of allKeys) {
|
|
143
|
+
const val = value[key];
|
|
144
|
+
const valStr = JSON.stringify(val);
|
|
145
|
+
|
|
146
|
+
if (valStr === undefined) {
|
|
147
|
+
const entry = ` "${key}": null`;
|
|
148
|
+
scalarEntries.push(entry);
|
|
149
|
+
scalarCharsUsed += entry.length + 2; // +2 for comma and newline
|
|
150
|
+
} else if (isScalar(val)) {
|
|
151
|
+
const entry = ` "${key}": ${valStr}`;
|
|
152
|
+
scalarEntries.push(entry);
|
|
153
|
+
scalarCharsUsed += entry.length + 2;
|
|
154
|
+
} else {
|
|
155
|
+
complexKeys.push(key);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Phase 2: Distribute remaining budget to complex values
|
|
160
|
+
const compressionHeader = ` "__compressed": "Some values were compressed. If you need the full data for a specific key, re-execute the tool returning only that key."`;
|
|
161
|
+
const headerCost = compressionHeader.length + 2;
|
|
162
|
+
const structuralOverhead = 4; // { } and newlines
|
|
163
|
+
const remainingBudget = charBudget - scalarCharsUsed - headerCost - structuralOverhead;
|
|
164
|
+
const complexBudget = Math.floor(
|
|
165
|
+
Math.max(remainingBudget, 100) / Math.max(complexKeys.length, 1),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const complexEntries: string[] = [];
|
|
169
|
+
const truncatedKeys: string[] = [];
|
|
170
|
+
|
|
171
|
+
for (const key of complexKeys) {
|
|
172
|
+
const val = value[key];
|
|
173
|
+
const valStr = JSON.stringify(val);
|
|
174
|
+
|
|
175
|
+
if (valStr !== undefined && valStr.length <= complexBudget) {
|
|
176
|
+
complexEntries.push(` "${key}": ${valStr}`);
|
|
177
|
+
} else {
|
|
178
|
+
truncatedKeys.push(key);
|
|
179
|
+
complexEntries.push(` "${key}": ${summarizeValue(val)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Build result with compression notice
|
|
184
|
+
const allEntries = [...scalarEntries, ...complexEntries];
|
|
185
|
+
|
|
186
|
+
if (truncatedKeys.length > 0) {
|
|
187
|
+
allEntries.push(compressionHeader);
|
|
188
|
+
allEntries.push(` "__truncatedKeys": ${JSON.stringify(truncatedKeys)}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = `{\n${allEntries.join(",\n")}\n}`;
|
|
192
|
+
if (result.length <= charBudget) return result;
|
|
193
|
+
|
|
194
|
+
// Final fallback: keep scalars + summaries, trim from the complex entries end
|
|
195
|
+
return result.slice(0, charBudget - 20) + "\n[...truncated]";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isScalar(val: unknown) {
|
|
199
|
+
return (
|
|
200
|
+
val === null ||
|
|
201
|
+
typeof val === "string" ||
|
|
202
|
+
typeof val === "number" ||
|
|
203
|
+
typeof val === "boolean"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isErrorLike(value: Record<string, unknown>) {
|
|
208
|
+
return "error" in value || "message" in value || "stack" in value || "stackTrace" in value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function compressErrorObject(value: Record<string, unknown>, charBudget: number) {
|
|
212
|
+
// Keep error/message fields, drop stack traces
|
|
213
|
+
const { stack: _stack, stackTrace: _stackTrace, ...rest } = value;
|
|
214
|
+
const compressed = JSON.stringify(rest, null, 2);
|
|
215
|
+
|
|
216
|
+
if (compressed.length <= charBudget) return compressed;
|
|
217
|
+
return compressed.slice(0, charBudget - 20) + "\n[...truncated]";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Produces a short human-readable summary of a value's shape and key data.
|
|
222
|
+
* Used both for inline object-key placeholders and for tool result one-liners.
|
|
223
|
+
*/
|
|
224
|
+
/** The first element stands in for the rest, which is the point of a summary. */
|
|
225
|
+
function summarizeArray(val: unknown[]) {
|
|
226
|
+
const first = val[0];
|
|
227
|
+
const schemaHint =
|
|
228
|
+
typeof first === "object" && first !== null
|
|
229
|
+
? ` {${Object.keys(first).slice(0, 5).join(", ")}}`
|
|
230
|
+
: "";
|
|
231
|
+
|
|
232
|
+
return `[Array(${val.length})${schemaHint}]`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A status/success envelope says more as its status plus a count than as its keys. */
|
|
236
|
+
function summarizeStatusEnvelope(obj: Record<string, unknown>, keys: string[]) {
|
|
237
|
+
const status = obj["status"] ?? obj["success"];
|
|
238
|
+
const dataKey = keys.find((k) => k === "data" || k === "result" || k === "results");
|
|
239
|
+
const data = dataKey ? obj[dataKey] : null;
|
|
240
|
+
const count = Array.isArray(data) ? `, ${data.length} items` : "";
|
|
241
|
+
|
|
242
|
+
return `{${String(status)}${count}}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function summarizeObject(obj: Record<string, unknown>) {
|
|
246
|
+
const keys = Object.keys(obj);
|
|
247
|
+
|
|
248
|
+
if ("status" in obj || "success" in obj) {
|
|
249
|
+
return summarizeStatusEnvelope(obj, keys);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const shown = keys.slice(0, 5).join(", ");
|
|
253
|
+
const ellipsis = keys.length > 5 ? ", ..." : "";
|
|
254
|
+
return `{Object(${keys.length} keys: ${shown}${ellipsis})}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function summarizeValue(val: unknown) {
|
|
258
|
+
if (Array.isArray(val)) return summarizeArray(val);
|
|
259
|
+
if (typeof val === "object" && val !== null) {
|
|
260
|
+
return summarizeObject(val as Record<string, unknown>);
|
|
261
|
+
}
|
|
262
|
+
if (typeof val === "string") {
|
|
263
|
+
return val.length > 80 ? `"${val.slice(0, 77)}..."` : JSON.stringify(val);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return String(val);
|
|
267
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { chat } from "@tanstack/ai";
|
|
2
|
+
import type { ModelMessage } from "@tanstack/ai";
|
|
3
|
+
import { createModel } from "@/ai/helpers";
|
|
4
|
+
import { estimateTokens } from "./token-estimator";
|
|
5
|
+
import { summarizeValue } from "./compressor";
|
|
6
|
+
import type { ContextConfig } from "./types";
|
|
7
|
+
|
|
8
|
+
type SummarizationModelConfig = NonNullable<ContextConfig["summarizationModel"]>;
|
|
9
|
+
|
|
10
|
+
type ToolCallOrigin = { name: string; step: number };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A tool message names only the call it answers, so both the tool's own name and the
|
|
14
|
+
* step it belongs to have to be recovered from the assistant message that asked for
|
|
15
|
+
* it. One tool message carries one result, so a step that issued three calls in
|
|
16
|
+
* parallel arrives as three messages; regrouping them into their step is what stops
|
|
17
|
+
* a step's own results from ageing each other out.
|
|
18
|
+
*/
|
|
19
|
+
function toolStepsOf(messages: ModelMessage[]) {
|
|
20
|
+
const calls = new Map<string, ToolCallOrigin>();
|
|
21
|
+
const steps: number[][] = [];
|
|
22
|
+
|
|
23
|
+
for (const [index, message] of messages.entries()) {
|
|
24
|
+
for (const call of message.toolCalls ?? []) {
|
|
25
|
+
calls.set(call.id, { name: call.function.name, step: steps.length });
|
|
26
|
+
}
|
|
27
|
+
if (message.role !== "tool") continue;
|
|
28
|
+
|
|
29
|
+
// A result whose call was never announced belongs to the step in progress;
|
|
30
|
+
// filing it anywhere else would invent a step that issued nothing.
|
|
31
|
+
const step = calls.get(message.toolCallId ?? "")?.step ?? steps.length;
|
|
32
|
+
(steps[step] ??= []).push(index);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { calls, steps };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toolNameOf(message: ModelMessage, calls: Map<string, ToolCallOrigin>) {
|
|
39
|
+
return calls.get(message.toolCallId ?? "")?.name ?? "tool";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A message's content is either a plain string or content parts, of which only the
|
|
44
|
+
* text ones can be digested — which is all a one-line stand-in was going to keep.
|
|
45
|
+
*/
|
|
46
|
+
function contentText(content: ModelMessage["content"]) {
|
|
47
|
+
if (typeof content === "string") return content;
|
|
48
|
+
if (content === null) return "";
|
|
49
|
+
|
|
50
|
+
return content.flatMap((part) => (part.type === "text" ? [part.content] : [])).join(" ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractPreview(text: string) {
|
|
54
|
+
// Try to parse as JSON for structured summary
|
|
55
|
+
try {
|
|
56
|
+
const parsed: unknown = JSON.parse(text);
|
|
57
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
58
|
+
return summarizeValue(parsed);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Not JSON
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (text.length <= 150) return text;
|
|
65
|
+
return text.slice(0, 147) + "...";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compresses tool results from older steps in the prompt.
|
|
70
|
+
*
|
|
71
|
+
* On step N, tool results from steps 1..N-2 are aggressively compressed
|
|
72
|
+
* to short one-liners. Only the most recent step's results stay in full.
|
|
73
|
+
* Does not mutate the input — returns a new array.
|
|
74
|
+
*/
|
|
75
|
+
export function compressIntraTurnToolResults(
|
|
76
|
+
messages: ModelMessage[],
|
|
77
|
+
toolResultMaxTokens: number,
|
|
78
|
+
) {
|
|
79
|
+
const { calls, steps } = toolStepsOf(messages);
|
|
80
|
+
|
|
81
|
+
// Below three there is nothing genuinely "old" to compress once the most
|
|
82
|
+
// recent step is kept intact.
|
|
83
|
+
if (steps.length < 3) return messages;
|
|
84
|
+
|
|
85
|
+
const result = [...messages];
|
|
86
|
+
|
|
87
|
+
for (const index of steps.slice(0, -1).flat()) {
|
|
88
|
+
const message = messages[index]!;
|
|
89
|
+
const text = contentText(message.content);
|
|
90
|
+
if (estimateTokens(text) <= toolResultMaxTokens) continue;
|
|
91
|
+
|
|
92
|
+
const tag = `[COMPRESSED prior ${toolNameOf(message, calls)} result]`;
|
|
93
|
+
result[index] = { ...message, content: `${tag} ${extractPreview(text)}` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Estimates the total token count of tool results in the prompt.
|
|
101
|
+
*/
|
|
102
|
+
export function estimateToolResultTokens(messages: ModelMessage[]) {
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (const message of messages) {
|
|
105
|
+
if (message.role === "tool") total += estimateTokens(contentText(message.content));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return total;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const SUMMARIZER_SYSTEM_PROMPT = `You are a tool result summarizer. Given previous tool call results from an AI agent's workflow, produce a concise summary preserving:
|
|
112
|
+
- Key data values, IDs, and counts
|
|
113
|
+
- Success/failure status of each operation
|
|
114
|
+
- Any error messages
|
|
115
|
+
- Data that subsequent tool calls may need
|
|
116
|
+
|
|
117
|
+
Maximum 300 tokens. Use bullet points. No preamble.`;
|
|
118
|
+
|
|
119
|
+
function requestSummary(resultTexts: string[], modelConfig: SummarizationModelConfig) {
|
|
120
|
+
return chat({
|
|
121
|
+
adapter: createModel(modelConfig, { name: "summarization" }),
|
|
122
|
+
systemPrompts: [SUMMARIZER_SYSTEM_PROMPT],
|
|
123
|
+
messages: [
|
|
124
|
+
{
|
|
125
|
+
role: "user",
|
|
126
|
+
content: `Summarize these tool results:\n\n${resultTexts.join("\n\n")}`,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
stream: false,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* When accumulated tool result tokens exceed the threshold, summarizes
|
|
135
|
+
* older step results using a lightweight LLM call.
|
|
136
|
+
*
|
|
137
|
+
* Replaces every tool result before the last two steps with a single
|
|
138
|
+
* synthetic summary in the oldest tool message.
|
|
139
|
+
*/
|
|
140
|
+
export async function summarizeOldStepResults(
|
|
141
|
+
messages: ModelMessage[],
|
|
142
|
+
thresholdTokens: number,
|
|
143
|
+
modelConfig: SummarizationModelConfig,
|
|
144
|
+
) {
|
|
145
|
+
if (estimateToolResultTokens(messages) <= thresholdTokens) return messages;
|
|
146
|
+
|
|
147
|
+
const { calls, steps } = toolStepsOf(messages);
|
|
148
|
+
|
|
149
|
+
// Summarization keeps the last two steps intact — one more than compression
|
|
150
|
+
// does, because a summary loses more detail than a truncated one-liner.
|
|
151
|
+
if (steps.length < 3) return messages;
|
|
152
|
+
|
|
153
|
+
// An emptied tool message has nothing to contribute to the digest, and nothing
|
|
154
|
+
// to gain from being pointed at one.
|
|
155
|
+
const indices = steps
|
|
156
|
+
.slice(0, -2)
|
|
157
|
+
.flat()
|
|
158
|
+
.filter((index) => contentText(messages[index]!.content) !== "");
|
|
159
|
+
if (indices.length === 0) return messages;
|
|
160
|
+
|
|
161
|
+
const resultTexts = indices.map((index) => {
|
|
162
|
+
const message = messages[index]!;
|
|
163
|
+
return `[${toolNameOf(message, calls)}]: ${contentText(message.content)}`;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const summary = await requestSummary(resultTexts, modelConfig);
|
|
167
|
+
|
|
168
|
+
// The first old tool message carries the summary and the rest point at it, so
|
|
169
|
+
// the model reads the digest once rather than once per collapsed message.
|
|
170
|
+
const result = [...messages];
|
|
171
|
+
indices.forEach((index, position) => {
|
|
172
|
+
const content =
|
|
173
|
+
position === 0
|
|
174
|
+
? `[Summary of ${indices.length} earlier tool results]:\n${summary}`
|
|
175
|
+
: "[see summary above]";
|
|
176
|
+
|
|
177
|
+
result[index] = { ...messages[index]!, content };
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ResolvedCortexAgentConfig } from "@/config";
|
|
2
|
+
import type { ChatMessage, Thread } from "@/types";
|
|
3
|
+
import type { ThreadContextMeta } from "./types";
|
|
4
|
+
import { estimateMessagesTokens } from "./token-estimator";
|
|
5
|
+
import { summarizeMessages } from "./summarizer";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Post-response context optimization, called fire-and-forget from onFinish.
|
|
9
|
+
*
|
|
10
|
+
* Below the summarization threshold this only refreshes the token estimate —
|
|
11
|
+
* summarizing early would burn a model call to compress context that still fits.
|
|
12
|
+
*/
|
|
13
|
+
export async function optimizeThreadContext(
|
|
14
|
+
thread: Thread,
|
|
15
|
+
messages: ChatMessage[],
|
|
16
|
+
config: ResolvedCortexAgentConfig,
|
|
17
|
+
) {
|
|
18
|
+
const contextConfig = config.context;
|
|
19
|
+
const estimates = estimateMessagesTokens(messages);
|
|
20
|
+
const totalEstimatedTokens = estimates.reduce((sum, e) => sum + e.tokens, 0);
|
|
21
|
+
const threshold = contextConfig.maxContextTokens * contextConfig.summarizationThreshold;
|
|
22
|
+
|
|
23
|
+
if (totalEstimatedTokens <= threshold) {
|
|
24
|
+
await config.db.threads.updateContextMeta(thread.id, {
|
|
25
|
+
summary: thread.contextMeta?.summary ?? null,
|
|
26
|
+
summaryUpToMessageId: thread.contextMeta?.summaryUpToMessageId ?? null,
|
|
27
|
+
totalEstimatedTokens,
|
|
28
|
+
lastOptimizedAt: new Date().toISOString(),
|
|
29
|
+
} satisfies ThreadContextMeta);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The most recent messages stay unsummarized — they're the hot context.
|
|
34
|
+
const recentCount = Math.min(contextConfig.recentMessagesToKeep, messages.length);
|
|
35
|
+
const messagesToSummarize = messages.slice(0, messages.length - recentCount);
|
|
36
|
+
if (messagesToSummarize.length === 0) return;
|
|
37
|
+
|
|
38
|
+
const summary = await summarizeMessages(
|
|
39
|
+
messagesToSummarize,
|
|
40
|
+
thread.contextMeta?.summary ?? null,
|
|
41
|
+
contextConfig.summarizationModel ?? config.model,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await config.db.threads.updateContextMeta(thread.id, {
|
|
45
|
+
summary,
|
|
46
|
+
summaryUpToMessageId: messagesToSummarize.at(-1)?.id ?? null,
|
|
47
|
+
totalEstimatedTokens,
|
|
48
|
+
lastOptimizedAt: new Date().toISOString(),
|
|
49
|
+
} satisfies ThreadContextMeta);
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { chat } from "@tanstack/ai";
|
|
2
|
+
import { createModel } from "@/ai/helpers";
|
|
3
|
+
import type { ChatMessage } from "@/types";
|
|
4
|
+
import type { ContextConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
type SummarizationModelConfig = NonNullable<ContextConfig["summarizationModel"]>;
|
|
7
|
+
|
|
8
|
+
const SYSTEM_PROMPT = `You are a precise conversation summarizer. Produce a concise summary that preserves:
|
|
9
|
+
- Key decisions and conclusions
|
|
10
|
+
- Important entities (names, IDs, URLs, values)
|
|
11
|
+
- User intent and goals
|
|
12
|
+
- Any unresolved questions or next steps
|
|
13
|
+
|
|
14
|
+
Maximum 500 tokens. Use bullet points. Do not include preamble.`;
|
|
15
|
+
|
|
16
|
+
export function summarizeMessages(
|
|
17
|
+
messages: ChatMessage[],
|
|
18
|
+
existingSummary: string | null,
|
|
19
|
+
modelConfig: SummarizationModelConfig,
|
|
20
|
+
) {
|
|
21
|
+
const conversationText = messages
|
|
22
|
+
.map(function (msg) {
|
|
23
|
+
const text = msg.parts
|
|
24
|
+
.flatMap((part) => (part.type === "text" ? [part.content] : []))
|
|
25
|
+
.join(" ");
|
|
26
|
+
return `[${msg.role}]: ${text}`;
|
|
27
|
+
})
|
|
28
|
+
.join("\n");
|
|
29
|
+
|
|
30
|
+
const summaryContext = existingSummary
|
|
31
|
+
? `\nPrior summary of earlier messages:\n${existingSummary}\n`
|
|
32
|
+
: "";
|
|
33
|
+
|
|
34
|
+
return chat({
|
|
35
|
+
adapter: createModel(modelConfig, { name: "summarization" }),
|
|
36
|
+
systemPrompts: [SYSTEM_PROMPT],
|
|
37
|
+
messages: [
|
|
38
|
+
{
|
|
39
|
+
role: "user",
|
|
40
|
+
content: `${summaryContext}
|
|
41
|
+
Summarize the following conversation:
|
|
42
|
+
|
|
43
|
+
${conversationText}`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
stream: false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MessagePart } from "@tanstack/ai";
|
|
2
|
+
import type { ChatMessage } from "@/types";
|
|
3
|
+
|
|
4
|
+
/** Average characters per token for English text. Used by the heuristic estimator. */
|
|
5
|
+
export const CHARS_PER_TOKEN = 4;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Estimates token count for a string using the chars/4 heuristic.
|
|
9
|
+
* ~10% accuracy for English text — good enough for budget decisions.
|
|
10
|
+
*/
|
|
11
|
+
export function estimateTokens(text: string) {
|
|
12
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Media and UI parts carry a source, not text. What the provider actually bills
|
|
17
|
+
* for them is set by its own tokenizer — tiles for an image, pages for a document —
|
|
18
|
+
* and the length of a base64 payload says nothing useful about it, so they are
|
|
19
|
+
* charged a flat rate instead.
|
|
20
|
+
*/
|
|
21
|
+
const OPAQUE_PART_TOKENS = 5;
|
|
22
|
+
|
|
23
|
+
function estimatePartTokens(part: MessagePart) {
|
|
24
|
+
switch (part.type) {
|
|
25
|
+
case "text":
|
|
26
|
+
case "thinking":
|
|
27
|
+
return estimateTokens(part.content);
|
|
28
|
+
case "tool-call": {
|
|
29
|
+
// `arguments` is already the JSON the model emitted. `output` is only
|
|
30
|
+
// set for client-executed tools and for calls resolved after approval.
|
|
31
|
+
const output: unknown = part.output;
|
|
32
|
+
const outputTokens = output === undefined ? 0 : estimateTokens(JSON.stringify(output));
|
|
33
|
+
|
|
34
|
+
return estimateTokens(part.arguments) + outputTokens;
|
|
35
|
+
}
|
|
36
|
+
case "tool-result":
|
|
37
|
+
return estimateTokens(
|
|
38
|
+
typeof part.content === "string" ? part.content : JSON.stringify(part.content),
|
|
39
|
+
);
|
|
40
|
+
case "structured-output":
|
|
41
|
+
return estimateTokens(part.raw);
|
|
42
|
+
default:
|
|
43
|
+
return OPAQUE_PART_TOKENS;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function estimateMessageTokens(message: ChatMessage) {
|
|
48
|
+
const parts = message.parts.reduce((sum, part) => sum + estimatePartTokens(part), 0);
|
|
49
|
+
|
|
50
|
+
// Per-message overhead (role, metadata framing)
|
|
51
|
+
return parts + 4;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Estimates token counts for an array of messages.
|
|
56
|
+
* Returns per-message estimates in the same order.
|
|
57
|
+
*/
|
|
58
|
+
export function estimateMessagesTokens(messages: ChatMessage[]) {
|
|
59
|
+
return messages.map(function (message) {
|
|
60
|
+
return {
|
|
61
|
+
message,
|
|
62
|
+
tokens: estimateMessageTokens(message),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -10,6 +10,8 @@ export type ContextConfig = {
|
|
|
10
10
|
};
|
|
11
11
|
toolResultMaxTokens: number;
|
|
12
12
|
recentMessagesToKeep: number;
|
|
13
|
+
/** Token threshold for triggering intra-turn summarization of older tool results */
|
|
14
|
+
intraTurnSummarizationThresholdTokens: number;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export type ThreadContextMeta = {
|
|
@@ -23,6 +25,7 @@ export const DEFAULT_CONTEXT_CONFIG: ContextConfig = {
|
|
|
23
25
|
maxContextTokens: 120_000,
|
|
24
26
|
reservedTokenBudget: 8_000,
|
|
25
27
|
summarizationThreshold: 0.75,
|
|
26
|
-
toolResultMaxTokens:
|
|
28
|
+
toolResultMaxTokens: 1_000,
|
|
27
29
|
recentMessagesToKeep: 6,
|
|
30
|
+
intraTurnSummarizationThresholdTokens: 20_000,
|
|
28
31
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ResolvedCortexAgentConfig } from "
|
|
1
|
+
import type { ResolvedCortexAgentConfig } from "@/config";
|
|
2
|
+
import { resolveBackendUrl } from "./backend-url";
|
|
2
3
|
|
|
3
4
|
export async function fetchBackend(
|
|
4
5
|
path: string,
|
|
@@ -14,18 +15,22 @@ export async function fetchBackend(
|
|
|
14
15
|
finalOptions?.body &&
|
|
15
16
|
typeof finalOptions.body === "string"
|
|
16
17
|
) {
|
|
18
|
+
let parsed: Record<string, unknown> | undefined;
|
|
17
19
|
try {
|
|
18
|
-
|
|
20
|
+
parsed = JSON.parse(finalOptions.body) as Record<string, unknown>;
|
|
21
|
+
} catch {
|
|
22
|
+
// Non-JSON bodies pass through unchanged.
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (parsed !== undefined) {
|
|
19
26
|
const transformed = await backendFetch.transformRequestBody(parsed, {
|
|
20
27
|
token,
|
|
21
28
|
});
|
|
22
29
|
finalOptions = { ...finalOptions, body: JSON.stringify(transformed) };
|
|
23
|
-
} catch {
|
|
24
|
-
// If body isn't valid JSON, pass through as-is
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
return fetch(
|
|
33
|
+
return fetch(resolveBackendUrl(backendFetch.baseUrl, path), {
|
|
29
34
|
...finalOptions,
|
|
30
35
|
headers: {
|
|
31
36
|
"Content-Type": "application/json",
|