@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5
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 +1654 -0
- package/LICENSE +21 -0
- package/README.md +1533 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +199 -0
- package/dist/history/ingest.js +468 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +466 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/bridge.js +750 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +88 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +141 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +110 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +350 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom-bridge.md +212 -0
- package/docs/intercom.md +581 -0
- package/docs/performance.md +121 -0
- package/package.json +102 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/intercom-bridge-credentials.mjs +143 -0
- package/scripts/intercom-remote.mjs +103 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +209 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +156 -0
- package/src/history/ingest.ts +349 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +321 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/bridge.ts +668 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +82 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +124 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +100 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +415 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1260 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
5
|
+
import { formatRunAction } from "../../shared/status-format.ts";
|
|
6
|
+
import { NativeJournal, ownerProjection, readJsonProjection } from "../../shared/journal-reader.ts";
|
|
7
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
8
|
+
import { runCooperatively, runSynchronously } from "../../shared/cooperative.ts";
|
|
9
|
+
import { ASYNC_DIR, TEMP_ROOT_DIR, type AsyncStatus, type AsyncResultFile, type ResolvedAcceptanceConfig, type JsonSchemaObject, type OutputMode, type SavedLaunchConfig, type SingleResult, type AgentProgress } from "../../shared/types.ts";
|
|
10
|
+
|
|
11
|
+
export const LEGACY_QUESTIONS_DIR = path.join(TEMP_ROOT_DIR, "supervisor-questions");
|
|
12
|
+
export const QUESTIONS_DIR = path.join(getAgentDir(), "sessions", "subagent-runs");
|
|
13
|
+
|
|
14
|
+
export interface SupervisorRunContract {
|
|
15
|
+
legacySource?: string;
|
|
16
|
+
recordVersion?: 3;
|
|
17
|
+
nativeSessionId?: string;
|
|
18
|
+
terminalLeafId?: string | null;
|
|
19
|
+
terminalEntryId?: string;
|
|
20
|
+
attemptBaseline?: string[];
|
|
21
|
+
baselineSource?: "native-migration";
|
|
22
|
+
effectiveConfiguration?: { model?: string; thinking?: string; modelRecordedAt?: number };
|
|
23
|
+
accounting?: { state: "complete" | "incomplete"; error?: string };
|
|
24
|
+
auditPath?: string;
|
|
25
|
+
task?: string;
|
|
26
|
+
label?: string;
|
|
27
|
+
result?: SingleResult;
|
|
28
|
+
effectiveAcceptance?: ResolvedAcceptanceConfig;
|
|
29
|
+
output?: string | false;
|
|
30
|
+
outputMode?: OutputMode;
|
|
31
|
+
outputSchema?: JsonSchemaObject;
|
|
32
|
+
launch?: SavedLaunchConfig;
|
|
33
|
+
/** Kept independently of progress, which is compacted away or stops streaming after detachment. */
|
|
34
|
+
modelSelection?: Pick<AgentProgress, "model" | "thinking" | "modelStartedAt">;
|
|
35
|
+
sessionFile?: string;
|
|
36
|
+
pid?: number;
|
|
37
|
+
processIdentity?: string;
|
|
38
|
+
updatedAt?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SupervisorQuestion extends SupervisorRunContract {
|
|
42
|
+
questionId: string;
|
|
43
|
+
runId: string;
|
|
44
|
+
ownerSessionId: string;
|
|
45
|
+
ownerTarget: string;
|
|
46
|
+
agent: string;
|
|
47
|
+
index: number;
|
|
48
|
+
childSessionId: string;
|
|
49
|
+
childTarget: string;
|
|
50
|
+
sessionFile: string;
|
|
51
|
+
cwd: string;
|
|
52
|
+
pid: number;
|
|
53
|
+
createdAt: number;
|
|
54
|
+
reason: "need_decision" | "interview_request";
|
|
55
|
+
message: string;
|
|
56
|
+
interview?: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface QuestionAnswer {
|
|
60
|
+
message: string;
|
|
61
|
+
answeredAt: number;
|
|
62
|
+
origin?: "human";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface QuestionDelivery {
|
|
66
|
+
kind: "live" | "revive";
|
|
67
|
+
runId: string;
|
|
68
|
+
deliveredAt: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SupervisorQuestionView extends SupervisorQuestion {
|
|
72
|
+
state: "awaiting_input" | "answer_pending" | "answered" | "cancelled";
|
|
73
|
+
answer?: QuestionAnswer;
|
|
74
|
+
delivery?: QuestionDelivery;
|
|
75
|
+
revival?: { runId: string; pid: number; startedAt: number };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SupervisorQuestionSummary extends Pick<SupervisorQuestionView,
|
|
79
|
+
"questionId" | "runId" | "ownerSessionId" | "ownerTarget" | "agent" | "index" | "childSessionId" | "childTarget" |
|
|
80
|
+
"sessionFile" | "cwd" | "pid" | "processIdentity" | "createdAt" | "reason" | "message" | "state" | "answer" | "delivery" | "revival"> {
|
|
81
|
+
questionPath: string;
|
|
82
|
+
answerPath?: string;
|
|
83
|
+
messageTruncated?: boolean;
|
|
84
|
+
answerTruncated?: boolean;
|
|
85
|
+
hasInterview?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function summarizeSupervisorQuestion(question: SupervisorQuestionView): SupervisorQuestionSummary {
|
|
89
|
+
const directory = questionDir(question);
|
|
90
|
+
return {
|
|
91
|
+
questionId: question.questionId, runId: question.runId, ownerSessionId: question.ownerSessionId, ownerTarget: question.ownerTarget,
|
|
92
|
+
agent: question.agent, index: question.index, childSessionId: question.childSessionId, childTarget: question.childTarget,
|
|
93
|
+
sessionFile: question.sessionFile, cwd: question.cwd, pid: question.pid, processIdentity: question.processIdentity,
|
|
94
|
+
createdAt: question.createdAt, reason: question.reason, state: question.state, message: question.message.slice(0, 2048),
|
|
95
|
+
questionPath: path.join(directory, "question.json"),
|
|
96
|
+
...(question.message.length > 2048 ? { messageTruncated: true } : {}),
|
|
97
|
+
...(question.interview ? { hasInterview: true } : {}),
|
|
98
|
+
...(question.answer ? { answer: { message: question.answer.message.slice(0, 2048), answeredAt: question.answer.answeredAt, origin: question.answer.origin },
|
|
99
|
+
answerPath: path.join(directory, "answer.json"), ...(question.answer.message.length > 2048 ? { answerTruncated: true } : {}) } : {}),
|
|
100
|
+
...(question.delivery ? { delivery: { kind: question.delivery.kind, runId: question.delivery.runId, deliveredAt: question.delivery.deliveredAt } } : {}),
|
|
101
|
+
...(question.revival ? { revival: { runId: question.revival.runId, pid: question.revival.pid, startedAt: question.revival.startedAt } } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function supervisorQuestionPage(questions: SupervisorQuestionView[], options: { offset?: number; limit?: number } = {}): {
|
|
106
|
+
questions: SupervisorQuestionSummary[];
|
|
107
|
+
questionList: { total: number; offset: number; limit: number; nextOffset?: number };
|
|
108
|
+
} {
|
|
109
|
+
const offset = options.offset ?? 0, limit = options.limit ?? 20;
|
|
110
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Question offset must be a non-negative integer.");
|
|
111
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error("Question limit must be from 1 to 100.");
|
|
112
|
+
const pending = (question: SupervisorQuestionView) => question.state === "awaiting_input" || question.state === "answer_pending" ? 0 : 1;
|
|
113
|
+
const sorted = questions.toSorted((a, b) => pending(a) - pending(b) || a.createdAt - b.createdAt || a.questionId.localeCompare(b.questionId));
|
|
114
|
+
return { questions: sorted.slice(offset, offset + limit).map((question) => summarizeSupervisorQuestion(question)),
|
|
115
|
+
questionList: { total: questions.length, offset, limit, ...(offset + limit < questions.length ? { nextOffset: offset + limit } : {}) } };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function safeId(value: string): string {
|
|
119
|
+
if (typeof value !== "string" || !/^[a-zA-Z0-9_-]+$/.test(value)) throw new Error("Invalid run or question ID.");
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getRunMetadataDir(runId: string, root = QUESTIONS_DIR): string {
|
|
124
|
+
return path.join(root, safeId(runId));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function saveAsyncRunResult(runId: string, result: AsyncResultFile): AsyncResultFile {
|
|
128
|
+
const file = path.join(getRunMetadataDir(runId), "result.json");
|
|
129
|
+
const previous = readRunJson<AsyncResultFile>(file);
|
|
130
|
+
if (previous?.completionId && result.completionId && previous.completionId !== result.completionId) throw new Error("Conflicting completion identity");
|
|
131
|
+
const saved = { ...result, recordVersion: 3 as const, completionId: previous?.completionId ?? result.completionId ?? randomUUID(),
|
|
132
|
+
...(preserveLegacyOwner(file, previous) ? { legacySource: `${file}.legacy` } : {}),
|
|
133
|
+
results: result.results?.map((child, index) => compactOwnerResult(runId, index, child)) };
|
|
134
|
+
writeAtomicJson(file, saved);
|
|
135
|
+
return saved;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function preserveLegacyOwner(file: string, previous: { recordVersion?: number } | undefined): boolean {
|
|
139
|
+
if (!previous || previous.recordVersion === 3) return false;
|
|
140
|
+
try { fs.copyFileSync(file, `${file}.legacy`, fs.constants.COPYFILE_EXCL); }
|
|
141
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function compactOwnerResult<T extends { finalOutput?: string; output?: string; initialOutput?: string; initialOutputPath?: string; messages?: unknown; artifactPaths?: { outputPath?: string }; fullOutputPath?: string }>(runId: string, index: number, result: T, root = QUESTIONS_DIR): T {
|
|
146
|
+
const output = result.finalOutput ?? result.output;
|
|
147
|
+
let fullOutputPath = result.fullOutputPath;
|
|
148
|
+
if (output && output.length > 8192) {
|
|
149
|
+
fullOutputPath ??= result.artifactPaths?.outputPath;
|
|
150
|
+
if (!fullOutputPath) {
|
|
151
|
+
fullOutputPath = path.join(getRunMetadataDir(runId, root), "outputs", `${index}.txt`);
|
|
152
|
+
fs.mkdirSync(path.dirname(fullOutputPath), { recursive: true });
|
|
153
|
+
fs.writeFileSync(fullOutputPath, output, { mode: 0o600 });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
let initialOutputPath = result.initialOutputPath;
|
|
157
|
+
if (result.initialOutput && result.initialOutput.length > 8192 && !initialOutputPath) {
|
|
158
|
+
initialOutputPath = path.join(getRunMetadataDir(runId, root), "outputs", `${index}.initial.txt`);
|
|
159
|
+
fs.mkdirSync(path.dirname(initialOutputPath), { recursive: true });
|
|
160
|
+
fs.writeFileSync(initialOutputPath, result.initialOutput, { mode: 0o600 });
|
|
161
|
+
}
|
|
162
|
+
return { ...result, messages: undefined, ...(fullOutputPath ? { fullOutputPath } : {}),
|
|
163
|
+
...(initialOutputPath ? { initialOutputPath } : {}),
|
|
164
|
+
...(result.finalOutput !== undefined ? { finalOutput: result.finalOutput.slice(-8192) } : {}),
|
|
165
|
+
...(result.output !== undefined ? { output: result.output.slice(-8192) } : {}),
|
|
166
|
+
...(result.initialOutput !== undefined ? { initialOutput: result.initialOutput.slice(-8192) } : {}) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function saveRunStatus(runId: string, status: AsyncStatus): void {
|
|
170
|
+
writeAtomicJson(path.join(getRunMetadataDir(runId), "status.json"), status);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function readRunJson<T>(file: string): T | undefined {
|
|
174
|
+
try {
|
|
175
|
+
return readJsonProjection(file, ownerProjection) as T;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function questionDir(question: Pick<SupervisorQuestion, "runId" | "questionId">, root = QUESTIONS_DIR): string {
|
|
183
|
+
return path.join(root, safeId(question.runId), "questions", safeId(question.questionId));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Atomic publication without replacement: concurrent answers/launches have exactly one winner.
|
|
187
|
+
function writeOnce(file: string, value: object): boolean {
|
|
188
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
189
|
+
writeAtomicJson(temporary, value);
|
|
190
|
+
try {
|
|
191
|
+
fs.linkSync(temporary, file);
|
|
192
|
+
return true;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
|
|
195
|
+
throw error;
|
|
196
|
+
} finally {
|
|
197
|
+
fs.rmSync(temporary, { force: true });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function saveQuestionOwner(runId: string, sessionId: string, root = QUESTIONS_DIR): void {
|
|
202
|
+
writeAtomicJson(path.join(root, safeId(runId), "question-owner.json"), { sessionId });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function saveQuestionContract(runId: string, index: number, contract: SupervisorRunContract, root = QUESTIONS_DIR): void {
|
|
206
|
+
if (!Number.isSafeInteger(index) || index < 0) throw new Error("Child index must be a non-negative integer.");
|
|
207
|
+
const file = path.join(root, safeId(runId), "contracts", `${index}.json`);
|
|
208
|
+
const previous = readRunJson<SupervisorRunContract>(file);
|
|
209
|
+
writeAtomicJson(file, { ...previous, ...contract, recordVersion: 3,
|
|
210
|
+
...(preserveLegacyOwner(file, previous) ? { legacySource: `${file}.legacy` } : {}),
|
|
211
|
+
...(previous?.attemptBaseline && contract.baselineSource !== "native-migration" ? { attemptBaseline: previous.attemptBaseline } : {}),
|
|
212
|
+
...(contract.result ? { result: compactOwnerResult(runId, index, contract.result, root) } : {}),
|
|
213
|
+
...(previous?.launch ? { launch: previous.launch } : {}) });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function readQuestionContract(runId: string, index: number, root = QUESTIONS_DIR, projection: { sessionFile?: string; endedAt?: number; readConfiguration?: false } = {}): SupervisorRunContract | undefined {
|
|
217
|
+
const contract = readRunJson<SupervisorRunContract>(path.join(root, safeId(runId), "contracts", `${index}.json`));
|
|
218
|
+
if (!contract?.launch || projection.readConfiguration === false) return contract;
|
|
219
|
+
const sessionFile = projection.sessionFile ?? contract.sessionFile;
|
|
220
|
+
if (contract.recordVersion !== 3 && !contract.effectiveConfiguration && sessionFile && fs.existsSync(sessionFile) && !contract.launch.model?.startsWith("claude-code/")) {
|
|
221
|
+
// One-time read-only native recovery. Subsequent controls use the compact
|
|
222
|
+
// selection, and the frozen requested profile is never rewritten.
|
|
223
|
+
const journal = new NativeJournal(sessionFile, "inspect", true);
|
|
224
|
+
contract.effectiveConfiguration = journal.configuration(projection.endedAt, contract.terminalLeafId);
|
|
225
|
+
saveQuestionContract(runId, index, { effectiveConfiguration: contract.effectiveConfiguration }, root);
|
|
226
|
+
}
|
|
227
|
+
const native = contract.effectiveConfiguration ?? {};
|
|
228
|
+
return { ...contract, launch: { ...contract.launch, ...native } };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function migrateSupervisorQuestions(ownerSessionId: string, runId?: string): void {
|
|
232
|
+
runSynchronously(migrateSupervisorQuestionSteps(ownerSessionId, runId));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function* migrateSupervisorQuestionSteps(ownerSessionId: string, runId?: string): Generator<void> {
|
|
236
|
+
if (!fs.existsSync(LEGACY_QUESTIONS_DIR)) return;
|
|
237
|
+
const runs = runId === undefined ? fs.readdirSync(LEGACY_QUESTIONS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [safeId(runId)];
|
|
238
|
+
for (const id of runs) {
|
|
239
|
+
yield;
|
|
240
|
+
try {
|
|
241
|
+
const source = getRunMetadataDir(id, LEGACY_QUESTIONS_DIR);
|
|
242
|
+
const owner = readRunJson<{ sessionId?: string }>(path.join(source, "question-owner.json"));
|
|
243
|
+
if (owner?.sessionId !== ownerSessionId) continue;
|
|
244
|
+
fs.cpSync(source, getRunMetadataDir(id), { recursive: true, force: false });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (runId !== undefined) throw error;
|
|
247
|
+
console.error(`Could not recover legacy questions for ${id}: ${String(error)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function createSupervisorQuestion(input: Omit<SupervisorQuestion, "questionId" | "createdAt" | "ownerSessionId">, root = QUESTIONS_DIR): SupervisorQuestion {
|
|
253
|
+
const runDir = path.join(root, safeId(input.runId));
|
|
254
|
+
const status = readRunJson<AsyncStatus>(path.join(runDir, "status.json"))
|
|
255
|
+
?? (root === QUESTIONS_DIR ? readRunJson<AsyncStatus>(path.join(ASYNC_DIR, safeId(input.runId), "status.json")) : undefined);
|
|
256
|
+
const owner = readRunJson<{ sessionId: string }>(path.join(runDir, "question-owner.json")) ?? status;
|
|
257
|
+
if (!owner?.sessionId) throw new Error(`Run ${input.runId} has no saved question owner; cannot create a recoverable supervisor question.`);
|
|
258
|
+
if (!input.sessionFile || path.extname(input.sessionFile) !== ".jsonl") throw new Error("Supervisor questions require a saved child session.");
|
|
259
|
+
if (!Number.isSafeInteger(input.index) || input.index < 0) throw new Error("Child index must be a non-negative integer.");
|
|
260
|
+
if (!input.message.trim()) throw new Error("Supervisor question must not be empty.");
|
|
261
|
+
const contract = readQuestionContract(input.runId, input.index, root, { sessionFile: input.sessionFile })
|
|
262
|
+
?? { effectiveAcceptance: status?.steps?.[input.index]?.acceptance?.effectiveAcceptance };
|
|
263
|
+
const question = { ...input, ...contract, sessionFile: input.sessionFile, pid: input.pid, ownerSessionId: owner.sessionId, questionId: randomUUID(), createdAt: Date.now() };
|
|
264
|
+
writeAtomicJson(path.join(questionDir(question, root), "question.json"), question);
|
|
265
|
+
return question;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function questionStatePaths(question: SupervisorQuestion, file: string, root: string): string[] {
|
|
269
|
+
const current = path.join(questionDir(question, root), file);
|
|
270
|
+
const legacy = questionDir(question, LEGACY_QUESTIONS_DIR);
|
|
271
|
+
// Old waiters still claim the legacy file; both runtimes must compete on that same inode.
|
|
272
|
+
return root === QUESTIONS_DIR && fs.existsSync(path.join(legacy, "question.json")) ? [path.join(legacy, file), current] : [current];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function writeQuestionStateOnce(question: SupervisorQuestion, file: string, value: object, root: string): boolean {
|
|
276
|
+
const [primary, ...mirrors] = questionStatePaths(question, file, root);
|
|
277
|
+
const written = writeOnce(primary!, value);
|
|
278
|
+
const winner = written ? value : readRunJson<object>(primary!)!;
|
|
279
|
+
for (const target of mirrors) writeOnce(target, winner);
|
|
280
|
+
return written;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function readQuestionState(question: SupervisorQuestion, root = QUESTIONS_DIR): SupervisorQuestionView {
|
|
284
|
+
const read = <T>(file: string): T | undefined => questionStatePaths(question, file, root).map((entry) => readRunJson<T>(entry)).find((entry) => entry !== undefined);
|
|
285
|
+
const answer = read<QuestionAnswer>("answer.json");
|
|
286
|
+
const delivery = read<QuestionDelivery>("delivery.json");
|
|
287
|
+
const cancelled = read("cancelled.json") !== undefined;
|
|
288
|
+
const revival = read<SupervisorQuestionView["revival"]>("revival.json");
|
|
289
|
+
return { ...question, state: delivery ? "answered" : cancelled ? "cancelled" : answer ? "answer_pending" : "awaiting_input", ...(answer ? { answer } : {}), ...(delivery ? { delivery } : {}), ...(revival ? { revival } : {}) };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function listRunQuestions(runDir: string): SupervisorQuestionView[] {
|
|
293
|
+
let entries: fs.Dirent[];
|
|
294
|
+
try {
|
|
295
|
+
entries = fs.readdirSync(path.join(runDir, "questions"), { withFileTypes: true });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
return entries.filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
301
|
+
const question = readRunJson<SupervisorQuestion>(path.join(runDir, "questions", entry.name, "question.json"));
|
|
302
|
+
return question ? [readQuestionState(question, path.dirname(runDir))] : [];
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Known run IDs need neither a global directory walk nor prefix resolution. */
|
|
307
|
+
export function listOwnedRunQuestions(ownerSessionId: string, runId: string): SupervisorQuestionView[] {
|
|
308
|
+
migrateSupervisorQuestions(ownerSessionId, runId);
|
|
309
|
+
return listRunQuestions(getRunMetadataDir(runId)).filter((question) => question.ownerSessionId === ownerSessionId);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function pendingSupervisorQuestion(input: { runId: string; agent: string; index: number; sessionFile?: string; pid?: number }): import("../../shared/types.ts").ControlEvent["supervisorQuestion"] {
|
|
313
|
+
if (!input.sessionFile) return undefined;
|
|
314
|
+
try {
|
|
315
|
+
const pid = input.pid ?? readQuestionContract(input.runId, input.index)?.pid;
|
|
316
|
+
const question = listRunQuestions(getRunMetadataDir(input.runId)).find((question) =>
|
|
317
|
+
question.runId === input.runId && question.agent === input.agent && question.index === input.index
|
|
318
|
+
&& question.sessionFile === input.sessionFile && question.pid === pid
|
|
319
|
+
&& (question.state === "awaiting_input" || question.state === "answer_pending"));
|
|
320
|
+
if (question && (question.state === "awaiting_input" || question.state === "answer_pending")) return {
|
|
321
|
+
questionId: question.questionId, state: question.state, ...(question.answer ? { answer: question.answer.message } : {}),
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`Could not read supervisor wait for ${input.runId}:${input.index}:`, error);
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function listSupervisorQuestions(ownerSessionId: string, runId?: string, root = QUESTIONS_DIR): SupervisorQuestionView[] {
|
|
330
|
+
return runSynchronously(supervisorQuestionSteps(ownerSessionId, runId, root));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function listSupervisorQuestionsAsync(ownerSessionId: string): Promise<SupervisorQuestionView[]> {
|
|
334
|
+
return runCooperatively(supervisorQuestionSteps(ownerSessionId));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function* supervisorQuestionSteps(ownerSessionId: string, runId?: string, root = QUESTIONS_DIR): Generator<void, SupervisorQuestionView[]> {
|
|
338
|
+
if (runId !== undefined) safeId(runId);
|
|
339
|
+
if (root === QUESTIONS_DIR) yield* migrateSupervisorQuestionSteps(ownerSessionId);
|
|
340
|
+
if (!fs.existsSync(root)) return [];
|
|
341
|
+
const runs = fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (!runId || entry.name.startsWith(runId)));
|
|
342
|
+
const questions: SupervisorQuestionView[] = [];
|
|
343
|
+
for (const entry of runs) {
|
|
344
|
+
yield;
|
|
345
|
+
try { questions.push(...listRunQuestions(path.join(root, entry.name)).filter((question) => question.ownerSessionId === ownerSessionId)); }
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (runId !== undefined) throw error;
|
|
348
|
+
console.error(`Could not list questions for ${entry.name}: ${String(error)}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (runId && new Set(questions.map((question) => question.runId)).size > 1) throw new Error(`Ambiguous run ID prefix '${runId}'.`);
|
|
352
|
+
return questions.sort((a, b) => a.createdAt - b.createdAt);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function saveQuestionAnswer(question: SupervisorQuestion, message: string, root = QUESTIONS_DIR, origin?: "human"): QuestionAnswer {
|
|
356
|
+
if (typeof message !== "string" || !message.trim()) throw new Error("action='answer' requires a non-empty message.");
|
|
357
|
+
const state = readQuestionState(question, root);
|
|
358
|
+
if (state.state === "cancelled") throw new Error(`Question ${question.questionId} was cancelled. Use continue for a new follow-up.`);
|
|
359
|
+
const answer: QuestionAnswer = { message: message.trim(), answeredAt: state.answer?.answeredAt ?? Date.now(), ...(state.answer?.origin ?? origin ? { origin: state.answer?.origin ?? origin } : {}) };
|
|
360
|
+
if (state.answer && state.answer.message !== answer.message) throw new Error(`Question ${question.questionId} already has a different saved answer. The original answer was retained.`);
|
|
361
|
+
if (writeQuestionStateOnce(question, "answer.json", answer, root)) return answer;
|
|
362
|
+
const existing = readQuestionState(question, root).answer!;
|
|
363
|
+
if (existing.message !== answer.message) throw new Error(`Question ${question.questionId} already has a different saved answer. The original answer was retained.`);
|
|
364
|
+
return existing;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function recordQuestionDelivery(question: SupervisorQuestion, delivery: QuestionDelivery, root = QUESTIONS_DIR): void {
|
|
368
|
+
writeQuestionStateOnce(question, "delivery.json", delivery, root);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function cancelSupervisorQuestion(question: SupervisorQuestion, root = QUESTIONS_DIR): void {
|
|
372
|
+
writeQuestionStateOnce(question, "cancelled.json", { cancelledAt: Date.now() }, root);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function questionProcessAlive(question: Pick<SupervisorQuestion, "pid">): boolean {
|
|
376
|
+
if (!Number.isSafeInteger(question.pid) || question.pid <= 0) throw new Error("Invalid child process ID.");
|
|
377
|
+
try {
|
|
378
|
+
process.kill(question.pid, 0);
|
|
379
|
+
return true;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function claimQuestionRevival(question: SupervisorQuestion, root = QUESTIONS_DIR): { claimed: boolean; runId: string } {
|
|
386
|
+
const runId = `answer-${question.questionId}`;
|
|
387
|
+
if (readQuestionState(question, root).revival) return { claimed: false, runId };
|
|
388
|
+
return { claimed: writeQuestionStateOnce(question, "revival.json", { runId, pid: process.pid, startedAt: Date.now() }, root), runId };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function releaseQuestionRevival(question: SupervisorQuestion, root = QUESTIONS_DIR): void {
|
|
392
|
+
for (const file of questionStatePaths(question, "revival.json", root)) fs.rmSync(file, { force: true });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function questionRecoveryHint(question: SupervisorQuestion, childSafe = false): string {
|
|
396
|
+
return `Recover an unlaunched continuation: ${formatRunAction("resume", question.runId, { index: question.index, message: "Continue with the saved supervisor answer." }, childSafe)}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function formatSupervisorQuestions(questions: SupervisorQuestionSummary[], childSafe = false): string {
|
|
400
|
+
if (!questions.length) return "No supervisor questions owned by this session.";
|
|
401
|
+
return questions.map((question) => [
|
|
402
|
+
`Question: ${question.questionId} | ${question.state}`,
|
|
403
|
+
`Run: ${question.runId} | Child: ${question.index} (${question.agent}) | Owner: ${question.ownerSessionId}`,
|
|
404
|
+
`Session: ${question.sessionFile} | Child target: ${question.childTarget}`,
|
|
405
|
+
question.message,
|
|
406
|
+
...(question.messageTruncated ? [`[Question preview truncated. Full question: ${question.questionPath}]`] : []),
|
|
407
|
+
...(question.hasInterview ? [`Structured interview: ${question.questionPath}`] : []),
|
|
408
|
+
...(question.answer ? [`Saved answer: ${question.answer.message}`, ...(question.answerTruncated ? [`[Answer preview truncated. Exact saved answer: ${question.answerPath}]`] : [])] : []),
|
|
409
|
+
...(question.revival && !question.delivery ? [`Continuation requested: ${question.revival.runId}; delivery unconfirmed. Answer retained.`, questionRecoveryHint(question, childSafe)] : []),
|
|
410
|
+
question.delivery ? `Answer delivered via ${question.delivery.kind}; run: ${question.delivery.runId}`
|
|
411
|
+
: question.state === "cancelled" ? "Question cancelled; use continue for a new follow-up."
|
|
412
|
+
: question.answerTruncated ? `Retry answer with the exact saved message from ${question.answerPath}; a preview is not a valid replacement.`
|
|
413
|
+
: `Answer: ${formatRunAction("answer", question.runId, { questionId: question.questionId, message: question.answer?.message ?? "..." }, childSafe)}`,
|
|
414
|
+
].join("\n")).join("\n\n");
|
|
415
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
|
|
2
|
+
import type { SingleResult, SubagentRunMode, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowNodeStatus } from "../../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
/** Pending fanouts keep their own slot until their actual children are known. */
|
|
5
|
+
export function workflowAgentNodes(graph: WorkflowGraphSnapshot): WorkflowGraphNode[] {
|
|
6
|
+
return graph.nodes.flatMap((node) => node.children?.length ? node.children
|
|
7
|
+
: node.kind === "dynamic-parallel-group" && node.status !== "completed" && node.status !== "complete" ? [node]
|
|
8
|
+
: node.children ?? [node]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkflowGraphBuildInput {
|
|
12
|
+
runId: string;
|
|
13
|
+
mode?: SubagentRunMode;
|
|
14
|
+
steps: ChainStep[];
|
|
15
|
+
results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "timedOut" | "error" | "acceptance">>;
|
|
16
|
+
currentFlatIndex?: number;
|
|
17
|
+
currentStepIndex?: number;
|
|
18
|
+
stepStatuses?: Array<{ status?: string; error?: string }>;
|
|
19
|
+
dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
|
|
20
|
+
dynamicGroupStatuses?: Record<number, { status: WorkflowNodeStatus; error?: string; acceptance?: SingleResult["acceptance"] }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undefined {
|
|
24
|
+
switch (status) {
|
|
25
|
+
case "complete":
|
|
26
|
+
case "completed":
|
|
27
|
+
return "completed";
|
|
28
|
+
case "running":
|
|
29
|
+
return "running";
|
|
30
|
+
case "failed":
|
|
31
|
+
return "failed";
|
|
32
|
+
case "paused":
|
|
33
|
+
return "paused";
|
|
34
|
+
case "blocked":
|
|
35
|
+
return "blocked";
|
|
36
|
+
case "detached":
|
|
37
|
+
return "detached";
|
|
38
|
+
case "timed-out":
|
|
39
|
+
return "timed-out";
|
|
40
|
+
case "pending":
|
|
41
|
+
return "pending";
|
|
42
|
+
default:
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "timedOut" | "acceptance"> | undefined): WorkflowNodeStatus | undefined {
|
|
48
|
+
if (!result) return undefined;
|
|
49
|
+
if (result.detached) return "detached";
|
|
50
|
+
if (result.timedOut) return "timed-out";
|
|
51
|
+
if (result.interrupted) return "paused";
|
|
52
|
+
return result.exitCode === 0 ? result.acceptance?.status === "blocked" ? "blocked" : "completed" : "failed";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function nodeStatus(input: WorkflowGraphBuildInput, flatIndex: number): WorkflowNodeStatus {
|
|
56
|
+
return normalizeStatus(input.stepStatuses?.[flatIndex]?.status)
|
|
57
|
+
?? resultStatus(input.results?.[flatIndex])
|
|
58
|
+
?? (input.currentFlatIndex === flatIndex ? "running" : "pending");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pushPhase(phases: WorkflowGraphSnapshot["phases"], phase: string | undefined, nodeId: string): void {
|
|
62
|
+
if (!phase) return;
|
|
63
|
+
let group = phases.find((candidate) => candidate.title === phase);
|
|
64
|
+
if (!group) {
|
|
65
|
+
group = { title: phase, nodeIds: [] };
|
|
66
|
+
phases.push(group);
|
|
67
|
+
}
|
|
68
|
+
group.nodeIds.push(nodeId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function seqLabel(step: SequentialStep, stepIndex: number): string {
|
|
72
|
+
return step.label?.trim() || step.agent || `Step ${stepIndex + 1}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function summarizeParallelStatuses(statuses: WorkflowNodeStatus[]): WorkflowNodeStatus {
|
|
76
|
+
if (statuses.some((status) => status === "running")) return "running";
|
|
77
|
+
if (statuses.some((status) => status === "failed")) return "failed";
|
|
78
|
+
if (statuses.some((status) => status === "timed-out")) return "timed-out";
|
|
79
|
+
if (statuses.some((status) => status === "blocked")) return "blocked";
|
|
80
|
+
if (statuses.some((status) => status === "paused")) return "paused";
|
|
81
|
+
if (statuses.some((status) => status === "detached")) return "detached";
|
|
82
|
+
if (statuses.length > 0 && statuses.every((status) => status === "completed")) return "completed";
|
|
83
|
+
if (statuses.some((status) => status === "completed")) return "running";
|
|
84
|
+
return "pending";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildWorkflowGraphSnapshot(input: WorkflowGraphBuildInput): WorkflowGraphSnapshot {
|
|
88
|
+
const nodes: WorkflowGraphNode[] = [];
|
|
89
|
+
const phases: WorkflowGraphSnapshot["phases"] = [];
|
|
90
|
+
let flatIndex = 0;
|
|
91
|
+
let currentNodeId: string | undefined;
|
|
92
|
+
|
|
93
|
+
for (let stepIndex = 0; stepIndex < input.steps.length; stepIndex++) {
|
|
94
|
+
const step = input.steps[stepIndex]!;
|
|
95
|
+
if (isParallelStep(step)) {
|
|
96
|
+
const groupId = `step-${stepIndex}`;
|
|
97
|
+
const children: WorkflowGraphNode[] = [];
|
|
98
|
+
const childStatuses: WorkflowNodeStatus[] = [];
|
|
99
|
+
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
100
|
+
const task = step.parallel[taskIndex]!;
|
|
101
|
+
const status = nodeStatus(input, flatIndex);
|
|
102
|
+
childStatuses.push(status);
|
|
103
|
+
const childId = `step-${stepIndex}-agent-${taskIndex}`;
|
|
104
|
+
const child: WorkflowGraphNode = {
|
|
105
|
+
id: childId,
|
|
106
|
+
kind: "agent",
|
|
107
|
+
agent: task.agent,
|
|
108
|
+
phase: task.phase,
|
|
109
|
+
label: task.label?.trim() || task.agent || `Agent ${taskIndex + 1}`,
|
|
110
|
+
status,
|
|
111
|
+
flatIndex,
|
|
112
|
+
stepIndex,
|
|
113
|
+
outputName: task.as,
|
|
114
|
+
structured: Boolean(task.outputSchema),
|
|
115
|
+
acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
|
|
116
|
+
error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error,
|
|
117
|
+
};
|
|
118
|
+
children.push(child);
|
|
119
|
+
pushPhase(phases, task.phase, childId);
|
|
120
|
+
if (status === "running" || input.currentFlatIndex === flatIndex) currentNodeId = childId;
|
|
121
|
+
flatIndex++;
|
|
122
|
+
}
|
|
123
|
+
const groupStatus = summarizeParallelStatuses(childStatuses);
|
|
124
|
+
if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
|
|
125
|
+
nodes.push({
|
|
126
|
+
id: groupId,
|
|
127
|
+
kind: "parallel-group",
|
|
128
|
+
label: step.parallel.length === 1 ? "Parallel task" : `Parallel group (${step.parallel.length})`,
|
|
129
|
+
status: groupStatus,
|
|
130
|
+
stepIndex,
|
|
131
|
+
children,
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isDynamicParallelStep(step)) {
|
|
137
|
+
const groupId = `step-${stepIndex}`;
|
|
138
|
+
const materialized = input.dynamicChildren?.[stepIndex] ?? [];
|
|
139
|
+
const groupOverride = input.dynamicGroupStatuses?.[stepIndex];
|
|
140
|
+
const children: WorkflowGraphNode[] = [];
|
|
141
|
+
const childStatuses: WorkflowNodeStatus[] = [];
|
|
142
|
+
for (let taskIndex = 0; taskIndex < materialized.length; taskIndex++) {
|
|
143
|
+
const task = materialized[taskIndex]!;
|
|
144
|
+
const status = nodeStatus(input, task.flatIndex);
|
|
145
|
+
childStatuses.push(status);
|
|
146
|
+
const childId = `step-${stepIndex}-item-${task.itemKey.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
147
|
+
const child: WorkflowGraphNode = {
|
|
148
|
+
id: childId,
|
|
149
|
+
kind: "agent",
|
|
150
|
+
agent: task.agent,
|
|
151
|
+
phase: step.parallel.phase ?? step.phase,
|
|
152
|
+
label: task.label?.trim() || step.parallel.label?.trim() || `${task.agent} ${task.itemKey}`,
|
|
153
|
+
status,
|
|
154
|
+
flatIndex: task.flatIndex,
|
|
155
|
+
stepIndex,
|
|
156
|
+
itemKey: task.itemKey,
|
|
157
|
+
outputName: task.outputName,
|
|
158
|
+
structured: task.structured,
|
|
159
|
+
acceptanceStatus: input.results?.[task.flatIndex]?.acceptance?.status,
|
|
160
|
+
error: input.stepStatuses?.[task.flatIndex]?.error ?? input.results?.[task.flatIndex]?.error ?? task.error,
|
|
161
|
+
};
|
|
162
|
+
children.push(child);
|
|
163
|
+
pushPhase(phases, child.phase, childId);
|
|
164
|
+
if (status === "running" || input.currentFlatIndex === task.flatIndex) currentNodeId = childId;
|
|
165
|
+
}
|
|
166
|
+
const groupStatus = groupOverride?.status ?? (children.length > 0 ? summarizeParallelStatuses(childStatuses) : (input.currentStepIndex === stepIndex ? "running" : "pending"));
|
|
167
|
+
if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
|
|
168
|
+
nodes.push({
|
|
169
|
+
id: groupId,
|
|
170
|
+
kind: "dynamic-parallel-group",
|
|
171
|
+
label: step.label?.trim() || step.parallel.label?.trim() || `Dynamic fanout (${step.collect.as})`,
|
|
172
|
+
status: groupStatus,
|
|
173
|
+
stepIndex,
|
|
174
|
+
outputName: step.collect.as,
|
|
175
|
+
structured: Boolean(step.collect.outputSchema),
|
|
176
|
+
acceptanceStatus: groupOverride?.acceptance?.status,
|
|
177
|
+
error: groupOverride?.error,
|
|
178
|
+
dynamic: {
|
|
179
|
+
sourceOutput: step.expand.from.output,
|
|
180
|
+
sourcePath: step.expand.from.path,
|
|
181
|
+
itemName: step.expand.item ?? "item",
|
|
182
|
+
maxItems: step.expand.maxItems,
|
|
183
|
+
collectAs: step.collect.as,
|
|
184
|
+
},
|
|
185
|
+
children,
|
|
186
|
+
});
|
|
187
|
+
if (materialized.length > 0) flatIndex = Math.max(flatIndex, ...materialized.map((child) => child.flatIndex + 1));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const seq = step as SequentialStep;
|
|
192
|
+
const status = nodeStatus(input, flatIndex);
|
|
193
|
+
const id = `step-${stepIndex}`;
|
|
194
|
+
nodes.push({
|
|
195
|
+
id,
|
|
196
|
+
kind: "step",
|
|
197
|
+
agent: seq.agent,
|
|
198
|
+
phase: seq.phase,
|
|
199
|
+
label: seqLabel(seq, stepIndex),
|
|
200
|
+
status,
|
|
201
|
+
flatIndex,
|
|
202
|
+
stepIndex,
|
|
203
|
+
outputName: seq.as,
|
|
204
|
+
structured: Boolean(seq.outputSchema),
|
|
205
|
+
acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
|
|
206
|
+
error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error,
|
|
207
|
+
});
|
|
208
|
+
pushPhase(phases, seq.phase, id);
|
|
209
|
+
if (status === "running" || input.currentFlatIndex === flatIndex || input.currentStepIndex === stepIndex) currentNodeId = id;
|
|
210
|
+
flatIndex++;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
runId: input.runId,
|
|
215
|
+
mode: input.mode ?? "chain",
|
|
216
|
+
phases,
|
|
217
|
+
nodes,
|
|
218
|
+
currentNodeId,
|
|
219
|
+
};
|
|
220
|
+
}
|