@andreprado/agentkit 0.1.0-alpha.17 → 0.1.0-alpha.19
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 +3 -0
- package/docs/guides/add-channel.md +14 -8
- package/docs/guides/add-knowledge.md +10 -0
- package/docs/guides/add-managed-composio.md +43 -17
- package/docs/guides/channel-security.md +60 -39
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/create-agent.md +13 -0
- package/docs/guides/debug-channel.md +147 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +30 -14
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +18 -0
- package/docs/guides/security-rules.md +5 -5
- package/docs/guides/use-provider.md +11 -1
- package/docs/llms-full.txt +106 -15
- package/docs/llms.txt +22 -4
- package/package.json +1 -3
- package/src/cli/args.ts +23 -2
- package/src/cli/cloud-client.ts +75 -0
- package/src/cli/commands/channels.ts +139 -14
- package/src/cli/deploy-chat-ui.ts +146 -3
- package/src/cli/deploy-readiness.ts +57 -1
- package/src/cli/help.ts +32 -8
- package/src/cli/index.ts +447 -17
- package/src/create-project.ts +13 -2
- package/src/index.ts +46 -3
- package/src/providers/pi.ts +49 -15
- package/src/runtime/channel-test-harness.ts +4 -1
- package/src/runtime/channels/discord.ts +887 -0
- package/src/runtime/channels.ts +15 -0
- package/src/runtime/config.ts +39 -3
- package/src/runtime/core/manifest.ts +2 -0
- package/src/runtime/dev-server.ts +149 -8
- package/src/runtime/evals.ts +27 -6
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +1 -0
- package/src/runtime/integrations/composio.ts +168 -2
- package/src/runtime/knowledge/retrieve.ts +25 -5
- package/src/runtime/knowledge/schema.ts +45 -1
- package/src/runtime/runtime-contract.ts +54 -0
- package/src/runtime/targets/cloudflare/build.ts +479 -194
- package/src/runtime/targets/vps/deploy.ts +1 -1
- package/src/storage/sqlite.ts +7 -2
- package/src/templates/skills/agentkit-capsule/SKILL.md +8 -1
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +1 -2
- package/src/templates/skills/agentkit-channels/SKILL.md +6 -1
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +2 -1
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +6 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +12 -3
- package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +12 -2
- package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
- package/src/templates/skills/agentkit-provider/SKILL.md +4 -1
- package/src/templates/skills/agentkit-security/SKILL.md +3 -2
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +9 -0
- package/src/templates/support.ts +4 -2
- package/docs/guides/agentkit-skills-architecture.md +0 -472
- package/docs/guides/channels-implementation-map.md +0 -243
- package/docs/guides/channels-production-handoff.md +0 -118
- package/docs/portable-deploy-release-checklist.md +0 -41
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { listConversationsFromCwd } from "./conversations";
|
|
6
|
+
import { findAgentCapsuleRoot } from "./config";
|
|
7
|
+
import { AgentKitError, isAgentKitError } from "./errors";
|
|
8
|
+
import { runAgentMessageFromCwd } from "./chat";
|
|
9
|
+
import { getConversationTraceFromCwd } from "./traces";
|
|
10
|
+
|
|
11
|
+
export type ImproveSource = "local" | "deploy";
|
|
12
|
+
|
|
13
|
+
export type ImproveTraceMessage = {
|
|
14
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
15
|
+
content: string;
|
|
16
|
+
created_at?: string;
|
|
17
|
+
sequence?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ImproveTracePacket = {
|
|
21
|
+
id: string;
|
|
22
|
+
source: ImproveSource;
|
|
23
|
+
conversation_id: string;
|
|
24
|
+
title?: string | null;
|
|
25
|
+
updated_at?: string;
|
|
26
|
+
trace: {
|
|
27
|
+
messages: ImproveTraceMessage[];
|
|
28
|
+
runs?: unknown[];
|
|
29
|
+
raw?: unknown;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ImproveBundle = {
|
|
34
|
+
schema_version: 1;
|
|
35
|
+
created_at: string;
|
|
36
|
+
source: ImproveSource;
|
|
37
|
+
since?: string;
|
|
38
|
+
deploy_id?: string;
|
|
39
|
+
evidence?: unknown;
|
|
40
|
+
traces: ImproveTracePacket[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ImproveCollectResult = {
|
|
44
|
+
root: string;
|
|
45
|
+
directory: string;
|
|
46
|
+
bundlePath: string;
|
|
47
|
+
reportPath: string;
|
|
48
|
+
traceCount: number;
|
|
49
|
+
source: ImproveSource;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ImproveEvalGenerationResult = {
|
|
53
|
+
root: string;
|
|
54
|
+
bundlePath: string;
|
|
55
|
+
files: string[];
|
|
56
|
+
skipped: Array<{ trace_id: string; reason: string }>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type ImproveReplayResult = {
|
|
60
|
+
root: string;
|
|
61
|
+
bundlePath: string;
|
|
62
|
+
passed: number;
|
|
63
|
+
failed: number;
|
|
64
|
+
skipped: number;
|
|
65
|
+
results: Array<{
|
|
66
|
+
trace_id: string;
|
|
67
|
+
conversation_id: string;
|
|
68
|
+
status: "passed" | "failed" | "skipped";
|
|
69
|
+
turns: number;
|
|
70
|
+
error?: string;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type DeployImproveCollector = {
|
|
75
|
+
exportEvidence?(input: {
|
|
76
|
+
deployId: string;
|
|
77
|
+
since?: string;
|
|
78
|
+
conversationId?: string;
|
|
79
|
+
}): Promise<unknown | null>;
|
|
80
|
+
listConversations(): Promise<unknown>;
|
|
81
|
+
getTrace(conversationId: string): Promise<unknown>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export async function collectLocalImproveBundle(
|
|
85
|
+
cwd: string,
|
|
86
|
+
options: {
|
|
87
|
+
since?: string;
|
|
88
|
+
conversationId?: string;
|
|
89
|
+
out?: string;
|
|
90
|
+
} = {},
|
|
91
|
+
): Promise<ImproveCollectResult> {
|
|
92
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
93
|
+
const sinceDate = resolveSinceDate(options.since);
|
|
94
|
+
const conversations = options.conversationId
|
|
95
|
+
? [{ id: options.conversationId }]
|
|
96
|
+
: (await listConversationsFromCwd(root)).filter((conversation) =>
|
|
97
|
+
sinceDate ? dateIsOnOrAfter(conversation.updatedAt, sinceDate) : true,
|
|
98
|
+
);
|
|
99
|
+
const traces: ImproveTracePacket[] = [];
|
|
100
|
+
|
|
101
|
+
for (const conversation of conversations) {
|
|
102
|
+
const trace = await getConversationTraceFromCwd(root, conversation.id);
|
|
103
|
+
traces.push(tracePacketFromPayload("local", trace.id, trace));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return writeImproveBundle(root, {
|
|
107
|
+
schema_version: 1,
|
|
108
|
+
created_at: new Date().toISOString(),
|
|
109
|
+
source: "local",
|
|
110
|
+
...(options.since ? { since: options.since } : {}),
|
|
111
|
+
traces,
|
|
112
|
+
}, options.out);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function collectDeployImproveBundle(
|
|
116
|
+
cwd: string,
|
|
117
|
+
options: {
|
|
118
|
+
collector: DeployImproveCollector;
|
|
119
|
+
deployId: string;
|
|
120
|
+
since?: string;
|
|
121
|
+
conversationId?: string;
|
|
122
|
+
out?: string;
|
|
123
|
+
},
|
|
124
|
+
): Promise<ImproveCollectResult> {
|
|
125
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
126
|
+
|
|
127
|
+
if (options.collector.exportEvidence) {
|
|
128
|
+
const exported = await options.collector.exportEvidence({
|
|
129
|
+
deployId: options.deployId,
|
|
130
|
+
...(options.since ? { since: options.since } : {}),
|
|
131
|
+
...(options.conversationId ? { conversationId: options.conversationId } : {}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (exported) {
|
|
135
|
+
const normalized = normalizeDeployEvidenceExport(exported);
|
|
136
|
+
const traces = await tracePacketsFromDeployEvidenceExport({
|
|
137
|
+
collector: options.collector,
|
|
138
|
+
deployId: options.deployId,
|
|
139
|
+
evidence: normalized,
|
|
140
|
+
source: "deploy",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return writeImproveBundle(root, {
|
|
144
|
+
schema_version: 1,
|
|
145
|
+
created_at: normalized.created_at ?? new Date().toISOString(),
|
|
146
|
+
source: "deploy",
|
|
147
|
+
deploy_id: options.deployId,
|
|
148
|
+
...(options.since ? { since: options.since } : {}),
|
|
149
|
+
evidence: redactEvidenceValue(normalized.raw),
|
|
150
|
+
traces,
|
|
151
|
+
}, options.out);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sinceDate = resolveSinceDate(options.since);
|
|
156
|
+
const summaries = options.conversationId
|
|
157
|
+
? [{ id: options.conversationId }]
|
|
158
|
+
: normalizeConversationSummaries(await options.collector.listConversations()).filter((conversation) =>
|
|
159
|
+
sinceDate && conversation.updated_at ? dateIsOnOrAfter(conversation.updated_at, sinceDate) : true,
|
|
160
|
+
);
|
|
161
|
+
const traces: ImproveTracePacket[] = [];
|
|
162
|
+
|
|
163
|
+
for (const summary of summaries) {
|
|
164
|
+
const trace = await options.collector.getTrace(summary.id);
|
|
165
|
+
traces.push(tracePacketFromPayload("deploy", summary.id, trace));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return writeImproveBundle(root, {
|
|
169
|
+
schema_version: 1,
|
|
170
|
+
created_at: new Date().toISOString(),
|
|
171
|
+
source: "deploy",
|
|
172
|
+
deploy_id: options.deployId,
|
|
173
|
+
...(options.since ? { since: options.since } : {}),
|
|
174
|
+
traces,
|
|
175
|
+
}, options.out);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function generateImproveEvals(
|
|
179
|
+
cwd: string,
|
|
180
|
+
options: {
|
|
181
|
+
bundle: string;
|
|
182
|
+
force?: boolean;
|
|
183
|
+
},
|
|
184
|
+
): Promise<ImproveEvalGenerationResult> {
|
|
185
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
186
|
+
const bundlePath = await resolveBundlePath(root, options.bundle);
|
|
187
|
+
const bundle = await readImproveBundle(bundlePath);
|
|
188
|
+
const files: string[] = [];
|
|
189
|
+
const skipped: ImproveEvalGenerationResult["skipped"] = [];
|
|
190
|
+
const usedPaths = new Set<string>();
|
|
191
|
+
|
|
192
|
+
for (const trace of bundle.traces) {
|
|
193
|
+
const turns = replayTurnsFromMessages(trace.trace.messages).map(redactEvalTurn);
|
|
194
|
+
|
|
195
|
+
if (turns.length === 0) {
|
|
196
|
+
skipped.push({ trace_id: trace.id, reason: "trace has no user turns" });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const file = resolveUniqueEvalPath(root, trace, usedPaths);
|
|
201
|
+
usedPaths.add(file);
|
|
202
|
+
const source = renderImproveEval(trace, turns);
|
|
203
|
+
|
|
204
|
+
await mkdir(dirname(file), { recursive: true });
|
|
205
|
+
try {
|
|
206
|
+
await writeFile(file, source, { flag: options.force ? "w" : "wx" });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
209
|
+
throw new AgentKitError(
|
|
210
|
+
"validation_error",
|
|
211
|
+
`${relative(root, file)} already exists. Re-run with --force after reviewing the existing eval.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
files.push(file);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
root,
|
|
223
|
+
bundlePath,
|
|
224
|
+
files,
|
|
225
|
+
skipped,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function replayImproveBundle(
|
|
230
|
+
cwd: string,
|
|
231
|
+
options: {
|
|
232
|
+
bundle: string;
|
|
233
|
+
against: "local";
|
|
234
|
+
},
|
|
235
|
+
): Promise<ImproveReplayResult> {
|
|
236
|
+
if (options.against !== "local") {
|
|
237
|
+
throw new AgentKitError("validation_error", 'agentkit replay currently supports only --against local.');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
241
|
+
const bundlePath = await resolveBundlePath(root, options.bundle);
|
|
242
|
+
const bundle = await readImproveBundle(bundlePath);
|
|
243
|
+
const results: ImproveReplayResult["results"] = [];
|
|
244
|
+
|
|
245
|
+
for (const trace of bundle.traces) {
|
|
246
|
+
const userTurns = trace.trace.messages.filter((message) => message.role === "user" && message.content.trim().length > 0);
|
|
247
|
+
|
|
248
|
+
if (userTurns.length === 0) {
|
|
249
|
+
results.push({
|
|
250
|
+
trace_id: trace.id,
|
|
251
|
+
conversation_id: trace.conversation_id,
|
|
252
|
+
status: "skipped",
|
|
253
|
+
turns: 0,
|
|
254
|
+
error: "trace has no user turns",
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const replayConversationId = `replay_${slugify(trace.conversation_id)}_${crypto.randomUUID()}`;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
for (const turn of userTurns) {
|
|
263
|
+
await runAgentMessageFromCwd(root, {
|
|
264
|
+
message: turn.content,
|
|
265
|
+
conversationId: replayConversationId,
|
|
266
|
+
runtime: {
|
|
267
|
+
environment: "eval",
|
|
268
|
+
invocation: "eval",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
results.push({
|
|
274
|
+
trace_id: trace.id,
|
|
275
|
+
conversation_id: trace.conversation_id,
|
|
276
|
+
status: "passed",
|
|
277
|
+
turns: userTurns.length,
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
results.push({
|
|
281
|
+
trace_id: trace.id,
|
|
282
|
+
conversation_id: trace.conversation_id,
|
|
283
|
+
status: "failed",
|
|
284
|
+
turns: userTurns.length,
|
|
285
|
+
error: formatError(error),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
root,
|
|
292
|
+
bundlePath,
|
|
293
|
+
passed: results.filter((result) => result.status === "passed").length,
|
|
294
|
+
failed: results.filter((result) => result.status === "failed").length,
|
|
295
|
+
skipped: results.filter((result) => result.status === "skipped").length,
|
|
296
|
+
results,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function writeImproveBundle(root: string, bundle: ImproveBundle, out?: string): Promise<ImproveCollectResult> {
|
|
301
|
+
const directory = resolveImproveDirectory(root, bundle.created_at, out);
|
|
302
|
+
const bundlePath = join(directory, "bundle.json");
|
|
303
|
+
const reportPath = join(directory, "report.json");
|
|
304
|
+
const seenTraceIds = new Set<string>();
|
|
305
|
+
|
|
306
|
+
await mkdir(join(directory, "traces"), { recursive: true });
|
|
307
|
+
for (const trace of bundle.traces) {
|
|
308
|
+
if (seenTraceIds.has(trace.id)) {
|
|
309
|
+
throw new AgentKitError("validation_error", `Improve trace id ${trace.id} is duplicated and would overwrite evidence.`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
seenTraceIds.add(trace.id);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await writeFile(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
316
|
+
await writeFile(
|
|
317
|
+
reportPath,
|
|
318
|
+
`${JSON.stringify(
|
|
319
|
+
{
|
|
320
|
+
created_at: bundle.created_at,
|
|
321
|
+
source: bundle.source,
|
|
322
|
+
deploy_id: bundle.deploy_id ?? null,
|
|
323
|
+
trace_count: bundle.traces.length,
|
|
324
|
+
conversations: bundle.traces.map((trace) => ({
|
|
325
|
+
trace_id: trace.id,
|
|
326
|
+
conversation_id: trace.conversation_id,
|
|
327
|
+
title: trace.title ?? null,
|
|
328
|
+
updated_at: trace.updated_at ?? null,
|
|
329
|
+
messages: trace.trace.messages.length,
|
|
330
|
+
runs: Array.isArray(trace.trace.runs) ? trace.trace.runs.length : 0,
|
|
331
|
+
})),
|
|
332
|
+
evidence: summarizeImproveEvidence(bundle.evidence),
|
|
333
|
+
},
|
|
334
|
+
null,
|
|
335
|
+
2,
|
|
336
|
+
)}\n`,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
for (const trace of bundle.traces) {
|
|
340
|
+
await writeFile(join(directory, "traces", `${trace.id}.json`), `${JSON.stringify(trace, null, 2)}\n`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
root,
|
|
345
|
+
directory,
|
|
346
|
+
bundlePath,
|
|
347
|
+
reportPath,
|
|
348
|
+
traceCount: bundle.traces.length,
|
|
349
|
+
source: bundle.source,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
type NormalizedDeployEvidenceExport = {
|
|
354
|
+
created_at?: string;
|
|
355
|
+
deploy_id?: string;
|
|
356
|
+
traces: unknown[];
|
|
357
|
+
conversations: Array<{ id: string; updated_at?: string }>;
|
|
358
|
+
runs: unknown[];
|
|
359
|
+
raw: unknown;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
function normalizeDeployEvidenceExport(payload: unknown): NormalizedDeployEvidenceExport {
|
|
363
|
+
const evidence = isRecord(payload) && isRecord(payload.evidence) ? payload.evidence : payload;
|
|
364
|
+
|
|
365
|
+
if (!isRecord(evidence)) {
|
|
366
|
+
throw new AgentKitError("deploy_improve_response_invalid", "Improve evidence export response was not an object.");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const traces = Array.isArray(evidence.traces) ? evidence.traces : [];
|
|
370
|
+
const conversations = Array.isArray(evidence.conversations)
|
|
371
|
+
? evidence.conversations
|
|
372
|
+
.flatMap((conversation) => {
|
|
373
|
+
if (!isRecord(conversation)) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const id = readString(conversation, "id") ?? readString(conversation, "conversation_id");
|
|
378
|
+
|
|
379
|
+
if (!id) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const updatedAt = readString(conversation, "updated_at") ?? readString(conversation, "updatedAt");
|
|
384
|
+
return {
|
|
385
|
+
id,
|
|
386
|
+
...(updatedAt ? { updated_at: updatedAt } : {}),
|
|
387
|
+
};
|
|
388
|
+
})
|
|
389
|
+
: [];
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
created_at: readString(evidence, "created_at") ?? readString(evidence, "createdAt"),
|
|
393
|
+
deploy_id: readString(evidence, "deploy_id") ?? readString(evidence, "deployId"),
|
|
394
|
+
traces,
|
|
395
|
+
conversations,
|
|
396
|
+
runs: Array.isArray(evidence.runs) ? evidence.runs : [],
|
|
397
|
+
raw: evidence,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function tracePacketsFromDeployEvidenceExport(input: {
|
|
402
|
+
collector: DeployImproveCollector;
|
|
403
|
+
deployId: string;
|
|
404
|
+
evidence: NormalizedDeployEvidenceExport;
|
|
405
|
+
source: ImproveSource;
|
|
406
|
+
}): Promise<ImproveTracePacket[]> {
|
|
407
|
+
const traces: ImproveTracePacket[] = [];
|
|
408
|
+
const seenConversations = new Set<string>();
|
|
409
|
+
|
|
410
|
+
for (const rawTrace of input.evidence.traces) {
|
|
411
|
+
const trace = tracePacketFromPayload(input.source, input.deployId, rawTrace);
|
|
412
|
+
traces.push(trace);
|
|
413
|
+
seenConversations.add(trace.conversation_id);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const conversation of input.evidence.conversations) {
|
|
417
|
+
if (seenConversations.has(conversation.id)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const trace = await input.collector.getTrace(conversation.id);
|
|
423
|
+
const packet = tracePacketFromPayload(input.source, conversation.id, trace);
|
|
424
|
+
traces.push(packet);
|
|
425
|
+
seenConversations.add(packet.conversation_id);
|
|
426
|
+
} catch {
|
|
427
|
+
traces.push(syntheticEvidenceTrace(input.source, conversation.id, {
|
|
428
|
+
updated_at: conversation.updated_at,
|
|
429
|
+
reason: "conversation trace was not available from the hosted runtime",
|
|
430
|
+
}));
|
|
431
|
+
seenConversations.add(conversation.id);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const run of input.evidence.runs) {
|
|
436
|
+
if (!isRecord(run)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const conversationId = readString(run, "conversation_id") ?? readString(run, "conversationId");
|
|
441
|
+
|
|
442
|
+
if (conversationId && seenConversations.has(conversationId)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const runId = readString(run, "id") ?? readString(run, "run_id") ?? `run_${shortHash(JSON.stringify(run))}`;
|
|
447
|
+
traces.push(syntheticEvidenceTrace(input.source, conversationId ?? runId, {
|
|
448
|
+
title: readString(run, "title") ?? runId,
|
|
449
|
+
updated_at: readString(run, "updated_at") ?? readString(run, "updatedAt"),
|
|
450
|
+
raw: run,
|
|
451
|
+
}));
|
|
452
|
+
|
|
453
|
+
if (conversationId) {
|
|
454
|
+
seenConversations.add(conversationId);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return traces;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function syntheticEvidenceTrace(
|
|
462
|
+
source: ImproveSource,
|
|
463
|
+
conversationId: string,
|
|
464
|
+
input: {
|
|
465
|
+
title?: string;
|
|
466
|
+
updated_at?: string;
|
|
467
|
+
reason?: string;
|
|
468
|
+
raw?: unknown;
|
|
469
|
+
},
|
|
470
|
+
): ImproveTracePacket {
|
|
471
|
+
const safeConversationId = redactEvalText(conversationId);
|
|
472
|
+
const title = input.title ? redactEvalText(input.title) : "Hosted evidence";
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
id: `trace_${slugify(title)}_${shortHash(`${source}:${conversationId}:${JSON.stringify(input.raw ?? input.reason ?? "")}`)}`,
|
|
476
|
+
source,
|
|
477
|
+
conversation_id: safeConversationId,
|
|
478
|
+
title,
|
|
479
|
+
updated_at: input.updated_at,
|
|
480
|
+
trace: {
|
|
481
|
+
messages: [
|
|
482
|
+
{
|
|
483
|
+
role: "system",
|
|
484
|
+
content: redactEvalText(input.reason ?? "Hosted evidence did not include replayable conversation messages."),
|
|
485
|
+
...(input.updated_at ? { created_at: input.updated_at } : {}),
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
raw: redactEvidenceValue(input.raw ?? input.reason ?? null),
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function summarizeImproveEvidence(evidence: unknown): unknown {
|
|
494
|
+
if (!isRecord(evidence)) {
|
|
495
|
+
return evidence === undefined ? undefined : { available: true };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
deploy_id: readString(evidence, "deploy_id") ?? readString(evidence, "deployId") ?? null,
|
|
500
|
+
runs: Array.isArray(evidence.runs) ? evidence.runs.length : 0,
|
|
501
|
+
conversations: Array.isArray(evidence.conversations) ? evidence.conversations.length : 0,
|
|
502
|
+
deliveries: Array.isArray(evidence.deliveries) ? evidence.deliveries.length : 0,
|
|
503
|
+
deploy_events: Array.isArray(evidence.deploy_events) ? evidence.deploy_events.length : 0,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function readImproveBundle(path: string): Promise<ImproveBundle> {
|
|
508
|
+
const parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
509
|
+
|
|
510
|
+
if (!isRecord(parsed) || parsed.schema_version !== 1 || !Array.isArray(parsed.traces)) {
|
|
511
|
+
throw new AgentKitError("validation_error", `${path} is not an AgentKit improve bundle.`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return parsed as ImproveBundle;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function resolveBundlePath(root: string, input: string): Promise<string> {
|
|
518
|
+
if (!input) {
|
|
519
|
+
throw new AgentKitError("validation_error", "Missing improve bundle path.");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const resolved = resolveInsideCapsule(root, input, "improve bundle");
|
|
523
|
+
const info = await stat(resolved);
|
|
524
|
+
|
|
525
|
+
if (info.isDirectory()) {
|
|
526
|
+
return join(resolved, "bundle.json");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return resolved;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function resolveImproveDirectory(root: string, createdAt: string, out?: string): string {
|
|
533
|
+
const defaultPath = join(".agentkit", "improve", createdAt.replace(/[:.]/g, "-"));
|
|
534
|
+
return resolveInsideCapsule(root, out ?? defaultPath, "improve output");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function resolveUniqueEvalPath(root: string, trace: ImproveTracePacket, usedPaths: Set<string>): string {
|
|
538
|
+
const baseName = trace.id.replace(/^trace_/, "");
|
|
539
|
+
const base = join("evals", "regressions", `improve-${baseName}.eval.ts`);
|
|
540
|
+
let candidate = resolveInsideCapsule(root, base, "improve eval output");
|
|
541
|
+
let index = 2;
|
|
542
|
+
|
|
543
|
+
while (usedPaths.has(candidate)) {
|
|
544
|
+
candidate = resolveInsideCapsule(
|
|
545
|
+
root,
|
|
546
|
+
join("evals", "regressions", `improve-${baseName}-${index}.eval.ts`),
|
|
547
|
+
"improve eval output",
|
|
548
|
+
);
|
|
549
|
+
index += 1;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return candidate;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function resolveInsideCapsule(root: string, path: string, context: string): string {
|
|
556
|
+
if (isAbsolute(path)) {
|
|
557
|
+
throw new AgentKitError("validation_error", `${context} path must be relative to the Agent Capsule.`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const resolved = resolve(root, path);
|
|
561
|
+
const relativePath = relative(root, resolved);
|
|
562
|
+
|
|
563
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
564
|
+
throw new AgentKitError("validation_error", `${context} path must stay inside the Agent Capsule.`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return resolved;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function normalizeConversationSummaries(payload: unknown): Array<{ id: string; updated_at?: string }> {
|
|
571
|
+
const conversations = Array.isArray(payload)
|
|
572
|
+
? payload
|
|
573
|
+
: isRecord(payload) && Array.isArray(payload.conversations)
|
|
574
|
+
? payload.conversations
|
|
575
|
+
: null;
|
|
576
|
+
|
|
577
|
+
if (!conversations) {
|
|
578
|
+
throw new AgentKitError("deploy_conversation_response_invalid", "Conversation list response did not include conversations.");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return conversations.map((conversation) => {
|
|
582
|
+
if (!isRecord(conversation)) {
|
|
583
|
+
throw new AgentKitError("deploy_conversation_response_invalid", "Conversation list contained a non-object entry.");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const id = readString(conversation, "id") ?? readString(conversation, "conversation_id");
|
|
587
|
+
|
|
588
|
+
if (!id) {
|
|
589
|
+
throw new AgentKitError("deploy_conversation_response_invalid", "Conversation list entry did not include an id.");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
id,
|
|
594
|
+
updated_at: readString(conversation, "updated_at") ?? readString(conversation, "updatedAt"),
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function tracePacketFromPayload(source: ImproveSource, fallbackConversationId: string, payload: unknown): ImproveTracePacket {
|
|
600
|
+
const trace = normalizeTracePayload(fallbackConversationId, payload);
|
|
601
|
+
const safeConversationId = redactEvalText(trace.conversation_id);
|
|
602
|
+
const safeTitle = trace.title ? redactEvalText(trace.title) : null;
|
|
603
|
+
const idLabel = slugify(safeTitle ?? safeConversationId);
|
|
604
|
+
const idHash = shortHash(`${source}:${trace.conversation_id}`);
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
id: `trace_${idLabel}_${idHash}`,
|
|
608
|
+
source,
|
|
609
|
+
conversation_id: safeConversationId,
|
|
610
|
+
title: safeTitle,
|
|
611
|
+
updated_at: trace.updated_at,
|
|
612
|
+
trace: {
|
|
613
|
+
messages: trace.messages.map(redactTraceMessage),
|
|
614
|
+
runs: Array.isArray(trace.runs) ? trace.runs.map(redactEvidenceValue) : trace.runs,
|
|
615
|
+
raw: redactEvidenceValue(payload),
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function normalizeTracePayload(
|
|
621
|
+
fallbackConversationId: string,
|
|
622
|
+
payload: unknown,
|
|
623
|
+
): {
|
|
624
|
+
conversation_id: string;
|
|
625
|
+
title?: string | null;
|
|
626
|
+
updated_at?: string;
|
|
627
|
+
messages: ImproveTraceMessage[];
|
|
628
|
+
runs?: unknown[];
|
|
629
|
+
} {
|
|
630
|
+
if (!isRecord(payload)) {
|
|
631
|
+
throw new AgentKitError("deploy_conversation_response_invalid", "Conversation trace response was not an object.");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const conversationId = readString(payload, "id") ?? readString(payload, "conversation_id") ?? fallbackConversationId;
|
|
635
|
+
const title = readString(payload, "title");
|
|
636
|
+
const updatedAt = readString(payload, "updated_at") ?? readString(payload, "updatedAt");
|
|
637
|
+
const messages = Array.isArray(payload.messages)
|
|
638
|
+
? payload.messages.map(normalizeMessage).filter((message): message is ImproveTraceMessage => message !== null)
|
|
639
|
+
: [];
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
conversation_id: conversationId,
|
|
643
|
+
title: title ?? null,
|
|
644
|
+
updated_at: updatedAt,
|
|
645
|
+
messages,
|
|
646
|
+
runs: Array.isArray(payload.runs) ? payload.runs : undefined,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function normalizeMessage(message: unknown): ImproveTraceMessage | null {
|
|
651
|
+
if (!isRecord(message)) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const role = readString(message, "role");
|
|
656
|
+
const content = readString(message, "content");
|
|
657
|
+
|
|
658
|
+
if (!isTraceRole(role) || typeof content !== "string") {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
role,
|
|
664
|
+
content,
|
|
665
|
+
created_at: readString(message, "created_at") ?? readString(message, "createdAt"),
|
|
666
|
+
sequence: readNumber(message, "sequence"),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function redactTraceMessage(message: ImproveTraceMessage): ImproveTraceMessage {
|
|
671
|
+
return {
|
|
672
|
+
...message,
|
|
673
|
+
content: redactEvalText(message.content),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
type ImproveEvalTurn = {
|
|
678
|
+
input: string;
|
|
679
|
+
expect?: {
|
|
680
|
+
response:
|
|
681
|
+
| { contains: string }
|
|
682
|
+
| { notRegex: string[] };
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
function replayTurnsFromMessages(messages: ImproveTraceMessage[]): ImproveEvalTurn[] {
|
|
687
|
+
const turns: ImproveEvalTurn[] = [];
|
|
688
|
+
|
|
689
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
690
|
+
const message = messages[index];
|
|
691
|
+
|
|
692
|
+
if (message.role !== "user") {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const nextAssistant = messages.slice(index + 1).find((candidate) => candidate.role === "assistant");
|
|
697
|
+
|
|
698
|
+
turns.push({
|
|
699
|
+
input: message.content,
|
|
700
|
+
...(nextAssistant ? { expect: { response: { contains: nextAssistant.content } } } : {}),
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return turns;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function redactEvalTurn(turn: ImproveEvalTurn): ImproveEvalTurn {
|
|
708
|
+
const input = redactEvalText(turn.input);
|
|
709
|
+
|
|
710
|
+
if (!turn.expect || !("contains" in turn.expect.response)) {
|
|
711
|
+
return { input, ...(turn.expect ? { expect: turn.expect } : {}) };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const contains = redactEvalText(turn.expect.response.contains);
|
|
715
|
+
const redactionChangedTurn = input !== turn.input || contains !== turn.expect.response.contains;
|
|
716
|
+
const redactionPresent = input.includes("[redacted_") || contains.includes("[redacted_");
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
input,
|
|
720
|
+
...(redactionChangedTurn || redactionPresent
|
|
721
|
+
? {
|
|
722
|
+
expect: {
|
|
723
|
+
response: {
|
|
724
|
+
notRegex: ["API_KEY|Bearer\\s+|(?:sk|agk|ak|dpat)[_-]"],
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
}
|
|
728
|
+
: { expect: { response: { contains } } }),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function redactEvalText(value: string): string {
|
|
733
|
+
return value
|
|
734
|
+
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[redacted_email]")
|
|
735
|
+
.replace(/\b[A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET)\s*=\s*[^\s,;]+/gi, (match) => {
|
|
736
|
+
const separator = match.includes("=") ? "=" : "";
|
|
737
|
+
const name = separator ? match.slice(0, match.indexOf("=")).trim() : "SECRET";
|
|
738
|
+
return `${name}=[redacted_secret]`;
|
|
739
|
+
})
|
|
740
|
+
.replace(/\b(?:sk|agk|ak|dpat)[_-][A-Za-z0-9_-]{8,}\b/g, "[redacted_token]")
|
|
741
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [redacted_token]")
|
|
742
|
+
.replace(/\+?\d[\d\s().-]{7,}\d/g, "[redacted_phone]");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function redactEvidenceValue(value: unknown): unknown {
|
|
746
|
+
if (typeof value === "string") {
|
|
747
|
+
return redactEvalText(value);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (Array.isArray(value)) {
|
|
751
|
+
return value.map(redactEvidenceValue);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!isRecord(value)) {
|
|
755
|
+
return value;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const redacted: Record<string, unknown> = {};
|
|
759
|
+
|
|
760
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
761
|
+
redacted[key] = shouldRedactEvidenceField(key)
|
|
762
|
+
? "[redacted_secret]"
|
|
763
|
+
: redactEvidenceValue(nestedValue);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return redacted;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function shouldRedactEvidenceField(key: string): boolean {
|
|
770
|
+
return /(?:token|authorization|api[_-]?key|secret|password|private[_-]?key|client[_-]?secret)$/i.test(key);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function renderImproveEval(
|
|
774
|
+
trace: ImproveTracePacket,
|
|
775
|
+
turns: ImproveEvalTurn[],
|
|
776
|
+
): string {
|
|
777
|
+
return `import { defineEval } from "@andreprado/agentkit";
|
|
778
|
+
|
|
779
|
+
export default defineEval({
|
|
780
|
+
name: ${JSON.stringify(`production replay ${trace.title ?? trace.id}`)},
|
|
781
|
+
turns: ${formatEvalTurns(turns)},
|
|
782
|
+
});
|
|
783
|
+
`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function formatEvalTurns(turns: ImproveEvalTurn[]): string {
|
|
787
|
+
return JSON.stringify(turns, null, 4)
|
|
788
|
+
.split("\n")
|
|
789
|
+
.map((line, index) => (index === 0 ? line : ` ${line}`))
|
|
790
|
+
.join("\n");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function resolveSinceDate(value: string | undefined): Date | null {
|
|
794
|
+
if (!value) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const trimmed = value.trim();
|
|
799
|
+
const duration = trimmed.match(/^(\d+)(m|h|d|w)$/);
|
|
800
|
+
|
|
801
|
+
if (duration) {
|
|
802
|
+
const amount = Number(duration[1]);
|
|
803
|
+
const unit = duration[2];
|
|
804
|
+
const multiplier = unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : unit === "d" ? 86_400_000 : 604_800_000;
|
|
805
|
+
return new Date(Date.now() - amount * multiplier);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const parsed = new Date(trimmed);
|
|
809
|
+
|
|
810
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
811
|
+
throw new AgentKitError("validation_error", 'improve --since must be an ISO timestamp or a duration like "24h", "7d", or "2w".');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return parsed;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function dateIsOnOrAfter(value: string, since: Date): boolean {
|
|
818
|
+
const timestamp = Date.parse(value);
|
|
819
|
+
return Number.isFinite(timestamp) && timestamp >= since.getTime();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function slugify(value: string): string {
|
|
823
|
+
const slug = value
|
|
824
|
+
.toLowerCase()
|
|
825
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
826
|
+
.replace(/^-+|-+$/g, "")
|
|
827
|
+
.slice(0, 48);
|
|
828
|
+
|
|
829
|
+
return slug || "trace";
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function shortHash(value: string): string {
|
|
833
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 10);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function formatError(error: unknown): string {
|
|
837
|
+
if (isAgentKitError(error)) {
|
|
838
|
+
return `${error.code}: ${error.message}`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (error instanceof Error) {
|
|
842
|
+
return `runtime_error: ${error.message}`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return `runtime_error: ${String(error)}`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function isTraceRole(value: unknown): value is ImproveTraceMessage["role"] {
|
|
849
|
+
return value === "user" || value === "assistant" || value === "system" || value === "tool";
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function readString(value: Record<string, unknown>, key: string): string | undefined {
|
|
853
|
+
const item = value[key];
|
|
854
|
+
return typeof item === "string" ? item : undefined;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function readNumber(value: Record<string, unknown>, key: string): number | undefined {
|
|
858
|
+
const item = value[key];
|
|
859
|
+
return typeof item === "number" && Number.isFinite(item) ? item : undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
863
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
867
|
+
return error instanceof Error && "code" in error;
|
|
868
|
+
}
|