refactor: isolate tmux runner implementation
This commit is contained in:
2
index.ts
2
index.ts
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { createRunArtifacts } from "./src/artifacts.ts";
|
import { createRunArtifacts } from "./src/artifacts.ts";
|
||||||
import { monitorRun } from "./src/monitor.ts";
|
import { monitorRun } from "./src/monitor.ts";
|
||||||
import { listAvailableModelReferences } from "./src/models.ts";
|
import { listAvailableModelReferences } from "./src/models.ts";
|
||||||
import { createTmuxSingleRunner } from "./src/runner.ts";
|
import { createTmuxSingleRunner } from "./src/tmux-runner.ts";
|
||||||
import {
|
import {
|
||||||
buildCurrentWindowArgs,
|
buildCurrentWindowArgs,
|
||||||
buildKillPaneArgs,
|
buildKillPaneArgs,
|
||||||
|
|||||||
@@ -1,44 +1,9 @@
|
|||||||
export function createTmuxSingleRunner(deps: {
|
import type { SubagentRunResult } from "./schema.ts";
|
||||||
assertInsideTmux(): void;
|
|
||||||
getCurrentWindowId: () => Promise<string>;
|
|
||||||
createArtifacts: (cwd: string, meta: Record<string, unknown>) => Promise<any>;
|
|
||||||
buildWrapperCommand: (metaPath: string) => string;
|
|
||||||
createPane: (input: { windowId: string; cwd: string; command: string }) => Promise<string>;
|
|
||||||
monitorRun: (input: { eventsPath: string; resultPath: string; onEvent?: (event: any) => void }) => Promise<any>;
|
|
||||||
killPane: (paneId: string) => Promise<void>;
|
|
||||||
}) {
|
|
||||||
return async function runSingleTask(input: {
|
|
||||||
cwd: string;
|
|
||||||
meta: Record<string, unknown>;
|
|
||||||
onEvent?: (event: any) => void;
|
|
||||||
}) {
|
|
||||||
deps.assertInsideTmux();
|
|
||||||
|
|
||||||
const artifacts = await deps.createArtifacts(input.cwd, input.meta);
|
export interface RunSingleTaskInput {
|
||||||
const windowId = await deps.getCurrentWindowId();
|
cwd: string;
|
||||||
const command = deps.buildWrapperCommand(artifacts.metaPath);
|
meta: Record<string, unknown>;
|
||||||
const paneId = await deps.createPane({ windowId, cwd: input.cwd, command });
|
onEvent?: (event: any) => void;
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await deps.monitorRun({
|
|
||||||
eventsPath: artifacts.eventsPath,
|
|
||||||
resultPath: artifacts.resultPath,
|
|
||||||
onEvent: input.onEvent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
runId: result.runId ?? artifacts.runId,
|
|
||||||
paneId,
|
|
||||||
windowId,
|
|
||||||
sessionPath: result.sessionPath ?? artifacts.sessionPath,
|
|
||||||
stdoutPath: result.stdoutPath ?? artifacts.stdoutPath,
|
|
||||||
stderrPath: result.stderrPath ?? artifacts.stderrPath,
|
|
||||||
resultPath: artifacts.resultPath,
|
|
||||||
eventsPath: artifacts.eventsPath,
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
await deps.killPane(paneId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RunSingleTask = (input: RunSingleTaskInput) => Promise<SubagentRunResult>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { createTmuxSingleRunner } from "./runner.ts";
|
import { createTmuxSingleRunner } from "./tmux-runner.ts";
|
||||||
|
|
||||||
test("createTmuxSingleRunner always kills the pane after monitor completion", async () => {
|
test("createTmuxSingleRunner always kills the pane after monitor completion", async () => {
|
||||||
const killed: string[] = [];
|
const killed: string[] = [];
|
||||||
@@ -31,3 +31,30 @@ test("createTmuxSingleRunner always kills the pane after monitor completion", as
|
|||||||
assert.equal(result.finalText, "done");
|
assert.equal(result.finalText, "done");
|
||||||
assert.deepEqual(killed, ["%9"]);
|
assert.deepEqual(killed, ["%9"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("createTmuxSingleRunner surfaces explicit tmux precondition errors", async () => {
|
||||||
|
const runSingleTask = createTmuxSingleRunner({
|
||||||
|
assertInsideTmux() {
|
||||||
|
throw new Error("tmux-backed subagents require pi to be running inside tmux.");
|
||||||
|
},
|
||||||
|
getCurrentWindowId: async () => "@1",
|
||||||
|
createArtifacts: async () => ({
|
||||||
|
metaPath: "/tmp/meta.json",
|
||||||
|
runId: "run-1",
|
||||||
|
eventsPath: "/tmp/events.jsonl",
|
||||||
|
resultPath: "/tmp/result.json",
|
||||||
|
sessionPath: "/tmp/child-session.jsonl",
|
||||||
|
stdoutPath: "/tmp/stdout.log",
|
||||||
|
stderrPath: "/tmp/stderr.log",
|
||||||
|
}),
|
||||||
|
buildWrapperCommand: () => "'node' '/wrapper.mjs' '/tmp/meta.json'",
|
||||||
|
createPane: async () => "%9",
|
||||||
|
monitorRun: async () => ({ finalText: "done", exitCode: 0 }),
|
||||||
|
killPane: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => runSingleTask({ cwd: "/repo", meta: { task: "inspect auth" } as any }),
|
||||||
|
/tmux-backed subagents require pi to be running inside tmux/,
|
||||||
|
);
|
||||||
|
});
|
||||||
44
src/tmux-runner.ts
Normal file
44
src/tmux-runner.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export function createTmuxSingleRunner(deps: {
|
||||||
|
assertInsideTmux(): void;
|
||||||
|
getCurrentWindowId: () => Promise<string>;
|
||||||
|
createArtifacts: (cwd: string, meta: Record<string, unknown>) => Promise<any>;
|
||||||
|
buildWrapperCommand: (metaPath: string) => string;
|
||||||
|
createPane: (input: { windowId: string; cwd: string; command: string }) => Promise<string>;
|
||||||
|
monitorRun: (input: { eventsPath: string; resultPath: string; onEvent?: (event: any) => void }) => Promise<any>;
|
||||||
|
killPane: (paneId: string) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
return async function runSingleTask(input: {
|
||||||
|
cwd: string;
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
onEvent?: (event: any) => void;
|
||||||
|
}) {
|
||||||
|
deps.assertInsideTmux();
|
||||||
|
|
||||||
|
const artifacts = await deps.createArtifacts(input.cwd, input.meta);
|
||||||
|
const windowId = await deps.getCurrentWindowId();
|
||||||
|
const command = deps.buildWrapperCommand(artifacts.metaPath);
|
||||||
|
const paneId = await deps.createPane({ windowId, cwd: input.cwd, command });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await deps.monitorRun({
|
||||||
|
eventsPath: artifacts.eventsPath,
|
||||||
|
resultPath: artifacts.resultPath,
|
||||||
|
onEvent: input.onEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
runId: result.runId ?? artifacts.runId,
|
||||||
|
paneId,
|
||||||
|
windowId,
|
||||||
|
sessionPath: result.sessionPath ?? artifacts.sessionPath,
|
||||||
|
stdoutPath: result.stdoutPath ?? artifacts.stdoutPath,
|
||||||
|
stderrPath: result.stderrPath ?? artifacts.stderrPath,
|
||||||
|
resultPath: artifacts.resultPath,
|
||||||
|
eventsPath: artifacts.eventsPath,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await deps.killPane(paneId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user