pi-agent-flow 2.2.14 → 2.2.16
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/README.md +12 -0
- package/dist/batch/apply-patch.d.ts +2 -1
- package/dist/batch/apply-patch.d.ts.map +1 -1
- package/dist/batch/batch-bash.d.ts.map +1 -1
- package/dist/batch/batch-bash.js +9 -43
- package/dist/batch/batch-bash.js.map +1 -1
- package/dist/batch/constants.d.ts +9 -1
- package/dist/batch/constants.d.ts.map +1 -1
- package/dist/batch/constants.js +7 -0
- package/dist/batch/constants.js.map +1 -1
- package/dist/batch/execute.d.ts +1 -13
- package/dist/batch/execute.d.ts.map +1 -1
- package/dist/batch/execute.js +87 -28
- package/dist/batch/execute.js.map +1 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +16 -14
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/render.d.ts.map +1 -1
- package/dist/batch/render.js.map +1 -1
- package/dist/batch/summary.d.ts +0 -6
- package/dist/batch/summary.d.ts.map +1 -1
- package/dist/batch/summary.js +3 -3
- package/dist/batch/summary.js.map +1 -1
- package/dist/batch/symbols.d.ts.map +1 -1
- package/dist/batch/symbols.js +3 -1
- package/dist/batch/symbols.js.map +1 -1
- package/dist/batch/truncate-output.d.ts +31 -0
- package/dist/batch/truncate-output.d.ts.map +1 -0
- package/dist/batch/truncate-output.js +120 -0
- package/dist/batch/truncate-output.js.map +1 -0
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +10 -24
- package/dist/config/config.js.map +1 -1
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +7 -5
- package/dist/config/models.js.map +1 -1
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +9 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/resolver-helpers.d.ts +44 -0
- package/dist/config/resolver-helpers.d.ts.map +1 -0
- package/dist/config/resolver-helpers.js +135 -0
- package/dist/config/resolver-helpers.js.map +1 -0
- package/dist/config/settings-resolver.d.ts +0 -6
- package/dist/config/settings-resolver.d.ts.map +1 -1
- package/dist/config/settings-resolver.js +27 -256
- package/dist/config/settings-resolver.js.map +1 -1
- package/dist/core2/snapshot.d.ts +0 -2
- package/dist/core2/snapshot.d.ts.map +1 -1
- package/dist/core2/snapshot.js +91 -24
- package/dist/core2/snapshot.js.map +1 -1
- package/dist/flow/agents.d.ts +1 -1
- package/dist/flow/agents.d.ts.map +1 -1
- package/dist/flow/agents.js +18 -9
- package/dist/flow/agents.js.map +1 -1
- package/dist/flow/audit-formatters.d.ts +20 -0
- package/dist/flow/audit-formatters.d.ts.map +1 -0
- package/dist/flow/audit-formatters.js +123 -0
- package/dist/flow/audit-formatters.js.map +1 -0
- package/dist/flow/complexity.d.ts +3 -2
- package/dist/flow/complexity.d.ts.map +1 -1
- package/dist/flow/complexity.js +1 -1
- package/dist/flow/complexity.js.map +1 -1
- package/dist/flow/continuation.d.ts.map +1 -1
- package/dist/flow/continuation.js +6 -4
- package/dist/flow/continuation.js.map +1 -1
- package/dist/flow/cycle-guard.d.ts +20 -0
- package/dist/flow/cycle-guard.d.ts.map +1 -0
- package/dist/flow/cycle-guard.js +56 -0
- package/dist/flow/cycle-guard.js.map +1 -0
- package/dist/flow/depth.d.ts +0 -1
- package/dist/flow/depth.d.ts.map +1 -1
- package/dist/flow/depth.js +3 -2
- package/dist/flow/depth.js.map +1 -1
- package/dist/flow/dump-io.d.ts +15 -0
- package/dist/flow/dump-io.d.ts.map +1 -0
- package/dist/flow/dump-io.js +169 -0
- package/dist/flow/dump-io.js.map +1 -0
- package/dist/flow/execute-single.d.ts +8 -0
- package/dist/flow/execute-single.d.ts.map +1 -0
- package/dist/flow/execute-single.js +132 -0
- package/dist/flow/execute-single.js.map +1 -0
- package/dist/flow/executor.d.ts +3 -62
- package/dist/flow/executor.d.ts.map +1 -1
- package/dist/flow/executor.js +36 -342
- package/dist/flow/executor.js.map +1 -1
- package/dist/flow/flow-args.d.ts +14 -0
- package/dist/flow/flow-args.d.ts.map +1 -0
- package/dist/flow/flow-args.js +119 -0
- package/dist/flow/flow-args.js.map +1 -0
- package/dist/flow/flow-live.d.ts +18 -0
- package/dist/flow/flow-live.d.ts.map +1 -0
- package/dist/flow/flow-live.js +46 -0
- package/dist/flow/flow-live.js.map +1 -0
- package/dist/flow/process-lifecycle.d.ts +28 -0
- package/dist/flow/process-lifecycle.d.ts.map +1 -0
- package/dist/flow/process-lifecycle.js +114 -0
- package/dist/flow/process-lifecycle.js.map +1 -0
- package/dist/flow/runner.d.ts +4 -54
- package/dist/flow/runner.d.ts.map +1 -1
- package/dist/flow/runner.js +55 -517
- package/dist/flow/runner.js.map +1 -1
- package/dist/flow/settings-command.d.ts +2 -24
- package/dist/flow/settings-command.d.ts.map +1 -1
- package/dist/flow/settings-command.js +14 -716
- package/dist/flow/settings-command.js.map +1 -1
- package/dist/flow/settings-handler.d.ts +9 -0
- package/dist/flow/settings-handler.d.ts.map +1 -0
- package/dist/flow/settings-handler.js +377 -0
- package/dist/flow/settings-handler.js.map +1 -0
- package/dist/flow/settings-items.d.ts +42 -0
- package/dist/flow/settings-items.d.ts.map +1 -0
- package/dist/flow/settings-items.js +380 -0
- package/dist/flow/settings-items.js.map +1 -0
- package/dist/flow/spawn-utils.d.ts +17 -0
- package/dist/flow/spawn-utils.d.ts.map +1 -0
- package/dist/flow/spawn-utils.js +40 -0
- package/dist/flow/spawn-utils.js.map +1 -0
- package/dist/flow/store.d.ts.map +1 -1
- package/dist/flow/store.js +2 -8
- package/dist/flow/store.js.map +1 -1
- package/dist/flow/transition.d.ts +0 -1
- package/dist/flow/transition.d.ts.map +1 -1
- package/dist/flow/transition.js +0 -3
- package/dist/flow/transition.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +94 -57
- package/dist/index.js.map +1 -1
- package/dist/io/atomic-write.d.ts +10 -0
- package/dist/io/atomic-write.d.ts.map +1 -0
- package/dist/io/atomic-write.js +30 -0
- package/dist/io/atomic-write.js.map +1 -0
- package/dist/notify/notify.d.ts.map +1 -1
- package/dist/notify/notify.js +1 -4
- package/dist/notify/notify.js.map +1 -1
- package/dist/snapshot/cli-args.d.ts +1 -2
- package/dist/snapshot/cli-args.d.ts.map +1 -1
- package/dist/snapshot/runner-events.d.ts +10 -0
- package/dist/snapshot/runner-events.d.ts.map +1 -1
- package/dist/snapshot/runner-events.js +112 -30
- package/dist/snapshot/runner-events.js.map +1 -1
- package/dist/snapshot/structured-output.d.ts.map +1 -1
- package/dist/snapshot/structured-output.js +3 -1
- package/dist/snapshot/structured-output.js.map +1 -1
- package/dist/snapshot/trace-output.d.ts.map +1 -1
- package/dist/snapshot/trace-output.js +8 -4
- package/dist/snapshot/trace-output.js.map +1 -1
- package/dist/steering/sliding-prompt.d.ts.map +1 -1
- package/dist/steering/sliding-prompt.js +12 -15
- package/dist/steering/sliding-prompt.js.map +1 -1
- package/dist/steering/tool-utils.d.ts +3 -3
- package/dist/steering/tool-utils.d.ts.map +1 -1
- package/dist/steering/tool-utils.js +3 -3
- package/dist/steering/tool-utils.js.map +1 -1
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/timed-bash.d.ts.map +1 -1
- package/dist/tools/timed-bash.js +8 -3
- package/dist/tools/timed-bash.js.map +1 -1
- package/dist/tools/trace.d.ts +0 -1
- package/dist/tools/trace.d.ts.map +1 -1
- package/dist/tools/trace.js +79 -62
- package/dist/tools/trace.js.map +1 -1
- package/dist/tools/web-ops.d.ts +0 -49
- package/dist/tools/web-ops.d.ts.map +1 -1
- package/dist/tools/web-ops.js +9 -9
- package/dist/tools/web-ops.js.map +1 -1
- package/dist/tui/body-render.d.ts +31 -0
- package/dist/tui/body-render.d.ts.map +1 -0
- package/dist/tui/body-render.js +589 -0
- package/dist/tui/body-render.js.map +1 -0
- package/dist/tui/context-display.d.ts +18 -0
- package/dist/tui/context-display.d.ts.map +1 -0
- package/dist/tui/context-display.js +29 -0
- package/dist/tui/context-display.js.map +1 -0
- package/dist/tui/flow-colors.d.ts +6 -1
- package/dist/tui/flow-colors.d.ts.map +1 -1
- package/dist/tui/flow-colors.js +1 -0
- package/dist/tui/flow-colors.js.map +1 -1
- package/dist/tui/flow-live-state.d.ts +36 -0
- package/dist/tui/flow-live-state.d.ts.map +1 -0
- package/dist/tui/flow-live-state.js +53 -0
- package/dist/tui/flow-live-state.js.map +1 -0
- package/dist/tui/grouping.d.ts +27 -0
- package/dist/tui/grouping.d.ts.map +1 -0
- package/dist/tui/grouping.js +191 -0
- package/dist/tui/grouping.js.map +1 -0
- package/dist/tui/header.d.ts +25 -0
- package/dist/tui/header.d.ts.map +1 -0
- package/dist/tui/header.js +95 -0
- package/dist/tui/header.js.map +1 -0
- package/dist/tui/render-utils.d.ts +1 -0
- package/dist/tui/render-utils.d.ts.map +1 -1
- package/dist/tui/render-utils.js +4 -5
- package/dist/tui/render-utils.js.map +1 -1
- package/dist/tui/render.d.ts +19 -33
- package/dist/tui/render.d.ts.map +1 -1
- package/dist/tui/render.js +112 -1014
- package/dist/tui/render.js.map +1 -1
- package/dist/tui/scramble/animation-status.d.ts +5 -0
- package/dist/tui/scramble/animation-status.d.ts.map +1 -0
- package/dist/tui/scramble/animation-status.js +93 -0
- package/dist/tui/scramble/animation-status.js.map +1 -0
- package/dist/tui/scramble/constants.d.ts +0 -4
- package/dist/tui/scramble/constants.d.ts.map +1 -1
- package/dist/tui/scramble/constants.js +3 -3
- package/dist/tui/scramble/constants.js.map +1 -1
- package/dist/tui/scramble/dynamic-text.d.ts +10 -0
- package/dist/tui/scramble/dynamic-text.d.ts.map +1 -0
- package/dist/tui/scramble/dynamic-text.js +19 -0
- package/dist/tui/scramble/dynamic-text.js.map +1 -0
- package/dist/tui/scramble/index.d.ts +8 -2
- package/dist/tui/scramble/index.d.ts.map +1 -1
- package/dist/tui/scramble/index.js +8 -1
- package/dist/tui/scramble/index.js.map +1 -1
- package/dist/tui/scramble/lifecycle.d.ts +4 -0
- package/dist/tui/scramble/lifecycle.d.ts.map +1 -0
- package/dist/tui/scramble/lifecycle.js +75 -0
- package/dist/tui/scramble/lifecycle.js.map +1 -0
- package/dist/tui/scramble/line-state.d.ts +5 -0
- package/dist/tui/scramble/line-state.d.ts.map +1 -0
- package/dist/tui/scramble/line-state.js +99 -0
- package/dist/tui/scramble/line-state.js.map +1 -0
- package/dist/tui/scramble/manager.d.ts +3 -14
- package/dist/tui/scramble/manager.d.ts.map +1 -1
- package/dist/tui/scramble/manager.js +22 -405
- package/dist/tui/scramble/manager.js.map +1 -1
- package/dist/tui/scramble/singleton.d.ts +3 -0
- package/dist/tui/scramble/singleton.d.ts.map +1 -0
- package/dist/tui/scramble/singleton.js +3 -0
- package/dist/tui/scramble/singleton.js.map +1 -0
- package/dist/tui/scramble/timer.d.ts +6 -0
- package/dist/tui/scramble/timer.d.ts.map +1 -0
- package/dist/tui/scramble/timer.js +28 -0
- package/dist/tui/scramble/timer.js.map +1 -0
- package/dist/tui/scramble/utils.d.ts +0 -2
- package/dist/tui/scramble/utils.d.ts.map +1 -1
- package/dist/tui/scramble/utils.js +0 -9
- package/dist/tui/scramble/utils.js.map +1 -1
- package/dist/tui/scramble/value-flash.d.ts +6 -0
- package/dist/tui/scramble/value-flash.d.ts.map +1 -0
- package/dist/tui/scramble/value-flash.js +141 -0
- package/dist/tui/scramble/value-flash.js.map +1 -0
- package/dist/tui/traces.d.ts +13 -0
- package/dist/tui/traces.d.ts.map +1 -0
- package/dist/tui/traces.js +97 -0
- package/dist/tui/traces.js.map +1 -0
- package/dist/types/ui.d.ts +0 -4
- package/dist/types/ui.d.ts.map +1 -1
- package/dist/types/ui.js +1 -30
- package/dist/types/ui.js.map +1 -1
- package/package.json +5 -1
package/dist/flow/runner.js
CHANGED
|
@@ -9,12 +9,21 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { getFlowTier } from "./agents.js";
|
|
12
|
-
import {
|
|
13
|
-
import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate, drainToolCallEstimate, drainCtxEstimate, updateSmoothedTps, drainSmoothedTps } from "../snapshot/runner-events.js";
|
|
12
|
+
import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate, drainToolCallEstimate, drainCtxEstimate, updateSmoothedTps, drainSmoothedTps, getCtxState } from "../snapshot/runner-events.js";
|
|
14
13
|
import { emptyFlowUsage, getFlowOutput, normalizeFlowResult, } from "../types/flow.js";
|
|
14
|
+
import { parseSharedContext } from "../core2/snapshot.js";
|
|
15
15
|
import { extractStructuredOutput, generateCommandsFromHistory } from "../snapshot/structured-output.js";
|
|
16
|
+
import { computeInitialContextTokens, mergeStreamingContextTokens } from "../tui/context-display.js";
|
|
16
17
|
import { logWarn, logError } from '../config/log.js';
|
|
18
|
+
import { atomicWriteFileSync } from "../io/atomic-write.js";
|
|
17
19
|
import { DEFAULT_COMPLEXITY, getComplexityTimeoutMs } from "./complexity.js";
|
|
20
|
+
import { makeUniqueDumpPath, makeUniqueDumpTxtPath, cleanupStaleDumps, cleanupStaleDebugDumps, writeReminderFile, getDebugDir, resolveDumpMaxAgeHours, FLOW_DUMP_SNAPSHOT_ENV, } from "./dump-io.js";
|
|
21
|
+
import { registerChildGroup, unregisterChildGroup, terminateChildProcess, SIGKILL_TIMEOUT_MS, isWindows, } from "./process-lifecycle.js";
|
|
22
|
+
import { buildFlowArgs, inheritedCliArgs } from "./flow-args.js";
|
|
23
|
+
import { resolveFlowSpawn, writeFlowSessionToTempFile, cleanupFlowTempDir } from "./spawn-utils.js";
|
|
24
|
+
export { cleanupStaleDumps } from "./dump-io.js";
|
|
25
|
+
export { terminateAllChildGroups } from "./process-lifecycle.js";
|
|
26
|
+
export { buildFlowArgs, getOptimizedTools } from "./flow-args.js";
|
|
18
27
|
function getEnvInt(name, fallback) {
|
|
19
28
|
const raw = process.env[name];
|
|
20
29
|
if (!raw)
|
|
@@ -22,81 +31,27 @@ function getEnvInt(name, fallback) {
|
|
|
22
31
|
const parsed = parseInt(raw, 10);
|
|
23
32
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
24
33
|
}
|
|
25
|
-
const
|
|
26
|
-
const SIGKILL_TIMEOUT_MS = getEnvInt("PI_FLOW_SIGKILL_TIMEOUT_MS", 5000);
|
|
27
|
-
const FINISH_KILL_GRACE_MS = getEnvInt("PI_FLOW_FINISH_KILL_GRACE_MS", 5_000); // wait 5s after finish() before force-killing the child process
|
|
34
|
+
const FINISH_KILL_GRACE_MS = getEnvInt("PI_FLOW_FINISH_KILL_GRACE_MS", 5_000);
|
|
28
35
|
const AGENT_END_GRACE_MS = getEnvInt("PI_FLOW_AGENT_END_GRACE_MS", 2000);
|
|
29
|
-
const FLOW_TIME_BUDGET_WARNING_MS = getEnvInt("PI_FLOW_TIME_BUDGET_WARNING_MS", 2 * 60 * 1000);
|
|
30
|
-
const FLOW_FINAL_URGE_MS = getEnvInt("PI_FLOW_FINAL_URGE_MS", 135 * 1000);
|
|
31
|
-
const REPORTING_GRACE_MS = getEnvInt("PI_FLOW_REPORTING_GRACE_MS", 90_000);
|
|
32
|
-
const SNAP_THRESHOLD_MS = getEnvInt("PI_FLOW_SNAP_THRESHOLD_MS", 120_000);
|
|
33
|
-
const FLOW_TOOL_SUMMARY_GRACE_MS = FLOW_FINAL_URGE_MS;
|
|
36
|
+
const FLOW_TIME_BUDGET_WARNING_MS = getEnvInt("PI_FLOW_TIME_BUDGET_WARNING_MS", 2 * 60 * 1000);
|
|
37
|
+
const FLOW_FINAL_URGE_MS = getEnvInt("PI_FLOW_FINAL_URGE_MS", 135 * 1000);
|
|
38
|
+
const REPORTING_GRACE_MS = getEnvInt("PI_FLOW_REPORTING_GRACE_MS", 90_000);
|
|
39
|
+
const SNAP_THRESHOLD_MS = getEnvInt("PI_FLOW_SNAP_THRESHOLD_MS", 120_000);
|
|
40
|
+
const FLOW_TOOL_SUMMARY_GRACE_MS = FLOW_FINAL_URGE_MS;
|
|
34
41
|
import { FLOW_DEPTH_ENV, FLOW_MAX_DEPTH_ENV, FLOW_STACK_ENV, FLOW_PREVENT_CYCLES_ENV, FLOW_TOOL_OPTIMIZE_ENV, } from "./depth.js";
|
|
35
|
-
import {
|
|
42
|
+
import { computeChildPropagation, } from "./transition.js";
|
|
36
43
|
const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
37
44
|
const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
|
|
38
45
|
const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
39
46
|
const FLOW_REMINDER_FILE_ENV = "PI_FLOW_REMINDER_FILE";
|
|
40
|
-
const FLOW_DUMP_SNAPSHOT_ENV = "PI_FLOW_DUMP_SNAPSHOT";
|
|
41
47
|
const packageJsonPath = path.join(path.dirname(new URL(import.meta.url).pathname), "../..", "package.json");
|
|
42
48
|
let pipelineVersion = "0.0.0";
|
|
43
49
|
try {
|
|
44
50
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
45
51
|
pipelineVersion = pkg.version ?? "0.0.0";
|
|
46
52
|
}
|
|
47
|
-
catch {
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Global child process group tracking for signal propagation
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
/** Track child process groups so we can kill them on parent exit / signal. */
|
|
54
|
-
const runningChildGroups = new Map();
|
|
55
|
-
/** Register a child process group for cleanup on shutdown. */
|
|
56
|
-
export function registerChildGroup(pid, name) {
|
|
57
|
-
if (pid > 0 && !isWindows) {
|
|
58
|
-
runningChildGroups.set(pid, { groupPid: pid, name });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/** Unregister a completed/stopped child process group. */
|
|
62
|
-
export function unregisterChildGroup(pid) {
|
|
63
|
-
runningChildGroups.delete(pid);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Terminate all registered child process groups via SIGTERM (then SIGKILL after timeout).
|
|
67
|
-
* Called on parent exit, SIGINT, SIGTERM, or pi-agent-flow:shutdown.
|
|
68
|
-
*/
|
|
69
|
-
export function terminateAllChildGroups() {
|
|
70
|
-
if (runningChildGroups.size === 0)
|
|
71
|
-
return;
|
|
72
|
-
const pids = Array.from(runningChildGroups.keys());
|
|
73
|
-
for (const pid of pids) {
|
|
74
|
-
try {
|
|
75
|
-
process.kill(-pid, "SIGTERM");
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
try {
|
|
79
|
-
process.kill(pid, "SIGTERM");
|
|
80
|
-
}
|
|
81
|
-
catch { /* gone */ }
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// Hard kill after timeout
|
|
85
|
-
const sigkillTimer = setTimeout(() => {
|
|
86
|
-
for (const pid of pids) {
|
|
87
|
-
try {
|
|
88
|
-
process.kill(-pid, "SIGKILL");
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
try {
|
|
92
|
-
process.kill(pid, "SIGKILL");
|
|
93
|
-
}
|
|
94
|
-
catch { /* gone */ }
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
runningChildGroups.clear();
|
|
98
|
-
}, 5000);
|
|
99
|
-
sigkillTimer.unref();
|
|
53
|
+
catch (err) {
|
|
54
|
+
logWarn(`[pi-agent-flow] Failed to read package.json: ${err}`);
|
|
100
55
|
}
|
|
101
56
|
/**
|
|
102
57
|
* Merge actual usage with streaming estimates.
|
|
@@ -110,384 +65,14 @@ function mergeStreamingUsage(actual, estimatedOutputTokens, ctxEstimate, smoothe
|
|
|
110
65
|
return {
|
|
111
66
|
...actual,
|
|
112
67
|
...(estimatedOutputTokens > 0 ? { output: Math.max(actual.output, estimatedOutputTokens) } : {}),
|
|
113
|
-
...(ctxEstimate > 0
|
|
114
|
-
|
|
68
|
+
...(ctxEstimate > 0 || actual.contextTokens > 0
|
|
69
|
+
? { contextTokens: mergeStreamingContextTokens(actual, ctxEstimate) }
|
|
70
|
+
: {}),
|
|
115
71
|
...(smoothedTps > 0 ? { smoothedTps } : {}),
|
|
116
72
|
};
|
|
117
73
|
}
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// Process helpers
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
/**
|
|
122
|
-
* Derive the spawn command from the current process context so child invocations
|
|
123
|
-
* work on Unix and Windows without going through a shell wrapper.
|
|
124
|
-
*/
|
|
125
|
-
function resolveFlowSpawn() {
|
|
126
|
-
// Support PI_FLOW_SPAWN_COMMAND env var override for exotic runtime
|
|
127
|
-
// environments (e.g. bundled with pkg/nexe where process.argv[1] is unreliable).
|
|
128
|
-
const envOverride = process.env["PI_FLOW_SPAWN_COMMAND"];
|
|
129
|
-
if (envOverride && envOverride.trim()) {
|
|
130
|
-
return { command: envOverride.trim(), prefixArgs: [] };
|
|
131
|
-
}
|
|
132
|
-
const isNode = /[\\/]node(?:\.exe)?$/i.test(process.execPath);
|
|
133
|
-
if (isNode && process.argv[1]) {
|
|
134
|
-
return { command: process.execPath, prefixArgs: [process.argv[1]] };
|
|
135
|
-
}
|
|
136
|
-
return { command: process.execPath, prefixArgs: [] };
|
|
137
|
-
}
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Temp file helpers
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
function writeFlowSessionToTempFile(flowName, sessionJsonl) {
|
|
142
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-flow-"));
|
|
143
|
-
const safeName = flowName.replace(/[^\w.-]+/g, "_");
|
|
144
|
-
const filePath = path.join(tmpDir, `flow-${safeName}.jsonl`);
|
|
145
|
-
fs.writeFileSync(filePath, sessionJsonl, { encoding: "utf-8", mode: 0o600 });
|
|
146
|
-
return { dir: tmpDir, filePath };
|
|
147
|
-
}
|
|
148
|
-
function cleanupFlowTempDir(dir) {
|
|
149
|
-
if (!dir)
|
|
150
|
-
return;
|
|
151
|
-
try {
|
|
152
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
logWarn(`[pi-agent-flow] cleanupFlowTempDir failed: ${err}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// Dump path helpers
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
function makeUniqueDumpPath(basePath, flowName) {
|
|
162
|
-
const ext = path.extname(basePath);
|
|
163
|
-
const base = ext ? basePath.slice(0, -ext.length) : basePath;
|
|
164
|
-
const timestamp = Date.now();
|
|
165
|
-
const safeFlowName = flowName.replace(/[^\w.-]+/g, "_");
|
|
166
|
-
return `${base}.${safeFlowName}.${timestamp}.md`;
|
|
167
|
-
}
|
|
168
|
-
function makeUniqueDumpTxtPath(mdPath) {
|
|
169
|
-
return mdPath.replace(/\.md$/, ".txt");
|
|
170
|
-
}
|
|
171
|
-
function atomicWriteFileSync(targetPath, data) {
|
|
172
|
-
const dir = path.dirname(targetPath);
|
|
173
|
-
const tmpPath = path.join(dir, `.tmp-${path.basename(targetPath)}.${Date.now()}`);
|
|
174
|
-
fs.writeFileSync(tmpPath, data, { encoding: "utf-8", mode: 0o600 });
|
|
175
|
-
fs.renameSync(tmpPath, targetPath);
|
|
176
|
-
}
|
|
177
|
-
function parseSessionSnapshotInfo(jsonl) {
|
|
178
|
-
const result = {};
|
|
179
|
-
if (!jsonl)
|
|
180
|
-
return result;
|
|
181
|
-
for (const line of jsonl.split("\n")) {
|
|
182
|
-
if (!line.trim())
|
|
183
|
-
continue;
|
|
184
|
-
try {
|
|
185
|
-
const entry = JSON.parse(line);
|
|
186
|
-
if (entry.type === "session" && entry.id && typeof entry.id === "string") {
|
|
187
|
-
result.sessionId = entry.id;
|
|
188
|
-
}
|
|
189
|
-
if (entry.type === "message" &&
|
|
190
|
-
entry.message &&
|
|
191
|
-
typeof entry.message === "object" &&
|
|
192
|
-
!result.firstUserText) {
|
|
193
|
-
const msg = entry.message;
|
|
194
|
-
if (msg.role === "user" && msg.content) {
|
|
195
|
-
let text = "";
|
|
196
|
-
if (typeof msg.content === "string") {
|
|
197
|
-
text = msg.content;
|
|
198
|
-
}
|
|
199
|
-
else if (Array.isArray(msg.content)) {
|
|
200
|
-
const firstText = msg.content.find((c) => c && typeof c === "object" && c.type === "text");
|
|
201
|
-
text = typeof firstText?.text === "string" ? firstText.text : "";
|
|
202
|
-
}
|
|
203
|
-
if (text.trim()) {
|
|
204
|
-
result.firstUserText = text.trim();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
/* ignore non-JSON lines */
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return result;
|
|
214
|
-
}
|
|
215
|
-
function sanitizeForFilesystem(text, maxLength) {
|
|
216
|
-
const safe = text
|
|
217
|
-
.slice(0, maxLength)
|
|
218
|
-
.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
219
|
-
.replace(/^_+|_+$/g, "");
|
|
220
|
-
return safe || "unknown";
|
|
221
|
-
}
|
|
222
|
-
function getDebugDir(cwd, jsonl) {
|
|
223
|
-
const { sessionId, firstUserText } = parseSessionSnapshotInfo(jsonl);
|
|
224
|
-
const safeText = sanitizeForFilesystem(firstUserText ?? "", 20);
|
|
225
|
-
const idShort = sanitizeForFilesystem((sessionId ?? "unknown").slice(0, 8), 8);
|
|
226
|
-
return path.join(cwd, "tmp", `session-${safeText}-${idShort}`);
|
|
227
|
-
}
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
|
-
// Dump TTL cleanup
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
/**
|
|
232
|
-
* Delete stale dump files from the dump directory.
|
|
233
|
-
* Called once at the start of each dump block to prevent unbounded accumulation.
|
|
234
|
-
* Silently skips on any error (defensive).
|
|
235
|
-
*/
|
|
236
|
-
function cleanupStaleDumps(dumpPath, maxAgeHours = 168) {
|
|
237
|
-
try {
|
|
238
|
-
const dir = path.dirname(dumpPath);
|
|
239
|
-
const baseName = path.basename(dumpPath);
|
|
240
|
-
const ext = path.extname(baseName);
|
|
241
|
-
const base = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
242
|
-
const entries = fs.readdirSync(dir);
|
|
243
|
-
const nowMs = Date.now();
|
|
244
|
-
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
|
245
|
-
let deleted = 0;
|
|
246
|
-
for (const entry of entries) {
|
|
247
|
-
// Match both pi-dump.* and snapshot-dump.* families, plus .txt twins
|
|
248
|
-
const isLegacyDump = entry.startsWith("snapshot-dump");
|
|
249
|
-
if (!entry.startsWith(base) && !isLegacyDump)
|
|
250
|
-
continue;
|
|
251
|
-
const entryPath = path.join(dir, entry);
|
|
252
|
-
try {
|
|
253
|
-
const stats = fs.statSync(entryPath);
|
|
254
|
-
if (nowMs - stats.mtimeMs > maxAgeMs) {
|
|
255
|
-
fs.unlinkSync(entryPath);
|
|
256
|
-
deleted++;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
catch { /* ignore per-entry errors */ }
|
|
260
|
-
}
|
|
261
|
-
if (deleted > 0) {
|
|
262
|
-
logError(`[pi-agent-flow] Cleaned ${deleted} stale dump file(s) from ${dir}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
catch (err) {
|
|
266
|
-
logWarn(`[pi-agent-flow] cleanupStaleDumps failed: ${err}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Delete stale debug dump files from session directories under the repo tmp/ path.
|
|
271
|
-
* Called once at the start of each debug block to prevent unbounded accumulation.
|
|
272
|
-
* Silently skips on any error (defensive).
|
|
273
|
-
*/
|
|
274
|
-
function cleanupStaleDebugDumps(cwd, maxAgeHours = 168) {
|
|
275
|
-
try {
|
|
276
|
-
const baseDir = path.join(cwd, "tmp");
|
|
277
|
-
if (!fs.existsSync(baseDir))
|
|
278
|
-
return;
|
|
279
|
-
const entries = fs.readdirSync(baseDir);
|
|
280
|
-
const nowMs = Date.now();
|
|
281
|
-
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
|
282
|
-
let deleted = 0;
|
|
283
|
-
for (const entry of entries) {
|
|
284
|
-
if (!entry.startsWith("session-"))
|
|
285
|
-
continue;
|
|
286
|
-
const sessionDir = path.join(baseDir, entry);
|
|
287
|
-
try {
|
|
288
|
-
const stat = fs.statSync(sessionDir);
|
|
289
|
-
if (!stat.isDirectory())
|
|
290
|
-
continue;
|
|
291
|
-
const files = fs.readdirSync(sessionDir);
|
|
292
|
-
for (const file of files) {
|
|
293
|
-
const filePath = path.join(sessionDir, file);
|
|
294
|
-
try {
|
|
295
|
-
const stats = fs.statSync(filePath);
|
|
296
|
-
if (nowMs - stats.mtimeMs > maxAgeMs) {
|
|
297
|
-
fs.unlinkSync(filePath);
|
|
298
|
-
deleted++;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
catch { /* ignore per-file errors */ }
|
|
302
|
-
}
|
|
303
|
-
// Remove empty session directories
|
|
304
|
-
if (fs.readdirSync(sessionDir).length === 0) {
|
|
305
|
-
fs.rmdirSync(sessionDir);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch { /* ignore per-directory errors */ }
|
|
309
|
-
}
|
|
310
|
-
if (deleted > 0) {
|
|
311
|
-
logWarn(`[pi-agent-flow] Cleaned ${deleted} stale debug file(s) from ${baseDir}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch (err) {
|
|
315
|
-
logWarn(`[pi-agent-flow] cleanupStaleDebugDumps failed: ${err}`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
// Reminder file helpers
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
/**
|
|
322
|
-
* Write a reminder message to the reminder file so the flow state can see it
|
|
323
|
-
* via the timed-bash wrapper before its next tool call.
|
|
324
|
-
* Creates the file if it doesn't exist; appends the message.
|
|
325
|
-
*/
|
|
326
|
-
function writeReminderFile(reminderFilePath, message) {
|
|
327
|
-
if (!reminderFilePath)
|
|
328
|
-
return;
|
|
329
|
-
try {
|
|
330
|
-
fs.writeFileSync(reminderFilePath, message + "\n", { encoding: "utf-8", flag: "a" });
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
/* best-effort */
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// ---------------------------------------------------------------------------
|
|
337
|
-
// Build pi CLI arguments (fork-only)
|
|
338
|
-
// ---------------------------------------------------------------------------
|
|
339
|
-
const inheritedCliArgs = getInheritedCliArgs();
|
|
340
|
-
/**
|
|
341
|
-
* Transform a flow's tool list when toolOptimize is enabled.
|
|
342
|
-
* Replaces separate read/write/edit tools with the unified batch tool.
|
|
343
|
-
*/
|
|
344
|
-
export function getOptimizedTools(flowTools, toolOptimize) {
|
|
345
|
-
if (!toolOptimize || !flowTools)
|
|
346
|
-
return flowTools;
|
|
347
|
-
const hasLegacyTools = flowTools.some((t) => t === "read" || t === "write" || t === "edit");
|
|
348
|
-
if (!hasLegacyTools)
|
|
349
|
-
return flowTools;
|
|
350
|
-
const filtered = flowTools.filter((t) => t !== "read" && t !== "write" && t !== "edit" && t !== "batch" && t !== "batch_read");
|
|
351
|
-
return filtered.includes("batch")
|
|
352
|
-
? filtered
|
|
353
|
-
: [...filtered, "batch"];
|
|
354
|
-
}
|
|
355
|
-
function buildFlowArgs(flow, intent, forkSessionPath, model, parentDepth = 0, maxDepth = 0, toolOptimize = false, structuredOutput = true, complexity = DEFAULT_COMPLEXITY, sessionTimeoutMs = getComplexityTimeoutMs(complexity), acceptance, discoveredFlows = [], parentFlowStack = [], preventCycles = true, goalContext, cwd, tools, preDispatchResults) {
|
|
356
|
-
const args = [
|
|
357
|
-
"--mode",
|
|
358
|
-
"json",
|
|
359
|
-
...inheritedCliArgs.extensionArgs,
|
|
360
|
-
...inheritedCliArgs.alwaysProxy,
|
|
361
|
-
];
|
|
362
|
-
// Fork mode: always use --session
|
|
363
|
-
if (forkSessionPath) {
|
|
364
|
-
args.push("--session", forkSessionPath);
|
|
365
|
-
}
|
|
366
|
-
if (inheritedCliArgs.flowModelConfig) {
|
|
367
|
-
args.push("--flow-model-config", inheritedCliArgs.flowModelConfig);
|
|
368
|
-
}
|
|
369
|
-
if (inheritedCliArgs.flowComplexity) {
|
|
370
|
-
args.push("--flow-complexity", inheritedCliArgs.flowComplexity);
|
|
371
|
-
}
|
|
372
|
-
if (inheritedCliArgs.tieredModels?.lite) {
|
|
373
|
-
args.push("--flow-lite-model", inheritedCliArgs.tieredModels.lite);
|
|
374
|
-
}
|
|
375
|
-
if (inheritedCliArgs.tieredModels?.flash) {
|
|
376
|
-
args.push("--flow-flash-model", inheritedCliArgs.tieredModels.flash);
|
|
377
|
-
}
|
|
378
|
-
if (inheritedCliArgs.tieredModels?.full) {
|
|
379
|
-
args.push("--flow-full-model", inheritedCliArgs.tieredModels.full);
|
|
380
|
-
}
|
|
381
|
-
const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
|
|
382
|
-
if (resolvedModel)
|
|
383
|
-
args.push("--model", resolvedModel);
|
|
384
|
-
// Opt out of appending the structured JSON appendix to the child `-p` mission.
|
|
385
|
-
// Set `PI_FLOW_SKIP_STRUCTURED_DIRECTIVE=1` if a provider rejects that prompt shape.
|
|
386
|
-
const rawSkipSo = process.env["PI_FLOW_SKIP_STRUCTURED_DIRECTIVE"];
|
|
387
|
-
const skipStructuredDirective = rawSkipSo !== undefined && ["1", "true", "yes"].includes(rawSkipSo.trim().toLowerCase());
|
|
388
|
-
// Do not inherit the parent CLI `--thinking` level. Child flows often use a
|
|
389
|
-
// different tier/model than the root state; inheriting `--thinking high` can
|
|
390
|
-
// be incompatible with the child model.
|
|
391
|
-
const thinking = flow.thinking;
|
|
392
|
-
if (thinking)
|
|
393
|
-
args.push("--thinking", thinking);
|
|
394
|
-
// Compute transition depth before building tool list — children that can
|
|
395
|
-
// transition need the "flow" tool in their available set.
|
|
396
|
-
const { currentDepth, effectiveMaxDepth, canTransition } = computeTransitionState(parentDepth, maxDepth);
|
|
397
|
-
// Default tools for child flows. Legacy read/write/edit are NOT registered
|
|
398
|
-
// for children — only batch (which includes read/write ops) is available.
|
|
399
|
-
// The flow's frontmatter `tools` field overrides this default when set.
|
|
400
|
-
const defaultTools = toolOptimize
|
|
401
|
-
? canTransition
|
|
402
|
-
? ["batch", "bash", "flow"]
|
|
403
|
-
: ["batch", "bash"]
|
|
404
|
-
: canTransition
|
|
405
|
-
? ["batch", "bash", "flow"]
|
|
406
|
-
: ["batch", "bash"];
|
|
407
|
-
// getOptimizedTools replaces legacy read/write/edit with batch when
|
|
408
|
-
// toolOptimize is on. If the flow's frontmatter explicitly lists "flow",
|
|
409
|
-
// it passes through; otherwise the defaultTools above handle it.
|
|
410
|
-
const sourceTools = tools ?? flow.tools;
|
|
411
|
-
const optimizedTools = getOptimizedTools(sourceTools, toolOptimize) ?? defaultTools;
|
|
412
|
-
let harnessTools = optimizedTools;
|
|
413
|
-
// If the flow explicitly listed only tools that got filtered (e.g. just
|
|
414
|
-
// "web"), or the remaining tools lack essentials (batch/bash), fall back
|
|
415
|
-
// to defaultTools so the child isn't orphaned.
|
|
416
|
-
const hasEssentials = harnessTools.some((t) => t === "batch" || t === "bash" || t === "batch_read");
|
|
417
|
-
if (harnessTools.length === 0 || !hasEssentials) {
|
|
418
|
-
harnessTools = [...new Set([...defaultTools, ...harnessTools])];
|
|
419
|
-
}
|
|
420
|
-
args.push("--tools", harnessTools.join(","));
|
|
421
|
-
// No --append-system-prompt: child inherits parent's system prompt for cache hits.
|
|
422
|
-
// Flow instructions go in the intent message instead.
|
|
423
|
-
const availableTools = harnessTools.join(", ");
|
|
424
|
-
// Phase 1: Context seal — sharp boundary declaring history sealed
|
|
425
|
-
const contextSeal = `<context-seal>\n` +
|
|
426
|
-
`The conversation above is sealed — it is your session history for situational awareness only.\n` +
|
|
427
|
-
`Your task begins NOW. Do not respond to or continue anything from the history.\n` +
|
|
428
|
-
`</context-seal>`;
|
|
429
|
-
// Phase 2: Activation — role, tools, depth, transition rules (dynamically generated)
|
|
430
|
-
const guardLine = buildGuardLine(currentDepth, effectiveMaxDepth, preventCycles, parentFlowStack);
|
|
431
|
-
const flowListSection = buildFlowListSection(canTransition, discoveredFlows);
|
|
432
|
-
const effectiveTier = flow.tier ?? getFlowTier(flow.name);
|
|
433
|
-
const lineage = buildLineage(flow.name, parentFlowStack);
|
|
434
|
-
const activation = `\n\n<activation flow="${flow.name}" depth="${currentDepth}" tools="${availableTools}" tier="${effectiveTier}" lineage="${lineage}">\n` +
|
|
435
|
-
`You are a [${flow.name}] agent operating at depth ${currentDepth}.\n` +
|
|
436
|
-
`${flowListSection}` +
|
|
437
|
-
`Do not attempt to use any tool outside the available set — it will fail.\n` +
|
|
438
|
-
`</activation>`;
|
|
439
|
-
// Phase 3: Directive — the flow's system prompt (renamed from <system-directive>)
|
|
440
|
-
let directiveBody = flow.systemPrompt.trim();
|
|
441
|
-
// Append structured output instructions when enabled (unless opted out via env).
|
|
442
|
-
const isTrace = flow.name.toLowerCase() === "trace";
|
|
443
|
-
if (structuredOutput && directiveBody && !skipStructuredDirective && !isTrace) {
|
|
444
|
-
const isAudit = flow.name.toLowerCase() === "audit";
|
|
445
|
-
const auditFields = isAudit
|
|
446
|
-
? " Also include `verdict: 'pass' | 'rework'` and `feedback: string` (required when verdict is 'rework', optional when 'pass'). When auditing multiple builds, also include `builds: { index: number, verdict: 'pass' | 'rework', feedback?: string }[]` with per-build verdicts."
|
|
447
|
-
: "";
|
|
448
|
-
directiveBody +=
|
|
449
|
-
`\n\n## Structured Output\n` +
|
|
450
|
-
`End with a \`\`\`json block: { version, status, summary, files[], actions[], notDone[], nextSteps[], reasoning[], notes[]${isAudit ? ", verdict, feedback, builds" : ""} }. Commands auto-extracted; omit empty arrays. Keep snippets under 300 chars. List at most 10 items per array.${auditFields}`;
|
|
451
|
-
}
|
|
452
|
-
const directive = directiveBody
|
|
453
|
-
? `\n\n<directive>\n${directiveBody}\n</directive>`
|
|
454
|
-
: "";
|
|
455
|
-
// Phase 4: Mission — the intent (and optional acceptance criteria)
|
|
456
|
-
const acceptanceLine = acceptance ? `\nAcceptance: ${acceptance}` : "";
|
|
457
|
-
const mission = `\n\n<mission>\n${intent}${acceptanceLine}\n` +
|
|
458
|
-
`\nExecute this mission. Use only your available tools. If blocked, report why — do not guess.\n` +
|
|
459
|
-
`Follow the output format specified in your directive.\n` +
|
|
460
|
-
`</mission>`;
|
|
461
|
-
// Phase 4.5: Flow goal context (optional)
|
|
462
|
-
let goalSection = "";
|
|
463
|
-
if (goalContext?.objective) {
|
|
464
|
-
const completedSummary = goalContext.completedFlows?.length
|
|
465
|
-
? `\nCompleted steps:\n${goalContext.completedFlows.map(f => `- [${f.type}] ${f.aim}`).join("\n")}`
|
|
466
|
-
: "";
|
|
467
|
-
goalSection = `\n\n<flow>\nObjective: ${goalContext.objective}\n${goalContext.acceptance ? `Acceptance: ${goalContext.acceptance}\n` : ""}${goalContext.maxFlows !== undefined ? `Progress: ${goalContext.flowCount ?? 0}/${goalContext.maxFlows} flows used.\n` : ""}${completedSummary}\n</flow>`;
|
|
468
|
-
}
|
|
469
|
-
// Phase 4.6: Pre-dispatch results (optional)
|
|
470
|
-
const preDispatchSection = preDispatchResults
|
|
471
|
-
? `\n\n<pre-dispatch>\nThe following tool calls were already executed on your behalf. Continue your exploration from there, or synthesize the outputs, pick up necessary tool id to keep.\n\n${preDispatchResults}\n</pre-dispatch>`
|
|
472
|
-
: "";
|
|
473
|
-
// -p must immediately precede the prompt so the CLI parser binds it correctly
|
|
474
|
-
args.push("-p", `${contextSeal}${activation}${directive}${mission}${goalSection}${preDispatchSection}`);
|
|
475
|
-
return args;
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Spawn a single flow process with forked session context.
|
|
479
|
-
*
|
|
480
|
-
* Returns a SingleResult even on failure (exitCode > 0, stderr populated).
|
|
481
|
-
*
|
|
482
|
-
* Why `spawn` instead of `pi.exec()`:
|
|
483
|
-
* - `pi.exec()` is designed for simple command execution (one-shot, wait for exit).
|
|
484
|
-
* - Flows need process-group isolation (detached mode on Unix), signal propagation,
|
|
485
|
-
* soft-timeout with background continuation, streaming stdout parsing, and
|
|
486
|
-
* mid-flight kill semantics. `spawn` gives full control over stdio, process
|
|
487
|
-
* groups, and lifecycle that `pi.exec()` does not expose.
|
|
488
|
-
*/
|
|
489
74
|
export async function runFlow(opts) {
|
|
490
|
-
const { cwd, flows, flowName, intent, aim, taskCwd, forkSessionSnapshotJsonl, parentDepth, parentFlowStack, maxDepth, preventCycles, toolOptimize = false, structuredOutput = true, model, signal, onUpdate, makeDetails, } = opts;
|
|
75
|
+
const { cwd, flows, flowName, intent, aim, taskCwd, forkSessionSnapshotJsonl, parentDepth, parentFlowStack, maxDepth, preventCycles, toolOptimize = false, structuredOutput = true, model, signal, onUpdate, makeDetails, toolCallId, } = opts;
|
|
491
76
|
const normalizedFlowName = flowName.toLowerCase();
|
|
492
77
|
const flow = flows.find((f) => f.name === normalizedFlowName);
|
|
493
78
|
if (!flow) {
|
|
@@ -511,6 +96,8 @@ export async function runFlow(opts) {
|
|
|
511
96
|
const deadlineAtMs = effectiveTimeout > 0 ? startedAtMs + effectiveTimeout : undefined;
|
|
512
97
|
const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
|
|
513
98
|
const resolvedMaxContextTokens = opts.maxContextTokens ?? inheritedCliArgs.maxContextTokens;
|
|
99
|
+
const sharedContext = forkSessionSnapshotJsonl ? parseSharedContext(forkSessionSnapshotJsonl) : undefined;
|
|
100
|
+
const initialContextTokens = computeInitialContextTokens(sharedContext, intent);
|
|
514
101
|
const result = {
|
|
515
102
|
type: normalizedFlowName,
|
|
516
103
|
agentSource: flow.source,
|
|
@@ -520,12 +107,19 @@ export async function runFlow(opts) {
|
|
|
520
107
|
exitCode: -1,
|
|
521
108
|
messages: [],
|
|
522
109
|
stderr: "",
|
|
523
|
-
usage:
|
|
110
|
+
usage: {
|
|
111
|
+
...emptyFlowUsage(),
|
|
112
|
+
contextTokens: initialContextTokens,
|
|
113
|
+
},
|
|
524
114
|
model: resolvedModel,
|
|
525
115
|
startedAtMs,
|
|
526
116
|
...(deadlineAtMs !== undefined ? { deadlineAtMs } : {}),
|
|
527
117
|
...(resolvedMaxContextTokens !== undefined ? { maxContextTokens: resolvedMaxContextTokens } : {}),
|
|
528
118
|
};
|
|
119
|
+
if (initialContextTokens > 0) {
|
|
120
|
+
const ctxState = getCtxState(result);
|
|
121
|
+
ctxState.baseline = initialContextTokens;
|
|
122
|
+
}
|
|
529
123
|
let liveStreamingText = "";
|
|
530
124
|
let liveEstimatedOutputTokens = 0;
|
|
531
125
|
let lastActualOutputTokens = result.usage.output;
|
|
@@ -533,7 +127,6 @@ export async function runFlow(opts) {
|
|
|
533
127
|
const streamingDelta = drainStreamingText(result);
|
|
534
128
|
if (streamingDelta)
|
|
535
129
|
liveStreamingText += streamingDelta;
|
|
536
|
-
// Live text is stored per-toolCallId by the executor's emitProgress, not here.
|
|
537
130
|
const estimatedTokens = drainStreamingEstimate(result);
|
|
538
131
|
const toolCallTokens = drainToolCallEstimate(result);
|
|
539
132
|
if (result.usage.output !== lastActualOutputTokens) {
|
|
@@ -544,7 +137,6 @@ export async function runFlow(opts) {
|
|
|
544
137
|
const ctxEst = drainCtxEstimate(result);
|
|
545
138
|
updateSmoothedTps(result, estimatedTokens);
|
|
546
139
|
const smoothedTps = drainSmoothedTps(result);
|
|
547
|
-
// Fallback TPS for providers that don't emit streaming deltas (e.g., Fireworks k2p6).
|
|
548
140
|
const elapsedSec = (Date.now() - startedAtMs) / 1000;
|
|
549
141
|
const fallbackTps = elapsedSec > 0.5 && smoothedTps <= 0 ? result.usage.output / elapsedSec : 0;
|
|
550
142
|
const displayTps = smoothedTps > 0 ? smoothedTps : fallbackTps;
|
|
@@ -557,9 +149,9 @@ export async function runFlow(opts) {
|
|
|
557
149
|
},
|
|
558
150
|
],
|
|
559
151
|
details: makeDetails([{ ...result, usage: mergedUsage, streamingText: liveStreamingText || undefined }]),
|
|
152
|
+
...(toolCallId ? { _toolCallId: toolCallId } : {}),
|
|
560
153
|
});
|
|
561
154
|
};
|
|
562
|
-
// Write forked session snapshot to temp file only when provided
|
|
563
155
|
let forkSessionTmpDir = null;
|
|
564
156
|
let forkSessionTmpPath = null;
|
|
565
157
|
if (forkSessionSnapshotJsonl) {
|
|
@@ -567,8 +159,6 @@ export async function runFlow(opts) {
|
|
|
567
159
|
forkSessionTmpDir = forkTmp.dir;
|
|
568
160
|
forkSessionTmpPath = forkTmp.filePath;
|
|
569
161
|
}
|
|
570
|
-
// Create a temp dir for the reminder file so the flow state can read timeout warnings
|
|
571
|
-
// via the timed-bash wrapper before its next tool call.
|
|
572
162
|
let reminderTmpDir = null;
|
|
573
163
|
let reminderFilePath = null;
|
|
574
164
|
if (effectiveTimeout > 0) {
|
|
@@ -584,14 +174,17 @@ export async function runFlow(opts) {
|
|
|
584
174
|
}
|
|
585
175
|
try {
|
|
586
176
|
const piArgs = buildFlowArgs(flow, intent, forkSessionTmpPath, model, parentDepth, maxDepth, toolOptimize, structuredOutput, effectiveComplexity, effectiveTimeout, opts.acceptance, flows, parentFlowStack, preventCycles, opts.goalContext, cwd, opts.tools, opts.preDispatchResults);
|
|
587
|
-
// Extract the activation prompt once for reuse by both dump and debug blocks.
|
|
588
177
|
const promptIndex = piArgs.indexOf("-p");
|
|
589
178
|
const prompt = promptIndex >= 0 ? piArgs[promptIndex + 1] : "";
|
|
590
|
-
|
|
179
|
+
result.usage.contextTokens = computeInitialContextTokens(sharedContext, intent, prompt);
|
|
180
|
+
const ctxState = getCtxState(result);
|
|
181
|
+
ctxState.baseline = result.usage.contextTokens;
|
|
182
|
+
if (onUpdate) {
|
|
183
|
+
emitUpdate();
|
|
184
|
+
}
|
|
591
185
|
const dumpPath = process.env[FLOW_DUMP_SNAPSHOT_ENV] || inheritedCliArgs.dumpPath;
|
|
592
186
|
if (dumpPath) {
|
|
593
|
-
|
|
594
|
-
cleanupStaleDumps(dumpPath, Number.isFinite(maxAgeHours) && maxAgeHours > 0 ? maxAgeHours : 168);
|
|
187
|
+
cleanupStaleDumps(dumpPath, resolveDumpMaxAgeHours());
|
|
595
188
|
const effectiveTier = flow.tier ?? getFlowTier(flow.name);
|
|
596
189
|
const sanitizationHeader = `<!-- pi-agent-flow dump | Flow: ${flow.name} | Tier: ${effectiveTier} | Pipeline: ${pipelineVersion} | Generated: ${new Date().toISOString()} -->`;
|
|
597
190
|
const markdownParts = [
|
|
@@ -602,7 +195,6 @@ export async function runFlow(opts) {
|
|
|
602
195
|
markdownParts.push(`## Session Snapshot (JSONL)`, ``, ...forkSessionSnapshotJsonl.split("\n"), ``);
|
|
603
196
|
}
|
|
604
197
|
markdownParts.push(`## Activation Prompt (-p)`, ``, prompt);
|
|
605
|
-
// Compression Stats section removed — zeroed values provide no signal
|
|
606
198
|
const markdown = markdownParts.join("\n");
|
|
607
199
|
const uniqueDumpPath = makeUniqueDumpPath(dumpPath, flow.name);
|
|
608
200
|
const uniqueTxtPath = makeUniqueDumpTxtPath(uniqueDumpPath);
|
|
@@ -615,10 +207,8 @@ export async function runFlow(opts) {
|
|
|
615
207
|
logError(`[pi-agent-flow] Snapshot dump FAILED: ${err}`);
|
|
616
208
|
}
|
|
617
209
|
}
|
|
618
|
-
// Debug mode: write the activation prompt and shared session snapshot to a dedicated temp file.
|
|
619
210
|
if (opts.debugMode) {
|
|
620
|
-
|
|
621
|
-
cleanupStaleDebugDumps(cwd, Number.isFinite(maxAgeHours) && maxAgeHours > 0 ? maxAgeHours : 168);
|
|
211
|
+
cleanupStaleDebugDumps(cwd, resolveDumpMaxAgeHours());
|
|
622
212
|
const safeFlowName = flow.name.replace(/[^\w.-]+/g, "_");
|
|
623
213
|
const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
624
214
|
const debugDir = getDebugDir(cwd, forkSessionSnapshotJsonl);
|
|
@@ -665,7 +255,8 @@ export async function runFlow(opts) {
|
|
|
665
255
|
const distMtime = fs.statSync(path.join(distDir, distFile)).mtimeMs;
|
|
666
256
|
return srcMtime > distMtime;
|
|
667
257
|
}
|
|
668
|
-
catch {
|
|
258
|
+
catch (e) {
|
|
259
|
+
logWarn(`[pi-agent-flow] checkStale failed for ${srcFile}: ${e}`);
|
|
669
260
|
return false;
|
|
670
261
|
}
|
|
671
262
|
};
|
|
@@ -677,7 +268,6 @@ export async function runFlow(opts) {
|
|
|
677
268
|
cwd: taskCwd ?? cwd,
|
|
678
269
|
shell: false,
|
|
679
270
|
stdio: ["pipe", "pipe", "pipe"],
|
|
680
|
-
// Process group on Unix so we can kill all descendants on timeout/abort.
|
|
681
271
|
detached: !isWindows,
|
|
682
272
|
...(signal ? { signal } : {}),
|
|
683
273
|
env: {
|
|
@@ -699,7 +289,6 @@ export async function runFlow(opts) {
|
|
|
699
289
|
}),
|
|
700
290
|
},
|
|
701
291
|
});
|
|
702
|
-
// Register the child process group for global cleanup on signal/exit
|
|
703
292
|
if (proc.pid !== undefined && !isWindows) {
|
|
704
293
|
registerChildGroup(proc.pid, normalizedFlowName);
|
|
705
294
|
}
|
|
@@ -711,10 +300,12 @@ export async function runFlow(opts) {
|
|
|
711
300
|
try {
|
|
712
301
|
proc.stdin.end();
|
|
713
302
|
}
|
|
714
|
-
catch {
|
|
303
|
+
catch (e) {
|
|
304
|
+
logWarn(`[pi-agent-flow] Failed to end child stdin: ${e}`);
|
|
305
|
+
}
|
|
715
306
|
};
|
|
716
|
-
proc.stdin.on("error", () => {
|
|
717
|
-
|
|
307
|
+
proc.stdin.on("error", (err) => {
|
|
308
|
+
logWarn(`[pi-agent-flow] Child stdin error: ${err}`);
|
|
718
309
|
});
|
|
719
310
|
proc.stdin.end();
|
|
720
311
|
let abortHandler;
|
|
@@ -745,44 +336,11 @@ export async function runFlow(opts) {
|
|
|
745
336
|
}
|
|
746
337
|
};
|
|
747
338
|
const terminateChild = () => {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
});
|
|
754
|
-
killer.unref();
|
|
755
|
-
}
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
// Kill the entire process group (negative PID).
|
|
759
|
-
if (proc.pid === undefined) {
|
|
760
|
-
proc.kill("SIGTERM");
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
try {
|
|
764
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
765
|
-
}
|
|
766
|
-
catch {
|
|
767
|
-
proc.kill("SIGTERM");
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
const sigkillTimer = setTimeout(() => {
|
|
771
|
-
if (!didClose) {
|
|
772
|
-
if (proc.pid === undefined) {
|
|
773
|
-
proc.kill("SIGKILL");
|
|
774
|
-
}
|
|
775
|
-
else {
|
|
776
|
-
try {
|
|
777
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
778
|
-
}
|
|
779
|
-
catch {
|
|
780
|
-
proc.kill("SIGKILL");
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}, SIGKILL_TIMEOUT_MS);
|
|
785
|
-
sigkillTimer.unref();
|
|
339
|
+
terminateChildProcess(proc, {
|
|
340
|
+
endStdin,
|
|
341
|
+
timeoutMs: SIGKILL_TIMEOUT_MS,
|
|
342
|
+
skipIfClosed: () => didClose,
|
|
343
|
+
});
|
|
786
344
|
};
|
|
787
345
|
const clearFinishKillTimer = () => {
|
|
788
346
|
if (finishKillTimer) {
|
|
@@ -801,8 +359,6 @@ export async function runFlow(opts) {
|
|
|
801
359
|
if (signal && abortHandler) {
|
|
802
360
|
signal.removeEventListener("abort", abortHandler);
|
|
803
361
|
}
|
|
804
|
-
// Soft-kill: give the child a short grace to exit naturally after stdin close.
|
|
805
|
-
// If it hasn't closed by then, force-kill to prevent orphaned processes.
|
|
806
362
|
clearFinishKillTimer();
|
|
807
363
|
finishKillTimer = setTimeout(() => {
|
|
808
364
|
if (!didClose) {
|
|
@@ -883,7 +439,6 @@ export async function runFlow(opts) {
|
|
|
883
439
|
result.stderr = err.message;
|
|
884
440
|
finish(1);
|
|
885
441
|
});
|
|
886
|
-
// Abort handling
|
|
887
442
|
if (signal) {
|
|
888
443
|
abortHandler = () => {
|
|
889
444
|
if (didClose || settled)
|
|
@@ -897,12 +452,7 @@ export async function runFlow(opts) {
|
|
|
897
452
|
else
|
|
898
453
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
899
454
|
}
|
|
900
|
-
// Execution timeout — two-stage: parent-side warnings, tool-level deadline abort, then grace, then hard kill
|
|
901
455
|
if (effectiveTimeout > 0) {
|
|
902
|
-
// Warning timer: notify the parent UI that the child is about to be killed.
|
|
903
|
-
// NOTE: True mid-flight injection into the child's context requires pi-core
|
|
904
|
-
// support for out-of-band messages. Until then, the agent only knows its
|
|
905
|
-
// budget from the initial prompt; this warning is parent-side only.
|
|
906
456
|
const warningMs = effectiveTimeout - FLOW_TIME_BUDGET_WARNING_MS;
|
|
907
457
|
if (warningMs > 0) {
|
|
908
458
|
const warnTimer = setTimeout(() => {
|
|
@@ -911,14 +461,11 @@ export async function runFlow(opts) {
|
|
|
911
461
|
const remainingSec = Math.round(FLOW_TIME_BUDGET_WARNING_MS / 1000);
|
|
912
462
|
const warnMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. The agent should wrap up now.`;
|
|
913
463
|
result.stderr += warnMsg;
|
|
914
|
-
// Write to reminder file so the flow state sees it on its next bash call.
|
|
915
464
|
writeReminderFile(reminderFilePath, `[Flow warning] ${remainingSec}s remaining before hard timeout. Wrap up your work and output structured findings.`);
|
|
916
|
-
// Force an update so the parent UI shows the warning immediately.
|
|
917
465
|
emitUpdate();
|
|
918
466
|
}, warningMs);
|
|
919
467
|
warnTimer.unref();
|
|
920
468
|
}
|
|
921
|
-
// Final urge timer: stronger warning 45 s before hard timeout
|
|
922
469
|
const urgeMs = effectiveTimeout - FLOW_FINAL_URGE_MS;
|
|
923
470
|
if (urgeMs > 0) {
|
|
924
471
|
const urgeTimer = setTimeout(() => {
|
|
@@ -927,7 +474,6 @@ export async function runFlow(opts) {
|
|
|
927
474
|
const remainingSec = Math.round(FLOW_FINAL_URGE_MS / 1000);
|
|
928
475
|
const urgeMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. Stop all work and output your structured findings.`;
|
|
929
476
|
result.stderr += urgeMsg;
|
|
930
|
-
// Write to reminder file so the flow state sees it on its next bash call.
|
|
931
477
|
writeReminderFile(reminderFilePath, `[Flow urge] ${remainingSec}s remaining before hard timeout. STOP all tool use and output your structured findings NOW.`);
|
|
932
478
|
emitUpdate();
|
|
933
479
|
}, urgeMs);
|
|
@@ -939,7 +485,6 @@ export async function runFlow(opts) {
|
|
|
939
485
|
timeoutFired = true;
|
|
940
486
|
result.stderr += `\nFlow timed out after ${Math.round(effectiveTimeout / 1000)}s.`;
|
|
941
487
|
emitUpdate();
|
|
942
|
-
// Grace period before hard kill
|
|
943
488
|
const graceTimer = setTimeout(() => {
|
|
944
489
|
if (didClose || settled)
|
|
945
490
|
return;
|
|
@@ -953,9 +498,6 @@ export async function runFlow(opts) {
|
|
|
953
498
|
}
|
|
954
499
|
});
|
|
955
500
|
result.exitCode = exitCode;
|
|
956
|
-
// Persist final smoothed TPS into the result's usage so it survives after streaming ends.
|
|
957
|
-
// During streaming, emitUpdate() only merges smoothedTps into a temporary display object;
|
|
958
|
-
// without this, result.usage.smoothedTps stays at 0 and the UI shows a dash.
|
|
959
501
|
const finalSmoothedTps = drainSmoothedTps(result);
|
|
960
502
|
const finalElapsedSec = (Date.now() - startedAtMs) / 1000;
|
|
961
503
|
const finalTps = finalSmoothedTps > 0 ? finalSmoothedTps
|
|
@@ -964,7 +506,6 @@ export async function runFlow(opts) {
|
|
|
964
506
|
result.usage.smoothedTps = finalTps;
|
|
965
507
|
}
|
|
966
508
|
const normalized = normalizeFlowResult(result, wasAborted);
|
|
967
|
-
// Extract structured JSON output from the final assistant text
|
|
968
509
|
if (structuredOutput) {
|
|
969
510
|
const flowText = getFlowOutput(normalized.messages);
|
|
970
511
|
const extracted = extractStructuredOutput(flowText);
|
|
@@ -983,9 +524,6 @@ export async function runFlow(opts) {
|
|
|
983
524
|
// ---------------------------------------------------------------------------
|
|
984
525
|
// Concurrency helper
|
|
985
526
|
// ---------------------------------------------------------------------------
|
|
986
|
-
/**
|
|
987
|
-
* Map over items with a bounded number of concurrent workers.
|
|
988
|
-
*/
|
|
989
527
|
export async function mapFlowConcurrent(items, concurrency, fn) {
|
|
990
528
|
if (items.length === 0)
|
|
991
529
|
return [];
|