@kirrosh/zond 0.13.0 → 0.16.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -7
  4. package/src/cli/commands/ci-init.ts +12 -1
  5. package/src/cli/commands/coverage.ts +21 -1
  6. package/src/cli/commands/db.ts +121 -0
  7. package/src/cli/commands/describe.ts +60 -0
  8. package/src/cli/commands/generate.ts +127 -0
  9. package/src/cli/commands/guide.ts +127 -0
  10. package/src/cli/commands/init.ts +50 -77
  11. package/src/cli/commands/request.ts +57 -0
  12. package/src/cli/commands/run.ts +53 -10
  13. package/src/cli/commands/serve.ts +62 -3
  14. package/src/cli/commands/validate.ts +18 -2
  15. package/src/cli/index.ts +213 -215
  16. package/src/cli/json-envelope.ts +19 -0
  17. package/src/core/diagnostics/db-analysis.ts +351 -0
  18. package/src/core/diagnostics/failure-hints.ts +1 -0
  19. package/src/core/generator/data-factory.ts +19 -8
  20. package/src/core/generator/describe.ts +250 -0
  21. package/src/core/generator/guide-builder.ts +20 -0
  22. package/src/core/generator/index.ts +0 -3
  23. package/src/core/generator/suite-generator.ts +133 -20
  24. package/src/core/runner/executor.ts +1 -0
  25. package/src/core/runner/send-request.ts +94 -0
  26. package/src/core/runner/types.ts +1 -0
  27. package/src/db/queries.ts +4 -2
  28. package/src/db/schema.ts +11 -3
  29. package/src/mcp/descriptions.ts +0 -24
  30. package/src/mcp/server.ts +1 -8
  31. package/src/mcp/tools/describe-endpoint.ts +3 -218
  32. package/src/mcp/tools/query-db.ts +6 -222
  33. package/src/mcp/tools/run-tests.ts +1 -0
  34. package/src/mcp/tools/send-request.ts +15 -61
  35. package/src/web/views/suites-tab.ts +1 -1
  36. package/src/cli/commands/add-api.ts +0 -53
  37. package/src/cli/commands/ai-generate.ts +0 -106
  38. package/src/cli/commands/chat.ts +0 -43
  39. package/src/cli/commands/collections.ts +0 -41
  40. package/src/cli/commands/compare.ts +0 -129
  41. package/src/cli/commands/doctor.ts +0 -127
  42. package/src/cli/commands/runs.ts +0 -108
  43. package/src/cli/commands/update.ts +0 -142
  44. package/src/core/agent/agent-loop.ts +0 -116
  45. package/src/core/agent/context-manager.ts +0 -41
  46. package/src/core/agent/system-prompt.ts +0 -27
  47. package/src/core/agent/tools/diagnose-failure.ts +0 -51
  48. package/src/core/agent/tools/index.ts +0 -42
  49. package/src/core/agent/tools/query-results.ts +0 -40
  50. package/src/core/agent/tools/run-tests.ts +0 -38
  51. package/src/core/agent/tools/send-request.ts +0 -44
  52. package/src/core/agent/types.ts +0 -22
  53. package/src/core/generator/ai/ai-generator.ts +0 -61
  54. package/src/core/generator/ai/llm-client.ts +0 -159
  55. package/src/core/generator/ai/output-parser.ts +0 -307
  56. package/src/core/generator/ai/prompt-builder.ts +0 -153
  57. package/src/core/generator/ai/types.ts +0 -56
  58. package/src/mcp/tools/generate-and-save.ts +0 -202
  59. package/src/mcp/tools/save-test-suite.ts +0 -218
  60. package/src/mcp/tools/set-work-dir.ts +0 -35
  61. package/src/tui/chat-ui.ts +0 -150
@@ -1,129 +0,0 @@
1
- import { getDb } from "../../db/schema.ts";
2
- import { getRunById, getResultsByRunId } from "../../db/queries.ts";
3
- import { printError } from "../output.ts";
4
-
5
- const RESET = "\x1b[0m";
6
- const GREEN = "\x1b[32m";
7
- const RED = "\x1b[31m";
8
- const YELLOW = "\x1b[33m";
9
- const BOLD = "\x1b[1m";
10
-
11
- function useColor(): boolean {
12
- return process.stdout.isTTY ?? false;
13
- }
14
-
15
- export interface CompareOptions {
16
- runA: number;
17
- runB: number;
18
- dbPath?: string;
19
- }
20
-
21
- export async function compareCommand(options: CompareOptions): Promise<number> {
22
- const { runA, runB, dbPath } = options;
23
-
24
- try {
25
- getDb(dbPath);
26
-
27
- const runARecord = getRunById(runA);
28
- const runBRecord = getRunById(runB);
29
-
30
- if (!runARecord) {
31
- printError(`Run #${runA} not found`);
32
- return 2;
33
- }
34
- if (!runBRecord) {
35
- printError(`Run #${runB} not found`);
36
- return 2;
37
- }
38
-
39
- const resultsA = getResultsByRunId(runA);
40
- const resultsB = getResultsByRunId(runB);
41
-
42
- // Build lookup maps: "suite_name::test_name" → status
43
- const mapA = new Map<string, string>();
44
- const mapB = new Map<string, string>();
45
-
46
- for (const r of resultsA) {
47
- mapA.set(`${r.suite_name}::${r.test_name}`, r.status);
48
- }
49
- for (const r of resultsB) {
50
- mapB.set(`${r.suite_name}::${r.test_name}`, r.status);
51
- }
52
-
53
- const regressions: Array<{ suite: string; test: string; before: string; after: string }> = [];
54
- const fixes: Array<{ suite: string; test: string; before: string; after: string }> = [];
55
- const unchanged: number[] = [];
56
- let newTests = 0;
57
- let removedTests = 0;
58
-
59
- // Check all keys from B (current run)
60
- for (const [key, statusB] of mapB) {
61
- const statusA = mapA.get(key);
62
- if (statusA === undefined) {
63
- newTests++;
64
- continue;
65
- }
66
- const wasPass = statusA === "pass";
67
- const isPass = statusB === "pass";
68
- const wasFail = statusA === "fail" || statusA === "error";
69
- const isFail = statusB === "fail" || statusB === "error";
70
-
71
- const [suite, test] = key.split("::") as [string, string];
72
-
73
- if (wasPass && isFail) {
74
- regressions.push({ suite, test, before: statusA, after: statusB });
75
- } else if (wasFail && isPass) {
76
- fixes.push({ suite, test, before: statusA, after: statusB });
77
- } else {
78
- unchanged.push(1);
79
- }
80
- }
81
-
82
- // Count removed tests
83
- for (const key of mapA.keys()) {
84
- if (!mapB.has(key)) removedTests++;
85
- }
86
-
87
- const color = useColor();
88
-
89
- // Header
90
- console.log(`\nComparing run #${runA} (${runARecord.started_at.slice(0, 19)}) → run #${runB} (${runBRecord.started_at.slice(0, 19)})\n`);
91
-
92
- // Summary line
93
- const parts = [
94
- `${color ? BOLD : ""}${regressions.length} regressions${color ? RESET : ""}`,
95
- `${fixes.length} fixes`,
96
- `${unchanged.length} unchanged`,
97
- ];
98
- if (newTests > 0) parts.push(`${newTests} new`);
99
- if (removedTests > 0) parts.push(`${removedTests} removed`);
100
- console.log(parts.join(" | ") + "\n");
101
-
102
- // Regressions
103
- if (regressions.length > 0) {
104
- console.log(`${color ? RED + BOLD : ""}Regressions (pass → fail):${color ? RESET : ""}`);
105
- for (const r of regressions) {
106
- console.log(` ${color ? RED : ""}✗${color ? RESET : ""} [${r.suite}] ${r.test} (${r.before} → ${r.after})`);
107
- }
108
- console.log("");
109
- }
110
-
111
- // Fixes
112
- if (fixes.length > 0) {
113
- console.log(`${color ? GREEN : ""}Fixes (fail → pass):${color ? RESET : ""}`);
114
- for (const f of fixes) {
115
- console.log(` ${color ? GREEN : ""}✓${color ? RESET : ""} [${f.suite}] ${f.test} (${f.before} → ${f.after})`);
116
- }
117
- console.log("");
118
- }
119
-
120
- if (regressions.length === 0 && fixes.length === 0) {
121
- console.log(`${color ? GREEN : ""}No regressions detected.${color ? RESET : ""}`);
122
- }
123
-
124
- return regressions.length > 0 ? 1 : 0;
125
- } catch (err) {
126
- printError(err instanceof Error ? err.message : String(err));
127
- return 2;
128
- }
129
- }
@@ -1,127 +0,0 @@
1
- import { existsSync } from "fs";
2
- import { resolve } from "path";
3
- import { getDb, closeDb } from "../../db/schema.ts";
4
-
5
- export interface DoctorOptions {
6
- dbPath?: string;
7
- }
8
-
9
- interface Check {
10
- label: string;
11
- ok: boolean;
12
- detail: string;
13
- }
14
-
15
- export async function doctorCommand(options: DoctorOptions): Promise<number> {
16
- const checks: Check[] = [];
17
-
18
- // 1. Database
19
- checks.push(checkDatabase(options.dbPath));
20
-
21
- // 2. Test files
22
- checks.push(checkTestFiles());
23
-
24
- // 3. OpenAPI spec
25
- checks.push(checkOpenApiSpec());
26
-
27
- // 4. Environment files
28
- checks.push(checkEnvFiles());
29
-
30
- // 5. Ollama
31
- checks.push(await checkOllama());
32
-
33
- // Print results
34
- console.log("\nzond doctor\n");
35
-
36
- let hasFailure = false;
37
- for (const check of checks) {
38
- const icon = check.ok ? "\u2713" : "\u2717";
39
- console.log(` ${icon} ${check.label}: ${check.detail}`);
40
- if (!check.ok) hasFailure = true;
41
- }
42
-
43
- console.log("");
44
- if (hasFailure) {
45
- console.log("Some checks failed. See details above.");
46
- } else {
47
- console.log("All checks passed.");
48
- }
49
-
50
- return hasFailure ? 1 : 0;
51
- }
52
-
53
- function checkDatabase(dbPath?: string): Check {
54
- const path = dbPath ? resolve(dbPath) : resolve(process.cwd(), "zond.db");
55
- try {
56
- const db = getDb(path);
57
- const runs = (db.query("SELECT COUNT(*) as cnt FROM runs").get() as { cnt: number }).cnt;
58
- const envs = (db.query("SELECT COUNT(*) as cnt FROM environments").get() as { cnt: number }).cnt;
59
- closeDb();
60
- return { label: "Database", ok: true, detail: `${path} (${runs} runs, ${envs} environments)` };
61
- } catch (err) {
62
- return { label: "Database", ok: false, detail: `Cannot open ${path}: ${(err as Error).message}` };
63
- }
64
- }
65
-
66
- function checkTestFiles(): Check {
67
- const dirs = [".", "tests", "test"];
68
- const found: string[] = [];
69
-
70
- for (const dir of dirs) {
71
- const full = resolve(process.cwd(), dir);
72
- if (!existsSync(full)) continue;
73
- try {
74
- const glob = new Bun.Glob("**/*.yaml");
75
- for (const file of glob.scanSync({ cwd: full, absolute: false })) {
76
- if (!file.startsWith(".env.")) {
77
- found.push(`${dir}/${file}`);
78
- }
79
- }
80
- } catch { /* ignore */ }
81
- }
82
-
83
- if (found.length > 0) {
84
- return { label: "Test files", ok: true, detail: `${found.length} YAML file(s) found` };
85
- }
86
- return { label: "Test files", ok: false, detail: "No YAML test files found in cwd or tests/" };
87
- }
88
-
89
- function checkOpenApiSpec(): Check {
90
- const candidates = ["openapi.yaml", "openapi.json", "openapi.yml", "swagger.yaml", "swagger.json"];
91
- for (const name of candidates) {
92
- const full = resolve(process.cwd(), name);
93
- if (existsSync(full)) {
94
- return { label: "OpenAPI spec", ok: true, detail: name };
95
- }
96
- }
97
- return { label: "OpenAPI spec", ok: false, detail: "No openapi.yaml/json found (optional)" };
98
- }
99
-
100
- function checkEnvFiles(): Check {
101
- const found: string[] = [];
102
- try {
103
- const glob = new Bun.Glob(".env.*.yaml");
104
- for (const file of glob.scanSync({ cwd: process.cwd(), absolute: false })) {
105
- found.push(file);
106
- }
107
- } catch { /* ignore */ }
108
-
109
- if (found.length > 0) {
110
- return { label: "Environment files", ok: true, detail: found.join(", ") };
111
- }
112
- return { label: "Environment files", ok: false, detail: "No .env.*.yaml files found (optional)" };
113
- }
114
-
115
- async function checkOllama(): Promise<Check> {
116
- try {
117
- const res = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
118
- if (res.ok) {
119
- const data = await res.json() as { models?: { name: string }[] };
120
- const count = data.models?.length ?? 0;
121
- return { label: "Ollama", ok: true, detail: `Running (${count} model(s) available)` };
122
- }
123
- return { label: "Ollama", ok: false, detail: `Responded with status ${res.status}` };
124
- } catch {
125
- return { label: "Ollama", ok: false, detail: "Not reachable at localhost:11434 (optional, needed for chat)" };
126
- }
127
- }
@@ -1,108 +0,0 @@
1
- import { getDb } from "../../db/schema.ts";
2
- import { listRuns, getRunById, getResultsByRunId } from "../../db/queries.ts";
3
- import { printError } from "../output.ts";
4
-
5
- export interface RunsOptions {
6
- runId?: number;
7
- limit?: number;
8
- dbPath?: string;
9
- }
10
-
11
- const RESET = "\x1b[0m";
12
- const GREEN = "\x1b[32m";
13
- const RED = "\x1b[31m";
14
- const YELLOW = "\x1b[33m";
15
-
16
- function useColor(): boolean {
17
- return process.stdout.isTTY ?? false;
18
- }
19
-
20
- function statusIcon(passed: number, failed: number): string {
21
- const color = useColor();
22
- if (failed === 0) return color ? `${GREEN}PASS${RESET}` : "PASS";
23
- return color ? `${RED}FAIL${RESET}` : "FAIL";
24
- }
25
-
26
- export function runsCommand(options: RunsOptions): number {
27
- const { runId, limit = 20, dbPath } = options;
28
-
29
- try {
30
- getDb(dbPath);
31
- } catch (err) {
32
- printError(`Failed to open database: ${(err as Error).message}`);
33
- return 2;
34
- }
35
-
36
- if (runId !== undefined) {
37
- return showRunDetail(runId);
38
- }
39
- return showRunList(limit);
40
- }
41
-
42
- function showRunList(limit: number): number {
43
- const runs = listRuns(limit);
44
-
45
- if (runs.length === 0) {
46
- console.log("No runs found.");
47
- return 0;
48
- }
49
-
50
- // Print table
51
- const header = "ID STATUS TOTAL PASS FAIL ENV DURATION STARTED";
52
- console.log(header);
53
- console.log("-".repeat(header.length));
54
-
55
- for (const run of runs) {
56
- const status = statusIcon(run.passed, run.failed);
57
- const env = (run.environment ?? "-").slice(0, 10).padEnd(10);
58
- const duration = run.duration_ms != null ? `${run.duration_ms}ms` : "-";
59
- const started = run.started_at.slice(0, 19).replace("T", " ");
60
- console.log(
61
- `${String(run.id).padEnd(6)} ${status.padEnd(useColor() ? 14 : 6)} ${String(run.total).padEnd(5)} ${String(run.passed).padEnd(4)} ${String(run.failed).padEnd(4)} ${env} ${duration.padEnd(8)} ${started}`,
62
- );
63
- }
64
-
65
- return 0;
66
- }
67
-
68
- function showRunDetail(runId: number): number {
69
- const run = getRunById(runId);
70
- if (!run) {
71
- printError(`Run #${runId} not found`);
72
- return 1;
73
- }
74
-
75
- const color = useColor();
76
-
77
- console.log(`Run #${run.id}`);
78
- console.log(` Started: ${run.started_at}`);
79
- if (run.finished_at) console.log(` Finished: ${run.finished_at}`);
80
- if (run.environment) console.log(` Environment: ${run.environment}`);
81
- if (run.duration_ms != null) console.log(` Duration: ${run.duration_ms}ms`);
82
- console.log(` Total: ${run.total} Passed: ${run.passed} Failed: ${run.failed} Skipped: ${run.skipped}`);
83
-
84
- const results = getResultsByRunId(runId);
85
- if (results.length === 0) {
86
- console.log("\nNo step results recorded.");
87
- return 0;
88
- }
89
-
90
- console.log("\nSteps:");
91
- for (const r of results) {
92
- let statusStr: string;
93
- if (r.status === "pass") {
94
- statusStr = color ? `${GREEN}PASS${RESET}` : "PASS";
95
- } else if (r.status === "fail" || r.status === "error") {
96
- statusStr = color ? `${RED}${r.status.toUpperCase()}${RESET}` : r.status.toUpperCase();
97
- } else {
98
- statusStr = color ? `${YELLOW}SKIP${RESET}` : "SKIP";
99
- }
100
-
101
- console.log(` ${statusStr} ${r.test_name} (${r.duration_ms}ms)`);
102
- if (r.error_message) {
103
- console.log(` ${color ? RED : ""}${r.error_message}${color ? RESET : ""}`);
104
- }
105
- }
106
-
107
- return 0;
108
- }
@@ -1,142 +0,0 @@
1
- import { tmpdir } from "os";
2
- import { join } from "path";
3
- import { existsSync, unlinkSync, renameSync, copyFileSync, chmodSync } from "fs";
4
- import { VERSION } from "../index.ts";
5
- import { isCompiledBinary } from "../runtime.ts";
6
-
7
- export interface UpdateCommandOptions {
8
- force?: boolean;
9
- }
10
-
11
- export function detectTarget(): { target: string; archive: "tar.gz" | "zip" } {
12
- const platform = process.platform;
13
- const arch = process.arch;
14
-
15
- const os = platform === "win32" ? "win" : platform;
16
- const archSuffix = arch === "arm64" ? "arm64" : "x64";
17
- const target = `${os}-${archSuffix}`;
18
- const archive = platform === "win32" ? "zip" : ("tar.gz" as const);
19
-
20
- return { target, archive };
21
- }
22
-
23
- export function parseVersion(tag: string): string {
24
- return tag.replace(/^v/, "");
25
- }
26
-
27
- export function compareVersions(a: string, b: string): number {
28
- const pa = a.split(".").map(Number);
29
- const pb = b.split(".").map(Number);
30
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
31
- const na = pa[i] ?? 0;
32
- const nb = pb[i] ?? 0;
33
- if (na !== nb) return na - nb;
34
- }
35
- return 0;
36
- }
37
-
38
- export async function updateCommand(options: UpdateCommandOptions): Promise<number> {
39
- if (!isCompiledBinary()) {
40
- console.log("Running from source — use git pull to update.");
41
- return 0;
42
- }
43
-
44
- console.log("Checking for updates...");
45
-
46
- // Fetch latest release
47
- const res = await fetch("https://api.github.com/repos/kirrosh/zond/releases/latest", {
48
- headers: { "User-Agent": "zond-updater" },
49
- });
50
-
51
- if (!res.ok) {
52
- console.error(`Failed to check for updates: HTTP ${res.status}`);
53
- return 1;
54
- }
55
-
56
- const release = (await res.json()) as { tag_name: string };
57
- const latestVersion = parseVersion(release.tag_name);
58
- const currentVersion = VERSION;
59
-
60
- if (!options.force && compareVersions(currentVersion, latestVersion) >= 0) {
61
- console.log(`Already up to date (v${currentVersion}).`);
62
- return 0;
63
- }
64
-
65
- console.log(`Updating v${currentVersion} → v${latestVersion}...`);
66
-
67
- const { target, archive } = detectTarget();
68
- const assetName = `zond-${target}.${archive}`;
69
- const downloadUrl = `https://github.com/kirrosh/zond/releases/download/${release.tag_name}/${assetName}`;
70
-
71
- // Download artifact
72
- const dlRes = await fetch(downloadUrl);
73
- if (!dlRes.ok) {
74
- console.error(`Failed to download ${assetName}: HTTP ${dlRes.status}`);
75
- return 1;
76
- }
77
-
78
- const tempDir = tmpdir();
79
- const archivePath = join(tempDir, assetName);
80
- const archiveBytes = new Uint8Array(await dlRes.arrayBuffer());
81
- await Bun.write(archivePath, archiveBytes);
82
-
83
- // Extract
84
- const extractDir = join(tempDir, `zond-update-${Date.now()}`);
85
- const mkdirResult = Bun.spawnSync(["mkdir", "-p", extractDir]);
86
- if (mkdirResult.exitCode !== 0) {
87
- // Fallback for Windows
88
- const { mkdirSync } = await import("fs");
89
- mkdirSync(extractDir, { recursive: true });
90
- }
91
-
92
- if (archive === "tar.gz") {
93
- const tar = Bun.spawnSync(["tar", "-xzf", archivePath, "-C", extractDir]);
94
- if (tar.exitCode !== 0) {
95
- console.error("Failed to extract archive.");
96
- return 1;
97
- }
98
- } else {
99
- // Windows zip — use tar (Windows 10+ includes bsdtar)
100
- const tar = Bun.spawnSync(["tar", "-xf", archivePath, "-C", extractDir]);
101
- if (tar.exitCode !== 0) {
102
- console.error("Failed to extract archive.");
103
- return 1;
104
- }
105
- }
106
-
107
- // Find extracted binary
108
- const binaryName = process.platform === "win32" ? "zond.exe" : "zond";
109
- const newBinary = join(extractDir, binaryName);
110
-
111
- if (!existsSync(newBinary)) {
112
- console.error(`Expected binary not found: ${newBinary}`);
113
- return 1;
114
- }
115
-
116
- // Replace current binary
117
- const currentBinary = process.execPath;
118
-
119
- if (process.platform === "win32") {
120
- // Windows: can't overwrite running exe — rename current to .old, copy new
121
- const oldPath = currentBinary + ".old";
122
- try {
123
- if (existsSync(oldPath)) unlinkSync(oldPath);
124
- } catch { /* ignore */ }
125
- renameSync(currentBinary, oldPath);
126
- copyFileSync(newBinary, currentBinary);
127
- } else {
128
- // Unix: rename new over current (atomic on same filesystem, but we copy across)
129
- unlinkSync(currentBinary);
130
- copyFileSync(newBinary, currentBinary);
131
- chmodSync(currentBinary, 0o755);
132
- }
133
-
134
- // Cleanup
135
- try {
136
- unlinkSync(archivePath);
137
- unlinkSync(newBinary);
138
- } catch { /* best effort */ }
139
-
140
- console.log(`Updated to v${latestVersion}.`);
141
- return 0;
142
- }
@@ -1,116 +0,0 @@
1
- // Suppress AI SDK v2 spec compatibility warnings for Ollama (cosmetic, tool calling works fine)
2
- (globalThis as any).AI_SDK_LOG_WARNINGS = false;
3
-
4
- import { generateText, stepCountIs } from "ai";
5
- import { createOpenAI } from "@ai-sdk/openai";
6
- import { createAnthropic } from "@ai-sdk/anthropic";
7
- import { AGENT_SYSTEM_PROMPT } from "./system-prompt.ts";
8
- import { buildAgentTools } from "./tools/index.ts";
9
- import type { AgentConfig, AgentTurnResult, ToolEvent } from "./types.ts";
10
- import type { ModelMessage } from "ai";
11
-
12
- export function buildProvider(config: AgentConfig) {
13
- const { provider } = config.provider;
14
-
15
- if (provider === "anthropic") {
16
- return createAnthropic({
17
- apiKey: config.provider.apiKey,
18
- baseURL: config.provider.baseUrl || undefined,
19
- });
20
- }
21
-
22
- // openai, ollama, custom all use OpenAI-compatible API
23
- return createOpenAI({
24
- apiKey: config.provider.apiKey ?? "ollama",
25
- baseURL: config.provider.baseUrl,
26
- });
27
- }
28
-
29
- function buildModel(config: AgentConfig) {
30
- const provider = buildProvider(config);
31
- const { provider: providerType } = config.provider;
32
-
33
- // For ollama/custom, use .chat() to avoid the responses API which they don't support.
34
- if (providerType === "ollama" || providerType === "custom") {
35
- return (provider as ReturnType<typeof createOpenAI>).chat(config.provider.model);
36
- }
37
-
38
- return provider(config.provider.model);
39
- }
40
-
41
- /**
42
- * Prepare messages with system prompt.
43
- * Some small/local models (e.g. qwen3 thinking mode via Ollama) break tool calling
44
- * when a separate `system` message is present. For ollama/custom providers, we inject
45
- * the system prompt into the first user message instead.
46
- */
47
- function prepareMessages(
48
- messages: ModelMessage[],
49
- config: AgentConfig,
50
- ): { system?: string; messages: ModelMessage[] } {
51
- const { provider } = config.provider;
52
-
53
- if (provider === "ollama" || provider === "custom") {
54
- // Inject system prompt into first user message to avoid breaking tool calling
55
- const prepared = [...messages];
56
- const firstUserIdx = prepared.findIndex(
57
- (m) => m.role === "user" && typeof m.content === "string",
58
- );
59
-
60
- if (firstUserIdx >= 0) {
61
- const msg = prepared[firstUserIdx] as { role: "user"; content: string };
62
- prepared[firstUserIdx] = {
63
- ...msg,
64
- content: `[System instructions]\n${AGENT_SYSTEM_PROMPT}\n[End instructions]\n\n${msg.content}`,
65
- };
66
- }
67
-
68
- return { messages: prepared };
69
- }
70
-
71
- // For OpenAI/Anthropic, use the standard system parameter
72
- return { system: AGENT_SYSTEM_PROMPT, messages };
73
- }
74
-
75
- export async function runAgentTurn(
76
- messages: ModelMessage[],
77
- config: AgentConfig,
78
- onToolEvent?: (event: ToolEvent) => void,
79
- ): Promise<AgentTurnResult> {
80
- const model = buildModel(config);
81
- const tools = buildAgentTools(config);
82
- const { system, messages: prepared } = prepareMessages(messages, config);
83
- const toolEvents: ToolEvent[] = [];
84
-
85
- const result = await generateText({
86
- model,
87
- system,
88
- messages: prepared,
89
- tools,
90
- stopWhen: stepCountIs(config.maxSteps ?? 10),
91
- maxOutputTokens: config.provider.maxTokens ?? 4096,
92
- onStepFinish: ({ toolCalls, toolResults }) => {
93
- if (toolCalls) {
94
- for (let i = 0; i < toolCalls.length; i++) {
95
- const call = toolCalls[i]!;
96
- const toolResult = toolResults?.[i];
97
- const event: ToolEvent = {
98
- toolName: call.toolName,
99
- args: ("input" in call ? call.input : {}) as Record<string, unknown>,
100
- result: toolResult ?? null,
101
- timestamp: new Date().toISOString(),
102
- };
103
- toolEvents.push(event);
104
- onToolEvent?.(event);
105
- }
106
- }
107
- },
108
- });
109
-
110
- return {
111
- text: result.text,
112
- toolEvents,
113
- inputTokens: result.usage?.inputTokens ?? 0,
114
- outputTokens: result.usage?.outputTokens ?? 0,
115
- };
116
- }
@@ -1,41 +0,0 @@
1
- import type { CoreMessageFormat } from "../../db/queries.ts";
2
-
3
- const MAX_MESSAGES = 20;
4
- const KEEP_RECENT_TURNS = 6; // 6 turns = 12 messages (user + assistant pairs)
5
- const KEEP_RECENT_MESSAGES = KEEP_RECENT_TURNS * 2;
6
-
7
- export function trimContext(messages: CoreMessageFormat[]): CoreMessageFormat[] {
8
- if (messages.length <= MAX_MESSAGES) {
9
- return messages;
10
- }
11
-
12
- const oldMessages = messages.slice(0, messages.length - KEEP_RECENT_MESSAGES);
13
- const recentMessages = messages.slice(messages.length - KEEP_RECENT_MESSAGES);
14
-
15
- const summary = buildSummary(oldMessages);
16
-
17
- // Use role "user" for the summary so that the conversation always starts with a user message.
18
- // Some providers require conversations to begin with a user turn.
19
- return [
20
- { role: "user" as const, content: summary },
21
- ...recentMessages,
22
- ];
23
- }
24
-
25
- function buildSummary(messages: CoreMessageFormat[]): string {
26
- const userMessages = messages.filter((m) => m.role === "user");
27
- const topics = userMessages
28
- .map((m) => m.content.slice(0, 80))
29
- .slice(0, 5);
30
-
31
- const topicList = topics.length > 0
32
- ? topics.map((t) => `- ${t}`).join("\n")
33
- : "- General conversation";
34
-
35
- return `[Conversation summary — ${messages.length} earlier messages condensed]
36
-
37
- Topics discussed:
38
- ${topicList}
39
-
40
- The conversation continues below with the most recent messages.`;
41
- }
@@ -1,27 +0,0 @@
1
- export const AGENT_SYSTEM_PROMPT = `You are an API testing assistant powered by zond. You help users run, create, and diagnose API tests.
2
-
3
- You have access to the following tools:
4
-
5
- - **run_tests**: Execute API test suites from YAML files or directories. Returns pass/fail summary with run ID.
6
- - **query_results**: Query historical test run results and collections from the database.
7
- - **diagnose_failure**: Analyze a failed test run to identify root causes and suggest fixes.
8
- - **send_request**: Send an ad-hoc HTTP request for quick testing.
9
-
10
- Tool usage examples:
11
- - run_tests: { testPath: "tests/api.yaml" } or { testPath: "tests/", envName: "staging", safe: true }
12
- - query_results: action must be "list_runs", "get_run" (requires runId), or "list_collections"
13
- - List runs: { action: "list_runs", limit: 10 }
14
- - Get run details: { action: "get_run", runId: 1 }
15
- - List collections: { action: "list_collections" }
16
- - diagnose_failure: { runId: 1 }
17
-
18
- Guidelines:
19
- - When asked to run tests, use the run_tests tool and report results clearly.
20
- - When a test run has failures, proactively use diagnose_failure to analyze the issues.
21
- - When asked about past results, use query_results to look up run history.
22
- - Always provide actionable suggestions when tests fail.
23
- - Be concise but thorough in your explanations.
24
- - If a tool call fails with a validation error, re-read the tool schema and retry with corrected arguments.
25
- - When in safe mode, only GET (read-only) tests will be executed.
26
- - When using thinking/reasoning, keep your internal reasoning focused and share conclusions with the user.
27
- `;