assistme 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4SBIN27G.js → chunk-ECEOBNDM.js} +60 -0
- package/dist/index.js +491 -22
- package/dist/{job-runner-CJ7HM4GZ.js → job-runner-RGP4CLYV.js} +1 -1
- package/package.json +2 -1
- package/src/agent/processor.ts +24 -4
- package/src/agent/self-analyzer.ts +444 -0
- package/src/commands/start.ts +15 -1
- package/src/db/analysis-data.ts +79 -0
- package/src/db/session-log.ts +71 -0
- package/src/utils/constants.ts +20 -0
- package/src/utils/logger.ts +28 -0
- package/src/utils/schemas.ts +30 -0
- package/tests/agent/processor.test.ts +4 -0
- package/tests/agent/self-analyzer.test.ts +349 -0
package/src/agent/processor.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { MemoryManager } from "./memory.js";
|
|
|
22
22
|
import { SkillManager } from "./skills.js";
|
|
23
23
|
import { type ToolCallRecord } from "./skill-extractor.js";
|
|
24
24
|
import { evaluateAndMaybeCreateSkill } from "./skill-evaluator.js";
|
|
25
|
+
import { analyzeSelfPostTask } from "./self-analyzer.js";
|
|
25
26
|
import { withRetry } from "../utils/retry.js";
|
|
26
27
|
import {
|
|
27
28
|
createBrowserMcpServer,
|
|
@@ -355,11 +356,30 @@ export class TaskProcessor {
|
|
|
355
356
|
);
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
// Post-task:
|
|
359
|
+
// Post-task: skill evaluation → self-analysis (serial to avoid session resume conflicts)
|
|
359
360
|
if (agentSessionId) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
(async () => {
|
|
362
|
+
try {
|
|
363
|
+
await this.evaluateSkillPostTask(agentSessionId, config.model);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
log.debug(`Post-task skill evaluation skipped: ${err}`);
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
await analyzeSelfPostTask({
|
|
369
|
+
model: config.model,
|
|
370
|
+
taskId: task.id,
|
|
371
|
+
conversationId: task.conversation_id,
|
|
372
|
+
taskPrompt: task.prompt,
|
|
373
|
+
taskResponse: finalResponse,
|
|
374
|
+
toolCallRecords,
|
|
375
|
+
toolFailures,
|
|
376
|
+
tokenUsage,
|
|
377
|
+
sessionId: this.sessionId || "",
|
|
378
|
+
});
|
|
379
|
+
} catch (err) {
|
|
380
|
+
log.debug(`Post-task self-analysis skipped: ${err}`);
|
|
381
|
+
}
|
|
382
|
+
})().catch(() => {});
|
|
363
383
|
}
|
|
364
384
|
} catch (err) {
|
|
365
385
|
const errMsg = errorMessage(err);
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import {
|
|
2
|
+
query,
|
|
3
|
+
type SDKResultMessage,
|
|
4
|
+
type SDKResultSuccess,
|
|
5
|
+
type OutputFormat,
|
|
6
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
7
|
+
import { submitFeedback, FeedbackError } from "edsger-feedback";
|
|
8
|
+
import { log } from "../utils/logger.js";
|
|
9
|
+
import {
|
|
10
|
+
SelfAnalysisResultSchema,
|
|
11
|
+
type SelfAnalysisResult,
|
|
12
|
+
safeParse,
|
|
13
|
+
} from "../utils/schemas.js";
|
|
14
|
+
import { errorMessage } from "../utils/errors.js";
|
|
15
|
+
import {
|
|
16
|
+
getSessionLogs,
|
|
17
|
+
getMessageEvents,
|
|
18
|
+
getConversationMessages,
|
|
19
|
+
} from "../db/analysis-data.js";
|
|
20
|
+
import {
|
|
21
|
+
SELF_ANALYSIS_MAX_SESSION_LOGS,
|
|
22
|
+
SELF_ANALYSIS_MAX_MESSAGE_EVENTS,
|
|
23
|
+
SELF_ANALYSIS_MAX_CONVERSATION_MESSAGES,
|
|
24
|
+
SELF_ANALYSIS_LOG_CONTEXT_CHARS,
|
|
25
|
+
SELF_ANALYSIS_EVENT_CONTEXT_CHARS,
|
|
26
|
+
EDSGER_PRODUCT_SLUG,
|
|
27
|
+
} from "../utils/constants.js";
|
|
28
|
+
import type { ToolCallRecord } from "./skill-extractor.js";
|
|
29
|
+
import type { ToolFailureRecord } from "./event-hooks.js";
|
|
30
|
+
|
|
31
|
+
// ── Structured Output Schema ────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const SELF_ANALYSIS_OUTPUT_FORMAT: OutputFormat = {
|
|
34
|
+
type: "json_schema",
|
|
35
|
+
schema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
is_perfect: { type: "boolean" },
|
|
39
|
+
overall_score: { type: "number" },
|
|
40
|
+
task_completion_quality: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
score: { type: "number" },
|
|
44
|
+
assessment: { type: "string" },
|
|
45
|
+
},
|
|
46
|
+
required: ["score", "assessment"],
|
|
47
|
+
},
|
|
48
|
+
improvements: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
area: { type: "string" },
|
|
54
|
+
severity: {
|
|
55
|
+
type: "string",
|
|
56
|
+
enum: ["critical", "major", "minor", "suggestion"],
|
|
57
|
+
},
|
|
58
|
+
description: { type: "string" },
|
|
59
|
+
suggestion: { type: "string" },
|
|
60
|
+
},
|
|
61
|
+
required: ["area", "severity", "description", "suggestion"],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
data_quality: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
session_logs_useful: { type: "boolean" },
|
|
68
|
+
session_logs_gaps: { type: "string" },
|
|
69
|
+
message_events_useful: { type: "boolean" },
|
|
70
|
+
message_events_gaps: { type: "string" },
|
|
71
|
+
conversation_context_useful: { type: "boolean" },
|
|
72
|
+
conversation_context_gaps: { type: "string" },
|
|
73
|
+
},
|
|
74
|
+
required: [
|
|
75
|
+
"session_logs_useful",
|
|
76
|
+
"message_events_useful",
|
|
77
|
+
"conversation_context_useful",
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
summary: { type: "string" },
|
|
81
|
+
},
|
|
82
|
+
required: [
|
|
83
|
+
"is_perfect",
|
|
84
|
+
"overall_score",
|
|
85
|
+
"task_completion_quality",
|
|
86
|
+
"improvements",
|
|
87
|
+
"data_quality",
|
|
88
|
+
"summary",
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ── Analysis Prompt ─────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
const SELF_ANALYSIS_PROMPT = `You just completed a task as the AssistMe agent. Now critically analyze AssistMe's own implementation — NOT the user's task itself, but how well AssistMe (the agent system) performed and whether AssistMe's codebase can be improved.
|
|
96
|
+
|
|
97
|
+
## Your Role
|
|
98
|
+
You are a perfectionist code reviewer analyzing the AssistMe agent system itself. Be critical, thorough, and constructive. Focus on:
|
|
99
|
+
|
|
100
|
+
1. **Task Completion Quality**: Did AssistMe handle the task optimally? Were there unnecessary steps, missed edge cases, or suboptimal tool usage?
|
|
101
|
+
2. **Agent Architecture**: Based on what you observed during execution, are there architectural improvements to AssistMe's code (processor, event hooks, MCP servers, skill system, etc.)?
|
|
102
|
+
3. **Data & Observability**: Evaluate the quality of the context data provided (session logs, message events, conversation messages). What information is missing that would help diagnose issues or improve the system?
|
|
103
|
+
4. **Error Handling & Resilience**: Were there any failures or retries? Could the error handling be improved?
|
|
104
|
+
5. **Performance & Efficiency**: Were there unnecessary API calls, redundant operations, or opportunities to optimize?
|
|
105
|
+
6. **User Experience**: Could the interaction flow be smoother? Is the feedback to the user adequate?
|
|
106
|
+
|
|
107
|
+
## Context Data Provided
|
|
108
|
+
Below you will find:
|
|
109
|
+
- **Session Logs**: stdout/stderr/status logs from the agent session
|
|
110
|
+
- **Message Events**: Real-time events emitted during task execution (tool calls, results, status changes, thinking blocks)
|
|
111
|
+
- **Conversation Messages**: User interaction history for this conversation
|
|
112
|
+
- **Tool Call Records**: Summary of all tool calls made during the task
|
|
113
|
+
- **Tool Failures**: Any tool calls that failed during execution
|
|
114
|
+
|
|
115
|
+
## Instructions
|
|
116
|
+
Analyze all provided data critically. Consider:
|
|
117
|
+
- Are the session logs capturing enough detail for debugging?
|
|
118
|
+
- Do the message events provide sufficient visibility into the agent's decision-making?
|
|
119
|
+
- Is the conversation context giving enough user intent signal?
|
|
120
|
+
- Were tools used efficiently?
|
|
121
|
+
- Could the overall execution flow be improved?
|
|
122
|
+
|
|
123
|
+
Set is_perfect to true ONLY if there are genuinely zero improvements to suggest (this should be rare).
|
|
124
|
+
The overall_score should be 1-10 where 10 means absolutely perfect.
|
|
125
|
+
|
|
126
|
+
Respond with a JSON object now.`;
|
|
127
|
+
|
|
128
|
+
// ── Context Building ────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
interface SelfAnalysisContext {
|
|
131
|
+
taskId: string;
|
|
132
|
+
conversationId: string;
|
|
133
|
+
sessionId: string;
|
|
134
|
+
taskPrompt: string;
|
|
135
|
+
taskResponse: string;
|
|
136
|
+
toolCallRecords: ToolCallRecord[];
|
|
137
|
+
toolFailures: ToolFailureRecord[];
|
|
138
|
+
tokenUsage?: Record<string, number>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function truncateToChars(text: string, maxChars: number): string {
|
|
142
|
+
if (text.length <= maxChars) return text;
|
|
143
|
+
return text.slice(0, maxChars) + "\n... [truncated]";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Max characters for tool result in event summary */
|
|
147
|
+
const EVENT_RESULT_SUMMARY_CHARS = 150;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Aggregate and filter events intelligently:
|
|
151
|
+
* - Merge consecutive text_delta events into a single entry
|
|
152
|
+
* - Summarize tool_result data (keep name + truncated result)
|
|
153
|
+
* - Keep tool_failure, status_change, error events in full
|
|
154
|
+
* - Drop thinking events (verbose, low signal for analysis)
|
|
155
|
+
*/
|
|
156
|
+
function filterAndAggregateEvents(
|
|
157
|
+
events: Array<{ event_type: string; event_data: Record<string, unknown>; sequence_number: number; created_at: string }>
|
|
158
|
+
): string[] {
|
|
159
|
+
const lines: string[] = [];
|
|
160
|
+
let pendingTextChunks: string[] = [];
|
|
161
|
+
|
|
162
|
+
const flushTextDelta = () => {
|
|
163
|
+
if (pendingTextChunks.length > 0) {
|
|
164
|
+
const merged = pendingTextChunks.join("");
|
|
165
|
+
const summary = merged.length > 200
|
|
166
|
+
? merged.slice(0, 200) + "..."
|
|
167
|
+
: merged;
|
|
168
|
+
lines.push(`[text_delta x${pendingTextChunks.length}] ${summary}`);
|
|
169
|
+
pendingTextChunks = [];
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
for (const e of events) {
|
|
174
|
+
switch (e.event_type) {
|
|
175
|
+
case "text_delta": {
|
|
176
|
+
const text = (e.event_data.text as string) || "";
|
|
177
|
+
pendingTextChunks.push(text);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case "thinking":
|
|
182
|
+
// Skip thinking events — verbose and low signal for self-analysis
|
|
183
|
+
flushTextDelta();
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case "tool_use_input":
|
|
187
|
+
// Skip raw input — tool_use_start + tool_result cover the essentials
|
|
188
|
+
flushTextDelta();
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case "tool_result": {
|
|
192
|
+
flushTextDelta();
|
|
193
|
+
const name = (e.event_data.name as string) || "unknown";
|
|
194
|
+
const result = ((e.event_data.result as string) || "").slice(0, EVENT_RESULT_SUMMARY_CHARS);
|
|
195
|
+
lines.push(`[tool_result] ${name}: ${result}`);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "tool_failure":
|
|
200
|
+
case "status_change":
|
|
201
|
+
case "error": {
|
|
202
|
+
flushTextDelta();
|
|
203
|
+
lines.push(`[${e.event_type}] ${JSON.stringify(e.event_data)}`);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
default: {
|
|
208
|
+
flushTextDelta();
|
|
209
|
+
const dataStr = JSON.stringify(e.event_data);
|
|
210
|
+
lines.push(`[${e.event_type}] ${dataStr.slice(0, 200)}`);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
flushTextDelta();
|
|
217
|
+
return lines;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function buildAnalysisContext(ctx: SelfAnalysisContext): Promise<string> {
|
|
221
|
+
// Fetch all data sources in parallel
|
|
222
|
+
const [sessionLogs, messageEvents, conversationMessages] = await Promise.all([
|
|
223
|
+
getSessionLogs(ctx.sessionId, SELF_ANALYSIS_MAX_SESSION_LOGS),
|
|
224
|
+
getMessageEvents(ctx.taskId, SELF_ANALYSIS_MAX_MESSAGE_EVENTS),
|
|
225
|
+
getConversationMessages(ctx.conversationId, SELF_ANALYSIS_MAX_CONVERSATION_MESSAGES),
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
let context = "";
|
|
229
|
+
|
|
230
|
+
// Session Logs
|
|
231
|
+
if (sessionLogs.length > 0) {
|
|
232
|
+
const logText = sessionLogs
|
|
233
|
+
.map((l) => `[${l.log_type}] ${l.message}`)
|
|
234
|
+
.join("\n");
|
|
235
|
+
context += `\n## Session Logs (${sessionLogs.length} entries)\n`;
|
|
236
|
+
context += truncateToChars(logText, SELF_ANALYSIS_LOG_CONTEXT_CHARS);
|
|
237
|
+
} else {
|
|
238
|
+
context += "\n## Session Logs\n(No session logs available — this is itself a data gap to note)\n";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Message Events (filtered & aggregated)
|
|
242
|
+
if (messageEvents.length > 0) {
|
|
243
|
+
const filteredLines = filterAndAggregateEvents(messageEvents);
|
|
244
|
+
const eventText = filteredLines.join("\n");
|
|
245
|
+
context += `\n## Message Events (${messageEvents.length} raw → ${filteredLines.length} aggregated)\n`;
|
|
246
|
+
context += truncateToChars(eventText, SELF_ANALYSIS_EVENT_CONTEXT_CHARS);
|
|
247
|
+
} else {
|
|
248
|
+
context += "\n## Message Events\n(No message events available — this is itself a data gap to note)\n";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Conversation Messages
|
|
252
|
+
if (conversationMessages.length > 0) {
|
|
253
|
+
context += `\n## Conversation Messages (${conversationMessages.length} entries)\n`;
|
|
254
|
+
for (const msg of conversationMessages) {
|
|
255
|
+
const role = (msg.role as string) || "unknown";
|
|
256
|
+
const content = ((msg.content as string) || "").slice(0, 500);
|
|
257
|
+
const status = (msg.status as string) || "";
|
|
258
|
+
context += `[${role}${status ? ` (${status})` : ""}] ${content}\n`;
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
context += "\n## Conversation Messages\n(No conversation messages available)\n";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Tool Call Records
|
|
265
|
+
if (ctx.toolCallRecords.length > 0) {
|
|
266
|
+
context += `\n## Tool Call Records (${ctx.toolCallRecords.length} calls)\n`;
|
|
267
|
+
for (const tc of ctx.toolCallRecords) {
|
|
268
|
+
const inputStr = JSON.stringify(tc.input).slice(0, 200);
|
|
269
|
+
context += `- ${tc.name}: ${inputStr} → ${tc.result.slice(0, 100)}\n`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Tool Failures
|
|
274
|
+
if (ctx.toolFailures.length > 0) {
|
|
275
|
+
context += `\n## Tool Failures (${ctx.toolFailures.length} failures)\n`;
|
|
276
|
+
for (const tf of ctx.toolFailures) {
|
|
277
|
+
context += `- ${tf.toolName}: ${tf.error}\n`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Token Usage
|
|
282
|
+
if (ctx.tokenUsage) {
|
|
283
|
+
context += `\n## Token Usage\n`;
|
|
284
|
+
context += `Input: ${ctx.tokenUsage.input_tokens}, Output: ${ctx.tokenUsage.output_tokens}\n`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Task Summary
|
|
288
|
+
context += `\n## Task\n`;
|
|
289
|
+
context += `Prompt: ${ctx.taskPrompt.slice(0, 500)}\n`;
|
|
290
|
+
context += `Response length: ${ctx.taskResponse.length} chars\n`;
|
|
291
|
+
|
|
292
|
+
return context;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Feedback Submission ─────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
async function submitSelfAnalysisFeedback(analysis: SelfAnalysisResult): Promise<void> {
|
|
298
|
+
const title = `Self-Analysis: Score ${analysis.overall_score}/10 — ${analysis.improvements.length} improvement(s)`;
|
|
299
|
+
|
|
300
|
+
const improvementDetails = analysis.improvements
|
|
301
|
+
.map((imp, i) => `${i + 1}. [${imp.severity}] **${imp.area}**: ${imp.description}\n → ${imp.suggestion}`)
|
|
302
|
+
.join("\n");
|
|
303
|
+
|
|
304
|
+
const dataQualityNotes = [
|
|
305
|
+
analysis.data_quality.session_logs_gaps
|
|
306
|
+
? `Session logs: ${analysis.data_quality.session_logs_gaps}`
|
|
307
|
+
: null,
|
|
308
|
+
analysis.data_quality.message_events_gaps
|
|
309
|
+
? `Message events: ${analysis.data_quality.message_events_gaps}`
|
|
310
|
+
: null,
|
|
311
|
+
analysis.data_quality.conversation_context_gaps
|
|
312
|
+
? `Conversation context: ${analysis.data_quality.conversation_context_gaps}`
|
|
313
|
+
: null,
|
|
314
|
+
]
|
|
315
|
+
.filter(Boolean)
|
|
316
|
+
.join("\n");
|
|
317
|
+
|
|
318
|
+
let description = `## Summary\n${analysis.summary}\n\n`;
|
|
319
|
+
description += `## Task Completion Quality (${analysis.task_completion_quality.score}/10)\n${analysis.task_completion_quality.assessment}\n\n`;
|
|
320
|
+
description += `## Improvements\n${improvementDetails}\n`;
|
|
321
|
+
|
|
322
|
+
if (dataQualityNotes) {
|
|
323
|
+
description += `\n## Data Quality Gaps\n${dataQualityNotes}\n`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Truncate to fit edsger-feedback's 5000 char limit
|
|
327
|
+
if (description.length > 4900) {
|
|
328
|
+
description = description.slice(0, 4900) + "\n...[truncated]";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const result = await submitFeedback({
|
|
333
|
+
slug: EDSGER_PRODUCT_SLUG,
|
|
334
|
+
title: title.slice(0, 200),
|
|
335
|
+
description,
|
|
336
|
+
category: "improvement",
|
|
337
|
+
});
|
|
338
|
+
log.info(`Self-analysis feedback submitted: ${result.id}`);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (err instanceof FeedbackError) {
|
|
341
|
+
log.debug(`Feedback submission failed (${err.statusCode}): ${err.message}`);
|
|
342
|
+
} else {
|
|
343
|
+
log.debug(`Feedback submission failed: ${errorMessage(err)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Main Entry Point ────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Post-task self-analysis: resume the agent SDK session to critically
|
|
352
|
+
* analyze AssistMe's own performance and code quality.
|
|
353
|
+
*
|
|
354
|
+
* If improvements are found, submits feedback via edsger-feedback.
|
|
355
|
+
* If the analysis determines everything is perfect, no feedback is sent.
|
|
356
|
+
*/
|
|
357
|
+
export async function analyzeSelfPostTask(opts: {
|
|
358
|
+
model: string;
|
|
359
|
+
taskId: string;
|
|
360
|
+
conversationId: string;
|
|
361
|
+
taskPrompt: string;
|
|
362
|
+
taskResponse: string;
|
|
363
|
+
toolCallRecords: ToolCallRecord[];
|
|
364
|
+
toolFailures: ToolFailureRecord[];
|
|
365
|
+
tokenUsage?: Record<string, number>;
|
|
366
|
+
sessionId: string;
|
|
367
|
+
}): Promise<void> {
|
|
368
|
+
const {
|
|
369
|
+
model,
|
|
370
|
+
taskId,
|
|
371
|
+
conversationId,
|
|
372
|
+
taskPrompt,
|
|
373
|
+
taskResponse,
|
|
374
|
+
toolCallRecords,
|
|
375
|
+
toolFailures,
|
|
376
|
+
tokenUsage,
|
|
377
|
+
sessionId,
|
|
378
|
+
} = opts;
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// Build context from all available data sources
|
|
382
|
+
const analysisContext = await buildAnalysisContext({
|
|
383
|
+
taskId,
|
|
384
|
+
conversationId,
|
|
385
|
+
sessionId,
|
|
386
|
+
taskPrompt,
|
|
387
|
+
taskResponse,
|
|
388
|
+
toolCallRecords,
|
|
389
|
+
toolFailures,
|
|
390
|
+
tokenUsage,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const prompt = `${SELF_ANALYSIS_PROMPT}\n${analysisContext}\n\nRespond with a JSON object now.`;
|
|
394
|
+
|
|
395
|
+
let structuredOutput: unknown;
|
|
396
|
+
|
|
397
|
+
// Use independent query() instead of session resume to avoid
|
|
398
|
+
// conflicts with skill evaluation which also resumes the session
|
|
399
|
+
for await (const message of query({
|
|
400
|
+
prompt,
|
|
401
|
+
options: {
|
|
402
|
+
model,
|
|
403
|
+
maxTurns: 1,
|
|
404
|
+
allowedTools: [],
|
|
405
|
+
effort: "low",
|
|
406
|
+
outputFormat: SELF_ANALYSIS_OUTPUT_FORMAT,
|
|
407
|
+
},
|
|
408
|
+
})) {
|
|
409
|
+
if (message.type === "result") {
|
|
410
|
+
const resultMsg = message as SDKResultMessage;
|
|
411
|
+
if (resultMsg.subtype === "success") {
|
|
412
|
+
const successMsg = resultMsg as SDKResultSuccess;
|
|
413
|
+
structuredOutput = successMsg.structured_output;
|
|
414
|
+
log.debug(
|
|
415
|
+
`Self-analysis cost: $${successMsg.total_cost_usd.toFixed(4)}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Validate against Zod schema
|
|
422
|
+
const analysis = structuredOutput
|
|
423
|
+
? safeParse(SelfAnalysisResultSchema, structuredOutput)
|
|
424
|
+
: null;
|
|
425
|
+
|
|
426
|
+
if (!analysis) {
|
|
427
|
+
log.debug("Self-analysis: no valid structured output");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
log.info(
|
|
432
|
+
`Self-analysis complete: score=${analysis.overall_score}/10, perfect=${analysis.is_perfect}, improvements=${analysis.improvements.length}`
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Only submit feedback if not perfect
|
|
436
|
+
if (!analysis.is_perfect && analysis.improvements.length > 0) {
|
|
437
|
+
await submitSelfAnalysisFeedback(analysis);
|
|
438
|
+
} else {
|
|
439
|
+
log.debug("Self-analysis: no improvements to report — skipping feedback");
|
|
440
|
+
}
|
|
441
|
+
} catch (err) {
|
|
442
|
+
log.debug(`Self-analysis error: ${errorMessage(err)}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
package/src/commands/start.ts
CHANGED
|
@@ -4,10 +4,11 @@ import ora from "ora";
|
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
5
|
import { getCurrentUserId } from "../db/supabase.js";
|
|
6
6
|
import { setConfig } from "../utils/config.js";
|
|
7
|
-
import { log, setLogLevel } from "../utils/logger.js";
|
|
7
|
+
import { log, setLogLevel, setLogHook } from "../utils/logger.js";
|
|
8
8
|
import { SessionManager } from "../agent/session.js";
|
|
9
9
|
import { TaskProcessor } from "../agent/processor.js";
|
|
10
10
|
import { getBrowser, ensureBrowserAvailable } from "../tools/browser.js";
|
|
11
|
+
import { SessionLogEmitter } from "../db/session-log.js";
|
|
11
12
|
|
|
12
13
|
export function registerStartCommand(program: Command): void {
|
|
13
14
|
program
|
|
@@ -85,12 +86,19 @@ async function runAgent(opts: { workspace?: string; name?: string; verbose?: boo
|
|
|
85
86
|
const processor = new TaskProcessor();
|
|
86
87
|
processor.init(userId);
|
|
87
88
|
const sessionManager = new SessionManager();
|
|
89
|
+
let logEmitter: SessionLogEmitter | null = null;
|
|
88
90
|
|
|
89
91
|
// Graceful shutdown
|
|
90
92
|
const browserRef = getBrowser();
|
|
91
93
|
const shutdown = async () => {
|
|
92
94
|
console.log();
|
|
93
95
|
log.info("Shutting down...");
|
|
96
|
+
setLogHook(null);
|
|
97
|
+
try {
|
|
98
|
+
if (logEmitter) await logEmitter.stop();
|
|
99
|
+
} catch {
|
|
100
|
+
/* ignore */
|
|
101
|
+
}
|
|
94
102
|
try {
|
|
95
103
|
if (browserRef.isConnected()) await browserRef.disconnect();
|
|
96
104
|
} catch {
|
|
@@ -109,6 +117,12 @@ async function runAgent(opts: { workspace?: string; name?: string; verbose?: boo
|
|
|
109
117
|
});
|
|
110
118
|
processor.setSessionId(session.id);
|
|
111
119
|
|
|
120
|
+
// Start persisting logs to Supabase
|
|
121
|
+
logEmitter = new SessionLogEmitter(session.id);
|
|
122
|
+
setLogHook((logType, message) => {
|
|
123
|
+
logEmitter?.push(logType, message);
|
|
124
|
+
});
|
|
125
|
+
|
|
112
126
|
log.info("Listening for tasks (chat + jobs) from web UI...");
|
|
113
127
|
log.info("Press Ctrl+C to stop.\n");
|
|
114
128
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { callMcpHandler } from "./api-client.js";
|
|
2
|
+
import { log } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
export interface SessionLogEntry {
|
|
5
|
+
log_type: "stdout" | "stderr" | "status";
|
|
6
|
+
message: string;
|
|
7
|
+
sequence_number: number;
|
|
8
|
+
created_at: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MessageEventEntry {
|
|
12
|
+
event_type: string;
|
|
13
|
+
event_data: Record<string, unknown>;
|
|
14
|
+
sequence_number: number;
|
|
15
|
+
created_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetch agent session logs for a given session.
|
|
20
|
+
* Uses the log.list MCP handler endpoint.
|
|
21
|
+
*/
|
|
22
|
+
export async function getSessionLogs(
|
|
23
|
+
sessionId: string,
|
|
24
|
+
limit = 500
|
|
25
|
+
): Promise<SessionLogEntry[]> {
|
|
26
|
+
try {
|
|
27
|
+
const data = await callMcpHandler<SessionLogEntry[]>("log.list", {
|
|
28
|
+
session_id: sessionId,
|
|
29
|
+
limit,
|
|
30
|
+
});
|
|
31
|
+
return data || [];
|
|
32
|
+
} catch (err) {
|
|
33
|
+
log.debug(`Failed to fetch session logs: ${err instanceof Error ? err.message : err}`);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch message events for a given task (message_id).
|
|
40
|
+
* Uses the event.list MCP handler endpoint.
|
|
41
|
+
*/
|
|
42
|
+
export async function getMessageEvents(
|
|
43
|
+
messageId: string,
|
|
44
|
+
limit = 500
|
|
45
|
+
): Promise<MessageEventEntry[]> {
|
|
46
|
+
try {
|
|
47
|
+
const data = await callMcpHandler<MessageEventEntry[]>("event.list", {
|
|
48
|
+
message_id: messageId,
|
|
49
|
+
limit,
|
|
50
|
+
});
|
|
51
|
+
return data || [];
|
|
52
|
+
} catch (err) {
|
|
53
|
+
log.debug(`Failed to fetch message events: ${err instanceof Error ? err.message : err}`);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetch conversation messages for context.
|
|
60
|
+
* Returns the most recent messages including the current task.
|
|
61
|
+
*/
|
|
62
|
+
export async function getConversationMessages(
|
|
63
|
+
conversationId: string,
|
|
64
|
+
limit = 10
|
|
65
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
66
|
+
try {
|
|
67
|
+
const data = await callMcpHandler<Array<Record<string, unknown>>>(
|
|
68
|
+
"conversation.list_messages",
|
|
69
|
+
{
|
|
70
|
+
conversation_id: conversationId,
|
|
71
|
+
limit,
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
return data || [];
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.debug(`Failed to fetch conversation messages: ${err instanceof Error ? err.message : err}`);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { callMcpHandler } from "./api-client.js";
|
|
2
|
+
import { log } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
const FLUSH_INTERVAL_MS = 3_000;
|
|
5
|
+
const MAX_BATCH_SIZE = 100;
|
|
6
|
+
|
|
7
|
+
interface PendingLog {
|
|
8
|
+
log_type: "stdout" | "stderr" | "status";
|
|
9
|
+
message: string;
|
|
10
|
+
seq: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Batches agent process logs and flushes them to Supabase periodically.
|
|
15
|
+
* Each session gets its own emitter with an auto-incrementing sequence.
|
|
16
|
+
*/
|
|
17
|
+
export class SessionLogEmitter {
|
|
18
|
+
private sequence = 0;
|
|
19
|
+
private buffer: PendingLog[] = [];
|
|
20
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
21
|
+
private flushing = false;
|
|
22
|
+
|
|
23
|
+
constructor(private sessionId: string) {
|
|
24
|
+
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Queue a log entry for batch insertion */
|
|
28
|
+
push(logType: "stdout" | "stderr" | "status", message: string): void {
|
|
29
|
+
this.sequence++;
|
|
30
|
+
this.buffer.push({ log_type: logType, message, seq: this.sequence });
|
|
31
|
+
|
|
32
|
+
// Flush immediately if buffer is large
|
|
33
|
+
if (this.buffer.length >= MAX_BATCH_SIZE) {
|
|
34
|
+
this.flush();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Flush buffered logs to Supabase */
|
|
39
|
+
async flush(): Promise<void> {
|
|
40
|
+
if (this.flushing || this.buffer.length === 0) return;
|
|
41
|
+
|
|
42
|
+
const batch = this.buffer.splice(0);
|
|
43
|
+
this.flushing = true;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await callMcpHandler("log.emit_batch", {
|
|
47
|
+
session_id: this.sessionId,
|
|
48
|
+
logs: batch,
|
|
49
|
+
});
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.debug(
|
|
52
|
+
`Failed to flush session logs: ${err instanceof Error ? err.message : err}`
|
|
53
|
+
);
|
|
54
|
+
// Re-queue failed batch at the front (best-effort, drop if too old)
|
|
55
|
+
if (this.buffer.length < MAX_BATCH_SIZE * 5) {
|
|
56
|
+
this.buffer.unshift(...batch);
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
this.flushing = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Stop the emitter and flush remaining logs */
|
|
64
|
+
async stop(): Promise<void> {
|
|
65
|
+
if (this.flushTimer) {
|
|
66
|
+
clearInterval(this.flushTimer);
|
|
67
|
+
this.flushTimer = null;
|
|
68
|
+
}
|
|
69
|
+
await this.flush();
|
|
70
|
+
}
|
|
71
|
+
}
|