@mycompbox/compbox-cli 1.19.0 → 1.20.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.
- package/.tsbuildinfo +1 -1
- package/commands/run.d.ts +12 -1
- package/commands/run.d.ts.map +1 -1
- package/commands/run.js +73 -28
- package/commands/run.js.map +1 -1
- package/index.js +409 -388
- package/package.json +1 -1
- package/utils/agent-invoker.d.ts +29 -44
- package/utils/agent-invoker.d.ts.map +1 -1
- package/utils/agent-invoker.js +89 -323
- package/utils/agent-invoker.js.map +1 -1
- package/utils/mailbox-client.d.ts +10 -1
- package/utils/mailbox-client.d.ts.map +1 -1
- package/utils/mailbox-client.js +26 -2
- package/utils/mailbox-client.js.map +1 -1
- package/utils/run-workspace.d.ts +6 -0
- package/utils/run-workspace.d.ts.map +1 -1
- package/utils/run-workspace.js +51 -17
- package/utils/run-workspace.js.map +1 -1
package/index.js
CHANGED
|
@@ -5555,76 +5555,10 @@ var INTERVIEW_DEFINITION = [
|
|
|
5555
5555
|
fields: ["alternatives"]
|
|
5556
5556
|
}
|
|
5557
5557
|
];
|
|
5558
|
-
|
|
5559
|
-
problemStatement: 60,
|
|
5560
|
-
proposedSolution: 100,
|
|
5561
|
-
impactAssessment: 40,
|
|
5562
|
-
description: 150
|
|
5563
|
-
};
|
|
5564
|
-
function buildInterviewSystemPrompt(definition = INTERVIEW_DEFINITION, seedContext) {
|
|
5565
|
-
const requiredSections = definition.filter((s) => s.required);
|
|
5566
|
-
const optionalSections = definition.filter((s) => !s.required);
|
|
5567
|
-
const renderSection = (s) => {
|
|
5568
|
-
const tag = s.required ? "REQUIRED" : "OPTIONAL \u2014 ask, but the user may decline";
|
|
5569
|
-
const followUps = s.followUps.map((q) => ` - ${q}`).join("\n");
|
|
5570
|
-
return `- ${s.title} (${tag}) [json keys: ${s.fields.join(", ")}]
|
|
5571
|
-
${s.purpose}
|
|
5572
|
-
${followUps}`;
|
|
5573
|
-
};
|
|
5574
|
-
const completionKeys = [
|
|
5575
|
-
"title",
|
|
5576
|
-
...interviewFieldKeysFor(definition),
|
|
5577
|
-
"description"
|
|
5578
|
-
];
|
|
5579
|
-
const completionShape = completionKeys.map((k) => `"${k}": "..."`).join(", ");
|
|
5580
|
-
const base = `You are the CompBox request-formulator, interviewing an employee to turn their idea into a well-formed Epic Request. Ask exactly ONE short, focused question at a time \u2014 never multiple questions in one message, never preamble or explanation.
|
|
5581
|
-
|
|
5582
|
-
Work through these interview sections in order. Cover EVERY required section; for optional sections, ask about them too, but if the employee has nothing to add or declines, move on and leave that field out (never invent or pad it):
|
|
5583
|
-
|
|
5584
|
-
REQUIRED sections (an Epic Request cannot omit these):
|
|
5585
|
-
${requiredSections.map(renderSection).join("\n")}
|
|
5586
|
-
|
|
5587
|
-
OPTIONAL-BUT-RECOMMENDED sections (ask about each; omit the field if the employee has nothing):
|
|
5588
|
-
${optionalSections.map(renderSection).join("\n")}
|
|
5589
|
-
|
|
5590
|
-
The employee's answers may be short or casual \u2014 that's fine, but when you write the final fields below, EXPAND them into complete, well-written prose using everything they told you across the whole conversation, not just a compressed one-liner. Each required field is scored by an automated quality rubric with hard minimums, so undershooting length is a real failure, not just a style nitpick:
|
|
5591
|
-
- problemStatement: at least ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words.
|
|
5592
|
-
- proposedSolution: at least ${String(INTERVIEW_FIELD_MIN_WORDS.proposedSolution)} words.
|
|
5593
|
-
- impactAssessment: at least ${String(INTERVIEW_FIELD_MIN_WORDS.impactAssessment)} words.
|
|
5594
|
-
- description: at least ${String(INTERVIEW_FIELD_MIN_WORDS.description)} words. This is a SEPARATE plain-text field (not shown to the employee) that stands alone as a full narrative summary of the whole request \u2014 restate the problem, the proposed solution, and the expected impact in flowing prose, as if writing the request from scratch for someone who hasn't seen the conversation.
|
|
5595
|
-
|
|
5596
|
-
Once you have gathered the required sections (and covered the optional ones), respond with ONLY a fenced JSON code block (no other text) in exactly this shape. Include a key for every optional field you gathered; omit any optional field the employee had nothing for:
|
|
5597
|
-
\`\`\`json
|
|
5598
|
-
{"done": true, ${completionShape}}
|
|
5599
|
-
\`\`\`
|
|
5600
|
-
|
|
5601
|
-
Until then, respond with ONLY your next question as plain text \u2014 no JSON, no labels, no numbering.`;
|
|
5602
|
-
if (!seedContext || seedContext.priorTranscript.length === 0) {
|
|
5603
|
-
return base;
|
|
5604
|
-
}
|
|
5605
|
-
return `${renderContinuationHeader(seedContext.priorTranscript)}
|
|
5606
|
-
|
|
5607
|
-
${base}`;
|
|
5608
|
-
}
|
|
5609
|
-
function renderContinuationHeader(priorTranscript) {
|
|
5610
|
-
const transcript = priorTranscript.map((t) => `${t.role === "agent" ? "You" : "Employee"}: ${t.content}`).join("\n");
|
|
5611
|
-
return `\u26A0\uFE0F CONTINUATION \u2014 this is NOT a fresh interview. You already interviewed this employee; below is that prior conversation, and an Epic Request already exists from it. Your job now is to ENRICH that same request, not restart it.
|
|
5612
|
-
|
|
5613
|
-
Prior conversation:
|
|
5614
|
-
${transcript}
|
|
5615
|
-
|
|
5616
|
-
How to continue:
|
|
5617
|
-
- Read the prior conversation first. Do NOT re-ask anything already answered there.
|
|
5618
|
-
- Use the section list below only as a checklist of what a complete request needs. Identify which sections are MISSING or THIN, and ask focused questions to fill ONLY those gaps \u2014 still one short question at a time.
|
|
5619
|
-
- If the prior conversation already covers everything well, go straight to the completion JSON.
|
|
5620
|
-
- When you emit the final completion JSON, MERGE the prior answers with whatever new detail you gathered \u2014 restate and expand every field using the WHOLE conversation (old + new). Never drop or shorten content that was already captured; the goal is a richer request than before, meeting the same length minimums.
|
|
5621
|
-
|
|
5622
|
-
`;
|
|
5623
|
-
}
|
|
5624
|
-
function interviewFieldKeysFor(definition) {
|
|
5558
|
+
function interviewFieldKeys() {
|
|
5625
5559
|
const seen = /* @__PURE__ */ new Set();
|
|
5626
5560
|
const out = [];
|
|
5627
|
-
for (const section of
|
|
5561
|
+
for (const section of INTERVIEW_DEFINITION) {
|
|
5628
5562
|
for (const f of section.fields) {
|
|
5629
5563
|
if (!seen.has(f)) {
|
|
5630
5564
|
seen.add(f);
|
|
@@ -5634,6 +5568,12 @@ function interviewFieldKeysFor(definition) {
|
|
|
5634
5568
|
}
|
|
5635
5569
|
return out;
|
|
5636
5570
|
}
|
|
5571
|
+
var INTERVIEW_FIELD_MIN_WORDS = {
|
|
5572
|
+
problemStatement: 60,
|
|
5573
|
+
proposedSolution: 100,
|
|
5574
|
+
impactAssessment: 40,
|
|
5575
|
+
description: 150
|
|
5576
|
+
};
|
|
5637
5577
|
|
|
5638
5578
|
// ../shared/dist/interview-completion.js
|
|
5639
5579
|
var TARGET_COMPLIANCE_TIER = "pass";
|
|
@@ -5759,6 +5699,218 @@ function looksLikeCompletionAttempt(text) {
|
|
|
5759
5699
|
}
|
|
5760
5700
|
var COMPLETION_REPAIR_INSTRUCTION = 'Your last message looked like a completion but I could NOT parse it as a valid JSON object with all required fields. Re-emit ONLY a single valid JSON object inside a ```json code fence \u2014 no other text \u2014 with every string value properly escaped (escape any internal double-quotes as \\" and any line breaks as \\n), and make sure title, problemStatement, proposedSolution, impactAssessment, and description are all present and non-empty:\n```json\n{"done": true, "title": "...", "problemStatement": "...", "proposedSolution": "...", "impactAssessment": "...", "description": "..."}\n```';
|
|
5761
5701
|
|
|
5702
|
+
// ../shared/dist/interview-conversation.js
|
|
5703
|
+
var CONVERSATION_QUALITY_ROUNDS = 2;
|
|
5704
|
+
var JSON_REPAIRS = 3;
|
|
5705
|
+
var STARTER = "Hi! I'd like to propose something.";
|
|
5706
|
+
var NUDGE = "(Please continue.)";
|
|
5707
|
+
function renderSection(s) {
|
|
5708
|
+
const tag = s.required ? "needed" : "nice to have";
|
|
5709
|
+
return `- ${s.title} (${tag}): ${s.purpose} For example: ${s.followUps.slice(0, 2).join(" ")}`;
|
|
5710
|
+
}
|
|
5711
|
+
function renderSaved(saved) {
|
|
5712
|
+
const fieldLines = Object.entries(saved.fields).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0).map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
5713
|
+
const score = saved.compliance ? `Its quality score is ${String(saved.compliance.overall)}/100 (${saved.compliance.tier}).` : "It has no quality score yet.";
|
|
5714
|
+
const issues = saved.compliance && saved.compliance.issues.length > 0 ? `
|
|
5715
|
+
What the score says is missing or weak:
|
|
5716
|
+
${saved.compliance.issues.slice(0, 8).map((i) => `- ${i.message}`).join("\n")}` : "";
|
|
5717
|
+
const locked = saved.status === "pending" ? "It can still be changed." : `Its status is "${saved.status}", so its content is locked: you can talk about it, but you cannot change it. Never emit the completion JSON for a locked request.`;
|
|
5718
|
+
return `THE REQUEST IS ALREADY SAVED. This conversation created the request "${saved.title}". ${score} ${locked}${issues}
|
|
5719
|
+
|
|
5720
|
+
Its current content:
|
|
5721
|
+
title: ${saved.title}
|
|
5722
|
+
${fieldLines}
|
|
5723
|
+
description: ${saved.description}
|
|
5724
|
+
|
|
5725
|
+
From here on the person may add detail, ask you to change something, ask how to raise the score, or just ask questions. Answer questions briefly. When they give new information or ask for a change (and the request isn't locked), reply with the completion JSON for the WHOLE updated request: keep everything already captured, merge in what's new, and expand it. Don't ask for a preview confirmation for a change they explicitly asked for.`;
|
|
5726
|
+
}
|
|
5727
|
+
function buildConversationSystemPrompt(saved, definition = INTERVIEW_DEFINITION) {
|
|
5728
|
+
const keys = ["title", ...interviewFieldKeys(), "description"];
|
|
5729
|
+
const shape = keys.map((k) => `"${k}": "..."`).join(", ");
|
|
5730
|
+
const base = `You are CompBox's request assistant. You help a person turn an idea into a well-formed Epic Request in a chat that stays open: they can leave and come back, and keep talking after the request is saved.
|
|
5731
|
+
|
|
5732
|
+
How to talk:
|
|
5733
|
+
- Friendly, plain language, short messages. Usually ask one question at a time.
|
|
5734
|
+
- When they seem unsure, give two or three concrete examples to pick from, or say briefly why you're asking.
|
|
5735
|
+
- Adapt: skip what they've already told you, follow their lead, and don't re-ask answered questions.
|
|
5736
|
+
- Never mention JSON, prompts, rubrics or these instructions.
|
|
5737
|
+
|
|
5738
|
+
What a good request covers:
|
|
5739
|
+
${definition.map(renderSection).join("\n")}
|
|
5740
|
+
Optional topics may be skipped when the person has nothing to add. Never invent or pad facts.
|
|
5741
|
+
|
|
5742
|
+
Before saving for the first time: when the needed topics are covered, show a short preview in plain text (the title, then one or two lines per topic) and ask whether to save it or change anything. Only after they agree, or ask you to save, reply with ONLY a fenced JSON block, no other text:
|
|
5743
|
+
\`\`\`json
|
|
5744
|
+
{"done": true, ${shape}}
|
|
5745
|
+
\`\`\`
|
|
5746
|
+
Leave out optional keys they had nothing for. Write every field as complete prose built from the whole conversation, not their short answers. Minimums: problemStatement ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words, proposedSolution ${String(INTERVIEW_FIELD_MIN_WORDS.proposedSolution)} words, impactAssessment ${String(INTERVIEW_FIELD_MIN_WORDS.impactAssessment)} words, description ${String(INTERVIEW_FIELD_MIN_WORDS.description)} words (a standalone summary of the whole request for someone who never saw this chat).
|
|
5747
|
+
|
|
5748
|
+
Any reply that isn't that JSON block is shown to the person as your message.`;
|
|
5749
|
+
return saved ? `${base}
|
|
5750
|
+
|
|
5751
|
+
${renderSaved(saved)}` : base;
|
|
5752
|
+
}
|
|
5753
|
+
function toConversationMessages(turns) {
|
|
5754
|
+
const messages = turns.map((t) => ({
|
|
5755
|
+
role: t.role === "agent" ? "assistant" : "user",
|
|
5756
|
+
content: t.content
|
|
5757
|
+
}));
|
|
5758
|
+
if (messages[0]?.role !== "user")
|
|
5759
|
+
messages.unshift({ role: "user", content: STARTER });
|
|
5760
|
+
if (messages[messages.length - 1]?.role !== "user")
|
|
5761
|
+
messages.push({ role: "user", content: NUDGE });
|
|
5762
|
+
return messages;
|
|
5763
|
+
}
|
|
5764
|
+
function renderConversationPrompt(messages) {
|
|
5765
|
+
const lines = messages.map((m) => `${m.role === "assistant" ? "You" : "Them"}: ${m.content}`);
|
|
5766
|
+
return `The conversation so far:
|
|
5767
|
+
|
|
5768
|
+
${lines.join("\n\n")}
|
|
5769
|
+
|
|
5770
|
+
Write your next reply to them.`;
|
|
5771
|
+
}
|
|
5772
|
+
function savedMessage(saved, created, followUp) {
|
|
5773
|
+
const verb = created ? "Saved your request" : "Updated your request";
|
|
5774
|
+
const c = saved.compliance;
|
|
5775
|
+
let text;
|
|
5776
|
+
if (!c) {
|
|
5777
|
+
text = `${verb} "${saved.title}". You can keep talking here to add detail or change anything.`;
|
|
5778
|
+
} else if (meetsComplianceTarget(c)) {
|
|
5779
|
+
text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100, ready for review. You can keep talking here to change anything.`;
|
|
5780
|
+
} else {
|
|
5781
|
+
const gaps = [...c.issues.filter((i) => i.tier === "critical"), ...c.issues.filter((i) => i.tier === "advisory")].slice(0, 4).map((i) => `- ${i.message}`).join("\n");
|
|
5782
|
+
text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100; 90 is the target.${gaps ? ` To raise it:
|
|
5783
|
+
${gaps}` : ""}
|
|
5784
|
+
|
|
5785
|
+
Tell me more here and I'll update it, or leave it as it is.`;
|
|
5786
|
+
}
|
|
5787
|
+
return followUp ? `${text}
|
|
5788
|
+
|
|
5789
|
+
${followUp}` : text;
|
|
5790
|
+
}
|
|
5791
|
+
async function runConversationStep(input, deps) {
|
|
5792
|
+
const system = buildConversationSystemPrompt(input.saved);
|
|
5793
|
+
const messages = toConversationMessages(input.turns);
|
|
5794
|
+
const locked = input.saved !== null && input.saved.status !== "pending";
|
|
5795
|
+
let requestId = input.saved?.id ?? null;
|
|
5796
|
+
let savedNow = null;
|
|
5797
|
+
let createdHere = false;
|
|
5798
|
+
let qualityRounds = 0;
|
|
5799
|
+
let repairs = 0;
|
|
5800
|
+
for (; ; ) {
|
|
5801
|
+
let reply;
|
|
5802
|
+
try {
|
|
5803
|
+
reply = await deps.reply(system, messages);
|
|
5804
|
+
} catch (error) {
|
|
5805
|
+
if (savedNow)
|
|
5806
|
+
return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow };
|
|
5807
|
+
const reason = error instanceof Error ? error.message : "Something went wrong reaching the AI.";
|
|
5808
|
+
return {
|
|
5809
|
+
kind: "error",
|
|
5810
|
+
text: `I couldn't reply just now: ${reason}
|
|
5811
|
+
|
|
5812
|
+
Send another message (for example "try again") when you're ready and I'll pick up where we left off.`
|
|
5813
|
+
};
|
|
5814
|
+
}
|
|
5815
|
+
const draft = extractCompletionJson(reply);
|
|
5816
|
+
if (draft) {
|
|
5817
|
+
if (locked) {
|
|
5818
|
+
messages.push({ role: "assistant", content: reply }, { role: "user", content: "That request is locked and can't be changed. Reply to me in plain text instead." });
|
|
5819
|
+
if (++repairs > JSON_REPAIRS)
|
|
5820
|
+
return { kind: "error", text: "This request is locked, so it can't be changed any more." };
|
|
5821
|
+
continue;
|
|
5822
|
+
}
|
|
5823
|
+
let saved;
|
|
5824
|
+
try {
|
|
5825
|
+
saved = await deps.save(requestId, draft);
|
|
5826
|
+
} catch (error) {
|
|
5827
|
+
const reason = error instanceof Error ? error.message : "the save failed";
|
|
5828
|
+
return { kind: "error", text: `I couldn't save the request: ${reason}
|
|
5829
|
+
|
|
5830
|
+
Reply "try again" and I'll retry.` };
|
|
5831
|
+
}
|
|
5832
|
+
createdHere ||= requestId === null;
|
|
5833
|
+
savedNow = { saved, created: createdHere };
|
|
5834
|
+
requestId = saved.id;
|
|
5835
|
+
if (meetsComplianceTarget(saved.compliance) || qualityRounds >= CONVERSATION_QUALITY_ROUNDS || !saved.compliance) {
|
|
5836
|
+
return { kind: "saved", text: savedMessage(saved, savedNow.created), ...savedNow };
|
|
5837
|
+
}
|
|
5838
|
+
qualityRounds++;
|
|
5839
|
+
messages.push({ role: "assistant", content: reply }, {
|
|
5840
|
+
role: "user",
|
|
5841
|
+
content: `${buildAuditFeedback(saved.compliance)}
|
|
5842
|
+
|
|
5843
|
+
The request IS already saved, and the person will be shown its score. If you ask them something, ask it as a way to improve the saved request; never say it hasn't been saved.`
|
|
5844
|
+
});
|
|
5845
|
+
continue;
|
|
5846
|
+
}
|
|
5847
|
+
if (looksLikeCompletionAttempt(reply)) {
|
|
5848
|
+
if (++repairs > JSON_REPAIRS) {
|
|
5849
|
+
return savedNow ? { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow } : { kind: "error", text: `I had trouble writing up the request. Reply "try again" and I'll have another go.` };
|
|
5850
|
+
}
|
|
5851
|
+
messages.push({ role: "assistant", content: reply }, { role: "user", content: COMPLETION_REPAIR_INSTRUCTION });
|
|
5852
|
+
continue;
|
|
5853
|
+
}
|
|
5854
|
+
const text = reply.trim();
|
|
5855
|
+
if (savedNow) {
|
|
5856
|
+
return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created, text || void 0), ...savedNow };
|
|
5857
|
+
}
|
|
5858
|
+
if (!text)
|
|
5859
|
+
return { kind: "error", text: "I didn't get a reply that time. Send another message and I'll try again." };
|
|
5860
|
+
return { kind: "message", text };
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
function savedContextFromRecord(record) {
|
|
5864
|
+
if (typeof record !== "object" || record === null)
|
|
5865
|
+
return null;
|
|
5866
|
+
const r = record;
|
|
5867
|
+
if (typeof r.id !== "string" || typeof r.title !== "string")
|
|
5868
|
+
return null;
|
|
5869
|
+
let structured = r.structuredDesc;
|
|
5870
|
+
if (typeof structured === "string") {
|
|
5871
|
+
try {
|
|
5872
|
+
structured = JSON.parse(structured);
|
|
5873
|
+
} catch {
|
|
5874
|
+
structured = null;
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
const fields = {};
|
|
5878
|
+
if (typeof structured === "object" && structured !== null) {
|
|
5879
|
+
for (const [key, value] of Object.entries(structured)) {
|
|
5880
|
+
if (typeof value === "string")
|
|
5881
|
+
fields[key] = value;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
let score = r.complianceScore;
|
|
5885
|
+
if (typeof score === "string") {
|
|
5886
|
+
try {
|
|
5887
|
+
score = JSON.parse(score);
|
|
5888
|
+
} catch {
|
|
5889
|
+
score = null;
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
return {
|
|
5893
|
+
id: r.id,
|
|
5894
|
+
title: r.title,
|
|
5895
|
+
status: typeof r.status === "string" ? r.status : "pending",
|
|
5896
|
+
description: typeof r.description === "string" ? r.description : "",
|
|
5897
|
+
fields,
|
|
5898
|
+
compliance: complianceFromRecord(score)
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
function complianceFromRecord(score) {
|
|
5902
|
+
if (typeof score !== "object" || score === null)
|
|
5903
|
+
return null;
|
|
5904
|
+
const s = score;
|
|
5905
|
+
if (typeof s.overall !== "number" || typeof s.tier !== "string")
|
|
5906
|
+
return null;
|
|
5907
|
+
const issues = Array.isArray(s.issues) ? s.issues.filter((i) => typeof i === "object" && i !== null).map((i) => ({
|
|
5908
|
+
message: typeof i.message === "string" ? i.message : typeof i.code === "string" ? i.code : "issue",
|
|
5909
|
+
tier: i.tier === "critical" ? "critical" : "advisory"
|
|
5910
|
+
})) : [];
|
|
5911
|
+
return { overall: s.overall, tier: s.tier, issues };
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5762
5914
|
// src/utils/config-store.ts
|
|
5763
5915
|
var CONFIG_DIR_ENV = "COMPBOX_CONFIG_DIR";
|
|
5764
5916
|
var PROJECT_DIR_ENV = "COMPBOX_PROJECT_DIR";
|
|
@@ -6360,12 +6512,18 @@ var MailboxClient = class {
|
|
|
6360
6512
|
* here. Cloud Run bills vCPU for the wall-clock life of a request, so a held
|
|
6361
6513
|
* connection kept one instance permanently awake at ~$64/month.
|
|
6362
6514
|
*/
|
|
6363
|
-
|
|
6515
|
+
/**
|
|
6516
|
+
* `runningIds`: jobs this daemon is already running, so the server hands out
|
|
6517
|
+
* new work instead of resuming one of them.
|
|
6518
|
+
*/
|
|
6519
|
+
async nextJob(machineToken, waitMs = 0, runningIds = []) {
|
|
6364
6520
|
let res;
|
|
6365
6521
|
try {
|
|
6366
6522
|
res = await this.http.get("/api/v1/machines/next-job", {
|
|
6367
6523
|
headers: { Authorization: `Bearer ${machineToken}` },
|
|
6368
|
-
|
|
6524
|
+
// steps=1: this daemon runs request conversations one reply at a time,
|
|
6525
|
+
// so it never wants back a job that is waiting on the person.
|
|
6526
|
+
params: { waitMs, steps: "1", ...runningIds.length > 0 && { running: runningIds.join(",") } }
|
|
6369
6527
|
});
|
|
6370
6528
|
} catch (error) {
|
|
6371
6529
|
throw toMailboxError(error, "next-job request failed");
|
|
@@ -6478,6 +6636,23 @@ var MailboxClient = class {
|
|
|
6478
6636
|
const data = res.data;
|
|
6479
6637
|
return data.data;
|
|
6480
6638
|
}
|
|
6639
|
+
/**
|
|
6640
|
+
* Tells CompBox that a request conversation saved its epic request. The
|
|
6641
|
+
* conversation stays open; this only links the job to the request.
|
|
6642
|
+
*/
|
|
6643
|
+
async markRequestSaved(machineToken, jobId, epicRequestId) {
|
|
6644
|
+
let res;
|
|
6645
|
+
try {
|
|
6646
|
+
res = await this.http.post(`/api/v1/jobs/${jobId}/saved`, { epicRequestId }, {
|
|
6647
|
+
headers: { Authorization: `Bearer ${machineToken}` }
|
|
6648
|
+
});
|
|
6649
|
+
} catch (error) {
|
|
6650
|
+
throw toMailboxError(error, "saved request failed");
|
|
6651
|
+
}
|
|
6652
|
+
if (res.status < 200 || res.status >= 300) {
|
|
6653
|
+
throw errorFromResponse(res, "saved failed");
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6481
6656
|
/** Reports what a run on the person's API key spent. */
|
|
6482
6657
|
async reportAiUsage(machineToken, jobId, usage) {
|
|
6483
6658
|
let res;
|
|
@@ -7784,11 +7959,7 @@ function parseClaudeJsonResult(stdout2, fallbackModel) {
|
|
|
7784
7959
|
|
|
7785
7960
|
// src/utils/agent-invoker.ts
|
|
7786
7961
|
var ANTHROPIC_MODEL = "claude-fable-5";
|
|
7787
|
-
var
|
|
7788
|
-
var MAX_QUALITY_ROUNDS = 4;
|
|
7789
|
-
var REPLY_POLL_INTERVAL_MS = 3e3;
|
|
7790
|
-
var REPLY_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
7791
|
-
var CLAUDE_CLI_TURN_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
7962
|
+
var CLAUDE_CLI_TURN_TIMEOUT_MS = 4 * 60 * 1e3;
|
|
7792
7963
|
function anthropicClientOptions(apiKey, routing) {
|
|
7793
7964
|
return routing.mode === "proxy" ? { apiKey, baseURL: routing.anthropicBaseUrl } : { apiKey };
|
|
7794
7965
|
}
|
|
@@ -7801,11 +7972,7 @@ var SUPPORTED_IMAGE_MEDIA_TYPES = [
|
|
|
7801
7972
|
"image/gif",
|
|
7802
7973
|
"image/webp"
|
|
7803
7974
|
];
|
|
7804
|
-
var MAX_IMAGES_PER_TURN = 8;
|
|
7805
7975
|
var MAX_IMAGE_BYTES_PER_TURN = 20 * 1024 * 1024;
|
|
7806
|
-
var defaultWarn = (message) => {
|
|
7807
|
-
console.warn(` \u26A0 ${message}`);
|
|
7808
|
-
};
|
|
7809
7976
|
function normalizeImageMediaType(mime) {
|
|
7810
7977
|
const base = (mime.split(";")[0] ?? "").trim().toLowerCase();
|
|
7811
7978
|
return SUPPORTED_IMAGE_MEDIA_TYPES.includes(base) ? base : null;
|
|
@@ -7813,67 +7980,10 @@ function normalizeImageMediaType(mime) {
|
|
|
7813
7980
|
function isSupportedImageMediaType(mime) {
|
|
7814
7981
|
return normalizeImageMediaType(mime) !== null;
|
|
7815
7982
|
}
|
|
7816
|
-
function selectImageAttachments(attachments) {
|
|
7817
|
-
if (!attachments || attachments.length === 0) return [];
|
|
7818
|
-
const selected = [];
|
|
7819
|
-
let declaredBytes = 0;
|
|
7820
|
-
for (const att of attachments) {
|
|
7821
|
-
if (selected.length >= MAX_IMAGES_PER_TURN) break;
|
|
7822
|
-
if (!isSupportedImageMediaType(att.mimeType)) continue;
|
|
7823
|
-
const size = typeof att.sizeBytes === "number" && att.sizeBytes > 0 ? att.sizeBytes : 0;
|
|
7824
|
-
if (declaredBytes + size > MAX_IMAGE_BYTES_PER_TURN) continue;
|
|
7825
|
-
declaredBytes += size;
|
|
7826
|
-
selected.push(att);
|
|
7827
|
-
}
|
|
7828
|
-
return selected;
|
|
7829
|
-
}
|
|
7830
7983
|
function imageAttachmentCount(attachments) {
|
|
7831
7984
|
if (!attachments || attachments.length === 0) return 0;
|
|
7832
7985
|
return attachments.filter((a) => isSupportedImageMediaType(a.mimeType)).length;
|
|
7833
7986
|
}
|
|
7834
|
-
async function fetchImageBlocks(attachments, warn = defaultWarn, fetchImpl = fetch) {
|
|
7835
|
-
const blocks = [];
|
|
7836
|
-
let totalBytes = 0;
|
|
7837
|
-
for (const att of attachments) {
|
|
7838
|
-
const mediaType = normalizeImageMediaType(att.mimeType);
|
|
7839
|
-
if (!mediaType) continue;
|
|
7840
|
-
try {
|
|
7841
|
-
const res = await fetchImpl(att.signedUrl);
|
|
7842
|
-
if (!res.ok) {
|
|
7843
|
-
warn(`Skipping image attachment ${att.id}: fetch failed (HTTP ${String(res.status)}).`);
|
|
7844
|
-
continue;
|
|
7845
|
-
}
|
|
7846
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
7847
|
-
if (bytes.byteLength === 0) {
|
|
7848
|
-
warn(`Skipping image attachment ${att.id}: empty response body.`);
|
|
7849
|
-
continue;
|
|
7850
|
-
}
|
|
7851
|
-
if (totalBytes + bytes.byteLength > MAX_IMAGE_BYTES_PER_TURN) {
|
|
7852
|
-
warn(`Skipping image attachment ${att.id}: exceeds the per-turn image byte budget.`);
|
|
7853
|
-
continue;
|
|
7854
|
-
}
|
|
7855
|
-
totalBytes += bytes.byteLength;
|
|
7856
|
-
blocks.push({
|
|
7857
|
-
type: "image",
|
|
7858
|
-
source: { type: "base64", media_type: mediaType, data: bytes.toString("base64") }
|
|
7859
|
-
});
|
|
7860
|
-
} catch (error) {
|
|
7861
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
7862
|
-
warn(`Skipping image attachment ${att.id}: ${message}.`);
|
|
7863
|
-
}
|
|
7864
|
-
}
|
|
7865
|
-
return blocks;
|
|
7866
|
-
}
|
|
7867
|
-
function buildApiUserContent(text, imageBlocks) {
|
|
7868
|
-
if (imageBlocks.length === 0) return text;
|
|
7869
|
-
return [...imageBlocks, { type: "text", text }];
|
|
7870
|
-
}
|
|
7871
|
-
async function apiUserTurnContent(turn, warn = defaultWarn, fetchImpl = fetch) {
|
|
7872
|
-
const selected = selectImageAttachments(turn.attachments);
|
|
7873
|
-
if (selected.length === 0) return turn.content;
|
|
7874
|
-
const blocks = await fetchImageBlocks(selected, warn, fetchImpl);
|
|
7875
|
-
return buildApiUserContent(turn.content, blocks);
|
|
7876
|
-
}
|
|
7877
7987
|
function appendAttachmentNote(text, imageCount) {
|
|
7878
7988
|
if (imageCount <= 0) return text;
|
|
7879
7989
|
const noun = imageCount === 1 ? "image" : "images";
|
|
@@ -7882,7 +7992,6 @@ function appendAttachmentNote(text, imageCount) {
|
|
|
7882
7992
|
|
|
7883
7993
|
${note}` : note;
|
|
7884
7994
|
}
|
|
7885
|
-
var SYSTEM_PROMPT = buildInterviewSystemPrompt();
|
|
7886
7995
|
function parseSeedPayload(payload) {
|
|
7887
7996
|
if (typeof payload !== "object" || payload === null) return null;
|
|
7888
7997
|
const p = payload;
|
|
@@ -7898,11 +8007,6 @@ function parseSeedPayload(payload) {
|
|
|
7898
8007
|
priorTranscript
|
|
7899
8008
|
};
|
|
7900
8009
|
}
|
|
7901
|
-
function resolveSystemPrompt(seed) {
|
|
7902
|
-
return seed ? buildInterviewSystemPrompt(INTERVIEW_DEFINITION, {
|
|
7903
|
-
priorTranscript: seed.priorTranscript
|
|
7904
|
-
}) : SYSTEM_PROMPT;
|
|
7905
|
-
}
|
|
7906
8010
|
async function submitAndScore(ctx, currentErId, draft) {
|
|
7907
8011
|
if (currentErId) {
|
|
7908
8012
|
const compliance = await updatePersonalEpicRequest(
|
|
@@ -7916,118 +8020,6 @@ async function submitAndScore(ctx, currentErId, draft) {
|
|
|
7916
8020
|
const created = await createPersonalEpicRequest(ctx.apiBaseUrl, ctx.userApiToken, draft);
|
|
7917
8021
|
return { erId: created.id, compliance: created.compliance };
|
|
7918
8022
|
}
|
|
7919
|
-
var MAX_JSON_REPAIRS = 3;
|
|
7920
|
-
function sleep(ms) {
|
|
7921
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
7922
|
-
}
|
|
7923
|
-
async function waitForUserReply(client, machineToken, jobId, turnsSoFar) {
|
|
7924
|
-
const deadline = Date.now() + REPLY_TIMEOUT_MS;
|
|
7925
|
-
while (Date.now() < deadline) {
|
|
7926
|
-
await sleep(REPLY_POLL_INTERVAL_MS);
|
|
7927
|
-
const job = await client.nextJob(machineToken, 0);
|
|
7928
|
-
if (job?.id !== jobId) continue;
|
|
7929
|
-
const last = job.turns[job.turns.length - 1];
|
|
7930
|
-
if (job.turns.length > turnsSoFar && last?.role === "user") {
|
|
7931
|
-
return job;
|
|
7932
|
-
}
|
|
7933
|
-
}
|
|
7934
|
-
return null;
|
|
7935
|
-
}
|
|
7936
|
-
async function invokeEpicRequestViaApi(job, ctx) {
|
|
7937
|
-
const routing = await resolveRouting();
|
|
7938
|
-
const anthropic = new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing));
|
|
7939
|
-
const seed = parseSeedPayload(job.payload);
|
|
7940
|
-
const systemPrompt = resolveSystemPrompt(seed);
|
|
7941
|
-
let currentErId = seed?.targetEpicRequestId ?? null;
|
|
7942
|
-
let qualityRounds = 0;
|
|
7943
|
-
let jsonRepairs = 0;
|
|
7944
|
-
const messages = [];
|
|
7945
|
-
for (const turn of job.turns) {
|
|
7946
|
-
if (turn.role === "agent") {
|
|
7947
|
-
messages.push({ role: "assistant", content: turn.content });
|
|
7948
|
-
} else {
|
|
7949
|
-
messages.push({ role: "user", content: await apiUserTurnContent(turn) });
|
|
7950
|
-
}
|
|
7951
|
-
}
|
|
7952
|
-
let turnsSoFar = job.turns.length;
|
|
7953
|
-
for (let i = 0; i < MAX_INTERVIEW_TURNS; i++) {
|
|
7954
|
-
let reply;
|
|
7955
|
-
try {
|
|
7956
|
-
const response = await anthropic.messages.create({
|
|
7957
|
-
model: ANTHROPIC_MODEL,
|
|
7958
|
-
max_tokens: 1024,
|
|
7959
|
-
system: systemPrompt,
|
|
7960
|
-
messages: messages.length > 0 ? messages : [{ role: "user", content: "Let's get started." }]
|
|
7961
|
-
});
|
|
7962
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
7963
|
-
reply = textBlock?.type === "text" ? textBlock.text : "";
|
|
7964
|
-
} catch (error) {
|
|
7965
|
-
const message = error instanceof Error ? error.message : "Anthropic API request failed";
|
|
7966
|
-
return { status: "failed", result: { error: `Agent invocation failed: ${message}` } };
|
|
7967
|
-
}
|
|
7968
|
-
const draft = extractCompletionJson(reply);
|
|
7969
|
-
if (draft) {
|
|
7970
|
-
let submitted;
|
|
7971
|
-
try {
|
|
7972
|
-
submitted = await submitAndScore(ctx, currentErId, draft);
|
|
7973
|
-
} catch (error) {
|
|
7974
|
-
const message = error instanceof Error ? error.message : "Failed to save the epic request";
|
|
7975
|
-
return { status: "failed", result: { error: message } };
|
|
7976
|
-
}
|
|
7977
|
-
currentErId = submitted.erId;
|
|
7978
|
-
const compliance = submitted.compliance;
|
|
7979
|
-
if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
|
|
7980
|
-
return { status: "done", result: { epicRequestId: currentErId } };
|
|
7981
|
-
}
|
|
7982
|
-
qualityRounds++;
|
|
7983
|
-
if (compliance !== null) {
|
|
7984
|
-
messages.push({ role: "assistant", content: reply });
|
|
7985
|
-
messages.push({ role: "user", content: buildAuditFeedback(compliance) });
|
|
7986
|
-
}
|
|
7987
|
-
continue;
|
|
7988
|
-
}
|
|
7989
|
-
if (looksLikeCompletionAttempt(reply)) {
|
|
7990
|
-
if (jsonRepairs >= MAX_JSON_REPAIRS) {
|
|
7991
|
-
return {
|
|
7992
|
-
status: "failed",
|
|
7993
|
-
result: {
|
|
7994
|
-
error: "The interview produced malformed completion data and could not be finalized after several attempts."
|
|
7995
|
-
}
|
|
7996
|
-
};
|
|
7997
|
-
}
|
|
7998
|
-
jsonRepairs++;
|
|
7999
|
-
messages.push({ role: "assistant", content: reply });
|
|
8000
|
-
messages.push({ role: "user", content: COMPLETION_REPAIR_INSTRUCTION });
|
|
8001
|
-
continue;
|
|
8002
|
-
}
|
|
8003
|
-
messages.push({ role: "assistant", content: reply });
|
|
8004
|
-
let updated;
|
|
8005
|
-
try {
|
|
8006
|
-
updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, reply);
|
|
8007
|
-
} catch (error) {
|
|
8008
|
-
const message = error instanceof Error ? error.message : "post-turn failed";
|
|
8009
|
-
return { status: "failed", result: { error: message } };
|
|
8010
|
-
}
|
|
8011
|
-
turnsSoFar = updated.turns.length;
|
|
8012
|
-
const answered = await waitForUserReply(ctx.mailboxClient, ctx.machineToken, job.id, turnsSoFar);
|
|
8013
|
-
if (!answered) {
|
|
8014
|
-
return {
|
|
8015
|
-
status: "failed",
|
|
8016
|
-
result: { error: "Timed out waiting for a reply \u2014 no response within 10 minutes." }
|
|
8017
|
-
};
|
|
8018
|
-
}
|
|
8019
|
-
const lastTurn = answered.turns[answered.turns.length - 1];
|
|
8020
|
-
messages.push({
|
|
8021
|
-
role: "user",
|
|
8022
|
-
content: lastTurn ? await apiUserTurnContent(lastTurn) : ""
|
|
8023
|
-
});
|
|
8024
|
-
turnsSoFar = answered.turns.length;
|
|
8025
|
-
}
|
|
8026
|
-
return currentErId ? { status: "done", result: { epicRequestId: currentErId } } : {
|
|
8027
|
-
status: "failed",
|
|
8028
|
-
result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` }
|
|
8029
|
-
};
|
|
8030
|
-
}
|
|
8031
8023
|
function claudeCliSessionCwd() {
|
|
8032
8024
|
const dir = join9(homedir5(), ".compbox", "claude-sessions");
|
|
8033
8025
|
try {
|
|
@@ -8038,11 +8030,11 @@ function claudeCliSessionCwd() {
|
|
|
8038
8030
|
}
|
|
8039
8031
|
function runClaudeCliTurn(prompt, opts) {
|
|
8040
8032
|
return new Promise((resolve3, reject) => {
|
|
8033
|
+
const model = opts.model === void 0 ? ANTHROPIC_MODEL : opts.model;
|
|
8041
8034
|
const args = [
|
|
8042
8035
|
"-p",
|
|
8043
8036
|
prompt,
|
|
8044
|
-
"--model",
|
|
8045
|
-
ANTHROPIC_MODEL,
|
|
8037
|
+
...model ? ["--model", model] : [],
|
|
8046
8038
|
"--output-format",
|
|
8047
8039
|
"json",
|
|
8048
8040
|
"--tools",
|
|
@@ -8116,91 +8108,78 @@ function runClaudeCliTurn(prompt, opts) {
|
|
|
8116
8108
|
});
|
|
8117
8109
|
});
|
|
8118
8110
|
}
|
|
8119
|
-
async function
|
|
8120
|
-
|
|
8121
|
-
|
|
8111
|
+
async function loadSavedRequest(apiBaseUrl, userApiToken, epicRequestId) {
|
|
8112
|
+
const res = await fetch(
|
|
8113
|
+
`${apiBaseUrl.replace(/\/+$/, "")}/api/v1/epic-requests/${encodeURIComponent(epicRequestId)}`,
|
|
8114
|
+
{ headers: { Authorization: `Bearer ${userApiToken}` } }
|
|
8115
|
+
);
|
|
8116
|
+
if (!res.ok) return null;
|
|
8117
|
+
const body = await res.json().catch(() => null);
|
|
8118
|
+
return savedContextFromRecord(body?.data);
|
|
8119
|
+
}
|
|
8120
|
+
function needsDefaultModel(error) {
|
|
8121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8122
|
+
return /requires usage credits|switch to another model|model.*not available|not have access to/i.test(message);
|
|
8123
|
+
}
|
|
8124
|
+
function savedRequestIdOf(job) {
|
|
8125
|
+
const result = job.result;
|
|
8126
|
+
if (typeof result?.epicRequestId === "string") return result.epicRequestId;
|
|
8127
|
+
return parseSeedPayload(job.payload)?.targetEpicRequestId ?? null;
|
|
8128
|
+
}
|
|
8129
|
+
async function invokeEpicRequestStep(job, ctx) {
|
|
8130
|
+
try {
|
|
8131
|
+
return await takeConversationStep(job, ctx);
|
|
8132
|
+
} catch (error) {
|
|
8133
|
+
const reason = error instanceof Error ? error.message : "something went wrong";
|
|
8134
|
+
await ctx.mailboxClient.postTurn(
|
|
8135
|
+
ctx.machineToken,
|
|
8136
|
+
job.id,
|
|
8137
|
+
`I couldn't continue: ${reason}
|
|
8138
|
+
|
|
8139
|
+
Send another message (for example "try again") and I'll pick up where we left off.`
|
|
8140
|
+
);
|
|
8141
|
+
return { status: "yield" };
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
async function takeConversationStep(job, ctx) {
|
|
8122
8145
|
const routing = await resolveRouting();
|
|
8123
|
-
const
|
|
8124
|
-
const
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
submitted = await submitAndScore(ctx, currentErId, draft);
|
|
8153
|
-
} catch (error) {
|
|
8154
|
-
const message = error instanceof Error ? error.message : "Failed to save the epic request";
|
|
8155
|
-
return { status: "failed", result: { error: message } };
|
|
8156
|
-
}
|
|
8157
|
-
currentErId = submitted.erId;
|
|
8158
|
-
const compliance = submitted.compliance;
|
|
8159
|
-
if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
|
|
8160
|
-
return { status: "done", result: { epicRequestId: currentErId } };
|
|
8161
|
-
}
|
|
8162
|
-
qualityRounds++;
|
|
8163
|
-
if (compliance !== null) {
|
|
8164
|
-
nextPrompt = buildAuditFeedback(compliance);
|
|
8165
|
-
}
|
|
8166
|
-
continue;
|
|
8167
|
-
}
|
|
8168
|
-
if (looksLikeCompletionAttempt(turn.text)) {
|
|
8169
|
-
if (jsonRepairs >= MAX_JSON_REPAIRS) {
|
|
8170
|
-
return {
|
|
8171
|
-
status: "failed",
|
|
8172
|
-
result: {
|
|
8173
|
-
error: "The interview produced malformed completion data and could not be finalized after several attempts."
|
|
8174
|
-
}
|
|
8175
|
-
};
|
|
8146
|
+
const requestId = savedRequestIdOf(job);
|
|
8147
|
+
const saved = requestId ? await loadSavedRequest(ctx.apiBaseUrl, ctx.userApiToken, requestId) : null;
|
|
8148
|
+
const turns = job.turns.map((t) => ({
|
|
8149
|
+
role: t.role,
|
|
8150
|
+
content: t.role === "user" ? appendAttachmentNote(t.content, imageAttachmentCount(t.attachments)) : t.content
|
|
8151
|
+
}));
|
|
8152
|
+
const anthropic = ctx.anthropicApiKey ? new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing)) : null;
|
|
8153
|
+
let cliModel = ANTHROPIC_MODEL;
|
|
8154
|
+
const outcome = await runConversationStep(
|
|
8155
|
+
{ turns, saved },
|
|
8156
|
+
{
|
|
8157
|
+
reply: async (system, messages) => {
|
|
8158
|
+
if (anthropic) {
|
|
8159
|
+
const response = await anthropic.messages.create({ model: ANTHROPIC_MODEL, max_tokens: 4096, system, messages });
|
|
8160
|
+
const text = response.content.find((b) => b.type === "text");
|
|
8161
|
+
return text?.type === "text" ? text.text : "";
|
|
8162
|
+
}
|
|
8163
|
+
const prompt = renderConversationPrompt(messages);
|
|
8164
|
+
try {
|
|
8165
|
+
return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: void 0, routing, model: cliModel })).text;
|
|
8166
|
+
} catch (error) {
|
|
8167
|
+
if (cliModel === null || !needsDefaultModel(error)) throw error;
|
|
8168
|
+
cliModel = null;
|
|
8169
|
+
return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: void 0, routing, model: null })).text;
|
|
8170
|
+
}
|
|
8171
|
+
},
|
|
8172
|
+
save: async (existingId, draft) => {
|
|
8173
|
+
const submitted = await submitAndScore(ctx, existingId, draft);
|
|
8174
|
+
return { id: submitted.erId, title: draft.title, compliance: submitted.compliance };
|
|
8176
8175
|
}
|
|
8177
|
-
jsonRepairs++;
|
|
8178
|
-
nextPrompt = COMPLETION_REPAIR_INSTRUCTION;
|
|
8179
|
-
continue;
|
|
8180
|
-
}
|
|
8181
|
-
let updated;
|
|
8182
|
-
try {
|
|
8183
|
-
updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, turn.text);
|
|
8184
|
-
} catch (error) {
|
|
8185
|
-
const message = error instanceof Error ? error.message : "post-turn failed";
|
|
8186
|
-
return { status: "failed", result: { error: message } };
|
|
8187
|
-
}
|
|
8188
|
-
turnsSoFar = updated.turns.length;
|
|
8189
|
-
const answered = await waitForUserReply(ctx.mailboxClient, ctx.machineToken, job.id, turnsSoFar);
|
|
8190
|
-
if (!answered) {
|
|
8191
|
-
return {
|
|
8192
|
-
status: "failed",
|
|
8193
|
-
result: { error: "Timed out waiting for a reply \u2014 no response within 10 minutes." }
|
|
8194
|
-
};
|
|
8195
8176
|
}
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8177
|
+
);
|
|
8178
|
+
if (outcome.kind === "saved") {
|
|
8179
|
+
await ctx.mailboxClient.markRequestSaved(ctx.machineToken, job.id, outcome.saved.id);
|
|
8199
8180
|
}
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` }
|
|
8203
|
-
};
|
|
8181
|
+
await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, outcome.text);
|
|
8182
|
+
return { status: "yield" };
|
|
8204
8183
|
}
|
|
8205
8184
|
var PLANNER_RUN_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
8206
8185
|
var PLANNER_HEARTBEAT_INTERVAL_MS = 20 * 1e3;
|
|
@@ -8834,6 +8813,17 @@ function githubCompareUrl(remoteUrl, baseBranch, branch) {
|
|
|
8834
8813
|
if (!m) return null;
|
|
8835
8814
|
return `https://github.com/${m[1] ?? ""}/${m[2] ?? ""}/compare/${encodeURIComponent(baseBranch)}...${encodeURIComponent(branch)}?expand=1`;
|
|
8836
8815
|
}
|
|
8816
|
+
var repoQueues = /* @__PURE__ */ new Map();
|
|
8817
|
+
function withRepoLock(projectDir, task) {
|
|
8818
|
+
const previous = repoQueues.get(projectDir) ?? Promise.resolve();
|
|
8819
|
+
const result = previous.catch(() => void 0).then(task);
|
|
8820
|
+
const tail = result.catch(() => void 0);
|
|
8821
|
+
repoQueues.set(projectDir, tail);
|
|
8822
|
+
void tail.then(() => {
|
|
8823
|
+
if (repoQueues.get(projectDir) === tail) repoQueues.delete(projectDir);
|
|
8824
|
+
});
|
|
8825
|
+
return result;
|
|
8826
|
+
}
|
|
8837
8827
|
var RunWorkspaceManager = class {
|
|
8838
8828
|
run;
|
|
8839
8829
|
now;
|
|
@@ -8857,6 +8847,31 @@ var RunWorkspaceManager = class {
|
|
|
8857
8847
|
}
|
|
8858
8848
|
/** Creates the branch + worktree for a run. Never touches the person's checkout. */
|
|
8859
8849
|
async prepare(projectDir, epicId) {
|
|
8850
|
+
const created = await withRepoLock(projectDir, () => this.createWorktree(projectDir, epicId));
|
|
8851
|
+
if (!created.ok) return created;
|
|
8852
|
+
const { workspace, notes } = created;
|
|
8853
|
+
const { dir, branch } = workspace;
|
|
8854
|
+
const install = installCommand2(dir, this.exists);
|
|
8855
|
+
if (install) {
|
|
8856
|
+
const [command, args] = install;
|
|
8857
|
+
const res = await this.run(command, args, dir);
|
|
8858
|
+
if (res.status !== 0) {
|
|
8859
|
+
await withRepoLock(projectDir, async () => {
|
|
8860
|
+
await this.git(["worktree", "remove", "--force", dir], projectDir);
|
|
8861
|
+
await this.git(["branch", "-D", branch], projectDir);
|
|
8862
|
+
});
|
|
8863
|
+
return {
|
|
8864
|
+
ok: false,
|
|
8865
|
+
error: [
|
|
8866
|
+
`Installing the project's dependencies failed (${command} ${args.join(" ")}): ${firstLine(res.stderr) || firstLine(res.stdout) || "unknown error"}`,
|
|
8867
|
+
"The run was not started. Check that the same command works in your project folder."
|
|
8868
|
+
].join("\n")
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
8872
|
+
return { ok: true, workspace, notes };
|
|
8873
|
+
}
|
|
8874
|
+
async createWorktree(projectDir, epicId) {
|
|
8860
8875
|
const notes = [];
|
|
8861
8876
|
const remotes = (await this.git(["remote"], projectDir)).stdout.split("\n").map((r) => r.trim()).filter(Boolean);
|
|
8862
8877
|
const remote = remotes.includes("origin") ? "origin" : remotes[0] ?? null;
|
|
@@ -8911,26 +8926,13 @@ ${untracked.stdout}`.split("\n").map((p) => p.trim()).filter((p) => p && !p.ends
|
|
|
8911
8926
|
notes.push(`Couldn't copy ${path3} into the run workspace.`);
|
|
8912
8927
|
}
|
|
8913
8928
|
}
|
|
8914
|
-
const install = installCommand2(dir, this.exists);
|
|
8915
|
-
if (install) {
|
|
8916
|
-
const [command, args] = install;
|
|
8917
|
-
const res = await this.run(command, args, dir);
|
|
8918
|
-
if (res.status !== 0) {
|
|
8919
|
-
await this.git(["worktree", "remove", "--force", dir], projectDir);
|
|
8920
|
-
await this.git(["branch", "-D", branch], projectDir);
|
|
8921
|
-
return {
|
|
8922
|
-
ok: false,
|
|
8923
|
-
error: [
|
|
8924
|
-
`Installing the project's dependencies failed (${command} ${args.join(" ")}): ${firstLine(res.stderr) || firstLine(res.stdout) || "unknown error"}`,
|
|
8925
|
-
"The run was not started. Check that the same command works in your project folder."
|
|
8926
|
-
].join("\n")
|
|
8927
|
-
};
|
|
8928
|
-
}
|
|
8929
|
-
}
|
|
8930
8929
|
return { ok: true, workspace: { dir, projectDir, branch, baseRef, baseBranch, remote }, notes };
|
|
8931
8930
|
}
|
|
8932
8931
|
/** Commits, pushes and opens the pull request; then cleans up the worktree. */
|
|
8933
|
-
|
|
8932
|
+
finish(input) {
|
|
8933
|
+
return withRepoLock(input.workspace.projectDir, () => this.finishLocked(input));
|
|
8934
|
+
}
|
|
8935
|
+
async finishLocked(input) {
|
|
8934
8936
|
const { workspace: ws } = input;
|
|
8935
8937
|
const notes = [];
|
|
8936
8938
|
if ((await this.git(["status", "--porcelain"], ws.dir)).stdout.trim()) {
|
|
@@ -9142,9 +9144,20 @@ var POLL_WAIT_MS = 0;
|
|
|
9142
9144
|
var IDLE_POLL_INTERVAL_MS = 15e3;
|
|
9143
9145
|
var ACTIVE_POLL_INTERVAL_MS = 2e3;
|
|
9144
9146
|
var ACTIVE_WINDOW_MS = 6e4;
|
|
9145
|
-
|
|
9147
|
+
var CONVERSATION_POLL_INTERVAL_MS = 3e3;
|
|
9148
|
+
var CONVERSATION_WINDOW_MS = 10 * 6e4;
|
|
9149
|
+
function idleDelay(now, lastJobAt, lastConversationAt = 0) {
|
|
9146
9150
|
const warm = lastJobAt > 0 && now - lastJobAt < ACTIVE_WINDOW_MS;
|
|
9147
|
-
|
|
9151
|
+
if (warm) return ACTIVE_POLL_INTERVAL_MS;
|
|
9152
|
+
const talking = lastConversationAt > 0 && now - lastConversationAt < CONVERSATION_WINDOW_MS;
|
|
9153
|
+
return talking ? CONVERSATION_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
|
|
9154
|
+
}
|
|
9155
|
+
var DEFAULT_MAX_PARALLEL_JOBS = 3;
|
|
9156
|
+
var MAX_JOBS_ENV = "COMPBOX_MAX_JOBS";
|
|
9157
|
+
function maxParallelJobs(env = process.env) {
|
|
9158
|
+
const raw = Number.parseInt(env[MAX_JOBS_ENV] ?? "", 10);
|
|
9159
|
+
if (!Number.isFinite(raw) || raw < 1) return DEFAULT_MAX_PARALLEL_JOBS;
|
|
9160
|
+
return Math.min(raw, 10);
|
|
9148
9161
|
}
|
|
9149
9162
|
var BACKOFF_BASE_MS = 1e3;
|
|
9150
9163
|
var BACKOFF_MAX_MS = 3e4;
|
|
@@ -9152,7 +9165,7 @@ var HEARTBEAT_INTERVAL_MS = 2e4;
|
|
|
9152
9165
|
function runtimeHeartbeatDue(nowMs, lastRuntimeSentMs, intervalMs = HEARTBEAT_INTERVAL_MS) {
|
|
9153
9166
|
return nowMs - lastRuntimeSentMs >= intervalMs;
|
|
9154
9167
|
}
|
|
9155
|
-
function
|
|
9168
|
+
function sleep(ms) {
|
|
9156
9169
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
9157
9170
|
}
|
|
9158
9171
|
function backoffDelay(consecutiveErrors) {
|
|
@@ -9307,6 +9320,10 @@ async function handleStartExecution(ctx, job) {
|
|
|
9307
9320
|
async function runInvokerAndComplete(ctx, job, invoke) {
|
|
9308
9321
|
try {
|
|
9309
9322
|
const outcome = await invoke();
|
|
9323
|
+
if (outcome.status === "yield") {
|
|
9324
|
+
console.log(chalk9.gray(` Job ${job.id}: replied; waiting for the person.`));
|
|
9325
|
+
return;
|
|
9326
|
+
}
|
|
9310
9327
|
await ctx.client.completeJob(ctx.machineToken, job.id, outcome.status, outcome.result);
|
|
9311
9328
|
console.log(
|
|
9312
9329
|
outcome.status === "done" ? chalk9.green(` Job ${job.id} completed: ${JSON.stringify(outcome.result)}`) : chalk9.yellow(` Job ${job.id} failed: ${JSON.stringify(outcome.result)}`)
|
|
@@ -9331,37 +9348,22 @@ async function handleJob(ctx, job) {
|
|
|
9331
9348
|
\u2192 Claimed job ${job.id} (type: ${job.type}, status: ${job.status})`)
|
|
9332
9349
|
);
|
|
9333
9350
|
const userApiToken = ctx.userApiToken;
|
|
9334
|
-
|
|
9335
|
-
|
|
9351
|
+
if (job.type === "epic_request" && userApiToken) {
|
|
9352
|
+
let anthropicApiKey;
|
|
9336
9353
|
if (ctx.aiMode === "api-key") {
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
ctx,
|
|
9342
|
-
job,
|
|
9343
|
-
() => invokeEpicRequestViaApi(job, {
|
|
9344
|
-
mailboxClient: ctx.client,
|
|
9345
|
-
machineToken: ctx.machineToken,
|
|
9346
|
-
apiBaseUrl: ctx.apiBaseUrl,
|
|
9347
|
-
userApiToken,
|
|
9348
|
-
anthropicApiKey
|
|
9349
|
-
})
|
|
9350
|
-
);
|
|
9351
|
-
return;
|
|
9352
|
-
}
|
|
9353
|
-
} else if (ctx.aiMode === "claude-subscription") {
|
|
9354
|
-
console.log(
|
|
9355
|
-
chalk9.gray(" Running the request-formulator interview (claude-subscription mode, local `claude` CLI)...")
|
|
9356
|
-
);
|
|
9354
|
+
anthropicApiKey = (await retrieveSecret(AI_KEY_ACCOUNT)).value ?? void 0;
|
|
9355
|
+
}
|
|
9356
|
+
if (ctx.aiMode === "claude-subscription" || anthropicApiKey) {
|
|
9357
|
+
console.log(chalk9.gray(` Replying in the request conversation (${anthropicApiKey ? "api mode" : "local claude CLI"})...`));
|
|
9357
9358
|
await runInvokerAndComplete(
|
|
9358
9359
|
ctx,
|
|
9359
9360
|
job,
|
|
9360
|
-
() =>
|
|
9361
|
+
() => invokeEpicRequestStep(job, {
|
|
9361
9362
|
mailboxClient: ctx.client,
|
|
9362
9363
|
machineToken: ctx.machineToken,
|
|
9363
9364
|
apiBaseUrl: ctx.apiBaseUrl,
|
|
9364
|
-
userApiToken
|
|
9365
|
+
userApiToken,
|
|
9366
|
+
...anthropicApiKey && { anthropicApiKey }
|
|
9365
9367
|
})
|
|
9366
9368
|
);
|
|
9367
9369
|
return;
|
|
@@ -9459,6 +9461,9 @@ var runCommand = new Command8("run").description("Start the Agent Runner daemon
|
|
|
9459
9461
|
`
|
|
9460
9462
|
)
|
|
9461
9463
|
);
|
|
9464
|
+
const maxJobs = maxParallelJobs();
|
|
9465
|
+
console.log(chalk9.gray(` Runs up to ${String(maxJobs)} job${maxJobs === 1 ? "" : "s"} at once (${MAX_JOBS_ENV} to change).
|
|
9466
|
+
`));
|
|
9462
9467
|
const runtimeProbe = () => probeAgentRuntime();
|
|
9463
9468
|
let agentRuntime = await safeProbeAgentRuntime(runtimeProbe, void 0);
|
|
9464
9469
|
if (agentRuntime) {
|
|
@@ -9489,10 +9494,14 @@ Received ${signal}, shutting down...`));
|
|
|
9489
9494
|
});
|
|
9490
9495
|
let consecutiveErrors = 0;
|
|
9491
9496
|
let lastJobAt = 0;
|
|
9497
|
+
let lastConversationAt = 0;
|
|
9498
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
9492
9499
|
do {
|
|
9493
9500
|
let job = null;
|
|
9494
9501
|
try {
|
|
9495
|
-
|
|
9502
|
+
if (inFlight.size < maxJobs) {
|
|
9503
|
+
job = await client.nextJob(machineToken, waitMs, [...inFlight.keys()]);
|
|
9504
|
+
}
|
|
9496
9505
|
consecutiveErrors = 0;
|
|
9497
9506
|
} catch (error) {
|
|
9498
9507
|
consecutiveErrors += 1;
|
|
@@ -9506,12 +9515,21 @@ Received ${signal}, shutting down...`));
|
|
|
9506
9515
|
);
|
|
9507
9516
|
console.error(chalk9.gray(` Reconnecting in ${String(Math.round(delay / 1e3))}s...`));
|
|
9508
9517
|
if (options.once) break;
|
|
9509
|
-
await
|
|
9518
|
+
await sleep(delay);
|
|
9510
9519
|
continue;
|
|
9511
9520
|
}
|
|
9521
|
+
if (job && inFlight.has(job.id)) job = null;
|
|
9512
9522
|
if (job) {
|
|
9513
|
-
|
|
9514
|
-
|
|
9523
|
+
const jobId = job.id;
|
|
9524
|
+
const isConversation = job.type === "epic_request";
|
|
9525
|
+
const work = handleJob(jobContext, job).catch((error) => {
|
|
9526
|
+
console.error(chalk9.red(` Job ${jobId} stopped unexpectedly: ${error instanceof Error ? error.message : String(error)}`));
|
|
9527
|
+
}).finally(() => {
|
|
9528
|
+
inFlight.delete(jobId);
|
|
9529
|
+
lastJobAt = Date.now();
|
|
9530
|
+
if (isConversation) lastConversationAt = lastJobAt;
|
|
9531
|
+
});
|
|
9532
|
+
inFlight.set(jobId, work);
|
|
9515
9533
|
}
|
|
9516
9534
|
if (runtimeHeartbeatDue(Date.now(), lastRuntimeSentAt)) {
|
|
9517
9535
|
const cycle = await runHeartbeatCycle({
|
|
@@ -9528,9 +9546,12 @@ Received ${signal}, shutting down...`));
|
|
|
9528
9546
|
lastRuntimeSentAt = Date.now();
|
|
9529
9547
|
}
|
|
9530
9548
|
}
|
|
9531
|
-
if (options.once)
|
|
9549
|
+
if (options.once) {
|
|
9550
|
+
await Promise.all(inFlight.values());
|
|
9551
|
+
break;
|
|
9552
|
+
}
|
|
9532
9553
|
if (!job) {
|
|
9533
|
-
await
|
|
9554
|
+
await sleep(idleDelay(Date.now(), lastJobAt, lastConversationAt));
|
|
9534
9555
|
}
|
|
9535
9556
|
} while (running);
|
|
9536
9557
|
console.log(chalk9.gray("Agent Runner stopped."));
|
|
@@ -9793,7 +9814,7 @@ machinesCommand.command("rm <id>").description("Remove (revoke) a paired machine
|
|
|
9793
9814
|
// src/index.ts
|
|
9794
9815
|
function resolveCliVersion() {
|
|
9795
9816
|
if (true) {
|
|
9796
|
-
return "1.
|
|
9817
|
+
return "1.20.0";
|
|
9797
9818
|
}
|
|
9798
9819
|
const require2 = createRequire(import.meta.url);
|
|
9799
9820
|
const pkg = require2("../package.json");
|