@agi-cli/server 0.1.124 → 0.1.126
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 +4 -0
- package/src/routes/research.ts +423 -0
- package/src/routes/sessions.ts +33 -2
- package/src/runtime/agent/registry.ts +18 -0
- package/src/runtime/agent/runner-setup.ts +16 -0
- package/src/runtime/message/compaction-auto.ts +32 -3
- package/src/tools/database/get-parent-session.ts +178 -0
- package/src/tools/database/get-session-context.ts +156 -0
- package/src/tools/database/index.ts +42 -0
- package/src/tools/database/present-session-links.ts +47 -0
- package/src/tools/database/query-messages.ts +160 -0
- package/src/tools/database/query-sessions.ts +124 -0
- package/src/tools/database/search-history.ts +135 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDb } from '@agi-cli/database';
|
|
4
|
+
import { sessions, messages, messageParts } from '@agi-cli/database/schema';
|
|
5
|
+
import { eq, asc, count } from 'drizzle-orm';
|
|
6
|
+
|
|
7
|
+
const inputSchema = z.object({
|
|
8
|
+
includeMessages: z
|
|
9
|
+
.boolean()
|
|
10
|
+
.default(true)
|
|
11
|
+
.describe('Include message content from the parent session'),
|
|
12
|
+
messageLimit: z
|
|
13
|
+
.number()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(100)
|
|
16
|
+
.default(50)
|
|
17
|
+
.describe('Max messages to include'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function buildGetParentSessionTool(
|
|
21
|
+
projectRoot: string,
|
|
22
|
+
parentSessionId: string | null,
|
|
23
|
+
) {
|
|
24
|
+
return {
|
|
25
|
+
name: 'get_parent_session',
|
|
26
|
+
tool: tool({
|
|
27
|
+
description:
|
|
28
|
+
'Get the context of the parent session that this research session is attached to. Use this to understand what the user was working on and what they might be asking about. This is the FIRST tool you should call when the user asks about "this session" or "current work".',
|
|
29
|
+
inputSchema,
|
|
30
|
+
async execute(input) {
|
|
31
|
+
if (!parentSessionId) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error:
|
|
35
|
+
'No parent session - this research session is not attached to a main session',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const db = await getDb(projectRoot);
|
|
40
|
+
|
|
41
|
+
const sessionRows = await db
|
|
42
|
+
.select()
|
|
43
|
+
.from(sessions)
|
|
44
|
+
.where(eq(sessions.id, parentSessionId))
|
|
45
|
+
.limit(1);
|
|
46
|
+
|
|
47
|
+
if (sessionRows.length === 0) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: 'Parent session not found',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const session = sessionRows[0];
|
|
55
|
+
|
|
56
|
+
const msgCountResult = await db
|
|
57
|
+
.select({ count: count() })
|
|
58
|
+
.from(messages)
|
|
59
|
+
.where(eq(messages.sessionId, parentSessionId));
|
|
60
|
+
|
|
61
|
+
const toolCallsResult = await db
|
|
62
|
+
.select({
|
|
63
|
+
toolName: messageParts.toolName,
|
|
64
|
+
count: count(),
|
|
65
|
+
})
|
|
66
|
+
.from(messageParts)
|
|
67
|
+
.innerJoin(messages, eq(messageParts.messageId, messages.id))
|
|
68
|
+
.where(eq(messages.sessionId, parentSessionId))
|
|
69
|
+
.groupBy(messageParts.toolName);
|
|
70
|
+
|
|
71
|
+
const uniqueTools: string[] = [];
|
|
72
|
+
let totalToolCalls = 0;
|
|
73
|
+
for (const row of toolCallsResult) {
|
|
74
|
+
if (row.toolName) {
|
|
75
|
+
uniqueTools.push(row.toolName);
|
|
76
|
+
totalToolCalls += Number(row.count);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const totalTokens =
|
|
81
|
+
(session.totalInputTokens ?? 0) + (session.totalOutputTokens ?? 0);
|
|
82
|
+
|
|
83
|
+
const stats = {
|
|
84
|
+
totalMessages: msgCountResult[0]?.count ?? 0,
|
|
85
|
+
totalToolCalls,
|
|
86
|
+
uniqueTools,
|
|
87
|
+
totalTokens,
|
|
88
|
+
totalInputTokens: session.totalInputTokens ?? 0,
|
|
89
|
+
totalOutputTokens: session.totalOutputTokens ?? 0,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let messagesData:
|
|
93
|
+
| Array<{
|
|
94
|
+
id: string;
|
|
95
|
+
role: string;
|
|
96
|
+
content: string;
|
|
97
|
+
toolCalls?: Array<{ name: string; args?: string }>;
|
|
98
|
+
createdAt: number;
|
|
99
|
+
}>
|
|
100
|
+
| undefined;
|
|
101
|
+
|
|
102
|
+
if (input.includeMessages) {
|
|
103
|
+
const msgRows = await db
|
|
104
|
+
.select({
|
|
105
|
+
id: messages.id,
|
|
106
|
+
role: messages.role,
|
|
107
|
+
createdAt: messages.createdAt,
|
|
108
|
+
})
|
|
109
|
+
.from(messages)
|
|
110
|
+
.where(eq(messages.sessionId, parentSessionId))
|
|
111
|
+
.orderBy(asc(messages.createdAt))
|
|
112
|
+
.limit(input.messageLimit);
|
|
113
|
+
|
|
114
|
+
messagesData = await Promise.all(
|
|
115
|
+
msgRows.map(async (msg) => {
|
|
116
|
+
const parts = await db
|
|
117
|
+
.select({
|
|
118
|
+
type: messageParts.type,
|
|
119
|
+
content: messageParts.content,
|
|
120
|
+
toolName: messageParts.toolName,
|
|
121
|
+
})
|
|
122
|
+
.from(messageParts)
|
|
123
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
124
|
+
.orderBy(asc(messageParts.index));
|
|
125
|
+
|
|
126
|
+
let textContent = '';
|
|
127
|
+
const toolCalls: Array<{ name: string; args?: string }> = [];
|
|
128
|
+
|
|
129
|
+
for (const part of parts) {
|
|
130
|
+
if (part.type === 'text' && part.content) {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(part.content);
|
|
133
|
+
if (parsed?.text) {
|
|
134
|
+
textContent += parsed.text + '\n';
|
|
135
|
+
} else {
|
|
136
|
+
textContent += part.content + '\n';
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
textContent += part.content + '\n';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (part.type === 'tool_call' && part.toolName) {
|
|
143
|
+
toolCalls.push({
|
|
144
|
+
name: part.toolName,
|
|
145
|
+
args: part.content?.slice(0, 200),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
id: msg.id,
|
|
152
|
+
role: msg.role,
|
|
153
|
+
content: textContent.trim().slice(0, 2000),
|
|
154
|
+
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
|
155
|
+
createdAt: msg.createdAt,
|
|
156
|
+
};
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
parentSession: {
|
|
164
|
+
id: session.id,
|
|
165
|
+
title: session.title,
|
|
166
|
+
agent: session.agent,
|
|
167
|
+
provider: session.provider,
|
|
168
|
+
model: session.model,
|
|
169
|
+
createdAt: session.createdAt,
|
|
170
|
+
lastActiveAt: session.lastActiveAt,
|
|
171
|
+
},
|
|
172
|
+
stats,
|
|
173
|
+
...(messagesData ? { messages: messagesData } : {}),
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDb } from '@agi-cli/database';
|
|
4
|
+
import { sessions, messages, messageParts } from '@agi-cli/database/schema';
|
|
5
|
+
import { eq, asc, count } from 'drizzle-orm';
|
|
6
|
+
|
|
7
|
+
const inputSchema = z.object({
|
|
8
|
+
sessionId: z.string().describe('The session ID to get context for'),
|
|
9
|
+
includeMessages: z
|
|
10
|
+
.boolean()
|
|
11
|
+
.default(false)
|
|
12
|
+
.describe('Include full message content'),
|
|
13
|
+
messageLimit: z
|
|
14
|
+
.number()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(100)
|
|
17
|
+
.default(50)
|
|
18
|
+
.describe('Max messages to include if includeMessages is true'),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function buildGetSessionContextTool(projectRoot: string) {
|
|
22
|
+
return {
|
|
23
|
+
name: 'get_session_context',
|
|
24
|
+
tool: tool({
|
|
25
|
+
description:
|
|
26
|
+
'Get detailed context for a specific session including summary, stats, and optionally full messages. Use to understand what happened in a past conversation.',
|
|
27
|
+
inputSchema,
|
|
28
|
+
async execute(input) {
|
|
29
|
+
const db = await getDb(projectRoot);
|
|
30
|
+
|
|
31
|
+
const sessionRows = await db
|
|
32
|
+
.select()
|
|
33
|
+
.from(sessions)
|
|
34
|
+
.where(eq(sessions.id, input.sessionId))
|
|
35
|
+
.limit(1);
|
|
36
|
+
|
|
37
|
+
if (sessionRows.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'Session not found',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const session = sessionRows[0];
|
|
45
|
+
|
|
46
|
+
if (session.projectPath !== projectRoot) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: 'Session belongs to a different project',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const msgCountResult = await db
|
|
54
|
+
.select({ count: count() })
|
|
55
|
+
.from(messages)
|
|
56
|
+
.where(eq(messages.sessionId, input.sessionId));
|
|
57
|
+
|
|
58
|
+
const toolCallsResult = await db
|
|
59
|
+
.select({
|
|
60
|
+
toolName: messageParts.toolName,
|
|
61
|
+
count: count(),
|
|
62
|
+
})
|
|
63
|
+
.from(messageParts)
|
|
64
|
+
.innerJoin(messages, eq(messageParts.messageId, messages.id))
|
|
65
|
+
.where(eq(messages.sessionId, input.sessionId))
|
|
66
|
+
.groupBy(messageParts.toolName);
|
|
67
|
+
|
|
68
|
+
const uniqueTools: string[] = [];
|
|
69
|
+
let totalToolCalls = 0;
|
|
70
|
+
for (const row of toolCallsResult) {
|
|
71
|
+
if (row.toolName) {
|
|
72
|
+
uniqueTools.push(row.toolName);
|
|
73
|
+
totalToolCalls += Number(row.count);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const totalTokens =
|
|
78
|
+
(session.totalInputTokens ?? 0) + (session.totalOutputTokens ?? 0);
|
|
79
|
+
|
|
80
|
+
const stats = {
|
|
81
|
+
totalMessages: msgCountResult[0]?.count ?? 0,
|
|
82
|
+
totalToolCalls,
|
|
83
|
+
uniqueTools,
|
|
84
|
+
totalTokens,
|
|
85
|
+
totalInputTokens: session.totalInputTokens ?? 0,
|
|
86
|
+
totalOutputTokens: session.totalOutputTokens ?? 0,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let messagesData:
|
|
90
|
+
| Array<{
|
|
91
|
+
id: string;
|
|
92
|
+
role: string;
|
|
93
|
+
content: string;
|
|
94
|
+
createdAt: number;
|
|
95
|
+
}>
|
|
96
|
+
| undefined;
|
|
97
|
+
|
|
98
|
+
if (input.includeMessages) {
|
|
99
|
+
const msgRows = await db
|
|
100
|
+
.select({
|
|
101
|
+
id: messages.id,
|
|
102
|
+
role: messages.role,
|
|
103
|
+
createdAt: messages.createdAt,
|
|
104
|
+
})
|
|
105
|
+
.from(messages)
|
|
106
|
+
.where(eq(messages.sessionId, input.sessionId))
|
|
107
|
+
.orderBy(asc(messages.createdAt))
|
|
108
|
+
.limit(input.messageLimit);
|
|
109
|
+
|
|
110
|
+
messagesData = await Promise.all(
|
|
111
|
+
msgRows.map(async (msg) => {
|
|
112
|
+
const parts = await db
|
|
113
|
+
.select({
|
|
114
|
+
type: messageParts.type,
|
|
115
|
+
content: messageParts.content,
|
|
116
|
+
})
|
|
117
|
+
.from(messageParts)
|
|
118
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
119
|
+
.orderBy(asc(messageParts.index));
|
|
120
|
+
|
|
121
|
+
const content = parts
|
|
122
|
+
.filter((p) => p.type === 'text' && p.content)
|
|
123
|
+
.map((p) => p.content)
|
|
124
|
+
.join('\n');
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
id: msg.id,
|
|
128
|
+
role: msg.role,
|
|
129
|
+
content: content.slice(0, 2000),
|
|
130
|
+
createdAt: msg.createdAt,
|
|
131
|
+
};
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
session: {
|
|
139
|
+
id: session.id,
|
|
140
|
+
title: session.title,
|
|
141
|
+
agent: session.agent,
|
|
142
|
+
provider: session.provider,
|
|
143
|
+
model: session.model,
|
|
144
|
+
createdAt: session.createdAt,
|
|
145
|
+
lastActiveAt: session.lastActiveAt,
|
|
146
|
+
sessionType: session.sessionType,
|
|
147
|
+
parentSessionId: session.parentSessionId,
|
|
148
|
+
},
|
|
149
|
+
contextSummary: session.contextSummary ?? null,
|
|
150
|
+
stats,
|
|
151
|
+
...(messagesData ? { messages: messagesData } : {}),
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { buildQuerySessionsTool } from './query-sessions.ts';
|
|
2
|
+
import { buildQueryMessagesTool } from './query-messages.ts';
|
|
3
|
+
import { buildGetSessionContextTool } from './get-session-context.ts';
|
|
4
|
+
import { buildSearchHistoryTool } from './search-history.ts';
|
|
5
|
+
import { buildGetParentSessionTool } from './get-parent-session.ts';
|
|
6
|
+
import { buildPresentActionTool } from './present-session-links.ts';
|
|
7
|
+
|
|
8
|
+
export type DatabaseTool =
|
|
9
|
+
| ReturnType<typeof buildQuerySessionsTool>
|
|
10
|
+
| ReturnType<typeof buildQueryMessagesTool>
|
|
11
|
+
| ReturnType<typeof buildGetSessionContextTool>
|
|
12
|
+
| ReturnType<typeof buildSearchHistoryTool>
|
|
13
|
+
| ReturnType<typeof buildPresentActionTool>
|
|
14
|
+
| ReturnType<typeof buildGetParentSessionTool>;
|
|
15
|
+
|
|
16
|
+
export function buildDatabaseTools(
|
|
17
|
+
projectRoot: string,
|
|
18
|
+
parentSessionId?: string | null,
|
|
19
|
+
): DatabaseTool[] {
|
|
20
|
+
const tools: DatabaseTool[] = [
|
|
21
|
+
buildQuerySessionsTool(projectRoot),
|
|
22
|
+
buildQueryMessagesTool(projectRoot),
|
|
23
|
+
buildGetSessionContextTool(projectRoot),
|
|
24
|
+
buildSearchHistoryTool(projectRoot),
|
|
25
|
+
buildPresentActionTool(),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
if (parentSessionId) {
|
|
29
|
+
tools.push(buildGetParentSessionTool(projectRoot, parentSessionId));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return tools;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
buildQuerySessionsTool,
|
|
37
|
+
buildQueryMessagesTool,
|
|
38
|
+
buildGetSessionContextTool,
|
|
39
|
+
buildSearchHistoryTool,
|
|
40
|
+
buildGetParentSessionTool,
|
|
41
|
+
buildPresentActionTool,
|
|
42
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const sessionLinkSchema = z.object({
|
|
5
|
+
sessionId: z.string().describe('The session ID to link to'),
|
|
6
|
+
title: z.string().describe('Display title for the link'),
|
|
7
|
+
description: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe('Brief description of what this session contains'),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const inputSchema = z.object({
|
|
14
|
+
type: z
|
|
15
|
+
.enum(['session_links', 'info', 'warning'])
|
|
16
|
+
.default('session_links')
|
|
17
|
+
.describe('Type of action to present'),
|
|
18
|
+
title: z.string().optional().describe('Optional title for the action block'),
|
|
19
|
+
summary: z.string().optional().describe('Summary text to display'),
|
|
20
|
+
links: z
|
|
21
|
+
.array(sessionLinkSchema)
|
|
22
|
+
.max(10)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Session links to present (for session_links type)'),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function buildPresentActionTool() {
|
|
28
|
+
return {
|
|
29
|
+
name: 'present_action',
|
|
30
|
+
tool: tool({
|
|
31
|
+
description:
|
|
32
|
+
'Present an action block to the user with session links or information. Use at the end of your research to let users navigate directly to relevant sessions. The links will be rendered as clickable buttons.',
|
|
33
|
+
inputSchema,
|
|
34
|
+
async execute(input) {
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
type: input.type,
|
|
38
|
+
title: input.title,
|
|
39
|
+
summary: input.summary,
|
|
40
|
+
links: input.links || [],
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { buildPresentActionTool as buildPresentSessionLinksTool };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDb } from '@agi-cli/database';
|
|
4
|
+
import { sessions, messages, messageParts } from '@agi-cli/database/schema';
|
|
5
|
+
import { eq, desc, asc, gte, lte, and, like, count, sql } from 'drizzle-orm';
|
|
6
|
+
|
|
7
|
+
const inputSchema = z.object({
|
|
8
|
+
sessionId: z.string().optional().describe('Filter by specific session ID'),
|
|
9
|
+
role: z
|
|
10
|
+
.enum(['user', 'assistant', 'system', 'tool'])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Filter by message role'),
|
|
13
|
+
search: z.string().optional().describe('Full-text search in message content'),
|
|
14
|
+
toolName: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Filter by tool calls with this name'),
|
|
18
|
+
limit: z
|
|
19
|
+
.number()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(100)
|
|
22
|
+
.default(50)
|
|
23
|
+
.describe('Max messages to return'),
|
|
24
|
+
offset: z.number().min(0).default(0).describe('Offset for pagination'),
|
|
25
|
+
startDate: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Filter messages created after this ISO date'),
|
|
29
|
+
endDate: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Filter messages created before this ISO date'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export function buildQueryMessagesTool(projectRoot: string) {
|
|
36
|
+
return {
|
|
37
|
+
name: 'query_messages',
|
|
38
|
+
tool: tool({
|
|
39
|
+
description:
|
|
40
|
+
'Search messages across sessions. Find specific conversations, tool calls, or content patterns in session history.',
|
|
41
|
+
inputSchema,
|
|
42
|
+
async execute(input) {
|
|
43
|
+
const db = await getDb(projectRoot);
|
|
44
|
+
|
|
45
|
+
const conditions = [];
|
|
46
|
+
|
|
47
|
+
if (input.sessionId) {
|
|
48
|
+
conditions.push(eq(messages.sessionId, input.sessionId));
|
|
49
|
+
} else {
|
|
50
|
+
const projectSessions = db
|
|
51
|
+
.select({ id: sessions.id })
|
|
52
|
+
.from(sessions)
|
|
53
|
+
.where(eq(sessions.projectPath, projectRoot));
|
|
54
|
+
conditions.push(
|
|
55
|
+
sql`${messages.sessionId} IN (SELECT id FROM sessions WHERE project_path = ${projectRoot})`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (input.role) {
|
|
60
|
+
conditions.push(eq(messages.role, input.role));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (input.startDate) {
|
|
64
|
+
const startTs = new Date(input.startDate).getTime();
|
|
65
|
+
conditions.push(gte(messages.createdAt, startTs));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (input.endDate) {
|
|
69
|
+
const endTs = new Date(input.endDate).getTime();
|
|
70
|
+
conditions.push(lte(messages.createdAt, endTs));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const rows = await db
|
|
74
|
+
.select({
|
|
75
|
+
id: messages.id,
|
|
76
|
+
sessionId: messages.sessionId,
|
|
77
|
+
role: messages.role,
|
|
78
|
+
agent: messages.agent,
|
|
79
|
+
model: messages.model,
|
|
80
|
+
createdAt: messages.createdAt,
|
|
81
|
+
totalTokens: messages.totalTokens,
|
|
82
|
+
status: messages.status,
|
|
83
|
+
})
|
|
84
|
+
.from(messages)
|
|
85
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
86
|
+
.orderBy(desc(messages.createdAt))
|
|
87
|
+
.limit(input.limit * 2)
|
|
88
|
+
.offset(input.offset);
|
|
89
|
+
|
|
90
|
+
const messagesWithContent = await Promise.all(
|
|
91
|
+
rows.map(async (msg) => {
|
|
92
|
+
const parts = await db
|
|
93
|
+
.select({
|
|
94
|
+
type: messageParts.type,
|
|
95
|
+
content: messageParts.content,
|
|
96
|
+
toolName: messageParts.toolName,
|
|
97
|
+
})
|
|
98
|
+
.from(messageParts)
|
|
99
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
100
|
+
.orderBy(asc(messageParts.index))
|
|
101
|
+
.limit(10);
|
|
102
|
+
|
|
103
|
+
let contentPreview = '';
|
|
104
|
+
let hasMatchingTool = false;
|
|
105
|
+
|
|
106
|
+
for (const part of parts) {
|
|
107
|
+
if (part.type === 'text' && part.content) {
|
|
108
|
+
contentPreview = part.content.slice(0, 300);
|
|
109
|
+
}
|
|
110
|
+
if (input.toolName && part.toolName === input.toolName) {
|
|
111
|
+
hasMatchingTool = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (input.toolName && !hasMatchingTool) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
input.search &&
|
|
121
|
+
!contentPreview.toLowerCase().includes(input.search.toLowerCase())
|
|
122
|
+
) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const session = await db
|
|
127
|
+
.select({ title: sessions.title })
|
|
128
|
+
.from(sessions)
|
|
129
|
+
.where(eq(sessions.id, msg.sessionId))
|
|
130
|
+
.limit(1);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...msg,
|
|
134
|
+
sessionTitle: session[0]?.title ?? null,
|
|
135
|
+
contentPreview: contentPreview.slice(0, 200),
|
|
136
|
+
};
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const filtered = messagesWithContent.filter(
|
|
141
|
+
(m): m is NonNullable<typeof m> => m !== null,
|
|
142
|
+
);
|
|
143
|
+
const limited = filtered.slice(0, input.limit);
|
|
144
|
+
|
|
145
|
+
const countResult = await db
|
|
146
|
+
.select({ total: count() })
|
|
147
|
+
.from(messages)
|
|
148
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
messages: limited,
|
|
153
|
+
total: countResult[0]?.total ?? 0,
|
|
154
|
+
limit: input.limit,
|
|
155
|
+
offset: input.offset,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDb } from '@agi-cli/database';
|
|
4
|
+
import { sessions, messages } from '@agi-cli/database/schema';
|
|
5
|
+
import { eq, desc, asc, gte, lte, and, sql, count } from 'drizzle-orm';
|
|
6
|
+
|
|
7
|
+
const inputSchema = z.object({
|
|
8
|
+
limit: z
|
|
9
|
+
.number()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(100)
|
|
12
|
+
.default(20)
|
|
13
|
+
.describe('Max sessions to return'),
|
|
14
|
+
offset: z.number().min(0).default(0).describe('Offset for pagination'),
|
|
15
|
+
agent: z.string().optional().describe('Filter by agent type'),
|
|
16
|
+
startDate: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Filter sessions created after this ISO date'),
|
|
20
|
+
endDate: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Filter sessions created before this ISO date'),
|
|
24
|
+
orderBy: z
|
|
25
|
+
.enum(['created_at', 'last_active_at', 'total_tokens'])
|
|
26
|
+
.default('last_active_at')
|
|
27
|
+
.describe('Sort field'),
|
|
28
|
+
orderDir: z.enum(['asc', 'desc']).default('desc').describe('Sort direction'),
|
|
29
|
+
sessionType: z
|
|
30
|
+
.enum(['main', 'research', 'all'])
|
|
31
|
+
.default('main')
|
|
32
|
+
.describe('Filter by session type'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export function buildQuerySessionsTool(projectRoot: string) {
|
|
36
|
+
return {
|
|
37
|
+
name: 'query_sessions',
|
|
38
|
+
tool: tool({
|
|
39
|
+
description:
|
|
40
|
+
'Search and list sessions from the local database. Use to find past conversations, check what work was done, or locate specific sessions.',
|
|
41
|
+
inputSchema,
|
|
42
|
+
async execute(input) {
|
|
43
|
+
const db = await getDb(projectRoot);
|
|
44
|
+
|
|
45
|
+
const conditions = [eq(sessions.projectPath, projectRoot)];
|
|
46
|
+
|
|
47
|
+
if (input.sessionType !== 'all') {
|
|
48
|
+
conditions.push(eq(sessions.sessionType, input.sessionType));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (input.agent) {
|
|
52
|
+
conditions.push(eq(sessions.agent, input.agent));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (input.startDate) {
|
|
56
|
+
const startTs = new Date(input.startDate).getTime();
|
|
57
|
+
conditions.push(gte(sessions.createdAt, startTs));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (input.endDate) {
|
|
61
|
+
const endTs = new Date(input.endDate).getTime();
|
|
62
|
+
conditions.push(lte(sessions.createdAt, endTs));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const orderField =
|
|
66
|
+
input.orderBy === 'created_at'
|
|
67
|
+
? sessions.createdAt
|
|
68
|
+
: input.orderBy === 'total_tokens'
|
|
69
|
+
? sessions.totalInputTokens
|
|
70
|
+
: sessions.lastActiveAt;
|
|
71
|
+
|
|
72
|
+
const orderFn = input.orderDir === 'asc' ? asc : desc;
|
|
73
|
+
|
|
74
|
+
const rows = await db
|
|
75
|
+
.select({
|
|
76
|
+
id: sessions.id,
|
|
77
|
+
title: sessions.title,
|
|
78
|
+
agent: sessions.agent,
|
|
79
|
+
provider: sessions.provider,
|
|
80
|
+
model: sessions.model,
|
|
81
|
+
createdAt: sessions.createdAt,
|
|
82
|
+
lastActiveAt: sessions.lastActiveAt,
|
|
83
|
+
totalInputTokens: sessions.totalInputTokens,
|
|
84
|
+
totalOutputTokens: sessions.totalOutputTokens,
|
|
85
|
+
sessionType: sessions.sessionType,
|
|
86
|
+
parentSessionId: sessions.parentSessionId,
|
|
87
|
+
})
|
|
88
|
+
.from(sessions)
|
|
89
|
+
.where(and(...conditions))
|
|
90
|
+
.orderBy(orderFn(orderField), desc(sessions.createdAt))
|
|
91
|
+
.limit(input.limit)
|
|
92
|
+
.offset(input.offset);
|
|
93
|
+
|
|
94
|
+
const countResult = await db
|
|
95
|
+
.select({ total: count() })
|
|
96
|
+
.from(sessions)
|
|
97
|
+
.where(and(...conditions));
|
|
98
|
+
|
|
99
|
+
const total = countResult[0]?.total ?? 0;
|
|
100
|
+
|
|
101
|
+
const sessionsWithCounts = await Promise.all(
|
|
102
|
+
rows.map(async (row) => {
|
|
103
|
+
const msgCount = await db
|
|
104
|
+
.select({ count: count() })
|
|
105
|
+
.from(messages)
|
|
106
|
+
.where(eq(messages.sessionId, row.id));
|
|
107
|
+
return {
|
|
108
|
+
...row,
|
|
109
|
+
messageCount: msgCount[0]?.count ?? 0,
|
|
110
|
+
};
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
sessions: sessionsWithCounts,
|
|
117
|
+
total,
|
|
118
|
+
limit: input.limit,
|
|
119
|
+
offset: input.offset,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|