@agwab/pi-workflow 0.1.1 → 0.1.2
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 +14 -3
- package/agents/researcher.md +17 -7
- package/dist/artifact-graph-runtime.js +1 -0
- package/dist/compiler.js +2 -2
- package/dist/dynamic-generated-task-runtime.js +4 -3
- package/dist/dynamic-runtime-bundle.js +3 -2
- package/dist/extension.js +40 -1
- package/dist/subagent-backend.js +82 -27
- package/dist/tool-metadata.d.ts +1 -0
- package/dist/tool-metadata.js +13 -1
- package/dist/workflow-artifact-extension.js +3 -2
- package/dist/workflow-artifact-tool.js +84 -4
- package/dist/workflow-web-source-extension.d.ts +43 -0
- package/dist/workflow-web-source-extension.js +1194 -0
- package/dist/workflow-web-source.d.ts +171 -0
- package/dist/workflow-web-source.js +897 -0
- package/docs/usage.md +32 -18
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
- package/package.json +2 -2
- package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
- package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
- package/src/artifact-graph-runtime.ts +1 -0
- package/src/compiler.ts +2 -1
- package/src/dynamic-generated-task-runtime.ts +4 -2
- package/src/dynamic-runtime-bundle.ts +3 -2
- package/src/extension.ts +46 -1
- package/src/subagent-backend.ts +121 -37
- package/src/tool-metadata.ts +22 -1
- package/src/workflow-artifact-extension.ts +3 -2
- package/src/workflow-artifact-tool.ts +96 -4
- package/src/workflow-web-source-extension.ts +1411 -0
- package/src/workflow-web-source.ts +1171 -0
- package/workflows/README.md +1 -1
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +474 -40
- package/workflows/deep-research/helpers/final-audit-packet.mjs +219 -0
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +436 -0
- package/workflows/deep-research/helpers/render-executive.mjs +571 -198
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +35 -8
- package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
- package/workflows/deep-research/spec.json +36 -21
- package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
- package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
- package/workflows/deep-review/spec.json +22 -1
|
@@ -2,319 +2,473 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { chmod, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createAttemptArtifactStore,
|
|
7
|
+
type ArtifactRef,
|
|
8
|
+
type ResultEnvelope,
|
|
9
|
+
} from "../artifacts/index.ts";
|
|
6
10
|
import type { ResultWorkspace } from "../artifacts/result.ts";
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
sandboxAllowedDomains,
|
|
13
|
+
type FailureKind,
|
|
14
|
+
type SandboxInput,
|
|
15
|
+
type Status,
|
|
16
|
+
} from "../core/constants.ts";
|
|
8
17
|
import { SandboxUnavailableError, withSandboxedArgv } from "../sandbox/srt.ts";
|
|
9
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
buildPiArgv,
|
|
20
|
+
detectContextLengthExceeded,
|
|
21
|
+
parsePiJsonFile,
|
|
22
|
+
parsePiJsonLines,
|
|
23
|
+
resolvePiJsonOutcome,
|
|
24
|
+
resultMetadataFromParse,
|
|
25
|
+
resultSessionMetadata,
|
|
26
|
+
type RunHeadlessModelOptions,
|
|
27
|
+
} from "./headless-model.ts";
|
|
10
28
|
|
|
11
29
|
const execFileAsync = promisify(execFile);
|
|
12
30
|
const POLL_INTERVAL_MS = 100;
|
|
13
31
|
|
|
14
32
|
interface RunTmuxProcessOptions {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
argv: readonly string[];
|
|
34
|
+
cwd?: string;
|
|
35
|
+
artifactCwd?: string;
|
|
36
|
+
runId?: string;
|
|
37
|
+
attemptId?: string;
|
|
38
|
+
runsDir?: string;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
sandbox?: SandboxInput | null;
|
|
42
|
+
workspace?: Partial<ResultWorkspace>;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
export type RunTmuxModelOptions = RunHeadlessModelOptions;
|
|
28
46
|
|
|
29
47
|
interface WorkerMeta {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
status: Status;
|
|
49
|
+
failureKind: FailureKind | null;
|
|
50
|
+
exitCode: number | null;
|
|
51
|
+
signal: string | null;
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
interface TmuxRunResult {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
meta: WorkerMeta;
|
|
56
|
+
stderrRef: ArtifactRef;
|
|
57
|
+
eventPath: string;
|
|
58
|
+
tmux: {
|
|
59
|
+
sessionName: string;
|
|
60
|
+
sessionId: string | null;
|
|
61
|
+
paneId: string | null;
|
|
62
|
+
};
|
|
45
63
|
}
|
|
46
64
|
|
|
47
|
-
function assertRunnableArgv(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
function assertRunnableArgv(
|
|
66
|
+
argv: readonly string[],
|
|
67
|
+
): asserts argv is readonly [string, ...string[]] {
|
|
68
|
+
if (!Array.isArray(argv) || argv.length === 0) {
|
|
69
|
+
throw new Error("argv must be a non-empty array of non-empty strings.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [index, value] of argv.entries()) {
|
|
73
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
74
|
+
throw new Error(`argv[${index}] must be a non-empty string.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
function normalizeTimeoutMs(timeoutMs: number | undefined): number | undefined {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
if (timeoutMs === undefined) return undefined;
|
|
81
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"timeoutMs must be a positive finite number when provided.",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return timeoutMs;
|
|
65
87
|
}
|
|
66
88
|
|
|
67
89
|
async function tmuxAvailable(): Promise<boolean> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
try {
|
|
91
|
+
await execFileAsync("tmux", ["-V"]);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
async function pathBytes(path: string): Promise<number> {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
try {
|
|
100
|
+
return (await stat(path)).size;
|
|
101
|
+
} catch {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
async function sleep(ms: number): Promise<void> {
|
|
85
|
-
|
|
107
|
+
await new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
function shellQuote(value: string): string {
|
|
89
|
-
|
|
111
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
90
112
|
}
|
|
91
113
|
|
|
92
114
|
async function readWorkerMeta(path: string): Promise<WorkerMeta | undefined> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
try {
|
|
116
|
+
const { readFile } = await import("node:fs/promises");
|
|
117
|
+
const parsed = JSON.parse(
|
|
118
|
+
await readFile(path, "utf8"),
|
|
119
|
+
) as Partial<WorkerMeta>;
|
|
120
|
+
if (parsed.status !== "completed" && parsed.status !== "failed")
|
|
121
|
+
return undefined;
|
|
122
|
+
return {
|
|
123
|
+
status: parsed.status,
|
|
124
|
+
failureKind: parsed.failureKind ?? null,
|
|
125
|
+
exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : null,
|
|
126
|
+
signal: typeof parsed.signal === "string" ? parsed.signal : null,
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
106
131
|
}
|
|
107
132
|
|
|
108
133
|
async function tmuxSessionAlive(sessionName: string): Promise<boolean> {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
try {
|
|
135
|
+
await execFileAsync("tmux", ["has-session", "-t", sessionName]);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
async function killTmuxSession(sessionName: string): Promise<void> {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
try {
|
|
144
|
+
await execFileAsync("tmux", ["kill-session", "-t", sessionName]);
|
|
145
|
+
} catch {
|
|
146
|
+
// Session may already have exited; cleanup remains best-effort.
|
|
147
|
+
}
|
|
123
148
|
}
|
|
124
149
|
|
|
125
|
-
function workerScript(
|
|
126
|
-
|
|
150
|
+
function workerScript(
|
|
151
|
+
argv: readonly [string, ...string[]],
|
|
152
|
+
cwd: string,
|
|
153
|
+
eventPath: string,
|
|
154
|
+
stderrPath: string,
|
|
155
|
+
metaPath: string,
|
|
156
|
+
): string {
|
|
157
|
+
return `import { spawn } from "node:child_process";\nimport { appendFileSync, closeSync, openSync, writeFileSync } from "node:fs";\nconst argv = ${JSON.stringify(argv)};\nconst cwd = ${JSON.stringify(cwd)};\nconst eventPath = ${JSON.stringify(eventPath)};\nconst stderrPath = ${JSON.stringify(stderrPath)};\nconst metaPath = ${JSON.stringify(metaPath)};\nconst messageUpdatePattern = /"type"\\s*:\\s*"message_update"/;\nconst maxStdoutLogLineChars = 64 * 1024 * 1024;\ncloseSync(openSync(eventPath, "w"));\ncloseSync(openSync(stderrPath, "w"));\nlet settled = false;\nlet stdoutBuffer = "";\nlet discardingOversizedLine = false;\nlet omittedMessageUpdates = 0;\nlet omittedMessageUpdateBytes = 0;\nlet omittedOversizedLines = 0;\nlet omittedOversizedBytes = 0;\nfunction writeStdoutLine(line) {\n if (messageUpdatePattern.test(line)) {\n omittedMessageUpdates += 1;\n omittedMessageUpdateBytes += Buffer.byteLength(line, "utf8");\n return;\n }\n appendFileSync(eventPath, line);\n process.stdout.write(line);\n}\nfunction handleStdoutChunk(chunk) {\n let text = chunk.toString("utf8");\n while (text.length > 0) {\n if (discardingOversizedLine) {\n const newline = text.indexOf("\\n");\n omittedOversizedBytes += Buffer.byteLength(newline < 0 ? text : text.slice(0, newline + 1), "utf8");\n if (newline < 0) return;\n discardingOversizedLine = false;\n text = text.slice(newline + 1);\n continue;\n }\n const newline = text.indexOf("\\n");\n const segment = newline < 0 ? text : text.slice(0, newline + 1);\n stdoutBuffer += segment;\n text = newline < 0 ? "" : text.slice(newline + 1);\n if (stdoutBuffer.length > maxStdoutLogLineChars) {\n omittedOversizedLines += 1;\n omittedOversizedBytes += Buffer.byteLength(stdoutBuffer, "utf8");\n stdoutBuffer = "";\n discardingOversizedLine = newline < 0;\n continue;\n }\n if (newline >= 0) {\n writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n }\n }\n}\nfunction finishStdoutFilter() {\n if (!discardingOversizedLine && stdoutBuffer.length > 0) writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n if (omittedMessageUpdates > 0 || omittedOversizedLines > 0) {\n appendFileSync(eventPath, JSON.stringify({ type: "pi-subagent.stdout_filter", omitted: { messageUpdateEvents: omittedMessageUpdates, messageUpdateBytes: omittedMessageUpdateBytes, oversizedLines: omittedOversizedLines, oversizedBytes: omittedOversizedBytes }, reason: "cumulative message_update snapshots are omitted from durable stdout artifacts; final assistant text is stored in output.log" }) + "\\n");\n }\n}\nfunction writeMeta(meta) {\n if (settled) return;\n settled = true;\n finishStdoutFilter();\n writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\\n");\n}\nconst env = { ...process.env };\ndelete env.TMUX;\nconst child = spawn(argv[0], argv.slice(1), { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], env });\nchild.stdout?.on("data", handleStdoutChunk);\nchild.stderr?.on("data", (chunk) => { appendFileSync(stderrPath, chunk); process.stderr.write(chunk); });\nchild.on("error", () => { writeMeta({ status: "failed", failureKind: "spawn", exitCode: null, signal: null }); });\nchild.on("close", (exitCode, signal) => {\n const failureKind = exitCode === 0 ? null : "exit";\n writeMeta({ status: failureKind === null ? "completed" : "failed", failureKind, exitCode, signal });\n});\n`;
|
|
127
158
|
}
|
|
128
159
|
|
|
129
|
-
async function runTmuxProcess(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
160
|
+
async function runTmuxProcess(
|
|
161
|
+
options: RunTmuxProcessOptions,
|
|
162
|
+
): Promise<{
|
|
163
|
+
result: TmuxRunResult | null;
|
|
164
|
+
store: Awaited<ReturnType<typeof createAttemptArtifactStore>>;
|
|
165
|
+
cwd: string;
|
|
166
|
+
artifactCwd: string;
|
|
167
|
+
startedAt: Date;
|
|
168
|
+
failure?: WorkerMeta;
|
|
169
|
+
stderr?: string;
|
|
170
|
+
}> {
|
|
171
|
+
const argv = options.argv;
|
|
172
|
+
assertRunnableArgv(argv);
|
|
173
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
174
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
175
|
+
const artifactCwd = resolve(options.artifactCwd ?? cwd);
|
|
176
|
+
const startedAt = new Date();
|
|
177
|
+
const store = await createAttemptArtifactStore({
|
|
178
|
+
cwd: artifactCwd,
|
|
179
|
+
runId: options.runId,
|
|
180
|
+
attemptId: options.attemptId,
|
|
181
|
+
runsDir: options.runsDir,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (!(await tmuxAvailable())) {
|
|
185
|
+
return {
|
|
186
|
+
result: null,
|
|
187
|
+
store,
|
|
188
|
+
cwd,
|
|
189
|
+
artifactCwd,
|
|
190
|
+
startedAt,
|
|
191
|
+
failure: {
|
|
192
|
+
status: "failed",
|
|
193
|
+
failureKind: "spawn",
|
|
194
|
+
exitCode: null,
|
|
195
|
+
signal: null,
|
|
196
|
+
},
|
|
197
|
+
stderr:
|
|
198
|
+
'tmux is not available on PATH; install tmux or choose backend "headless".\n',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sessionName = `pi-subagent-${store.runId}-${store.attemptId}`.replace(
|
|
203
|
+
/[^A-Za-z0-9_-]/g,
|
|
204
|
+
"-",
|
|
205
|
+
);
|
|
206
|
+
const eventPath = join(store.taskDir, "pi-events.jsonl");
|
|
207
|
+
const stderrPath = store.pathFor("stderr");
|
|
208
|
+
const metaPath = join(store.taskDir, "tmux-worker-meta.json");
|
|
209
|
+
const scriptPath = join(store.taskDir, "tmux-worker.mjs");
|
|
210
|
+
const launchPath = join(store.taskDir, "tmux-launch.sh");
|
|
211
|
+
|
|
212
|
+
await writeFile(
|
|
213
|
+
scriptPath,
|
|
214
|
+
workerScript(argv, cwd, eventPath, stderrPath, metaPath),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await writeFile(
|
|
218
|
+
launchPath,
|
|
219
|
+
`#!/usr/bin/env bash\nset -euo pipefail\nunset TMUX\nexec ${shellQuote(process.execPath)} ${shellQuote(scriptPath)}\n`,
|
|
220
|
+
);
|
|
221
|
+
await chmod(launchPath, 0o700);
|
|
222
|
+
|
|
223
|
+
async function runSession(
|
|
224
|
+
tmuxCommand: string,
|
|
225
|
+
tmuxArgs: readonly string[],
|
|
226
|
+
tmuxEnv?: NodeJS.ProcessEnv,
|
|
227
|
+
): Promise<{
|
|
228
|
+
result: TmuxRunResult | null;
|
|
229
|
+
store: Awaited<ReturnType<typeof createAttemptArtifactStore>>;
|
|
230
|
+
cwd: string;
|
|
231
|
+
artifactCwd: string;
|
|
232
|
+
startedAt: Date;
|
|
233
|
+
failure?: WorkerMeta;
|
|
234
|
+
stderr?: string;
|
|
235
|
+
}> {
|
|
236
|
+
let sessionId: string | null = null;
|
|
237
|
+
let paneId: string | null = null;
|
|
238
|
+
try {
|
|
239
|
+
const { stdout } = await execFileAsync(
|
|
240
|
+
"tmux",
|
|
241
|
+
[
|
|
242
|
+
"new-session",
|
|
243
|
+
"-d",
|
|
244
|
+
"-s",
|
|
245
|
+
sessionName,
|
|
246
|
+
"-P",
|
|
247
|
+
"-F",
|
|
248
|
+
"#{session_id}\t#{pane_id}",
|
|
249
|
+
tmuxCommand,
|
|
250
|
+
...tmuxArgs,
|
|
251
|
+
],
|
|
252
|
+
{ cwd, ...(tmuxEnv === undefined ? {} : { env: tmuxEnv }) },
|
|
253
|
+
);
|
|
254
|
+
const [rawSessionId, rawPaneId] = stdout.trim().split("\t");
|
|
255
|
+
sessionId = rawSessionId || null;
|
|
256
|
+
paneId = rawPaneId || null;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return {
|
|
259
|
+
result: null,
|
|
260
|
+
store,
|
|
261
|
+
cwd,
|
|
262
|
+
artifactCwd,
|
|
263
|
+
startedAt,
|
|
264
|
+
failure: {
|
|
265
|
+
status: "failed",
|
|
266
|
+
failureKind: "spawn",
|
|
267
|
+
exitCode: null,
|
|
268
|
+
signal: null,
|
|
269
|
+
},
|
|
270
|
+
stderr:
|
|
271
|
+
error instanceof Error ? `${error.message}\n` : `${String(error)}\n`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const deadline =
|
|
276
|
+
timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
277
|
+
let stopKind: "timeout" | "abort" | null = null;
|
|
278
|
+
|
|
279
|
+
while (true) {
|
|
280
|
+
const meta = await readWorkerMeta(metaPath);
|
|
281
|
+
if (meta !== undefined) {
|
|
282
|
+
await killTmuxSession(sessionName);
|
|
283
|
+
return {
|
|
284
|
+
result: {
|
|
285
|
+
meta,
|
|
286
|
+
stderrRef: store.refFor("stderr", await pathBytes(stderrPath)),
|
|
287
|
+
eventPath,
|
|
288
|
+
tmux: { sessionName, sessionId, paneId },
|
|
289
|
+
},
|
|
290
|
+
store,
|
|
291
|
+
cwd,
|
|
292
|
+
artifactCwd,
|
|
293
|
+
startedAt,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (options.signal?.aborted) stopKind = "abort";
|
|
298
|
+
if (deadline !== undefined && Date.now() >= deadline)
|
|
299
|
+
stopKind = "timeout";
|
|
300
|
+
if (stopKind !== null) {
|
|
301
|
+
await killTmuxSession(sessionName);
|
|
302
|
+
return {
|
|
303
|
+
result: {
|
|
304
|
+
meta: {
|
|
305
|
+
status: "failed",
|
|
306
|
+
failureKind: stopKind,
|
|
307
|
+
exitCode: null,
|
|
308
|
+
signal: "SIGTERM",
|
|
309
|
+
},
|
|
310
|
+
stderrRef: store.refFor("stderr", await pathBytes(stderrPath)),
|
|
311
|
+
eventPath,
|
|
312
|
+
tmux: { sessionName, sessionId, paneId },
|
|
313
|
+
},
|
|
314
|
+
store,
|
|
315
|
+
cwd,
|
|
316
|
+
artifactCwd,
|
|
317
|
+
startedAt,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!(await tmuxSessionAlive(sessionName))) {
|
|
322
|
+
return {
|
|
323
|
+
result: {
|
|
324
|
+
meta: {
|
|
325
|
+
status: "failed",
|
|
326
|
+
failureKind: "spawn",
|
|
327
|
+
exitCode: null,
|
|
328
|
+
signal: null,
|
|
329
|
+
},
|
|
330
|
+
stderrRef: store.refFor("stderr", await pathBytes(stderrPath)),
|
|
331
|
+
eventPath,
|
|
332
|
+
tmux: { sessionName, sessionId, paneId },
|
|
333
|
+
},
|
|
334
|
+
store,
|
|
335
|
+
cwd,
|
|
336
|
+
artifactCwd,
|
|
337
|
+
startedAt,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await sleep(POLL_INTERVAL_MS);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
if (options.sandbox) {
|
|
347
|
+
return await withSandboxedArgv(
|
|
348
|
+
[process.execPath, scriptPath],
|
|
349
|
+
{
|
|
350
|
+
sandbox: options.sandbox,
|
|
351
|
+
cwd,
|
|
352
|
+
writablePaths: [store.taskDir],
|
|
353
|
+
allowPty: true,
|
|
354
|
+
signal: options.signal,
|
|
355
|
+
},
|
|
356
|
+
async (launch) => {
|
|
357
|
+
await writeFile(
|
|
358
|
+
launchPath,
|
|
359
|
+
`#!/usr/bin/env bash\nset -euo pipefail\nunset TMUX\nexec ${launch.argv.map(shellQuote).join(" ")}\n`,
|
|
360
|
+
);
|
|
361
|
+
await chmod(launchPath, 0o700);
|
|
362
|
+
return await runSession("/bin/bash", [launchPath], launch.env);
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return await runSession("/bin/bash", [launchPath]);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (!(error instanceof SandboxUnavailableError)) throw error;
|
|
369
|
+
return {
|
|
370
|
+
result: null,
|
|
371
|
+
store,
|
|
372
|
+
cwd,
|
|
373
|
+
artifactCwd,
|
|
374
|
+
startedAt,
|
|
375
|
+
failure: {
|
|
376
|
+
status: "failed",
|
|
377
|
+
failureKind: "sandbox",
|
|
378
|
+
exitCode: null,
|
|
379
|
+
signal: null,
|
|
380
|
+
},
|
|
381
|
+
stderr: `${error.message}\n`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
261
384
|
}
|
|
262
385
|
|
|
263
|
-
export async function runTmuxModel(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
386
|
+
export async function runTmuxModel(
|
|
387
|
+
options: RunTmuxModelOptions,
|
|
388
|
+
): Promise<ResultEnvelope> {
|
|
389
|
+
const sandbox = options.sandbox
|
|
390
|
+
? { enabled: true, allowedDomains: sandboxAllowedDomains(options.sandbox) }
|
|
391
|
+
: { enabled: false };
|
|
392
|
+
if (typeof options.agent !== "string" || options.agent.length === 0) {
|
|
393
|
+
throw new Error("agent must be a non-empty string.");
|
|
394
|
+
}
|
|
395
|
+
if (typeof options.task !== "string" || options.task.length === 0) {
|
|
396
|
+
throw new Error("task must be a non-empty string.");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const sessionMetadata = await resultSessionMetadata(
|
|
400
|
+
resolve(options.cwd ?? process.cwd()),
|
|
401
|
+
options.sessionId,
|
|
402
|
+
);
|
|
403
|
+
const { result, store, cwd, artifactCwd, startedAt, failure, stderr } =
|
|
404
|
+
await runTmuxProcess({ ...options, argv: buildPiArgv(options) });
|
|
405
|
+
if (result === null) {
|
|
406
|
+
const artifacts: ArtifactRef[] = [
|
|
407
|
+
await store.writeTextArtifact("stderr", stderr ?? ""),
|
|
408
|
+
await store.writeTextArtifact("output", ""),
|
|
409
|
+
];
|
|
410
|
+
return await store.writeResult({
|
|
411
|
+
backend: "tmux",
|
|
412
|
+
status: failure?.status ?? "failed",
|
|
413
|
+
failureKind: failure?.failureKind ?? "spawn",
|
|
414
|
+
cwd: artifactCwd,
|
|
415
|
+
startedAt,
|
|
416
|
+
completedAt: new Date(),
|
|
417
|
+
workspace: options.workspace ?? { mode: "shared", cwd },
|
|
418
|
+
sandbox,
|
|
419
|
+
exitCode: failure?.exitCode ?? null,
|
|
420
|
+
signal: failure?.signal ?? null,
|
|
421
|
+
artifacts,
|
|
422
|
+
correlationId: options.correlationId,
|
|
423
|
+
metadata: {
|
|
424
|
+
contextLengthExceeded: detectContextLengthExceeded({
|
|
425
|
+
stderrText: stderr ?? "",
|
|
426
|
+
}),
|
|
427
|
+
...sessionMetadata,
|
|
428
|
+
...(options.parentSessionId === undefined
|
|
429
|
+
? {}
|
|
430
|
+
: { parentSessionId: options.parentSessionId }),
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const stderrText = await import("node:fs/promises").then(({ readFile }) =>
|
|
436
|
+
readFile(store.pathFor("stderr"), "utf8").catch(() => ""),
|
|
437
|
+
);
|
|
438
|
+
const parsed = await parsePiJsonFile(result.eventPath).catch(() =>
|
|
439
|
+
parsePiJsonLines(""),
|
|
440
|
+
);
|
|
441
|
+
await unlink(result.eventPath).catch(() => undefined);
|
|
442
|
+
const contextLengthExceeded = detectContextLengthExceeded({
|
|
443
|
+
stderrText,
|
|
444
|
+
errors: parsed.errors,
|
|
445
|
+
});
|
|
446
|
+
const meta = resolvePiJsonOutcome(result.meta, parsed, contextLengthExceeded);
|
|
447
|
+
|
|
448
|
+
const outputRef = await store.writeTextArtifact(
|
|
449
|
+
"output",
|
|
450
|
+
parsed.finalAssistantText,
|
|
451
|
+
);
|
|
452
|
+
return await store.writeResult({
|
|
453
|
+
backend: "tmux",
|
|
454
|
+
status: meta.status,
|
|
455
|
+
failureKind: meta.failureKind,
|
|
456
|
+
cwd: artifactCwd,
|
|
457
|
+
startedAt,
|
|
458
|
+
completedAt: new Date(),
|
|
459
|
+
workspace: options.workspace ?? { mode: "shared", cwd },
|
|
460
|
+
sandbox,
|
|
461
|
+
exitCode: meta.exitCode,
|
|
462
|
+
signal: meta.signal,
|
|
463
|
+
artifacts: [result.stderrRef, outputRef],
|
|
464
|
+
tmux: result.tmux,
|
|
465
|
+
correlationId: options.correlationId,
|
|
466
|
+
metadata: {
|
|
467
|
+
...resultMetadataFromParse(parsed, contextLengthExceeded, meta),
|
|
468
|
+
...sessionMetadata,
|
|
469
|
+
...(options.parentSessionId === undefined
|
|
470
|
+
? {}
|
|
471
|
+
: { parentSessionId: options.parentSessionId }),
|
|
472
|
+
},
|
|
473
|
+
});
|
|
320
474
|
}
|