agent-dealer 0.1.4 → 0.1.6

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 (40) hide show
  1. package/bundle/server/dist/queue/dispatcher.js +26 -3
  2. package/bundle/server/dist/queue/plan-delegation.js +27 -0
  3. package/bundle/server/dist/queue/plan-delegation.test.js +61 -0
  4. package/bundle/server/dist/queue/result-qa.js +66 -0
  5. package/bundle/server/dist/queue/result-qa.test.js +146 -0
  6. package/bundle/server/dist/repository/result-qa.js +37 -0
  7. package/bundle/server/dist/routes/index.js +22 -2
  8. package/bundle/server/dist/runners/claude-args.js +6 -1
  9. package/bundle/server/dist/runners/claude.js +13 -5
  10. package/bundle/server/dist/runners/claude.test.js +18 -1
  11. package/bundle/server/dist/runners/persist.js +1 -1
  12. package/bundle/server/dist/runners/prompts.js +95 -1
  13. package/bundle/server/dist/runners/prompts.test.js +92 -1
  14. package/bundle/server/dist/runners/qa.js +41 -0
  15. package/bundle/server/dist/runners/run-context.js +15 -0
  16. package/bundle/server/dist/usage-summary.js +10 -3
  17. package/bundle/server/dist/usage-summary.test.js +33 -0
  18. package/bundle/server/package.json +1 -1
  19. package/bundle/server/static-ui/assets/index-BAANKt1U.js +78 -0
  20. package/bundle/server/static-ui/assets/index-CJbiq5Vq.css +1 -0
  21. package/bundle/server/static-ui/index.html +2 -2
  22. package/bundle/shared/dist/index.d.ts +76 -67
  23. package/bundle/shared/dist/index.js +7 -1
  24. package/bundle/shared/dist/plan-triage.d.ts +4 -4
  25. package/bundle/shared/dist/plan-triage.js +2 -2
  26. package/bundle/shared/dist/result-qa.d.ts +50 -0
  27. package/bundle/shared/dist/result-qa.js +27 -0
  28. package/bundle/shared/dist/result-qa.test.d.ts +1 -0
  29. package/bundle/shared/dist/result-qa.test.js +63 -0
  30. package/bundle/shared/package.json +1 -1
  31. package/dist/index.js +28 -0
  32. package/dist/npm-registry.d.ts +3 -0
  33. package/dist/npm-registry.js +22 -0
  34. package/dist/update-check.d.ts +5 -0
  35. package/dist/update-check.js +123 -0
  36. package/dist/upgrade.d.ts +6 -0
  37. package/dist/upgrade.js +59 -0
  38. package/package.json +1 -1
  39. package/bundle/server/static-ui/assets/index-Cc-h06U6.js +0 -78
  40. package/bundle/server/static-ui/assets/index-DKnME33I.css +0 -1
@@ -1,6 +1,6 @@
1
1
  import { getLatestArtifact } from "../repository/runs.js";
2
2
  import { documentOutputHint } from "./persist.js";
3
- import { humanFeedbackText, appendPlanRetrySections, appendExecutionRetrySections, retryContext, } from "./run-context.js";
3
+ import { humanFeedbackText, appendPlanRetrySections, appendExecutionRetrySections, retryContext, reviewQaPairs, } from "./run-context.js";
4
4
  export function workspaceForRun(run) {
5
5
  return run.repo ?? run.artifactWorkspace ?? process.cwd();
6
6
  }
@@ -45,6 +45,13 @@ function outboundDraftContractSection() {
45
45
  "- Do not perform the send — the human approves and the server delivers verbatim.",
46
46
  ].join("\n");
47
47
  }
48
+ function reviewQaSections(run) {
49
+ const pairs = reviewQaPairs(run);
50
+ if (pairs.length === 0)
51
+ return [];
52
+ const lines = pairs.flatMap((p) => [`Q: ${p.question}`, `A: ${p.answer}`, ``]);
53
+ return [`## Review Q&A`, `The human asked about the prior result. Honor these answers.`, ...lines];
54
+ }
48
55
  export function buildExecutionContinuationPrompt(run) {
49
56
  const ctx = retryContext(run);
50
57
  const parts = [
@@ -64,6 +71,7 @@ export function buildExecutionContinuationPrompt(run) {
64
71
  parts.push(`## Human feedback`, feedback, ``);
65
72
  }
66
73
  }
74
+ parts.push(...reviewQaSections(run));
67
75
  if (run.deckId) {
68
76
  parts.push(`Use Agent Deck: bind_workspace({ deckId: "${run.deckId}", workspaceRoot: "${workspaceForRun(run)}" })`);
69
77
  if (run.playbookId) {
@@ -104,6 +112,8 @@ export function buildExecutionPrompt(run) {
104
112
  }
105
113
  }
106
114
  parts.push(...planAnswersSections(run));
115
+ parts.push(...planDelegationSections(run));
116
+ parts.push(...reviewQaSections(run));
107
117
  if (run.taskCategory === "content" || run.taskCategory === "research") {
108
118
  parts.push(`## Deliverable`, documentOutputHint(run), `You MUST write the file using your Write/edit tool — do not only print markdown in chat.`, ``);
109
119
  }
@@ -155,6 +165,35 @@ export function buildReflectPrompt(run, opts) {
155
165
  }
156
166
  return parts.join("\n");
157
167
  }
168
+ export function buildQaPrompt(run, question, opts) {
169
+ const parts = [
170
+ `The human reviewing your finished work has a question about it.`,
171
+ ``,
172
+ `IMPORTANT:`,
173
+ `- Answer the question. Do NOT modify anything — no files, no edits, no commands, no sends.`,
174
+ `- You may read files to check your own work.`,
175
+ `- Answer concisely in markdown. No preamble, no restating the question.`,
176
+ `- If you do not know, say so plainly rather than guessing.`,
177
+ ``,
178
+ ];
179
+ if (opts.grounded) {
180
+ parts.push(`## Task`, taskText(run), ``);
181
+ }
182
+ else {
183
+ parts.push(`Your original session is gone, so the relevant context is reproduced below.`, ``, `## Task`, taskText(run), ``);
184
+ const plan = artifactMarkdown("approved_plan", run.id);
185
+ if (plan)
186
+ parts.push(`## Approved plan`, plan, ``);
187
+ const result = artifactMarkdown("execution_result", run.id);
188
+ if (result)
189
+ parts.push(`## Execution outcome`, result, ``);
190
+ const doc = artifactMarkdown("document", run.id);
191
+ if (doc)
192
+ parts.push(`## Deliverable`, doc, ``);
193
+ }
194
+ parts.push(`## Question`, question);
195
+ return parts.join("\n");
196
+ }
158
197
  function hasOutboundSendGate(category) {
159
198
  return category === "communication" || category === "email";
160
199
  }
@@ -200,6 +239,61 @@ function planAnswersSections(run) {
200
239
  return [];
201
240
  }
202
241
  }
242
+ function planDelegationSections(run) {
243
+ const ansArt = getLatestArtifact(run.id, "plan_answers");
244
+ if (!ansArt?.contentJson)
245
+ return [];
246
+ try {
247
+ const ans = JSON.parse(ansArt.contentJson);
248
+ if (ans.outcome !== "delegated")
249
+ return [];
250
+ const triArt = getLatestArtifact(run.id, "plan_triage");
251
+ const questions = triArt?.contentJson
252
+ ? JSON.parse(triArt.contentJson).questions
253
+ : [];
254
+ if (questions.length === 0)
255
+ return [];
256
+ const lines = questions.map((q) => {
257
+ const options = q.options.map((o) => o.label).join(" | ");
258
+ return `- ${q.question} (options: ${options})`;
259
+ });
260
+ return [
261
+ "## Unanswered plan questions",
262
+ "The reviewer chose to proceed without answering these. Use your best judgment.",
263
+ ...lines,
264
+ "",
265
+ ];
266
+ }
267
+ catch {
268
+ return [];
269
+ }
270
+ }
271
+ export function buildPlanFeedbackPrompt(run, feedback) {
272
+ return [
273
+ "The human reviewed your plan and wants a revision. Revise the plan accordingly — do not execute anything.",
274
+ "",
275
+ "## Task",
276
+ taskText(run),
277
+ "",
278
+ "## Human feedback",
279
+ feedback.trim(),
280
+ "",
281
+ planTriageContractSection(run.taskCategory),
282
+ ].join("\n");
283
+ }
284
+ export function buildPlanEditedReplanPrompt(run, editedMarkdown) {
285
+ return [
286
+ "The human edited the plan directly. Start from their edits and produce a refined plan — do not execute anything.",
287
+ "",
288
+ "## Task",
289
+ taskText(run),
290
+ "",
291
+ "## Human-edited plan",
292
+ editedMarkdown.trim(),
293
+ "",
294
+ planTriageContractSection(run.taskCategory),
295
+ ].join("\n");
296
+ }
203
297
  export function buildPlanRevisePrompt(run, questions, answers) {
204
298
  const qa = answers.map((a) => {
205
299
  const q = questions.find((x) => x.id === a.questionId);
@@ -9,7 +9,7 @@ const { migrate } = await import("../db/index.js");
9
9
  const { BUILTIN_AGENT_CLAUDE_ID } = await import("@agent-dealer/shared");
10
10
  const { updateAgent } = await import("../repository/agents.js");
11
11
  const { addArtifact, createRun } = await import("../repository/runs.js");
12
- const { buildExecutionPrompt, buildPlanPrompt, buildPlanRevisePrompt } = await import("./prompts.js");
12
+ const { buildExecutionPrompt, buildPlanPrompt, buildPlanEditedReplanPrompt, buildPlanFeedbackPrompt, buildPlanRevisePrompt, buildQaPrompt } = await import("./prompts.js");
13
13
  const QUESTIONS = [
14
14
  {
15
15
  id: "q1",
@@ -75,6 +75,18 @@ test("execution prompt omits answers section when none exist", () => {
75
75
  addArtifact(run.id, "approved_plan", { markdown: "# Plan" }, "human");
76
76
  assert.doesNotMatch(buildExecutionPrompt(run), /Human answers to plan questions/);
77
77
  });
78
+ test("feedback replan prompt includes human comments and contract", () => {
79
+ const run = makeRun();
80
+ const prompt = buildPlanFeedbackPrompt(run, "Add integration tests and skip the migration step.");
81
+ assert.match(prompt, /Add integration tests/);
82
+ assert.match(prompt, /"verdict"/);
83
+ });
84
+ test("edited replan prompt includes human-edited markdown", () => {
85
+ const run = makeRun();
86
+ const prompt = buildPlanEditedReplanPrompt(run, "# Revised plan\n\n1. Ship feature");
87
+ assert.match(prompt, /Revised plan/);
88
+ assert.match(prompt, /Ship feature/);
89
+ });
78
90
  test("revise prompt pairs each answer with its question and re-states the contract", () => {
79
91
  const run = makeRun();
80
92
  const prompt = buildPlanRevisePrompt(run, QUESTIONS, [{ questionId: "q1", freeText: "Use flat files instead" }]);
@@ -82,6 +94,85 @@ test("revise prompt pairs each answer with its question and re-states the contra
82
94
  assert.match(prompt, /Use flat files instead/);
83
95
  assert.match(prompt, /"verdict"/);
84
96
  });
97
+ test("delegated plan answers surface the unanswered questions to the executor", () => {
98
+ const run = makeRun();
99
+ addArtifact(run.id, "draft_plan", { markdown: "# Plan" }, "agent");
100
+ addArtifact(run.id, "plan_triage", {
101
+ verdict: "needs_review", rationale: "r", questions: QUESTIONS, parseFallback: false, consumed: true,
102
+ }, "agent");
103
+ addArtifact(run.id, "approved_plan", { markdown: "# Plan" }, "human");
104
+ addArtifact(run.id, "plan_answers", { answers: [], outcome: "delegated", answeredAt: new Date().toISOString() }, "human");
105
+ const prompt = buildExecutionPrompt(run);
106
+ assert.match(prompt, /## Unanswered plan questions/);
107
+ assert.match(prompt, /Which storage backend\?/);
108
+ assert.match(prompt, /SQLite/);
109
+ assert.match(prompt, /best judgment/i);
110
+ assert.doesNotMatch(prompt, /## Human answers to plan questions/);
111
+ });
112
+ test("execution prompt renders answered review Q&A from the lineage parent", () => {
113
+ const parent = makeRun();
114
+ addArtifact(parent.id, "approved_plan", { markdown: "# Plan" }, "human");
115
+ addArtifact(parent.id, "result_qa", {
116
+ exchangeId: "x1",
117
+ question: "Did you run the tests?",
118
+ answer: "Yes, all 12 pass.",
119
+ status: "answered",
120
+ sessionResumed: true,
121
+ askedAt: "2026-07-08T00:00:00.000Z",
122
+ answeredAt: "2026-07-08T00:01:00.000Z",
123
+ }, "agent");
124
+ addArtifact(parent.id, "result_qa", {
125
+ exchangeId: "x2",
126
+ question: "Pending one",
127
+ status: "pending",
128
+ sessionResumed: true,
129
+ askedAt: "2026-07-08T00:02:00.000Z",
130
+ }, "human");
131
+ const retry = createRun({
132
+ title: "Prompt test task",
133
+ taskCategory: "other",
134
+ status: "plan_approved",
135
+ agentId: BUILTIN_AGENT_CLAUDE_ID,
136
+ }, { lineageId: parent.lineageId ?? parent.id });
137
+ addArtifact(retry.id, "approved_plan", { markdown: "# Plan" }, "human");
138
+ addArtifact(retry.id, "feedback", { markdown: "Tighten the summary" }, "human");
139
+ const prompt = buildExecutionPrompt(retry);
140
+ assert.match(prompt, /## Review Q&A/);
141
+ assert.match(prompt, /Did you run the tests\?/);
142
+ assert.match(prompt, /Yes, all 12 pass\./);
143
+ assert.doesNotMatch(prompt, /Pending one/);
144
+ });
145
+ test("execution prompt omits the Q&A section when there are no answered exchanges", () => {
146
+ const run = makeRun();
147
+ addArtifact(run.id, "approved_plan", { markdown: "# Plan" }, "human");
148
+ assert.doesNotMatch(buildExecutionPrompt(run), /## Review Q&A/);
149
+ });
150
+ test("grounded qa prompt asks the question without re-stating artifacts", () => {
151
+ const run = makeRun();
152
+ addArtifact(run.id, "approved_plan", { markdown: "# Plan\n1. Use SQLite" }, "human");
153
+ const prompt = buildQaPrompt(run, "Why SQLite?", { grounded: true });
154
+ assert.match(prompt, /Why SQLite\?/);
155
+ assert.match(prompt, /Do NOT modify anything/i);
156
+ assert.doesNotMatch(prompt, /## Approved plan/);
157
+ });
158
+ test("ungrounded qa prompt rebuilds context from artifacts", () => {
159
+ const run = makeRun();
160
+ addArtifact(run.id, "approved_plan", { markdown: "# Plan\n1. Use SQLite" }, "human");
161
+ addArtifact(run.id, "execution_result", { phase: "execute", exitCode: 0, resultText: "Wrote the doc", isError: false }, "agent");
162
+ addArtifact(run.id, "document", { path: "/tmp/x.md", title: "x", markdown: "# Deliverable body" }, "agent");
163
+ const prompt = buildQaPrompt(run, "Why SQLite?", { grounded: false });
164
+ assert.match(prompt, /## Approved plan/);
165
+ assert.match(prompt, /## Execution outcome/);
166
+ assert.match(prompt, /## Deliverable/);
167
+ assert.match(prompt, /# Deliverable body/);
168
+ assert.match(prompt, /Why SQLite\?/);
169
+ });
170
+ test("qa prompt never asks for a json block or an outbound draft", () => {
171
+ const run = makeRun();
172
+ const prompt = buildQaPrompt(run, "What did you check?", { grounded: true });
173
+ assert.doesNotMatch(prompt, /Outbound actions/);
174
+ assert.doesNotMatch(prompt, /"verdict"/);
175
+ });
85
176
  test("execution prompt includes outbound draft contract", () => {
86
177
  const run = makeRun();
87
178
  addArtifact(run.id, "approved_plan", { markdown: "# Plan" }, "human");
@@ -0,0 +1,41 @@
1
+ import { QA_PHASE_BUDGET } from "@agent-dealer/shared";
2
+ import { runClaude, runCursor } from "./claude.js";
3
+ import { buildQaPrompt } from "./prompts.js";
4
+ import { extractResultIsError, extractResultText, extractUsage, parseNdjson } from "./stream-json.js";
5
+ /** One read-only Q&A turn against a finished run. Resumes the execute session when we still have it. */
6
+ export async function runQa(run, question, resumeSessionId) {
7
+ let result;
8
+ const runtime = run.runtime ?? "claude_code";
9
+ const executeModel = run.executeModel ?? undefined;
10
+ if (runtime === "cursor_local") {
11
+ const prompt = buildQaPrompt(run, question, { grounded: Boolean(resumeSessionId) });
12
+ result = await runCursor(run, "qa", executeModel, {
13
+ promptOverride: prompt,
14
+ ...(resumeSessionId ? { resumeSessionId } : {}),
15
+ });
16
+ // Resume can fail when the local Cursor chat has expired. Retry ungrounded from artifacts.
17
+ if (result.exitCode !== 0 && resumeSessionId) {
18
+ result = await runCursor(run, "qa", executeModel, {
19
+ promptOverride: buildQaPrompt(run, question, { grounded: false }),
20
+ });
21
+ }
22
+ }
23
+ else {
24
+ result = await runClaude(run, "qa", executeModel, {
25
+ promptOverride: buildQaPrompt(run, question, { grounded: Boolean(resumeSessionId) }),
26
+ ...(resumeSessionId ? { resumeSessionId } : {}),
27
+ });
28
+ }
29
+ const events = parseNdjson(result.transcript);
30
+ const usage = extractUsage(events, "qa", runtime);
31
+ usage.maxTurns = QA_PHASE_BUDGET.maxTurns;
32
+ usage.maxBudgetUsd = QA_PHASE_BUDGET.maxBudgetUsd;
33
+ const answer = extractResultText(events)?.trim() ?? "";
34
+ const failed = result.exitCode !== 0 || extractResultIsError(events) || !answer;
35
+ return {
36
+ ok: !failed,
37
+ answer,
38
+ error: failed ? `qa exited ${result.exitCode}${answer ? "" : " with no answer"}` : undefined,
39
+ usage,
40
+ };
41
+ }
@@ -1,4 +1,5 @@
1
1
  import { getLatestArtifact, getLineageParentRun, listArtifacts } from "../repository/runs.js";
2
+ import { listQaExchanges } from "../repository/result-qa.js";
2
3
  const EXCERPT = 2000;
3
4
  function artifactMarkdown(runId, kind) {
4
5
  const art = getLatestArtifact(runId, kind);
@@ -64,6 +65,20 @@ export function lineageParentExecuteSessionId(run) {
64
65
  }
65
66
  return null;
66
67
  }
68
+ /** Answered result Q&A from this run and its lineage parent — a retry inherits the discussion. */
69
+ export function reviewQaPairs(run) {
70
+ const parent = getLineageParentRun(run);
71
+ const runIds = parent ? [parent.id, run.id] : [run.id];
72
+ const pairs = [];
73
+ for (const id of runIds) {
74
+ for (const e of listQaExchanges(id)) {
75
+ if (e.status === "answered" && e.answer) {
76
+ pairs.push({ question: e.question, answer: e.answer });
77
+ }
78
+ }
79
+ }
80
+ return pairs;
81
+ }
67
82
  function priorExecuteTraceTail(run, max = 12) {
68
83
  const parent = getLineageParentRun(run);
69
84
  if (!parent)
@@ -8,10 +8,13 @@ function parseUsage(contentJson) {
8
8
  return null;
9
9
  }
10
10
  }
11
- function labelUsage(lineageIdx, phase, planCount, execCount) {
11
+ function labelUsage(lineageIdx, phase, planCount, execCount, qaCount) {
12
12
  if (phase === "plan") {
13
13
  return planCount > 1 ? `plan ${planCount}` : "plan";
14
14
  }
15
+ if (phase === "qa") {
16
+ return qaCount > 1 ? `Q&A ${qaCount}` : "Q&A";
17
+ }
15
18
  if (lineageIdx === 0) {
16
19
  return execCount > 1 ? `execute ${execCount}` : "execute";
17
20
  }
@@ -23,6 +26,7 @@ export function buildLineageUsageSummary(run, opts) {
23
26
  lineageRuns.forEach((lr, lineageIdx) => {
24
27
  let planCount = 0;
25
28
  let execCount = 0;
29
+ let qaCount = 0;
26
30
  const arts = opts?.artifactsByRunId?.[lr.id] ?? listArtifacts(lr.id);
27
31
  for (const art of arts) {
28
32
  if (art.kind !== "usage" || !art.contentJson)
@@ -32,11 +36,14 @@ export function buildLineageUsageSummary(run, opts) {
32
36
  continue;
33
37
  if (usage.phase === "plan")
34
38
  planCount++;
39
+ else if (usage.phase === "qa")
40
+ qaCount++;
35
41
  else
36
42
  execCount++;
37
- const resolved = resolveBudgetForPhase(lr, usage.phase);
43
+ // qa has no configurable budget — its cap is snapshotted on the artifact
44
+ const resolved = usage.phase === "qa" ? null : resolveBudgetForPhase(lr, usage.phase);
38
45
  lines.push({
39
- label: labelUsage(lineageIdx, usage.phase, planCount, execCount),
46
+ label: labelUsage(lineageIdx, usage.phase, planCount, execCount, qaCount),
40
47
  usage,
41
48
  maxTurns: usage.maxTurns ?? resolved?.maxTurns,
42
49
  maxBudgetUsd: usage.maxBudgetUsd ?? resolved?.maxBudgetUsd,
@@ -0,0 +1,33 @@
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-usage-"));
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, getRun, transitionRun } = await import("./repository/runs.js");
12
+ const { buildLineageUsageSummary } = await import("./usage-summary.js");
13
+ before(() => {
14
+ migrate();
15
+ updateAgent(BUILTIN_AGENT_CLAUDE_ID, { workspaceRoot: process.env.AGENT_DEALER_HOME });
16
+ });
17
+ test("qa usage artifacts get their own label and do not inflate the execute count", () => {
18
+ const run = createRun({
19
+ title: "Usage test",
20
+ taskCategory: "other",
21
+ status: "plan_approved",
22
+ agentId: BUILTIN_AGENT_CLAUDE_ID,
23
+ });
24
+ transitionRun(run.id, "running");
25
+ transitionRun(run.id, "review");
26
+ addArtifact(run.id, "usage", { phase: "execute", runtime: "claude_code", totalCostUsd: 1 }, "agent");
27
+ addArtifact(run.id, "usage", { phase: "qa", runtime: "claude_code", totalCostUsd: 0.1 }, "agent");
28
+ addArtifact(run.id, "usage", { phase: "qa", runtime: "claude_code", totalCostUsd: 0.1 }, "agent");
29
+ const summary = buildLineageUsageSummary(getRun(run.id));
30
+ const labels = summary.lines.map((l) => l.label);
31
+ assert.deepEqual(labels, ["execute", "Q&A", "Q&A 2"]);
32
+ assert.equal(Math.round(summary.total.totalCostUsd * 100) / 100, 1.2);
33
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/server",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "files": [