@agi-cli/server 0.1.119 → 0.1.121
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/package.json +3 -3
- package/src/index.ts +9 -5
- package/src/openapi/paths/git.ts +4 -0
- package/src/routes/ask.ts +13 -14
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +1 -1
- package/src/routes/config/cwd.ts +1 -1
- package/src/routes/config/main.ts +1 -1
- package/src/routes/config/models.ts +32 -4
- package/src/routes/config/providers.ts +1 -1
- package/src/routes/config/utils.ts +14 -1
- package/src/routes/files.ts +1 -1
- package/src/routes/git/commit.ts +23 -6
- package/src/routes/git/schemas.ts +1 -0
- package/src/routes/session-files.ts +1 -1
- package/src/routes/session-messages.ts +2 -2
- package/src/routes/sessions.ts +8 -6
- package/src/runtime/agent/registry.ts +333 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +265 -0
- package/src/runtime/agent/runner.ts +356 -0
- package/src/runtime/agent-registry.ts +6 -333
- package/src/runtime/{ask-service.ts → ask/service.ts} +5 -5
- package/src/runtime/{debug.ts → debug/index.ts} +1 -1
- package/src/runtime/{api-error.ts → errors/api-error.ts} +2 -2
- package/src/runtime/message/compaction-auto.ts +137 -0
- package/src/runtime/message/compaction-context.ts +64 -0
- package/src/runtime/message/compaction-detect.ts +19 -0
- package/src/runtime/message/compaction-limits.ts +58 -0
- package/src/runtime/message/compaction-mark.ts +115 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +23 -0
- package/src/runtime/{history-builder.ts → message/history-builder.ts} +2 -2
- package/src/runtime/{message-service.ts → message/service.ts} +8 -14
- package/src/runtime/{history → message}/tool-history-tracker.ts +1 -1
- package/src/runtime/{prompt.ts → prompt/builder.ts} +1 -1
- package/src/runtime/{provider.ts → provider/anthropic.ts} +4 -219
- package/src/runtime/provider/google.ts +12 -0
- package/src/runtime/provider/index.ts +44 -0
- package/src/runtime/provider/openai.ts +26 -0
- package/src/runtime/provider/opencode.ts +61 -0
- package/src/runtime/provider/openrouter.ts +11 -0
- package/src/runtime/provider/solforge.ts +22 -0
- package/src/runtime/provider/zai.ts +53 -0
- package/src/runtime/session/branch.ts +277 -0
- package/src/runtime/{db-operations.ts → session/db-operations.ts} +1 -1
- package/src/runtime/{session-manager.ts → session/manager.ts} +1 -1
- package/src/runtime/{session-queue.ts → session/queue.ts} +2 -2
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +200 -0
- package/src/runtime/stream/finish-handler.ts +123 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +17 -0
- package/src/runtime/{tool-context.ts → tools/context.ts} +1 -1
- package/src/runtime/{tool-context-setup.ts → tools/setup.ts} +3 -3
- package/src/runtime/{token-utils.ts → utils/token.ts} +2 -2
- package/src/tools/adapter.ts +4 -4
- package/src/runtime/compaction.ts +0 -536
- package/src/runtime/runner.ts +0 -654
- package/src/runtime/stream-handlers.ts +0 -508
- /package/src/runtime/{cache-optimizer.ts → context/cache-optimizer.ts} +0 -0
- /package/src/runtime/{environment.ts → context/environment.ts} +0 -0
- /package/src/runtime/{context-optimizer.ts → context/optimizer.ts} +0 -0
- /package/src/runtime/{debug-state.ts → debug/state.ts} +0 -0
- /package/src/runtime/{error-handling.ts → errors/handling.ts} +0 -0
- /package/src/runtime/{history-truncator.ts → message/history-truncator.ts} +0 -0
- /package/src/runtime/{provider-selection.ts → provider/selection.ts} +0 -0
- /package/src/runtime/{tool-mapping.ts → tools/mapping.ts} +0 -0
- /package/src/runtime/{cwd.ts → utils/cwd.ts} +0 -0
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
createSession,
|
|
6
6
|
getLastSession,
|
|
7
7
|
getSessionById,
|
|
8
|
-
} from '
|
|
8
|
+
} from '../session/manager.ts';
|
|
9
9
|
import {
|
|
10
10
|
selectProviderAndModel,
|
|
11
11
|
type ProviderSelection,
|
|
12
|
-
} from '
|
|
13
|
-
import { resolveAgentConfig } from '
|
|
14
|
-
import { dispatchAssistantMessage } from '
|
|
12
|
+
} from '../provider/selection.ts';
|
|
13
|
+
import { resolveAgentConfig } from '../agent/registry.ts';
|
|
14
|
+
import { dispatchAssistantMessage } from '../message/service.ts';
|
|
15
15
|
import {
|
|
16
16
|
validateProviderModel,
|
|
17
17
|
isProviderAuthorized,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type ProviderId,
|
|
22
22
|
} from '@agi-cli/sdk';
|
|
23
23
|
import { sessions } from '@agi-cli/database/schema';
|
|
24
|
-
import { time } from '
|
|
24
|
+
import { time } from '../debug/index.ts';
|
|
25
25
|
|
|
26
26
|
export class AskServiceError extends Error {
|
|
27
27
|
constructor(
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* centralized debug-state and logger modules.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { isDebugEnabled as isDebugEnabledNew } from './
|
|
8
|
+
import { isDebugEnabled as isDebugEnabledNew } from './state.ts';
|
|
9
9
|
import { time as timeNew, debug as debugNew } from '@agi-cli/sdk';
|
|
10
10
|
|
|
11
11
|
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* across all API endpoints.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { isDebugEnabled } from '
|
|
9
|
-
import { toErrorPayload } from './
|
|
8
|
+
import { isDebugEnabled } from '../debug/state.ts';
|
|
9
|
+
import { toErrorPayload } from './handling.ts';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Standard API error response format
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { getDb } from '@agi-cli/database';
|
|
2
|
+
import { messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { streamText } from 'ai';
|
|
5
|
+
import { resolveModel } from '../provider/index.ts';
|
|
6
|
+
import { loadConfig } from '@agi-cli/sdk';
|
|
7
|
+
import { debugLog } from '../debug/index.ts';
|
|
8
|
+
import { getModelLimits } from './compaction-limits.ts';
|
|
9
|
+
import { buildCompactionContext } from './compaction-context.ts';
|
|
10
|
+
import { getCompactionSystemPrompt } from './compaction-detect.ts';
|
|
11
|
+
import { markSessionCompacted } from './compaction-mark.ts';
|
|
12
|
+
|
|
13
|
+
export async function performAutoCompaction(
|
|
14
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
15
|
+
sessionId: string,
|
|
16
|
+
assistantMessageId: string,
|
|
17
|
+
publishFn: (event: {
|
|
18
|
+
type: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
payload: Record<string, unknown>;
|
|
21
|
+
}) => void,
|
|
22
|
+
provider: string,
|
|
23
|
+
modelId: string,
|
|
24
|
+
): Promise<{
|
|
25
|
+
success: boolean;
|
|
26
|
+
summary?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
compactMessageId?: string;
|
|
29
|
+
}> {
|
|
30
|
+
debugLog(`[compaction] Starting auto-compaction for session ${sessionId}`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const limits = getModelLimits(provider, modelId);
|
|
34
|
+
const contextTokenLimit = limits
|
|
35
|
+
? Math.max(Math.floor(limits.context * 0.5), 15000)
|
|
36
|
+
: 15000;
|
|
37
|
+
debugLog(
|
|
38
|
+
`[compaction] Model ${modelId} context limit: ${limits?.context ?? 'unknown'}, using ${contextTokenLimit} tokens for compaction`,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const context = await buildCompactionContext(
|
|
42
|
+
db,
|
|
43
|
+
sessionId,
|
|
44
|
+
contextTokenLimit,
|
|
45
|
+
);
|
|
46
|
+
if (!context || context.length < 100) {
|
|
47
|
+
debugLog('[compaction] Not enough context to compact');
|
|
48
|
+
return { success: false, error: 'Not enough context to compact' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const cfg = await loadConfig();
|
|
52
|
+
debugLog(
|
|
53
|
+
`[compaction] Using session model ${provider}/${modelId} for auto-compaction`,
|
|
54
|
+
);
|
|
55
|
+
const model = await resolveModel(
|
|
56
|
+
provider as Parameters<typeof resolveModel>[0],
|
|
57
|
+
modelId,
|
|
58
|
+
cfg,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const compactPartId = crypto.randomUUID();
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
|
|
64
|
+
await db.insert(messageParts).values({
|
|
65
|
+
id: compactPartId,
|
|
66
|
+
messageId: assistantMessageId,
|
|
67
|
+
index: 0,
|
|
68
|
+
stepIndex: 0,
|
|
69
|
+
type: 'text',
|
|
70
|
+
content: JSON.stringify({ text: '' }),
|
|
71
|
+
agent: 'system',
|
|
72
|
+
provider: provider,
|
|
73
|
+
model: modelId,
|
|
74
|
+
startedAt: now,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const prompt = getCompactionSystemPrompt();
|
|
78
|
+
const result = streamText({
|
|
79
|
+
model,
|
|
80
|
+
system: `${prompt}\n\nIMPORTANT: Generate a comprehensive summary. This will replace the detailed conversation history.`,
|
|
81
|
+
messages: [
|
|
82
|
+
{
|
|
83
|
+
role: 'user',
|
|
84
|
+
content: `Please summarize this conversation:\n\n<conversation-to-summarize>\n${context}\n</conversation-to-summarize>`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
maxOutputTokens: 2000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let summary = '';
|
|
91
|
+
for await (const chunk of result.textStream) {
|
|
92
|
+
summary += chunk;
|
|
93
|
+
|
|
94
|
+
publishFn({
|
|
95
|
+
type: 'message.part.delta',
|
|
96
|
+
sessionId,
|
|
97
|
+
payload: {
|
|
98
|
+
messageId: assistantMessageId,
|
|
99
|
+
partId: compactPartId,
|
|
100
|
+
stepIndex: 0,
|
|
101
|
+
type: 'text',
|
|
102
|
+
delta: chunk,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await db
|
|
108
|
+
.update(messageParts)
|
|
109
|
+
.set({
|
|
110
|
+
content: JSON.stringify({ text: summary }),
|
|
111
|
+
completedAt: Date.now(),
|
|
112
|
+
})
|
|
113
|
+
.where(eq(messageParts.id, compactPartId));
|
|
114
|
+
|
|
115
|
+
if (!summary || summary.length < 50) {
|
|
116
|
+
debugLog('[compaction] Failed to generate summary');
|
|
117
|
+
return { success: false, error: 'Failed to generate summary' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
debugLog(`[compaction] Generated summary: ${summary.slice(0, 100)}...`);
|
|
121
|
+
|
|
122
|
+
const compactResult = await markSessionCompacted(
|
|
123
|
+
db,
|
|
124
|
+
sessionId,
|
|
125
|
+
assistantMessageId,
|
|
126
|
+
);
|
|
127
|
+
debugLog(
|
|
128
|
+
`[compaction] Marked ${compactResult.compacted} parts as compacted, saved ~${compactResult.saved} tokens`,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return { success: true, summary, compactMessageId: assistantMessageId };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
134
|
+
debugLog(`[compaction] Auto-compaction failed: ${errorMsg}`);
|
|
135
|
+
return { success: false, error: errorMsg };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { getDb } from '@agi-cli/database';
|
|
2
|
+
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq, asc } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
export async function buildCompactionContext(
|
|
6
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
7
|
+
sessionId: string,
|
|
8
|
+
contextTokenLimit?: number,
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
const allMessages = await db
|
|
11
|
+
.select()
|
|
12
|
+
.from(messages)
|
|
13
|
+
.where(eq(messages.sessionId, sessionId))
|
|
14
|
+
.orderBy(asc(messages.createdAt));
|
|
15
|
+
|
|
16
|
+
const lines: string[] = [];
|
|
17
|
+
let totalChars = 0;
|
|
18
|
+
const maxChars = contextTokenLimit ? contextTokenLimit * 4 : 60000;
|
|
19
|
+
|
|
20
|
+
for (const msg of allMessages) {
|
|
21
|
+
if (totalChars > maxChars) {
|
|
22
|
+
lines.unshift('[...earlier content truncated...]');
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parts = await db
|
|
27
|
+
.select()
|
|
28
|
+
.from(messageParts)
|
|
29
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
30
|
+
.orderBy(asc(messageParts.index));
|
|
31
|
+
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
if (part.compactedAt) continue;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const content = JSON.parse(part.content ?? '{}');
|
|
37
|
+
|
|
38
|
+
if (part.type === 'text' && content.text) {
|
|
39
|
+
const text = `[${msg.role.toUpperCase()}]: ${content.text}`;
|
|
40
|
+
lines.push(text.slice(0, 3000));
|
|
41
|
+
totalChars += text.length;
|
|
42
|
+
} else if (part.type === 'tool_call' && content.name) {
|
|
43
|
+
const argsStr =
|
|
44
|
+
typeof content.args === 'object'
|
|
45
|
+
? JSON.stringify(content.args).slice(0, 500)
|
|
46
|
+
: '';
|
|
47
|
+
const text = `[TOOL ${content.name}]: ${argsStr}`;
|
|
48
|
+
lines.push(text);
|
|
49
|
+
totalChars += text.length;
|
|
50
|
+
} else if (part.type === 'tool_result' && content.result !== null) {
|
|
51
|
+
const resultStr =
|
|
52
|
+
typeof content.result === 'string'
|
|
53
|
+
? content.result.slice(0, 1500)
|
|
54
|
+
: JSON.stringify(content.result ?? '').slice(0, 1500);
|
|
55
|
+
const text = `[RESULT]: ${resultStr}`;
|
|
56
|
+
lines.push(text);
|
|
57
|
+
totalChars += text.length;
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function isCompactCommand(content: string): boolean {
|
|
2
|
+
const trimmed = content.trim().toLowerCase();
|
|
3
|
+
return trimmed === '/compact';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function getCompactionSystemPrompt(): string {
|
|
7
|
+
return `
|
|
8
|
+
The user has requested to compact the conversation. Generate a comprehensive summary that captures:
|
|
9
|
+
|
|
10
|
+
1. **Main Goals**: What was the user trying to accomplish?
|
|
11
|
+
2. **Key Actions**: What files were created, modified, or deleted?
|
|
12
|
+
3. **Important Decisions**: What approaches or solutions were chosen and why?
|
|
13
|
+
4. **Current State**: What is done and what might be pending?
|
|
14
|
+
5. **Critical Context**: Any gotchas, errors encountered, or important details for continuing.
|
|
15
|
+
|
|
16
|
+
Format your response as a clear, structured summary. Start with "📦 **Context Compacted**" header.
|
|
17
|
+
Keep under 2000 characters but be thorough. This summary will replace detailed tool history.
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const PRUNE_PROTECT = 40_000;
|
|
2
|
+
|
|
3
|
+
export function estimateTokens(text: string): number {
|
|
4
|
+
return Math.max(0, Math.round((text || '').length / 4));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TokenUsage {
|
|
8
|
+
input: number;
|
|
9
|
+
output: number;
|
|
10
|
+
cacheRead?: number;
|
|
11
|
+
cacheWrite?: number;
|
|
12
|
+
reasoning?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ModelLimits {
|
|
16
|
+
context: number;
|
|
17
|
+
output: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isOverflow(tokens: TokenUsage, limits: ModelLimits): boolean {
|
|
21
|
+
if (limits.context === 0) return false;
|
|
22
|
+
|
|
23
|
+
const count = tokens.input + (tokens.cacheRead ?? 0) + tokens.output;
|
|
24
|
+
const usableContext = limits.context - limits.output;
|
|
25
|
+
|
|
26
|
+
return count > usableContext;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getModelLimits(
|
|
30
|
+
_provider: string,
|
|
31
|
+
model: string,
|
|
32
|
+
): ModelLimits | null {
|
|
33
|
+
const defaults: Record<string, ModelLimits> = {
|
|
34
|
+
'claude-sonnet-4-20250514': { context: 200000, output: 16000 },
|
|
35
|
+
'claude-3-5-sonnet-20241022': { context: 200000, output: 8192 },
|
|
36
|
+
'claude-3-5-haiku-20241022': { context: 200000, output: 8192 },
|
|
37
|
+
'gpt-4o': { context: 128000, output: 16384 },
|
|
38
|
+
'gpt-4o-mini': { context: 128000, output: 16384 },
|
|
39
|
+
o1: { context: 200000, output: 100000 },
|
|
40
|
+
'o3-mini': { context: 200000, output: 100000 },
|
|
41
|
+
'gemini-2.0-flash': { context: 1000000, output: 8192 },
|
|
42
|
+
'gemini-1.5-pro': { context: 2000000, output: 8192 },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (defaults[model]) return defaults[model];
|
|
46
|
+
|
|
47
|
+
for (const [key, limits] of Object.entries(defaults)) {
|
|
48
|
+
if (model.includes(key) || key.includes(model)) return limits;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isCompacted(part: { compactedAt?: number | null }): boolean {
|
|
55
|
+
return !!part.compactedAt;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const COMPACTED_PLACEHOLDER = '[Compacted]';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { getDb } from '@agi-cli/database';
|
|
2
|
+
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq, desc, and, lt } from 'drizzle-orm';
|
|
4
|
+
import { debugLog } from '../debug/index.ts';
|
|
5
|
+
import { estimateTokens, PRUNE_PROTECT } from './compaction-limits.ts';
|
|
6
|
+
|
|
7
|
+
const PROTECTED_TOOLS = ['skill'];
|
|
8
|
+
|
|
9
|
+
export async function markSessionCompacted(
|
|
10
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
11
|
+
sessionId: string,
|
|
12
|
+
compactMessageId: string,
|
|
13
|
+
): Promise<{ compacted: number; saved: number }> {
|
|
14
|
+
debugLog(`[compaction] Marking session ${sessionId} as compacted`);
|
|
15
|
+
|
|
16
|
+
const compactMsg = await db
|
|
17
|
+
.select()
|
|
18
|
+
.from(messages)
|
|
19
|
+
.where(eq(messages.id, compactMessageId))
|
|
20
|
+
.limit(1);
|
|
21
|
+
|
|
22
|
+
if (!compactMsg.length) {
|
|
23
|
+
debugLog('[compaction] Compact message not found');
|
|
24
|
+
return { compacted: 0, saved: 0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cutoffTime = compactMsg[0].createdAt;
|
|
28
|
+
|
|
29
|
+
const oldMessages = await db
|
|
30
|
+
.select()
|
|
31
|
+
.from(messages)
|
|
32
|
+
.where(
|
|
33
|
+
and(
|
|
34
|
+
eq(messages.sessionId, sessionId),
|
|
35
|
+
lt(messages.createdAt, cutoffTime),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
.orderBy(desc(messages.createdAt));
|
|
39
|
+
|
|
40
|
+
let totalTokens = 0;
|
|
41
|
+
let compactedTokens = 0;
|
|
42
|
+
const toCompact: Array<{ id: string; content: string }> = [];
|
|
43
|
+
let turns = 0;
|
|
44
|
+
|
|
45
|
+
for (const msg of oldMessages) {
|
|
46
|
+
if (msg.role === 'user') {
|
|
47
|
+
turns++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (turns < 2) continue;
|
|
51
|
+
|
|
52
|
+
const parts = await db
|
|
53
|
+
.select()
|
|
54
|
+
.from(messageParts)
|
|
55
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
56
|
+
.orderBy(desc(messageParts.index));
|
|
57
|
+
|
|
58
|
+
for (const part of parts) {
|
|
59
|
+
if (part.type !== 'tool_call' && part.type !== 'tool_result') continue;
|
|
60
|
+
|
|
61
|
+
if (part.toolName && PROTECTED_TOOLS.includes(part.toolName)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (part.compactedAt) continue;
|
|
66
|
+
|
|
67
|
+
let content: { result?: unknown; args?: unknown };
|
|
68
|
+
try {
|
|
69
|
+
content = JSON.parse(part.content ?? '{}');
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const contentStr =
|
|
75
|
+
part.type === 'tool_result'
|
|
76
|
+
? typeof content.result === 'string'
|
|
77
|
+
? content.result
|
|
78
|
+
: JSON.stringify(content.result ?? '')
|
|
79
|
+
: JSON.stringify(content.args ?? '');
|
|
80
|
+
|
|
81
|
+
const estimate = estimateTokens(contentStr);
|
|
82
|
+
totalTokens += estimate;
|
|
83
|
+
|
|
84
|
+
if (totalTokens > PRUNE_PROTECT) {
|
|
85
|
+
compactedTokens += estimate;
|
|
86
|
+
toCompact.push({ id: part.id, content: part.content ?? '{}' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
debugLog(
|
|
92
|
+
`[compaction] Found ${toCompact.length} parts to compact, saving ~${compactedTokens} tokens`,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (toCompact.length > 0) {
|
|
96
|
+
const compactedAt = Date.now();
|
|
97
|
+
|
|
98
|
+
for (const part of toCompact) {
|
|
99
|
+
try {
|
|
100
|
+
await db
|
|
101
|
+
.update(messageParts)
|
|
102
|
+
.set({ compactedAt })
|
|
103
|
+
.where(eq(messageParts.id, part.id));
|
|
104
|
+
} catch (err) {
|
|
105
|
+
debugLog(
|
|
106
|
+
`[compaction] Failed to mark part ${part.id}: ${err instanceof Error ? err.message : String(err)}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
debugLog(`[compaction] Marked ${toCompact.length} parts as compacted`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { compacted: toCompact.length, saved: compactedTokens };
|
|
115
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { getDb } from '@agi-cli/database';
|
|
2
|
+
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq, desc } from 'drizzle-orm';
|
|
4
|
+
import { debugLog } from '../debug/index.ts';
|
|
5
|
+
import { estimateTokens, PRUNE_PROTECT } from './compaction-limits.ts';
|
|
6
|
+
|
|
7
|
+
const PROTECTED_TOOLS = ['skill'];
|
|
8
|
+
|
|
9
|
+
export async function pruneSession(
|
|
10
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
11
|
+
sessionId: string,
|
|
12
|
+
): Promise<{ pruned: number; saved: number }> {
|
|
13
|
+
debugLog(`[compaction] Auto-pruning session ${sessionId}`);
|
|
14
|
+
|
|
15
|
+
const allMessages = await db
|
|
16
|
+
.select()
|
|
17
|
+
.from(messages)
|
|
18
|
+
.where(eq(messages.sessionId, sessionId))
|
|
19
|
+
.orderBy(desc(messages.createdAt));
|
|
20
|
+
|
|
21
|
+
let totalTokens = 0;
|
|
22
|
+
let prunedTokens = 0;
|
|
23
|
+
const toPrune: Array<{ id: string }> = [];
|
|
24
|
+
let turns = 0;
|
|
25
|
+
|
|
26
|
+
for (const msg of allMessages) {
|
|
27
|
+
if (msg.role === 'user') turns++;
|
|
28
|
+
if (turns < 2) continue;
|
|
29
|
+
|
|
30
|
+
const parts = await db
|
|
31
|
+
.select()
|
|
32
|
+
.from(messageParts)
|
|
33
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
34
|
+
.orderBy(desc(messageParts.index));
|
|
35
|
+
|
|
36
|
+
for (const part of parts) {
|
|
37
|
+
if (part.type !== 'tool_result') continue;
|
|
38
|
+
if (part.toolName && PROTECTED_TOOLS.includes(part.toolName)) continue;
|
|
39
|
+
if (part.compactedAt) continue;
|
|
40
|
+
|
|
41
|
+
let content: { result?: unknown };
|
|
42
|
+
try {
|
|
43
|
+
content = JSON.parse(part.content ?? '{}');
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const estimate = estimateTokens(
|
|
49
|
+
typeof content.result === 'string'
|
|
50
|
+
? content.result
|
|
51
|
+
: JSON.stringify(content.result ?? ''),
|
|
52
|
+
);
|
|
53
|
+
totalTokens += estimate;
|
|
54
|
+
|
|
55
|
+
if (totalTokens > PRUNE_PROTECT) {
|
|
56
|
+
prunedTokens += estimate;
|
|
57
|
+
toPrune.push({ id: part.id });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (toPrune.length > 0) {
|
|
63
|
+
const compactedAt = Date.now();
|
|
64
|
+
for (const part of toPrune) {
|
|
65
|
+
try {
|
|
66
|
+
await db
|
|
67
|
+
.update(messageParts)
|
|
68
|
+
.set({ compactedAt })
|
|
69
|
+
.where(eq(messageParts.id, part.id));
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { pruned: toPrune.length, saved: prunedTokens };
|
|
75
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PRUNE_PROTECT,
|
|
3
|
+
estimateTokens,
|
|
4
|
+
type TokenUsage,
|
|
5
|
+
type ModelLimits,
|
|
6
|
+
isOverflow,
|
|
7
|
+
getModelLimits,
|
|
8
|
+
isCompacted,
|
|
9
|
+
COMPACTED_PLACEHOLDER,
|
|
10
|
+
} from './compaction-limits.ts';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
isCompactCommand,
|
|
14
|
+
getCompactionSystemPrompt,
|
|
15
|
+
} from './compaction-detect.ts';
|
|
16
|
+
|
|
17
|
+
export { buildCompactionContext } from './compaction-context.ts';
|
|
18
|
+
|
|
19
|
+
export { markSessionCompacted } from './compaction-mark.ts';
|
|
20
|
+
|
|
21
|
+
export { pruneSession } from './compaction-prune.ts';
|
|
22
|
+
|
|
23
|
+
export { performAutoCompaction } from './compaction-auto.ts';
|
|
@@ -2,8 +2,8 @@ import { convertToModelMessages, type ModelMessage, type UIMessage } from 'ai';
|
|
|
2
2
|
import type { getDb } from '@agi-cli/database';
|
|
3
3
|
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
4
4
|
import { eq, asc } from 'drizzle-orm';
|
|
5
|
-
import { debugLog } from '
|
|
6
|
-
import { ToolHistoryTracker } from './
|
|
5
|
+
import { debugLog } from '../debug/index.ts';
|
|
6
|
+
import { ToolHistoryTracker } from './tool-history-tracker.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Builds the conversation history for a session from the database,
|
|
@@ -3,12 +3,12 @@ import { eq } from 'drizzle-orm';
|
|
|
3
3
|
import type { AGIConfig } from '@agi-cli/sdk';
|
|
4
4
|
import type { DB } from '@agi-cli/database';
|
|
5
5
|
import { messages, messageParts, sessions } from '@agi-cli/database/schema';
|
|
6
|
-
import { publish } from '
|
|
7
|
-
import { enqueueAssistantRun } from '
|
|
8
|
-
import { runSessionLoop } from '
|
|
9
|
-
import { resolveModel } from '
|
|
10
|
-
import {
|
|
11
|
-
import { debugLog } from '
|
|
6
|
+
import { publish } from '../../events/bus.ts';
|
|
7
|
+
import { enqueueAssistantRun } from '../session/queue.ts';
|
|
8
|
+
import { runSessionLoop } from '../agent/runner.ts';
|
|
9
|
+
import { resolveModel } from '../provider/index.ts';
|
|
10
|
+
import { getFastModelForAuth, type ProviderId } from '@agi-cli/sdk';
|
|
11
|
+
import { debugLog } from '../debug/index.ts';
|
|
12
12
|
import { isCompactCommand, buildCompactionContext } from './compaction.ts';
|
|
13
13
|
|
|
14
14
|
type SessionRow = typeof sessions.$inferSelect;
|
|
@@ -287,20 +287,14 @@ async function generateSessionTitle(args: {
|
|
|
287
287
|
debugLog(`[TITLE_GEN] Provider: ${provider}, Model: ${modelName}`);
|
|
288
288
|
|
|
289
289
|
const { getAuth } = await import('@agi-cli/sdk');
|
|
290
|
-
const { getProviderSpoofPrompt } = await import('
|
|
290
|
+
const { getProviderSpoofPrompt } = await import('../prompt/builder.ts');
|
|
291
291
|
const auth = await getAuth(provider, cfg.projectRoot);
|
|
292
292
|
const needsSpoof = auth?.type === 'oauth';
|
|
293
293
|
const spoofPrompt = needsSpoof
|
|
294
294
|
? getProviderSpoofPrompt(provider)
|
|
295
295
|
: undefined;
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
// Look up the cheapest/fastest model from the catalog for this provider
|
|
299
|
-
// For OpenAI OAuth, use codex-mini as it works with ChatGPT backend
|
|
300
|
-
const titleModel =
|
|
301
|
-
needsSpoof && provider === 'openai'
|
|
302
|
-
? 'gpt-5.1-codex-mini'
|
|
303
|
-
: (getFastModel(provider) ?? modelName);
|
|
297
|
+
const titleModel = getFastModelForAuth(provider, auth?.type) ?? modelName;
|
|
304
298
|
debugLog(`[TITLE_GEN] Using title model: ${titleModel}`);
|
|
305
299
|
const model = await resolveModel(provider, titleModel, cfg);
|
|
306
300
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { providerBasePrompt } from '@agi-cli/sdk';
|
|
2
|
-
import { composeEnvironmentAndInstructions } from '
|
|
2
|
+
import { composeEnvironmentAndInstructions } from '../context/environment.ts';
|
|
3
3
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
4
4
|
import BASE_PROMPT from '@agi-cli/sdk/prompts/base.txt' with { type: 'text' };
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|