@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/package.json
CHANGED
package/utils/agent-invoker.d.ts
CHANGED
|
@@ -2,33 +2,19 @@
|
|
|
2
2
|
* Agent invoker (ENG-E23 / ENG-122-2) — the daemon's "heart": actually runs a
|
|
3
3
|
* claimed job instead of immediately reporting it as unimplemented.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
5
|
+
* Request conversations (`epic_request` jobs) run one AI reply per claim
|
|
6
|
+
* (invokeEpicRequestStep, on the shared runConversationStep): the reply is
|
|
7
|
+
* posted, the job is left waiting on the person, and the server hands it back
|
|
8
|
+
* when they write again. Nothing waits in between, so a conversation never
|
|
9
|
+
* times out and stays open after the request is saved. Two ways to reply:
|
|
8
10
|
* - `api`: Anthropic Messages API, directly (Anthropic API key, billed
|
|
9
|
-
* per-token).
|
|
11
|
+
* per-token).
|
|
10
12
|
* - `claude-subscription`: shells out to the local `claude` CLI
|
|
11
13
|
* non-interactively, using whichever account is already logged in on
|
|
12
14
|
* this machine (personal Pro/Max or a company Team seat) — no API key
|
|
13
|
-
* ever touches CompBox.
|
|
15
|
+
* ever touches CompBox.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
* - The interview is driven by the shared, declarative INTERVIEW_DEFINITION
|
|
17
|
-
* (@mycompbox/shared, ENG-143) so the machine/mailbox interview reaches the
|
|
18
|
-
* SAME depth as the CLI request-formulator: it covers all seven sections
|
|
19
|
-
* (problem, solution, impact, success metrics, target audience, technical
|
|
20
|
-
* context, alternatives) rather than only the 3 required fields. The system
|
|
21
|
-
* prompt is rendered from that definition (buildInterviewSystemPrompt), and
|
|
22
|
-
* the completion object carries the required fields plus any optional ones
|
|
23
|
-
* the employee provided; there is no shortened variant left in this path.
|
|
24
|
-
*
|
|
25
|
-
* Turn relay: the mailbox has no "wait for a new turn" primitive for a
|
|
26
|
-
* machine caller (next-job's "resume active job" path returns immediately
|
|
27
|
-
* with whatever the job's current state is — see jobService.ts tryClaimOnce).
|
|
28
|
-
* So after posting an agent question, this module polls next-job on a short
|
|
29
|
-
* client-side interval until it observes a new trailing user turn, with a
|
|
30
|
-
* bounded overall timeout so an abandoned interview eventually fails cleanly
|
|
31
|
-
* instead of polling forever.
|
|
17
|
+
* `start_execution` jobs run the planner/orchestrator headless (below).
|
|
32
18
|
*/
|
|
33
19
|
import Anthropic from '@anthropic-ai/sdk';
|
|
34
20
|
import { spawn } from 'node:child_process';
|
|
@@ -98,6 +84,13 @@ export interface InvokeResult {
|
|
|
98
84
|
status: 'done' | 'failed';
|
|
99
85
|
result: Record<string, unknown>;
|
|
100
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A request conversation step that left the job open (waiting on the person):
|
|
89
|
+
* the job must NOT be completed.
|
|
90
|
+
*/
|
|
91
|
+
export interface YieldResult {
|
|
92
|
+
status: 'yield';
|
|
93
|
+
}
|
|
101
94
|
/**
|
|
102
95
|
* ENG-149 — parse a claimed job's `payload` as an interview-CONTINUATION seed
|
|
103
96
|
* (the ENG-148 dispatch shape), or return null for a normal fresh-start job
|
|
@@ -107,11 +100,6 @@ export interface InvokeResult {
|
|
|
107
100
|
* jobB). Exported for unit testing.
|
|
108
101
|
*/
|
|
109
102
|
export declare function parseSeedPayload(payload: unknown): InterviewSeedPayload | null;
|
|
110
|
-
/**
|
|
111
|
-
* ENG-149 — the interview system prompt for a job: a seed-aware RESUME/ENRICH
|
|
112
|
-
* prompt for a continuation, otherwise the module-level fresh-start prompt.
|
|
113
|
-
*/
|
|
114
|
-
export declare function resolveSystemPrompt(seed: InterviewSeedPayload | null): string;
|
|
115
103
|
/**
|
|
116
104
|
* Submit the current draft and read back the server-computed compliance score
|
|
117
105
|
* (bug #2 — audit-aware finishing). Fresh interview: the FIRST submit
|
|
@@ -128,30 +116,27 @@ export declare function submitAndScore(ctx: {
|
|
|
128
116
|
compliance: ComplianceSummary | null;
|
|
129
117
|
}>;
|
|
130
118
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* fields, then creates a personal-scope epic request via the real API
|
|
134
|
-
* (POST /api/v1/me/epic-requests) using the user's own stored token.
|
|
119
|
+
* True when the person's Claude plan can't use the requested model (for
|
|
120
|
+
* example Fable needs usage credits on some plans): retry on their default.
|
|
135
121
|
*/
|
|
136
|
-
export declare function
|
|
137
|
-
mailboxClient: MailboxClient;
|
|
138
|
-
machineToken: string;
|
|
139
|
-
apiBaseUrl: string;
|
|
140
|
-
userApiToken: string;
|
|
141
|
-
anthropicApiKey: string;
|
|
142
|
-
}): Promise<InvokeResult>;
|
|
122
|
+
export declare function needsDefaultModel(error: unknown): boolean;
|
|
143
123
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
124
|
+
* Takes one step of a request conversation on this computer: the AI's reply to
|
|
125
|
+
* the person's latest message, which may save or update their request. Then
|
|
126
|
+
* the job is left waiting on the person (never completed), so the chat stays
|
|
127
|
+
* open: there is no reply timeout, and the computer is free for other work in
|
|
128
|
+
* the meantime. The server hands the job back when they write again.
|
|
129
|
+
*
|
|
130
|
+
* `anthropicApiKey` set → the Anthropic API; otherwise the local `claude` CLI
|
|
131
|
+
* on the person's own Claude sign-in.
|
|
148
132
|
*/
|
|
149
|
-
export declare function
|
|
133
|
+
export declare function invokeEpicRequestStep(job: MailboxJob, ctx: {
|
|
150
134
|
mailboxClient: MailboxClient;
|
|
151
135
|
machineToken: string;
|
|
152
136
|
apiBaseUrl: string;
|
|
153
137
|
userApiToken: string;
|
|
154
|
-
|
|
138
|
+
anthropicApiKey?: string;
|
|
139
|
+
}): Promise<YieldResult>;
|
|
155
140
|
/**
|
|
156
141
|
* Per-run wall-clock budget for the headless planner child (H4). Deliberately
|
|
157
142
|
* FAR larger than the interview's {@link CLAUDE_CLI_TURN_TIMEOUT_MS} (~2 min):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-invoker.d.ts","sourceRoot":"","sources":["../../src/utils/agent-invoker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"agent-invoker.d.ts","sourceRoot":"","sources":["../../src/utils/agent-invoker.ts"],"names":[],"mappings":"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"}
|
package/utils/agent-invoker.js
CHANGED
|
@@ -2,40 +2,26 @@
|
|
|
2
2
|
* Agent invoker (ENG-E23 / ENG-122-2) — the daemon's "heart": actually runs a
|
|
3
3
|
* claimed job instead of immediately reporting it as unimplemented.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
5
|
+
* Request conversations (`epic_request` jobs) run one AI reply per claim
|
|
6
|
+
* (invokeEpicRequestStep, on the shared runConversationStep): the reply is
|
|
7
|
+
* posted, the job is left waiting on the person, and the server hands it back
|
|
8
|
+
* when they write again. Nothing waits in between, so a conversation never
|
|
9
|
+
* times out and stays open after the request is saved. Two ways to reply:
|
|
8
10
|
* - `api`: Anthropic Messages API, directly (Anthropic API key, billed
|
|
9
|
-
* per-token).
|
|
11
|
+
* per-token).
|
|
10
12
|
* - `claude-subscription`: shells out to the local `claude` CLI
|
|
11
13
|
* non-interactively, using whichever account is already logged in on
|
|
12
14
|
* this machine (personal Pro/Max or a company Team seat) — no API key
|
|
13
|
-
* ever touches CompBox.
|
|
15
|
+
* ever touches CompBox.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
* - The interview is driven by the shared, declarative INTERVIEW_DEFINITION
|
|
17
|
-
* (@mycompbox/shared, ENG-143) so the machine/mailbox interview reaches the
|
|
18
|
-
* SAME depth as the CLI request-formulator: it covers all seven sections
|
|
19
|
-
* (problem, solution, impact, success metrics, target audience, technical
|
|
20
|
-
* context, alternatives) rather than only the 3 required fields. The system
|
|
21
|
-
* prompt is rendered from that definition (buildInterviewSystemPrompt), and
|
|
22
|
-
* the completion object carries the required fields plus any optional ones
|
|
23
|
-
* the employee provided; there is no shortened variant left in this path.
|
|
24
|
-
*
|
|
25
|
-
* Turn relay: the mailbox has no "wait for a new turn" primitive for a
|
|
26
|
-
* machine caller (next-job's "resume active job" path returns immediately
|
|
27
|
-
* with whatever the job's current state is — see jobService.ts tryClaimOnce).
|
|
28
|
-
* So after posting an agent question, this module polls next-job on a short
|
|
29
|
-
* client-side interval until it observes a new trailing user turn, with a
|
|
30
|
-
* bounded overall timeout so an abandoned interview eventually fails cleanly
|
|
31
|
-
* instead of polling forever.
|
|
17
|
+
* `start_execution` jobs run the planner/orchestrator headless (below).
|
|
32
18
|
*/
|
|
33
19
|
import Anthropic from '@anthropic-ai/sdk';
|
|
34
20
|
import { spawn, spawnSync } from 'node:child_process';
|
|
35
21
|
import { mkdirSync } from 'node:fs';
|
|
36
22
|
import { homedir } from 'node:os';
|
|
37
23
|
import { join } from 'node:path';
|
|
38
|
-
import { resolveRouting,
|
|
24
|
+
import { resolveRouting, extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, renderConversationPrompt, runConversationStep, savedContextFromRecord, } from '@mycompbox/shared';
|
|
39
25
|
// Completion parsing lives in @mycompbox/shared so CompBox's server-side
|
|
40
26
|
// (API-key) interview finishes the same way; re-exported for existing callers.
|
|
41
27
|
export { extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, };
|
|
@@ -47,22 +33,8 @@ import { keyRunArgs, keyRunEnv, parseClaudeJsonResult } from './key-run.js';
|
|
|
47
33
|
// quality loss. Opus/Sonnet are deliberately NOT used here — pxpipe passes
|
|
48
34
|
// Opus through uncompressed, defeating the token savings.
|
|
49
35
|
const ANTHROPIC_MODEL = 'claude-fable-5';
|
|
50
|
-
// Turn budget for the interview loop. Raised from 8: a full 7-section interview
|
|
51
|
-
// (problem / solution / impact + success metrics / audience / technical context /
|
|
52
|
-
// alternatives) plus follow-ups AND audit-quality rounds needs more headroom
|
|
53
|
-
// than 8 (deep interviews were being cut off mid-way — bug #3).
|
|
54
|
-
const MAX_INTERVIEW_TURNS = 18;
|
|
55
|
-
// Audit-quality gate (bug #2): the interview must reach the `pass` tier
|
|
56
|
-
// (>=90, zero critical issues) before it may finish. After the model says
|
|
57
|
-
// "done", we submit, read the server-computed compliance score, and if it is
|
|
58
|
-
// not yet `pass` we feed the failing rules back and keep going. This bounds how
|
|
59
|
-
// many re-submit/re-audit rounds we allow before finalizing with the best draft
|
|
60
|
-
// (so we never loop forever, and never strand the request at a failing score).
|
|
61
|
-
const MAX_QUALITY_ROUNDS = 4;
|
|
62
|
-
const REPLY_POLL_INTERVAL_MS = 3_000;
|
|
63
|
-
const REPLY_TIMEOUT_MS = 10 * 60 * 1_000; // 10 minutes
|
|
64
36
|
/** Per-turn wall-clock budget for a single `claude -p` child process. */
|
|
65
|
-
const CLAUDE_CLI_TURN_TIMEOUT_MS =
|
|
37
|
+
const CLAUDE_CLI_TURN_TIMEOUT_MS = 4 * 60 * 1_000; // 4 minutes: a full write-up can take a while
|
|
66
38
|
/**
|
|
67
39
|
* pxpipe routing (ENG-136). Anthropic SDK options carrying the proxy `baseURL`
|
|
68
40
|
* when routed through pxpipe, otherwise just the API key. Pure — unit tested.
|
|
@@ -228,13 +200,6 @@ export function appendAttachmentNote(text, imageCount) {
|
|
|
228
200
|
const note = `[${String(imageCount)} ${noun} attached — not shown in subscription mode; vision is handled when the daemon runs in API mode]`;
|
|
229
201
|
return text.trim().length > 0 ? `${text}\n\n${note}` : note;
|
|
230
202
|
}
|
|
231
|
-
/**
|
|
232
|
-
* The interview system prompt, rendered from the shared, declarative
|
|
233
|
-
* INTERVIEW_DEFINITION (ENG-143). This is the single wiring point that gives
|
|
234
|
-
* the machine/mailbox interview the same section depth as the CLI
|
|
235
|
-
* request-formulator — there is no shortened hardcoded variant anymore.
|
|
236
|
-
*/
|
|
237
|
-
const SYSTEM_PROMPT = buildInterviewSystemPrompt();
|
|
238
203
|
/**
|
|
239
204
|
* ENG-149 — parse a claimed job's `payload` as an interview-CONTINUATION seed
|
|
240
205
|
* (the ENG-148 dispatch shape), or return null for a normal fresh-start job
|
|
@@ -263,17 +228,6 @@ export function parseSeedPayload(payload) {
|
|
|
263
228
|
priorTranscript,
|
|
264
229
|
};
|
|
265
230
|
}
|
|
266
|
-
/**
|
|
267
|
-
* ENG-149 — the interview system prompt for a job: a seed-aware RESUME/ENRICH
|
|
268
|
-
* prompt for a continuation, otherwise the module-level fresh-start prompt.
|
|
269
|
-
*/
|
|
270
|
-
export function resolveSystemPrompt(seed) {
|
|
271
|
-
return seed
|
|
272
|
-
? buildInterviewSystemPrompt(INTERVIEW_DEFINITION, {
|
|
273
|
-
priorTranscript: seed.priorTranscript,
|
|
274
|
-
})
|
|
275
|
-
: SYSTEM_PROMPT;
|
|
276
|
-
}
|
|
277
231
|
/**
|
|
278
232
|
* Submit the current draft and read back the server-computed compliance score
|
|
279
233
|
* (bug #2 — audit-aware finishing). Fresh interview: the FIRST submit
|
|
@@ -290,162 +244,6 @@ export async function submitAndScore(ctx, currentErId, draft) {
|
|
|
290
244
|
const created = await createPersonalEpicRequest(ctx.apiBaseUrl, ctx.userApiToken, draft);
|
|
291
245
|
return { erId: created.id, compliance: created.compliance };
|
|
292
246
|
}
|
|
293
|
-
/** How many times to ask the model to fix a malformed completion before failing. */
|
|
294
|
-
const MAX_JSON_REPAIRS = 3;
|
|
295
|
-
function sleep(ms) {
|
|
296
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Polls next-job (the "resume active job" path) until a new trailing user
|
|
300
|
-
* turn appears, or the timeout elapses. Returns the updated job, or null on
|
|
301
|
-
* timeout.
|
|
302
|
-
*/
|
|
303
|
-
async function waitForUserReply(client, machineToken, jobId, turnsSoFar) {
|
|
304
|
-
const deadline = Date.now() + REPLY_TIMEOUT_MS;
|
|
305
|
-
while (Date.now() < deadline) {
|
|
306
|
-
await sleep(REPLY_POLL_INTERVAL_MS);
|
|
307
|
-
const job = await client.nextJob(machineToken, 0);
|
|
308
|
-
if (job?.id !== jobId)
|
|
309
|
-
continue;
|
|
310
|
-
const last = job.turns[job.turns.length - 1];
|
|
311
|
-
if (job.turns.length > turnsSoFar && last?.role === 'user') {
|
|
312
|
-
return job;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Runs the epic_request interview end-to-end: converses with the user via
|
|
319
|
-
* post-turn/next-job until it has a title + the 3 required structured-desc
|
|
320
|
-
* fields, then creates a personal-scope epic request via the real API
|
|
321
|
-
* (POST /api/v1/me/epic-requests) using the user's own stored token.
|
|
322
|
-
*/
|
|
323
|
-
export async function invokeEpicRequestViaApi(job, ctx) {
|
|
324
|
-
// pxpipe (ENG-136): route API-mode calls through the proxy when enabled+healthy.
|
|
325
|
-
const routing = await resolveRouting();
|
|
326
|
-
const anthropic = new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing));
|
|
327
|
-
// ENG-149: a continuation job carries a seed → resume/enrich prompt + update
|
|
328
|
-
// the target ER in place; a fresh job → normal prompt + create a new ER.
|
|
329
|
-
const seed = parseSeedPayload(job.payload);
|
|
330
|
-
const systemPrompt = resolveSystemPrompt(seed);
|
|
331
|
-
// Audit-gate state (bug #2): the ER we are building (a continuation starts with
|
|
332
|
-
// its target), and how many re-submit/re-audit rounds we've spent.
|
|
333
|
-
let currentErId = seed?.targetEpicRequestId ?? null;
|
|
334
|
-
let qualityRounds = 0;
|
|
335
|
-
// Malformed-completion recovery: a model can emit "done" JSON we can't parse.
|
|
336
|
-
let jsonRepairs = 0;
|
|
337
|
-
const messages = [];
|
|
338
|
-
// Seed the conversation with any turns already on the job (e.g. a resumed
|
|
339
|
-
// daemon restart), so we don't re-ask questions already answered. A seeded
|
|
340
|
-
// USER turn may carry image attachments (ENG-153) — fetch + attach them as
|
|
341
|
-
// vision blocks; agent turns and image-free user turns are unchanged.
|
|
342
|
-
for (const turn of job.turns) {
|
|
343
|
-
if (turn.role === 'agent') {
|
|
344
|
-
messages.push({ role: 'assistant', content: turn.content });
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
messages.push({ role: 'user', content: await apiUserTurnContent(turn) });
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
let turnsSoFar = job.turns.length;
|
|
351
|
-
for (let i = 0; i < MAX_INTERVIEW_TURNS; i++) {
|
|
352
|
-
let reply;
|
|
353
|
-
try {
|
|
354
|
-
const response = await anthropic.messages.create({
|
|
355
|
-
model: ANTHROPIC_MODEL,
|
|
356
|
-
max_tokens: 1024,
|
|
357
|
-
system: systemPrompt,
|
|
358
|
-
messages: messages.length > 0 ? messages : [{ role: 'user', content: 'Let\'s get started.' }],
|
|
359
|
-
});
|
|
360
|
-
const textBlock = response.content.find((b) => b.type === 'text');
|
|
361
|
-
reply = textBlock?.type === 'text' ? textBlock.text : '';
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
const message = error instanceof Error ? error.message : 'Anthropic API request failed';
|
|
365
|
-
return { status: 'failed', result: { error: `Agent invocation failed: ${message}` } };
|
|
366
|
-
}
|
|
367
|
-
const draft = extractCompletionJson(reply);
|
|
368
|
-
if (draft) {
|
|
369
|
-
let submitted;
|
|
370
|
-
try {
|
|
371
|
-
submitted = await submitAndScore(ctx, currentErId, draft);
|
|
372
|
-
}
|
|
373
|
-
catch (error) {
|
|
374
|
-
const message = error instanceof Error ? error.message : 'Failed to save the epic request';
|
|
375
|
-
return { status: 'failed', result: { error: message } };
|
|
376
|
-
}
|
|
377
|
-
currentErId = submitted.erId;
|
|
378
|
-
const compliance = submitted.compliance;
|
|
379
|
-
// Finish only once the request passes the audit (>=90, no criticals), or
|
|
380
|
-
// when the quality-round budget is spent — best-effort, never stranded.
|
|
381
|
-
if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
|
|
382
|
-
return { status: 'done', result: { epicRequestId: currentErId } };
|
|
383
|
-
}
|
|
384
|
-
// Not good enough: feed the failing rules back (a model-facing round, no
|
|
385
|
-
// user turn) and loop — the model re-emits a richer completion or asks the
|
|
386
|
-
// employee a targeted question next iteration. (compliance is non-null here:
|
|
387
|
-
// meetsComplianceTarget(null) is true, so a null score would have finished.)
|
|
388
|
-
qualityRounds++;
|
|
389
|
-
if (compliance !== null) {
|
|
390
|
-
messages.push({ role: 'assistant', content: reply });
|
|
391
|
-
messages.push({ role: 'user', content: buildAuditFeedback(compliance) });
|
|
392
|
-
}
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
// The model TRIED to finish but the JSON was unusable (bug: malformed
|
|
396
|
-
// completion relayed to the employee as a question, looping forever). Correct
|
|
397
|
-
// the model instead of posting raw JSON — bounded, then fail cleanly.
|
|
398
|
-
if (looksLikeCompletionAttempt(reply)) {
|
|
399
|
-
if (jsonRepairs >= MAX_JSON_REPAIRS) {
|
|
400
|
-
return {
|
|
401
|
-
status: 'failed',
|
|
402
|
-
result: {
|
|
403
|
-
error: 'The interview produced malformed completion data and could not be finalized after several attempts.',
|
|
404
|
-
},
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
jsonRepairs++;
|
|
408
|
-
messages.push({ role: 'assistant', content: reply });
|
|
409
|
-
messages.push({ role: 'user', content: COMPLETION_REPAIR_INSTRUCTION });
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
// Not done yet — `reply` is the next question. Post it and wait for the answer.
|
|
413
|
-
messages.push({ role: 'assistant', content: reply });
|
|
414
|
-
let updated;
|
|
415
|
-
try {
|
|
416
|
-
updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, reply);
|
|
417
|
-
}
|
|
418
|
-
catch (error) {
|
|
419
|
-
const message = error instanceof Error ? error.message : 'post-turn failed';
|
|
420
|
-
return { status: 'failed', result: { error: message } };
|
|
421
|
-
}
|
|
422
|
-
turnsSoFar = updated.turns.length;
|
|
423
|
-
const answered = await waitForUserReply(ctx.mailboxClient, ctx.machineToken, job.id, turnsSoFar);
|
|
424
|
-
if (!answered) {
|
|
425
|
-
return {
|
|
426
|
-
status: 'failed',
|
|
427
|
-
result: { error: 'Timed out waiting for a reply — no response within 10 minutes.' },
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
const lastTurn = answered.turns[answered.turns.length - 1];
|
|
431
|
-
// ENG-153: the employee's reply may include image attachments — attach them
|
|
432
|
-
// as vision blocks (image-free replies stay a plain string, as before).
|
|
433
|
-
messages.push({
|
|
434
|
-
role: 'user',
|
|
435
|
-
content: lastTurn ? await apiUserTurnContent(lastTurn) : '',
|
|
436
|
-
});
|
|
437
|
-
turnsSoFar = answered.turns.length;
|
|
438
|
-
}
|
|
439
|
-
// Turn budget exhausted. If we already produced an ER (fresh create or a
|
|
440
|
-
// continuation's target), finalize with it best-effort rather than losing the
|
|
441
|
-
// work (bug #3); only fail outright if nothing was ever submitted.
|
|
442
|
-
return currentErId
|
|
443
|
-
? { status: 'done', result: { epicRequestId: currentErId } }
|
|
444
|
-
: {
|
|
445
|
-
status: 'failed',
|
|
446
|
-
result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` },
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
247
|
/**
|
|
450
248
|
* Fixed working directory for every `claude` CLI invocation made by the
|
|
451
249
|
* subscription-mode invoker. Two reasons this must be a stable, dedicated
|
|
@@ -503,11 +301,11 @@ function claudeCliSessionCwd() {
|
|
|
503
301
|
*/
|
|
504
302
|
function runClaudeCliTurn(prompt, opts) {
|
|
505
303
|
return new Promise((resolve, reject) => {
|
|
304
|
+
const model = opts.model === undefined ? ANTHROPIC_MODEL : opts.model;
|
|
506
305
|
const args = [
|
|
507
306
|
'-p',
|
|
508
307
|
prompt,
|
|
509
|
-
'--model',
|
|
510
|
-
ANTHROPIC_MODEL,
|
|
308
|
+
...(model ? ['--model', model] : []),
|
|
511
309
|
'--output-format',
|
|
512
310
|
'json',
|
|
513
311
|
'--tools',
|
|
@@ -595,123 +393,91 @@ function runClaudeCliTurn(prompt, opts) {
|
|
|
595
393
|
});
|
|
596
394
|
});
|
|
597
395
|
}
|
|
396
|
+
// ---------------------------------------------------------------------
|
|
397
|
+
// Request conversations (always open): one AI reply per claim.
|
|
398
|
+
// ---------------------------------------------------------------------
|
|
399
|
+
/** The request this conversation already saved, read with the person's token. */
|
|
400
|
+
async function loadSavedRequest(apiBaseUrl, userApiToken, epicRequestId) {
|
|
401
|
+
const res = await fetch(`${apiBaseUrl.replace(/\/+$/, '')}/api/v1/epic-requests/${encodeURIComponent(epicRequestId)}`, { headers: { Authorization: `Bearer ${userApiToken}` } });
|
|
402
|
+
if (!res.ok)
|
|
403
|
+
return null;
|
|
404
|
+
const body = (await res.json().catch(() => null));
|
|
405
|
+
return savedContextFromRecord(body?.data);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* True when the person's Claude plan can't use the requested model (for
|
|
409
|
+
* example Fable needs usage credits on some plans): retry on their default.
|
|
410
|
+
*/
|
|
411
|
+
export function needsDefaultModel(error) {
|
|
412
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
413
|
+
return /requires usage credits|switch to another model|model.*not available|not have access to/i.test(message);
|
|
414
|
+
}
|
|
415
|
+
function savedRequestIdOf(job) {
|
|
416
|
+
const result = job.result;
|
|
417
|
+
if (typeof result?.epicRequestId === 'string')
|
|
418
|
+
return result.epicRequestId;
|
|
419
|
+
return parseSeedPayload(job.payload)?.targetEpicRequestId ?? null;
|
|
420
|
+
}
|
|
598
421
|
/**
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
422
|
+
* Takes one step of a request conversation on this computer: the AI's reply to
|
|
423
|
+
* the person's latest message, which may save or update their request. Then
|
|
424
|
+
* the job is left waiting on the person (never completed), so the chat stays
|
|
425
|
+
* open: there is no reply timeout, and the computer is free for other work in
|
|
426
|
+
* the meantime. The server hands the job back when they write again.
|
|
427
|
+
*
|
|
428
|
+
* `anthropicApiKey` set → the Anthropic API; otherwise the local `claude` CLI
|
|
429
|
+
* on the person's own Claude sign-in.
|
|
603
430
|
*/
|
|
604
|
-
export async function
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
431
|
+
export async function invokeEpicRequestStep(job, ctx) {
|
|
432
|
+
try {
|
|
433
|
+
return await takeConversationStep(job, ctx);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
// Leave the conversation waiting on the person rather than retrying a
|
|
437
|
+
// broken step over and over; their next message tries again.
|
|
438
|
+
const reason = error instanceof Error ? error.message : 'something went wrong';
|
|
439
|
+
await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, `I couldn't continue: ${reason}\n\nSend another message (for example "try again") and I'll pick up where we left off.`);
|
|
440
|
+
return { status: 'yield' };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function takeConversationStep(job, ctx) {
|
|
608
444
|
const routing = await resolveRouting();
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
: // ENG-153 (A4): subscription mode has no inline-image channel, so a
|
|
626
|
-
// user turn's images degrade to an explicit textual note.
|
|
627
|
-
`They replied: ${appendAttachmentNote(t.content, imageAttachmentCount(t.attachments))}`)
|
|
628
|
-
.join('\n') +
|
|
629
|
-
'\n\nContinue the interview from exactly where this left off — ask the next question, or if you already have everything, respond with the completion JSON.'
|
|
630
|
-
: "Let's begin the interview.";
|
|
631
|
-
for (let i = 0; i < MAX_INTERVIEW_TURNS; i++) {
|
|
632
|
-
let turn;
|
|
633
|
-
try {
|
|
634
|
-
turn = await runClaudeCliTurn(nextPrompt, {
|
|
635
|
-
systemPrompt,
|
|
636
|
-
resumeSessionId: sessionId,
|
|
637
|
-
routing,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
catch (error) {
|
|
641
|
-
const message = error instanceof Error ? error.message : 'claude CLI invocation failed';
|
|
642
|
-
return { status: 'failed', result: { error: `Agent invocation failed: ${message}` } };
|
|
643
|
-
}
|
|
644
|
-
sessionId = turn.sessionId;
|
|
645
|
-
const draft = extractCompletionJson(turn.text);
|
|
646
|
-
if (draft) {
|
|
647
|
-
let submitted;
|
|
445
|
+
const requestId = savedRequestIdOf(job);
|
|
446
|
+
const saved = requestId ? await loadSavedRequest(ctx.apiBaseUrl, ctx.userApiToken, requestId) : null;
|
|
447
|
+
const turns = job.turns.map((t) => ({
|
|
448
|
+
role: t.role,
|
|
449
|
+
content: t.role === 'user' ? appendAttachmentNote(t.content, imageAttachmentCount(t.attachments)) : t.content,
|
|
450
|
+
}));
|
|
451
|
+
const anthropic = ctx.anthropicApiKey ? new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing)) : null;
|
|
452
|
+
let cliModel = ANTHROPIC_MODEL;
|
|
453
|
+
const outcome = await runConversationStep({ turns, saved }, {
|
|
454
|
+
reply: async (system, messages) => {
|
|
455
|
+
if (anthropic) {
|
|
456
|
+
const response = await anthropic.messages.create({ model: ANTHROPIC_MODEL, max_tokens: 4096, system, messages });
|
|
457
|
+
const text = response.content.find((b) => b.type === 'text');
|
|
458
|
+
return text?.type === 'text' ? text.text : '';
|
|
459
|
+
}
|
|
460
|
+
const prompt = renderConversationPrompt(messages);
|
|
648
461
|
try {
|
|
649
|
-
|
|
462
|
+
return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: undefined, routing, model: cliModel })).text;
|
|
650
463
|
}
|
|
651
464
|
catch (error) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const compliance = submitted.compliance;
|
|
657
|
-
if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
|
|
658
|
-
return { status: 'done', result: { epicRequestId: currentErId } };
|
|
659
|
-
}
|
|
660
|
-
// Feed the failing rules back into the SAME resumed session (no user turn);
|
|
661
|
-
// the model re-emits a richer completion or asks a targeted question next.
|
|
662
|
-
// (compliance is non-null here — see the api-mode note above.)
|
|
663
|
-
qualityRounds++;
|
|
664
|
-
if (compliance !== null) {
|
|
665
|
-
nextPrompt = buildAuditFeedback(compliance);
|
|
465
|
+
if (cliModel === null || !needsDefaultModel(error))
|
|
466
|
+
throw error;
|
|
467
|
+
cliModel = null;
|
|
468
|
+
return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: undefined, routing, model: null })).text;
|
|
666
469
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
result: {
|
|
676
|
-
error: 'The interview produced malformed completion data and could not be finalized after several attempts.',
|
|
677
|
-
},
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
jsonRepairs++;
|
|
681
|
-
nextPrompt = COMPLETION_REPAIR_INSTRUCTION;
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
// Not done yet — turn.text is the next question. Post it and wait for the answer.
|
|
685
|
-
let updated;
|
|
686
|
-
try {
|
|
687
|
-
updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, turn.text);
|
|
688
|
-
}
|
|
689
|
-
catch (error) {
|
|
690
|
-
const message = error instanceof Error ? error.message : 'post-turn failed';
|
|
691
|
-
return { status: 'failed', result: { error: message } };
|
|
692
|
-
}
|
|
693
|
-
turnsSoFar = updated.turns.length;
|
|
694
|
-
const answered = await waitForUserReply(ctx.mailboxClient, ctx.machineToken, job.id, turnsSoFar);
|
|
695
|
-
if (!answered) {
|
|
696
|
-
return {
|
|
697
|
-
status: 'failed',
|
|
698
|
-
result: { error: 'Timed out waiting for a reply — no response within 10 minutes.' },
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
const lastTurn = answered.turns[answered.turns.length - 1];
|
|
702
|
-
// ENG-153 (A4): note any attached images textually (subscription-mode degradation).
|
|
703
|
-
nextPrompt = lastTurn
|
|
704
|
-
? appendAttachmentNote(lastTurn.content, imageAttachmentCount(lastTurn.attachments))
|
|
705
|
-
: '';
|
|
706
|
-
turnsSoFar = answered.turns.length;
|
|
470
|
+
},
|
|
471
|
+
save: async (existingId, draft) => {
|
|
472
|
+
const submitted = await submitAndScore(ctx, existingId, draft);
|
|
473
|
+
return { id: submitted.erId, title: draft.title, compliance: submitted.compliance };
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
if (outcome.kind === 'saved') {
|
|
477
|
+
await ctx.mailboxClient.markRequestSaved(ctx.machineToken, job.id, outcome.saved.id);
|
|
707
478
|
}
|
|
708
|
-
|
|
709
|
-
return
|
|
710
|
-
? { status: 'done', result: { epicRequestId: currentErId } }
|
|
711
|
-
: {
|
|
712
|
-
status: 'failed',
|
|
713
|
-
result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` },
|
|
714
|
-
};
|
|
479
|
+
await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, outcome.text);
|
|
480
|
+
return { status: 'yield' };
|
|
715
481
|
}
|
|
716
482
|
// ---------------------------------------------------------------------
|
|
717
483
|
// start_execution / kind:'plan' (ENG-164) — run the local @planner headless
|