@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Child-Pi editor-state reporter (issue #68): polls the child Pi's editor text
|
|
2
|
+
* and pushes `{type:"editor_state", empty}` over the control socket whenever the
|
|
3
|
+
* text changes, so the attach surface can gate ← on the authoritative state
|
|
4
|
+
* instead of render heuristics. Dependency-injected for unit testing. */
|
|
5
|
+
|
|
6
|
+
import { CLIENT_ID_EDITOR_REPORTER } from "./host-protocol.mjs";
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ write(jsonLine: string): void; on?(event: "close" | "error", fn: () => void): void }} SocketLike */
|
|
9
|
+
|
|
10
|
+
const defaultScheduler = {
|
|
11
|
+
interval(fn, ms) { const h = setInterval(fn, ms); h.unref?.(); return h; },
|
|
12
|
+
timeout(fn, ms) { const h = setTimeout(fn, ms); h.unref?.(); return h; },
|
|
13
|
+
clear(handle) { clearInterval(handle); clearTimeout(handle); },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createEditorStateReporter({ getEditorText, connect, intervalMs = 100, scheduler = defaultScheduler }) {
|
|
17
|
+
let started = false;
|
|
18
|
+
let stopped = false;
|
|
19
|
+
let socket = null;
|
|
20
|
+
let pollTimer = null;
|
|
21
|
+
let reconnectTimer = null;
|
|
22
|
+
let backoffMs = 1000;
|
|
23
|
+
let lastText = null;
|
|
24
|
+
|
|
25
|
+
function send(json) {
|
|
26
|
+
if (!socket) return;
|
|
27
|
+
try {
|
|
28
|
+
socket.write(JSON.stringify(json) + "\n");
|
|
29
|
+
} catch {
|
|
30
|
+
teardownSocket();
|
|
31
|
+
scheduleReconnect();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function teardownSocket() {
|
|
36
|
+
if (pollTimer !== null) { scheduler.clear(pollTimer); pollTimer = null; }
|
|
37
|
+
const s = socket;
|
|
38
|
+
socket = null;
|
|
39
|
+
try { s?.destroy?.(); } catch { /* already torn down */ }
|
|
40
|
+
try { s?.removeAllListeners?.(); } catch { /* already torn down */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function poll() {
|
|
44
|
+
if (stopped || !socket) return;
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = getEditorText();
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
text = typeof text === "string" ? text : "";
|
|
52
|
+
if (text !== lastText) {
|
|
53
|
+
lastText = text;
|
|
54
|
+
send({ type: "editor_state", empty: text.length === 0 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function startPolling() {
|
|
59
|
+
if (pollTimer !== null) return;
|
|
60
|
+
lastText = null; // force a first report after (re)connect
|
|
61
|
+
pollTimer = scheduler.interval(poll, intervalMs);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scheduleReconnect() {
|
|
65
|
+
if (stopped) return;
|
|
66
|
+
teardownSocket();
|
|
67
|
+
reconnectTimer = scheduler.timeout(tryConnect, backoffMs);
|
|
68
|
+
backoffMs = Math.min(backoffMs * 2, 5000);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tryConnect() {
|
|
72
|
+
reconnectTimer = null;
|
|
73
|
+
if (stopped) return;
|
|
74
|
+
let s;
|
|
75
|
+
try {
|
|
76
|
+
s = connect();
|
|
77
|
+
} catch {
|
|
78
|
+
scheduleReconnect();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
socket = s;
|
|
82
|
+
s?.resume?.();
|
|
83
|
+
s?.unref?.();
|
|
84
|
+
s?.on?.("data", () => {}); // consume and discard broadcast traffic
|
|
85
|
+
s?.on?.("connect", () => {
|
|
86
|
+
if (socket !== s) return;
|
|
87
|
+
backoffMs = 1000;
|
|
88
|
+
// Announce the client id before any state: the runner keeps this
|
|
89
|
+
// resident socket out of attachedClients/attachedEver so warm-host
|
|
90
|
+
// reclaim still sees an idle host (issue #103 §C).
|
|
91
|
+
send({ type: "hello", clientId: CLIENT_ID_EDITOR_REPORTER });
|
|
92
|
+
startPolling();
|
|
93
|
+
});
|
|
94
|
+
s?.on?.("close", () => { if (socket === s) scheduleReconnect(); });
|
|
95
|
+
s?.on?.("error", () => { if (socket === s) scheduleReconnect(); });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function start() {
|
|
99
|
+
if (started) return;
|
|
100
|
+
started = true;
|
|
101
|
+
stopped = false;
|
|
102
|
+
tryConnect();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stop() {
|
|
106
|
+
stopped = true;
|
|
107
|
+
if (reconnectTimer !== null) { scheduler.clear(reconnectTimer); reconnectTimer = null; }
|
|
108
|
+
teardownSocket();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { start, stop };
|
|
112
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduce Pi JSON-mode worker events into an evolving RunStatus.
|
|
3
|
+
*
|
|
4
|
+
* Event vocabulary (verified against dist `agent-session.ts`; see docs/EXPLORATION.md §4):
|
|
5
|
+
* first line: session header {type:"session",...}
|
|
6
|
+
* message_start | message_update | message_end (.message is an AgentMessage)
|
|
7
|
+
* tool_execution_start {toolCallId,toolName,args}
|
|
8
|
+
* tool_execution_end {toolCallId,toolName,result,isError}
|
|
9
|
+
* turn_start | turn_end | agent_start | agent_end
|
|
10
|
+
* NOTE: `tool_result_end` (used by the subagent example) is never emitted — do not rely on it.
|
|
11
|
+
*/
|
|
12
|
+
import { deriveSummary, finalizeSemanticState } from "./derive.mjs";
|
|
13
|
+
import { assistantText, detectNeedsInput, toolPath, toolSummary, truncate } from "./heuristics.mjs";
|
|
14
|
+
|
|
15
|
+
/** @typedef {import("./types.mjs").RunConfig} RunConfig */
|
|
16
|
+
/** @typedef {import("./types.mjs").RunStatus} RunStatus */
|
|
17
|
+
|
|
18
|
+
const PREVIEW_MAX = 240;
|
|
19
|
+
|
|
20
|
+
/** Tool names whose interactive execution blocks on a user answer. */
|
|
21
|
+
const QUESTION_TOOL_NAMES = new Set(["ask_user_question", "ask_questions", "question", "questionnaire"]);
|
|
22
|
+
|
|
23
|
+
/** Questionnaire args shared by pi/Claude-Code style question tools. */
|
|
24
|
+
function isQuestionArgShape(args) {
|
|
25
|
+
const questions = Array.isArray(args?.questions) ? args.questions : null;
|
|
26
|
+
return Boolean(questions && questions.length > 0 && typeof questions[0]?.question === "string");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A start event is a question when its name is known or its args carry the questionnaire shape. */
|
|
30
|
+
function isQuestionToolCall(name, args) {
|
|
31
|
+
return QUESTION_TOOL_NAMES.has(name) || isQuestionArgShape(args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the initial status for a freshly-launched run.
|
|
36
|
+
* @param {RunConfig} config
|
|
37
|
+
* @param {number|null} pid
|
|
38
|
+
* @param {number} now
|
|
39
|
+
* @returns {RunStatus}
|
|
40
|
+
*/
|
|
41
|
+
export function createRunStatus(config, pid, now) {
|
|
42
|
+
/** @type {RunStatus} */
|
|
43
|
+
const status = {
|
|
44
|
+
version: 1,
|
|
45
|
+
runId: config.runId,
|
|
46
|
+
viewId: config.viewId,
|
|
47
|
+
pid,
|
|
48
|
+
startedAt: now,
|
|
49
|
+
endedAt: null,
|
|
50
|
+
exitCode: null,
|
|
51
|
+
kind: config.kind,
|
|
52
|
+
prompt: config.prompt,
|
|
53
|
+
model: config.model ?? null,
|
|
54
|
+
semanticState: "queued",
|
|
55
|
+
processState: "alive",
|
|
56
|
+
summary: "Queued",
|
|
57
|
+
lastActivityAt: now,
|
|
58
|
+
currentTool: null,
|
|
59
|
+
latestAssistantPreview: "",
|
|
60
|
+
question: null,
|
|
61
|
+
pendingQuestions: [],
|
|
62
|
+
error: null,
|
|
63
|
+
lastAgentActivityAt: null,
|
|
64
|
+
stopReason: null,
|
|
65
|
+
stoppedByUser: false,
|
|
66
|
+
turns: 0,
|
|
67
|
+
toolCount: 0,
|
|
68
|
+
eventCount: 0,
|
|
69
|
+
lastEventAt: null,
|
|
70
|
+
usage: null,
|
|
71
|
+
stallReason: null,
|
|
72
|
+
evidenceSummary: null,
|
|
73
|
+
autoState: null,
|
|
74
|
+
};
|
|
75
|
+
return status;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Apply a single worker event to `status` (mutates in place).
|
|
80
|
+
* @param {RunStatus} status
|
|
81
|
+
* @param {any} event
|
|
82
|
+
* @param {number} now
|
|
83
|
+
* @param {{ interactive?: boolean }} [opts]
|
|
84
|
+
* @returns {boolean} whether this event produced a user-visible change worth persisting.
|
|
85
|
+
*/
|
|
86
|
+
export function reduceEvent(status, event, now, opts = {}) {
|
|
87
|
+
if (!event || typeof event !== "object" || typeof event.type !== "string") return false;
|
|
88
|
+
let meaningful = false;
|
|
89
|
+
status.eventCount = (status.eventCount ?? 0) + 1;
|
|
90
|
+
status.lastEventAt = now;
|
|
91
|
+
|
|
92
|
+
switch (event.type) {
|
|
93
|
+
case "tool_execution_start": {
|
|
94
|
+
const name = event.toolName ?? event.args?.name ?? "tool";
|
|
95
|
+
const args = event.args ?? {};
|
|
96
|
+
status.toolCount += 1;
|
|
97
|
+
if (opts.interactive && isQuestionToolCall(name, args)) {
|
|
98
|
+
upsertPendingQuestion(status, event.toolCallId, questionFromArgs(args));
|
|
99
|
+
} else if (pendingQuestions(status).length === 0) {
|
|
100
|
+
status.currentTool = { name, path: toolPath(args), summary: toolSummary(name, args) };
|
|
101
|
+
status.semanticState = "working";
|
|
102
|
+
}
|
|
103
|
+
preservePendingQuestion(status);
|
|
104
|
+
status.lastActivityAt = now;
|
|
105
|
+
// Unread is keyed off the latest assistant message, not intermediate tool churn.
|
|
106
|
+
meaningful = true;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "tool_execution_end": {
|
|
110
|
+
if (event.isError) status.error = `Tool ${event.toolName ?? ""} failed`.trim();
|
|
111
|
+
// The end event carries no args, so a shape-matched start cannot be recognized here;
|
|
112
|
+
// remove by call id instead (a no-op when this call had no pending question).
|
|
113
|
+
if (opts.interactive) removePendingQuestion(status, event.toolCallId);
|
|
114
|
+
status.currentTool = null;
|
|
115
|
+
status.semanticState = "working";
|
|
116
|
+
status.question = null;
|
|
117
|
+
preservePendingQuestion(status);
|
|
118
|
+
status.lastActivityAt = now;
|
|
119
|
+
// Unread is keyed off the latest assistant message, not intermediate tool churn.
|
|
120
|
+
meaningful = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "message_start": {
|
|
124
|
+
if (event.message?.role === "assistant") {
|
|
125
|
+
status.semanticState = "working";
|
|
126
|
+
preservePendingQuestion(status);
|
|
127
|
+
status.lastActivityAt = now;
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "message_end": {
|
|
132
|
+
const msg = event.message;
|
|
133
|
+
if (msg?.usage || event.usage) status.usage = mergeUsage(status.usage ?? null, msg?.usage ?? event.usage);
|
|
134
|
+
if (msg?.role === "assistant") {
|
|
135
|
+
status.turns += 1;
|
|
136
|
+
if (msg.model && !status.model) status.model = msg.model;
|
|
137
|
+
if (msg.stopReason) status.stopReason = msg.stopReason;
|
|
138
|
+
if (msg.errorMessage) status.error = msg.errorMessage;
|
|
139
|
+
else if (msg.stopReason === "stop") status.error = null;
|
|
140
|
+
const text = assistantText(msg);
|
|
141
|
+
let nb = { needsInput: false, question: null };
|
|
142
|
+
if (text) {
|
|
143
|
+
// Store the full latest text (truncated) so peek shows meaningful output;
|
|
144
|
+
// deriveSummary() condenses it to a first sentence for the row.
|
|
145
|
+
status.latestAssistantPreview = truncate(text, PREVIEW_MAX);
|
|
146
|
+
nb = detectNeedsInput(text);
|
|
147
|
+
status.question = nb.question;
|
|
148
|
+
}
|
|
149
|
+
status.semanticState = nb.needsInput ? "needs_input" : "working";
|
|
150
|
+
preservePendingQuestion(status);
|
|
151
|
+
status.lastActivityAt = now;
|
|
152
|
+
status.lastAgentActivityAt = now;
|
|
153
|
+
meaningful = true;
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "agent_start":
|
|
158
|
+
case "turn_start":
|
|
159
|
+
case "turn_end":
|
|
160
|
+
status.lastActivityAt = now;
|
|
161
|
+
break;
|
|
162
|
+
default:
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (meaningful) status.summary = deriveSummary(status);
|
|
167
|
+
return meaningful;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Mark a run finished and compute its terminal state + summary.
|
|
172
|
+
* @param {RunStatus} status
|
|
173
|
+
* @param {{ exitCode:number|null, stoppedByUser?:boolean, openEnded?:boolean }} opts
|
|
174
|
+
* @param {number} now
|
|
175
|
+
* @returns {RunStatus}
|
|
176
|
+
*/
|
|
177
|
+
export function finalizeRun(status, opts, now) {
|
|
178
|
+
status.endedAt = now;
|
|
179
|
+
status.exitCode = opts.exitCode;
|
|
180
|
+
status.processState = "exited";
|
|
181
|
+
status.pid = null;
|
|
182
|
+
status.stoppedByUser = Boolean(opts.stoppedByUser);
|
|
183
|
+
status.currentTool = null;
|
|
184
|
+
|
|
185
|
+
const hadPendingQuestions = pendingQuestions(status).length > 0;
|
|
186
|
+
status.pendingQuestions = [];
|
|
187
|
+
const nb = detectNeedsInput(status.latestAssistantPreview);
|
|
188
|
+
// An exited interactive tool can no longer accept an answer. Keep only an
|
|
189
|
+
// independently detected assistant question, never the stale tool prompt.
|
|
190
|
+
if (hadPendingQuestions) status.question = nb.question;
|
|
191
|
+
const needsInput = nb.needsInput || Boolean(status.question);
|
|
192
|
+
if (needsInput && !status.question) status.question = nb.question;
|
|
193
|
+
|
|
194
|
+
status.semanticState = finalizeSemanticState({
|
|
195
|
+
exitCode: status.exitCode,
|
|
196
|
+
stopReason: status.stopReason,
|
|
197
|
+
stoppedByUser: status.stoppedByUser,
|
|
198
|
+
needsInput,
|
|
199
|
+
openEnded: opts.openEnded,
|
|
200
|
+
});
|
|
201
|
+
status.lastActivityAt = now;
|
|
202
|
+
status.summary = deriveSummary(status);
|
|
203
|
+
return status;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Project a RunStatus into the row-level ViewState written to `state.json`.
|
|
208
|
+
* @param {RunStatus} status
|
|
209
|
+
* @param {number} now
|
|
210
|
+
* @returns {import("./types.mjs").ViewState}
|
|
211
|
+
*/
|
|
212
|
+
export function projectViewState(status, now, previousState = null) {
|
|
213
|
+
return {
|
|
214
|
+
version: 1,
|
|
215
|
+
viewId: status.viewId,
|
|
216
|
+
currentRunId: status.runId,
|
|
217
|
+
semanticState: status.semanticState,
|
|
218
|
+
processState: status.processState,
|
|
219
|
+
summary: status.summary,
|
|
220
|
+
lastActivityAt: status.lastActivityAt,
|
|
221
|
+
updatedAt: now,
|
|
222
|
+
needsInput: status.semanticState === "needs_input",
|
|
223
|
+
hasError: status.semanticState === "failed",
|
|
224
|
+
latestAssistantPreview: status.latestAssistantPreview,
|
|
225
|
+
latestTool: status.currentTool ? { name: status.currentTool.name, path: status.currentTool.path } : null,
|
|
226
|
+
question: status.question,
|
|
227
|
+
pendingQuestions: pendingQuestions(status),
|
|
228
|
+
error: status.error,
|
|
229
|
+
lastVisitedAt: previousState?.lastVisitedAt ?? null,
|
|
230
|
+
lastAgentActivityAt: status.lastAgentActivityAt ?? previousState?.lastAgentActivityAt ?? null,
|
|
231
|
+
review: status.evidenceSummary ?? previousState?.review,
|
|
232
|
+
diagnostics: previousState?.diagnostics,
|
|
233
|
+
autoState: status.autoState ?? previousState?.autoState ?? null,
|
|
234
|
+
followUps: previousState?.followUps,
|
|
235
|
+
steering: previousState?.steering,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** @param {import("./types.mjs").RunStatus} status */
|
|
240
|
+
function pendingQuestions(status) {
|
|
241
|
+
if (!Array.isArray(status.pendingQuestions)) status.pendingQuestions = [];
|
|
242
|
+
return status.pendingQuestions;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function questionFromArgs(args) {
|
|
246
|
+
if (typeof args?.question === "string" && args.question.trim()) return args.question.trim();
|
|
247
|
+
for (const item of Array.isArray(args?.questions) ? args.questions : []) {
|
|
248
|
+
const question = String(item?.question ?? item?.prompt ?? "").trim();
|
|
249
|
+
if (question) return question;
|
|
250
|
+
}
|
|
251
|
+
return "Answer the pending question";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function upsertPendingQuestion(status, toolCallId, question) {
|
|
255
|
+
const id = String(toolCallId ?? "ask_questions");
|
|
256
|
+
const pending = pendingQuestions(status);
|
|
257
|
+
const existing = pending.find((item) => item.toolCallId === id);
|
|
258
|
+
if (existing) existing.question = question;
|
|
259
|
+
else pending.push({ toolCallId: id, question });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function removePendingQuestion(status, toolCallId) {
|
|
263
|
+
const id = String(toolCallId ?? "ask_questions");
|
|
264
|
+
status.pendingQuestions = pendingQuestions(status).filter((item) => item.toolCallId !== id);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function preservePendingQuestion(status) {
|
|
268
|
+
const first = pendingQuestions(status)[0];
|
|
269
|
+
if (!first) return;
|
|
270
|
+
status.currentTool = null;
|
|
271
|
+
status.semanticState = "needs_input";
|
|
272
|
+
status.question = first.question;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @param {import("./types.mjs").EvidenceUsage|null} current @param {any} usage */
|
|
276
|
+
function mergeUsage(current, usage) {
|
|
277
|
+
if (!usage || typeof usage !== "object") return current;
|
|
278
|
+
const input = Number(usage.inputTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
279
|
+
const output = Number(usage.outputTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
280
|
+
const total = Number(usage.totalTokens ?? usage.total_tokens ?? input + output) || input + output;
|
|
281
|
+
return {
|
|
282
|
+
inputTokens: (current?.inputTokens ?? 0) + input,
|
|
283
|
+
outputTokens: (current?.outputTokens ?? 0) + output,
|
|
284
|
+
totalTokens: (current?.totalTokens ?? 0) + total,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/** Review evidence extraction and persistence helpers. */
|
|
2
|
+
import { assistantText, classifyCommand, toolFileOperation, toolResultText, truncate } from "./heuristics.mjs";
|
|
3
|
+
import { atomicWriteJson, readJson } from "./atomic.mjs";
|
|
4
|
+
import * as P from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
/** @param {{ viewId:string, runId?:string|null, now?:number, source?:string }} opts @returns {import("./types.mjs").EvidenceSnapshot} */
|
|
7
|
+
export function emptyEvidenceSnapshot(opts) {
|
|
8
|
+
const now = opts.now ?? Date.now();
|
|
9
|
+
return {
|
|
10
|
+
version: 1,
|
|
11
|
+
viewId: opts.viewId,
|
|
12
|
+
runId: opts.runId ?? null,
|
|
13
|
+
updatedAt: now,
|
|
14
|
+
outcome: "unknown",
|
|
15
|
+
ready: false,
|
|
16
|
+
summary: "",
|
|
17
|
+
commands: [],
|
|
18
|
+
fileChanges: [],
|
|
19
|
+
errors: [],
|
|
20
|
+
assistantEvidence: [],
|
|
21
|
+
usage: null,
|
|
22
|
+
eventCount: 0,
|
|
23
|
+
source: opts.source ?? "events",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {any} snapshot @param {{ viewId:string, runId?:string|null }} fallback @returns {import("./types.mjs").EvidenceSnapshot} */
|
|
28
|
+
export function normalizeEvidenceSnapshot(snapshot, fallback) {
|
|
29
|
+
const base = emptyEvidenceSnapshot({ viewId: fallback.viewId, runId: fallback.runId ?? null });
|
|
30
|
+
if (!snapshot || typeof snapshot !== "object") return base;
|
|
31
|
+
return {
|
|
32
|
+
...base,
|
|
33
|
+
...snapshot,
|
|
34
|
+
viewId: typeof snapshot.viewId === "string" ? snapshot.viewId : base.viewId,
|
|
35
|
+
runId: typeof snapshot.runId === "string" ? snapshot.runId : (snapshot.runId === null ? null : base.runId),
|
|
36
|
+
commands: Array.isArray(snapshot.commands) ? snapshot.commands : [],
|
|
37
|
+
fileChanges: Array.isArray(snapshot.fileChanges) ? snapshot.fileChanges : [],
|
|
38
|
+
errors: Array.isArray(snapshot.errors) ? snapshot.errors : [],
|
|
39
|
+
assistantEvidence: Array.isArray(snapshot.assistantEvidence) ? snapshot.assistantEvidence : [],
|
|
40
|
+
usage: snapshot.usage && typeof snapshot.usage === "object" ? snapshot.usage : null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {string} root @param {string} viewId */
|
|
45
|
+
export function readEvidence(root, viewId) {
|
|
46
|
+
return normalizeEvidenceSnapshot(readJson(P.evidencePath(root, viewId), null), { viewId });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} root @param {import("./types.mjs").EvidenceSnapshot} snapshot */
|
|
50
|
+
export function writeEvidence(root, snapshot) {
|
|
51
|
+
const normalized = normalizeEvidenceSnapshot(snapshot, { viewId: snapshot.viewId, runId: snapshot.runId ?? null });
|
|
52
|
+
normalized.updatedAt = Date.now();
|
|
53
|
+
atomicWriteJson(P.evidencePath(root, normalized.viewId), normalized);
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
58
|
+
export function readRunEvidence(root, viewId, runId) {
|
|
59
|
+
return normalizeEvidenceSnapshot(readJson(P.runEvidencePath(root, viewId, runId), null), { viewId, runId });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {string} root @param {import("./types.mjs").EvidenceSnapshot} snapshot */
|
|
63
|
+
export function writeRunEvidence(root, snapshot) {
|
|
64
|
+
const normalized = normalizeEvidenceSnapshot(snapshot, { viewId: snapshot.viewId, runId: snapshot.runId ?? null });
|
|
65
|
+
normalized.updatedAt = Date.now();
|
|
66
|
+
if (normalized.runId) atomicWriteJson(P.runEvidencePath(root, normalized.viewId, normalized.runId), normalized);
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} snapshot @returns {import("./types.mjs").ReviewSummary} */
|
|
71
|
+
export function summarizeEvidence(snapshot) {
|
|
72
|
+
const s = normalizeEvidenceSnapshot(snapshot, { viewId: snapshot?.viewId ?? "" });
|
|
73
|
+
const failedCommandCount = s.commands.filter((c) => c.status === "failed").length;
|
|
74
|
+
const latestAssistantEvidence = s.assistantEvidence[s.assistantEvidence.length - 1]?.text ?? "";
|
|
75
|
+
return {
|
|
76
|
+
ready: Boolean(s.ready),
|
|
77
|
+
outcome: s.outcome ?? "unknown",
|
|
78
|
+
fileChangeCount: s.fileChanges.length,
|
|
79
|
+
commandCount: s.commands.length,
|
|
80
|
+
failedCommandCount,
|
|
81
|
+
errorCount: s.errors.length,
|
|
82
|
+
latestAssistantEvidence: truncate(latestAssistantEvidence, 180),
|
|
83
|
+
updatedAt: s.updatedAt ?? null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} snapshot @param {Partial<import("./types.mjs").EvidenceCommand>} command */
|
|
88
|
+
export function upsertEvidenceCommand(snapshot, command) {
|
|
89
|
+
const id = command.id ?? `cmd_${snapshot.commands.length + 1}`;
|
|
90
|
+
const existing = snapshot.commands.find((c) => c.id === id);
|
|
91
|
+
const commandText = String(command.command || existing?.command || "");
|
|
92
|
+
const next = {
|
|
93
|
+
id,
|
|
94
|
+
at: command.at ?? existing?.at ?? Date.now(),
|
|
95
|
+
command: commandText,
|
|
96
|
+
kind: command.kind ?? existing?.kind ?? classifyCommand(commandText),
|
|
97
|
+
status: command.status ?? existing?.status ?? "unknown",
|
|
98
|
+
exitCode: command.exitCode ?? existing?.exitCode ?? null,
|
|
99
|
+
durationMs: command.durationMs ?? existing?.durationMs ?? null,
|
|
100
|
+
outputPreview: command.outputPreview ?? existing?.outputPreview ?? "",
|
|
101
|
+
};
|
|
102
|
+
if (existing) Object.assign(existing, next);
|
|
103
|
+
else snapshot.commands.push(next);
|
|
104
|
+
snapshot.updatedAt = Date.now();
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} snapshot @param {Partial<import("./types.mjs").EvidenceFileChange>} change */
|
|
109
|
+
export function upsertEvidenceFileChange(snapshot, change) {
|
|
110
|
+
const p = String(change.path ?? "").trim();
|
|
111
|
+
if (!p) return null;
|
|
112
|
+
const existing = snapshot.fileChanges.find((f) => f.path === p);
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
if (existing) {
|
|
115
|
+
existing.lastSeenAt = now;
|
|
116
|
+
existing.count = (existing.count ?? 0) + 1;
|
|
117
|
+
if (change.action) existing.action = change.action;
|
|
118
|
+
if (change.toolName) existing.toolName = change.toolName;
|
|
119
|
+
snapshot.updatedAt = now;
|
|
120
|
+
return existing;
|
|
121
|
+
}
|
|
122
|
+
const next = {
|
|
123
|
+
path: p,
|
|
124
|
+
action: change.action ?? "unknown",
|
|
125
|
+
toolName: change.toolName ?? null,
|
|
126
|
+
firstSeenAt: change.firstSeenAt ?? now,
|
|
127
|
+
lastSeenAt: change.lastSeenAt ?? now,
|
|
128
|
+
count: change.count ?? 1,
|
|
129
|
+
};
|
|
130
|
+
snapshot.fileChanges.push(next);
|
|
131
|
+
snapshot.updatedAt = now;
|
|
132
|
+
return next;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} snapshot @param {Partial<import("./types.mjs").EvidenceError>} error */
|
|
136
|
+
export function recordEvidenceError(snapshot, error) {
|
|
137
|
+
const next = {
|
|
138
|
+
at: error.at ?? Date.now(),
|
|
139
|
+
source: error.source ?? "event",
|
|
140
|
+
message: String(error.message ?? "Unknown error"),
|
|
141
|
+
toolName: error.toolName ?? null,
|
|
142
|
+
};
|
|
143
|
+
snapshot.errors.push(next);
|
|
144
|
+
snapshot.updatedAt = next.at;
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} snapshot @param {string} text @param {{ at?:number }} [opts] */
|
|
149
|
+
export function recordAssistantEvidence(snapshot, text, opts = {}) {
|
|
150
|
+
const cleaned = String(text || "").replace(/\s+/g, " ").trim();
|
|
151
|
+
if (!cleaned) return null;
|
|
152
|
+
const next = { at: opts.at ?? Date.now(), text: truncate(cleaned, 12_000) };
|
|
153
|
+
snapshot.assistantEvidence.push(next);
|
|
154
|
+
snapshot.summary = truncate(cleaned, 240);
|
|
155
|
+
snapshot.updatedAt = next.at;
|
|
156
|
+
return next;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} base @param {import("./types.mjs").EvidenceSnapshot} incoming */
|
|
160
|
+
export function mergeEvidenceSnapshot(base, incoming) {
|
|
161
|
+
for (const c of incoming.commands ?? []) upsertEvidenceCommand(base, c);
|
|
162
|
+
for (const f of incoming.fileChanges ?? []) upsertEvidenceFileChange(base, f);
|
|
163
|
+
for (const e of incoming.errors ?? []) recordEvidenceError(base, e);
|
|
164
|
+
for (const a of incoming.assistantEvidence ?? []) recordAssistantEvidence(base, a.text, { at: a.at });
|
|
165
|
+
base.outcome = incoming.outcome ?? base.outcome;
|
|
166
|
+
base.ready = Boolean(incoming.ready || base.ready);
|
|
167
|
+
base.usage = mergeUsage(base.usage, incoming.usage);
|
|
168
|
+
base.updatedAt = Date.now();
|
|
169
|
+
return base;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} snapshot @param {any} event @param {number} now */
|
|
173
|
+
export function reduceEvidence(snapshot, event, now = Date.now()) {
|
|
174
|
+
if (!event || typeof event !== "object") return false;
|
|
175
|
+
snapshot.eventCount = (snapshot.eventCount ?? 0) + 1;
|
|
176
|
+
let changed = false;
|
|
177
|
+
if (event.type === "tool_execution_start") {
|
|
178
|
+
const name = event.toolName ?? event.args?.name ?? "tool";
|
|
179
|
+
const args = event.args ?? {};
|
|
180
|
+
if (name === "bash" && typeof args.command === "string") {
|
|
181
|
+
upsertEvidenceCommand(snapshot, { id: event.toolCallId, at: now, command: args.command, kind: classifyCommand(args.command), status: "started" });
|
|
182
|
+
changed = true;
|
|
183
|
+
}
|
|
184
|
+
const fileOp = toolFileOperation(name, args);
|
|
185
|
+
if (fileOp) {
|
|
186
|
+
upsertEvidenceFileChange(snapshot, { path: fileOp.path, action: fileOp.action, toolName: name, firstSeenAt: now, lastSeenAt: now });
|
|
187
|
+
changed = true;
|
|
188
|
+
}
|
|
189
|
+
} else if (event.type === "tool_execution_end") {
|
|
190
|
+
const name = event.toolName ?? "tool";
|
|
191
|
+
if (name === "bash") {
|
|
192
|
+
upsertEvidenceCommand(snapshot, {
|
|
193
|
+
id: event.toolCallId,
|
|
194
|
+
at: now,
|
|
195
|
+
command: event.args?.command ?? "",
|
|
196
|
+
status: event.isError ? "failed" : "passed",
|
|
197
|
+
exitCode: event.isError ? 1 : 0,
|
|
198
|
+
outputPreview: truncate(toolResultText(event.result), 500),
|
|
199
|
+
});
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
if (event.isError) {
|
|
203
|
+
recordEvidenceError(snapshot, { at: now, source: "tool", toolName: name, message: `Tool ${name} failed` });
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
} else if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
207
|
+
const text = assistantText(event.message);
|
|
208
|
+
if (text) {
|
|
209
|
+
recordAssistantEvidence(snapshot, text, { at: now });
|
|
210
|
+
changed = true;
|
|
211
|
+
}
|
|
212
|
+
snapshot.usage = mergeUsage(snapshot.usage, event.message?.usage ?? event.usage ?? null);
|
|
213
|
+
} else if (event.usage) {
|
|
214
|
+
snapshot.usage = mergeUsage(snapshot.usage, event.usage);
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
if (changed) snapshot.updatedAt = now;
|
|
218
|
+
return changed;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** @param {import("./types.mjs").EvidenceSnapshot} snapshot @param {import("./types.mjs").RunStatus} status @param {number} now */
|
|
222
|
+
export function finalizeEvidence(snapshot, status, now = Date.now()) {
|
|
223
|
+
snapshot.outcome = status.semanticState === "idle" ? "ready" : status.semanticState;
|
|
224
|
+
// holding keeps ready=true (issue #145 spec D8): an on-hold row's evidence is
|
|
225
|
+
// reviewable like a completed one; `ready` feeds display + the review:ready
|
|
226
|
+
// filter only (verified — no functional gate reads it).
|
|
227
|
+
snapshot.ready = status.semanticState === "idle" || status.semanticState === "completed" || status.semanticState === "holding";
|
|
228
|
+
snapshot.usage = mergeUsage(snapshot.usage, status.usage ?? null);
|
|
229
|
+
snapshot.updatedAt = now;
|
|
230
|
+
return snapshot;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** @param {import("./types.mjs").EvidenceUsage|null} a @param {any} b */
|
|
234
|
+
function mergeUsage(a, b) {
|
|
235
|
+
if (!b || typeof b !== "object") return a ?? null;
|
|
236
|
+
const current = a ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
237
|
+
const input = Number(b.inputTokens ?? b.input_tokens ?? b.prompt_tokens ?? 0) || 0;
|
|
238
|
+
const output = Number(b.outputTokens ?? b.output_tokens ?? b.completion_tokens ?? 0) || 0;
|
|
239
|
+
const total = Number(b.totalTokens ?? b.total_tokens ?? input + output) || input + output;
|
|
240
|
+
return {
|
|
241
|
+
inputTokens: (current.inputTokens ?? 0) + input,
|
|
242
|
+
outputTokens: (current.outputTokens ?? 0) + output,
|
|
243
|
+
totalTokens: (current.totalTokens ?? 0) + total,
|
|
244
|
+
};
|
|
245
|
+
}
|