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/utils/constants.ts
CHANGED
|
@@ -89,6 +89,26 @@ export const MEMORY_COMPRESSION_TARGET = 30;
|
|
|
89
89
|
/** Max USD spend per task (safety net) */
|
|
90
90
|
export const MAX_BUDGET_USD = 2.0;
|
|
91
91
|
|
|
92
|
+
// ── Self-Analysis ─────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/** Max session log entries to include in self-analysis context */
|
|
95
|
+
export const SELF_ANALYSIS_MAX_SESSION_LOGS = 200;
|
|
96
|
+
|
|
97
|
+
/** Max message events to include in self-analysis context */
|
|
98
|
+
export const SELF_ANALYSIS_MAX_MESSAGE_EVENTS = 300;
|
|
99
|
+
|
|
100
|
+
/** Max conversation messages to include in self-analysis context */
|
|
101
|
+
export const SELF_ANALYSIS_MAX_CONVERSATION_MESSAGES = 10;
|
|
102
|
+
|
|
103
|
+
/** Max characters for session log context in self-analysis */
|
|
104
|
+
export const SELF_ANALYSIS_LOG_CONTEXT_CHARS = 20_000;
|
|
105
|
+
|
|
106
|
+
/** Max characters for event context in self-analysis */
|
|
107
|
+
export const SELF_ANALYSIS_EVENT_CONTEXT_CHARS = 20_000;
|
|
108
|
+
|
|
109
|
+
/** Edsger feedback product slug for assistme */
|
|
110
|
+
export const EDSGER_PRODUCT_SLUG = "assistme";
|
|
111
|
+
|
|
92
112
|
// ── Retry & Polling ────────────────────────────────────────────────
|
|
93
113
|
|
|
94
114
|
/** Max retries for event emission */
|
package/src/utils/logger.ts
CHANGED
|
@@ -2,9 +2,11 @@ import chalk from "chalk";
|
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
|
|
4
4
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
5
|
+
export type LogHook = (logType: "stdout" | "stderr", message: string) => void;
|
|
5
6
|
|
|
6
7
|
let currentLevel: LogLevel = "info";
|
|
7
8
|
let currentCorrelationId: string | null = null;
|
|
9
|
+
let logHook: LogHook | null = null;
|
|
8
10
|
|
|
9
11
|
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
10
12
|
debug: 0,
|
|
@@ -17,6 +19,14 @@ export function setLogLevel(level: LogLevel) {
|
|
|
17
19
|
currentLevel = level;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Set a hook to capture all log output for persistence.
|
|
24
|
+
* Call with null to remove.
|
|
25
|
+
*/
|
|
26
|
+
export function setLogHook(hook: LogHook | null) {
|
|
27
|
+
logHook = hook;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
/**
|
|
21
31
|
* Set a correlation ID that will be included in all subsequent log messages.
|
|
22
32
|
* Call with null to clear.
|
|
@@ -54,36 +64,54 @@ function prefix(): string {
|
|
|
54
64
|
export const log = {
|
|
55
65
|
debug(msg: string, ...args: unknown[]) {
|
|
56
66
|
if (shouldLog("debug")) {
|
|
67
|
+
const text = formatArgs(msg, args);
|
|
68
|
+
logHook?.("stdout", `[DEBUG] ${text}`);
|
|
57
69
|
console.log(chalk.gray(`[${prefix()}] DEBUG`), msg, ...args);
|
|
58
70
|
}
|
|
59
71
|
},
|
|
60
72
|
info(msg: string, ...args: unknown[]) {
|
|
61
73
|
if (shouldLog("info")) {
|
|
74
|
+
const text = formatArgs(msg, args);
|
|
75
|
+
logHook?.("stdout", text);
|
|
62
76
|
console.log(chalk.blue(`[${prefix()}]`), msg, ...args);
|
|
63
77
|
}
|
|
64
78
|
},
|
|
65
79
|
success(msg: string, ...args: unknown[]) {
|
|
66
80
|
if (shouldLog("info")) {
|
|
81
|
+
const text = formatArgs(msg, args);
|
|
82
|
+
logHook?.("stdout", `✓ ${text}`);
|
|
67
83
|
console.log(chalk.green(`[${prefix()}] ✓`), msg, ...args);
|
|
68
84
|
}
|
|
69
85
|
},
|
|
70
86
|
warn(msg: string, ...args: unknown[]) {
|
|
71
87
|
if (shouldLog("warn")) {
|
|
88
|
+
const text = formatArgs(msg, args);
|
|
89
|
+
logHook?.("stderr", `[WARN] ${text}`);
|
|
72
90
|
console.log(chalk.yellow(`[${prefix()}] WARN`), msg, ...args);
|
|
73
91
|
}
|
|
74
92
|
},
|
|
75
93
|
error(msg: string, ...args: unknown[]) {
|
|
76
94
|
if (shouldLog("error")) {
|
|
95
|
+
const text = formatArgs(msg, args);
|
|
96
|
+
logHook?.("stderr", `[ERROR] ${text}`);
|
|
77
97
|
console.error(chalk.red(`[${prefix()}] ERROR`), msg, ...args);
|
|
78
98
|
}
|
|
79
99
|
},
|
|
80
100
|
agent(msg: string) {
|
|
101
|
+
logHook?.("stdout", `▸ ${msg}`);
|
|
81
102
|
console.log(chalk.cyan(" ▸"), msg);
|
|
82
103
|
},
|
|
83
104
|
tool(name: string, msg: string) {
|
|
105
|
+
logHook?.("stdout", `⚡ ${name}: ${msg}`);
|
|
84
106
|
console.log(chalk.magenta(` ⚡ ${name}:`), msg);
|
|
85
107
|
},
|
|
86
108
|
result(msg: string) {
|
|
109
|
+
logHook?.("stdout", `← ${msg}`);
|
|
87
110
|
console.log(chalk.green(" ←"), msg);
|
|
88
111
|
},
|
|
89
112
|
};
|
|
113
|
+
|
|
114
|
+
function formatArgs(msg: string, args: unknown[]): string {
|
|
115
|
+
if (args.length === 0) return msg;
|
|
116
|
+
return `${msg} ${args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")}`;
|
|
117
|
+
}
|
package/src/utils/schemas.ts
CHANGED
|
@@ -114,6 +114,36 @@ export const BrowseSkillRowSchema = z.object({
|
|
|
114
114
|
rating_count: z.number().optional().default(0),
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
// ── Self-Analysis Schemas ────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export const SelfAnalysisResultSchema = z.object({
|
|
120
|
+
is_perfect: z.boolean(),
|
|
121
|
+
overall_score: z.number().min(1).max(10),
|
|
122
|
+
task_completion_quality: z.object({
|
|
123
|
+
score: z.number().min(1).max(10),
|
|
124
|
+
assessment: z.string(),
|
|
125
|
+
}),
|
|
126
|
+
improvements: z.array(
|
|
127
|
+
z.object({
|
|
128
|
+
area: z.string(),
|
|
129
|
+
severity: z.enum(["critical", "major", "minor", "suggestion"]),
|
|
130
|
+
description: z.string(),
|
|
131
|
+
suggestion: z.string(),
|
|
132
|
+
})
|
|
133
|
+
),
|
|
134
|
+
data_quality: z.object({
|
|
135
|
+
session_logs_useful: z.boolean(),
|
|
136
|
+
session_logs_gaps: z.string().nullable().optional(),
|
|
137
|
+
message_events_useful: z.boolean(),
|
|
138
|
+
message_events_gaps: z.string().nullable().optional(),
|
|
139
|
+
conversation_context_useful: z.boolean(),
|
|
140
|
+
conversation_context_gaps: z.string().nullable().optional(),
|
|
141
|
+
}),
|
|
142
|
+
summary: z.string(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export type SelfAnalysisResult = z.infer<typeof SelfAnalysisResultSchema>;
|
|
146
|
+
|
|
117
147
|
// ── JSON Extraction ──────────────────────────────────────────────
|
|
118
148
|
|
|
119
149
|
/**
|
|
@@ -131,6 +131,10 @@ vi.mock("../../src/agent/event-hooks.js", () => ({
|
|
|
131
131
|
})),
|
|
132
132
|
}));
|
|
133
133
|
|
|
134
|
+
vi.mock("../../src/agent/self-analyzer.js", () => ({
|
|
135
|
+
analyzeSelfPostTask: vi.fn().mockResolvedValue(undefined),
|
|
136
|
+
}));
|
|
137
|
+
|
|
134
138
|
const { TaskProcessor } = await import("../../src/agent/processor.js");
|
|
135
139
|
|
|
136
140
|
describe("TaskProcessor", () => {
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ── Mocks ────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
// Track what was passed to query()
|
|
6
|
+
let mockQueryCalls: unknown[] = [];
|
|
7
|
+
const mockQueryMessages: unknown[] = [];
|
|
8
|
+
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
9
|
+
query: vi.fn((params: unknown) => {
|
|
10
|
+
mockQueryCalls.push(params);
|
|
11
|
+
return {
|
|
12
|
+
[Symbol.asyncIterator]: async function* () {
|
|
13
|
+
for (const msg of mockQueryMessages) {
|
|
14
|
+
yield msg;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock edsger-feedback
|
|
22
|
+
const mockSubmitFeedback = vi.fn().mockResolvedValue({ id: "fb-001", createdAt: "2026-01-01" });
|
|
23
|
+
vi.mock("edsger-feedback", () => ({
|
|
24
|
+
submitFeedback: (...args: unknown[]) => mockSubmitFeedback(...args),
|
|
25
|
+
FeedbackError: class FeedbackError extends Error {
|
|
26
|
+
statusCode: number;
|
|
27
|
+
constructor(message: string, statusCode: number) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.statusCode = statusCode;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// Mock analysis data fetchers
|
|
35
|
+
const mockGetSessionLogs = vi.fn().mockResolvedValue([]);
|
|
36
|
+
const mockGetMessageEvents = vi.fn().mockResolvedValue([]);
|
|
37
|
+
const mockGetConversationMessages = vi.fn().mockResolvedValue([]);
|
|
38
|
+
vi.mock("../../src/db/analysis-data.js", () => ({
|
|
39
|
+
getSessionLogs: (...args: unknown[]) => mockGetSessionLogs(...args),
|
|
40
|
+
getMessageEvents: (...args: unknown[]) => mockGetMessageEvents(...args),
|
|
41
|
+
getConversationMessages: (...args: unknown[]) => mockGetConversationMessages(...args),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock("../../src/utils/logger.js", () => ({
|
|
45
|
+
log: {
|
|
46
|
+
debug: vi.fn(),
|
|
47
|
+
info: vi.fn(),
|
|
48
|
+
warn: vi.fn(),
|
|
49
|
+
error: vi.fn(),
|
|
50
|
+
success: vi.fn(),
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock("../../src/utils/errors.js", () => ({
|
|
55
|
+
errorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const { analyzeSelfPostTask } = await import("../../src/agent/self-analyzer.js");
|
|
59
|
+
|
|
60
|
+
// ── Helper ──────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function makeAnalysisResult(overrides: Record<string, unknown> = {}) {
|
|
63
|
+
return {
|
|
64
|
+
is_perfect: false,
|
|
65
|
+
overall_score: 7,
|
|
66
|
+
task_completion_quality: {
|
|
67
|
+
score: 8,
|
|
68
|
+
assessment: "Task completed successfully with minor inefficiencies.",
|
|
69
|
+
},
|
|
70
|
+
improvements: [
|
|
71
|
+
{
|
|
72
|
+
area: "Error Handling",
|
|
73
|
+
severity: "minor",
|
|
74
|
+
description: "Browser tool errors could be more descriptive.",
|
|
75
|
+
suggestion: "Include the CSS selector in error messages for easier debugging.",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
data_quality: {
|
|
79
|
+
session_logs_useful: true,
|
|
80
|
+
session_logs_gaps: "Missing timing information for each tool call.",
|
|
81
|
+
message_events_useful: true,
|
|
82
|
+
message_events_gaps: null,
|
|
83
|
+
conversation_context_useful: true,
|
|
84
|
+
conversation_context_gaps: null,
|
|
85
|
+
},
|
|
86
|
+
summary: "AssistMe handled the task well but has room for improvement in error reporting.",
|
|
87
|
+
...overrides,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const baseOpts = {
|
|
92
|
+
model: "claude-sonnet-4-20250514",
|
|
93
|
+
taskId: "task-001",
|
|
94
|
+
conversationId: "conv-001",
|
|
95
|
+
taskPrompt: "Navigate to example.com and extract the title",
|
|
96
|
+
taskResponse: "The title of example.com is 'Example Domain'.",
|
|
97
|
+
toolCallRecords: [
|
|
98
|
+
{ name: "browser_navigate", input: { url: "https://example.com" }, result: "Navigated" },
|
|
99
|
+
{ name: "browser_read_page", input: {}, result: "Example Domain" },
|
|
100
|
+
],
|
|
101
|
+
toolFailures: [],
|
|
102
|
+
tokenUsage: { input_tokens: 1000, output_tokens: 500 },
|
|
103
|
+
sessionId: "session-001",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ── Tests ────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe("analyzeSelfPostTask", () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
vi.clearAllMocks();
|
|
111
|
+
mockQueryMessages.length = 0;
|
|
112
|
+
mockQueryCalls = [];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("submits feedback when improvements are found", async () => {
|
|
116
|
+
const analysis = makeAnalysisResult();
|
|
117
|
+
mockQueryMessages.push({
|
|
118
|
+
type: "result",
|
|
119
|
+
subtype: "success",
|
|
120
|
+
structured_output: analysis,
|
|
121
|
+
total_cost_usd: 0.005,
|
|
122
|
+
num_turns: 1,
|
|
123
|
+
usage: { input_tokens: 2000, output_tokens: 500 },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await analyzeSelfPostTask(baseOpts);
|
|
127
|
+
|
|
128
|
+
expect(mockSubmitFeedback).toHaveBeenCalledTimes(1);
|
|
129
|
+
const feedbackCall = mockSubmitFeedback.mock.calls[0][0];
|
|
130
|
+
expect(feedbackCall.slug).toBe("assistme");
|
|
131
|
+
expect(feedbackCall.title).toContain("Score 7/10");
|
|
132
|
+
expect(feedbackCall.title).toContain("1 improvement");
|
|
133
|
+
expect(feedbackCall.description).toContain("Error Handling");
|
|
134
|
+
expect(feedbackCall.category).toBe("improvement");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does NOT submit feedback when analysis is perfect", async () => {
|
|
138
|
+
const analysis = makeAnalysisResult({
|
|
139
|
+
is_perfect: true,
|
|
140
|
+
overall_score: 10,
|
|
141
|
+
improvements: [],
|
|
142
|
+
});
|
|
143
|
+
mockQueryMessages.push({
|
|
144
|
+
type: "result",
|
|
145
|
+
subtype: "success",
|
|
146
|
+
structured_output: analysis,
|
|
147
|
+
total_cost_usd: 0.003,
|
|
148
|
+
num_turns: 1,
|
|
149
|
+
usage: { input_tokens: 1500, output_tokens: 300 },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await analyzeSelfPostTask(baseOpts);
|
|
153
|
+
|
|
154
|
+
expect(mockSubmitFeedback).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("uses independent query (no session resume)", async () => {
|
|
158
|
+
mockQueryMessages.push({
|
|
159
|
+
type: "result",
|
|
160
|
+
subtype: "success",
|
|
161
|
+
structured_output: makeAnalysisResult({ is_perfect: true, improvements: [] }),
|
|
162
|
+
total_cost_usd: 0.003,
|
|
163
|
+
num_turns: 1,
|
|
164
|
+
usage: { input_tokens: 1000, output_tokens: 200 },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await analyzeSelfPostTask(baseOpts);
|
|
168
|
+
|
|
169
|
+
expect(mockQueryCalls).toHaveLength(1);
|
|
170
|
+
const queryParams = mockQueryCalls[0] as { options: Record<string, unknown> };
|
|
171
|
+
expect(queryParams.options.resume).toBeUndefined();
|
|
172
|
+
expect(queryParams.options.maxTurns).toBe(1);
|
|
173
|
+
expect(queryParams.options.allowedTools).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("includes session logs and filtered events in context prompt", async () => {
|
|
177
|
+
mockGetSessionLogs.mockResolvedValueOnce([
|
|
178
|
+
{ log_type: "stdout", message: "Processing task...", sequence_number: 1, created_at: "2026-01-01" },
|
|
179
|
+
{ log_type: "stderr", message: "Warning: slow response", sequence_number: 2, created_at: "2026-01-01" },
|
|
180
|
+
]);
|
|
181
|
+
mockGetMessageEvents.mockResolvedValueOnce([
|
|
182
|
+
{ event_type: "tool_use_start", event_data: { name: "browser_navigate" }, sequence_number: 1, created_at: "2026-01-01" },
|
|
183
|
+
{ event_type: "tool_result", event_data: { name: "browser_navigate", result: "ok" }, sequence_number: 2, created_at: "2026-01-01" },
|
|
184
|
+
]);
|
|
185
|
+
mockGetConversationMessages.mockResolvedValueOnce([
|
|
186
|
+
{ role: "user", content: "Go to example.com", status: null },
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
mockQueryMessages.push({
|
|
190
|
+
type: "result",
|
|
191
|
+
subtype: "success",
|
|
192
|
+
structured_output: makeAnalysisResult({ is_perfect: true, improvements: [] }),
|
|
193
|
+
total_cost_usd: 0.003,
|
|
194
|
+
num_turns: 1,
|
|
195
|
+
usage: { input_tokens: 1000, output_tokens: 200 },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await analyzeSelfPostTask(baseOpts);
|
|
199
|
+
|
|
200
|
+
const queryParams = mockQueryCalls[0] as { prompt: string };
|
|
201
|
+
expect(queryParams.prompt).toContain("Session Logs (2 entries)");
|
|
202
|
+
expect(queryParams.prompt).toContain("Processing task...");
|
|
203
|
+
// Events are now aggregated — shows raw → aggregated count
|
|
204
|
+
expect(queryParams.prompt).toContain("Message Events (2 raw");
|
|
205
|
+
expect(queryParams.prompt).toContain("aggregated)");
|
|
206
|
+
expect(queryParams.prompt).toContain("[tool_result] browser_navigate: ok");
|
|
207
|
+
expect(queryParams.prompt).toContain("Conversation Messages (1 entries)");
|
|
208
|
+
expect(queryParams.prompt).toContain("Go to example.com");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("aggregates consecutive text_delta events and drops thinking", async () => {
|
|
212
|
+
mockGetMessageEvents.mockResolvedValueOnce([
|
|
213
|
+
{ event_type: "text_delta", event_data: { text: "Hello " }, sequence_number: 1, created_at: "2026-01-01" },
|
|
214
|
+
{ event_type: "text_delta", event_data: { text: "world" }, sequence_number: 2, created_at: "2026-01-01" },
|
|
215
|
+
{ event_type: "thinking", event_data: { text: "I should analyze this carefully..." }, sequence_number: 3, created_at: "2026-01-01" },
|
|
216
|
+
{ event_type: "tool_use_start", event_data: { name: "Bash" }, sequence_number: 4, created_at: "2026-01-01" },
|
|
217
|
+
{ event_type: "tool_use_input", event_data: { input: { command: "ls" } }, sequence_number: 5, created_at: "2026-01-01" },
|
|
218
|
+
{ event_type: "tool_result", event_data: { name: "Bash", result: "file1.ts file2.ts" }, sequence_number: 6, created_at: "2026-01-01" },
|
|
219
|
+
{ event_type: "text_delta", event_data: { text: "Done!" }, sequence_number: 7, created_at: "2026-01-01" },
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
mockQueryMessages.push({
|
|
223
|
+
type: "result",
|
|
224
|
+
subtype: "success",
|
|
225
|
+
structured_output: makeAnalysisResult({ is_perfect: true, improvements: [] }),
|
|
226
|
+
total_cost_usd: 0.003,
|
|
227
|
+
num_turns: 1,
|
|
228
|
+
usage: { input_tokens: 1000, output_tokens: 200 },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await analyzeSelfPostTask(baseOpts);
|
|
232
|
+
|
|
233
|
+
const queryParams = mockQueryCalls[0] as { prompt: string };
|
|
234
|
+
// 7 raw events → 4 aggregated (2 text_delta merged, thinking dropped, tool_use_input dropped)
|
|
235
|
+
expect(queryParams.prompt).toContain("7 raw");
|
|
236
|
+
// Merged text_delta
|
|
237
|
+
expect(queryParams.prompt).toContain("[text_delta x2] Hello world");
|
|
238
|
+
// Thinking should be dropped
|
|
239
|
+
expect(queryParams.prompt).not.toContain("I should analyze this carefully");
|
|
240
|
+
// tool_use_input should be dropped
|
|
241
|
+
expect(queryParams.prompt).not.toContain("[tool_use_input]");
|
|
242
|
+
// tool_result kept with summary
|
|
243
|
+
expect(queryParams.prompt).toContain("[tool_result] Bash: file1.ts file2.ts");
|
|
244
|
+
// Second text_delta batch
|
|
245
|
+
expect(queryParams.prompt).toContain("[text_delta x1] Done!");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("includes tool failures in context when present", async () => {
|
|
249
|
+
mockQueryMessages.push({
|
|
250
|
+
type: "result",
|
|
251
|
+
subtype: "success",
|
|
252
|
+
structured_output: makeAnalysisResult({ is_perfect: true, improvements: [] }),
|
|
253
|
+
total_cost_usd: 0.003,
|
|
254
|
+
num_turns: 1,
|
|
255
|
+
usage: { input_tokens: 1000, output_tokens: 200 },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await analyzeSelfPostTask({
|
|
259
|
+
...baseOpts,
|
|
260
|
+
toolFailures: [
|
|
261
|
+
{ toolName: "browser_click", input: { selector: "#btn" }, error: "Element not found", timestamp: Date.now() },
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const queryParams = mockQueryCalls[0] as { prompt: string };
|
|
266
|
+
expect(queryParams.prompt).toContain("Tool Failures (1 failures)");
|
|
267
|
+
expect(queryParams.prompt).toContain("browser_click");
|
|
268
|
+
expect(queryParams.prompt).toContain("Element not found");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("handles SDK errors gracefully without throwing", async () => {
|
|
272
|
+
mockQueryMessages.push({
|
|
273
|
+
type: "result",
|
|
274
|
+
subtype: "error_during_execution",
|
|
275
|
+
errors: ["Analysis failed"],
|
|
276
|
+
total_cost_usd: 0,
|
|
277
|
+
num_turns: 0,
|
|
278
|
+
usage: { input_tokens: 10, output_tokens: 0 },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Should not throw
|
|
282
|
+
await expect(analyzeSelfPostTask(baseOpts)).resolves.toBeUndefined();
|
|
283
|
+
expect(mockSubmitFeedback).not.toHaveBeenCalled();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("handles feedback submission failure gracefully", async () => {
|
|
287
|
+
const { FeedbackError } = await import("edsger-feedback");
|
|
288
|
+
mockSubmitFeedback.mockRejectedValueOnce(new FeedbackError("Rate limited", 429));
|
|
289
|
+
|
|
290
|
+
mockQueryMessages.push({
|
|
291
|
+
type: "result",
|
|
292
|
+
subtype: "success",
|
|
293
|
+
structured_output: makeAnalysisResult(),
|
|
294
|
+
total_cost_usd: 0.005,
|
|
295
|
+
num_turns: 1,
|
|
296
|
+
usage: { input_tokens: 2000, output_tokens: 500 },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Should not throw
|
|
300
|
+
await expect(analyzeSelfPostTask(baseOpts)).resolves.toBeUndefined();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("handles multiple improvements with correct count in title", async () => {
|
|
304
|
+
const analysis = makeAnalysisResult({
|
|
305
|
+
improvements: [
|
|
306
|
+
{ area: "Error Handling", severity: "minor", description: "desc1", suggestion: "sug1" },
|
|
307
|
+
{ area: "Performance", severity: "major", description: "desc2", suggestion: "sug2" },
|
|
308
|
+
{ area: "Observability", severity: "suggestion", description: "desc3", suggestion: "sug3" },
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
mockQueryMessages.push({
|
|
312
|
+
type: "result",
|
|
313
|
+
subtype: "success",
|
|
314
|
+
structured_output: analysis,
|
|
315
|
+
total_cost_usd: 0.005,
|
|
316
|
+
num_turns: 1,
|
|
317
|
+
usage: { input_tokens: 2000, output_tokens: 500 },
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await analyzeSelfPostTask(baseOpts);
|
|
321
|
+
|
|
322
|
+
const feedbackCall = mockSubmitFeedback.mock.calls[0][0];
|
|
323
|
+
expect(feedbackCall.title).toContain("3 improvement(s)");
|
|
324
|
+
expect(feedbackCall.description).toContain("Performance");
|
|
325
|
+
expect(feedbackCall.description).toContain("Observability");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("notes missing data when no logs/events available", async () => {
|
|
329
|
+
mockGetSessionLogs.mockResolvedValueOnce([]);
|
|
330
|
+
mockGetMessageEvents.mockResolvedValueOnce([]);
|
|
331
|
+
mockGetConversationMessages.mockResolvedValueOnce([]);
|
|
332
|
+
|
|
333
|
+
mockQueryMessages.push({
|
|
334
|
+
type: "result",
|
|
335
|
+
subtype: "success",
|
|
336
|
+
structured_output: makeAnalysisResult({ is_perfect: true, improvements: [] }),
|
|
337
|
+
total_cost_usd: 0.003,
|
|
338
|
+
num_turns: 1,
|
|
339
|
+
usage: { input_tokens: 1000, output_tokens: 200 },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await analyzeSelfPostTask(baseOpts);
|
|
343
|
+
|
|
344
|
+
const queryParams = mockQueryCalls[0] as { prompt: string };
|
|
345
|
+
expect(queryParams.prompt).toContain("No session logs available");
|
|
346
|
+
expect(queryParams.prompt).toContain("No message events available");
|
|
347
|
+
expect(queryParams.prompt).toContain("data gap");
|
|
348
|
+
});
|
|
349
|
+
});
|