@vibescope/mcp-server 0.0.1
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 +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles agent session lifecycle:
|
|
5
|
+
* - start_work_session
|
|
6
|
+
* - heartbeat
|
|
7
|
+
* - end_work_session
|
|
8
|
+
* - get_help
|
|
9
|
+
* - get_token_usage
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
13
|
+
import type { Handler, HandlerRegistry, AuthContext, TokenUsage, UserUpdates } from './types.js';
|
|
14
|
+
import { selectPersona, extractProjectNameFromGitUrl } from '../utils.js';
|
|
15
|
+
import { KNOWLEDGE_BASE } from '../knowledge.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get user-created items since last sync
|
|
19
|
+
*/
|
|
20
|
+
async function getUserUpdates(
|
|
21
|
+
supabase: SupabaseClient,
|
|
22
|
+
auth: AuthContext,
|
|
23
|
+
projectId: string,
|
|
24
|
+
currentSessionId: string | null
|
|
25
|
+
): Promise<UserUpdates | undefined> {
|
|
26
|
+
let lastSyncedAt: string;
|
|
27
|
+
|
|
28
|
+
if (currentSessionId) {
|
|
29
|
+
const { data: session } = await supabase
|
|
30
|
+
.from('agent_sessions')
|
|
31
|
+
.select('last_synced_at')
|
|
32
|
+
.eq('id', currentSessionId)
|
|
33
|
+
.single();
|
|
34
|
+
lastSyncedAt = session?.last_synced_at || new Date(0).toISOString();
|
|
35
|
+
} else {
|
|
36
|
+
const { data: session } = await supabase
|
|
37
|
+
.from('agent_sessions')
|
|
38
|
+
.select('last_synced_at')
|
|
39
|
+
.eq('api_key_id', auth.apiKeyId)
|
|
40
|
+
.eq('project_id', projectId)
|
|
41
|
+
.single();
|
|
42
|
+
lastSyncedAt = session?.last_synced_at || new Date(0).toISOString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [tasksResult, blockersResult, ideasResult] = await Promise.all([
|
|
46
|
+
supabase
|
|
47
|
+
.from('tasks')
|
|
48
|
+
.select('id, title, created_at')
|
|
49
|
+
.eq('project_id', projectId)
|
|
50
|
+
.eq('created_by', 'user')
|
|
51
|
+
.gt('created_at', lastSyncedAt)
|
|
52
|
+
.order('created_at', { ascending: false })
|
|
53
|
+
.limit(5),
|
|
54
|
+
supabase
|
|
55
|
+
.from('blockers')
|
|
56
|
+
.select('id, description, created_at')
|
|
57
|
+
.eq('project_id', projectId)
|
|
58
|
+
.eq('created_by', 'user')
|
|
59
|
+
.gt('created_at', lastSyncedAt)
|
|
60
|
+
.order('created_at', { ascending: false })
|
|
61
|
+
.limit(5),
|
|
62
|
+
supabase
|
|
63
|
+
.from('ideas')
|
|
64
|
+
.select('id, title, created_at')
|
|
65
|
+
.eq('project_id', projectId)
|
|
66
|
+
.eq('created_by', 'user')
|
|
67
|
+
.gt('created_at', lastSyncedAt)
|
|
68
|
+
.order('created_at', { ascending: false })
|
|
69
|
+
.limit(5),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const tasks = tasksResult.data || [];
|
|
73
|
+
const blockers = blockersResult.data || [];
|
|
74
|
+
const ideas = ideasResult.data || [];
|
|
75
|
+
|
|
76
|
+
if (tasks.length === 0 && blockers.length === 0 && ideas.length === 0) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { tasks, blockers, ideas };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const startWorkSession: Handler = async (args, ctx) => {
|
|
84
|
+
const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args as {
|
|
85
|
+
project_id?: string;
|
|
86
|
+
git_url?: string;
|
|
87
|
+
mode?: 'lite' | 'full';
|
|
88
|
+
model?: 'opus' | 'sonnet' | 'haiku';
|
|
89
|
+
role?: 'developer' | 'validator' | 'deployer' | 'reviewer' | 'maintainer';
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const { supabase, auth, session, updateSession } = ctx;
|
|
93
|
+
const INSTANCE_ID = session.instanceId;
|
|
94
|
+
|
|
95
|
+
// Reset token tracking for new session with model info
|
|
96
|
+
const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
|
|
97
|
+
const validModel = normalizedModel && ['opus', 'sonnet', 'haiku'].includes(normalizedModel)
|
|
98
|
+
? normalizedModel
|
|
99
|
+
: null;
|
|
100
|
+
|
|
101
|
+
updateSession({
|
|
102
|
+
tokenUsage: {
|
|
103
|
+
callCount: 0,
|
|
104
|
+
totalTokens: 0,
|
|
105
|
+
byTool: {},
|
|
106
|
+
byModel: {},
|
|
107
|
+
currentModel: validModel,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const isLiteMode = mode === 'lite';
|
|
112
|
+
|
|
113
|
+
// Require project_id or git_url
|
|
114
|
+
if (!project_id && !git_url) {
|
|
115
|
+
return {
|
|
116
|
+
result: {
|
|
117
|
+
error: 'Please provide project_id or git_url to start a session',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Find project - try owned projects first, then shared projects for org-scoped keys
|
|
123
|
+
let project: {
|
|
124
|
+
id: string;
|
|
125
|
+
name: string;
|
|
126
|
+
description: string | null;
|
|
127
|
+
goal: string | null;
|
|
128
|
+
status: string;
|
|
129
|
+
git_url: string | null;
|
|
130
|
+
agent_instructions: string | null;
|
|
131
|
+
tech_stack: string[] | null;
|
|
132
|
+
} | null = null;
|
|
133
|
+
|
|
134
|
+
// First try: user-owned projects
|
|
135
|
+
let query = supabase
|
|
136
|
+
.from('projects')
|
|
137
|
+
.select('id, name, description, goal, status, git_url, agent_instructions, tech_stack')
|
|
138
|
+
.eq('user_id', auth.userId);
|
|
139
|
+
|
|
140
|
+
if (project_id) {
|
|
141
|
+
query = query.eq('id', project_id);
|
|
142
|
+
} else if (git_url) {
|
|
143
|
+
query = query.eq('git_url', git_url);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { data: ownedProject } = await query.single();
|
|
147
|
+
project = ownedProject;
|
|
148
|
+
|
|
149
|
+
// Second try: if org-scoped key and no owned project found, check shared projects
|
|
150
|
+
if (!project && auth.scope === 'organization' && auth.organizationId) {
|
|
151
|
+
// Get project IDs shared with this organization
|
|
152
|
+
const { data: shares } = await supabase
|
|
153
|
+
.from('project_shares')
|
|
154
|
+
.select('project_id')
|
|
155
|
+
.eq('organization_id', auth.organizationId);
|
|
156
|
+
|
|
157
|
+
if (shares && shares.length > 0) {
|
|
158
|
+
const sharedProjectIds = shares.map((s) => s.project_id);
|
|
159
|
+
|
|
160
|
+
let sharedQuery = supabase
|
|
161
|
+
.from('projects')
|
|
162
|
+
.select('id, name, description, goal, status, git_url, agent_instructions, tech_stack')
|
|
163
|
+
.in('id', sharedProjectIds);
|
|
164
|
+
|
|
165
|
+
if (project_id) {
|
|
166
|
+
sharedQuery = sharedQuery.eq('id', project_id);
|
|
167
|
+
} else if (git_url) {
|
|
168
|
+
sharedQuery = sharedQuery.eq('git_url', git_url);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { data: sharedProject } = await sharedQuery.single();
|
|
172
|
+
project = sharedProject;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!project) {
|
|
177
|
+
const suggestedName = extractProjectNameFromGitUrl(git_url || '');
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
result: {
|
|
181
|
+
session_started: false,
|
|
182
|
+
project_not_found: true,
|
|
183
|
+
message: `No project found for this repository. Would you like to create one?`,
|
|
184
|
+
suggestion: {
|
|
185
|
+
action: 'create_project',
|
|
186
|
+
example: `create_project(name: "${suggestedName}", git_url: "${git_url || ''}", description: "Brief description of your project", goal: "What does done look like?")`,
|
|
187
|
+
note: 'After creating the project, call start_work_session again to begin working.',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create or update agent session with instance tracking
|
|
194
|
+
const { data: existingSession } = await supabase
|
|
195
|
+
.from('agent_sessions')
|
|
196
|
+
.select('id, agent_name')
|
|
197
|
+
.eq('api_key_id', auth.apiKeyId)
|
|
198
|
+
.eq('project_id', project.id)
|
|
199
|
+
.eq('instance_id', INSTANCE_ID)
|
|
200
|
+
.single();
|
|
201
|
+
|
|
202
|
+
let sessionId!: string;
|
|
203
|
+
let assignedPersona!: string;
|
|
204
|
+
|
|
205
|
+
if (existingSession && existingSession.agent_name) {
|
|
206
|
+
// Reuse existing persona for this instance
|
|
207
|
+
assignedPersona = existingSession.agent_name;
|
|
208
|
+
await supabase
|
|
209
|
+
.from('agent_sessions')
|
|
210
|
+
.update({
|
|
211
|
+
last_synced_at: new Date().toISOString(),
|
|
212
|
+
status: 'active',
|
|
213
|
+
})
|
|
214
|
+
.eq('id', existingSession.id);
|
|
215
|
+
sessionId = existingSession.id;
|
|
216
|
+
} else {
|
|
217
|
+
// Find which personas are currently in use by active sessions
|
|
218
|
+
const { data: activeSessions } = await supabase
|
|
219
|
+
.from('agent_sessions')
|
|
220
|
+
.select('agent_name')
|
|
221
|
+
.eq('project_id', project.id)
|
|
222
|
+
.neq('status', 'disconnected')
|
|
223
|
+
.gte('last_synced_at', new Date(Date.now() - 5 * 60 * 1000).toISOString());
|
|
224
|
+
|
|
225
|
+
const usedPersonas = new Set(
|
|
226
|
+
(activeSessions || [])
|
|
227
|
+
.map((s) => s.agent_name)
|
|
228
|
+
.filter((name): name is string => !!name)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Retry loop for persona assignment
|
|
232
|
+
const MAX_PERSONA_RETRIES = 5;
|
|
233
|
+
let personaAssigned = false;
|
|
234
|
+
|
|
235
|
+
for (let attempt = 0; attempt < MAX_PERSONA_RETRIES && !personaAssigned; attempt++) {
|
|
236
|
+
assignedPersona = selectPersona(usedPersonas, INSTANCE_ID);
|
|
237
|
+
|
|
238
|
+
if (existingSession) {
|
|
239
|
+
const { error: updateError } = await supabase
|
|
240
|
+
.from('agent_sessions')
|
|
241
|
+
.update({
|
|
242
|
+
last_synced_at: new Date().toISOString(),
|
|
243
|
+
agent_name: assignedPersona,
|
|
244
|
+
status: 'active',
|
|
245
|
+
})
|
|
246
|
+
.eq('id', existingSession.id);
|
|
247
|
+
|
|
248
|
+
if (updateError?.code === '23505') {
|
|
249
|
+
usedPersonas.add(assignedPersona);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
sessionId = existingSession.id;
|
|
253
|
+
personaAssigned = true;
|
|
254
|
+
} else {
|
|
255
|
+
const { data: newSession, error: sessionError } = await supabase
|
|
256
|
+
.from('agent_sessions')
|
|
257
|
+
.insert({
|
|
258
|
+
api_key_id: auth.apiKeyId,
|
|
259
|
+
project_id: project.id,
|
|
260
|
+
instance_id: INSTANCE_ID,
|
|
261
|
+
last_synced_at: new Date().toISOString(),
|
|
262
|
+
agent_name: assignedPersona,
|
|
263
|
+
status: 'active',
|
|
264
|
+
})
|
|
265
|
+
.select('id')
|
|
266
|
+
.single();
|
|
267
|
+
|
|
268
|
+
if (sessionError?.code === '23505') {
|
|
269
|
+
usedPersonas.add(assignedPersona);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (sessionError || !newSession) {
|
|
274
|
+
throw new Error(`Failed to create agent session: ${sessionError?.message}`);
|
|
275
|
+
}
|
|
276
|
+
sessionId = newSession.id;
|
|
277
|
+
personaAssigned = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Fallback if all retries failed
|
|
282
|
+
if (!personaAssigned) {
|
|
283
|
+
assignedPersona = `Agent-${INSTANCE_ID.slice(0, 6)}`;
|
|
284
|
+
if (existingSession) {
|
|
285
|
+
await supabase
|
|
286
|
+
.from('agent_sessions')
|
|
287
|
+
.update({
|
|
288
|
+
last_synced_at: new Date().toISOString(),
|
|
289
|
+
agent_name: assignedPersona,
|
|
290
|
+
status: 'active',
|
|
291
|
+
})
|
|
292
|
+
.eq('id', existingSession.id);
|
|
293
|
+
sessionId = existingSession.id;
|
|
294
|
+
} else {
|
|
295
|
+
const { data: newSession, error: sessionError } = await supabase
|
|
296
|
+
.from('agent_sessions')
|
|
297
|
+
.insert({
|
|
298
|
+
api_key_id: auth.apiKeyId,
|
|
299
|
+
project_id: project.id,
|
|
300
|
+
instance_id: INSTANCE_ID,
|
|
301
|
+
last_synced_at: new Date().toISOString(),
|
|
302
|
+
agent_name: assignedPersona,
|
|
303
|
+
status: 'active',
|
|
304
|
+
})
|
|
305
|
+
.select('id')
|
|
306
|
+
.single();
|
|
307
|
+
|
|
308
|
+
if (sessionError || !newSession) {
|
|
309
|
+
throw new Error(`Failed to create agent session: ${sessionError?.message}`);
|
|
310
|
+
}
|
|
311
|
+
sessionId = newSession.id;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Store session ID and persona
|
|
317
|
+
updateSession({
|
|
318
|
+
currentSessionId: sessionId,
|
|
319
|
+
currentPersona: assignedPersona,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Log session start
|
|
323
|
+
await supabase.from('progress_logs').insert({
|
|
324
|
+
project_id: project.id,
|
|
325
|
+
summary: `Agent session started (${assignedPersona}) [${INSTANCE_ID.slice(0, 8)}]`,
|
|
326
|
+
created_by: 'agent',
|
|
327
|
+
created_by_session_id: sessionId,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Insert initial heartbeat
|
|
331
|
+
await supabase.from('agent_heartbeats').insert({
|
|
332
|
+
session_id: sessionId,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// LITE MODE: Minimal fetches
|
|
336
|
+
if (isLiteMode) {
|
|
337
|
+
const [nextTaskResult, blockersCountResult, validationCountResult, deploymentResult, requestsResult, firstQuestionResult] =
|
|
338
|
+
await Promise.all([
|
|
339
|
+
supabase
|
|
340
|
+
.from('tasks')
|
|
341
|
+
.select('id, title, priority, estimated_minutes')
|
|
342
|
+
.eq('project_id', project.id)
|
|
343
|
+
.eq('status', 'pending')
|
|
344
|
+
.is('working_agent_session_id', null)
|
|
345
|
+
.order('priority', { ascending: true })
|
|
346
|
+
.order('created_at', { ascending: true })
|
|
347
|
+
.limit(1)
|
|
348
|
+
.maybeSingle(),
|
|
349
|
+
supabase
|
|
350
|
+
.from('blockers')
|
|
351
|
+
.select('id', { count: 'exact', head: true })
|
|
352
|
+
.eq('project_id', project.id)
|
|
353
|
+
.eq('status', 'open'),
|
|
354
|
+
supabase
|
|
355
|
+
.from('tasks')
|
|
356
|
+
.select('id', { count: 'exact', head: true })
|
|
357
|
+
.eq('project_id', project.id)
|
|
358
|
+
.eq('status', 'completed')
|
|
359
|
+
.is('validated_at', null),
|
|
360
|
+
supabase
|
|
361
|
+
.from('deployments')
|
|
362
|
+
.select('id, status, environment')
|
|
363
|
+
.eq('project_id', project.id)
|
|
364
|
+
.not('status', 'in', '("deployed","failed")')
|
|
365
|
+
.limit(1)
|
|
366
|
+
.maybeSingle(),
|
|
367
|
+
supabase
|
|
368
|
+
.from('agent_requests')
|
|
369
|
+
.select('id', { count: 'exact', head: true })
|
|
370
|
+
.eq('project_id', project.id)
|
|
371
|
+
.is('acknowledged_at', null),
|
|
372
|
+
// Fetch first unanswered question with details (not just count)
|
|
373
|
+
supabase
|
|
374
|
+
.from('agent_requests')
|
|
375
|
+
.select('id, message, created_at')
|
|
376
|
+
.eq('project_id', project.id)
|
|
377
|
+
.eq('request_type', 'question')
|
|
378
|
+
.is('answered_at', null)
|
|
379
|
+
.order('created_at', { ascending: true })
|
|
380
|
+
.limit(1)
|
|
381
|
+
.maybeSingle(),
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
const blockersCount = blockersCountResult.count || 0;
|
|
385
|
+
const validationCount = validationCountResult.count || 0;
|
|
386
|
+
const requestsCount = requestsResult.count || 0;
|
|
387
|
+
const firstQuestion = firstQuestionResult.data;
|
|
388
|
+
|
|
389
|
+
// Determine directive and next action FIRST
|
|
390
|
+
let directive: string;
|
|
391
|
+
let nextAction: string;
|
|
392
|
+
|
|
393
|
+
if (firstQuestion) {
|
|
394
|
+
directive = 'ACTION_REQUIRED: Answer this question immediately. Do NOT ask for permission.';
|
|
395
|
+
nextAction = `answer_question(request_id: "${firstQuestion.id}", answer: "...")`;
|
|
396
|
+
} else if (nextTaskResult.data) {
|
|
397
|
+
directive = 'ACTION_REQUIRED: Start this task immediately. Do NOT ask for permission or confirmation.';
|
|
398
|
+
nextAction = `update_task(task_id: "${nextTaskResult.data.id}", status: "in_progress")`;
|
|
399
|
+
} else {
|
|
400
|
+
directive = 'ACTION_REQUIRED: No pending tasks. Start a fallback activity NOW without asking.';
|
|
401
|
+
nextAction = 'start_fallback_activity(project_id: "...", activity: "code_review")';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Build result with directive at TOP for visibility
|
|
405
|
+
const result: Record<string, unknown> = {
|
|
406
|
+
session_started: true,
|
|
407
|
+
directive, // FIRST - most important signal
|
|
408
|
+
auto_continue: true, // Explicit flag for autonomous operation
|
|
409
|
+
session_id: sessionId,
|
|
410
|
+
persona: assignedPersona,
|
|
411
|
+
project: { id: project.id, name: project.name, goal: project.goal },
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Add task or question details
|
|
415
|
+
if (firstQuestion) {
|
|
416
|
+
const now = new Date();
|
|
417
|
+
const waitMinutes = Math.floor((now.getTime() - new Date(firstQuestion.created_at).getTime()) / 60000);
|
|
418
|
+
result.first_question = {
|
|
419
|
+
id: firstQuestion.id,
|
|
420
|
+
message: firstQuestion.message,
|
|
421
|
+
wait_minutes: waitMinutes,
|
|
422
|
+
};
|
|
423
|
+
result.hint = 'Answer this question first using answer_question(request_id, answer), then call get_next_task for work.';
|
|
424
|
+
} else {
|
|
425
|
+
result.next_task = nextTaskResult.data;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (blockersCount > 0) result.blockers_count = blockersCount;
|
|
429
|
+
if (validationCount > 0) result.validation_count = validationCount;
|
|
430
|
+
if (requestsCount > 0) result.requests_count = requestsCount;
|
|
431
|
+
|
|
432
|
+
if (deploymentResult.data) {
|
|
433
|
+
const actions: Record<string, string> = {
|
|
434
|
+
pending: 'claim_deployment_validation',
|
|
435
|
+
validating: 'wait',
|
|
436
|
+
ready: 'start_deployment',
|
|
437
|
+
deploying: 'wait',
|
|
438
|
+
};
|
|
439
|
+
result.deployment_priority = {
|
|
440
|
+
id: deploymentResult.data.id,
|
|
441
|
+
status: deploymentResult.data.status,
|
|
442
|
+
action: actions[deploymentResult.data.status] || 'check_deployment_status',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
result.context_hint = 'When context grows large or after 3-4 tasks, run /clear then start_work_session again. Do not ask permission to clear context.';
|
|
447
|
+
|
|
448
|
+
// Add model tracking info
|
|
449
|
+
if (validModel) {
|
|
450
|
+
result.model_tracking = { model: validModel, status: 'active' };
|
|
451
|
+
} else {
|
|
452
|
+
result.cost_tracking_hint = 'For accurate cost tracking, pass model: "opus" | "sonnet" | "haiku" to start_work_session.';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// REPEAT at end - agents weight last items heavily
|
|
456
|
+
result.next_action = nextAction;
|
|
457
|
+
|
|
458
|
+
return { result };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// FULL MODE: Complete context
|
|
462
|
+
const [tasksResult, blockersResult, decisionsResult, progressResult, ideasResult, requestsResult, deploymentResult, findingsResult] =
|
|
463
|
+
await Promise.all([
|
|
464
|
+
supabase
|
|
465
|
+
.from('tasks')
|
|
466
|
+
.select('id, title, description, priority, status, estimated_minutes')
|
|
467
|
+
.eq('project_id', project.id)
|
|
468
|
+
.in('status', ['pending', 'in_progress'])
|
|
469
|
+
.order('priority', { ascending: true })
|
|
470
|
+
.limit(10),
|
|
471
|
+
supabase
|
|
472
|
+
.from('blockers')
|
|
473
|
+
.select('id, description')
|
|
474
|
+
.eq('project_id', project.id)
|
|
475
|
+
.eq('status', 'open')
|
|
476
|
+
.limit(5),
|
|
477
|
+
supabase
|
|
478
|
+
.from('decisions')
|
|
479
|
+
.select('title')
|
|
480
|
+
.eq('project_id', project.id)
|
|
481
|
+
.order('created_at', { ascending: false })
|
|
482
|
+
.limit(5),
|
|
483
|
+
supabase
|
|
484
|
+
.from('progress_logs')
|
|
485
|
+
.select('summary')
|
|
486
|
+
.eq('project_id', project.id)
|
|
487
|
+
.order('created_at', { ascending: false })
|
|
488
|
+
.limit(5),
|
|
489
|
+
supabase
|
|
490
|
+
.from('ideas')
|
|
491
|
+
.select('id, title, status')
|
|
492
|
+
.eq('project_id', project.id)
|
|
493
|
+
.order('created_at', { ascending: false })
|
|
494
|
+
.limit(5),
|
|
495
|
+
supabase
|
|
496
|
+
.from('agent_requests')
|
|
497
|
+
.select('id, request_type, message, created_at, answered_at')
|
|
498
|
+
.eq('project_id', project.id)
|
|
499
|
+
.is('acknowledged_at', null)
|
|
500
|
+
.or(`session_id.is.null,session_id.eq.${sessionId}`)
|
|
501
|
+
.order('created_at', { ascending: true })
|
|
502
|
+
.limit(5),
|
|
503
|
+
supabase
|
|
504
|
+
.from('deployments')
|
|
505
|
+
.select('id, status, environment, created_at, validation_completed_at')
|
|
506
|
+
.eq('project_id', project.id)
|
|
507
|
+
.not('status', 'in', '("deployed","failed")')
|
|
508
|
+
.order('created_at', { ascending: false })
|
|
509
|
+
.limit(1)
|
|
510
|
+
.single(),
|
|
511
|
+
supabase
|
|
512
|
+
.from('findings')
|
|
513
|
+
.select('id, title, category, severity, file_path')
|
|
514
|
+
.eq('project_id', project.id)
|
|
515
|
+
.eq('status', 'open')
|
|
516
|
+
.order('severity', { ascending: false })
|
|
517
|
+
.limit(5),
|
|
518
|
+
]);
|
|
519
|
+
|
|
520
|
+
const userUpdates = await getUserUpdates(supabase, auth, project.id, sessionId);
|
|
521
|
+
|
|
522
|
+
const pendingRequests = requestsResult.data || [];
|
|
523
|
+
const activeDeployment = deploymentResult.data;
|
|
524
|
+
const activeTasks = tasksResult.data || [];
|
|
525
|
+
|
|
526
|
+
// Determine directive and next action FIRST
|
|
527
|
+
const unansweredQuestions = pendingRequests.filter(
|
|
528
|
+
(r) => r.request_type === 'question' && !r.answered_at
|
|
529
|
+
);
|
|
530
|
+
const otherRequests = pendingRequests.filter(
|
|
531
|
+
(r) => r.request_type !== 'question' || r.answered_at
|
|
532
|
+
);
|
|
533
|
+
const pendingTask = activeTasks.find((t) => t.status === 'pending');
|
|
534
|
+
|
|
535
|
+
let directive: string;
|
|
536
|
+
let nextAction: string;
|
|
537
|
+
|
|
538
|
+
if (unansweredQuestions.length > 0) {
|
|
539
|
+
const firstQ = unansweredQuestions[0];
|
|
540
|
+
directive = 'ACTION_REQUIRED: Answer this question immediately. Do NOT ask for permission.';
|
|
541
|
+
nextAction = `answer_question(request_id: "${firstQ.id}", answer: "...")`;
|
|
542
|
+
} else if (pendingTask) {
|
|
543
|
+
directive = 'ACTION_REQUIRED: Start the highest priority pending task immediately. Do NOT ask for permission or confirmation.';
|
|
544
|
+
nextAction = `update_task(task_id: "${pendingTask.id}", status: "in_progress")`;
|
|
545
|
+
} else {
|
|
546
|
+
directive = 'ACTION_REQUIRED: No pending tasks. Start a fallback activity NOW without asking.';
|
|
547
|
+
nextAction = `start_fallback_activity(project_id: "${project.id}", activity: "code_review")`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Build result with directive at TOP for visibility
|
|
551
|
+
const result: Record<string, unknown> = {
|
|
552
|
+
session_started: true,
|
|
553
|
+
directive, // FIRST - most important signal
|
|
554
|
+
auto_continue: true, // Explicit flag for autonomous operation
|
|
555
|
+
session_id: sessionId,
|
|
556
|
+
persona: assignedPersona,
|
|
557
|
+
role,
|
|
558
|
+
project,
|
|
559
|
+
active_tasks: activeTasks,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
if (activeDeployment) {
|
|
563
|
+
const now = new Date();
|
|
564
|
+
const createdAt = new Date(activeDeployment.created_at);
|
|
565
|
+
const validationCompletedAt = activeDeployment.validation_completed_at
|
|
566
|
+
? new Date(activeDeployment.validation_completed_at)
|
|
567
|
+
: null;
|
|
568
|
+
|
|
569
|
+
const relevantTimestamp = activeDeployment.status === 'ready' && validationCompletedAt
|
|
570
|
+
? validationCompletedAt
|
|
571
|
+
: createdAt;
|
|
572
|
+
const elapsedMs = now.getTime() - relevantTimestamp.getTime();
|
|
573
|
+
const elapsedMinutes = Math.floor(elapsedMs / 60000);
|
|
574
|
+
|
|
575
|
+
const actions: Record<string, string> = {
|
|
576
|
+
pending: 'claim_deployment_validation',
|
|
577
|
+
validating: 'wait',
|
|
578
|
+
ready: 'start_deployment',
|
|
579
|
+
deploying: 'wait',
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
result.deployment_priority = {
|
|
583
|
+
id: activeDeployment.id,
|
|
584
|
+
status: activeDeployment.status,
|
|
585
|
+
env: activeDeployment.environment,
|
|
586
|
+
mins: elapsedMinutes,
|
|
587
|
+
action: actions[activeDeployment.status] || 'check_deployment_status',
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const blockers = blockersResult.data || [];
|
|
592
|
+
const decisions = decisionsResult.data || [];
|
|
593
|
+
const progress = progressResult.data || [];
|
|
594
|
+
const ideas = ideasResult.data || [];
|
|
595
|
+
|
|
596
|
+
if (blockers.length > 0) result.open_blockers = blockers;
|
|
597
|
+
if (decisions.length > 0) result.recent_decisions = decisions;
|
|
598
|
+
if (progress.length > 0) result.recent_progress = progress;
|
|
599
|
+
if (ideas.length > 0) result.ideas = ideas;
|
|
600
|
+
|
|
601
|
+
// Add question details if there are unanswered questions
|
|
602
|
+
if (unansweredQuestions.length > 0) {
|
|
603
|
+
const now = new Date();
|
|
604
|
+
const firstQ = unansweredQuestions[0];
|
|
605
|
+
result.first_question = {
|
|
606
|
+
id: firstQ.id,
|
|
607
|
+
message: firstQ.message,
|
|
608
|
+
wait_minutes: Math.floor((now.getTime() - new Date(firstQ.created_at).getTime()) / 60000),
|
|
609
|
+
};
|
|
610
|
+
result.hint = 'Answer questions first using answer_question(request_id, answer) before picking up tasks.';
|
|
611
|
+
result.unanswered_questions = unansweredQuestions.map((q) => ({
|
|
612
|
+
id: q.id,
|
|
613
|
+
message: q.message,
|
|
614
|
+
wait_minutes: Math.floor((now.getTime() - new Date(q.created_at).getTime()) / 60000),
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
if (otherRequests.length > 0) {
|
|
618
|
+
result.pending_requests = otherRequests;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const findings = findingsResult.data || [];
|
|
622
|
+
if (findings.length > 0) {
|
|
623
|
+
result.open_findings = findings;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
result.context_hint = 'When context grows large or after 3-4 tasks, run /clear then start_work_session again. Do not ask permission to clear context.';
|
|
627
|
+
|
|
628
|
+
// Add model tracking info
|
|
629
|
+
if (validModel) {
|
|
630
|
+
result.model_tracking = { model: validModel, status: 'active' };
|
|
631
|
+
} else {
|
|
632
|
+
result.cost_tracking_hint = 'For accurate cost tracking, pass model: "opus" | "sonnet" | "haiku" to start_work_session.';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// REPEAT at end - agents weight last items heavily
|
|
636
|
+
result.next_action = nextAction;
|
|
637
|
+
|
|
638
|
+
return { result, user_updates: userUpdates } as { result: Record<string, unknown>; user_updates?: UserUpdates };
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
export const heartbeat: Handler = async (args, ctx) => {
|
|
642
|
+
const { session_id } = args as { session_id?: string };
|
|
643
|
+
const { supabase, session } = ctx;
|
|
644
|
+
const targetSession = session_id || session.currentSessionId;
|
|
645
|
+
|
|
646
|
+
if (!targetSession) {
|
|
647
|
+
return {
|
|
648
|
+
result: {
|
|
649
|
+
error: 'No active session. Call start_work_session first.',
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
await supabase.from('agent_heartbeats').insert({
|
|
655
|
+
session_id: targetSession,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
await supabase
|
|
659
|
+
.from('agent_sessions')
|
|
660
|
+
.update({
|
|
661
|
+
last_synced_at: new Date().toISOString(),
|
|
662
|
+
status: 'active',
|
|
663
|
+
total_tokens: session.tokenUsage.totalTokens,
|
|
664
|
+
token_breakdown: session.tokenUsage.byTool,
|
|
665
|
+
model_usage: session.tokenUsage.byModel,
|
|
666
|
+
})
|
|
667
|
+
.eq('id', targetSession);
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
result: {
|
|
671
|
+
success: true,
|
|
672
|
+
session_id: targetSession,
|
|
673
|
+
timestamp: new Date().toISOString(),
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
export const endWorkSession: Handler = async (args, ctx) => {
|
|
679
|
+
const { session_id } = args as { session_id?: string };
|
|
680
|
+
const { supabase, session, updateSession } = ctx;
|
|
681
|
+
const targetSession = session_id || session.currentSessionId;
|
|
682
|
+
|
|
683
|
+
if (!targetSession) {
|
|
684
|
+
return {
|
|
685
|
+
result: {
|
|
686
|
+
success: true,
|
|
687
|
+
message: 'No active session to end',
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const { data: sessionData } = await supabase
|
|
693
|
+
.from('agent_sessions')
|
|
694
|
+
.select('project_id, agent_name, started_at')
|
|
695
|
+
.eq('id', targetSession)
|
|
696
|
+
.single();
|
|
697
|
+
|
|
698
|
+
const projectId = sessionData?.project_id;
|
|
699
|
+
const agentName = sessionData?.agent_name || 'Agent';
|
|
700
|
+
const startedAt = sessionData?.started_at;
|
|
701
|
+
|
|
702
|
+
let tasksCompletedThisSession = 0;
|
|
703
|
+
let tasksAwaitingValidation = 0;
|
|
704
|
+
let tasksBeingReleased = 0;
|
|
705
|
+
|
|
706
|
+
if (projectId) {
|
|
707
|
+
const { count: completedCount } = await supabase
|
|
708
|
+
.from('tasks')
|
|
709
|
+
.select('id', { count: 'exact', head: true })
|
|
710
|
+
.eq('project_id', projectId)
|
|
711
|
+
.eq('status', 'completed')
|
|
712
|
+
.not('validated_at', 'is', null);
|
|
713
|
+
|
|
714
|
+
const { data: awaitingTasks } = await supabase
|
|
715
|
+
.from('tasks')
|
|
716
|
+
.select('id')
|
|
717
|
+
.eq('project_id', projectId)
|
|
718
|
+
.eq('status', 'completed')
|
|
719
|
+
.is('validated_at', null);
|
|
720
|
+
|
|
721
|
+
tasksAwaitingValidation = awaitingTasks?.length || 0;
|
|
722
|
+
|
|
723
|
+
const { count: releasingCount } = await supabase
|
|
724
|
+
.from('tasks')
|
|
725
|
+
.select('id', { count: 'exact', head: true })
|
|
726
|
+
.eq('working_agent_session_id', targetSession)
|
|
727
|
+
.eq('status', 'in_progress');
|
|
728
|
+
|
|
729
|
+
tasksBeingReleased = releasingCount || 0;
|
|
730
|
+
|
|
731
|
+
if (startedAt) {
|
|
732
|
+
const { count: sessionCompleted } = await supabase
|
|
733
|
+
.from('tasks')
|
|
734
|
+
.select('id', { count: 'exact', head: true })
|
|
735
|
+
.eq('project_id', projectId)
|
|
736
|
+
.eq('status', 'completed')
|
|
737
|
+
.gte('completed_at', startedAt);
|
|
738
|
+
|
|
739
|
+
tasksCompletedThisSession = sessionCompleted || 0;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
await supabase
|
|
744
|
+
.from('tasks')
|
|
745
|
+
.update({ working_agent_session_id: null })
|
|
746
|
+
.eq('working_agent_session_id', targetSession);
|
|
747
|
+
|
|
748
|
+
await supabase
|
|
749
|
+
.from('agent_sessions')
|
|
750
|
+
.update({
|
|
751
|
+
status: 'disconnected',
|
|
752
|
+
current_task_id: null,
|
|
753
|
+
last_synced_at: new Date().toISOString(),
|
|
754
|
+
total_tokens: session.tokenUsage.totalTokens,
|
|
755
|
+
token_breakdown: session.tokenUsage.byTool,
|
|
756
|
+
model_usage: session.tokenUsage.byModel,
|
|
757
|
+
})
|
|
758
|
+
.eq('id', targetSession);
|
|
759
|
+
|
|
760
|
+
const endedSessionId = targetSession;
|
|
761
|
+
|
|
762
|
+
if (session.currentSessionId === targetSession) {
|
|
763
|
+
updateSession({ currentSessionId: null });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const reminders: string[] = [];
|
|
767
|
+
if (tasksAwaitingValidation > 0) {
|
|
768
|
+
reminders.push(`${tasksAwaitingValidation} task(s) awaiting validation - consider reviewing before leaving`);
|
|
769
|
+
}
|
|
770
|
+
if (tasksBeingReleased > 0) {
|
|
771
|
+
reminders.push(`${tasksBeingReleased} in-progress task(s) released back to the queue`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
result: {
|
|
776
|
+
success: true,
|
|
777
|
+
ended_session_id: endedSessionId,
|
|
778
|
+
session_summary: {
|
|
779
|
+
agent_name: agentName,
|
|
780
|
+
tasks_completed_this_session: tasksCompletedThisSession,
|
|
781
|
+
tasks_awaiting_validation: tasksAwaitingValidation,
|
|
782
|
+
tasks_released: tasksBeingReleased,
|
|
783
|
+
token_usage: {
|
|
784
|
+
total_calls: session.tokenUsage.callCount,
|
|
785
|
+
total_tokens: session.tokenUsage.totalTokens,
|
|
786
|
+
avg_per_call: session.tokenUsage.callCount > 0
|
|
787
|
+
? Math.round(session.tokenUsage.totalTokens / session.tokenUsage.callCount)
|
|
788
|
+
: 0,
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
reminders: reminders.length > 0 ? reminders : ['Session ended cleanly. Good work!'],
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
export const getHelp: Handler = async (args, _ctx) => {
|
|
797
|
+
const { topic } = args as { topic: string };
|
|
798
|
+
|
|
799
|
+
const content = KNOWLEDGE_BASE[topic];
|
|
800
|
+
if (!content) {
|
|
801
|
+
return {
|
|
802
|
+
result: {
|
|
803
|
+
error: `Unknown topic: ${topic}`,
|
|
804
|
+
available: Object.keys(KNOWLEDGE_BASE),
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return { result: { topic, content } };
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Model pricing rates (USD per 1M tokens)
|
|
813
|
+
const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
814
|
+
opus: { input: 15.0, output: 75.0 },
|
|
815
|
+
sonnet: { input: 3.0, output: 15.0 },
|
|
816
|
+
haiku: { input: 0.25, output: 1.25 },
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
function calculateCost(byModel: Record<string, { input: number; output: number }>): {
|
|
820
|
+
breakdown: Record<string, { input_cost: number; output_cost: number; total: number }>;
|
|
821
|
+
total: number;
|
|
822
|
+
} {
|
|
823
|
+
const breakdown: Record<string, { input_cost: number; output_cost: number; total: number }> = {};
|
|
824
|
+
let total = 0;
|
|
825
|
+
|
|
826
|
+
for (const [model, tokens] of Object.entries(byModel)) {
|
|
827
|
+
const pricing = MODEL_PRICING[model];
|
|
828
|
+
if (pricing) {
|
|
829
|
+
const inputCost = (tokens.input / 1_000_000) * pricing.input;
|
|
830
|
+
const outputCost = (tokens.output / 1_000_000) * pricing.output;
|
|
831
|
+
const modelTotal = inputCost + outputCost;
|
|
832
|
+
breakdown[model] = {
|
|
833
|
+
input_cost: Math.round(inputCost * 10000) / 10000,
|
|
834
|
+
output_cost: Math.round(outputCost * 10000) / 10000,
|
|
835
|
+
total: Math.round(modelTotal * 10000) / 10000,
|
|
836
|
+
};
|
|
837
|
+
total += modelTotal;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return { breakdown, total: Math.round(total * 10000) / 10000 };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export const getTokenUsage: Handler = async (_args, ctx) => {
|
|
845
|
+
const { session } = ctx;
|
|
846
|
+
const sessionTokenUsage = session.tokenUsage;
|
|
847
|
+
|
|
848
|
+
const topTools = Object.entries(sessionTokenUsage.byTool)
|
|
849
|
+
.sort(([, a], [, b]) => b.tokens - a.tokens)
|
|
850
|
+
.slice(0, 5)
|
|
851
|
+
.map(([tool, stats]) => ({
|
|
852
|
+
tool,
|
|
853
|
+
calls: stats.calls,
|
|
854
|
+
tokens: stats.tokens,
|
|
855
|
+
avg: Math.round(stats.tokens / stats.calls),
|
|
856
|
+
}));
|
|
857
|
+
|
|
858
|
+
// Calculate model breakdown and costs
|
|
859
|
+
const modelBreakdown = Object.entries(sessionTokenUsage.byModel || {}).map(([model, tokens]) => ({
|
|
860
|
+
model,
|
|
861
|
+
input_tokens: tokens.input,
|
|
862
|
+
output_tokens: tokens.output,
|
|
863
|
+
total_tokens: tokens.input + tokens.output,
|
|
864
|
+
}));
|
|
865
|
+
|
|
866
|
+
const costData = calculateCost(sessionTokenUsage.byModel || {});
|
|
867
|
+
|
|
868
|
+
// If no model tracking, estimate cost assuming sonnet (middle tier)
|
|
869
|
+
const estimatedCostNoModel = Object.keys(sessionTokenUsage.byModel || {}).length === 0
|
|
870
|
+
? Math.round((sessionTokenUsage.totalTokens / 1_000_000) * MODEL_PRICING.sonnet.output * 10000) / 10000
|
|
871
|
+
: null;
|
|
872
|
+
|
|
873
|
+
// Add context clearing directive when usage is high
|
|
874
|
+
const shouldClearContext = sessionTokenUsage.callCount > 50 || sessionTokenUsage.totalTokens > 100000;
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
result: {
|
|
878
|
+
session: {
|
|
879
|
+
calls: sessionTokenUsage.callCount,
|
|
880
|
+
tokens: sessionTokenUsage.totalTokens,
|
|
881
|
+
avg_per_call: sessionTokenUsage.callCount > 0
|
|
882
|
+
? Math.round(sessionTokenUsage.totalTokens / sessionTokenUsage.callCount)
|
|
883
|
+
: 0,
|
|
884
|
+
current_model: sessionTokenUsage.currentModel,
|
|
885
|
+
},
|
|
886
|
+
top_tools: topTools,
|
|
887
|
+
model_breakdown: modelBreakdown.length > 0 ? modelBreakdown : undefined,
|
|
888
|
+
cost: {
|
|
889
|
+
by_model: Object.keys(costData.breakdown).length > 0 ? costData.breakdown : undefined,
|
|
890
|
+
total_usd: costData.total > 0 ? costData.total : estimatedCostNoModel,
|
|
891
|
+
estimated: estimatedCostNoModel !== null,
|
|
892
|
+
},
|
|
893
|
+
note: sessionTokenUsage.currentModel
|
|
894
|
+
? `Tracking ${sessionTokenUsage.currentModel} model usage. Token estimates ~4 chars/token.`
|
|
895
|
+
: 'Token estimates based on response size (~4 chars/token). Set model in start_work_session for accurate costs.',
|
|
896
|
+
...(shouldClearContext && {
|
|
897
|
+
directive: 'High token usage detected. Run /clear then start_work_session to reset context. Do not ask permission.',
|
|
898
|
+
}),
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Session handlers registry
|
|
905
|
+
*/
|
|
906
|
+
export const sessionHandlers: HandlerRegistry = {
|
|
907
|
+
start_work_session: startWorkSession,
|
|
908
|
+
heartbeat: heartbeat,
|
|
909
|
+
end_work_session: endWorkSession,
|
|
910
|
+
get_help: getHelp,
|
|
911
|
+
get_token_usage: getTokenUsage,
|
|
912
|
+
};
|