@proagentstore/cli 0.4.50 → 0.4.52
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.
|
@@ -84,6 +84,44 @@ export function splitSegments(command) {
|
|
|
84
84
|
.map((s) => s.trim())
|
|
85
85
|
.filter(Boolean);
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The engine's verdict on ONE tool call — the single expression of it (#597).
|
|
89
|
+
*
|
|
90
|
+
* `is_error` is the only outcome Claude Code's stream-json protocol states for a `tool_result`: it
|
|
91
|
+
* sets the flag when the call failed and omits it otherwise, so `!== true` is the protocol's own
|
|
92
|
+
* reading rather than an inference about the result text. This is the fact `settleAct` has always
|
|
93
|
+
* published as a consequential act's `ok`.
|
|
94
|
+
*
|
|
95
|
+
* It is a function because two call sites now need it — the act record and the transcript line the
|
|
96
|
+
* cloud parses — and a verdict stated twice is a verdict that can disagree with itself.
|
|
97
|
+
*/
|
|
98
|
+
export function toolCallOk(block) {
|
|
99
|
+
return block.is_error !== true;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The outcome marker on the transcript's `↳` result line — `↳✓` succeeded, `↳✗` failed (#597).
|
|
103
|
+
*
|
|
104
|
+
* The runner computed this and threw it away: `settleAct` read `is_error` while the line pushed
|
|
105
|
+
* into the transcript carried only the result text. The transcript is the ONLY channel the cloud
|
|
106
|
+
* reads for an ordinary tool call (a read, a grep, a file open leaves no `agent_events` row), so
|
|
107
|
+
* the per-tool-call record of #581 AC7 could state the argument and the result and never whether
|
|
108
|
+
* the call worked.
|
|
109
|
+
*
|
|
110
|
+
* ── Why the marker is welded to the arrow, with no space
|
|
111
|
+
*
|
|
112
|
+
* The reader (`workers/api/src/lib/engine-tool-calls.ts`) takes the outcome from the character at a
|
|
113
|
+
* FIXED offset after `↳`, and every runner predating this wrote a space there. So a row written by
|
|
114
|
+
* an old runner reads *unknown* and can never read as a pass, whatever the tool's own output
|
|
115
|
+
* happens to start with — which is the property AC1 asks for and the reason a marker after the
|
|
116
|
+
* space would not do: `toolResult()` collapses a result to one line, and vitest's begins
|
|
117
|
+
* `✓ src/foo.test.ts`. An old row carrying that text must not be read as a success claim.
|
|
118
|
+
*
|
|
119
|
+
* `terminal-render.ts` and `engine-auth-prompt.ts`'s `QUOTED_LINE_RE` both match `↳` unanchored on
|
|
120
|
+
* its right, so the pane still renders and quoted-line filtering still fires.
|
|
121
|
+
*/
|
|
122
|
+
export function toolResultMark(block) {
|
|
123
|
+
return toolCallOk(block) ? "✓" : "✗";
|
|
124
|
+
}
|
|
87
125
|
/** A token that is exactly the trunk, not a branch merely containing the word (`feature/main-fix`). */
|
|
88
126
|
const TRUNK_TOKEN = /(?:^|\s)(?:HEAD:)?(?:refs\/heads\/)?(?:main|master)(?:\s|$)/;
|
|
89
127
|
function prRef(segment) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the LAST completed engine turn ended (#545).
|
|
3
|
+
*
|
|
4
|
+
* The exit code was already known on this side and spent entirely on prose: `runOneShot`'s close
|
|
5
|
+
* handler pushed `[codex exited with code 1]` into the transcript and set no field, so a production
|
|
6
|
+
* Codex session whose every turn exited 1 — three times, the reason printed each time — reported
|
|
7
|
+
* `alive: true, ready: true, runState: "idle"`, and the Pilot spent fifteen minutes and three BYOK
|
|
8
|
+
* decisions rediscovering it from the pane.
|
|
9
|
+
*
|
|
10
|
+
* OUTCOME AND LIVENESS ARE TWO FACTS, and this module owns only the first. Nothing here touches
|
|
11
|
+
* `alive`: a one-shot session has no process between turns, a failing turn does not make the
|
|
12
|
+
* session unable to take another, and conflating the two once killed every delegated goal on
|
|
13
|
+
* codex/grok/gemini at iteration 0 (see `HeadlessSession.alive`). The failure is REPORTED here;
|
|
14
|
+
* what to make of one is the cloud's judgement, in `workers/api/src/lib/coding-turn-outcome.ts`.
|
|
15
|
+
*
|
|
16
|
+
* Its own module, beside `engine-usage.ts` / `engine-acts.ts` / `engine-auth.ts`, for the reason
|
|
17
|
+
* they are: the RULE that turns a process exit into a verdict is worth testing without spawning a
|
|
18
|
+
* process, and it is the part someone will later be tempted to change.
|
|
19
|
+
*/
|
|
20
|
+
/** Longest engine line carried as {@link EngineTurnReport.detail}. The pane holds the rest. */
|
|
21
|
+
export const MAX_TURN_DETAIL = 240;
|
|
22
|
+
/**
|
|
23
|
+
* A one-shot turn's process has exited — say what that means, and nothing more.
|
|
24
|
+
*
|
|
25
|
+
* A SIGNAL outranks the code because a signalled process's code is null and the kill is ours: the
|
|
26
|
+
* 15-minute wedge ceiling and `interrupt()` both land here, and counting either as an engine
|
|
27
|
+
* failure would let three slow builds read as a broken CLI.
|
|
28
|
+
*/
|
|
29
|
+
export function turnReportFromExit(code, signal, lastLine = "", now = Date.now()) {
|
|
30
|
+
const detail = lastLine.trim().slice(0, MAX_TURN_DETAIL);
|
|
31
|
+
return {
|
|
32
|
+
verdict: signal !== null ? "killed" : code === 0 ? "ok" : "failed",
|
|
33
|
+
exitCode: code,
|
|
34
|
+
signal,
|
|
35
|
+
at: now,
|
|
36
|
+
...(detail ? { detail } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A stream-json turn ended with a `result` event — the structured path's analogue of an exit code.
|
|
41
|
+
*
|
|
42
|
+
* Without it the field would exist for three engines and silently not for the flagship, which is
|
|
43
|
+
* the shape of gap that makes a platform-wide claim ("we notice a failed turn") false in the one
|
|
44
|
+
* case that runs most.
|
|
45
|
+
*/
|
|
46
|
+
export function turnReportFromResult(isError, detail = "", now = Date.now()) {
|
|
47
|
+
const text = detail.trim().slice(0, MAX_TURN_DETAIL);
|
|
48
|
+
return {
|
|
49
|
+
verdict: isError ? "failed" : "ok",
|
|
50
|
+
exitCode: null,
|
|
51
|
+
signal: null,
|
|
52
|
+
at: now,
|
|
53
|
+
...(text ? { detail: text } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { classifyCommand, commandFromToolInput, fillTargetFromResult } from "./engine-acts.js";
|
|
2
|
+
import { classifyCommand, commandFromToolInput, fillTargetFromResult, toolCallOk, toolResultMark } from "./engine-acts.js";
|
|
3
3
|
import { parseEngineUsage } from "./engine-usage.js";
|
|
4
|
+
import { turnReportFromExit, turnReportFromResult } from "./engine-turn.js";
|
|
4
5
|
/**
|
|
5
6
|
* Merge the platform's resolved engine env over the machine's, where an EMPTY value means
|
|
6
7
|
* REMOVE rather than "set to empty".
|
|
@@ -88,6 +89,16 @@ export class HeadlessSession {
|
|
|
88
89
|
turnStartedAt = 0;
|
|
89
90
|
/** Set by stop() — the only thing that ends a one-shot session (see `alive`). */
|
|
90
91
|
stopped = false;
|
|
92
|
+
/** How the last COMPLETED turn ended (#545). Null until one has. See {@link EngineTurnReport}. */
|
|
93
|
+
turnReport = null;
|
|
94
|
+
/**
|
|
95
|
+
* The engine's own last output line of the turn in flight (#545).
|
|
96
|
+
*
|
|
97
|
+
* Recorded as it is written, so the failure detail is the engine's sentence rather than
|
|
98
|
+
* something parsed back out of a rendered pane. Reset when a turn starts, so a report can never
|
|
99
|
+
* carry a previous turn's line.
|
|
100
|
+
*/
|
|
101
|
+
turnLastLine = "";
|
|
91
102
|
/** Measured engine spend not yet handed to the cloud (#267). Drained by {@link takeUsage}. */
|
|
92
103
|
pendingUsage = [];
|
|
93
104
|
/**
|
|
@@ -216,6 +227,15 @@ export class HeadlessSession {
|
|
|
216
227
|
return !this.stopped && !this.spawnFailed;
|
|
217
228
|
return this.procAlive;
|
|
218
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* How the last completed turn ended (#545) — NOT whether the session can take another.
|
|
232
|
+
*
|
|
233
|
+
* Read by the snapshot and carried to the cloud. Null means no turn has completed on this
|
|
234
|
+
* session; it is never a claim that a turn went well.
|
|
235
|
+
*/
|
|
236
|
+
get lastTurn() {
|
|
237
|
+
return this.turnReport;
|
|
238
|
+
}
|
|
219
239
|
/** Is a process running THIS instant? The persistent engine's liveness, and the spawn guard. */
|
|
220
240
|
get procAlive() {
|
|
221
241
|
return this.proc !== null && this.proc.exitCode === null && this.proc.signalCode === null;
|
|
@@ -397,6 +417,9 @@ export class HeadlessSession {
|
|
|
397
417
|
* preset decides what runs and what it costs; the platform does not need to know the engine.
|
|
398
418
|
*/
|
|
399
419
|
runOneShot(text) {
|
|
420
|
+
// Arm the per-turn line capture BEFORE the spawn, so a report can only ever carry a line
|
|
421
|
+
// this turn produced (#545).
|
|
422
|
+
this.turnLastLine = "";
|
|
400
423
|
const proc = spawn(this.cmdBin, [...this.cmdArgs, text], {
|
|
401
424
|
cwd: this.config.workDir,
|
|
402
425
|
env: mergeEnv(process.env, this.config.env),
|
|
@@ -450,7 +473,7 @@ export class HeadlessSession {
|
|
|
450
473
|
}
|
|
451
474
|
}, maxTurnMs);
|
|
452
475
|
ceiling.unref();
|
|
453
|
-
proc.on("close", (code) => {
|
|
476
|
+
proc.on("close", (code, signal) => {
|
|
454
477
|
clearTimeout(ceiling); // cleared before the staleness guard: the timer belongs to THIS process
|
|
455
478
|
// A non-zero exit is the engine's own failure (bad flags, not signed in) and the
|
|
456
479
|
// operator needs to see it — silently going idle is how "stdin is not a terminal"
|
|
@@ -465,6 +488,14 @@ export class HeadlessSession {
|
|
|
465
488
|
// kill-tmux.
|
|
466
489
|
if (this.proc !== proc)
|
|
467
490
|
return;
|
|
491
|
+
// THE EXIT CODE STOPS BEING ONLY PROSE HERE (#545). Recorded after the staleness guard
|
|
492
|
+
// on purpose: a turn aborted by its successor (see the kill above) must not overwrite
|
|
493
|
+
// the report of the turn that replaced it — the loser's outcome is about a turn nobody
|
|
494
|
+
// is waiting on any more.
|
|
495
|
+
//
|
|
496
|
+
// A signal means WE ended it (the wedge ceiling, an interrupt), which is the `killed`
|
|
497
|
+
// verdict: evidence about this platform's timers, not about the engine's health.
|
|
498
|
+
this.turnReport = turnReportFromExit(code, signal, this.turnLastLine);
|
|
468
499
|
this.run = "idle";
|
|
469
500
|
this.proc = null;
|
|
470
501
|
});
|
|
@@ -536,8 +567,12 @@ export class HeadlessSession {
|
|
|
536
567
|
/** Raw-engine stdout: strip ANSI control codes and append to the transcript. */
|
|
537
568
|
pushRaw(line) {
|
|
538
569
|
const clean = stripAnsi(line);
|
|
539
|
-
if (clean.trim())
|
|
570
|
+
if (clean.trim()) {
|
|
540
571
|
this.push(clean);
|
|
572
|
+
// The engine's own words, kept for the turn's report (#545) — captured on the way in,
|
|
573
|
+
// never scraped back out of the rendered pane.
|
|
574
|
+
this.turnLastLine = clean.trim();
|
|
575
|
+
}
|
|
541
576
|
if (this.transcript.length > 4000)
|
|
542
577
|
this.transcript = this.transcript.slice(-3000);
|
|
543
578
|
}
|
|
@@ -570,14 +605,20 @@ export class HeadlessSession {
|
|
|
570
605
|
case "user": // tool results come back as a synthetic user message
|
|
571
606
|
for (const block of ev.message?.content ?? []) {
|
|
572
607
|
if (block.type === "tool_result") {
|
|
573
|
-
this.push(`
|
|
608
|
+
this.push(` ↳${toolResultMark(block)} ${toolResult(block.content)}`); // ↳✓ / ↳✗ (#597)
|
|
574
609
|
this.settleAct(block);
|
|
575
610
|
}
|
|
576
611
|
}
|
|
577
612
|
break;
|
|
578
613
|
case "result": {
|
|
579
|
-
|
|
580
|
-
|
|
614
|
+
const failure = ev.is_error ? String(ev.result ?? ev.subtype ?? "failed") : "";
|
|
615
|
+
if (failure)
|
|
616
|
+
this.push(`[error] ${failure}`);
|
|
617
|
+
// The structured path's ANALOGUE of a non-zero exit (#545). Claude has no process
|
|
618
|
+
// per turn, so `exitCode` is honestly null and the verdict comes from the protocol's
|
|
619
|
+
// own `is_error` — the same claim, in the words the engine states it in. Without
|
|
620
|
+
// this the field would exist for three engines and silently not for the flagship.
|
|
621
|
+
this.turnReport = turnReportFromResult(ev.is_error === true, failure);
|
|
581
622
|
// The same event that ends the turn also reports what the turn COST (#267). It was
|
|
582
623
|
// parsed and thrown away, which is why Engine spend was absent from the ledger.
|
|
583
624
|
// An errored turn still burned tokens, so this is recorded regardless of is_error.
|
|
@@ -638,7 +679,7 @@ export class HeadlessSession {
|
|
|
638
679
|
if (!acts)
|
|
639
680
|
return;
|
|
640
681
|
this.awaitingResult.delete(id);
|
|
641
|
-
const ok = block
|
|
682
|
+
const ok = toolCallOk(block);
|
|
642
683
|
for (const a of fillTargetFromResult(acts, block.content))
|
|
643
684
|
this.publishAct({ ...a, ok });
|
|
644
685
|
}
|
|
@@ -151,6 +151,10 @@ export class CodingRuntime {
|
|
|
151
151
|
// exactly the question asked about a session that just stopped.
|
|
152
152
|
authResolved: session.authResolved,
|
|
153
153
|
engineRuntime: session.engineRuntime,
|
|
154
|
+
// Reported on EVERY capture, including one where the session is not alive: "how did the
|
|
155
|
+
// last turn end" is exactly the question asked about a session that just stopped, and
|
|
156
|
+
// the omitted-when-null shape keeps "not measured" distinguishable from a verdict.
|
|
157
|
+
...(session.lastTurn ? { lastTurn: session.lastTurn } : {}),
|
|
154
158
|
...(opts.drainUsage ? { usage: session.takeUsage(), acts: session.takeActs() } : {}),
|
|
155
159
|
};
|
|
156
160
|
}
|