agent-dealer 0.1.2 → 0.1.3
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/bundle/server/dist/queue/answers.test.js +117 -0
- package/bundle/server/dist/queue/dispatcher.js +164 -2
- package/bundle/server/dist/queue/plan-gate.test.js +55 -0
- package/bundle/server/dist/repository/runs.js +17 -1
- package/bundle/server/dist/routes/index.js +34 -8
- package/bundle/server/dist/runners/claude.js +9 -8
- package/bundle/server/dist/runners/persist.js +17 -4
- package/bundle/server/dist/runners/prompts.js +52 -1
- package/bundle/server/dist/runners/prompts.test.js +63 -0
- package/bundle/server/dist/runners/reflect.js +1 -1
- package/bundle/server/dist/runners/stream-json.js +28 -0
- package/bundle/server/dist/runners/stream-json.test.js +57 -0
- package/bundle/server/dist/usage-summary.js +9 -4
- package/bundle/server/package.json +1 -1
- package/bundle/server/static-ui/assets/index-CH7u5bXC.js +78 -0
- package/bundle/server/static-ui/assets/index-CKuYOeDN.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/budget.d.ts +1 -1
- package/bundle/shared/dist/budget.js +3 -3
- package/bundle/shared/dist/index.d.ts +306 -91
- package/bundle/shared/dist/index.js +15 -0
- package/bundle/shared/dist/plan-triage.d.ts +317 -0
- package/bundle/shared/dist/plan-triage.js +65 -0
- package/bundle/shared/dist/plan-triage.test.d.ts +1 -0
- package/bundle/shared/dist/plan-triage.test.js +66 -0
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-ChdPKYDd.css +0 -1
- package/bundle/server/static-ui/assets/index-hIzZ8TM8.js +0 -77
|
@@ -87,6 +87,7 @@ export function buildExecutionPrompt(run) {
|
|
|
87
87
|
parts.push(`## Human feedback`, feedback, ``);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
parts.push(...planAnswersSections(run));
|
|
90
91
|
if (run.taskCategory === "content" || run.taskCategory === "research") {
|
|
91
92
|
parts.push(`## Deliverable`, documentOutputHint(run), `You MUST write the file using your Write/edit tool — do not only print markdown in chat.`, ``);
|
|
92
93
|
}
|
|
@@ -137,6 +138,56 @@ export function buildReflectPrompt(run, opts) {
|
|
|
137
138
|
}
|
|
138
139
|
return parts.join("\n");
|
|
139
140
|
}
|
|
141
|
+
function planTriageContractSection() {
|
|
142
|
+
return [
|
|
143
|
+
"## Required final JSON block",
|
|
144
|
+
"After the plan markdown, end your reply with exactly one fenced ```json block shaped like:",
|
|
145
|
+
'{"verdict":"trivial"|"needs_review","rationale":"one sentence","questions":[{"id":"q1","question":"...","options":[{"label":"...","description":"..."}]}]}',
|
|
146
|
+
"Rules:",
|
|
147
|
+
'- "trivial" means this plan is safe to execute without human review; it requires an empty questions array. When in doubt, use "needs_review".',
|
|
148
|
+
"- Ask at most 3 questions, and only when the answer changes how you would execute. Never ask permission to proceed.",
|
|
149
|
+
"- Each question needs 2-4 concrete options; give each option a short description.",
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
function planAnswersSections(run) {
|
|
153
|
+
const ansArt = getLatestArtifact(run.id, "plan_answers");
|
|
154
|
+
if (!ansArt?.contentJson)
|
|
155
|
+
return [];
|
|
156
|
+
try {
|
|
157
|
+
const ans = JSON.parse(ansArt.contentJson);
|
|
158
|
+
if (ans.outcome !== "approved" || ans.answers.length === 0)
|
|
159
|
+
return [];
|
|
160
|
+
const triArt = getLatestArtifact(run.id, "plan_triage");
|
|
161
|
+
const questions = triArt?.contentJson
|
|
162
|
+
? JSON.parse(triArt.contentJson).questions
|
|
163
|
+
: [];
|
|
164
|
+
const lines = ans.answers.map((a) => {
|
|
165
|
+
const q = questions.find((x) => x.id === a.questionId);
|
|
166
|
+
return `- ${q?.question ?? a.questionId}: **${a.selectedLabel ?? a.freeText ?? ""}**`;
|
|
167
|
+
});
|
|
168
|
+
return ["## Human answers to plan questions", ...lines, ""];
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function buildPlanRevisePrompt(run, questions, answers) {
|
|
175
|
+
const qa = answers.map((a) => {
|
|
176
|
+
const q = questions.find((x) => x.id === a.questionId);
|
|
177
|
+
return `- Q: ${q?.question ?? a.questionId}\n A: ${a.selectedLabel ?? a.freeText ?? ""}`;
|
|
178
|
+
});
|
|
179
|
+
return [
|
|
180
|
+
"The human answered your plan questions. Revise the plan accordingly — do not execute anything.",
|
|
181
|
+
"",
|
|
182
|
+
"## Task",
|
|
183
|
+
taskText(run),
|
|
184
|
+
"",
|
|
185
|
+
"## Answers",
|
|
186
|
+
...qa,
|
|
187
|
+
"",
|
|
188
|
+
planTriageContractSection(),
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
140
191
|
export function buildPlanPrompt(run) {
|
|
141
192
|
const parts = [
|
|
142
193
|
`Draft a concise execution plan (markdown) for this task.`,
|
|
@@ -159,6 +210,6 @@ export function buildPlanPrompt(run) {
|
|
|
159
210
|
if (run.taskCategory === "content" || run.taskCategory === "research") {
|
|
160
211
|
parts.push(`## Deliverable (execution phase only)`, `After approval, execution will produce a markdown document. Plan the steps; do not create the file now.`, ``);
|
|
161
212
|
}
|
|
162
|
-
parts.push(`Output a step-by-step plan with risks
|
|
213
|
+
parts.push(`Output a step-by-step plan with risks.`, ``, planTriageContractSection());
|
|
163
214
|
return parts.join("\n");
|
|
164
215
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { test, before } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-prompts-"));
|
|
7
|
+
process.env.MAX_CONCURRENT_RUNS = "0";
|
|
8
|
+
const { migrate } = await import("../db/index.js");
|
|
9
|
+
const { BUILTIN_AGENT_CLAUDE_ID } = await import("@agent-dealer/shared");
|
|
10
|
+
const { updateAgent } = await import("../repository/agents.js");
|
|
11
|
+
const { addArtifact, createRun } = await import("../repository/runs.js");
|
|
12
|
+
const { buildExecutionPrompt, buildPlanPrompt, buildPlanRevisePrompt } = await import("./prompts.js");
|
|
13
|
+
const QUESTIONS = [
|
|
14
|
+
{
|
|
15
|
+
id: "q1",
|
|
16
|
+
question: "Which storage backend?",
|
|
17
|
+
options: [{ label: "SQLite" }, { label: "Postgres" }],
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
before(() => {
|
|
21
|
+
migrate();
|
|
22
|
+
updateAgent(BUILTIN_AGENT_CLAUDE_ID, { workspaceRoot: process.env.AGENT_DEALER_HOME });
|
|
23
|
+
});
|
|
24
|
+
function makeRun() {
|
|
25
|
+
return createRun({
|
|
26
|
+
title: "Prompt test task",
|
|
27
|
+
taskCategory: "other",
|
|
28
|
+
status: "plan_pending",
|
|
29
|
+
agentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
test("plan prompt includes the triage contract", () => {
|
|
33
|
+
const prompt = buildPlanPrompt(makeRun());
|
|
34
|
+
assert.match(prompt, /"verdict"/);
|
|
35
|
+
assert.match(prompt, /needs_review/);
|
|
36
|
+
assert.match(prompt, /at most 3 questions/i);
|
|
37
|
+
});
|
|
38
|
+
test("execution prompt renders human answers as Q→A pairs", () => {
|
|
39
|
+
const run = makeRun();
|
|
40
|
+
addArtifact(run.id, "draft_plan", { markdown: "# Plan" }, "agent");
|
|
41
|
+
addArtifact(run.id, "plan_triage", { verdict: "needs_review", rationale: "r", questions: QUESTIONS, parseFallback: false, consumed: false }, "agent");
|
|
42
|
+
addArtifact(run.id, "approved_plan", { markdown: "# Plan" }, "human");
|
|
43
|
+
addArtifact(run.id, "plan_answers", {
|
|
44
|
+
answers: [{ questionId: "q1", selectedLabel: "SQLite" }],
|
|
45
|
+
outcome: "approved",
|
|
46
|
+
answeredAt: new Date().toISOString(),
|
|
47
|
+
}, "human");
|
|
48
|
+
const prompt = buildExecutionPrompt(run);
|
|
49
|
+
assert.match(prompt, /## Human answers to plan questions/);
|
|
50
|
+
assert.match(prompt, /Which storage backend\?.*SQLite/s);
|
|
51
|
+
});
|
|
52
|
+
test("execution prompt omits answers section when none exist", () => {
|
|
53
|
+
const run = makeRun();
|
|
54
|
+
addArtifact(run.id, "approved_plan", { markdown: "# Plan" }, "human");
|
|
55
|
+
assert.doesNotMatch(buildExecutionPrompt(run), /Human answers to plan questions/);
|
|
56
|
+
});
|
|
57
|
+
test("revise prompt pairs each answer with its question and re-states the contract", () => {
|
|
58
|
+
const run = makeRun();
|
|
59
|
+
const prompt = buildPlanRevisePrompt(run, QUESTIONS, [{ questionId: "q1", freeText: "Use flat files instead" }]);
|
|
60
|
+
assert.match(prompt, /Which storage backend\?/);
|
|
61
|
+
assert.match(prompt, /Use flat files instead/);
|
|
62
|
+
assert.match(prompt, /"verdict"/);
|
|
63
|
+
});
|
|
@@ -31,7 +31,7 @@ export async function runReflect(run, opts) {
|
|
|
31
31
|
try {
|
|
32
32
|
const playbook = await fetchPlaybook(run.playbookId);
|
|
33
33
|
const previousBody = playbook.body ?? "";
|
|
34
|
-
const result = await runClaude(run, "reflect", undefined, buildReflectPrompt(run, opts));
|
|
34
|
+
const result = await runClaude(run, "reflect", undefined, { promptOverride: buildReflectPrompt(run, opts) });
|
|
35
35
|
const events = parseNdjson(result.transcript);
|
|
36
36
|
const resultText = extractResultText(events) ?? result.transcript;
|
|
37
37
|
const proposal = parseReflectProposal(resultText);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { PlanTriageBlock } from "@agent-dealer/shared";
|
|
2
3
|
export function parseNdjson(raw) {
|
|
3
4
|
const events = [];
|
|
4
5
|
for (const line of raw.split("\n")) {
|
|
@@ -62,6 +63,33 @@ export function extractPlanMarkdown(events) {
|
|
|
62
63
|
const raw = extractResultText(events) ?? "";
|
|
63
64
|
return stripMarkdownFences(raw);
|
|
64
65
|
}
|
|
66
|
+
const TRIAGE_FALLBACK = {
|
|
67
|
+
verdict: "needs_review",
|
|
68
|
+
rationale: "Agent did not return a valid triage block",
|
|
69
|
+
questions: [],
|
|
70
|
+
parseFallback: true,
|
|
71
|
+
};
|
|
72
|
+
/** Parse the trailing fenced json triage block from plan markdown (PRD F1.2). */
|
|
73
|
+
export function extractPlanTriage(planMarkdown) {
|
|
74
|
+
const blocks = [...planMarkdown.matchAll(/```json\s*\n([\s\S]*?)\n```/g)];
|
|
75
|
+
const last = blocks[blocks.length - 1];
|
|
76
|
+
if (!last)
|
|
77
|
+
return { markdown: planMarkdown.trim(), ...TRIAGE_FALLBACK };
|
|
78
|
+
const strippedMarkdown = (planMarkdown.slice(0, last.index) + planMarkdown.slice(last.index + last[0].length)).trim();
|
|
79
|
+
try {
|
|
80
|
+
const parsed = PlanTriageBlock.parse(JSON.parse(last[1]));
|
|
81
|
+
return {
|
|
82
|
+
markdown: strippedMarkdown,
|
|
83
|
+
verdict: parsed.verdict,
|
|
84
|
+
rationale: parsed.rationale,
|
|
85
|
+
questions: parsed.questions,
|
|
86
|
+
parseFallback: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { markdown: strippedMarkdown, ...TRIAGE_FALLBACK };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
65
93
|
export function extractUsage(events, phase, runtime) {
|
|
66
94
|
const result = events.find((e) => e.type === "result");
|
|
67
95
|
const init = events.find((e) => e.type === "system");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { extractPlanTriage } from "./stream-json.js";
|
|
4
|
+
const PLAN = "# Plan\n1. Do the thing\n2. Verify";
|
|
5
|
+
const VALID_BLOCK = [
|
|
6
|
+
"```json",
|
|
7
|
+
JSON.stringify({
|
|
8
|
+
verdict: "needs_review",
|
|
9
|
+
rationale: "Two viable approaches",
|
|
10
|
+
questions: [
|
|
11
|
+
{
|
|
12
|
+
id: "q1",
|
|
13
|
+
question: "Which approach?",
|
|
14
|
+
options: [{ label: "A" }, { label: "B", description: "slower but safer" }],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
}),
|
|
18
|
+
"```",
|
|
19
|
+
].join("\n");
|
|
20
|
+
test("parses trailing triage block and strips it from the markdown", () => {
|
|
21
|
+
const r = extractPlanTriage(`${PLAN}\n\n${VALID_BLOCK}`);
|
|
22
|
+
assert.equal(r.parseFallback, false);
|
|
23
|
+
assert.equal(r.verdict, "needs_review");
|
|
24
|
+
assert.equal(r.questions.length, 1);
|
|
25
|
+
assert.equal(r.markdown, PLAN);
|
|
26
|
+
});
|
|
27
|
+
test("trivial block with no questions parses", () => {
|
|
28
|
+
const block = '```json\n{"verdict":"trivial","rationale":"one-line change"}\n```';
|
|
29
|
+
const r = extractPlanTriage(`${PLAN}\n\n${block}`);
|
|
30
|
+
assert.equal(r.verdict, "trivial");
|
|
31
|
+
assert.equal(r.parseFallback, false);
|
|
32
|
+
assert.deepEqual(r.questions, []);
|
|
33
|
+
});
|
|
34
|
+
test("missing block falls back to needs_review with full markdown kept", () => {
|
|
35
|
+
const r = extractPlanTriage(PLAN);
|
|
36
|
+
assert.equal(r.parseFallback, true);
|
|
37
|
+
assert.equal(r.verdict, "needs_review");
|
|
38
|
+
assert.equal(r.markdown, PLAN);
|
|
39
|
+
});
|
|
40
|
+
test("malformed JSON falls back and strips the fence", () => {
|
|
41
|
+
const r = extractPlanTriage(`${PLAN}\n\n\`\`\`json\n{not json}\n\`\`\``);
|
|
42
|
+
assert.equal(r.parseFallback, true);
|
|
43
|
+
assert.equal(r.markdown, PLAN);
|
|
44
|
+
assert.equal(r.markdown.includes("```json"), false);
|
|
45
|
+
});
|
|
46
|
+
test("last json block wins; earlier json-in-prose is ignored", () => {
|
|
47
|
+
const early = '```json\n{"some":"config"}\n```';
|
|
48
|
+
const r = extractPlanTriage(`${PLAN}\n\n${early}\n\nMore prose\n\n${VALID_BLOCK}`);
|
|
49
|
+
assert.equal(r.parseFallback, false);
|
|
50
|
+
assert.equal(r.markdown.includes('{"some":"config"}'), true);
|
|
51
|
+
assert.equal(r.markdown.includes('"verdict"'), false);
|
|
52
|
+
});
|
|
53
|
+
test("valid json block that is not a triage shape falls back and strips the fence", () => {
|
|
54
|
+
const r = extractPlanTriage(`${PLAN}\n\n\`\`\`json\n{"foo":"bar"}\n\`\`\``);
|
|
55
|
+
assert.equal(r.parseFallback, true);
|
|
56
|
+
assert.equal(r.markdown, PLAN);
|
|
57
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UsageContent as UsageContentSchema } from "@agent-dealer/shared";
|
|
2
|
-
import { listArtifacts, listLineageRuns } from "./repository/runs.js";
|
|
2
|
+
import { listArtifacts, listLineageRuns, resolveBudgetForPhase } from "./repository/runs.js";
|
|
3
3
|
function parseUsage(contentJson) {
|
|
4
4
|
try {
|
|
5
5
|
return UsageContentSchema.parse(JSON.parse(contentJson));
|
|
@@ -17,13 +17,14 @@ function labelUsage(lineageIdx, phase, planCount, execCount) {
|
|
|
17
17
|
}
|
|
18
18
|
return lineageIdx === 1 ? "retry" : `retry ${lineageIdx}`;
|
|
19
19
|
}
|
|
20
|
-
export function buildLineageUsageSummary(run) {
|
|
20
|
+
export function buildLineageUsageSummary(run, opts) {
|
|
21
21
|
const lineageRuns = listLineageRuns(run);
|
|
22
22
|
const lines = [];
|
|
23
23
|
lineageRuns.forEach((lr, lineageIdx) => {
|
|
24
24
|
let planCount = 0;
|
|
25
25
|
let execCount = 0;
|
|
26
|
-
|
|
26
|
+
const arts = opts?.artifactsByRunId?.[lr.id] ?? listArtifacts(lr.id);
|
|
27
|
+
for (const art of arts) {
|
|
27
28
|
if (art.kind !== "usage" || !art.contentJson)
|
|
28
29
|
continue;
|
|
29
30
|
const usage = parseUsage(art.contentJson);
|
|
@@ -33,9 +34,12 @@ export function buildLineageUsageSummary(run) {
|
|
|
33
34
|
planCount++;
|
|
34
35
|
else
|
|
35
36
|
execCount++;
|
|
37
|
+
const resolved = resolveBudgetForPhase(lr, usage.phase);
|
|
36
38
|
lines.push({
|
|
37
39
|
label: labelUsage(lineageIdx, usage.phase, planCount, execCount),
|
|
38
40
|
usage,
|
|
41
|
+
maxTurns: usage.maxTurns ?? resolved?.maxTurns,
|
|
42
|
+
maxBudgetUsd: usage.maxBudgetUsd ?? resolved?.maxBudgetUsd,
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
45
|
});
|
|
@@ -44,6 +48,7 @@ export function buildLineageUsageSummary(run) {
|
|
|
44
48
|
inputTokens: acc.inputTokens + (usage.inputTokens ?? 0),
|
|
45
49
|
outputTokens: acc.outputTokens + (usage.outputTokens ?? 0),
|
|
46
50
|
durationMs: acc.durationMs + (usage.durationMs ?? 0),
|
|
47
|
-
|
|
51
|
+
numTurns: acc.numTurns + (usage.numTurns ?? 0),
|
|
52
|
+
}), { totalCostUsd: 0, inputTokens: 0, outputTokens: 0, durationMs: 0, numTurns: 0 });
|
|
48
53
|
return { lines, total };
|
|
49
54
|
}
|