@andreprado/agentkit 0.1.0-alpha.6 → 0.1.0-alpha.8
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 +4 -0
- package/docs/guides/connect-telegram.md +2 -0
- package/docs/guides/create-agent.md +9 -0
- package/docs/guides/run-evals.md +36 -1
- package/package.json +1 -1
- package/src/cli/cloud-client.ts +10 -2
- package/src/cli/commands/channels.ts +28 -2
- package/src/cli/deploy-chat-ui.ts +7 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +124 -4
- package/src/index.ts +44 -1
- package/src/providers/pi.ts +1 -1
- package/src/providers/test.ts +1 -1
- package/src/runtime/config.ts +8 -0
- package/src/runtime/database.ts +93 -2
- package/src/runtime/db-commands.ts +9 -0
- package/src/runtime/dev-server.ts +43 -2
- package/src/runtime/evals.ts +210 -20
- package/src/runtime/inspect.ts +5 -0
- package/src/runtime/spec.ts +152 -0
- package/src/runtime/sync.ts +144 -0
- package/src/runtime/targets/cloudflare/build.ts +45 -0
- package/src/runtime/tools.ts +121 -1
- package/src/runtime/traces.ts +41 -0
- package/src/storage/sqlite.ts +136 -0
- package/src/templates/blank.ts +8 -1
- package/src/templates/dentista.ts +14 -1
- package/src/templates/skills/agentkit-build-agent/SKILL.md +9 -7
- package/src/templates/skills/agentkit-database/SKILL.md +5 -2
- package/src/templates/skills/agentkit-evals/SKILL.md +32 -3
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +22 -0
- package/src/templates/support.ts +7 -0
package/src/runtime/database.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
3
|
+
import { basename, relative, resolve } from "node:path";
|
|
3
4
|
|
|
4
5
|
import type { DatabaseArgs, DatabaseResult, DatabaseRunner, DatabaseStatement } from "../index";
|
|
5
6
|
import { openCapsuleStore, type SqliteAgentKitStore } from "../storage/sqlite";
|
|
@@ -9,6 +10,12 @@ import { AgentKitError } from "./errors";
|
|
|
9
10
|
export type LocalDatabasePrepareResult = {
|
|
10
11
|
store: SqliteAgentKitStore;
|
|
11
12
|
databasePath: string;
|
|
13
|
+
migrations: {
|
|
14
|
+
configured: boolean;
|
|
15
|
+
path: string | null;
|
|
16
|
+
applied: string[];
|
|
17
|
+
skipped: string[];
|
|
18
|
+
};
|
|
12
19
|
schema: {
|
|
13
20
|
configured: boolean;
|
|
14
21
|
applied: boolean;
|
|
@@ -20,10 +27,12 @@ export async function openPreparedCapsuleStore(capsule: LoadedAgentCapsule): Pro
|
|
|
20
27
|
const store = await openCapsuleStore(capsule);
|
|
21
28
|
|
|
22
29
|
try {
|
|
30
|
+
const migrations = await applyCapsuleDatabaseMigrations(capsule, store);
|
|
23
31
|
const schema = await applyCapsuleDatabaseSchema(capsule, store);
|
|
24
32
|
return {
|
|
25
33
|
store,
|
|
26
34
|
databasePath: store.dbPath,
|
|
35
|
+
migrations,
|
|
27
36
|
schema,
|
|
28
37
|
};
|
|
29
38
|
} catch (error) {
|
|
@@ -32,6 +41,84 @@ export async function openPreparedCapsuleStore(capsule: LoadedAgentCapsule): Pro
|
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
export async function applyCapsuleDatabaseMigrations(
|
|
45
|
+
capsule: LoadedAgentCapsule,
|
|
46
|
+
store: SqliteAgentKitStore,
|
|
47
|
+
): Promise<LocalDatabasePrepareResult["migrations"]> {
|
|
48
|
+
const database = capsule.config.storage.database;
|
|
49
|
+
|
|
50
|
+
if (!database || database.driver !== "turso") {
|
|
51
|
+
return {
|
|
52
|
+
configured: false,
|
|
53
|
+
path: null,
|
|
54
|
+
applied: [],
|
|
55
|
+
skipped: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const migrationsPath = resolve(capsule.root, database.migrations ?? "migrations");
|
|
60
|
+
const migrations = await readMigrationFiles(migrationsPath);
|
|
61
|
+
|
|
62
|
+
if (migrations.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
configured: false,
|
|
65
|
+
path: migrationsPath,
|
|
66
|
+
applied: [],
|
|
67
|
+
skipped: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = store.applyApplicationMigrations(migrations);
|
|
73
|
+
return {
|
|
74
|
+
configured: true,
|
|
75
|
+
path: migrationsPath,
|
|
76
|
+
applied: result.applied,
|
|
77
|
+
skipped: result.skipped,
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof AgentKitError && error.code === "database_schema_error") {
|
|
81
|
+
throw new AgentKitError(
|
|
82
|
+
"database_schema_error",
|
|
83
|
+
[
|
|
84
|
+
`Could not apply database migrations from ${relative(capsule.root, migrationsPath)}.`,
|
|
85
|
+
"Migrations must be ordered .sql files and should use safe, forward-only changes.",
|
|
86
|
+
error.message,
|
|
87
|
+
].join(" "),
|
|
88
|
+
{ cause: error },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readMigrationFiles(directory: string): Promise<Array<{ id: string; sql: string }>> {
|
|
97
|
+
let entries: Dirent[];
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const files = entries
|
|
110
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
|
|
111
|
+
.map((entry) => entry.name)
|
|
112
|
+
.sort();
|
|
113
|
+
|
|
114
|
+
return Promise.all(
|
|
115
|
+
files.map(async (file) => ({
|
|
116
|
+
id: basename(file, ".sql"),
|
|
117
|
+
sql: await readFile(resolve(directory, file), "utf8"),
|
|
118
|
+
})),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
35
122
|
export async function applyCapsuleDatabaseSchema(
|
|
36
123
|
capsule: LoadedAgentCapsule,
|
|
37
124
|
store: SqliteAgentKitStore,
|
|
@@ -163,3 +250,7 @@ function validateStatements(statements: DatabaseStatement[]): void {
|
|
|
163
250
|
validateSql(statement.sql, statement.args ?? []);
|
|
164
251
|
}
|
|
165
252
|
}
|
|
253
|
+
|
|
254
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
255
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
256
|
+
}
|
|
@@ -10,6 +10,9 @@ import { AgentKitError } from "./errors";
|
|
|
10
10
|
export type DbMigrateResult = {
|
|
11
11
|
root: string;
|
|
12
12
|
databasePath: string;
|
|
13
|
+
migrationsPath: string | null;
|
|
14
|
+
migrationsApplied: string[];
|
|
15
|
+
migrationsSkipped: string[];
|
|
13
16
|
schemaPath: string | null;
|
|
14
17
|
schemaApplied: boolean;
|
|
15
18
|
};
|
|
@@ -43,6 +46,9 @@ export async function migrateLocalDatabase(cwd = process.cwd()): Promise<DbMigra
|
|
|
43
46
|
return {
|
|
44
47
|
root: capsule.root,
|
|
45
48
|
databasePath: prepared.databasePath,
|
|
49
|
+
migrationsPath: prepared.migrations.path,
|
|
50
|
+
migrationsApplied: prepared.migrations.applied,
|
|
51
|
+
migrationsSkipped: prepared.migrations.skipped,
|
|
46
52
|
schemaPath: prepared.schema.path,
|
|
47
53
|
schemaApplied: prepared.schema.applied,
|
|
48
54
|
};
|
|
@@ -87,6 +93,9 @@ export async function seedLocalDatabase(cwd = process.cwd(), seedFile = "seed.sq
|
|
|
87
93
|
return {
|
|
88
94
|
root: capsule.root,
|
|
89
95
|
databasePath: prepared.databasePath,
|
|
96
|
+
migrationsPath: prepared.migrations.path,
|
|
97
|
+
migrationsApplied: prepared.migrations.applied,
|
|
98
|
+
migrationsSkipped: prepared.migrations.skipped,
|
|
90
99
|
schemaPath: prepared.schema.path,
|
|
91
100
|
schemaApplied: prepared.schema.applied,
|
|
92
101
|
seedPath,
|
|
@@ -147,18 +147,20 @@ function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
|
|
|
147
147
|
dedupeKeys: new Set<string>(),
|
|
148
148
|
buffers: new Map<string, DevChannelBuffer>(),
|
|
149
149
|
};
|
|
150
|
+
const capsuleRoot = capsule.root;
|
|
150
151
|
return createHttpServer((incoming, outgoing) => {
|
|
151
|
-
void handleNodeRequest(
|
|
152
|
+
void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime);
|
|
152
153
|
});
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
async function handleNodeRequest(
|
|
156
|
-
|
|
157
|
+
capsuleRoot: string,
|
|
157
158
|
incoming: IncomingMessage,
|
|
158
159
|
outgoing: ServerResponse,
|
|
159
160
|
channelRuntime: DevChannelRuntimeState,
|
|
160
161
|
): Promise<void> {
|
|
161
162
|
try {
|
|
163
|
+
const capsule = await loadAgentCapsule(capsuleRoot);
|
|
162
164
|
const request = nodeRequestToFetchRequest(incoming);
|
|
163
165
|
const response = await handleDevServerRequest(capsule, request, channelRuntime);
|
|
164
166
|
await writeFetchResponse(outgoing, response);
|
|
@@ -296,6 +298,12 @@ async function handleDevServerRequest(
|
|
|
296
298
|
return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
|
|
302
|
+
|
|
303
|
+
if (request.method === "GET" && conversationTraceMatch) {
|
|
304
|
+
return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
|
|
305
|
+
}
|
|
306
|
+
|
|
299
307
|
return jsonError(new AgentKitError("not_found", `Route ${request.method} ${route} was not found.`), 404);
|
|
300
308
|
} catch (error) {
|
|
301
309
|
return jsonError(error, statusForError(error));
|
|
@@ -570,6 +578,32 @@ async function getConversation(capsule: LoadedAgentCapsule, conversationId: stri
|
|
|
570
578
|
}
|
|
571
579
|
}
|
|
572
580
|
|
|
581
|
+
async function getConversationTrace(capsule: LoadedAgentCapsule, conversationId: string) {
|
|
582
|
+
if (!conversationId) {
|
|
583
|
+
throw new AgentKitError("validation_error", "Missing conversation id.");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const store = await openCapsuleStore(capsule);
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const conversation = store.getConversation(conversationId);
|
|
590
|
+
|
|
591
|
+
if (!conversation) {
|
|
592
|
+
throw new AgentKitError("conversation_not_found", `Conversation "${conversationId}" was not found.`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
...conversation,
|
|
597
|
+
runs: store.listRunsForConversation(conversation.id).map((run) => ({
|
|
598
|
+
...run,
|
|
599
|
+
toolCalls: store.listToolCallsForRun(run.id),
|
|
600
|
+
})),
|
|
601
|
+
};
|
|
602
|
+
} finally {
|
|
603
|
+
store.close();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
573
607
|
async function readJsonBody(request: Request): Promise<unknown> {
|
|
574
608
|
const contentType = request.headers.get("content-type") ?? "";
|
|
575
609
|
|
|
@@ -1066,6 +1100,13 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
1066
1100
|
messages.scrollTop = messages.scrollHeight;
|
|
1067
1101
|
}
|
|
1068
1102
|
|
|
1103
|
+
textarea.addEventListener("keydown", (event) => {
|
|
1104
|
+
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
|
1105
|
+
event.preventDefault();
|
|
1106
|
+
if (button.disabled) return;
|
|
1107
|
+
form.requestSubmit();
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1069
1110
|
form.addEventListener("submit", async (event) => {
|
|
1070
1111
|
event.preventDefault();
|
|
1071
1112
|
const content = textarea.value.trim();
|
package/src/runtime/evals.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import type { Dirent } from "node:fs";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
|
|
6
6
|
import type { AgentRunResult } from "./chat";
|
|
@@ -8,6 +8,7 @@ import { runAgentMessageFromCwd } from "./chat";
|
|
|
8
8
|
import { findAgentCapsuleRoot, loadAgentCapsule } from "./config";
|
|
9
9
|
import { AgentKitError } from "./errors";
|
|
10
10
|
import { openCapsuleStore, type StoredToolCall } from "../storage/sqlite";
|
|
11
|
+
import { getConversationTraceFromCwd } from "./traces";
|
|
11
12
|
|
|
12
13
|
export type EvalRunSummary = {
|
|
13
14
|
root: string;
|
|
@@ -27,9 +28,17 @@ export type EvalResult = {
|
|
|
27
28
|
type EvalCase = {
|
|
28
29
|
name?: string;
|
|
29
30
|
input?: string;
|
|
31
|
+
turns?: EvalTurn[];
|
|
30
32
|
expect?: EvalExpect;
|
|
31
33
|
};
|
|
32
34
|
|
|
35
|
+
type EvalTurn =
|
|
36
|
+
| string
|
|
37
|
+
| {
|
|
38
|
+
input: string;
|
|
39
|
+
expect?: EvalExpect;
|
|
40
|
+
};
|
|
41
|
+
|
|
33
42
|
type EvalExpect = {
|
|
34
43
|
contains?: string | string[];
|
|
35
44
|
not_contains?: string | string[];
|
|
@@ -55,16 +64,25 @@ type ToolExpectation =
|
|
|
55
64
|
output?: unknown;
|
|
56
65
|
status?: "running" | "completed" | "failed";
|
|
57
66
|
visibility?: "user" | "internal";
|
|
67
|
+
rendered?: unknown;
|
|
58
68
|
};
|
|
59
69
|
|
|
60
70
|
type ToolCallSnapshot = {
|
|
61
71
|
name?: unknown;
|
|
62
72
|
input?: unknown;
|
|
63
73
|
output?: unknown;
|
|
74
|
+
rendered?: unknown;
|
|
64
75
|
status?: unknown;
|
|
65
76
|
visibility?: unknown;
|
|
66
77
|
};
|
|
67
78
|
|
|
79
|
+
export type EvalFromConversationResult = {
|
|
80
|
+
root: string;
|
|
81
|
+
file: string;
|
|
82
|
+
conversationId: string;
|
|
83
|
+
turns: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
68
86
|
export async function runEvalsFromCwd(cwd: string): Promise<EvalRunSummary> {
|
|
69
87
|
const root = await findAgentCapsuleRoot(cwd);
|
|
70
88
|
const evalFiles = await findEvalFiles(join(root, "evals"));
|
|
@@ -77,28 +95,14 @@ export async function runEvalsFromCwd(cwd: string): Promise<EvalRunSummary> {
|
|
|
77
95
|
|
|
78
96
|
for (const file of evalFiles) {
|
|
79
97
|
const evalCase = await loadEvalCase(file);
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (typeof input !== "string" || input.trim().length === 0) {
|
|
83
|
-
throw new AgentKitError("validation_error", `${relative(root, file)} must export an eval with a non-empty input string.`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const run = await runAgentMessageFromCwd(root, {
|
|
87
|
-
message: input,
|
|
88
|
-
runtime: {
|
|
89
|
-
environment: "eval",
|
|
90
|
-
invocation: "eval",
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
const persistedToolCalls = await loadPersistedToolCalls(root, run.runId);
|
|
94
|
-
const failures = evaluateExpectations(evalCase.expect ?? {}, run, persistedToolCalls);
|
|
98
|
+
const run = await runEvalCase(root, relative(root, file), evalCase);
|
|
95
99
|
|
|
96
100
|
results.push({
|
|
97
101
|
name: evalCase.name ?? relative(root, file),
|
|
98
102
|
file: relative(root, file),
|
|
99
|
-
passed: failures.length === 0,
|
|
100
|
-
failures,
|
|
101
|
-
output: run.
|
|
103
|
+
passed: run.failures.length === 0,
|
|
104
|
+
failures: run.failures,
|
|
105
|
+
output: run.output,
|
|
102
106
|
});
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -112,6 +116,133 @@ export async function runEvalsFromCwd(cwd: string): Promise<EvalRunSummary> {
|
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
export async function writeEvalFromConversation(
|
|
120
|
+
cwd: string,
|
|
121
|
+
input: {
|
|
122
|
+
conversationId: string;
|
|
123
|
+
out?: string;
|
|
124
|
+
name?: string;
|
|
125
|
+
force?: boolean;
|
|
126
|
+
},
|
|
127
|
+
): Promise<EvalFromConversationResult> {
|
|
128
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
129
|
+
const trace = await getConversationTraceFromCwd(root, input.conversationId);
|
|
130
|
+
const turns = replayTurnsFromMessages(trace.messages);
|
|
131
|
+
|
|
132
|
+
if (turns.length === 0) {
|
|
133
|
+
throw new AgentKitError(
|
|
134
|
+
"validation_error",
|
|
135
|
+
`Conversation "${input.conversationId}" does not contain user/assistant turns that can become an eval.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const file = resolve(root, input.out ?? join("evals", `replay-${slugify(trace.title ?? trace.id)}.eval.ts`));
|
|
140
|
+
|
|
141
|
+
if (!input.force && await pathExists(file)) {
|
|
142
|
+
throw new AgentKitError(
|
|
143
|
+
"validation_error",
|
|
144
|
+
`${relative(root, file)} already exists. Re-run with --force or choose --out <path>.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await mkdir(dirname(file), { recursive: true });
|
|
149
|
+
await writeFile(
|
|
150
|
+
file,
|
|
151
|
+
`export default {
|
|
152
|
+
name: ${JSON.stringify(input.name ?? `replay ${trace.title ?? trace.id}`)},
|
|
153
|
+
turns: ${formatEvalTurns(turns)},
|
|
154
|
+
};
|
|
155
|
+
`,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
root,
|
|
160
|
+
file,
|
|
161
|
+
conversationId: trace.id,
|
|
162
|
+
turns: turns.length,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function runEvalCase(
|
|
167
|
+
root: string,
|
|
168
|
+
file: string,
|
|
169
|
+
evalCase: EvalCase,
|
|
170
|
+
): Promise<{ failures: string[]; output: string }> {
|
|
171
|
+
const turns = normalizeEvalTurns(evalCase);
|
|
172
|
+
|
|
173
|
+
if (turns.length === 0) {
|
|
174
|
+
throw new AgentKitError(
|
|
175
|
+
"validation_error",
|
|
176
|
+
`${file} must export either a non-empty input string or a non-empty turns array.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const conversationId = `eval_${crypto.randomUUID()}`;
|
|
181
|
+
const failures: string[] = [];
|
|
182
|
+
let output = "";
|
|
183
|
+
|
|
184
|
+
for (const [index, turn] of turns.entries()) {
|
|
185
|
+
const run = await runAgentMessageFromCwd(root, {
|
|
186
|
+
message: turn.input,
|
|
187
|
+
conversationId,
|
|
188
|
+
runtime: {
|
|
189
|
+
environment: "eval",
|
|
190
|
+
invocation: "eval",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const persistedToolCalls = await loadPersistedToolCalls(root, run.runId);
|
|
194
|
+
const turnFailures = evaluateExpectations(turn.expect ?? {}, run, persistedToolCalls);
|
|
195
|
+
|
|
196
|
+
for (const failure of turnFailures) {
|
|
197
|
+
failures.push(turns.length === 1 ? failure : `turn ${index + 1}: ${failure}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
output = run.message.content;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { failures, output };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeEvalTurns(evalCase: EvalCase): Array<{ input: string; expect?: EvalExpect }> {
|
|
207
|
+
if (evalCase.turns !== undefined) {
|
|
208
|
+
if (!Array.isArray(evalCase.turns)) {
|
|
209
|
+
throw new AgentKitError("validation_error", "eval turns must be an array when provided.");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return evalCase.turns.map((turn, index) => normalizeEvalTurn(turn, index));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (typeof evalCase.input === "string" && evalCase.input.trim().length > 0) {
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
input: evalCase.input,
|
|
219
|
+
expect: evalCase.expect,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeEvalTurn(turn: EvalTurn, index: number): { input: string; expect?: EvalExpect } {
|
|
228
|
+
if (typeof turn === "string") {
|
|
229
|
+
if (turn.trim().length === 0) {
|
|
230
|
+
throw new AgentKitError("validation_error", `eval turns[${index}] must be non-empty.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { input: turn };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!isRecord(turn) || typeof turn.input !== "string" || turn.input.trim().length === 0) {
|
|
237
|
+
throw new AgentKitError("validation_error", `eval turns[${index}].input must be a non-empty string.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
input: turn.input,
|
|
242
|
+
...(turn.expect ? { expect: turn.expect } : {}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
115
246
|
async function findEvalFiles(directory: string): Promise<string[]> {
|
|
116
247
|
let entries: Dirent[];
|
|
117
248
|
|
|
@@ -253,6 +384,10 @@ function matchesToolExpectation(toolCall: ToolCallSnapshot, expectation: ToolExp
|
|
|
253
384
|
return false;
|
|
254
385
|
}
|
|
255
386
|
|
|
387
|
+
if ("rendered" in expectation && !jsonContains(toolCall.rendered, expectation.rendered)) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
256
391
|
return true;
|
|
257
392
|
}
|
|
258
393
|
|
|
@@ -269,11 +404,66 @@ function toolCallSnapshotFromStored(toolCall: StoredToolCall): ToolCallSnapshot
|
|
|
269
404
|
name: toolCall.toolName,
|
|
270
405
|
input: toolCall.input,
|
|
271
406
|
output: toolCall.output,
|
|
407
|
+
rendered: toolCall.rendered,
|
|
272
408
|
status: toolCall.status,
|
|
273
409
|
visibility: toolCall.visibility,
|
|
274
410
|
};
|
|
275
411
|
}
|
|
276
412
|
|
|
413
|
+
function replayTurnsFromMessages(
|
|
414
|
+
messages: Array<{ role: string; content: string }>,
|
|
415
|
+
): Array<{ input: string; expect?: EvalExpect }> {
|
|
416
|
+
const turns: Array<{ input: string; expect?: EvalExpect }> = [];
|
|
417
|
+
|
|
418
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
419
|
+
const message = messages[index];
|
|
420
|
+
|
|
421
|
+
if (message.role !== "user") {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const nextAssistant = messages.slice(index + 1).find((candidate) => candidate.role === "assistant");
|
|
426
|
+
turns.push({
|
|
427
|
+
input: message.content,
|
|
428
|
+
...(nextAssistant
|
|
429
|
+
? {
|
|
430
|
+
expect: {
|
|
431
|
+
contains: nextAssistant.content,
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
: {}),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return turns;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatEvalTurns(turns: Array<{ input: string; expect?: EvalExpect }>): string {
|
|
442
|
+
return JSON.stringify(turns, null, 4)
|
|
443
|
+
.split("\n")
|
|
444
|
+
.map((line, index) => (index === 0 ? line : ` ${line}`))
|
|
445
|
+
.join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function slugify(value: string): string {
|
|
449
|
+
const slug = value
|
|
450
|
+
.toLowerCase()
|
|
451
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
452
|
+
.replace(/^-+|-+$/g, "")
|
|
453
|
+
.slice(0, 48);
|
|
454
|
+
|
|
455
|
+
return slug || "conversation";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
459
|
+
try {
|
|
460
|
+
await stat(path);
|
|
461
|
+
return true;
|
|
462
|
+
} catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
277
467
|
function jsonContains(actual: unknown, expected: unknown): boolean {
|
|
278
468
|
if (Array.isArray(expected)) {
|
|
279
469
|
return jsonEqual(actual, expected);
|
package/src/runtime/inspect.ts
CHANGED
|
@@ -59,11 +59,13 @@ export type AgentInspectDatabase = {
|
|
|
59
59
|
driver: "sqlite";
|
|
60
60
|
path: string | null;
|
|
61
61
|
schema: string | null;
|
|
62
|
+
migrations: string | null;
|
|
62
63
|
};
|
|
63
64
|
hosted: {
|
|
64
65
|
driver: "none" | "turso";
|
|
65
66
|
provisioning: "none" | "agentkit-managed";
|
|
66
67
|
schema: string | null;
|
|
68
|
+
migrations: string | null;
|
|
67
69
|
};
|
|
68
70
|
};
|
|
69
71
|
|
|
@@ -171,17 +173,20 @@ function toCapsulePath(root: string, path: string): string {
|
|
|
171
173
|
function inspectDatabase(capsule: LoadedAgentCapsule): AgentInspectDatabase {
|
|
172
174
|
const database = capsule.config.storage.database;
|
|
173
175
|
const schema = database?.driver === "turso" && database.schema ? database.schema : null;
|
|
176
|
+
const migrations = database?.driver === "turso" ? database.migrations ?? "migrations" : null;
|
|
174
177
|
|
|
175
178
|
return {
|
|
176
179
|
local: {
|
|
177
180
|
driver: "sqlite",
|
|
178
181
|
path: capsule.storagePath ? toCapsulePath(capsule.root, capsule.storagePath) : null,
|
|
179
182
|
schema,
|
|
183
|
+
migrations,
|
|
180
184
|
},
|
|
181
185
|
hosted: {
|
|
182
186
|
driver: database?.driver === "turso" ? "turso" : "none",
|
|
183
187
|
provisioning: database?.driver === "turso" ? "agentkit-managed" : "none",
|
|
184
188
|
schema,
|
|
189
|
+
migrations,
|
|
185
190
|
},
|
|
186
191
|
};
|
|
187
192
|
}
|