@tekmidian/pai 0.42.6 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chain-BbcfP77K.mjs → chain-CEC1a5mX.mjs} +2 -2
- package/dist/{chain-BbcfP77K.mjs.map → chain-CEC1a5mX.mjs.map} +1 -1
- package/dist/cli/index.mjs +5 -5
- package/dist/cli/program.mjs +5 -5
- package/dist/daemon/index.mjs +4 -4
- package/dist/{daemon-BppziPi6.mjs → daemon-BhldpFvO.mjs} +6 -6
- package/dist/{daemon-BppziPi6.mjs.map → daemon-BhldpFvO.mjs.map} +1 -1
- package/dist/{daemon-BNjbVsQU.mjs → daemon-QIvCXB5f.mjs} +4 -4
- package/dist/daemon-mcp/index.mjs +3 -3
- package/dist/{fallback-Cwt4hmh5.mjs → fallback-BQJ8PbCK.mjs} +6 -4
- package/dist/fallback-BQJ8PbCK.mjs.map +1 -0
- package/dist/hooks/load-project-context.mjs +51 -48
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/whisper-reinject.mjs +8 -0
- package/dist/hooks/whisper-reinject.mjs.map +3 -3
- package/dist/hooks/worker-status-line.mjs +108 -41
- package/dist/hooks/worker-status-line.mjs.map +3 -3
- package/dist/{planner-pXRc-8yt.mjs → planner-BlKGgpWI.mjs} +3 -3
- package/dist/{planner-pXRc-8yt.mjs.map → planner-BlKGgpWI.mjs.map} +1 -1
- package/dist/{program-LLNPpxlj.mjs → program-C5jRQ1kg.mjs} +749 -25
- package/dist/program-C5jRQ1kg.mjs.map +1 -0
- package/dist/{run-DDdT9teG.mjs → run-ByBKowN5.mjs} +472 -59
- package/dist/run-ByBKowN5.mjs.map +1 -0
- package/dist/{tools-BradfQk7.mjs → tools-CAG2sUjf.mjs} +2 -2
- package/dist/{tools-UrSHIe29.mjs → tools-DiRC5glA.mjs} +2 -2
- package/dist/{tools-UrSHIe29.mjs.map → tools-DiRC5glA.mjs.map} +1 -1
- package/dist/{wakeup-TnGjJY3I.mjs → wakeup-BfElduEU.mjs} +21 -8
- package/dist/wakeup-BfElduEU.mjs.map +1 -0
- package/docs/commands/README.md +3 -1
- package/docs/commands/audit.md +27 -2
- package/docs/commands/worker.md +2 -0
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +2 -0
- package/src/hooks/ts/post-tool-use/whisper-reinject.test.ts +55 -0
- package/src/hooks/ts/post-tool-use/whisper-reinject.ts +3 -0
- package/src/hooks/ztk-auto.sh +28 -0
- package/src/hooks/ztk-auto.test.ts +72 -0
- package/src/hooks/ztk-passthrough.mjs +71 -0
- package/dist/fallback-Cwt4hmh5.mjs.map +0 -1
- package/dist/program-LLNPpxlj.mjs.map +0 -1
- package/dist/run-DDdT9teG.mjs.map +0 -1
- package/dist/wakeup-TnGjJY3I.mjs.map +0 -1
|
@@ -1825,70 +1825,333 @@ function worktreeSystemPrompt(id, branch, dir) {
|
|
|
1825
1825
|
].join("\n");
|
|
1826
1826
|
}
|
|
1827
1827
|
|
|
1828
|
+
//#endregion
|
|
1829
|
+
//#region src/workers/agentish.ts
|
|
1830
|
+
/**
|
|
1831
|
+
* agentish.ts — Agentish v2 (AG2), the wire format a worker's final report
|
|
1832
|
+
* uses by default: a kind line plus one-letter k=v lines instead of JSON
|
|
1833
|
+
* prose. Spec and validator live in the `aibroker` CLI
|
|
1834
|
+
* (see /Users/i052341/Daten/Cloud/Development/ai/AIBroker/docs/agentish.md);
|
|
1835
|
+
* this module shells out to it when present and falls back to a frozen copy
|
|
1836
|
+
* of the spec text otherwise, so a worker still gets a contract when
|
|
1837
|
+
* `aibroker` is not installed on its box.
|
|
1838
|
+
*/
|
|
1839
|
+
/** Frozen copy of `aibroker agentish spec`'s first line, for when the CLI is absent. */
|
|
1840
|
+
const AG2_SPEC_FALLBACK = "AG2. msg=kind line+k=v lines. kinds T R S Q A X. keys i id g goal o own n forbid d steps p proof u out l limits r res c changes t tests G gate I inst m images # nums w worst x next z note(<200ch). sep |. outcomes + - ~ ? !. @n=path declared once then reused; @n:12=file:line. tests as Name+ Name-. no prose, no articles, never restate, unknown=?. r=+ only if all t +.";
|
|
1841
|
+
/** Frozen copy of the `y` (why) extension line. */
|
|
1842
|
+
const AG2_EXTENSIONS_FALLBACK = "y why(≤600ch)";
|
|
1843
|
+
let cachedSpec = null;
|
|
1844
|
+
/**
|
|
1845
|
+
* The AG2 spec + extensions lines: from `aibroker agentish spec` when the
|
|
1846
|
+
* CLI runs and its output looks right, else the frozen fallback. Memoised —
|
|
1847
|
+
* every caller in one process gets the same spec without re-spawning aibroker.
|
|
1848
|
+
*/
|
|
1849
|
+
function ag2Spec() {
|
|
1850
|
+
if (cachedSpec) return cachedSpec;
|
|
1851
|
+
try {
|
|
1852
|
+
const lines = execFileSync("aibroker", ["agentish", "spec"], {
|
|
1853
|
+
timeout: 3e3,
|
|
1854
|
+
stdio: [
|
|
1855
|
+
"ignore",
|
|
1856
|
+
"pipe",
|
|
1857
|
+
"ignore"
|
|
1858
|
+
],
|
|
1859
|
+
encoding: "utf8"
|
|
1860
|
+
}).split("\n").filter((l) => l.trim());
|
|
1861
|
+
if (lines[0]?.startsWith("AG2.")) {
|
|
1862
|
+
cachedSpec = {
|
|
1863
|
+
spec: lines[0],
|
|
1864
|
+
extensions: lines[1] ?? AG2_EXTENSIONS_FALLBACK,
|
|
1865
|
+
source: "aibroker"
|
|
1866
|
+
};
|
|
1867
|
+
return cachedSpec;
|
|
1868
|
+
}
|
|
1869
|
+
} catch {}
|
|
1870
|
+
cachedSpec = {
|
|
1871
|
+
spec: AG2_SPEC_FALLBACK,
|
|
1872
|
+
extensions: AG2_EXTENSIONS_FALLBACK,
|
|
1873
|
+
source: "builtin"
|
|
1874
|
+
};
|
|
1875
|
+
return cachedSpec;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Parse one AG2 message: first non-empty line is the kind (T R S Q A X,
|
|
1879
|
+
* optionally followed by trailing text — only the first character is the
|
|
1880
|
+
* kind), later lines are `key=value` or `@n=path` symbol declarations.
|
|
1881
|
+
* ```-fenced lines are ignored so a message pasted inside a code fence still
|
|
1882
|
+
* parses. Returns null when the first non-empty line is not a known kind.
|
|
1883
|
+
*/
|
|
1884
|
+
function parseAg2Report(text) {
|
|
1885
|
+
const kinds = new Set([
|
|
1886
|
+
"T",
|
|
1887
|
+
"R",
|
|
1888
|
+
"S",
|
|
1889
|
+
"Q",
|
|
1890
|
+
"A",
|
|
1891
|
+
"X"
|
|
1892
|
+
]);
|
|
1893
|
+
const lines = (text ?? "").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("```"));
|
|
1894
|
+
if (!lines.length) return null;
|
|
1895
|
+
const kind = lines[0][0];
|
|
1896
|
+
if (!kinds.has(kind)) return null;
|
|
1897
|
+
const fields = {};
|
|
1898
|
+
const symbols = {};
|
|
1899
|
+
for (const line of lines.slice(1)) {
|
|
1900
|
+
const eq = line.indexOf("=");
|
|
1901
|
+
if (eq < 0) continue;
|
|
1902
|
+
const key = line.slice(0, eq).trim();
|
|
1903
|
+
const value = line.slice(eq + 1).trim();
|
|
1904
|
+
if (key.startsWith("@")) symbols[key.slice(1)] = value;
|
|
1905
|
+
else fields[key] = value;
|
|
1906
|
+
}
|
|
1907
|
+
return {
|
|
1908
|
+
kind,
|
|
1909
|
+
fields,
|
|
1910
|
+
symbols
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
/** true iff the value ends with the AG2 pass outcome character. */
|
|
1914
|
+
function passed(entry) {
|
|
1915
|
+
return entry.endsWith("+");
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Map a parsed AG2 report onto the runner's WorkerReport shape, so the
|
|
1919
|
+
* viewer and `printResult` can render an AG2 `R` exactly like a JSON one.
|
|
1920
|
+
*/
|
|
1921
|
+
function ag2ToWorkerReport(parsed) {
|
|
1922
|
+
const f = parsed.fields;
|
|
1923
|
+
const out = { format: "ag2" };
|
|
1924
|
+
if (f.c) out.changed = f.c.split("|").map((s) => s.trim()).filter(Boolean).map((entry) => {
|
|
1925
|
+
const colon = entry.indexOf(":");
|
|
1926
|
+
const space = entry.indexOf(" ");
|
|
1927
|
+
const cut = colon >= 0 && (space < 0 || colon < space) ? colon : space;
|
|
1928
|
+
if (cut < 0) return {
|
|
1929
|
+
path: entry,
|
|
1930
|
+
summary: ""
|
|
1931
|
+
};
|
|
1932
|
+
return {
|
|
1933
|
+
path: entry.slice(0, cut).trim(),
|
|
1934
|
+
summary: entry.slice(cut + 1).trim()
|
|
1935
|
+
};
|
|
1936
|
+
});
|
|
1937
|
+
if (f.t) out.checks = f.t.split(/\s+/).filter(Boolean).map((entry) => ({
|
|
1938
|
+
name: entry.slice(0, -1),
|
|
1939
|
+
ok: passed(entry),
|
|
1940
|
+
detail: entry.slice(-1)
|
|
1941
|
+
}));
|
|
1942
|
+
if (f.p) out.commands = f.p.split("|").map((s) => s.trim()).filter(Boolean);
|
|
1943
|
+
const open = [];
|
|
1944
|
+
if (f.x) open.push(f.x);
|
|
1945
|
+
for (const [k, v] of Object.entries(f)) if ((v.endsWith("!") || v.endsWith("?")) && k !== "r" && k !== "res") open.push(`${k}: ${v}`);
|
|
1946
|
+
if (open.length) out.open = open;
|
|
1947
|
+
if (f.z) out.notes = f.z;
|
|
1948
|
+
const res = f.r ?? f.res;
|
|
1949
|
+
if (res === "+" || res === "-" || res === "~" || res === "?" || res === "!") out.result = res;
|
|
1950
|
+
if (f.y) out.why = f.y;
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Validate an AG2 message with `aibroker agentish check - --json`. Never
|
|
1955
|
+
* fails the caller: a missing/broken CLI reports ok:true, validator "none" —
|
|
1956
|
+
* a worker's run is never blocked on a local tooling gap, only its
|
|
1957
|
+
* `reportValid` marker goes unset.
|
|
1958
|
+
*/
|
|
1959
|
+
function validateAg2(text) {
|
|
1960
|
+
try {
|
|
1961
|
+
const out = execFileSync("aibroker", [
|
|
1962
|
+
"agentish",
|
|
1963
|
+
"check",
|
|
1964
|
+
"-",
|
|
1965
|
+
"--json"
|
|
1966
|
+
], {
|
|
1967
|
+
input: text,
|
|
1968
|
+
timeout: 5e3,
|
|
1969
|
+
stdio: [
|
|
1970
|
+
"pipe",
|
|
1971
|
+
"pipe",
|
|
1972
|
+
"ignore"
|
|
1973
|
+
],
|
|
1974
|
+
encoding: "utf8"
|
|
1975
|
+
});
|
|
1976
|
+
const parsed = JSON.parse(out);
|
|
1977
|
+
const errors = (parsed.errors ?? []).map((e) => e.message ?? e.code ?? "unknown error");
|
|
1978
|
+
return {
|
|
1979
|
+
ok: parsed.ok !== false,
|
|
1980
|
+
errors,
|
|
1981
|
+
validator: "aibroker"
|
|
1982
|
+
};
|
|
1983
|
+
} catch (e) {
|
|
1984
|
+
const out = e.stdout;
|
|
1985
|
+
if (out) try {
|
|
1986
|
+
const parsed = JSON.parse(out.toString());
|
|
1987
|
+
const errors = (parsed.errors ?? []).map((er) => er.message ?? er.code ?? "unknown error");
|
|
1988
|
+
return {
|
|
1989
|
+
ok: parsed.ok !== false,
|
|
1990
|
+
errors,
|
|
1991
|
+
validator: "aibroker"
|
|
1992
|
+
};
|
|
1993
|
+
} catch {}
|
|
1994
|
+
return {
|
|
1995
|
+
ok: true,
|
|
1996
|
+
errors: [],
|
|
1997
|
+
validator: "none"
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1828
2002
|
//#endregion
|
|
1829
2003
|
//#region src/workers/report.ts
|
|
1830
2004
|
/**
|
|
1831
2005
|
* report.ts — the worker contract: terse, structured final reports.
|
|
1832
2006
|
*
|
|
1833
2007
|
* Headless workers run with an appended system prompt that fixes their output
|
|
1834
|
-
* shape: act, verify, then finish with ONE final message
|
|
1835
|
-
*
|
|
1836
|
-
* it as a compact block instead of a
|
|
2008
|
+
* shape: act, verify, then finish with ONE final message — AG2 (see
|
|
2009
|
+
* agentish.ts) by default, JSON as the pre-AG2 fallback. The runner parses it
|
|
2010
|
+
* out of the result; the viewer renders it as a compact block instead of a
|
|
2011
|
+
* wall of text.
|
|
1837
2012
|
*/
|
|
1838
2013
|
/** The marker the runner puts on operator messages the worker must answer. */
|
|
1839
2014
|
const OPERATOR_MARK = "[operator]";
|
|
1840
|
-
/**
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2015
|
+
/** Classes that get the short contract — cheap, bounded, single-answer tasks. */
|
|
2016
|
+
const SHORT_CONTRACT_CLASSES = new Set(["spotcheck", "simple"]);
|
|
2017
|
+
/**
|
|
2018
|
+
* The AG2 report block appended to a contract when format is "ag2" — field
|
|
2019
|
+
* set matches `aibroker agentish check`'s real R schema (verified 2026-09-20:
|
|
2020
|
+
* i/r/t/c are required, not optional as an early draft of this contract had
|
|
2021
|
+
* it; r=+ needs G=+ and non-empty p too, not just all-t-passing; R has no
|
|
2022
|
+
* `u`/out field at all — u is T-only — so the answer goes in z instead).
|
|
2023
|
+
*/
|
|
2024
|
+
function ag2ReportBlock() {
|
|
2025
|
+
const spec = ag2Spec();
|
|
2026
|
+
return [
|
|
2027
|
+
spec.spec,
|
|
2028
|
+
spec.extensions,
|
|
2029
|
+
"Final message: one AG2 `R` message only — no prose, no code fence, no markdown.",
|
|
2030
|
+
"R requires i (id), r (+ - ~ ? !), t (tests, Name+ Name-), c (path summary|path summary).",
|
|
2031
|
+
"r=+ requires G=+ (gate) and p non-empty (proof command), and every t entry +. z= the answer/note,",
|
|
2032
|
+
"one line, always present. x= what is left (omit if nothing). y= one line why when r is not +.",
|
|
2033
|
+
"Example:",
|
|
2034
|
+
"R",
|
|
2035
|
+
"i=count-audit-ts",
|
|
2036
|
+
"r=+",
|
|
2037
|
+
"G=+",
|
|
2038
|
+
"c=src/audit summary of files counted",
|
|
2039
|
+
"t=Count+",
|
|
2040
|
+
"p=wc -l src/audit/*.ts",
|
|
2041
|
+
"z=19 files, 2523 lines"
|
|
2042
|
+
];
|
|
2043
|
+
}
|
|
2044
|
+
/** The JSON report block appended to a contract when format is "json" (pre-AG2 fallback). */
|
|
2045
|
+
function jsonReportBlock(lead) {
|
|
2046
|
+
return [lead, "{\"changed\":[{\"path\":\"…\",\"summary\":\"…\"}],\"commands\":[\"…\"],\"checks\":[{\"name\":\"…\",\"ok\":true,\"detail\":\"…\"}],\"open\":[\"…\"],\"notes\":\"one line\"}"];
|
|
2047
|
+
}
|
|
2048
|
+
/** The two sentences that stop a worker from breaking on shell quoting — kept in both contracts. */
|
|
2049
|
+
function fileNotInlineBlock() {
|
|
2050
|
+
return [
|
|
2051
|
+
"NEVER put a multi-line or quoted payload inline in a shell command — no heredocs (<<EOF), no long",
|
|
2052
|
+
"-p '…' specs for child workers, no inline JSON, no inline scripts. ALWAYS write it to a file with",
|
|
2053
|
+
"the Write tool first and pass the file: -p \"$(cat spec.txt)\", --file spec.txt, or < spec.txt.",
|
|
2054
|
+
"Inline quoting fails silently or dies with a parse error and burns a worker launch; there are no",
|
|
2055
|
+
"exceptions.",
|
|
2056
|
+
"Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
|
|
2057
|
+
"denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
|
|
2058
|
+
"Write tool and run it (python3 script.py), or use jq for JSON inspection."
|
|
2059
|
+
];
|
|
2060
|
+
}
|
|
2061
|
+
/** The file-not-inline rule, condensed for the short contract's tight length budget. */
|
|
2062
|
+
function fileNotInlineBlockShort() {
|
|
2063
|
+
return [
|
|
2064
|
+
"NEVER put a multi-line or quoted payload inline in a shell command (no heredocs, no inline",
|
|
2065
|
+
"scripts): write it to a file with the Write tool and pass it, e.g. -p \"$(cat spec.txt)\".",
|
|
2066
|
+
"Never inline code (python3 -c and friends) — write a script file and run it, or use jq."
|
|
2067
|
+
];
|
|
2068
|
+
}
|
|
2069
|
+
/** spotcheck/simple: headless, bounded, no delegation language — under 1500 chars in ag2 format. */
|
|
2070
|
+
function shortContractPrompt(format) {
|
|
2071
|
+
const lines = [
|
|
2072
|
+
"You are a headless implementation worker, run non-interactively.",
|
|
2073
|
+
"This task is yours: answer the question or do the bounded task now. Run at most one command",
|
|
2074
|
+
"to confirm a result and never re-derive a number you already have. No child workers, no handoff.",
|
|
2075
|
+
...fileNotInlineBlockShort()
|
|
2076
|
+
];
|
|
2077
|
+
lines.push(...format === "ag2" ? ag2ReportBlock() : jsonReportBlock("Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:"));
|
|
2078
|
+
return lines.join("\n");
|
|
2079
|
+
}
|
|
2080
|
+
/** Every other class (and no class): the full worker/orchestrator contract. */
|
|
2081
|
+
function fullContractPrompt(format) {
|
|
2082
|
+
const lines = [
|
|
2083
|
+
"You are a headless implementation worker, run non-interactively by an orchestrating session.",
|
|
2084
|
+
"You are the worker: this task is yours to finish in this process. Never hand your whole task to a",
|
|
2085
|
+
"single child worker and step back — that is delegation, not work. Child workers are for two things",
|
|
2086
|
+
"only: (1) PARALLELISING independent parts of your task, each part a self-contained spec; (2) CHEAPER",
|
|
2087
|
+
"bounded sub-tasks — probes, renders, test runs, lookups — run one tier down with --class spotcheck or",
|
|
2088
|
+
"--class simple. Always pin --provider anthropic on children.",
|
|
2089
|
+
"You own your children's results: run them in the foreground or poll until each has finished, read",
|
|
2090
|
+
"their result, verify it, and fold it into your own report. When your turn ends this run ends and any",
|
|
2091
|
+
"running child is killed, so never end with a child still running.",
|
|
2092
|
+
"Ending your turn ends this run. There is no later wake-up: never call ScheduleWakeup, never say you",
|
|
2093
|
+
"will pick something up later, never leave a background command as your final action. Finish, then",
|
|
2094
|
+
"report.",
|
|
2095
|
+
"No narration, no timestamps, no greetings, no summaries of what you read; act, verify, then stop.",
|
|
2096
|
+
...fileNotInlineBlock(),
|
|
2097
|
+
"",
|
|
2098
|
+
`Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
|
|
2099
|
+
"run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
|
|
2100
|
+
"a question gets its answer, an instruction gets one line naming what will change — then continue the",
|
|
2101
|
+
"task you were on.",
|
|
2102
|
+
"",
|
|
2103
|
+
"Sub-workers: you may start your own workers with `pai worker run --class <class> -p '<prompt>'`",
|
|
2104
|
+
"(Bash tool). Your children run on their own provider and report back to you automatically when",
|
|
2105
|
+
"they finish. Send a handoff UP instead of doing work yourself when a cheaper provider would",
|
|
2106
|
+
"suffice, the task is out of your scope, or a decision is needed:",
|
|
2107
|
+
"`pai worker handoff '{\"kind\":\"proposal\",\"text\":\"…\",\"data\":{…}}'` (kinds: proposal,",
|
|
2108
|
+
"question, blocker; results are sent for you when you finish). Sibling workers are not a",
|
|
2109
|
+
"coordination path — there is no sideways channel; everything goes up to your parent.",
|
|
2110
|
+
""
|
|
2111
|
+
];
|
|
2112
|
+
lines.push(...format === "ag2" ? ag2ReportBlock() : jsonReportBlock("Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:"));
|
|
2113
|
+
if (format === "json") lines.push("changed: files you touched (path + one-line summary). commands: the commands that verify the work.", "checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.", "notes: one line, the headline a reviewer reads first.");
|
|
2114
|
+
return lines.join("\n");
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* The system prompt appended to every headless run, chosen by task class and
|
|
2118
|
+
* report format. `spotcheck`/`simple` get a short contract (no delegation
|
|
2119
|
+
* language, under 1200 chars in json format / 1500 in ag2); every other class (and no class) gets the
|
|
2120
|
+
* full contract. Both end with the AG2 report block unless `format` is
|
|
2121
|
+
* "json", the pre-AG2 fallback.
|
|
2122
|
+
*/
|
|
2123
|
+
function workerContractPrompt(cls, format) {
|
|
2124
|
+
return SHORT_CONTRACT_CLASSES.has(cls ?? "") ? shortContractPrompt(format) : fullContractPrompt(format);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* The line appended to a headless prompt's own text (never the system
|
|
2128
|
+
* prompt): models weight an instruction at the end of the user turn more
|
|
2129
|
+
* than one buried in a long system-prompt paragraph, and this is the
|
|
2130
|
+
* difference between a worker that returns a valid AG2/JSON report and one
|
|
2131
|
+
* that answers in prose despite the contract (measured 2026-09-20 on both
|
|
2132
|
+
* Haiku and Sonnet with the AG2 contract alone).
|
|
2133
|
+
*/
|
|
2134
|
+
function promptTrailer(format) {
|
|
2135
|
+
return format === "ag2" ? "\n\nFinal message: one AG2 R message only, no prose (format in system prompt)." : "\n\nFinal message: a JSON object only, no prose before or after, no code fence (format in system prompt).";
|
|
2136
|
+
}
|
|
2137
|
+
/** The exact text sent to resend a final message that failed AG2 validation. */
|
|
2138
|
+
const AG2_REASK_TEXT = "Your final message was not a valid AG2 R message. Resend it now as a single AG2 R message only: first line R, then i=id r=result G=+ (gate, if r=+) t=tests(Name+) c=path summary p=proof z=the answer/note. No prose.";
|
|
2139
|
+
/** The full-contract AG2 prompt — the keepalive path's identical-construction baseline. */
|
|
2140
|
+
const WORKER_CONTRACT_PROMPT = fullContractPrompt("ag2");
|
|
2141
|
+
/**
|
|
2142
|
+
* Extract the report from a worker's final message: AG2 first (an `R`
|
|
2143
|
+
* message, see agentish.ts), then the JSON contract as a fallback for
|
|
2144
|
+
* workers still on the pre-AG2 shape. Accepts a bare JSON object, a
|
|
2145
|
+
* ```json fenced block, or an object embedded in surrounding text — only
|
|
2146
|
+
* objects that look like the contract count (at least one of changed /
|
|
1886
2147
|
* checks / notes) — any other JSON falls through to null so the raw text is
|
|
1887
2148
|
* kept as-is.
|
|
1888
2149
|
*/
|
|
1889
2150
|
function parseWorkerReport(text) {
|
|
1890
2151
|
const trimmed = (text ?? "").trim();
|
|
1891
2152
|
if (!trimmed) return null;
|
|
2153
|
+
const ag2 = parseAg2Report(trimmed);
|
|
2154
|
+
if (ag2 && ag2.kind === "R") return ag2ToWorkerReport(ag2);
|
|
1892
2155
|
const candidates = [];
|
|
1893
2156
|
const fence = trimmed.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
1894
2157
|
if (fence) candidates.push(fence[1]);
|
|
@@ -1898,7 +2161,10 @@ function parseWorkerReport(text) {
|
|
|
1898
2161
|
if (first >= 0 && last > first) candidates.push(trimmed.slice(first, last + 1));
|
|
1899
2162
|
for (const cand of candidates) try {
|
|
1900
2163
|
const v = JSON.parse(cand);
|
|
1901
|
-
if (typeof v === "object" && v !== null && !Array.isArray(v) && looksLikeReport(v)) return
|
|
2164
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v) && looksLikeReport(v)) return {
|
|
2165
|
+
...v,
|
|
2166
|
+
format: "json"
|
|
2167
|
+
};
|
|
1902
2168
|
} catch {}
|
|
1903
2169
|
return null;
|
|
1904
2170
|
}
|
|
@@ -2204,6 +2470,14 @@ function codexInstalled() {
|
|
|
2204
2470
|
* Auto-routed runs that die of a quota error before the first tool call are
|
|
2205
2471
|
* restarted on the next provider in routing order (WORKER-REROUTE ledger line).
|
|
2206
2472
|
*/
|
|
2473
|
+
/**
|
|
2474
|
+
* ag2 unless the caller passed --report json or PAI_WORKER_REPORT=json is
|
|
2475
|
+
* set in the environment; an explicit flag always wins over the env default.
|
|
2476
|
+
*/
|
|
2477
|
+
function resolveReportFormat(flag, env = process.env) {
|
|
2478
|
+
if (flag) return flag;
|
|
2479
|
+
return env.PAI_WORKER_REPORT === "json" ? "json" : "ag2";
|
|
2480
|
+
}
|
|
2207
2481
|
/** A stream-json user message for the child's stdin. */
|
|
2208
2482
|
function stdinUserMessage(text) {
|
|
2209
2483
|
return JSON.stringify({
|
|
@@ -2312,6 +2586,31 @@ function modelArgs(model, callerPinned) {
|
|
|
2312
2586
|
function modelFlagArgs(headless, model, callerPinned) {
|
|
2313
2587
|
return headless ? modelArgs(model, callerPinned) : [];
|
|
2314
2588
|
}
|
|
2589
|
+
/**
|
|
2590
|
+
* The prompt text actually sent to the worker and whether the trailer got
|
|
2591
|
+
* appended. An instruction at the END of the user turn outweighs one buried
|
|
2592
|
+
* in the system prompt (measured 2026-09-20: the AG2 contract alone was
|
|
2593
|
+
* ignored by both Haiku and Sonnet). Never applied to an interactive launch
|
|
2594
|
+
* (no -p) or when the caller brought its own --append-system-prompt — the
|
|
2595
|
+
* trailer's "format in system prompt" would then point at nothing.
|
|
2596
|
+
*/
|
|
2597
|
+
function headlessPromptText(prompt, headless, callerSystemPrompt, format) {
|
|
2598
|
+
const applied = headless && prompt !== null && !callerSystemPrompt;
|
|
2599
|
+
return {
|
|
2600
|
+
text: applied ? prompt + promptTrailer(format) : prompt,
|
|
2601
|
+
applied
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Whether an invalid final report earns the one bounded re-ask: only when a
|
|
2606
|
+
* real validator caught the problem (a missing/broken aibroker must never
|
|
2607
|
+
* block the run further), the caller did not opt out, and there is a session
|
|
2608
|
+
* to resume into. Cap of exactly one is enforced by the caller never calling
|
|
2609
|
+
* this twice for the same run.
|
|
2610
|
+
*/
|
|
2611
|
+
function shouldRetryReport(validation, noReportRetry, hasSession) {
|
|
2612
|
+
return !validation.ok && validation.validator === "aibroker" && !noReportRetry && hasSession;
|
|
2613
|
+
}
|
|
2315
2614
|
/** Context window announced by the init event, when the endpoint sends one. */
|
|
2316
2615
|
function initContextWindow(e) {
|
|
2317
2616
|
if (typeof e.context_window === "number" && e.context_window > 0) return e.context_window;
|
|
@@ -2329,7 +2628,7 @@ async function runWorker(opts) {
|
|
|
2329
2628
|
const logDir = workersLogDir(config);
|
|
2330
2629
|
mkdirSync(logDir, { recursive: true });
|
|
2331
2630
|
if (opts.className === "plan" && !opts._planner) {
|
|
2332
|
-
const { runPlanner } = await import("./planner-
|
|
2631
|
+
const { runPlanner } = await import("./planner-BlKGgpWI.mjs");
|
|
2333
2632
|
return runPlanner(opts);
|
|
2334
2633
|
}
|
|
2335
2634
|
const parent = launchParent(opts.parent);
|
|
@@ -2342,6 +2641,7 @@ async function runWorker(opts) {
|
|
|
2342
2641
|
const parsed = parseRunnerArgs(opts.claudeArgs);
|
|
2343
2642
|
const label = opts.label ?? shortText(parsed.prompt ?? UNLABELED, 70);
|
|
2344
2643
|
const model = resolveRunModel(target, opts.className, opts.modelFlag);
|
|
2644
|
+
const reportFormat = resolveReportFormat(opts.reportFormatFlag);
|
|
2345
2645
|
try {
|
|
2346
2646
|
if (target.provider.engine === "codex") return await executeCodexRun({
|
|
2347
2647
|
config,
|
|
@@ -2360,7 +2660,8 @@ async function runWorker(opts) {
|
|
|
2360
2660
|
onWorkerStart: opts.onWorkerStart,
|
|
2361
2661
|
id: opts.id,
|
|
2362
2662
|
worktreeFlag: opts.worktreeFlag,
|
|
2363
|
-
className: opts.className
|
|
2663
|
+
className: opts.className,
|
|
2664
|
+
reportFormat
|
|
2364
2665
|
});
|
|
2365
2666
|
return await executeRun({
|
|
2366
2667
|
config,
|
|
@@ -2382,7 +2683,9 @@ async function runWorker(opts) {
|
|
|
2382
2683
|
worktreeFlag: opts.worktreeFlag,
|
|
2383
2684
|
className: opts.className,
|
|
2384
2685
|
reroutes: opts._reroutes ?? 0,
|
|
2385
|
-
printCmd: opts.printCmd
|
|
2686
|
+
printCmd: opts.printCmd,
|
|
2687
|
+
reportFormat,
|
|
2688
|
+
noReportRetry: opts.noReportRetry ?? false
|
|
2386
2689
|
});
|
|
2387
2690
|
} catch (e) {
|
|
2388
2691
|
if (e instanceof Error && e.message.startsWith("key file")) throw new Error(`provider "${target.providerName}": ${e.message}\nPut the token in that file (chmod 600) or point keyFile elsewhere.`);
|
|
@@ -2430,6 +2733,39 @@ function headlessToolsFlag(allowedTools) {
|
|
|
2430
2733
|
return names.length ? ["--tools", names.join(",")] : [];
|
|
2431
2734
|
}
|
|
2432
2735
|
/**
|
|
2736
|
+
* A `--tools` value with no ToolSearch silently disables MCP tool deferral
|
|
2737
|
+
* (measured 2026-09-20: a run with `--tools "Bash,Read,Grep,Glob,Agent"` paid
|
|
2738
|
+
* 65,300 first-turn tokens with 5 MCP servers, 230,448 with 16, versus
|
|
2739
|
+
* 24,889 / 37,133 with ToolSearch present). Applied to the fully-assembled
|
|
2740
|
+
* argv so it catches a `--tools` from any source — the computed
|
|
2741
|
+
* `headlessToolsFlag`/project-pin flag, or one the caller put directly on
|
|
2742
|
+
* the command line.
|
|
2743
|
+
*/
|
|
2744
|
+
function ensureToolSearch(cmd) {
|
|
2745
|
+
const flagIdx = cmd.findIndex((a) => a === "--tools");
|
|
2746
|
+
if (flagIdx >= 0 && flagIdx + 1 < cmd.length) {
|
|
2747
|
+
const value = cmd[flagIdx + 1];
|
|
2748
|
+
if (value && !value.split(",").includes("ToolSearch")) {
|
|
2749
|
+
const out = [...cmd];
|
|
2750
|
+
out[flagIdx + 1] = `${value},ToolSearch`;
|
|
2751
|
+
process.stderr.write("pai: added ToolSearch to --tools (keeps MCP tool deferral on)\n");
|
|
2752
|
+
return out;
|
|
2753
|
+
}
|
|
2754
|
+
return cmd;
|
|
2755
|
+
}
|
|
2756
|
+
const eqIdx = cmd.findIndex((a) => a.startsWith("--tools="));
|
|
2757
|
+
if (eqIdx >= 0) {
|
|
2758
|
+
const value = cmd[eqIdx].slice(8);
|
|
2759
|
+
if (value && !value.split(",").includes("ToolSearch")) {
|
|
2760
|
+
const out = [...cmd];
|
|
2761
|
+
out[eqIdx] = `--tools=${value},ToolSearch`;
|
|
2762
|
+
process.stderr.write("pai: added ToolSearch to --tools (keeps MCP tool deferral on)\n");
|
|
2763
|
+
return out;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
return cmd;
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2433
2769
|
* The claude flag that turns on the browser bridge, when the run asked for it.
|
|
2434
2770
|
*
|
|
2435
2771
|
* The bridge is not an MCP server — it rides the Chrome native-host channel,
|
|
@@ -2520,14 +2856,16 @@ async function executeRun(a) {
|
|
|
2520
2856
|
}
|
|
2521
2857
|
const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
|
|
2522
2858
|
const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
|
|
2523
|
-
const
|
|
2859
|
+
const { text: promptText, applied: trailerApplied } = headlessPromptText(parsed.prompt, headless, parsed.callerSystemPrompt, a.reportFormat);
|
|
2860
|
+
let cmd = ["claude"];
|
|
2524
2861
|
cmd.push(...modelFlagArgs(headless, model, Boolean(parsed.callerModel)));
|
|
2525
2862
|
cmd.push(...chromeArgs, ...mcpArgs, ...toolArgs, ...toolsFlag, ...restArgs);
|
|
2526
2863
|
if (headless) {
|
|
2527
2864
|
cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
|
|
2528
|
-
if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt",
|
|
2865
|
+
if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", workerContractPrompt(a.className, a.reportFormat));
|
|
2529
2866
|
if (worktree) cmd.push("--append-system-prompt", worktreeSystemPrompt(wid, worktree.branch, worktree.dir));
|
|
2530
2867
|
}
|
|
2868
|
+
cmd = ensureToolSearch(cmd);
|
|
2531
2869
|
if (a.printCmd) {
|
|
2532
2870
|
console.log(JSON.stringify({
|
|
2533
2871
|
cmd,
|
|
@@ -2553,6 +2891,8 @@ async function executeRun(a) {
|
|
|
2553
2891
|
secs: null,
|
|
2554
2892
|
origin: headless ? "spawn" : "chat",
|
|
2555
2893
|
outputFormat: parsed.outputFormat,
|
|
2894
|
+
...headless ? { reportFormat: a.reportFormat } : {},
|
|
2895
|
+
...trailerApplied ? { promptTrailer: true } : {},
|
|
2556
2896
|
...session ? { session } : {},
|
|
2557
2897
|
...spawnerSession ? { spawnerSession } : {},
|
|
2558
2898
|
...a.parent ? {
|
|
@@ -2663,8 +3003,8 @@ async function executeRun(a) {
|
|
|
2663
3003
|
resultReport: null
|
|
2664
3004
|
};
|
|
2665
3005
|
if (headless) {
|
|
2666
|
-
if (
|
|
2667
|
-
proc.stdin.write(stdinUserMessage(
|
|
3006
|
+
if (promptText !== null) try {
|
|
3007
|
+
proc.stdin.write(stdinUserMessage(promptText) + "\n");
|
|
2668
3008
|
} catch {}
|
|
2669
3009
|
eventsFd = openSync(eventsPath(logDir, wid), "a");
|
|
2670
3010
|
createInterface({ input: proc.stdout }).on("line", (line) => {
|
|
@@ -2723,6 +3063,40 @@ async function executeRun(a) {
|
|
|
2723
3063
|
status.secs = secs;
|
|
2724
3064
|
if (resultEvent) status.last = shortText(resultEvent.result ?? "", 90);
|
|
2725
3065
|
if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
|
|
3066
|
+
if (headless && a.reportFormat === "ag2" && resultEvent?.result) {
|
|
3067
|
+
const v = validateAg2(resultEvent.result);
|
|
3068
|
+
status.reportValid = v.ok;
|
|
3069
|
+
status.reportErrors = v.errors;
|
|
3070
|
+
if (shouldRetryReport(v, a.noReportRetry ?? false, Boolean(status.claudeSession))) {
|
|
3071
|
+
status.reportRetried = true;
|
|
3072
|
+
saveStatus(logDir, status);
|
|
3073
|
+
appendLedger(ledger, "WORKER-REPORT-RETRY", {
|
|
3074
|
+
id: wid,
|
|
3075
|
+
reason: v.errors.join("; ") || "invalid AG2 message"
|
|
3076
|
+
});
|
|
3077
|
+
try {
|
|
3078
|
+
const reaskText = await reaskAg2Report({
|
|
3079
|
+
env,
|
|
3080
|
+
cwd: worktree?.dir ?? cwd,
|
|
3081
|
+
model,
|
|
3082
|
+
callerPinnedModel: Boolean(parsed.callerModel),
|
|
3083
|
+
chromeArgs,
|
|
3084
|
+
mcpArgs,
|
|
3085
|
+
toolArgs,
|
|
3086
|
+
toolsFlag,
|
|
3087
|
+
sessionId: status.claudeSession
|
|
3088
|
+
});
|
|
3089
|
+
const v2 = validateAg2(reaskText);
|
|
3090
|
+
status.reportValid = v2.ok;
|
|
3091
|
+
status.reportErrors = v2.errors;
|
|
3092
|
+
if (v2.ok) {
|
|
3093
|
+
resultEvent.result = reaskText;
|
|
3094
|
+
ctx.resultReport = parseWorkerReport(reaskText);
|
|
3095
|
+
if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
|
|
3096
|
+
}
|
|
3097
|
+
} catch {}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
2726
3100
|
saveStatus(logDir, status);
|
|
2727
3101
|
appendLedger(ledger, "WORKER-END", {
|
|
2728
3102
|
id: wid,
|
|
@@ -2791,6 +3165,44 @@ function worktreeExtras(s) {
|
|
|
2791
3165
|
commits: s.commits ?? 0
|
|
2792
3166
|
} : void 0;
|
|
2793
3167
|
}
|
|
3168
|
+
/**
|
|
3169
|
+
* The one bounded re-ask, cap exactly one: `claude --resume <session>` with
|
|
3170
|
+
* AG2_REASK_TEXT, single-shot `--output-format json` (one turn, no streaming
|
|
3171
|
+
* plumbing needed). Same env/model/mcp/tool flags as the original launch —
|
|
3172
|
+
* only --resume and the fixed re-ask prompt are new — so the resumed turn
|
|
3173
|
+
* sees the same grants the worker ran with.
|
|
3174
|
+
*/
|
|
3175
|
+
async function reaskAg2Report(a) {
|
|
3176
|
+
let cmd = ["claude"];
|
|
3177
|
+
cmd.push(...modelFlagArgs(true, a.model, a.callerPinnedModel));
|
|
3178
|
+
cmd.push(...a.chromeArgs, ...a.mcpArgs, ...a.toolArgs, ...a.toolsFlag);
|
|
3179
|
+
cmd.push("--resume", a.sessionId, "-p", AG2_REASK_TEXT, "--output-format", "json");
|
|
3180
|
+
cmd = ensureToolSearch(cmd);
|
|
3181
|
+
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
3182
|
+
env: a.env,
|
|
3183
|
+
cwd: a.cwd,
|
|
3184
|
+
stdio: [
|
|
3185
|
+
"ignore",
|
|
3186
|
+
"pipe",
|
|
3187
|
+
"inherit"
|
|
3188
|
+
]
|
|
3189
|
+
});
|
|
3190
|
+
const timer = setTimeout(() => {
|
|
3191
|
+
try {
|
|
3192
|
+
proc.kill("SIGKILL");
|
|
3193
|
+
} catch {}
|
|
3194
|
+
}, a.timeoutMs ?? 6e4);
|
|
3195
|
+
let out = "";
|
|
3196
|
+
proc.stdout.on("data", (chunk) => {
|
|
3197
|
+
out += chunk.toString("utf8");
|
|
3198
|
+
});
|
|
3199
|
+
await new Promise((resolve) => {
|
|
3200
|
+
proc.on("error", () => resolve());
|
|
3201
|
+
proc.on("close", () => resolve());
|
|
3202
|
+
});
|
|
3203
|
+
clearTimeout(timer);
|
|
3204
|
+
return resultFromOutput(out);
|
|
3205
|
+
}
|
|
2794
3206
|
async function executeCodexRun(a) {
|
|
2795
3207
|
const { config, logDir, target, model, label, parsed, noPane } = a;
|
|
2796
3208
|
if (!parsed.headless || parsed.prompt === null) throw new Error(`provider "${target.providerName}" (engine codex) supports headless runs only: pass the task with -p '<prompt>'`);
|
|
@@ -2835,6 +3247,7 @@ async function executeCodexRun(a) {
|
|
|
2835
3247
|
secs: null,
|
|
2836
3248
|
origin: "spawn",
|
|
2837
3249
|
outputFormat: parsed.outputFormat,
|
|
3250
|
+
reportFormat: "json",
|
|
2838
3251
|
...session ? { session } : {},
|
|
2839
3252
|
...spawnerSession ? { spawnerSession } : {},
|
|
2840
3253
|
...target.provider.contextWindow ? { contextWindow: target.provider.contextWindow } : {},
|
|
@@ -3113,5 +3526,5 @@ function resultFromOutput(out) {
|
|
|
3113
3526
|
}
|
|
3114
3527
|
|
|
3115
3528
|
//#endregion
|
|
3116
|
-
export {
|
|
3117
|
-
//# sourceMappingURL=run-
|
|
3529
|
+
export { loadStatuses as A, expandMcpNames as B, UNLABELED as C, contextPercent as D, contextLabel as E, waitForTerminalStatus as F, longInlinePromptHint as I, parseRunnerArgs as L, nowStamp as M, saveStatus as N, isChatPane as O, setWorkerLabel as P, shortText as R, ledgerSummary as S, alive as T, itermForClaudeSession as _, parseWorkerReport as a, workerInScope as b, mergeWorker as c, sayToWorker as d, workerDepth as f, currentTabKey as g, openPaneForWorker as h, usageContextTokens as i, newWorkerId as j, loadStatus as k, handoffFromInside as l, openFollowPane as m, runWorker as n, renderReport as o, checkPaneForWorker as p, testProvider as r, discardWorker as s, printResult as t, readInbox as u, resolveSession as v, ageOf as w, appendLedger as x, sessionTag as y, describeMcp as z };
|
|
3530
|
+
//# sourceMappingURL=run-ByBKowN5.mjs.map
|