wayang-ai 0.6.2 → 0.7.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/cli.js +2202 -703
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -192,9 +192,9 @@ var RpcClient = class {
|
|
|
192
192
|
return Promise.reject(new Error("RPC transport closed: cannot send request"));
|
|
193
193
|
}
|
|
194
194
|
const id = this.nextId++;
|
|
195
|
-
return new Promise((
|
|
195
|
+
return new Promise((resolve12, reject) => {
|
|
196
196
|
this.pending.set(id, {
|
|
197
|
-
resolve: (v) =>
|
|
197
|
+
resolve: (v) => resolve12(v),
|
|
198
198
|
reject
|
|
199
199
|
});
|
|
200
200
|
this.transport.send({ id, method, params });
|
|
@@ -398,8 +398,8 @@ async function connectRemoteServer(options) {
|
|
|
398
398
|
child.stderr?.on("data", (chunk) => {
|
|
399
399
|
stderrTail = (stderrTail + chunk).slice(-4e3);
|
|
400
400
|
});
|
|
401
|
-
const exited = new Promise((
|
|
402
|
-
child.on("exit", (code) =>
|
|
401
|
+
const exited = new Promise((resolve12) => {
|
|
402
|
+
child.on("exit", (code) => resolve12(code));
|
|
403
403
|
});
|
|
404
404
|
const transport = createStreamTransport(child.stdout, child.stdin);
|
|
405
405
|
const rpc = new RpcClient(transport);
|
|
@@ -418,10 +418,10 @@ async function connectRemoteServer(options) {
|
|
|
418
418
|
}
|
|
419
419
|
const exitOrKill = Promise.race([
|
|
420
420
|
exited,
|
|
421
|
-
new Promise((
|
|
421
|
+
new Promise((resolve12) => {
|
|
422
422
|
setTimeout(() => {
|
|
423
423
|
child.kill("SIGKILL");
|
|
424
|
-
|
|
424
|
+
resolve12(null);
|
|
425
425
|
}, SHUTDOWN_GRACE_MS);
|
|
426
426
|
})
|
|
427
427
|
]);
|
|
@@ -439,7 +439,7 @@ ${stderrTail}`);
|
|
|
439
439
|
return { remote, stop };
|
|
440
440
|
}
|
|
441
441
|
function waitForReady(child, rpc, exited, getStderrTail, timeoutMs) {
|
|
442
|
-
return new Promise((
|
|
442
|
+
return new Promise((resolve12, reject) => {
|
|
443
443
|
let settled = false;
|
|
444
444
|
const finish = (err) => {
|
|
445
445
|
if (settled) return;
|
|
@@ -447,7 +447,7 @@ function waitForReady(child, rpc, exited, getStderrTail, timeoutMs) {
|
|
|
447
447
|
clearTimeout(timer);
|
|
448
448
|
offReady();
|
|
449
449
|
if (err) reject(err);
|
|
450
|
-
else
|
|
450
|
+
else resolve12();
|
|
451
451
|
};
|
|
452
452
|
const timer = setTimeout(() => {
|
|
453
453
|
child.kill("SIGKILL");
|
|
@@ -604,7 +604,7 @@ import React6 from "react";
|
|
|
604
604
|
import { render } from "ink";
|
|
605
605
|
|
|
606
606
|
// src/tui/app.tsx
|
|
607
|
-
import { useRef as
|
|
607
|
+
import { useRef as useRef3 } from "react";
|
|
608
608
|
|
|
609
609
|
// src/tui/hooks/use-memoized-fn.ts
|
|
610
610
|
import { useRef, useCallback } from "react";
|
|
@@ -1104,14 +1104,17 @@ function summarizeWorkerMessage(argsJson, width, result, maxLines = 3) {
|
|
|
1104
1104
|
const message = args ? str(args.message) ?? "" : argsJson;
|
|
1105
1105
|
const { lines, hidden } = previewLines(message, maxLines, width);
|
|
1106
1106
|
const failed = result !== void 0 && (result.isError === true || /^\[ERROR\]/.test(result.text));
|
|
1107
|
+
let delivery;
|
|
1108
|
+
if (result !== void 0 && !failed) {
|
|
1109
|
+
delivery = /\bqueued\b/i.test(result.text) ? "queued" : /\bdelivered\b/i.test(result.text) ? "delivered" : void 0;
|
|
1110
|
+
}
|
|
1107
1111
|
return {
|
|
1108
1112
|
...workerId && { workerId },
|
|
1109
1113
|
lines,
|
|
1110
1114
|
hiddenLines: hidden,
|
|
1111
1115
|
outcome: result === void 0 ? "pending" : failed ? "failed" : "ok",
|
|
1112
1116
|
failure: failed ? failureText(result.text, width) : "",
|
|
1113
|
-
...
|
|
1114
|
-
...result !== void 0 && !failed && /\bdelivered\b/.test(result.text) && { delivery: "delivered" }
|
|
1117
|
+
...delivery && { delivery }
|
|
1115
1118
|
};
|
|
1116
1119
|
}
|
|
1117
1120
|
var TASK_ID_RE = /^Task (\S+) (?:added|assigned)/;
|
|
@@ -1369,6 +1372,7 @@ function WorkerMessageRow({ section: section2, result, lookups }) {
|
|
|
1369
1372
|
const worker = summary.workerId ? lookups.workers.get(summary.workerId) : void 0;
|
|
1370
1373
|
const target = formatWorkerTag(worker ?? { workerId: summary.workerId }) || "(\u672A\u77E5 worker)";
|
|
1371
1374
|
const iconColor = summary.outcome === "failed" ? theme.action.failColor : theme.action.color;
|
|
1375
|
+
const delivered = summary.delivery === "queued" ? "\u5DF2\u6392\u961F" : summary.delivery === "delivered" ? "\u5DF2\u9001\u8FBE" : "\u5DF2\u53D1\u9001";
|
|
1372
1376
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1373
1377
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1374
1378
|
/* @__PURE__ */ jsxs2(Text2, { color: iconColor, bold: true, children: [
|
|
@@ -1379,7 +1383,7 @@ function WorkerMessageRow({ section: section2, result, lookups }) {
|
|
|
1379
1383
|
" ",
|
|
1380
1384
|
target
|
|
1381
1385
|
] }),
|
|
1382
|
-
/* @__PURE__ */ jsx3(OutcomeMark, { outcome: summary.outcome, ok:
|
|
1386
|
+
/* @__PURE__ */ jsx3(OutcomeMark, { outcome: summary.outcome, ok: delivered, failed: "\u672A\u9001\u8FBE" })
|
|
1383
1387
|
] }),
|
|
1384
1388
|
/* @__PURE__ */ jsx3(PreviewLines, { lines: summary.lines, hidden: summary.hiddenLines }),
|
|
1385
1389
|
summary.failure && /* @__PURE__ */ jsx3(SecondaryLine, { color: theme.action.failColor, children: summary.failure })
|
|
@@ -1651,7 +1655,7 @@ var ChatArea = React2.memo(function ChatArea2({ items, busy, streamingItems, con
|
|
|
1651
1655
|
});
|
|
1652
1656
|
|
|
1653
1657
|
// src/tui/components/input-area.tsx
|
|
1654
|
-
import { useState as useState2, useMemo as useMemo2 } from "react";
|
|
1658
|
+
import { useState as useState2, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
1655
1659
|
|
|
1656
1660
|
// src/tui/utils/format-tokens.ts
|
|
1657
1661
|
function formatTokenCount(tokens) {
|
|
@@ -1798,21 +1802,302 @@ function useSlashCommands(supervisor) {
|
|
|
1798
1802
|
|
|
1799
1803
|
// src/tui/components/input-area.tsx
|
|
1800
1804
|
import { Box as Box6, Text as Text7, useInput } from "ink";
|
|
1801
|
-
|
|
1802
|
-
|
|
1805
|
+
|
|
1806
|
+
// src/tui/utils/input-display.ts
|
|
1807
|
+
function normalizeInputText(s) {
|
|
1808
|
+
return s.replace(/\r\n?/g, "\n");
|
|
1809
|
+
}
|
|
1810
|
+
function isHighSurrogate(codeUnit) {
|
|
1811
|
+
return codeUnit >= 55296 && codeUnit <= 56319;
|
|
1812
|
+
}
|
|
1813
|
+
function isLowSurrogate(codeUnit) {
|
|
1814
|
+
return codeUnit >= 56320 && codeUnit <= 57343;
|
|
1815
|
+
}
|
|
1816
|
+
function prevCodePointLength(s, index) {
|
|
1817
|
+
if (index <= 0) return 0;
|
|
1818
|
+
if (index >= 2 && isHighSurrogate(s.charCodeAt(index - 2)) && isLowSurrogate(s.charCodeAt(index - 1))) {
|
|
1819
|
+
return 2;
|
|
1820
|
+
}
|
|
1821
|
+
return 1;
|
|
1822
|
+
}
|
|
1823
|
+
function nextCodePointLength(s, index) {
|
|
1824
|
+
if (index < 0 || index >= s.length) return 0;
|
|
1825
|
+
if (index + 1 < s.length && isHighSurrogate(s.charCodeAt(index)) && isLowSurrogate(s.charCodeAt(index + 1))) {
|
|
1826
|
+
return 2;
|
|
1827
|
+
}
|
|
1828
|
+
return 1;
|
|
1829
|
+
}
|
|
1830
|
+
function wrapByDisplayWidth(line, maxWidth) {
|
|
1831
|
+
const limit = Math.max(1, Math.floor(maxWidth));
|
|
1832
|
+
if (stringWidth(line) <= limit) return [line];
|
|
1833
|
+
const segments = [];
|
|
1834
|
+
let current = "";
|
|
1835
|
+
let width = 0;
|
|
1836
|
+
for (const ch of line) {
|
|
1837
|
+
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
1838
|
+
if (current !== "" && width + cw > limit) {
|
|
1839
|
+
segments.push(current);
|
|
1840
|
+
current = ch;
|
|
1841
|
+
width = cw;
|
|
1842
|
+
} else {
|
|
1843
|
+
current += ch;
|
|
1844
|
+
width += cw;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (current !== "") segments.push(current);
|
|
1848
|
+
return segments.length > 0 ? segments : [""];
|
|
1849
|
+
}
|
|
1850
|
+
function splitLineBySelection(line, selection) {
|
|
1851
|
+
const lo = line.offset;
|
|
1852
|
+
const hi = line.offset + line.text.length;
|
|
1853
|
+
const s = Math.max(lo, Math.min(hi, selection.start));
|
|
1854
|
+
const e = Math.max(lo, Math.min(hi, selection.end));
|
|
1855
|
+
if (s >= e) return [{ text: line.text, selected: false }];
|
|
1856
|
+
const out = [];
|
|
1857
|
+
if (s > lo) out.push({ text: line.text.slice(0, s - lo), selected: false });
|
|
1858
|
+
out.push({ text: line.text.slice(s - lo, e - lo), selected: true });
|
|
1859
|
+
if (e < hi) out.push({ text: line.text.slice(e - lo), selected: false });
|
|
1860
|
+
return out.filter((seg) => seg.text.length > 0);
|
|
1861
|
+
}
|
|
1862
|
+
function layoutInput(value, cursor, maxWidth) {
|
|
1863
|
+
const c = Math.max(0, Math.min(value.length, cursor));
|
|
1864
|
+
const logicalLines = value.split("\n");
|
|
1865
|
+
const lines = [];
|
|
1866
|
+
let cursorLine = 0;
|
|
1867
|
+
let cursorCol = 0;
|
|
1868
|
+
let offset = 0;
|
|
1869
|
+
for (const lineText of logicalLines) {
|
|
1870
|
+
const col = c - offset;
|
|
1871
|
+
const ownsCursor = col >= 0 && col <= lineText.length;
|
|
1872
|
+
const segments = wrapByDisplayWidth(lineText, maxWidth);
|
|
1873
|
+
for (const segment of segments) {
|
|
1874
|
+
lines.push({ text: segment, offset });
|
|
1875
|
+
offset += segment.length;
|
|
1876
|
+
}
|
|
1877
|
+
if (ownsCursor) {
|
|
1878
|
+
let base = 0;
|
|
1879
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1880
|
+
const segment = segments[i];
|
|
1881
|
+
const segEnd = base + segment.length;
|
|
1882
|
+
const isLastSegment = i === segments.length - 1;
|
|
1883
|
+
if (col < segEnd || col === segEnd && isLastSegment) {
|
|
1884
|
+
cursorLine = lines.length - segments.length + i;
|
|
1885
|
+
cursorCol = col - base;
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
base = segEnd;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
offset += 1;
|
|
1892
|
+
}
|
|
1893
|
+
return { lines, cursorLine, cursorCol };
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/tui/utils/input-editing.ts
|
|
1897
|
+
function clampIndex(value, index) {
|
|
1898
|
+
return Math.max(0, Math.min(value.length, index));
|
|
1899
|
+
}
|
|
1900
|
+
function normalizeCursorIndex(value, index) {
|
|
1901
|
+
const i = clampIndex(value, index);
|
|
1902
|
+
if (i > 0 && i < value.length && (value.codePointAt(i - 1) ?? 0) > 65535) {
|
|
1903
|
+
return i + 1;
|
|
1904
|
+
}
|
|
1905
|
+
return i;
|
|
1906
|
+
}
|
|
1907
|
+
var CJK_RANGES = [
|
|
1908
|
+
// Hiragana + Katakana, incl. the prolonged sound mark ー (the middle dot ・
|
|
1909
|
+
// is punctuation and filtered out before this check).
|
|
1910
|
+
[12352, 12543],
|
|
1911
|
+
// CJK Extension A + Unified Ideographs.
|
|
1912
|
+
[13312, 40959],
|
|
1913
|
+
// CJK compatibility ideographs.
|
|
1914
|
+
[63744, 64255],
|
|
1915
|
+
// CJK Extension B and beyond.
|
|
1916
|
+
[131072, 262143]
|
|
1917
|
+
];
|
|
1918
|
+
var WHITESPACE = /\s/u;
|
|
1919
|
+
var PUNCTUATION = new RegExp("\\p{P}", "u");
|
|
1920
|
+
function classifyCodePoint(cp) {
|
|
1921
|
+
if (cp === 95) return "word";
|
|
1922
|
+
const ch = String.fromCodePoint(cp);
|
|
1923
|
+
if (WHITESPACE.test(ch)) return "separator";
|
|
1924
|
+
if (PUNCTUATION.test(ch)) return "separator";
|
|
1925
|
+
for (const [lo, hi] of CJK_RANGES) {
|
|
1926
|
+
if (cp >= lo && cp <= hi) return "cjk";
|
|
1927
|
+
}
|
|
1928
|
+
return "word";
|
|
1929
|
+
}
|
|
1930
|
+
function classBefore(value, index) {
|
|
1931
|
+
if (index <= 0) return "separator";
|
|
1932
|
+
const len = prevCodePointLength(value, index);
|
|
1933
|
+
return classifyCodePoint(value.codePointAt(index - len) ?? 0);
|
|
1934
|
+
}
|
|
1935
|
+
function classAt(value, index) {
|
|
1936
|
+
return classifyCodePoint(value.codePointAt(index) ?? 0);
|
|
1937
|
+
}
|
|
1938
|
+
function wordLeft(value, index) {
|
|
1939
|
+
let i = normalizeCursorIndex(value, index);
|
|
1940
|
+
while (i > 0 && classBefore(value, i) === "separator") {
|
|
1941
|
+
i -= prevCodePointLength(value, i);
|
|
1942
|
+
}
|
|
1943
|
+
if (i === 0) return 0;
|
|
1944
|
+
if (classBefore(value, i) === "cjk") {
|
|
1945
|
+
return i - prevCodePointLength(value, i);
|
|
1946
|
+
}
|
|
1947
|
+
while (i > 0 && classBefore(value, i) === "word") {
|
|
1948
|
+
i -= prevCodePointLength(value, i);
|
|
1949
|
+
}
|
|
1950
|
+
return i;
|
|
1951
|
+
}
|
|
1952
|
+
function wordRight(value, index) {
|
|
1953
|
+
const len = value.length;
|
|
1954
|
+
let i = normalizeCursorIndex(value, index);
|
|
1955
|
+
while (i < len && classAt(value, i) === "separator") {
|
|
1956
|
+
i += nextCodePointLength(value, i);
|
|
1957
|
+
}
|
|
1958
|
+
if (i === len) return len;
|
|
1959
|
+
if (classAt(value, i) === "cjk") {
|
|
1960
|
+
return i + nextCodePointLength(value, i);
|
|
1961
|
+
}
|
|
1962
|
+
while (i < len && classAt(value, i) === "word") {
|
|
1963
|
+
i += nextCodePointLength(value, i);
|
|
1964
|
+
}
|
|
1965
|
+
return i;
|
|
1966
|
+
}
|
|
1967
|
+
function lineStart(value, index) {
|
|
1968
|
+
let i = normalizeCursorIndex(value, index);
|
|
1969
|
+
while (i > 0 && value.charCodeAt(i - 1) !== 10) i -= 1;
|
|
1970
|
+
return i;
|
|
1971
|
+
}
|
|
1972
|
+
function lineEnd(value, index) {
|
|
1973
|
+
let i = normalizeCursorIndex(value, index);
|
|
1974
|
+
while (i < value.length && value.charCodeAt(i) !== 10) i += 1;
|
|
1975
|
+
return i;
|
|
1976
|
+
}
|
|
1977
|
+
function killToLineEnd(value, index) {
|
|
1978
|
+
const cursor = normalizeCursorIndex(value, index);
|
|
1979
|
+
const end = lineEnd(value, cursor);
|
|
1980
|
+
return { value: value.slice(0, cursor) + value.slice(end), cursor };
|
|
1981
|
+
}
|
|
1982
|
+
function killToLineStart(value, index) {
|
|
1983
|
+
const cursor = normalizeCursorIndex(value, index);
|
|
1984
|
+
const start = lineStart(value, cursor);
|
|
1985
|
+
return { value: value.slice(0, start) + value.slice(cursor), cursor: start };
|
|
1986
|
+
}
|
|
1987
|
+
function killWordBefore(value, index) {
|
|
1988
|
+
const cursor = normalizeCursorIndex(value, index);
|
|
1989
|
+
const start = wordLeft(value, cursor);
|
|
1990
|
+
return { value: value.slice(0, start) + value.slice(cursor), cursor: start };
|
|
1991
|
+
}
|
|
1992
|
+
function normalizeSelection(value, anchor, cursor) {
|
|
1993
|
+
const c = normalizeCursorIndex(value, cursor);
|
|
1994
|
+
const a = anchor === null ? c : normalizeCursorIndex(value, anchor);
|
|
1995
|
+
return a <= c ? { start: a, end: c } : { start: c, end: a };
|
|
1996
|
+
}
|
|
1997
|
+
function replaceRange(value, start, end, insert) {
|
|
1998
|
+
let a = clampIndex(value, start);
|
|
1999
|
+
let b = clampIndex(value, end);
|
|
2000
|
+
if (a > b) [a, b] = [b, a];
|
|
2001
|
+
return { value: value.slice(0, a) + insert + value.slice(b), cursor: a + insert.length };
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// src/tui/utils/input-history.ts
|
|
2005
|
+
var MERGE_WINDOW_MS = 500;
|
|
2006
|
+
var MAX_ENTRIES = 100;
|
|
2007
|
+
function initInputHistory(value, ts = Date.now()) {
|
|
2008
|
+
return {
|
|
2009
|
+
entries: [{ value, cursor: value.length, anchor: null, ts, kind: "snap" }],
|
|
2010
|
+
pointer: 0,
|
|
2011
|
+
rewound: false
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
function continuesRun(prev, edit) {
|
|
2015
|
+
if (prev.kind === "insert" && edit.kind === "insert") {
|
|
2016
|
+
const lenDiff = edit.value.length - prev.value.length;
|
|
2017
|
+
return (lenDiff === 1 || lenDiff === 2) && edit.cursor === prev.cursor + lenDiff;
|
|
2018
|
+
}
|
|
2019
|
+
if (prev.kind === "delete" && edit.kind === "delete") {
|
|
2020
|
+
const diff = prev.value.length - edit.value.length;
|
|
2021
|
+
return (diff === 1 || diff === 2) && edit.cursor === prev.cursor - diff;
|
|
2022
|
+
}
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
function commitEdit(history, edit) {
|
|
2026
|
+
const { entries, pointer, rewound } = history;
|
|
2027
|
+
const prev = entries[pointer];
|
|
2028
|
+
const atTail = pointer === entries.length - 1;
|
|
2029
|
+
if (prev && atTail && !rewound && edit.ts - prev.ts < MERGE_WINDOW_MS && continuesRun(prev, edit)) {
|
|
2030
|
+
const merged = entries.slice();
|
|
2031
|
+
merged[pointer] = { ...edit };
|
|
2032
|
+
return { entries: merged, pointer, rewound: false };
|
|
2033
|
+
}
|
|
2034
|
+
const next = [...entries.slice(0, pointer + 1), { ...edit }];
|
|
2035
|
+
if (next.length > MAX_ENTRIES) {
|
|
2036
|
+
const trimmed = next.slice(next.length - MAX_ENTRIES);
|
|
2037
|
+
return { entries: trimmed, pointer: trimmed.length - 1, rewound: false };
|
|
2038
|
+
}
|
|
2039
|
+
return { entries: next, pointer: next.length - 1, rewound: false };
|
|
2040
|
+
}
|
|
2041
|
+
function undoEdit(history) {
|
|
2042
|
+
if (history.pointer === 0) return null;
|
|
2043
|
+
return { entries: history.entries, pointer: history.pointer - 1, rewound: true };
|
|
2044
|
+
}
|
|
2045
|
+
function redoEdit(history) {
|
|
2046
|
+
if (history.pointer === history.entries.length - 1) return null;
|
|
2047
|
+
return { entries: history.entries, pointer: history.pointer + 1, rewound: true };
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// src/tui/components/input-area.tsx
|
|
2051
|
+
import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2052
|
+
var PREFIX2 = `${theme.user.prefix} `;
|
|
2053
|
+
var PREFIX_WIDTH2 = stringWidth(PREFIX2);
|
|
2054
|
+
function InputArea({ onSubmit, onExit, onEscape, busy, width }) {
|
|
1803
2055
|
const [input, setInput] = useState2("");
|
|
1804
2056
|
const [cursor, setCursor] = useState2(0);
|
|
2057
|
+
const [anchor, setAnchor] = useState2(null);
|
|
1805
2058
|
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
2059
|
+
const historyRef = useRef2(initInputHistory(""));
|
|
1806
2060
|
const suggestions = useMemo2(() => {
|
|
1807
2061
|
if (!input.startsWith("/") || input.includes(" ")) return [];
|
|
1808
2062
|
const typed = input.slice(1).toLowerCase();
|
|
1809
2063
|
return SLASH_COMMANDS.filter((c) => c.name.startsWith(typed));
|
|
1810
2064
|
}, [input]);
|
|
1811
2065
|
const showSuggestions = suggestions.length > 0;
|
|
2066
|
+
const sel = useMemo2(
|
|
2067
|
+
() => normalizeSelection(input, anchor, cursor),
|
|
2068
|
+
[input, anchor, cursor]
|
|
2069
|
+
);
|
|
2070
|
+
const hasSelection = sel.start < sel.end;
|
|
2071
|
+
const visibleSelection = hasSelection && input.slice(sel.start, sel.end).replace(/\n/g, "").length > 0;
|
|
1812
2072
|
const clampedIndex = showSuggestions ? Math.min(selectedIndex, suggestions.length - 1) : 0;
|
|
1813
2073
|
const reset = useMemoizedFn(() => {
|
|
1814
2074
|
setInput("");
|
|
1815
2075
|
setCursor(0);
|
|
2076
|
+
setAnchor(null);
|
|
2077
|
+
setSelectedIndex(0);
|
|
2078
|
+
historyRef.current = initInputHistory("");
|
|
2079
|
+
});
|
|
2080
|
+
const applyEdit = useMemoizedFn((r, kind) => {
|
|
2081
|
+
setInput(r.value);
|
|
2082
|
+
setCursor(r.cursor);
|
|
2083
|
+
setAnchor(null);
|
|
2084
|
+
setSelectedIndex(0);
|
|
2085
|
+
if (r.value === input && r.cursor === cursor) return;
|
|
2086
|
+
historyRef.current = commitEdit(historyRef.current, {
|
|
2087
|
+
value: r.value,
|
|
2088
|
+
cursor: r.cursor,
|
|
2089
|
+
anchor: null,
|
|
2090
|
+
ts: Date.now(),
|
|
2091
|
+
kind
|
|
2092
|
+
});
|
|
2093
|
+
});
|
|
2094
|
+
const applySnapshot = useMemoizedFn((history) => {
|
|
2095
|
+
const entry = history.entries[history.pointer];
|
|
2096
|
+
if (!entry) return;
|
|
2097
|
+
historyRef.current = history;
|
|
2098
|
+
setInput(entry.value);
|
|
2099
|
+
setCursor(entry.cursor);
|
|
2100
|
+
setAnchor(entry.anchor);
|
|
1816
2101
|
setSelectedIndex(0);
|
|
1817
2102
|
});
|
|
1818
2103
|
const applySuggestion = useMemoizedFn((index) => {
|
|
@@ -1820,9 +2105,7 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1820
2105
|
if (!cmd) return;
|
|
1821
2106
|
const text = `/${cmd.name}`;
|
|
1822
2107
|
const filled = "args" in cmd ? `${text} ` : text;
|
|
1823
|
-
|
|
1824
|
-
setCursor(filled.length);
|
|
1825
|
-
setSelectedIndex(0);
|
|
2108
|
+
applyEdit(replaceRange(input, 0, input.length, filled), "replace");
|
|
1826
2109
|
});
|
|
1827
2110
|
useInput((ch, key) => {
|
|
1828
2111
|
if (key.return) {
|
|
@@ -1872,39 +2155,133 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1872
2155
|
return;
|
|
1873
2156
|
}
|
|
1874
2157
|
if (key.escape) {
|
|
2158
|
+
if (hasSelection) {
|
|
2159
|
+
setAnchor(null);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
1875
2162
|
if (showSuggestions) {
|
|
1876
|
-
|
|
1877
|
-
setCursor(0);
|
|
1878
|
-
setSelectedIndex(0);
|
|
2163
|
+
reset();
|
|
1879
2164
|
} else {
|
|
1880
2165
|
onEscape?.();
|
|
1881
2166
|
reset();
|
|
1882
2167
|
}
|
|
1883
2168
|
return;
|
|
1884
2169
|
}
|
|
2170
|
+
if (key.ctrl && ch === "a") {
|
|
2171
|
+
setAnchor(0);
|
|
2172
|
+
setCursor(input.length);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
if (key.ctrl && ch === "e") {
|
|
2176
|
+
setAnchor(null);
|
|
2177
|
+
setCursor(lineEnd(input, cursor));
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (key.ctrl && ch === "k") {
|
|
2181
|
+
if (hasSelection) {
|
|
2182
|
+
applyEdit(replaceRange(input, sel.start, sel.end, ""), "replace");
|
|
2183
|
+
} else {
|
|
2184
|
+
applyEdit(killToLineEnd(input, cursor), "kill");
|
|
2185
|
+
}
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
if (key.ctrl && ch === "u") {
|
|
2189
|
+
if (hasSelection) {
|
|
2190
|
+
applyEdit(replaceRange(input, sel.start, sel.end, ""), "replace");
|
|
2191
|
+
} else {
|
|
2192
|
+
applyEdit(killToLineStart(input, cursor), "kill");
|
|
2193
|
+
}
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (key.ctrl && ch === "w") {
|
|
2197
|
+
if (hasSelection) {
|
|
2198
|
+
applyEdit(replaceRange(input, sel.start, sel.end, ""), "replace");
|
|
2199
|
+
} else {
|
|
2200
|
+
applyEdit(killWordBefore(input, cursor), "kill");
|
|
2201
|
+
}
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const moveWordLeft = () => {
|
|
2205
|
+
if (key.shift) setAnchor((a) => a ?? cursor);
|
|
2206
|
+
else setAnchor(null);
|
|
2207
|
+
setCursor(wordLeft(input, cursor));
|
|
2208
|
+
};
|
|
2209
|
+
const moveWordRight = () => {
|
|
2210
|
+
if (key.shift) setAnchor((a) => a ?? cursor);
|
|
2211
|
+
else setAnchor(null);
|
|
2212
|
+
setCursor(wordRight(input, cursor));
|
|
2213
|
+
};
|
|
2214
|
+
if (key.ctrl && key.leftArrow) {
|
|
2215
|
+
moveWordLeft();
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (key.ctrl && key.rightArrow) {
|
|
2219
|
+
moveWordRight();
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (key.meta && !key.escape && ch === "b") {
|
|
2223
|
+
moveWordLeft();
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (key.meta && !key.escape && ch === "f") {
|
|
2227
|
+
moveWordRight();
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
if (key.ctrl && ch === "z") {
|
|
2231
|
+
const next = undoEdit(historyRef.current);
|
|
2232
|
+
if (next) applySnapshot(next);
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (key.ctrl && ch === "y") {
|
|
2236
|
+
const next = redoEdit(historyRef.current);
|
|
2237
|
+
if (next) applySnapshot(next);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (key.shift && key.leftArrow) {
|
|
2241
|
+
setAnchor((a) => a ?? cursor);
|
|
2242
|
+
setCursor((c) => Math.max(0, c - prevCodePointLength(input, c)));
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
if (key.shift && key.rightArrow) {
|
|
2246
|
+
setAnchor((a) => a ?? cursor);
|
|
2247
|
+
setCursor((c) => Math.min(input.length, c + nextCodePointLength(input, c)));
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
1885
2250
|
if (key.backspace || key.delete) {
|
|
2251
|
+
if (hasSelection) {
|
|
2252
|
+
applyEdit(replaceRange(input, sel.start, sel.end, ""), "delete");
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
1886
2255
|
if (cursor === 0) return;
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2256
|
+
const len = prevCodePointLength(input, cursor);
|
|
2257
|
+
applyEdit(
|
|
2258
|
+
{ value: input.slice(0, cursor - len) + input.slice(cursor), cursor: cursor - len },
|
|
2259
|
+
"delete"
|
|
2260
|
+
);
|
|
1890
2261
|
return;
|
|
1891
2262
|
}
|
|
1892
2263
|
if (key.leftArrow) {
|
|
1893
|
-
|
|
2264
|
+
setAnchor(null);
|
|
2265
|
+
setCursor((c) => Math.max(0, c - prevCodePointLength(input, c)));
|
|
1894
2266
|
return;
|
|
1895
2267
|
}
|
|
1896
2268
|
if (key.rightArrow) {
|
|
1897
|
-
|
|
2269
|
+
setAnchor(null);
|
|
2270
|
+
setCursor((c) => Math.min(input.length, c + nextCodePointLength(input, c)));
|
|
1898
2271
|
return;
|
|
1899
2272
|
}
|
|
1900
2273
|
if (ch && !key.ctrl && !key.meta) {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2274
|
+
const text = normalizeInputText(ch);
|
|
2275
|
+
if (text.length === 0) return;
|
|
2276
|
+
const r = replaceRange(input, sel.start, sel.end, text);
|
|
2277
|
+
applyEdit(r, [...text].length === 1 ? "insert" : "paste");
|
|
1904
2278
|
}
|
|
1905
2279
|
});
|
|
1906
|
-
const
|
|
1907
|
-
const
|
|
2280
|
+
const textBudget = Math.max(1, width - 2 - PREFIX_WIDTH2);
|
|
2281
|
+
const layout = useMemo2(
|
|
2282
|
+
() => layoutInput(input, cursor, textBudget),
|
|
2283
|
+
[input, cursor, textBudget]
|
|
2284
|
+
);
|
|
1908
2285
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1909
2286
|
showSuggestions && /* @__PURE__ */ jsx9(Box6, { flexDirection: "column", paddingX: 1, marginBottom: 0, children: suggestions.map((cmd, i) => {
|
|
1910
2287
|
const isSelected = i === clampedIndex;
|
|
@@ -1918,7 +2295,7 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1918
2295
|
] })
|
|
1919
2296
|
] }, cmd.name);
|
|
1920
2297
|
}) }),
|
|
1921
|
-
/* @__PURE__ */
|
|
2298
|
+
/* @__PURE__ */ jsx9(
|
|
1922
2299
|
Box6,
|
|
1923
2300
|
{
|
|
1924
2301
|
borderStyle: "single",
|
|
@@ -1926,16 +2303,26 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1926
2303
|
borderLeft: false,
|
|
1927
2304
|
borderRight: false,
|
|
1928
2305
|
paddingX: 1,
|
|
1929
|
-
children:
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
/* @__PURE__ */
|
|
1937
|
-
|
|
1938
|
-
|
|
2306
|
+
children: /* @__PURE__ */ jsx9(Box6, { flexDirection: "column", children: layout.lines.map((line, i) => {
|
|
2307
|
+
const isCursorLine = i === layout.cursorLine;
|
|
2308
|
+
const col = layout.cursorCol;
|
|
2309
|
+
const charLen = isCursorLine ? nextCodePointLength(line.text, col) : 0;
|
|
2310
|
+
const before = isCursorLine ? line.text.slice(0, col) : line.text;
|
|
2311
|
+
const cursorCell = isCursorLine && charLen > 0 ? line.text.slice(col, col + charLen) : " ";
|
|
2312
|
+
const after = isCursorLine ? line.text.slice(col + charLen) : "";
|
|
2313
|
+
return /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
2314
|
+
i === 0 ? /* @__PURE__ */ jsx9(Text7, { color: theme.user.prefixColor, bold: true, children: theme.user.prefix }) : /* @__PURE__ */ jsx9(Text7, { children: " ".repeat(PREFIX_WIDTH2) }),
|
|
2315
|
+
visibleSelection ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
2316
|
+
i === 0 && /* @__PURE__ */ jsx9(Text7, { children: " " }),
|
|
2317
|
+
splitLineBySelection(line, sel).map((seg, j) => /* @__PURE__ */ jsx9(Text7, { inverse: seg.selected, children: seg.text }, j))
|
|
2318
|
+
] }) : /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
2319
|
+
/* @__PURE__ */ jsx9(Text7, { children: i === 0 ? ` ${before}` : before }),
|
|
2320
|
+
isCursorLine && /* @__PURE__ */ jsx9(Text7, { color: "white", inverse: true, children: cursorCell }),
|
|
2321
|
+
/* @__PURE__ */ jsx9(Text7, { children: after })
|
|
2322
|
+
] }),
|
|
2323
|
+
isCursorLine && busy && /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: " (thinking...)" })
|
|
2324
|
+
] }, i);
|
|
2325
|
+
}) })
|
|
1939
2326
|
}
|
|
1940
2327
|
)
|
|
1941
2328
|
] });
|
|
@@ -1944,13 +2331,59 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1944
2331
|
// src/tui/components/inquiry-prompt.tsx
|
|
1945
2332
|
import React4, { useState as useState3 } from "react";
|
|
1946
2333
|
import { Box as Box7, Text as Text8, useInput as useInput2 } from "ink";
|
|
2334
|
+
|
|
2335
|
+
// src/types/config.ts
|
|
2336
|
+
function validateConfig(config) {
|
|
2337
|
+
for (const [name, provider] of Object.entries(config.providers)) {
|
|
2338
|
+
if (!provider.endpoint) return `providers.${name}.endpoint is required`;
|
|
2339
|
+
if (!provider.apiKey) return `providers.${name}.apiKey is required (set WAYANG_LLM_API_KEY or configure in config file)`;
|
|
2340
|
+
if (!provider.modelName) return `providers.${name}.modelName is required`;
|
|
2341
|
+
const window = provider.contextWindowTokens;
|
|
2342
|
+
if (window !== void 0 && !(Number.isInteger(window) && window > 0)) {
|
|
2343
|
+
return `providers.${name}.contextWindowTokens must be a positive integer (got ${JSON.stringify(window)})`;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
const cp = config.controller.provider;
|
|
2347
|
+
if (!config.providers[cp]) return `controller.provider "${cp}" not found`;
|
|
2348
|
+
const wp = config.worker.provider;
|
|
2349
|
+
if (!config.providers[wp]) return `worker.provider "${wp}" not found`;
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/types/signal.ts
|
|
2354
|
+
function getSignalField(sig, field) {
|
|
2355
|
+
return sig.payload[field];
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/types/inquiry.ts
|
|
2359
|
+
var INQUIRE_OTHER_OPTION = "\u5176\u4ED6\uFF08\u81EA\u884C\u8F93\u5165\uFF09";
|
|
2360
|
+
var INQUIRE_CUSTOM_ANSWER_PREFIX = "[Other] ";
|
|
2361
|
+
function buildSelectChoices(options, allowOther) {
|
|
2362
|
+
return allowOther === false ? options : [...options, INQUIRE_OTHER_OPTION];
|
|
2363
|
+
}
|
|
2364
|
+
function isOtherChoice(choice) {
|
|
2365
|
+
return choice === INQUIRE_OTHER_OPTION;
|
|
2366
|
+
}
|
|
2367
|
+
function formatCustomAnswer(text) {
|
|
2368
|
+
return `${INQUIRE_CUSTOM_ANSWER_PREFIX}${text}`;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/tui/components/inquiry-prompt.tsx
|
|
1947
2372
|
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1948
2373
|
function InquiryPrompt({ inquiry, onAnswer }) {
|
|
1949
2374
|
if (inquiry.type === "confirm") {
|
|
1950
2375
|
return /* @__PURE__ */ jsx10(ConfirmPrompt, { message: inquiry.message, defaultVal: inquiry.default, onAnswer });
|
|
1951
2376
|
}
|
|
1952
2377
|
if (inquiry.type === "select") {
|
|
1953
|
-
return /* @__PURE__ */ jsx10(
|
|
2378
|
+
return /* @__PURE__ */ jsx10(
|
|
2379
|
+
SelectPrompt,
|
|
2380
|
+
{
|
|
2381
|
+
message: inquiry.message,
|
|
2382
|
+
options: inquiry.options ?? [],
|
|
2383
|
+
allowOther: inquiry.allowOther,
|
|
2384
|
+
onAnswer
|
|
2385
|
+
}
|
|
2386
|
+
);
|
|
1954
2387
|
}
|
|
1955
2388
|
return /* @__PURE__ */ jsx10(TextPrompt, { message: inquiry.message, onAnswer });
|
|
1956
2389
|
}
|
|
@@ -1974,25 +2407,53 @@ function ConfirmPrompt({ message, defaultVal, onAnswer }) {
|
|
|
1974
2407
|
] }, opt)) })
|
|
1975
2408
|
] });
|
|
1976
2409
|
}
|
|
1977
|
-
function SelectPrompt({ message, options, onAnswer }) {
|
|
2410
|
+
function SelectPrompt({ message, options, allowOther, onAnswer }) {
|
|
2411
|
+
const [awaitingCustom, setAwaitingCustom] = useState3(false);
|
|
2412
|
+
if (awaitingCustom) {
|
|
2413
|
+
return /* @__PURE__ */ jsx10(
|
|
2414
|
+
TextPrompt,
|
|
2415
|
+
{
|
|
2416
|
+
message,
|
|
2417
|
+
inputLabel: INQUIRE_OTHER_OPTION,
|
|
2418
|
+
onAnswer: (text) => onAnswer(formatCustomAnswer(text))
|
|
2419
|
+
}
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
return /* @__PURE__ */ jsx10(
|
|
2423
|
+
SelectList,
|
|
2424
|
+
{
|
|
2425
|
+
message,
|
|
2426
|
+
options,
|
|
2427
|
+
allowOther,
|
|
2428
|
+
onAnswer,
|
|
2429
|
+
onRequestCustom: () => setAwaitingCustom(true)
|
|
2430
|
+
}
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
function SelectList({ message, options, allowOther, onAnswer, onRequestCustom }) {
|
|
2434
|
+
const choices = buildSelectChoices(options, allowOther);
|
|
1978
2435
|
const [selected, setSelected] = useState3(0);
|
|
1979
2436
|
useInput2((_ch, key) => {
|
|
1980
|
-
if (key.upArrow) setSelected((i) => (i - 1 +
|
|
1981
|
-
if (key.downArrow) setSelected((i) => (i + 1) %
|
|
1982
|
-
if (key.return &&
|
|
2437
|
+
if (key.upArrow && choices.length > 0) setSelected((i) => (i - 1 + choices.length) % choices.length);
|
|
2438
|
+
if (key.downArrow && choices.length > 0) setSelected((i) => (i + 1) % choices.length);
|
|
2439
|
+
if (key.return && choices.length > 0) {
|
|
2440
|
+
const choice = choices[selected];
|
|
2441
|
+
if (isOtherChoice(choice)) onRequestCustom();
|
|
2442
|
+
else onAnswer(choice);
|
|
2443
|
+
}
|
|
1983
2444
|
});
|
|
1984
2445
|
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1985
2446
|
/* @__PURE__ */ jsxs7(Box7, { paddingX: 1, borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: [
|
|
1986
2447
|
/* @__PURE__ */ jsx10(Text8, { color: "cyan", children: "? " }),
|
|
1987
2448
|
/* @__PURE__ */ jsx10(Text8, { bold: true, children: message })
|
|
1988
2449
|
] }),
|
|
1989
|
-
/* @__PURE__ */ jsx10(Box7, { flexDirection: "column", paddingX: 1, children:
|
|
2450
|
+
/* @__PURE__ */ jsx10(Box7, { flexDirection: "column", paddingX: 1, children: choices.map((choice, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1990
2451
|
/* @__PURE__ */ jsx10(Text8, { color: i === selected ? theme.baseToken.color.accent : void 0, children: i === selected ? "\u25B8 " : " " }),
|
|
1991
|
-
/* @__PURE__ */ jsx10(Text8, { bold: i === selected, color: i === selected ? theme.baseToken.color.accent : void 0, children:
|
|
2452
|
+
/* @__PURE__ */ jsx10(Text8, { bold: i === selected, color: i === selected ? theme.baseToken.color.accent : void 0, dimColor: isOtherChoice(choice) && i !== selected, children: choice })
|
|
1992
2453
|
] }, i)) })
|
|
1993
2454
|
] });
|
|
1994
2455
|
}
|
|
1995
|
-
function TextPrompt({ message, onAnswer }) {
|
|
2456
|
+
function TextPrompt({ message, onAnswer, inputLabel }) {
|
|
1996
2457
|
const [input, setInput] = useState3("");
|
|
1997
2458
|
const [cursor, setCursor] = useState3(0);
|
|
1998
2459
|
useInput2((ch, key) => {
|
|
@@ -2028,10 +2489,12 @@ function TextPrompt({ message, onAnswer }) {
|
|
|
2028
2489
|
/* @__PURE__ */ jsx10(Text8, { bold: true, children: message })
|
|
2029
2490
|
] }),
|
|
2030
2491
|
/* @__PURE__ */ jsxs7(Box7, { paddingX: 1, borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: [
|
|
2031
|
-
/* @__PURE__ */ jsxs7(Text8, { children: [
|
|
2492
|
+
inputLabel ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
2032
2493
|
" ",
|
|
2033
|
-
|
|
2034
|
-
|
|
2494
|
+
inputLabel,
|
|
2495
|
+
": "
|
|
2496
|
+
] }) : /* @__PURE__ */ jsx10(Text8, { children: " " }),
|
|
2497
|
+
/* @__PURE__ */ jsx10(Text8, { children: before }),
|
|
2035
2498
|
/* @__PURE__ */ jsx10(Text8, { color: "white", inverse: true, children: after.length > 0 ? after[0] : " " }),
|
|
2036
2499
|
/* @__PURE__ */ jsx10(Text8, { children: after.slice(1) })
|
|
2037
2500
|
] })
|
|
@@ -2170,11 +2633,6 @@ function OverlayPanel({ overlay }) {
|
|
|
2170
2633
|
] });
|
|
2171
2634
|
}
|
|
2172
2635
|
|
|
2173
|
-
// src/types/signal.ts
|
|
2174
|
-
function getSignalField(sig, field) {
|
|
2175
|
-
return sig.payload[field];
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
2636
|
// src/tui/types/display-item.ts
|
|
2179
2637
|
function entryToDisplayItem(entry) {
|
|
2180
2638
|
const base = {
|
|
@@ -2402,7 +2860,7 @@ function ControllerPage({ onExit }) {
|
|
|
2402
2860
|
}
|
|
2403
2861
|
) }),
|
|
2404
2862
|
overlay && /* @__PURE__ */ jsx13(OverlayPanel, { overlay }),
|
|
2405
|
-
pendingInquiry ? /* @__PURE__ */ jsx13(InquiryPrompt, { inquiry: pendingInquiry, onAnswer: (answer) => supervisor.resolveInquiry(answer) }) : /* @__PURE__ */ jsx13(InputArea, { onSubmit: handleSubmit, onExit, onEscape: dismissOverlay, busy }),
|
|
2863
|
+
pendingInquiry ? /* @__PURE__ */ jsx13(InquiryPrompt, { inquiry: pendingInquiry, onAnswer: (answer) => supervisor.resolveInquiry(answer) }) : /* @__PURE__ */ jsx13(InputArea, { onSubmit: handleSubmit, onExit, onEscape: dismissOverlay, busy, width: terminalCols }),
|
|
2406
2864
|
/* @__PURE__ */ jsx13(
|
|
2407
2865
|
StatusBar,
|
|
2408
2866
|
{
|
|
@@ -2424,7 +2882,7 @@ function AppContent() {
|
|
|
2424
2882
|
const { exit } = useApp();
|
|
2425
2883
|
const supervisor = useSupervisor();
|
|
2426
2884
|
const busy = useWayangState(supervisor.controllerAgent, "dynamicState.busy") ?? false;
|
|
2427
|
-
const lastCtrlC =
|
|
2885
|
+
const lastCtrlC = useRef3(0);
|
|
2428
2886
|
const handleExit = useMemoizedFn(() => {
|
|
2429
2887
|
exit();
|
|
2430
2888
|
});
|
|
@@ -2448,7 +2906,7 @@ function AppContent() {
|
|
|
2448
2906
|
// package.json
|
|
2449
2907
|
var package_default = {
|
|
2450
2908
|
name: "wayang-ai",
|
|
2451
|
-
version: "0.
|
|
2909
|
+
version: "0.7.0",
|
|
2452
2910
|
description: "Multi-agent orchestration platform",
|
|
2453
2911
|
type: "module",
|
|
2454
2912
|
license: "MIT",
|
|
@@ -2550,13 +3008,13 @@ function writeWelcome(workspace) {
|
|
|
2550
3008
|
|
|
2551
3009
|
// src/tui/render.ts
|
|
2552
3010
|
function renderInkUI(supervisor) {
|
|
2553
|
-
return new Promise((
|
|
3011
|
+
return new Promise((resolve12) => {
|
|
2554
3012
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
2555
3013
|
writeWelcome(supervisor.ctx.workspaceDir);
|
|
2556
3014
|
const { waitUntilExit } = render(
|
|
2557
3015
|
React6.createElement(App, { supervisor })
|
|
2558
3016
|
);
|
|
2559
|
-
waitUntilExit().then(() =>
|
|
3017
|
+
waitUntilExit().then(() => resolve12());
|
|
2560
3018
|
});
|
|
2561
3019
|
}
|
|
2562
3020
|
|
|
@@ -2565,26 +3023,6 @@ import * as fs3 from "fs";
|
|
|
2565
3023
|
import * as path3 from "path";
|
|
2566
3024
|
import * as childProcess from "child_process";
|
|
2567
3025
|
import prompts from "prompts";
|
|
2568
|
-
|
|
2569
|
-
// src/types/config.ts
|
|
2570
|
-
function validateConfig(config) {
|
|
2571
|
-
for (const [name, provider] of Object.entries(config.providers)) {
|
|
2572
|
-
if (!provider.endpoint) return `providers.${name}.endpoint is required`;
|
|
2573
|
-
if (!provider.apiKey) return `providers.${name}.apiKey is required (set WAYANG_LLM_API_KEY or configure in config file)`;
|
|
2574
|
-
if (!provider.modelName) return `providers.${name}.modelName is required`;
|
|
2575
|
-
const window = provider.contextWindowTokens;
|
|
2576
|
-
if (window !== void 0 && !(Number.isInteger(window) && window > 0)) {
|
|
2577
|
-
return `providers.${name}.contextWindowTokens must be a positive integer (got ${JSON.stringify(window)})`;
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
const cp = config.controller.provider;
|
|
2581
|
-
if (!config.providers[cp]) return `controller.provider "${cp}" not found`;
|
|
2582
|
-
const wp = config.worker.provider;
|
|
2583
|
-
if (!config.providers[wp]) return `worker.provider "${wp}" not found`;
|
|
2584
|
-
return null;
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
// src/onboard.ts
|
|
2588
3026
|
function fillApiKeys(config) {
|
|
2589
3027
|
const envKey = process.env.WAYANG_LLM_API_KEY || "";
|
|
2590
3028
|
for (const provider of Object.values(config.providers)) {
|
|
@@ -3508,10 +3946,10 @@ var SignalQueue = class {
|
|
|
3508
3946
|
waitForSignal() {
|
|
3509
3947
|
const unread = this.state.get("signals").filter((s) => s.status === "unread");
|
|
3510
3948
|
if (unread.length > 0) return Promise.resolve();
|
|
3511
|
-
return new Promise((
|
|
3949
|
+
return new Promise((resolve12) => {
|
|
3512
3950
|
this.resolveWait = () => {
|
|
3513
3951
|
this.logger.debug("waitForSignal resolved");
|
|
3514
|
-
|
|
3952
|
+
resolve12();
|
|
3515
3953
|
};
|
|
3516
3954
|
});
|
|
3517
3955
|
}
|
|
@@ -3571,267 +4009,12 @@ var SignalQueue = class {
|
|
|
3571
4009
|
}
|
|
3572
4010
|
};
|
|
3573
4011
|
|
|
3574
|
-
// src/services/
|
|
3575
|
-
import {
|
|
3576
|
-
|
|
3577
|
-
jsonFile;
|
|
3578
|
-
logger;
|
|
3579
|
-
constructor(ctx) {
|
|
3580
|
-
const jsonFile = new JSONFileHelper(join6(ctx.sessionDir, "tasks.json"));
|
|
3581
|
-
super(
|
|
3582
|
-
{
|
|
3583
|
-
tasks: {
|
|
3584
|
-
pending: [],
|
|
3585
|
-
running: [],
|
|
3586
|
-
history: []
|
|
3587
|
-
}
|
|
3588
|
-
},
|
|
3589
|
-
[{ path: "tasks", helper: jsonFile }]
|
|
3590
|
-
);
|
|
3591
|
-
this.jsonFile = jsonFile;
|
|
3592
|
-
this.logger = ctx.logger;
|
|
3593
|
-
}
|
|
3594
|
-
async restore() {
|
|
3595
|
-
const data = this.jsonFile.read();
|
|
3596
|
-
if (data) {
|
|
3597
|
-
this.data.tasks = data;
|
|
3598
|
-
}
|
|
3599
|
-
this.logger.info("TaskPoolState restored");
|
|
3600
|
-
}
|
|
3601
|
-
};
|
|
3602
|
-
|
|
3603
|
-
// src/services/agents/base-agent.ts
|
|
3604
|
-
import { streamText, stepCountIs, hasToolCall } from "ai";
|
|
3605
|
-
|
|
3606
|
-
// src/services/agents/model-factory.ts
|
|
3607
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
3608
|
-
function createModel(provider) {
|
|
3609
|
-
const compatible = createOpenAICompatible({
|
|
3610
|
-
name: "wayang",
|
|
3611
|
-
baseURL: provider.endpoint,
|
|
3612
|
-
apiKey: provider.apiKey
|
|
3613
|
-
});
|
|
3614
|
-
return compatible.chatModel(provider.modelName);
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
|
-
// src/utils/id.ts
|
|
3618
|
-
function generateId(prefix) {
|
|
3619
|
-
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
3620
|
-
}
|
|
3621
|
-
function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
3622
|
-
const Y = date.getFullYear();
|
|
3623
|
-
const M = String(date.getMonth() + 1).padStart(2, "0");
|
|
3624
|
-
const D = String(date.getDate()).padStart(2, "0");
|
|
3625
|
-
const h = String(date.getHours()).padStart(2, "0");
|
|
3626
|
-
const m = String(date.getMinutes()).padStart(2, "0");
|
|
3627
|
-
const s = String(date.getSeconds()).padStart(2, "0");
|
|
3628
|
-
return `${Y}${M}${D}-${h}${m}${s}`;
|
|
3629
|
-
}
|
|
4012
|
+
// src/services/worker-transcript.ts
|
|
4013
|
+
import { closeSync as closeSync2, existsSync as existsSync5, openSync as openSync2, readSync, statSync } from "fs";
|
|
4014
|
+
import { join as join7 } from "path";
|
|
3630
4015
|
|
|
3631
|
-
// src/
|
|
3632
|
-
|
|
3633
|
-
const now = /* @__PURE__ */ new Date();
|
|
3634
|
-
const off = -now.getTimezoneOffset();
|
|
3635
|
-
const sign = off >= 0 ? "+" : "-";
|
|
3636
|
-
const absOff = Math.abs(off);
|
|
3637
|
-
const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
|
|
3638
|
-
const mm = String(absOff % 60).padStart(2, "0");
|
|
3639
|
-
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
3640
|
-
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}${sign}${hh}:${mm}`;
|
|
3641
|
-
}
|
|
3642
|
-
|
|
3643
|
-
// src/services/agents/base-agent.ts
|
|
3644
|
-
var DEFAULT_LLM_IDLE_TIMEOUT_MS = 3e5;
|
|
3645
|
-
var BaseAgent = class {
|
|
3646
|
-
id;
|
|
3647
|
-
abortController;
|
|
3648
|
-
model;
|
|
3649
|
-
hooks = {};
|
|
3650
|
-
constructor(provider) {
|
|
3651
|
-
this.id = generateId("agent");
|
|
3652
|
-
this.abortController = new AbortController();
|
|
3653
|
-
this.model = createModel(provider);
|
|
3654
|
-
}
|
|
3655
|
-
/** Set lifecycle hooks. */
|
|
3656
|
-
setHooks(hooks) {
|
|
3657
|
-
this.hooks = hooks;
|
|
3658
|
-
}
|
|
3659
|
-
/** Core streaming primitive — yields text chunks, returns final result.
|
|
3660
|
-
* Catches abort signal internally so callers see a clean completion. */
|
|
3661
|
-
async *streamLoop(opts) {
|
|
3662
|
-
const step = 0;
|
|
3663
|
-
const idleTimeoutMs = opts.llmIdleTimeoutMs ?? DEFAULT_LLM_IDLE_TIMEOUT_MS;
|
|
3664
|
-
const callAbort = new AbortController();
|
|
3665
|
-
const forwardAbort = () => callAbort.abort();
|
|
3666
|
-
if (this.abortController.signal.aborted) {
|
|
3667
|
-
callAbort.abort();
|
|
3668
|
-
} else {
|
|
3669
|
-
this.abortController.signal.addEventListener("abort", forwardAbort);
|
|
3670
|
-
}
|
|
3671
|
-
let watchdog = null;
|
|
3672
|
-
let timedOut = false;
|
|
3673
|
-
let toolsInFlight = 0;
|
|
3674
|
-
let finished = false;
|
|
3675
|
-
const clearWatchdog = () => {
|
|
3676
|
-
if (watchdog !== null) {
|
|
3677
|
-
clearTimeout(watchdog);
|
|
3678
|
-
watchdog = null;
|
|
3679
|
-
}
|
|
3680
|
-
};
|
|
3681
|
-
const armWatchdog = () => {
|
|
3682
|
-
if (idleTimeoutMs <= 0 || finished || toolsInFlight > 0) return;
|
|
3683
|
-
clearWatchdog();
|
|
3684
|
-
watchdog = setTimeout(() => {
|
|
3685
|
-
timedOut = true;
|
|
3686
|
-
callAbort.abort();
|
|
3687
|
-
}, idleTimeoutMs);
|
|
3688
|
-
};
|
|
3689
|
-
const tools = pauseWatchdogDuringTools(
|
|
3690
|
-
opts.tools,
|
|
3691
|
-
() => {
|
|
3692
|
-
toolsInFlight++;
|
|
3693
|
-
clearWatchdog();
|
|
3694
|
-
},
|
|
3695
|
-
() => {
|
|
3696
|
-
toolsInFlight--;
|
|
3697
|
-
if (toolsInFlight === 0) armWatchdog();
|
|
3698
|
-
}
|
|
3699
|
-
);
|
|
3700
|
-
let stream;
|
|
3701
|
-
const stopConditions = [];
|
|
3702
|
-
if (opts.maxSteps) stopConditions.push(stepCountIs(opts.maxSteps));
|
|
3703
|
-
if (opts.stopTools?.length) {
|
|
3704
|
-
for (const name of opts.stopTools) {
|
|
3705
|
-
stopConditions.push(hasToolCall(name));
|
|
3706
|
-
}
|
|
3707
|
-
}
|
|
3708
|
-
let startTime;
|
|
3709
|
-
let firstChunkAt;
|
|
3710
|
-
let stepStartedAt;
|
|
3711
|
-
const onStep = opts.onStep ? (event) => {
|
|
3712
|
-
armWatchdog();
|
|
3713
|
-
const meta = { startedAt: stepStartedAt, firstTokenAt: firstChunkAt };
|
|
3714
|
-
firstChunkAt = void 0;
|
|
3715
|
-
stepStartedAt = nowISO();
|
|
3716
|
-
opts.onStep(event, meta);
|
|
3717
|
-
} : void 0;
|
|
3718
|
-
try {
|
|
3719
|
-
this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
|
|
3720
|
-
startTime = Date.now();
|
|
3721
|
-
stepStartedAt = nowISO();
|
|
3722
|
-
armWatchdog();
|
|
3723
|
-
stream = streamText({
|
|
3724
|
-
model: this.model,
|
|
3725
|
-
system: opts.system,
|
|
3726
|
-
messages: opts.messages,
|
|
3727
|
-
tools,
|
|
3728
|
-
toolChoice: opts.toolChoice,
|
|
3729
|
-
stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
|
|
3730
|
-
abortSignal: callAbort.signal,
|
|
3731
|
-
...opts.prepareStep && { prepareStep: opts.prepareStep },
|
|
3732
|
-
onStepFinish: onStep,
|
|
3733
|
-
onError: ({ error }) => {
|
|
3734
|
-
opts.onStreamError?.(error);
|
|
3735
|
-
},
|
|
3736
|
-
// SignalEntry entries are trusted system-generated notifications emitted
|
|
3737
|
-
// as `system`-role messages (in-band, order-sensitive). AI SDK v6 warns
|
|
3738
|
-
// on any system message inside `messages` unless this is opted in.
|
|
3739
|
-
allowSystemInMessages: true
|
|
3740
|
-
});
|
|
3741
|
-
} catch (err) {
|
|
3742
|
-
finished = true;
|
|
3743
|
-
clearWatchdog();
|
|
3744
|
-
this.abortController.signal.removeEventListener("abort", forwardAbort);
|
|
3745
|
-
if (this.abortController.signal.aborted) {
|
|
3746
|
-
return { text: "", toolResults: [], usage: void 0 };
|
|
3747
|
-
}
|
|
3748
|
-
throw err;
|
|
3749
|
-
}
|
|
3750
|
-
try {
|
|
3751
|
-
for await (const chunk of stream.textStream) {
|
|
3752
|
-
armWatchdog();
|
|
3753
|
-
if (chunk && firstChunkAt === void 0) firstChunkAt = nowISO();
|
|
3754
|
-
yield chunk;
|
|
3755
|
-
}
|
|
3756
|
-
} catch (err) {
|
|
3757
|
-
if (this.abortController.signal.aborted) {
|
|
3758
|
-
return { text: "", toolResults: [], usage: void 0 };
|
|
3759
|
-
}
|
|
3760
|
-
if (timedOut) {
|
|
3761
|
-
throw new Error(
|
|
3762
|
-
`LLM call idle timeout: no stream progress (text chunk or step finish) for ${idleTimeoutMs}ms`
|
|
3763
|
-
);
|
|
3764
|
-
}
|
|
3765
|
-
if (this.abortController.signal.aborted) {
|
|
3766
|
-
return { text: "", toolResults: [], usage: void 0 };
|
|
3767
|
-
}
|
|
3768
|
-
throw err;
|
|
3769
|
-
} finally {
|
|
3770
|
-
finished = true;
|
|
3771
|
-
clearWatchdog();
|
|
3772
|
-
this.abortController.signal.removeEventListener("abort", forwardAbort);
|
|
3773
|
-
}
|
|
3774
|
-
const duration = Date.now() - startTime;
|
|
3775
|
-
const text = await stream.text;
|
|
3776
|
-
const reasoningText = await stream.reasoningText;
|
|
3777
|
-
const toolResults = await stream.toolResults;
|
|
3778
|
-
const rawUsage = await stream.usage;
|
|
3779
|
-
const finishReason = await stream.finishReason;
|
|
3780
|
-
const steps = await stream.steps;
|
|
3781
|
-
const usage = rawUsage ? { inputTokens: rawUsage.inputTokens ?? 0, outputTokens: rawUsage.outputTokens ?? 0 } : void 0;
|
|
3782
|
-
this.hooks.afterLLM?.({ step, usage, durationMs: duration });
|
|
3783
|
-
return {
|
|
3784
|
-
text,
|
|
3785
|
-
reasoningText,
|
|
3786
|
-
toolResults: toolResults?.map((tr) => ({
|
|
3787
|
-
toolCallId: tr.toolCallId,
|
|
3788
|
-
result: tr.result,
|
|
3789
|
-
isError: tr.isError
|
|
3790
|
-
})),
|
|
3791
|
-
usage,
|
|
3792
|
-
finishReason,
|
|
3793
|
-
steps: steps?.length
|
|
3794
|
-
};
|
|
3795
|
-
}
|
|
3796
|
-
/** Batch wrapper — iterates streamLoop and returns the final result. */
|
|
3797
|
-
async collectLoop(opts) {
|
|
3798
|
-
const gen = this.streamLoop(opts);
|
|
3799
|
-
let result;
|
|
3800
|
-
while (true) {
|
|
3801
|
-
const { done, value } = await gen.next();
|
|
3802
|
-
if (done) {
|
|
3803
|
-
result = value;
|
|
3804
|
-
break;
|
|
3805
|
-
}
|
|
3806
|
-
}
|
|
3807
|
-
return result;
|
|
3808
|
-
}
|
|
3809
|
-
abort() {
|
|
3810
|
-
this.abortController.abort();
|
|
3811
|
-
}
|
|
3812
|
-
};
|
|
3813
|
-
function pauseWatchdogDuringTools(tools, onStart, onEnd) {
|
|
3814
|
-
const wrapped = {};
|
|
3815
|
-
for (const [name, def] of Object.entries(tools ?? {})) {
|
|
3816
|
-
const original = def?.execute;
|
|
3817
|
-
if (typeof original !== "function") {
|
|
3818
|
-
wrapped[name] = def;
|
|
3819
|
-
continue;
|
|
3820
|
-
}
|
|
3821
|
-
wrapped[name] = {
|
|
3822
|
-
...def,
|
|
3823
|
-
execute: async (...args) => {
|
|
3824
|
-
onStart();
|
|
3825
|
-
try {
|
|
3826
|
-
return await original(...args);
|
|
3827
|
-
} finally {
|
|
3828
|
-
onEnd();
|
|
3829
|
-
}
|
|
3830
|
-
}
|
|
3831
|
-
};
|
|
3832
|
-
}
|
|
3833
|
-
return wrapped;
|
|
3834
|
-
}
|
|
4016
|
+
// src/services/agents/worker-state.ts
|
|
4017
|
+
import { join as join6 } from "path";
|
|
3835
4018
|
|
|
3836
4019
|
// src/services/agents/utils/conversation-to-sdk-messages.ts
|
|
3837
4020
|
function conversationToSdkMessages(entries) {
|
|
@@ -4005,6 +4188,32 @@ function toolOutputText(output) {
|
|
|
4005
4188
|
return toolPayloadText(output);
|
|
4006
4189
|
}
|
|
4007
4190
|
|
|
4191
|
+
// src/utils/id.ts
|
|
4192
|
+
function generateId(prefix) {
|
|
4193
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
4194
|
+
}
|
|
4195
|
+
function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
4196
|
+
const Y = date.getFullYear();
|
|
4197
|
+
const M = String(date.getMonth() + 1).padStart(2, "0");
|
|
4198
|
+
const D = String(date.getDate()).padStart(2, "0");
|
|
4199
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
4200
|
+
const m = String(date.getMinutes()).padStart(2, "0");
|
|
4201
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
4202
|
+
return `${Y}${M}${D}-${h}${m}${s}`;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
// src/utils/time.ts
|
|
4206
|
+
function nowISO() {
|
|
4207
|
+
const now = /* @__PURE__ */ new Date();
|
|
4208
|
+
const off = -now.getTimezoneOffset();
|
|
4209
|
+
const sign = off >= 0 ? "+" : "-";
|
|
4210
|
+
const absOff = Math.abs(off);
|
|
4211
|
+
const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
|
|
4212
|
+
const mm = String(absOff % 60).padStart(2, "0");
|
|
4213
|
+
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
4214
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}${sign}${hh}:${mm}`;
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4008
4217
|
// src/services/agents/context-manager.ts
|
|
4009
4218
|
var DEFAULT_CONTEXT_WINDOW_TOKENS = 256e3;
|
|
4010
4219
|
var COMPACT_THRESHOLD_RATIO = 0.9;
|
|
@@ -4180,121 +4389,641 @@ ${this.compactSummary}` : "";
|
|
|
4180
4389
|
}
|
|
4181
4390
|
}
|
|
4182
4391
|
}
|
|
4183
|
-
breakdown.total = breakdown.systemPrompt + breakdown.user + breakdown.assistant + breakdown.toolCalls + breakdown.toolResults + breakdown.compactSummary;
|
|
4184
|
-
return breakdown;
|
|
4185
|
-
}
|
|
4186
|
-
/**
|
|
4187
|
-
* Restore compact state from a summary computed by the state layer.
|
|
4188
|
-
*
|
|
4189
|
-
* Call this after the state layer's restore() has already sliced the
|
|
4190
|
-
* in-memory conversation to the restore window (entries before the cut are
|
|
4191
|
-
* gone from memory). The window itself is the verbatim zone, so
|
|
4192
|
-
* compactStartIdx resets to 0 — see the field's semantics note.
|
|
4193
|
-
*/
|
|
4194
|
-
restoreCompact(summary) {
|
|
4195
|
-
this.compactSummary = summary;
|
|
4196
|
-
this.compactStartIdx = 0;
|
|
4197
|
-
}
|
|
4198
|
-
/** Get the cached compaction summary, or null if no compaction is active. */
|
|
4199
|
-
getCompactSummary() {
|
|
4200
|
-
return this.compactSummary;
|
|
4392
|
+
breakdown.total = breakdown.systemPrompt + breakdown.user + breakdown.assistant + breakdown.toolCalls + breakdown.toolResults + breakdown.compactSummary;
|
|
4393
|
+
return breakdown;
|
|
4394
|
+
}
|
|
4395
|
+
/**
|
|
4396
|
+
* Restore compact state from a summary computed by the state layer.
|
|
4397
|
+
*
|
|
4398
|
+
* Call this after the state layer's restore() has already sliced the
|
|
4399
|
+
* in-memory conversation to the restore window (entries before the cut are
|
|
4400
|
+
* gone from memory). The window itself is the verbatim zone, so
|
|
4401
|
+
* compactStartIdx resets to 0 — see the field's semantics note.
|
|
4402
|
+
*/
|
|
4403
|
+
restoreCompact(summary) {
|
|
4404
|
+
this.compactSummary = summary;
|
|
4405
|
+
this.compactStartIdx = 0;
|
|
4406
|
+
}
|
|
4407
|
+
/** Get the cached compaction summary, or null if no compaction is active. */
|
|
4408
|
+
getCompactSummary() {
|
|
4409
|
+
return this.compactSummary;
|
|
4410
|
+
}
|
|
4411
|
+
/**
|
|
4412
|
+
* Compact the conversation by summarizing all current entries.
|
|
4413
|
+
*
|
|
4414
|
+
* Appends a compact marker entry to the conversation (and JSONL file).
|
|
4415
|
+
* Does NOT modify or remove any existing entries — storage is append-only.
|
|
4416
|
+
*
|
|
4417
|
+
* The summarizer receives ALL entries and returns a summary.
|
|
4418
|
+
* After compaction, getSystemPrompt() carries the summary and getMessages() returns only the entries appended after the marker.
|
|
4419
|
+
*
|
|
4420
|
+
* @param meta Optional provenance recorded on the marker (model name).
|
|
4421
|
+
*/
|
|
4422
|
+
async compact(summarizer, meta) {
|
|
4423
|
+
const conversation = this.state.get("conversation");
|
|
4424
|
+
if (conversation.length < 4) return;
|
|
4425
|
+
const fromEntryId = conversation[0]?.uuid;
|
|
4426
|
+
const toEntryId = conversation[conversation.length - 1]?.uuid;
|
|
4427
|
+
const summary = await summarizer(conversation);
|
|
4428
|
+
this.compactSummary = summary;
|
|
4429
|
+
this.compactStartIdx = conversation.length;
|
|
4430
|
+
this.lastReportedTokens = null;
|
|
4431
|
+
const markerTs = nowISO();
|
|
4432
|
+
this.state.append("conversation", {
|
|
4433
|
+
type: "system" /* System */,
|
|
4434
|
+
uuid: generateId("compact"),
|
|
4435
|
+
parentUuid: null,
|
|
4436
|
+
sessionId: this.state.get("runtimeState.session.id") ?? "",
|
|
4437
|
+
timestamp: markerTs,
|
|
4438
|
+
subtype: "compact" /* Compact */,
|
|
4439
|
+
content: summary,
|
|
4440
|
+
// System entries are point-in-time events: startedAt/endedAt share the
|
|
4441
|
+
// marker's creation moment.
|
|
4442
|
+
startedAt: markerTs,
|
|
4443
|
+
endedAt: markerTs,
|
|
4444
|
+
compactMeta: {
|
|
4445
|
+
summarizedTokenCount: estimateConversationTokens(conversation),
|
|
4446
|
+
...fromEntryId !== void 0 && { fromEntryId },
|
|
4447
|
+
...toEntryId !== void 0 && { toEntryId },
|
|
4448
|
+
...meta?.model !== void 0 && { model: meta.model },
|
|
4449
|
+
createdAt: markerTs
|
|
4450
|
+
}
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
};
|
|
4454
|
+
|
|
4455
|
+
// src/services/agents/worker-state.ts
|
|
4456
|
+
function workerDir(sessionDir, workerId) {
|
|
4457
|
+
return join6(sessionDir, "workers", workerId);
|
|
4458
|
+
}
|
|
4459
|
+
var WorkerState = class extends BaseWayangState {
|
|
4460
|
+
constructor(sessionDir, workerId, logger) {
|
|
4461
|
+
const dir = workerDir(sessionDir, workerId);
|
|
4462
|
+
const jsonFile = new JSONFileHelper(join6(dir, "runtime-state.json"));
|
|
4463
|
+
const jsonlFile = new JSONLFileHelper(join6(dir, "conversation.jsonl"));
|
|
4464
|
+
const initialData = {
|
|
4465
|
+
runtimeState: {
|
|
4466
|
+
session: { id: workerId, startedAt: Date.now() },
|
|
4467
|
+
task: { id: "", description: "" }
|
|
4468
|
+
},
|
|
4469
|
+
conversation: [],
|
|
4470
|
+
inbox: []
|
|
4471
|
+
};
|
|
4472
|
+
super(initialData, [
|
|
4473
|
+
{ path: "runtimeState", helper: jsonFile },
|
|
4474
|
+
{ path: "conversation", helper: jsonlFile }
|
|
4475
|
+
]);
|
|
4476
|
+
this.logger = logger;
|
|
4477
|
+
this.jsonFile = jsonFile;
|
|
4478
|
+
this.jsonlFile = jsonlFile;
|
|
4479
|
+
}
|
|
4480
|
+
logger;
|
|
4481
|
+
jsonFile;
|
|
4482
|
+
jsonlFile;
|
|
4483
|
+
async restore() {
|
|
4484
|
+
const runtimeState = this.jsonFile.read();
|
|
4485
|
+
if (runtimeState) {
|
|
4486
|
+
this.data.runtimeState = runtimeState;
|
|
4487
|
+
}
|
|
4488
|
+
const allEntries = this.jsonlFile.read();
|
|
4489
|
+
const compactIdx = allEntries.findLastIndex(isCompactMarker);
|
|
4490
|
+
if (compactIdx === -1) {
|
|
4491
|
+
if (allEntries.length > 0) {
|
|
4492
|
+
this.data.conversation = allEntries;
|
|
4493
|
+
}
|
|
4494
|
+
} else {
|
|
4495
|
+
let budget = RECENT_TOKEN_BUDGET;
|
|
4496
|
+
let cutIdx = 0;
|
|
4497
|
+
for (let i = compactIdx - 1; i >= 0; i--) {
|
|
4498
|
+
budget -= estimateEntryTokens(allEntries[i]);
|
|
4499
|
+
if (budget <= 0) {
|
|
4500
|
+
cutIdx = i + 1;
|
|
4501
|
+
break;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
this.data.conversation = allEntries.slice(cutIdx);
|
|
4505
|
+
}
|
|
4506
|
+
this.logger.info("WorkerState restored");
|
|
4507
|
+
}
|
|
4508
|
+
};
|
|
4509
|
+
|
|
4510
|
+
// src/services/worker-transcript.ts
|
|
4511
|
+
var WORKER_TRANSCRIPT_FILE = "conversation.jsonl";
|
|
4512
|
+
var MAX_TRANSCRIPT_RESULT_CHARS = 4e4;
|
|
4513
|
+
var WORKER_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
4514
|
+
function isValidWorkerId(workerId) {
|
|
4515
|
+
return WORKER_ID_PATTERN.test(workerId) && workerId !== "." && workerId !== "..";
|
|
4516
|
+
}
|
|
4517
|
+
function workerTranscriptPath(sessionDir, workerId) {
|
|
4518
|
+
return join7(workerDir(sessionDir, workerId), WORKER_TRANSCRIPT_FILE);
|
|
4519
|
+
}
|
|
4520
|
+
function readWorkerTranscript(sessionDir, workerId) {
|
|
4521
|
+
if (!isValidWorkerId(workerId)) return null;
|
|
4522
|
+
const path7 = workerTranscriptPath(sessionDir, workerId);
|
|
4523
|
+
if (!existsSync5(path7)) return null;
|
|
4524
|
+
return new JSONLFileHelper(path7).read();
|
|
4525
|
+
}
|
|
4526
|
+
var MAX_TRANSCRIPT_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
4527
|
+
var MIN_WINDOW_BYTES = 64 * 1024;
|
|
4528
|
+
var ENTRY_BYTES_ESTIMATE = 1024;
|
|
4529
|
+
function parseWindow(text, startsAtFileHead) {
|
|
4530
|
+
let body = text;
|
|
4531
|
+
if (!startsAtFileHead) {
|
|
4532
|
+
const nl = body.indexOf("\n");
|
|
4533
|
+
if (nl === -1) return { entries: [], incompleteTail: false };
|
|
4534
|
+
body = body.slice(nl + 1);
|
|
4535
|
+
}
|
|
4536
|
+
const endsComplete = body.endsWith("\n");
|
|
4537
|
+
const lines = body.split("\n");
|
|
4538
|
+
if (endsComplete) lines.pop();
|
|
4539
|
+
const entries = [];
|
|
4540
|
+
let incompleteTail = false;
|
|
4541
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4542
|
+
const line = lines[i];
|
|
4543
|
+
if (line.length === 0) continue;
|
|
4544
|
+
try {
|
|
4545
|
+
entries.push(JSON.parse(line));
|
|
4546
|
+
} catch {
|
|
4547
|
+
if (i === lines.length - 1 && !endsComplete) incompleteTail = true;
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
return { entries, incompleteTail };
|
|
4551
|
+
}
|
|
4552
|
+
function readWorkerTranscriptWindow(sessionDir, workerId, opts = {}) {
|
|
4553
|
+
if (!isValidWorkerId(workerId)) return null;
|
|
4554
|
+
const path7 = workerTranscriptPath(sessionDir, workerId);
|
|
4555
|
+
let size;
|
|
4556
|
+
try {
|
|
4557
|
+
size = statSync(path7).size;
|
|
4558
|
+
} catch {
|
|
4559
|
+
return null;
|
|
4560
|
+
}
|
|
4561
|
+
if (size === 0) {
|
|
4562
|
+
return { entries: [], incompleteTail: false, windowTruncated: false, coversFile: true };
|
|
4563
|
+
}
|
|
4564
|
+
const matches = opts.matches ?? (() => true);
|
|
4565
|
+
const need = opts.need ?? Number.POSITIVE_INFINITY;
|
|
4566
|
+
let windowBytes = need === Number.POSITIVE_INFINITY ? MAX_TRANSCRIPT_WINDOW_BYTES : Math.min(MAX_TRANSCRIPT_WINDOW_BYTES, Math.max(MIN_WINDOW_BYTES, Math.ceil(need) * ENTRY_BYTES_ESTIMATE));
|
|
4567
|
+
for (; ; ) {
|
|
4568
|
+
const readLen = Math.min(size, windowBytes);
|
|
4569
|
+
const start = size - readLen;
|
|
4570
|
+
const chunk = Buffer.alloc(readLen);
|
|
4571
|
+
const fd = openSync2(path7, "r");
|
|
4572
|
+
try {
|
|
4573
|
+
readSync(fd, chunk, 0, readLen, start);
|
|
4574
|
+
} finally {
|
|
4575
|
+
closeSync2(fd);
|
|
4576
|
+
}
|
|
4577
|
+
const parsed = parseWindow(chunk.toString("utf8"), start === 0);
|
|
4578
|
+
const enough = parsed.entries.filter(matches).length >= need;
|
|
4579
|
+
const canGrow = start > 0 && windowBytes < MAX_TRANSCRIPT_WINDOW_BYTES;
|
|
4580
|
+
if (enough || !canGrow) {
|
|
4581
|
+
return {
|
|
4582
|
+
entries: parsed.entries,
|
|
4583
|
+
incompleteTail: parsed.incompleteTail,
|
|
4584
|
+
windowTruncated: start > 0,
|
|
4585
|
+
coversFile: start === 0
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
windowBytes = Math.min(MAX_TRANSCRIPT_WINDOW_BYTES, windowBytes * 2);
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
function blockText(block) {
|
|
4592
|
+
if (typeof block === "string") return block;
|
|
4593
|
+
if (block && typeof block === "object") {
|
|
4594
|
+
const b = block;
|
|
4595
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
4596
|
+
return JSON.stringify(block);
|
|
4597
|
+
}
|
|
4598
|
+
return String(block ?? "");
|
|
4599
|
+
}
|
|
4600
|
+
function entryText(entry) {
|
|
4601
|
+
const raw = entry;
|
|
4602
|
+
const content = raw.message?.content ?? raw.content ?? "";
|
|
4603
|
+
if (typeof content === "string") return content;
|
|
4604
|
+
if (Array.isArray(content)) return content.map(blockText).join("\n");
|
|
4605
|
+
return content === null || content === void 0 ? "" : String(content);
|
|
4606
|
+
}
|
|
4607
|
+
function entryToolCalls(entry) {
|
|
4608
|
+
const calls = entry.toolCalls;
|
|
4609
|
+
if (!Array.isArray(calls)) return [];
|
|
4610
|
+
return calls.filter((c) => typeof c?.toolName === "string").map((c) => ({ toolName: c.toolName, arguments: c.arguments }));
|
|
4611
|
+
}
|
|
4612
|
+
function entryToolResults(entry) {
|
|
4613
|
+
const results = entry.toolResults;
|
|
4614
|
+
if (!Array.isArray(results)) return [];
|
|
4615
|
+
return results.map((r) => ({
|
|
4616
|
+
output: toolOutputText(r.output),
|
|
4617
|
+
isError: r.isError
|
|
4618
|
+
}));
|
|
4619
|
+
}
|
|
4620
|
+
function entryLabel(entry) {
|
|
4621
|
+
const subtype = entry.subtype;
|
|
4622
|
+
return subtype ? `${entry.type}/${subtype}` : entry.type;
|
|
4623
|
+
}
|
|
4624
|
+
var TERMINAL_SUBTYPES = /* @__PURE__ */ new Set(["task_result" /* TaskResult */, "error" /* Error */]);
|
|
4625
|
+
function entryTimestamp(entry) {
|
|
4626
|
+
const e = entry;
|
|
4627
|
+
for (const raw of [e.timestamp, e.endedAt, e.startedAt]) {
|
|
4628
|
+
if (typeof raw === "string") {
|
|
4629
|
+
const ms = Date.parse(raw);
|
|
4630
|
+
if (Number.isFinite(ms)) return ms;
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
return null;
|
|
4634
|
+
}
|
|
4635
|
+
function summarizeTranscript(entries) {
|
|
4636
|
+
let lastActivityAt = null;
|
|
4637
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
4638
|
+
lastActivityAt = entryTimestamp(entries[i]);
|
|
4639
|
+
if (lastActivityAt !== null) break;
|
|
4640
|
+
}
|
|
4641
|
+
let currentTool = null;
|
|
4642
|
+
let terminalSubtype = null;
|
|
4643
|
+
for (let i = entries.length - 1; i >= 0 && (currentTool === null || terminalSubtype === null); i--) {
|
|
4644
|
+
const entry = entries[i];
|
|
4645
|
+
if (currentTool === null) {
|
|
4646
|
+
const calls = entryToolCalls(entry);
|
|
4647
|
+
if (calls.length > 0) currentTool = calls[calls.length - 1].toolName;
|
|
4648
|
+
}
|
|
4649
|
+
if (terminalSubtype === null && entry.type === "system" /* System */ && TERMINAL_SUBTYPES.has(String(entry.subtype))) {
|
|
4650
|
+
terminalSubtype = String(entry.subtype);
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
return {
|
|
4654
|
+
totalEntries: entries.length,
|
|
4655
|
+
lastActivityAt,
|
|
4656
|
+
lastEntryType: entries.length > 0 ? entries[entries.length - 1].type : null,
|
|
4657
|
+
currentTool,
|
|
4658
|
+
hasTerminalEntry: terminalSubtype !== null,
|
|
4659
|
+
terminalSubtype
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
function formatDuration(ms) {
|
|
4663
|
+
if (!Number.isFinite(ms)) return "unknown";
|
|
4664
|
+
const abs = Math.max(0, ms);
|
|
4665
|
+
if (abs < 1e3) return `${Math.round(abs)}ms`;
|
|
4666
|
+
if (abs < 6e4) return `${(abs / 1e3).toFixed(1)}s`;
|
|
4667
|
+
if (abs < 36e5) {
|
|
4668
|
+
const m2 = Math.floor(abs / 6e4);
|
|
4669
|
+
const s = Math.floor(abs % 6e4 / 1e3);
|
|
4670
|
+
return `${m2}m ${String(s).padStart(2, "0")}s`;
|
|
4671
|
+
}
|
|
4672
|
+
const h = Math.floor(abs / 36e5);
|
|
4673
|
+
const m = Math.floor(abs % 36e5 / 6e4);
|
|
4674
|
+
return `${h}h ${String(m).padStart(2, "0")}m`;
|
|
4675
|
+
}
|
|
4676
|
+
function formatLastActivity(lastActivityAt, now) {
|
|
4677
|
+
if (lastActivityAt === null) return "unknown";
|
|
4678
|
+
return `${new Date(lastActivityAt).toISOString()} (${formatDuration(now - lastActivityAt)} ago)`;
|
|
4679
|
+
}
|
|
4680
|
+
function selectWindow(entries, offset, limit) {
|
|
4681
|
+
const total = entries.length;
|
|
4682
|
+
const safeOffset = Math.max(0, Number.isFinite(offset) ? Math.trunc(offset) : 0);
|
|
4683
|
+
const safeLimit = Math.max(0, Number.isFinite(limit) ? Math.trunc(limit) : 0);
|
|
4684
|
+
const end = Math.max(0, total - safeOffset);
|
|
4685
|
+
const start = safeLimit === 0 ? end : Math.max(0, end - safeLimit);
|
|
4686
|
+
const window = entries.slice(start, end);
|
|
4687
|
+
return {
|
|
4688
|
+
entries: window,
|
|
4689
|
+
totalEntries: total,
|
|
4690
|
+
returnedEntries: window.length,
|
|
4691
|
+
offset: safeOffset,
|
|
4692
|
+
limit: safeLimit,
|
|
4693
|
+
startIndex: window.length > 0 ? start + 1 : 0,
|
|
4694
|
+
endIndex: window.length > 0 ? end : 0,
|
|
4695
|
+
windowTruncated: window.length < total
|
|
4696
|
+
};
|
|
4697
|
+
}
|
|
4698
|
+
function formatTranscriptEntry(entry, position, opts) {
|
|
4699
|
+
const body = entryText(entry).replace(/\r/g, "");
|
|
4700
|
+
const toolNames = entryToolCalls(entry).map((c) => c.toolName);
|
|
4701
|
+
const toolNote = toolNames.length > 0 ? ` (tools: ${toolNames.join(", ")})` : "";
|
|
4702
|
+
const head = ` #${position} [${entryLabel(entry)}]${toolNote}`;
|
|
4703
|
+
const cap = opts.maxChars;
|
|
4704
|
+
const cut2 = cap !== null && body.length > cap;
|
|
4705
|
+
const shown = cut2 ? body.slice(0, cap) + "\u2026" : body;
|
|
4706
|
+
const marker = cut2 ? ` (truncated: ${cap}/${body.length} chars)` : "";
|
|
4707
|
+
const lines = [];
|
|
4708
|
+
if (shown.length === 0) {
|
|
4709
|
+
lines.push(`${head}${marker} (empty)`);
|
|
4710
|
+
} else if (!shown.includes("\n")) {
|
|
4711
|
+
lines.push(`${head}${marker} ${shown}`);
|
|
4712
|
+
} else {
|
|
4713
|
+
lines.push(`${head}${marker}`);
|
|
4714
|
+
for (const line of shown.split("\n")) lines.push(` ${line}`);
|
|
4715
|
+
}
|
|
4716
|
+
if (opts.includeToolDetail) {
|
|
4717
|
+
for (const call of entryToolCalls(entry)) {
|
|
4718
|
+
const args = call.arguments ?? "";
|
|
4719
|
+
const argsShown = cap !== null && args.length > cap ? args.slice(0, cap) + "\u2026" : args;
|
|
4720
|
+
lines.push(` \u21B3 ${call.toolName} ${argsShown}`.trimEnd());
|
|
4721
|
+
}
|
|
4722
|
+
for (const result of entryToolResults(entry)) {
|
|
4723
|
+
const output = result.output ?? "";
|
|
4724
|
+
const outputShown = cap !== null && output.length > cap ? output.slice(0, cap) + "\u2026" : output;
|
|
4725
|
+
lines.push(` \u21B3 result${result.isError ? " [error]" : ""}: ${outputShown}`.trimEnd());
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
return { lines, truncated: cut2, originalChars: body.length };
|
|
4729
|
+
}
|
|
4730
|
+
function shrinkWindow(win, dropFromStart) {
|
|
4731
|
+
const drop = Math.max(0, Math.min(dropFromStart, win.entries.length));
|
|
4732
|
+
if (drop === 0) return win;
|
|
4733
|
+
const entries = win.entries.slice(drop);
|
|
4734
|
+
return {
|
|
4735
|
+
...win,
|
|
4736
|
+
entries,
|
|
4737
|
+
returnedEntries: entries.length,
|
|
4738
|
+
startIndex: win.startIndex + drop,
|
|
4739
|
+
windowTruncated: true
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
function renderEntriesFitting(entries, startIndex, opts, budgetChars) {
|
|
4743
|
+
const formatted = entries.map((entry, i) => formatTranscriptEntry(entry, startIndex + i, opts));
|
|
4744
|
+
let used = 0;
|
|
4745
|
+
let first = formatted.length;
|
|
4746
|
+
for (let i = formatted.length - 1; i >= 0; i--) {
|
|
4747
|
+
const size = formatted[i].lines.reduce((n, line) => n + line.length + 1, 0);
|
|
4748
|
+
if (used + size > budgetChars && first < formatted.length) break;
|
|
4749
|
+
used += size;
|
|
4750
|
+
first = i;
|
|
4751
|
+
}
|
|
4752
|
+
const kept = formatted.slice(first);
|
|
4753
|
+
return {
|
|
4754
|
+
lines: kept.flatMap((f) => f.lines),
|
|
4755
|
+
dropped: first,
|
|
4756
|
+
shortened: kept.filter((f) => f.truncated).length
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
function buildWindowMeta(win, opts) {
|
|
4760
|
+
return {
|
|
4761
|
+
totalEntries: win.totalEntries,
|
|
4762
|
+
returnedEntries: win.returnedEntries,
|
|
4763
|
+
offset: win.offset,
|
|
4764
|
+
limit: win.limit,
|
|
4765
|
+
maxChars: opts.maxChars,
|
|
4766
|
+
full: opts.full,
|
|
4767
|
+
truncated: win.windowTruncated || opts.shortenedEntries > 0,
|
|
4768
|
+
windowTruncated: win.windowTruncated,
|
|
4769
|
+
startIndex: win.startIndex,
|
|
4770
|
+
endIndex: win.endIndex,
|
|
4771
|
+
shortenedEntries: opts.shortenedEntries
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
function buildTruncationNotice(meta) {
|
|
4775
|
+
const perEntry = meta.maxChars === null ? "none" : `${meta.maxChars}`;
|
|
4776
|
+
const shortened = meta.shortenedEntries > 0 ? ` (${meta.shortenedEntries} entries shortened)` : "";
|
|
4777
|
+
if (meta.windowTruncated) {
|
|
4778
|
+
const how = meta.full ? "output cap reached \u2014 page further back with offset" : "pass offset/limit or full=true";
|
|
4779
|
+
return `[truncated: showing ${meta.returnedEntries} of ${meta.totalEntries} entries (${meta.startIndex}-${meta.endIndex}), per-entry limit ${perEntry} chars${shortened}; ${how}]`;
|
|
4780
|
+
}
|
|
4781
|
+
if (meta.shortenedEntries > 0) {
|
|
4782
|
+
return `[truncated: showing all ${meta.totalEntries} entries, per-entry limit ${perEntry} chars${shortened}; pass full=true for complete content]`;
|
|
4783
|
+
}
|
|
4784
|
+
return null;
|
|
4785
|
+
}
|
|
4786
|
+
function buildTranscriptHeader(meta) {
|
|
4787
|
+
if (meta.totalEntries === 0) return "Conversation (0 entries):";
|
|
4788
|
+
if (!meta.windowTruncated) return `Conversation (${meta.totalEntries} entries):`;
|
|
4789
|
+
return `Conversation (${meta.totalEntries} entries total, showing ${meta.startIndex}-${meta.endIndex} by offset=${meta.offset} limit=${meta.limit}):`;
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
// src/services/task/task-pool-state.ts
|
|
4793
|
+
import { join as join8 } from "path";
|
|
4794
|
+
var TaskPoolState = class extends BaseWayangState {
|
|
4795
|
+
jsonFile;
|
|
4796
|
+
logger;
|
|
4797
|
+
constructor(ctx) {
|
|
4798
|
+
const jsonFile = new JSONFileHelper(join8(ctx.sessionDir, "tasks.json"));
|
|
4799
|
+
super(
|
|
4800
|
+
{
|
|
4801
|
+
tasks: {
|
|
4802
|
+
pending: [],
|
|
4803
|
+
running: [],
|
|
4804
|
+
history: []
|
|
4805
|
+
}
|
|
4806
|
+
},
|
|
4807
|
+
[{ path: "tasks", helper: jsonFile }]
|
|
4808
|
+
);
|
|
4809
|
+
this.jsonFile = jsonFile;
|
|
4810
|
+
this.logger = ctx.logger;
|
|
4811
|
+
}
|
|
4812
|
+
async restore() {
|
|
4813
|
+
const data = this.jsonFile.read();
|
|
4814
|
+
if (data) {
|
|
4815
|
+
this.data.tasks = data;
|
|
4816
|
+
}
|
|
4817
|
+
this.logger.info("TaskPoolState restored");
|
|
4818
|
+
}
|
|
4819
|
+
};
|
|
4820
|
+
|
|
4821
|
+
// src/services/agents/base-agent.ts
|
|
4822
|
+
import { streamText, stepCountIs, hasToolCall } from "ai";
|
|
4823
|
+
|
|
4824
|
+
// src/services/agents/model-factory.ts
|
|
4825
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
4826
|
+
function createModel(provider) {
|
|
4827
|
+
const compatible = createOpenAICompatible({
|
|
4828
|
+
name: "wayang",
|
|
4829
|
+
baseURL: provider.endpoint,
|
|
4830
|
+
apiKey: provider.apiKey
|
|
4831
|
+
});
|
|
4832
|
+
return compatible.chatModel(provider.modelName);
|
|
4833
|
+
}
|
|
4834
|
+
|
|
4835
|
+
// src/services/agents/base-agent.ts
|
|
4836
|
+
var DEFAULT_LLM_IDLE_TIMEOUT_MS = 3e5;
|
|
4837
|
+
var BaseAgent = class {
|
|
4838
|
+
id;
|
|
4839
|
+
abortController;
|
|
4840
|
+
model;
|
|
4841
|
+
hooks = {};
|
|
4842
|
+
constructor(provider) {
|
|
4843
|
+
this.id = generateId("agent");
|
|
4844
|
+
this.abortController = new AbortController();
|
|
4845
|
+
this.model = createModel(provider);
|
|
4846
|
+
}
|
|
4847
|
+
/** Set lifecycle hooks. */
|
|
4848
|
+
setHooks(hooks) {
|
|
4849
|
+
this.hooks = hooks;
|
|
4850
|
+
}
|
|
4851
|
+
/** Core streaming primitive — yields text chunks, returns final result.
|
|
4852
|
+
* Catches abort signal internally so callers see a clean completion. */
|
|
4853
|
+
async *streamLoop(opts) {
|
|
4854
|
+
const step = 0;
|
|
4855
|
+
const idleTimeoutMs = opts.llmIdleTimeoutMs ?? DEFAULT_LLM_IDLE_TIMEOUT_MS;
|
|
4856
|
+
const callAbort = new AbortController();
|
|
4857
|
+
const forwardAbort = () => callAbort.abort();
|
|
4858
|
+
if (this.abortController.signal.aborted) {
|
|
4859
|
+
callAbort.abort();
|
|
4860
|
+
} else {
|
|
4861
|
+
this.abortController.signal.addEventListener("abort", forwardAbort);
|
|
4862
|
+
}
|
|
4863
|
+
let watchdog = null;
|
|
4864
|
+
let timedOut = false;
|
|
4865
|
+
let toolsInFlight = 0;
|
|
4866
|
+
let finished = false;
|
|
4867
|
+
const clearWatchdog = () => {
|
|
4868
|
+
if (watchdog !== null) {
|
|
4869
|
+
clearTimeout(watchdog);
|
|
4870
|
+
watchdog = null;
|
|
4871
|
+
}
|
|
4872
|
+
};
|
|
4873
|
+
const armWatchdog = () => {
|
|
4874
|
+
if (idleTimeoutMs <= 0 || finished || toolsInFlight > 0) return;
|
|
4875
|
+
clearWatchdog();
|
|
4876
|
+
watchdog = setTimeout(() => {
|
|
4877
|
+
timedOut = true;
|
|
4878
|
+
callAbort.abort();
|
|
4879
|
+
}, idleTimeoutMs);
|
|
4880
|
+
};
|
|
4881
|
+
const tools = pauseWatchdogDuringTools(
|
|
4882
|
+
opts.tools,
|
|
4883
|
+
() => {
|
|
4884
|
+
toolsInFlight++;
|
|
4885
|
+
clearWatchdog();
|
|
4886
|
+
},
|
|
4887
|
+
() => {
|
|
4888
|
+
toolsInFlight--;
|
|
4889
|
+
if (toolsInFlight === 0) armWatchdog();
|
|
4890
|
+
}
|
|
4891
|
+
);
|
|
4892
|
+
let stream;
|
|
4893
|
+
const stopConditions = [];
|
|
4894
|
+
if (opts.maxSteps) stopConditions.push(stepCountIs(opts.maxSteps));
|
|
4895
|
+
if (opts.stopTools?.length) {
|
|
4896
|
+
for (const name of opts.stopTools) {
|
|
4897
|
+
stopConditions.push(hasToolCall(name));
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
let startTime;
|
|
4901
|
+
let firstChunkAt;
|
|
4902
|
+
let stepStartedAt;
|
|
4903
|
+
const onStep = opts.onStep ? (event) => {
|
|
4904
|
+
armWatchdog();
|
|
4905
|
+
const meta = { startedAt: stepStartedAt, firstTokenAt: firstChunkAt };
|
|
4906
|
+
firstChunkAt = void 0;
|
|
4907
|
+
stepStartedAt = nowISO();
|
|
4908
|
+
opts.onStep(event, meta);
|
|
4909
|
+
} : void 0;
|
|
4910
|
+
try {
|
|
4911
|
+
this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
|
|
4912
|
+
startTime = Date.now();
|
|
4913
|
+
stepStartedAt = nowISO();
|
|
4914
|
+
armWatchdog();
|
|
4915
|
+
stream = streamText({
|
|
4916
|
+
model: this.model,
|
|
4917
|
+
system: opts.system,
|
|
4918
|
+
messages: opts.messages,
|
|
4919
|
+
tools,
|
|
4920
|
+
toolChoice: opts.toolChoice,
|
|
4921
|
+
stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
|
|
4922
|
+
abortSignal: callAbort.signal,
|
|
4923
|
+
...opts.prepareStep && { prepareStep: opts.prepareStep },
|
|
4924
|
+
onStepFinish: onStep,
|
|
4925
|
+
onError: ({ error }) => {
|
|
4926
|
+
opts.onStreamError?.(error);
|
|
4927
|
+
},
|
|
4928
|
+
// SignalEntry entries are trusted system-generated notifications emitted
|
|
4929
|
+
// as `system`-role messages (in-band, order-sensitive). AI SDK v6 warns
|
|
4930
|
+
// on any system message inside `messages` unless this is opted in.
|
|
4931
|
+
allowSystemInMessages: true
|
|
4932
|
+
});
|
|
4933
|
+
} catch (err) {
|
|
4934
|
+
finished = true;
|
|
4935
|
+
clearWatchdog();
|
|
4936
|
+
this.abortController.signal.removeEventListener("abort", forwardAbort);
|
|
4937
|
+
if (this.abortController.signal.aborted) {
|
|
4938
|
+
return { text: "", toolResults: [], usage: void 0 };
|
|
4939
|
+
}
|
|
4940
|
+
throw err;
|
|
4941
|
+
}
|
|
4942
|
+
try {
|
|
4943
|
+
for await (const chunk of stream.textStream) {
|
|
4944
|
+
armWatchdog();
|
|
4945
|
+
if (chunk && firstChunkAt === void 0) firstChunkAt = nowISO();
|
|
4946
|
+
yield chunk;
|
|
4947
|
+
}
|
|
4948
|
+
} catch (err) {
|
|
4949
|
+
if (this.abortController.signal.aborted) {
|
|
4950
|
+
return { text: "", toolResults: [], usage: void 0 };
|
|
4951
|
+
}
|
|
4952
|
+
if (timedOut) {
|
|
4953
|
+
throw new Error(
|
|
4954
|
+
`LLM call idle timeout: no stream progress (text chunk or step finish) for ${idleTimeoutMs}ms`
|
|
4955
|
+
);
|
|
4956
|
+
}
|
|
4957
|
+
if (this.abortController.signal.aborted) {
|
|
4958
|
+
return { text: "", toolResults: [], usage: void 0 };
|
|
4959
|
+
}
|
|
4960
|
+
throw err;
|
|
4961
|
+
} finally {
|
|
4962
|
+
finished = true;
|
|
4963
|
+
clearWatchdog();
|
|
4964
|
+
this.abortController.signal.removeEventListener("abort", forwardAbort);
|
|
4965
|
+
}
|
|
4966
|
+
const duration = Date.now() - startTime;
|
|
4967
|
+
const text = await stream.text;
|
|
4968
|
+
const reasoningText = await stream.reasoningText;
|
|
4969
|
+
const toolResults = await stream.toolResults;
|
|
4970
|
+
const rawUsage = await stream.usage;
|
|
4971
|
+
const finishReason = await stream.finishReason;
|
|
4972
|
+
const steps = await stream.steps;
|
|
4973
|
+
const usage = rawUsage ? { inputTokens: rawUsage.inputTokens ?? 0, outputTokens: rawUsage.outputTokens ?? 0 } : void 0;
|
|
4974
|
+
this.hooks.afterLLM?.({ step, usage, durationMs: duration });
|
|
4975
|
+
return {
|
|
4976
|
+
text,
|
|
4977
|
+
reasoningText,
|
|
4978
|
+
toolResults: toolResults?.map((tr) => ({
|
|
4979
|
+
toolCallId: tr.toolCallId,
|
|
4980
|
+
result: tr.result,
|
|
4981
|
+
isError: tr.isError
|
|
4982
|
+
})),
|
|
4983
|
+
usage,
|
|
4984
|
+
finishReason,
|
|
4985
|
+
steps: steps?.length
|
|
4986
|
+
};
|
|
4201
4987
|
}
|
|
4202
|
-
/**
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
* @param meta Optional provenance recorded on the marker (model name).
|
|
4212
|
-
*/
|
|
4213
|
-
async compact(summarizer, meta) {
|
|
4214
|
-
const conversation = this.state.get("conversation");
|
|
4215
|
-
if (conversation.length < 4) return;
|
|
4216
|
-
const fromEntryId = conversation[0]?.uuid;
|
|
4217
|
-
const toEntryId = conversation[conversation.length - 1]?.uuid;
|
|
4218
|
-
const summary = await summarizer(conversation);
|
|
4219
|
-
this.compactSummary = summary;
|
|
4220
|
-
this.compactStartIdx = conversation.length;
|
|
4221
|
-
this.lastReportedTokens = null;
|
|
4222
|
-
const markerTs = nowISO();
|
|
4223
|
-
this.state.append("conversation", {
|
|
4224
|
-
type: "system" /* System */,
|
|
4225
|
-
uuid: generateId("compact"),
|
|
4226
|
-
parentUuid: null,
|
|
4227
|
-
sessionId: this.state.get("runtimeState.session.id") ?? "",
|
|
4228
|
-
timestamp: markerTs,
|
|
4229
|
-
subtype: "compact" /* Compact */,
|
|
4230
|
-
content: summary,
|
|
4231
|
-
// System entries are point-in-time events: startedAt/endedAt share the
|
|
4232
|
-
// marker's creation moment.
|
|
4233
|
-
startedAt: markerTs,
|
|
4234
|
-
endedAt: markerTs,
|
|
4235
|
-
compactMeta: {
|
|
4236
|
-
summarizedTokenCount: estimateConversationTokens(conversation),
|
|
4237
|
-
...fromEntryId !== void 0 && { fromEntryId },
|
|
4238
|
-
...toEntryId !== void 0 && { toEntryId },
|
|
4239
|
-
...meta?.model !== void 0 && { model: meta.model },
|
|
4240
|
-
createdAt: markerTs
|
|
4988
|
+
/** Batch wrapper — iterates streamLoop and returns the final result. */
|
|
4989
|
+
async collectLoop(opts) {
|
|
4990
|
+
const gen = this.streamLoop(opts);
|
|
4991
|
+
let result;
|
|
4992
|
+
while (true) {
|
|
4993
|
+
const { done, value } = await gen.next();
|
|
4994
|
+
if (done) {
|
|
4995
|
+
result = value;
|
|
4996
|
+
break;
|
|
4241
4997
|
}
|
|
4242
|
-
}
|
|
4998
|
+
}
|
|
4999
|
+
return result;
|
|
4243
5000
|
}
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
// src/services/agents/worker-state.ts
|
|
4247
|
-
import { join as join7 } from "path";
|
|
4248
|
-
var WorkerState = class extends BaseWayangState {
|
|
4249
|
-
constructor(sessionDir, workerId, logger) {
|
|
4250
|
-
const workerDir = join7(sessionDir, "workers", workerId);
|
|
4251
|
-
const jsonFile = new JSONFileHelper(join7(workerDir, "runtime-state.json"));
|
|
4252
|
-
const jsonlFile = new JSONLFileHelper(join7(workerDir, "conversation.jsonl"));
|
|
4253
|
-
const initialData = {
|
|
4254
|
-
runtimeState: {
|
|
4255
|
-
session: { id: workerId, startedAt: Date.now() },
|
|
4256
|
-
task: { id: "", description: "" }
|
|
4257
|
-
},
|
|
4258
|
-
conversation: [],
|
|
4259
|
-
inbox: []
|
|
4260
|
-
};
|
|
4261
|
-
super(initialData, [
|
|
4262
|
-
{ path: "runtimeState", helper: jsonFile },
|
|
4263
|
-
{ path: "conversation", helper: jsonlFile }
|
|
4264
|
-
]);
|
|
4265
|
-
this.logger = logger;
|
|
4266
|
-
this.jsonFile = jsonFile;
|
|
4267
|
-
this.jsonlFile = jsonlFile;
|
|
5001
|
+
abort() {
|
|
5002
|
+
this.abortController.abort();
|
|
4268
5003
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
const
|
|
4274
|
-
if (
|
|
4275
|
-
|
|
5004
|
+
};
|
|
5005
|
+
function pauseWatchdogDuringTools(tools, onStart, onEnd) {
|
|
5006
|
+
const wrapped = {};
|
|
5007
|
+
for (const [name, def] of Object.entries(tools ?? {})) {
|
|
5008
|
+
const original = def?.execute;
|
|
5009
|
+
if (typeof original !== "function") {
|
|
5010
|
+
wrapped[name] = def;
|
|
5011
|
+
continue;
|
|
4276
5012
|
}
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
let cutIdx = 0;
|
|
4286
|
-
for (let i = compactIdx - 1; i >= 0; i--) {
|
|
4287
|
-
budget -= estimateEntryTokens(allEntries[i]);
|
|
4288
|
-
if (budget <= 0) {
|
|
4289
|
-
cutIdx = i + 1;
|
|
4290
|
-
break;
|
|
5013
|
+
wrapped[name] = {
|
|
5014
|
+
...def,
|
|
5015
|
+
execute: async (...args) => {
|
|
5016
|
+
onStart();
|
|
5017
|
+
try {
|
|
5018
|
+
return await original(...args);
|
|
5019
|
+
} finally {
|
|
5020
|
+
onEnd();
|
|
4291
5021
|
}
|
|
4292
5022
|
}
|
|
4293
|
-
|
|
4294
|
-
}
|
|
4295
|
-
this.logger.info("WorkerState restored");
|
|
5023
|
+
};
|
|
4296
5024
|
}
|
|
4297
|
-
|
|
5025
|
+
return wrapped;
|
|
5026
|
+
}
|
|
4298
5027
|
|
|
4299
5028
|
// src/services/agents/utils/build-assistant-entry.ts
|
|
4300
5029
|
function buildAssistantEntry(event, sessionId, uuid, extras) {
|
|
@@ -4537,18 +5266,39 @@ function controllerMessageContent(content) {
|
|
|
4537
5266
|
return `[Controller message] ${content}`;
|
|
4538
5267
|
}
|
|
4539
5268
|
function createInboxInjection(deps) {
|
|
5269
|
+
let baseLength = null;
|
|
4540
5270
|
const injected = [];
|
|
4541
5271
|
return {
|
|
4542
5272
|
prepareStep({ messages }) {
|
|
4543
5273
|
const fresh = deps.drainInbox();
|
|
4544
5274
|
if (fresh.length === 0 && injected.length === 0) return void 0;
|
|
5275
|
+
if (baseLength === null) baseLength = messages.length;
|
|
5276
|
+
const responseCount = Math.max(0, messages.length - baseLength);
|
|
4545
5277
|
for (const m of fresh) {
|
|
4546
|
-
injected.push({
|
|
5278
|
+
injected.push({
|
|
5279
|
+
message: { role: "user", content: controllerMessageContent(m.content) },
|
|
5280
|
+
// After the response messages that exist NOW — i.e. at the tail of
|
|
5281
|
+
// the conversation as this step sees it, before this step's reply.
|
|
5282
|
+
insertAfter: responseCount
|
|
5283
|
+
});
|
|
4547
5284
|
deps.onInject(m.content);
|
|
4548
5285
|
}
|
|
4549
|
-
|
|
5286
|
+
const merged = messages.slice(0, baseLength);
|
|
5287
|
+
let cursor = 0;
|
|
5288
|
+
for (const inj of injected) {
|
|
5289
|
+
while (cursor < Math.min(inj.insertAfter, responseCount)) {
|
|
5290
|
+
merged.push(messages[baseLength + cursor]);
|
|
5291
|
+
cursor++;
|
|
5292
|
+
}
|
|
5293
|
+
merged.push(inj.message);
|
|
5294
|
+
}
|
|
5295
|
+
while (cursor < responseCount) {
|
|
5296
|
+
merged.push(messages[baseLength + cursor]);
|
|
5297
|
+
cursor++;
|
|
5298
|
+
}
|
|
5299
|
+
return { messages: merged };
|
|
4550
5300
|
},
|
|
4551
|
-
injectedMessages: () =>
|
|
5301
|
+
injectedMessages: () => injected.map((i) => i.message)
|
|
4552
5302
|
};
|
|
4553
5303
|
}
|
|
4554
5304
|
|
|
@@ -4566,15 +5316,16 @@ function isInsideWorkspace(resolved, workspace) {
|
|
|
4566
5316
|
const rel = relative(workspace, resolved);
|
|
4567
5317
|
return !rel.startsWith("..") && resolve(workspace, rel) === resolved;
|
|
4568
5318
|
}
|
|
4569
|
-
function truncate(result) {
|
|
4570
|
-
if (result.length <=
|
|
4571
|
-
return result.slice(0,
|
|
5319
|
+
function truncate(result, maxChars = MAX_RESULT_CHARS) {
|
|
5320
|
+
if (result.length <= maxChars) return result;
|
|
5321
|
+
return result.slice(0, maxChars) + "\n...(truncated)";
|
|
4572
5322
|
}
|
|
4573
|
-
function safeExecute(name, fn) {
|
|
4574
|
-
|
|
5323
|
+
function safeExecute(name, fn, options) {
|
|
5324
|
+
const maxChars = options?.maxResultChars ?? MAX_RESULT_CHARS;
|
|
5325
|
+
return async (args, callOptions) => {
|
|
4575
5326
|
try {
|
|
4576
|
-
const result = await fn(args,
|
|
4577
|
-
return truncate(typeof result === "string" ? result : JSON.stringify(result));
|
|
5327
|
+
const result = await fn(args, callOptions);
|
|
5328
|
+
return truncate(typeof result === "string" ? result : JSON.stringify(result), maxChars);
|
|
4578
5329
|
} catch (err) {
|
|
4579
5330
|
return `[ERROR] ${name}: ${err.message ?? String(err)}`;
|
|
4580
5331
|
}
|
|
@@ -4728,12 +5479,12 @@ Check the "Available workers" section below for current options.
|
|
|
4728
5479
|
|
|
4729
5480
|
## How to write a good task description
|
|
4730
5481
|
|
|
4731
|
-
The worker only sees the task description \u2014 it has NO access to your conversation.
|
|
4732
5482
|
Write it as a **self-contained instruction**:
|
|
4733
5483
|
1. What to do (specific, unambiguous)
|
|
4734
5484
|
2. All context/data the worker needs
|
|
4735
5485
|
3. Expected output (file path, format, etc.)
|
|
4736
5486
|
4. Constraints (language, length, style)
|
|
5487
|
+
5. Acceptance criteria \u2014 what counts as done
|
|
4737
5488
|
|
|
4738
5489
|
Bad: "Write the essay the user asked for"
|
|
4739
5490
|
Good: "Write a 200-word Chinese prose essay about autumn. Save to sanwen.txt in UTF-8."
|
|
@@ -4799,11 +5550,37 @@ Use get_task_detail when you need more information about a specific task:
|
|
|
4799
5550
|
- You need to understand what a worker actually did before deciding next steps
|
|
4800
5551
|
- Checking the error details of a failed task before retrying
|
|
4801
5552
|
|
|
4802
|
-
The tool shows task metadata (status, result, error) and
|
|
5553
|
+
The tool shows task metadata (status, result, error) and the worker conversation. The conversation comes from the worker's live in-memory state while it runs, and from its on-disk transcript after the worker ended (marked [from transcript]). The conversation is paginated and ALWAYS says what was cut:
|
|
5554
|
+
- Pagination semantics: \`offset\` skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination.
|
|
5555
|
+
- \`limit\` (default 10, counted back from the newest), \`offset\` (default 0, skip the newest N), \`maxChars\` (per-entry cap, default 200), \`full=true\` (complete text, all entries).
|
|
5556
|
+
- A meta line reports \`totalEntries / returnedEntries / offset / truncated\`; a \`[truncated: ...]\` line appears whenever anything was cut, and each cut entry carries \`(truncated: 200/4642 chars)\`.
|
|
5557
|
+
- NEVER conclude that a worker "stopped mid-sentence" from a truncated entry. If an entry is marked truncated, re-read it with full=true (or page back with offset) before judging.
|
|
5558
|
+
|
|
5559
|
+
## Reading a worker's full transcript (read_worker_conversation)
|
|
5560
|
+
|
|
5561
|
+
read_worker_conversation(workerId, limit?, offset?, maxChars?, full?, type?) reads a worker's conversation with a layered source: while the engine still tracks the worker (running or idle), its live in-memory conversation is returned (meta \`source="memory"\`); after the worker finished/failed/was cancelled or disposed, its append-only on-disk \`conversation.jsonl\` is read instead (meta \`source="disk"\`). So it works for running AND already-finished workers, including after cancel_task.
|
|
5562
|
+
- Pagination semantics: \`offset\` skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination.
|
|
5563
|
+
- Same per-entry truncation markers as get_task_detail; entries print in chronological order.
|
|
5564
|
+
- \`type\` filters: user / assistant / system / signal / tool ("tool" = assistant entries that made tool calls).
|
|
5565
|
+
- A \`[note: last entry is incomplete \u2014 the transcript is still being written]\` line means the newest entry was mid-append when you read it: re-read in a moment instead of treating the tail as the final state. \`readWindowTruncated\` in the meta means older entries exist beyond the read window's byte cap \u2014 page back with offset.
|
|
5566
|
+
- Use it to see what a worker actually produced before you report, retry, or cancel.
|
|
5567
|
+
|
|
5568
|
+
## Checking whether a worker is alive (get_worker_status)
|
|
5569
|
+
|
|
5570
|
+
get_worker_status(workerId?) \u2014 with an id returns that worker's status object, without one returns all current workers.
|
|
5571
|
+
Fields: workerId, type, taskId, status, runningForMs, lastActivityAt (epoch ms of the newest transcript entry) with lastActivityAgoMs (how long ago that was), currentTool, streamEnded, resultReceived, idleSince.
|
|
5572
|
+
Use it to tell "still working" from "stuck / already finished but not released":
|
|
5573
|
+
- Many minutes without a new transcript entry and no current tool \u2192 likely stuck: inspect with read_worker_conversation, then cancel_task.
|
|
5574
|
+
- resultReceived=true but streamEnded=false / status still running \u2192 the worker is in its wrap-up; wait or inspect instead of cancelling.
|
|
5575
|
+
- Unknown worker ids return a not-found message (never an exception).
|
|
4803
5576
|
|
|
4804
5577
|
## Sending messages to workers (chat_worker)
|
|
4805
5578
|
|
|
4806
|
-
Use chat_worker(workerId, message) to send guidance to a running worker.
|
|
5579
|
+
Use chat_worker(workerId, message) to send guidance to a running worker.
|
|
5580
|
+
The worker reads it at its next step (puppet) or turn boundary (third-party) \u2014 a tool call in progress is NEVER interrupted; an idle multi-stage worker reads it when its next stage starts.
|
|
5581
|
+
Delivery is blind: "queued" only means the message sits in the worker's inbox until that boundary. A stuck worker never reads it.
|
|
5582
|
+
A third-party worker that already finished its session rejects the message with an explicit [ERROR] \u2014 inspect its output instead of retrying.
|
|
5583
|
+
Before chatting, check get_worker_status; if the worker is stuck, cancel_task is the honest action (a queued message will not revive it).
|
|
4807
5584
|
Use sparingly \u2014 only when you need to:
|
|
4808
5585
|
- Correct a worker that is going in the wrong direction
|
|
4809
5586
|
- Provide additional context the worker did not have
|
|
@@ -4840,7 +5617,8 @@ Types:
|
|
|
4840
5617
|
Guidelines:
|
|
4841
5618
|
- Prefer confirm and select over text. Less typing = better experience.
|
|
4842
5619
|
- Keep messages short and specific.
|
|
4843
|
-
- For select, provide 2-5 clear options.
|
|
5620
|
+
- For select, provide 2-5 clear preset options \u2014 an "Other" choice is appended automatically so the user can always type a custom answer, so never add your own catch-all option. Custom answers come back prefixed with "[Other] ".
|
|
5621
|
+
- Set allowOther=false only when the answer must be one of the preset options.`
|
|
4844
5622
|
);
|
|
4845
5623
|
var SIGNAL_HANDLING = section(
|
|
4846
5624
|
"Handling signals",
|
|
@@ -4876,8 +5654,9 @@ Explain the error. Suggest a retry or alternative.
|
|
|
4876
5654
|
## [HEARTBEAT] \u2014 periodic check-in while workers are running
|
|
4877
5655
|
|
|
4878
5656
|
You were woken up because workers are running but no events arrived for a while.
|
|
4879
|
-
Review the worker status in the heartbeat message:
|
|
4880
|
-
- A worker has been running unusually long \u2192
|
|
5657
|
+
Review the worker status in the heartbeat message (runningForMs, last activity, current tool):
|
|
5658
|
+
- A worker has been running unusually long AND its last activity is old (or it shows no tool) \u2192 inspect it: get_worker_status then read_worker_conversation. If it is genuinely stuck, cancel_task and report honestly.
|
|
5659
|
+
- A worker has been running unusually long but activity is recent \u2192 inform the user proactively
|
|
4881
5660
|
- Everything looks normal \u2192 call skip_reply()
|
|
4882
5661
|
|
|
4883
5662
|
## [PERMISSION REQUEST ...] \u2014 worker needs approval
|
|
@@ -4900,6 +5679,39 @@ You resumed a previous session that had unfinished tasks. Workers were NOT autom
|
|
|
4900
5679
|
|
|
4901
5680
|
Do NOT blindly re-create everything. The user may have moved on. Prefer to summarize and ask unless the intent is obvious.`
|
|
4902
5681
|
);
|
|
5682
|
+
var WORKING_PRINCIPLES = section(
|
|
5683
|
+
"Working principles",
|
|
5684
|
+
`\u3010\u4EFB\u52A1\u62C6\u5206\u3011
|
|
5685
|
+
- \u4E00\u4E2A\u4EFB\u52A1\u53EA\u4EA4\u4ED8\u4E00\u4E2A\u53EF\u72EC\u7ACB\u9A8C\u6536\u7684\u7ED3\u679C\uFF1B\u4E0D\u8981\u628A\u8BBE\u8BA1\u3001\u5B9E\u73B0\u3001\u6D4B\u8BD5\u585E\u8FDB\u540C\u4E00\u6BB5\u3002
|
|
5686
|
+
- \u5148\u505A\u80FD\u88AB\u5355\u6D4B\u65AD\u8A00\u7684\u6838\u5FC3\u903B\u8F91\uFF0C\u628A\u8FB9\u754C\uFF08\u5BBD\u5B57\u7B26\u3001\u4EE3\u7406\u5BF9\u3001\u6298\u884C\u4F4D\u7F6E\u4E4B\u7C7B\uFF09\u56FA\u5316\u6210\u7528\u4F8B\uFF0C\u518D\u53E0\u52A0\u96C6\u6210\u4E0E\u4EA4\u4E92\u5C42\u3002
|
|
5687
|
+
- \u4EFB\u52A1\u63CF\u8FF0\u5FC5\u987B\u81EA\u5305\u542B\uFF1A\u80CC\u666F\u3001\u76EE\u6807\u3001\u8F93\u5165\u8F93\u51FA\u3001\u7EA6\u675F\u3001\u9A8C\u6536\u6807\u51C6\u3002\u6267\u884C\u65B9\u770B\u4E0D\u5230\u4F60\u7684\u5BF9\u8BDD\u3002
|
|
5688
|
+
|
|
5689
|
+
\u3010\u5206\u5DE5\u4E0E\u5BA1\u6838\u3011
|
|
5690
|
+
- \u8BBE\u8BA1\u5224\u65AD\u4E0E\u673A\u68B0\u5B9E\u73B0\u5206\u5F00\u6D3E\uFF1A\u8BBE\u8BA1\u65B9\u5148\u5B9E\u6D4B\u5173\u952E\u884C\u4E3A\uFF0C\u4EA7\u51FA\u300C\u6539\u54EA\u4E2A\u6587\u4EF6\u3001\u51FD\u6570\u7B7E\u540D\u3001\u6570\u636E\u7ED3\u6784\u3001\u8FB9\u754C\u60C5\u51B5\u3001\u6D4B\u8BD5\u6E05\u5355\u300D\u7684\u65B9\u6848\uFF1B\u6267\u884C\u65B9\u7167\u65B9\u6848\u7F16\u7801\uFF0C\u4E0D\u505A\u8BBE\u8BA1\u51B3\u7B56\u3002
|
|
5691
|
+
- \u65B9\u6848\u91CC\u8981\u663E\u5F0F\u5199\u51FA\u300C\u4E0D\u8981\u505A\u4EC0\u4E48\u300D\uFF0C\u5426\u5219\u6267\u884C\u65B9\u4F1A\u987A\u624B\u91CD\u6784\u65E0\u5173\u4EE3\u7801\u3002
|
|
5692
|
+
- \u5B9E\u73B0\u5B8C\u6210\u540E\u8BA9\u8BBE\u8BA1\u65B9\u56DE\u5BA1\uFF0C\u53EA\u67E5 diff\u3001\u4E0D\u53D8\u91CF\u4E0E\u56DE\u5F52\uFF0C\u4E0D\u63D0\u98CE\u683C\u504F\u597D\u3002
|
|
5693
|
+
|
|
5694
|
+
\u3010\u5E76\u884C\u3011
|
|
5695
|
+
- \u53EA\u6709\u72EC\u7ACB\u5DE5\u4F5C\u533A\u624D\u80FD\u5E76\u884C\uFF1B\u540C\u4E00\u5DE5\u4F5C\u533A\u5185\u7684\u5199\u5165\u4E0E\u63D0\u4EA4\u5FC5\u987B\u4E32\u884C\u3002
|
|
5696
|
+
- \u8DE8\u53D8\u4F53\u642C\u8FD0\u6539\u52A8\u65F6\u53EA\u642C\u6539\u52A8\u672C\u8EAB\uFF1A\u5265\u79BB\u6765\u6E90\u5206\u652F\u7684\u9644\u5E26\u4F9D\u8D56\uFF0C\u5408\u6D41\u4EE5\u76EE\u6807\u57FA\u5EA7\u7684\u8BED\u4E49\u4E3A\u51C6\u3002
|
|
5697
|
+
|
|
5698
|
+
\u3010\u5224\u65AD\u4E0E\u5904\u7F6E\u3011
|
|
5699
|
+
- \u4E0D\u8981\u51ED\u300C\u6CA1\u6709\u65B0\u8F93\u51FA\u300D\u5224\u65AD\u6B7B\u6D3B\u3002\u5148\u53D6\u8BC1\uFF1A\u6267\u884C\u75D5\u8FF9\u6700\u540E\u4E00\u6761\u662F\u4EC0\u4E48\u3001\u6709\u6CA1\u6709\u60AC\u7A7A\u7684\u8C03\u7528\u3001\u8D44\u6E90\u5360\u7528\u662F\u5426\u5728\u589E\u957F\uFF0C\u518D\u51B3\u5B9A\u7B49\u5F85\u3001\u62C6\u5C0F\u91CD\u6D3E\u8FD8\u662F\u53D6\u6D88\u3002
|
|
5700
|
+
- \u5DF2\u7ECF\u9677\u5165\u5FAA\u73AF\u7684\uFF08\u53CD\u590D\u88AB max_tokens \u622A\u65AD\u3001\u957F\u65F6\u95F4\u65E0\u4EA7\u51FA\uFF09\u7B49\u4E0D\u51FA\u6765\uFF0C\u53CA\u65F6\u62C6\u5C0F\u6216\u6362\u7B56\u7565\u3002
|
|
5701
|
+
- \u8FD0\u884C\u65F6\u7684\u4EA4\u4E92\u8BED\u4E49\u662F\u7248\u672C\u76F8\u5173\u7684\uFF08\u4F8B\u5982\u4E2D\u9014\u6D88\u606F\u662F\u6253\u65AD\u5F53\u524D\u56DE\u5408\u8FD8\u662F\u5728\u8FB9\u754C\u4EA4\u4ED8\uFF09\uFF0C\u4F9D\u8D56\u524D\u5148\u5728\u5F53\u524D\u7248\u672C\u5B9E\u6D4B\u3002
|
|
5702
|
+
|
|
5703
|
+
\u3010\u9A8C\u8BC1\u3011
|
|
5704
|
+
- \u6BCF\u6B21\u6539\u52A8\u8DD1\u540C\u4E00\u5957\u95E8\u69DB\uFF08\u7C7B\u578B\u68C0\u67E5\u3001lint\u3001\u6D4B\u8BD5\u3001\u6784\u5EFA\uFF09\uFF0C\u8303\u56F4\u968F\u5F71\u54CD\u9762\u4F38\u7F29\uFF1A\u5C0F\u6539\u8DD1\u76F8\u5173\u5B50\u96C6\uFF0C\u5927\u6539\u8DD1\u5168\u91CF\u3002
|
|
5705
|
+
- \u91CD\u6392\u5386\u53F2\u3001\u8DE8\u5206\u652F\u642C\u8FD0\u540E\uFF0C\u7528\u7B26\u53F7\u68C0\u7D22\u4E0E tree hash \u8FD9\u7C7B\u673A\u68B0\u8BC1\u636E\u9A8C\u8BC1\u7B49\u4EF7\uFF0C\u4E0D\u8981\u8089\u773C\u590D\u67E5\u3002
|
|
5706
|
+
- \u4EA4\u4E92\u7C7B\u6539\u52A8\u8981\u6709\u4E00\u6B21\u771F\u5B9E\u8FD0\u884C\u7684\u7AEF\u5230\u7AEF\u590D\u6F14\uFF1B\u5355\u6D4B\u5168\u7EFF\u4E0D\u7B49\u4E8E\u5B8C\u6210\u3002
|
|
5707
|
+
|
|
5708
|
+
\u3010\u9519\u8BEF\u6A21\u5F0F\u3011
|
|
5709
|
+
- \u6DF7\u6742\u6D3E\u5DE5 \u2014\u2014 \u628A\u8BBE\u8BA1\u3001\u5B9E\u73B0\u3001\u6D4B\u8BD5\u585E\u8FDB\u4E00\u4E2A\u4EFB\u52A1\uFF0C\u7ED3\u679C\u957F\u65F6\u95F4\u7A7A\u8F6C\u3001\u6700\u7EC8\u4EA4\u4ED8\u4E3A\u7A7A\u3002
|
|
5710
|
+
- \u540C\u533A\u4E92\u5377 \u2014\u2014 \u8BA9\u4E24\u4E2A\u6267\u884C\u8005\u5728\u540C\u4E00\u5DE5\u4F5C\u533A\u5E76\u53D1\u63D0\u4EA4\uFF0C\u4E92\u76F8\u628A\u5BF9\u65B9\u7684\u6539\u52A8\u5377\u8FDB\u81EA\u5DF1\u7684 commit\u3002
|
|
5711
|
+
- \u9759\u9ED8\u5224\u6B7B \u2014\u2014 \u51ED\u8F93\u51FA\u9759\u9ED8\u5224\u65AD\u6B7B\u6D3B\uFF1A\u628A\u5728\u8DD1\u957F\u4EFB\u52A1\u5F53\u6210\u5361\u6B7B\u6740\u6389\uFF0C\u6216\u628A\u6D3B\u9501\u5F53\u6210\u6162\u4EFB\u52A1\u6B7B\u7B49\u3002
|
|
5712
|
+
- \u7EFF\u706F\u5373\u5B8C\u6210 \u2014\u2014 \u5355\u6D4B\u6216\u6D41\u7A0B\u5168\u7EFF\u5C31\u5BA3\u5E03\u5B8C\u6210\uFF1B\u5076\u53D1\u5931\u8D25\u590D\u8DD1\u4E00\u6B21\u901A\u8FC7\u5C31\u5F53\u6CA1\u53D1\u751F\uFF0C\u4E0D\u505A\u8BB0\u5F55\u3002
|
|
5713
|
+
- \u96F6\u788E\u53D6\u8BC1 \u2014\u2014 \u628A\u4E00\u6B21\u53D6\u8BC1\u62C6\u6210\u51E0\u5341\u6761\u96F6\u788E\u53EA\u8BFB\u547D\u4EE4\u9010\u6761\u7B49\u6279\u51C6\uFF0C\u6392\u969C\u88AB\u6D41\u7A0B\u672C\u8EAB\u62D6\u6B7B\u3002`
|
|
5714
|
+
);
|
|
4903
5715
|
var HARD_CONSTRAINTS = section(
|
|
4904
5716
|
"Hard constraints",
|
|
4905
5717
|
`- NEVER execute commands or write files yourself. Delegate via add_task\uFF08worker will execute\uFF09.
|
|
@@ -4925,6 +5737,7 @@ function buildControllerSystemPrompt(ctx, skills) {
|
|
|
4925
5737
|
TOOL_USAGE,
|
|
4926
5738
|
PLANNING_TOOLS,
|
|
4927
5739
|
SIGNAL_HANDLING,
|
|
5740
|
+
WORKING_PRINCIPLES,
|
|
4928
5741
|
HARD_CONSTRAINTS,
|
|
4929
5742
|
buildWorkerList(ctx.config.workers),
|
|
4930
5743
|
buildSkillCatalog(skills),
|
|
@@ -4932,6 +5745,54 @@ function buildControllerSystemPrompt(ctx, skills) {
|
|
|
4932
5745
|
);
|
|
4933
5746
|
}
|
|
4934
5747
|
|
|
5748
|
+
// src/services/agents/prompts/project-rules.ts
|
|
5749
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
5750
|
+
import { basename as basename2, dirname as dirname4, join as join9, resolve as resolve2 } from "path";
|
|
5751
|
+
var RULE_FILE_NAMES = ["AGENTS.md", "CLAUDE.md"];
|
|
5752
|
+
var MAX_LEVELS = 5;
|
|
5753
|
+
var PROJECT_RULES_MAX_LENGTH = 8e3;
|
|
5754
|
+
function findProjectRules(startDir) {
|
|
5755
|
+
let current = resolve2(startDir);
|
|
5756
|
+
for (let level = 0; level < MAX_LEVELS; level++) {
|
|
5757
|
+
const found = readRuleFile(current);
|
|
5758
|
+
if (found) return found;
|
|
5759
|
+
if (existsSync6(join9(current, ".git"))) break;
|
|
5760
|
+
const parent = dirname4(current);
|
|
5761
|
+
if (parent === current) break;
|
|
5762
|
+
current = parent;
|
|
5763
|
+
}
|
|
5764
|
+
return null;
|
|
5765
|
+
}
|
|
5766
|
+
function readRuleFile(dir) {
|
|
5767
|
+
for (const name of RULE_FILE_NAMES) {
|
|
5768
|
+
const path7 = join9(dir, name);
|
|
5769
|
+
try {
|
|
5770
|
+
if (!statSync2(path7).isFile()) continue;
|
|
5771
|
+
const content = readFileSync6(path7, "utf8");
|
|
5772
|
+
if (!content.trim()) continue;
|
|
5773
|
+
return { path: path7, content };
|
|
5774
|
+
} catch {
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
return null;
|
|
5778
|
+
}
|
|
5779
|
+
function buildProjectRulesSection(rules) {
|
|
5780
|
+
if (!rules) return "";
|
|
5781
|
+
const name = basename2(rules.path);
|
|
5782
|
+
const truncated = rules.content.length > PROJECT_RULES_MAX_LENGTH;
|
|
5783
|
+
const content = truncated ? `${rules.content.slice(0, PROJECT_RULES_MAX_LENGTH).trimEnd()}
|
|
5784
|
+
|
|
5785
|
+
...truncated (rules file exceeded ${PROJECT_RULES_MAX_LENGTH} characters)` : rules.content.trimEnd();
|
|
5786
|
+
return section(
|
|
5787
|
+
`Project rules (${name})`,
|
|
5788
|
+
`Rules from this project's ${name}. They apply to all work on the task \u2014 follow them unless the task description explicitly overrides them.
|
|
5789
|
+
|
|
5790
|
+
Source: ${rules.path}
|
|
5791
|
+
|
|
5792
|
+
${content}`
|
|
5793
|
+
);
|
|
5794
|
+
}
|
|
5795
|
+
|
|
4935
5796
|
// src/services/agents/prompts/worker-prompt.ts
|
|
4936
5797
|
var IDENTITY2 = section(
|
|
4937
5798
|
"Identity",
|
|
@@ -5083,6 +5944,10 @@ function buildWorkerSystemPrompt(ctx, skills) {
|
|
|
5083
5944
|
TOOL_USAGE2,
|
|
5084
5945
|
EXECUTION_STRATEGY,
|
|
5085
5946
|
PROJECT_CONVENTIONS,
|
|
5947
|
+
// Project rules discovered near the task workspace (AGENTS.md, falling
|
|
5948
|
+
// back to CLAUDE.md). Empty when no rules file exists — assemble() drops
|
|
5949
|
+
// the section, so no empty paragraph leaks into the prompt.
|
|
5950
|
+
buildProjectRulesSection(findProjectRules(ctx.workspaceDir ?? process.cwd())),
|
|
5086
5951
|
QUALITY_RULES,
|
|
5087
5952
|
HARD_CONSTRAINTS2,
|
|
5088
5953
|
CONTROLLER_COMMUNICATION,
|
|
@@ -5261,6 +6126,15 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
5261
6126
|
getState() {
|
|
5262
6127
|
return this.state;
|
|
5263
6128
|
}
|
|
6129
|
+
/**
|
|
6130
|
+
* Live run flags (issue #16). A puppet worker has no SDK result/stream
|
|
6131
|
+
* concept: `resultReceived` means the model already called done/fail, and
|
|
6132
|
+
* `streamEnded` is not tracked (null), so status readers fall back to the
|
|
6133
|
+
* transcript / task state instead of guessing.
|
|
6134
|
+
*/
|
|
6135
|
+
getLiveStatus() {
|
|
6136
|
+
return { resultReceived: this._terminalResult !== null, streamEnded: null };
|
|
6137
|
+
}
|
|
5264
6138
|
/** Get conversation entries. */
|
|
5265
6139
|
getConversation() {
|
|
5266
6140
|
return this.state.get("conversation") ?? [];
|
|
@@ -5272,7 +6146,11 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
5272
6146
|
getSnapshot(path7) {
|
|
5273
6147
|
return this.state.get(path7);
|
|
5274
6148
|
}
|
|
5275
|
-
/**
|
|
6149
|
+
/**
|
|
6150
|
+
* Accept a message from the controller — writes to internal inbox. The
|
|
6151
|
+
* puppet reads it at its next LLM step, so a running (or idle) worker
|
|
6152
|
+
* always accepts.
|
|
6153
|
+
*/
|
|
5276
6154
|
acceptMessage(message) {
|
|
5277
6155
|
this.state.append("inbox", {
|
|
5278
6156
|
id: generateId("msg"),
|
|
@@ -5280,6 +6158,7 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
5280
6158
|
timestamp: Date.now()
|
|
5281
6159
|
});
|
|
5282
6160
|
this.logger.info({ workerId: this.id }, "Controller message accepted");
|
|
6161
|
+
return true;
|
|
5283
6162
|
}
|
|
5284
6163
|
/** Unread controller messages (non-destructive) — engine reports these when the run ends. */
|
|
5285
6164
|
pendingMessages() {
|
|
@@ -5312,11 +6191,61 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
5312
6191
|
message: { role: "user", content: controllerMessageContent(content) }
|
|
5313
6192
|
}, turnEnv));
|
|
5314
6193
|
}
|
|
5315
|
-
};
|
|
5316
|
-
|
|
5317
|
-
// src/services/agents/claude-code-worker.ts
|
|
5318
|
-
import { query, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
5319
|
-
import { z as z2 } from "zod";
|
|
6194
|
+
};
|
|
6195
|
+
|
|
6196
|
+
// src/services/agents/claude-code-worker.ts
|
|
6197
|
+
import { query, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
6198
|
+
import { z as z2 } from "zod";
|
|
6199
|
+
|
|
6200
|
+
// src/services/agents/utils/claude-tool-records.ts
|
|
6201
|
+
var CLAUDE_TOOL_RESULT_STORE_CHARS = 2e3;
|
|
6202
|
+
var CLAUDE_TOOL_ARGS_STORE_CHARS = 8e3;
|
|
6203
|
+
function truncateForStorage(text, maxChars) {
|
|
6204
|
+
if (text.length <= maxChars) return text;
|
|
6205
|
+
return `${text.slice(0, maxChars)}... [truncated, ${text.length} chars]`;
|
|
6206
|
+
}
|
|
6207
|
+
function toolUseBlockToRecord(block, startedAt) {
|
|
6208
|
+
return {
|
|
6209
|
+
toolCallId: block.id,
|
|
6210
|
+
toolName: block.name,
|
|
6211
|
+
arguments: truncateForStorage(toolPayloadText(block.input) || "{}", CLAUDE_TOOL_ARGS_STORE_CHARS),
|
|
6212
|
+
status: "running",
|
|
6213
|
+
startedAt
|
|
6214
|
+
};
|
|
6215
|
+
}
|
|
6216
|
+
function flattenToolResultContent(content) {
|
|
6217
|
+
if (content === void 0 || content === null) return "";
|
|
6218
|
+
if (typeof content === "string") return content;
|
|
6219
|
+
if (!Array.isArray(content)) return toolPayloadText(content);
|
|
6220
|
+
const parts = [];
|
|
6221
|
+
for (const block of content) {
|
|
6222
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
6223
|
+
const b = block;
|
|
6224
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
6225
|
+
parts.push(b.text);
|
|
6226
|
+
} else {
|
|
6227
|
+
parts.push(`[${String(b.type)} block]`);
|
|
6228
|
+
}
|
|
6229
|
+
} else if (typeof block === "string") {
|
|
6230
|
+
parts.push(block);
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
return parts.join("\n");
|
|
6234
|
+
}
|
|
6235
|
+
function toolResultBlockToRecord(block, toolName, endedAt) {
|
|
6236
|
+
const isError = block.is_error === true;
|
|
6237
|
+
return {
|
|
6238
|
+
toolCallId: block.tool_use_id,
|
|
6239
|
+
toolName,
|
|
6240
|
+
output: {
|
|
6241
|
+
type: "text",
|
|
6242
|
+
value: truncateForStorage(flattenToolResultContent(block.content), CLAUDE_TOOL_RESULT_STORE_CHARS)
|
|
6243
|
+
},
|
|
6244
|
+
...isError && { isError: true, errorType: "ToolError" },
|
|
6245
|
+
status: isError ? "failed" : "succeeded",
|
|
6246
|
+
endedAt
|
|
6247
|
+
};
|
|
6248
|
+
}
|
|
5320
6249
|
|
|
5321
6250
|
// src/infra/blocking-requests.ts
|
|
5322
6251
|
var BlockingRequestMap = class {
|
|
@@ -5329,14 +6258,14 @@ var BlockingRequestMap = class {
|
|
|
5329
6258
|
if (this.pending.has(id)) {
|
|
5330
6259
|
throw new Error(`BlockingRequestMap: duplicate wait id "${id}"`);
|
|
5331
6260
|
}
|
|
5332
|
-
return new Promise((
|
|
6261
|
+
return new Promise((resolve12) => {
|
|
5333
6262
|
const settle = (value) => {
|
|
5334
6263
|
this.pending.delete(id);
|
|
5335
|
-
|
|
6264
|
+
resolve12(value);
|
|
5336
6265
|
};
|
|
5337
6266
|
const timer = setTimeout(() => {
|
|
5338
6267
|
this.pending.delete(id);
|
|
5339
|
-
|
|
6268
|
+
resolve12(makeTimeoutValue());
|
|
5340
6269
|
}, timeoutMs);
|
|
5341
6270
|
if (typeof timer === "object" && timer && "unref" in timer) {
|
|
5342
6271
|
timer.unref();
|
|
@@ -5373,17 +6302,35 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5373
6302
|
logger;
|
|
5374
6303
|
abortController = new AbortController();
|
|
5375
6304
|
_terminalResult = null;
|
|
6305
|
+
/** True once the SDK message stream of the current run has closed (issue #16). */
|
|
6306
|
+
_streamEnded = false;
|
|
5376
6307
|
// --- Controller messaging via streamInput ---
|
|
5377
6308
|
_messageQueue = [];
|
|
5378
6309
|
_messageResolve = null;
|
|
5379
|
-
/** Active SDK query — interrupted to deliver controller messages promptly. */
|
|
5380
|
-
_activeQuery = null;
|
|
5381
6310
|
/**
|
|
5382
|
-
*
|
|
5383
|
-
*
|
|
5384
|
-
*
|
|
6311
|
+
* True while run() is executing. Controller messages are rejected once the
|
|
6312
|
+
* run's input has closed (see `_inputClosed`), but idle multi-stage workers
|
|
6313
|
+
* (no run in flight) keep accepting — the next stage folds them into its
|
|
6314
|
+
* opening prompt.
|
|
6315
|
+
*/
|
|
6316
|
+
_runInFlight = false;
|
|
6317
|
+
/**
|
|
6318
|
+
* True once the prompt iterator reported done for the current run: the
|
|
6319
|
+
* session can no longer take input (a turn result arrived with nothing
|
|
6320
|
+
* queued, or the run was aborted). Further acceptMessage calls fail
|
|
6321
|
+
* explicitly instead of pretending to queue.
|
|
6322
|
+
*/
|
|
6323
|
+
_inputClosed = false;
|
|
6324
|
+
// --- Tool-call recording (see utils/claude-tool-records.ts) ---
|
|
6325
|
+
/**
|
|
6326
|
+
* Assistant steps buffered while their tool results are still in flight.
|
|
6327
|
+
* The SDK stream splits one LLM step across two messages (assistant
|
|
6328
|
+
* tool_use → user tool_result), but the schema stores a step atomically on
|
|
6329
|
+
* one AssistantEntry and storage is append-only — so the entry waits in
|
|
6330
|
+
* memory until its results arrive (or the stream ends). FIFO order is kept:
|
|
6331
|
+
* only the head is flushed, so the file order always matches arrival order.
|
|
5385
6332
|
*/
|
|
5386
|
-
|
|
6333
|
+
_pendingToolSteps = [];
|
|
5387
6334
|
// --- ask_controller via in-process SDK MCP server ---
|
|
5388
6335
|
_askController = null;
|
|
5389
6336
|
// --- Permission control via canUseTool ---
|
|
@@ -5392,31 +6339,17 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5392
6339
|
/** Exposed for engine to resolve pending permission requests. */
|
|
5393
6340
|
permissionHandler = {
|
|
5394
6341
|
resolvePermission: (requestId, approved, reason) => {
|
|
5395
|
-
|
|
6342
|
+
return this._pendingPermissions.resolve(
|
|
5396
6343
|
requestId,
|
|
5397
6344
|
approved ? { behavior: "allow" } : { behavior: "deny", message: reason ?? "Denied by controller" }
|
|
5398
6345
|
);
|
|
5399
|
-
if (ok) this.afterPermissionSettled();
|
|
5400
|
-
return ok;
|
|
5401
6346
|
},
|
|
5402
6347
|
cleanup: () => {
|
|
5403
|
-
this._interruptQueued = false;
|
|
5404
6348
|
this._pendingPermissions.cleanupAll(() => ({ behavior: "deny", message: "Worker aborted" }));
|
|
5405
6349
|
},
|
|
5406
6350
|
wrappedTools: {}
|
|
5407
6351
|
// not used for ClaudeCodeWorker
|
|
5408
6352
|
};
|
|
5409
|
-
/**
|
|
5410
|
-
* A permission settled (approved / denied / timed out). If a controller
|
|
5411
|
-
* message was waiting for the permission window to close, deliver its
|
|
5412
|
-
* interrupt now — this runs on BOTH the resolve and the timeout path.
|
|
5413
|
-
*/
|
|
5414
|
-
afterPermissionSettled() {
|
|
5415
|
-
if (this._interruptQueued && this._pendingPermissions.size === 0) {
|
|
5416
|
-
this._interruptQueued = false;
|
|
5417
|
-
this.requestInterrupt();
|
|
5418
|
-
}
|
|
5419
|
-
}
|
|
5420
6353
|
// Signal context set during run()
|
|
5421
6354
|
_signalQueue = null;
|
|
5422
6355
|
_runContext = null;
|
|
@@ -5463,6 +6396,10 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5463
6396
|
}
|
|
5464
6397
|
async run(task, _tools, onProgress) {
|
|
5465
6398
|
this._terminalResult = null;
|
|
6399
|
+
this._streamEnded = false;
|
|
6400
|
+
this._inputClosed = false;
|
|
6401
|
+
this._runInFlight = true;
|
|
6402
|
+
this._pendingToolSteps = [];
|
|
5466
6403
|
this.state.set("runtimeState.task", { id: task.id, description: task.description });
|
|
5467
6404
|
const turnEnv = this.buildTurnEnv(task);
|
|
5468
6405
|
this._currentTurnEnv = turnEnv;
|
|
@@ -5520,18 +6457,20 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5520
6457
|
prompt: this.buildPromptIterable(task.description),
|
|
5521
6458
|
options
|
|
5522
6459
|
});
|
|
5523
|
-
this._activeQuery = stream;
|
|
5524
6460
|
try {
|
|
5525
6461
|
for await (const message of stream) {
|
|
5526
6462
|
if (this.abortController.signal.aborted) break;
|
|
5527
6463
|
this.processStreamMessage(message, onProgress);
|
|
5528
|
-
if (this._terminalResult) break;
|
|
5529
6464
|
}
|
|
5530
6465
|
} finally {
|
|
5531
|
-
this.
|
|
6466
|
+
this._streamEnded = true;
|
|
6467
|
+
this._inputClosed = true;
|
|
6468
|
+
this.clearPendingProgress();
|
|
6469
|
+
this.flushAllToolSteps();
|
|
5532
6470
|
}
|
|
5533
6471
|
} catch (err) {
|
|
5534
6472
|
this.logger.error({ err, taskId: task.id }, "ClaudeCodeWorker query error");
|
|
6473
|
+
this._inputClosed = true;
|
|
5535
6474
|
if (!this._terminalResult) {
|
|
5536
6475
|
this._terminalResult = {
|
|
5537
6476
|
status: "failed",
|
|
@@ -5558,6 +6497,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5558
6497
|
startedAt: resultTs,
|
|
5559
6498
|
endedAt: resultTs
|
|
5560
6499
|
}, turnEnv));
|
|
6500
|
+
this._runInFlight = false;
|
|
5561
6501
|
return this._terminalResult;
|
|
5562
6502
|
}
|
|
5563
6503
|
/** Set signal context for permission requests. Called by engine before run(). */
|
|
@@ -5568,6 +6508,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5568
6508
|
}
|
|
5569
6509
|
abort() {
|
|
5570
6510
|
this.abortController.abort();
|
|
6511
|
+
this.clearPendingProgress();
|
|
5571
6512
|
this.permissionHandler.cleanup();
|
|
5572
6513
|
this.flushMessageQueue();
|
|
5573
6514
|
}
|
|
@@ -5575,6 +6516,14 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5575
6516
|
getState() {
|
|
5576
6517
|
return this.state;
|
|
5577
6518
|
}
|
|
6519
|
+
/**
|
|
6520
|
+
* Live run flags (issue #16): `resultReceived` flips as soon as the SDK result
|
|
6521
|
+
* message lands, `streamEnded` when the message loop exits. The gap between
|
|
6522
|
+
* them is exactly the #15 window where the controller saw nothing.
|
|
6523
|
+
*/
|
|
6524
|
+
getLiveStatus() {
|
|
6525
|
+
return { resultReceived: this._terminalResult !== null, streamEnded: this._streamEnded };
|
|
6526
|
+
}
|
|
5578
6527
|
/** Get conversation entries. */
|
|
5579
6528
|
getConversation() {
|
|
5580
6529
|
return this.state.get("conversation") ?? [];
|
|
@@ -5586,34 +6535,36 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5586
6535
|
getSnapshot(path7) {
|
|
5587
6536
|
return this.state.get(path7);
|
|
5588
6537
|
}
|
|
5589
|
-
/**
|
|
6538
|
+
/**
|
|
6539
|
+
* Accept a message from the controller. The message is queued and handed
|
|
6540
|
+
* to the SDK only when it pulls the next prompt: the CLI parks pulled
|
|
6541
|
+
* prompts in an internal queue that it drains strictly BETWEEN turns, so
|
|
6542
|
+
* delivery always lands on a turn boundary — a tool call in progress is
|
|
6543
|
+
* never cut off (the v0.6 interrupt delivery did exactly that and killed
|
|
6544
|
+
* the session with an orphaned tool_use; see docs/develop/v0.6-design.md
|
|
6545
|
+
* §6.1 — the hazard is any unclosed tool_use, not just a pending
|
|
6546
|
+
* permission).
|
|
6547
|
+
*
|
|
6548
|
+
* Returns false when delivery is impossible: the run's input has closed
|
|
6549
|
+
* (final result received) or the worker was aborted. Idle multi-stage
|
|
6550
|
+
* workers (no run in flight) always accept — the next stage folds the
|
|
6551
|
+
* message into its opening prompt.
|
|
6552
|
+
*/
|
|
5590
6553
|
acceptMessage(message) {
|
|
6554
|
+
if (this.abortController.signal.aborted) return false;
|
|
6555
|
+
if (this._runInFlight && this._inputClosed) return false;
|
|
5591
6556
|
this._messageQueue.push(message);
|
|
5592
6557
|
if (this._messageResolve) {
|
|
5593
6558
|
this._messageResolve();
|
|
5594
6559
|
this._messageResolve = null;
|
|
5595
6560
|
}
|
|
5596
|
-
if ((this.config.interruptOnMessage ?? "immediate") === "immediate") {
|
|
5597
|
-
this.requestInterrupt();
|
|
5598
|
-
}
|
|
5599
6561
|
this.logger.info({ workerId: this.id }, "Controller message accepted");
|
|
6562
|
+
return true;
|
|
5600
6563
|
}
|
|
5601
6564
|
/** Unread controller messages (non-destructive) — engine reports these when the run ends. */
|
|
5602
6565
|
pendingMessages() {
|
|
5603
6566
|
return [...this._messageQueue];
|
|
5604
6567
|
}
|
|
5605
|
-
/** Interrupt the active query unless a permission is pending (queued until it settles). */
|
|
5606
|
-
requestInterrupt() {
|
|
5607
|
-
if (this.abortController.signal.aborted || !this._activeQuery) return;
|
|
5608
|
-
if (this._pendingPermissions.size > 0) {
|
|
5609
|
-
this._interruptQueued = true;
|
|
5610
|
-
return;
|
|
5611
|
-
}
|
|
5612
|
-
if (typeof this._activeQuery.interrupt !== "function") return;
|
|
5613
|
-
this._activeQuery.interrupt().catch((err) => {
|
|
5614
|
-
this.logger.warn({ workerId: this.id, err: String(err) }, "interrupt() failed");
|
|
5615
|
-
});
|
|
5616
|
-
}
|
|
5617
6568
|
/**
|
|
5618
6569
|
* Provide the engine-backed ask_controller implementation. Once set, the
|
|
5619
6570
|
* worker exposes it to Claude Code as an in-process SDK MCP tool.
|
|
@@ -5651,9 +6602,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5651
6602
|
...this._turnLineage
|
|
5652
6603
|
});
|
|
5653
6604
|
return this._pendingPermissions.wait(requestId, _ClaudeCodeWorker.PERMISSION_TIMEOUT_MS, () => {
|
|
5654
|
-
|
|
5655
|
-
this.afterPermissionSettled();
|
|
5656
|
-
return value;
|
|
6605
|
+
return { behavior: "deny", message: "Permission request timed out" };
|
|
5657
6606
|
});
|
|
5658
6607
|
}
|
|
5659
6608
|
// --- Prompt as AsyncIterable (supports follow-up messages) ---
|
|
@@ -5680,13 +6629,22 @@ ${leftoverText}` : `${buildThirdPartyPrompt(this.skills)}${initialPrompt}`;
|
|
|
5680
6629
|
};
|
|
5681
6630
|
}
|
|
5682
6631
|
while (true) {
|
|
5683
|
-
if (this.abortController.signal.aborted
|
|
6632
|
+
if (this.abortController.signal.aborted) {
|
|
6633
|
+
this._inputClosed = true;
|
|
5684
6634
|
return { value: void 0, done: true };
|
|
5685
6635
|
}
|
|
5686
|
-
if (this.
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
6636
|
+
if (this._terminalResult) {
|
|
6637
|
+
if (this._messageQueue.length === 0) {
|
|
6638
|
+
this._inputClosed = true;
|
|
6639
|
+
return { value: void 0, done: true };
|
|
6640
|
+
}
|
|
6641
|
+
} else if (this._messageQueue.length === 0) {
|
|
6642
|
+
await new Promise((resolve12) => {
|
|
6643
|
+
this._messageResolve = resolve12;
|
|
6644
|
+
});
|
|
6645
|
+
continue;
|
|
6646
|
+
}
|
|
6647
|
+
break;
|
|
5690
6648
|
}
|
|
5691
6649
|
const msg = this._messageQueue.shift();
|
|
5692
6650
|
return {
|
|
@@ -5751,25 +6709,49 @@ controller: ${answer}`,
|
|
|
5751
6709
|
processStreamMessage(message, onProgress) {
|
|
5752
6710
|
if (message.type === "assistant") {
|
|
5753
6711
|
this.handleAssistantMessage(message, onProgress);
|
|
6712
|
+
} else if (message.type === "user") {
|
|
6713
|
+
this.handleUserMessage(message);
|
|
5754
6714
|
} else if (message.type === "result") {
|
|
5755
6715
|
this.handleResultMessage(message);
|
|
5756
6716
|
}
|
|
5757
6717
|
}
|
|
6718
|
+
/** Append one entry to the conversation under the current turn envelope. */
|
|
6719
|
+
appendConversationEntry(entry) {
|
|
6720
|
+
this.state.append("conversation", applyTurnEnvelope(entry, this._currentTurnEnv ?? void 0));
|
|
6721
|
+
}
|
|
6722
|
+
/**
|
|
6723
|
+
* Drop a debounced progress delivery: cancel the timer and the buffered
|
|
6724
|
+
* text. Called when a result supersedes the progress, when the run is
|
|
6725
|
+
* aborted, and when the stream loop exits (all paths — the run is over, so
|
|
6726
|
+
* nothing may be delivered afterwards).
|
|
6727
|
+
*/
|
|
6728
|
+
clearPendingProgress() {
|
|
6729
|
+
if (this._progressTimer) {
|
|
6730
|
+
clearTimeout(this._progressTimer);
|
|
6731
|
+
this._progressTimer = null;
|
|
6732
|
+
}
|
|
6733
|
+
this._pendingProgress = null;
|
|
6734
|
+
}
|
|
5758
6735
|
handleAssistantMessage(msg, onProgress) {
|
|
5759
6736
|
const textParts = [];
|
|
6737
|
+
const toolCalls = [];
|
|
6738
|
+
const ts = nowISO();
|
|
5760
6739
|
for (const block of msg.message.content) {
|
|
5761
6740
|
if (block.type === "text") {
|
|
5762
6741
|
textParts.push(block.text);
|
|
6742
|
+
} else if (block.type === "tool_use") {
|
|
6743
|
+
toolCalls.push(toolUseBlockToRecord(block, ts));
|
|
5763
6744
|
}
|
|
5764
6745
|
}
|
|
5765
6746
|
const text = textParts.join("\n");
|
|
5766
|
-
if (!text) return;
|
|
5767
|
-
|
|
6747
|
+
if (!text && toolCalls.length === 0) return;
|
|
6748
|
+
const entry = {
|
|
5768
6749
|
type: "assistant" /* Assistant */,
|
|
5769
6750
|
uuid: generateId("a"),
|
|
5770
6751
|
parentUuid: null,
|
|
5771
6752
|
sessionId: this.ctx.sessionId,
|
|
5772
|
-
timestamp:
|
|
6753
|
+
timestamp: ts,
|
|
6754
|
+
startedAt: ts,
|
|
5773
6755
|
message: {
|
|
5774
6756
|
role: "assistant",
|
|
5775
6757
|
content: text,
|
|
@@ -5780,26 +6762,112 @@ controller: ${answer}`,
|
|
|
5780
6762
|
cachedInputTokens: msg.message.usage.cache_read_input_tokens
|
|
5781
6763
|
}
|
|
5782
6764
|
}
|
|
5783
|
-
}
|
|
5784
|
-
|
|
5785
|
-
|
|
6765
|
+
},
|
|
6766
|
+
...toolCalls.length > 0 && { toolCalls }
|
|
6767
|
+
};
|
|
6768
|
+
if (this._pendingToolSteps.length > 0 || toolCalls.length > 0) {
|
|
6769
|
+
this._pendingToolSteps.push(entry);
|
|
6770
|
+
} else {
|
|
6771
|
+
entry.endedAt = ts;
|
|
6772
|
+
this.appendConversationEntry(entry);
|
|
6773
|
+
}
|
|
6774
|
+
if (text && onProgress) {
|
|
5786
6775
|
this._pendingProgress = text.slice(0, 200);
|
|
5787
6776
|
if (this._progressTimer) clearTimeout(this._progressTimer);
|
|
5788
6777
|
this._progressTimer = setTimeout(() => {
|
|
5789
|
-
|
|
5790
|
-
|
|
6778
|
+
this._progressTimer = null;
|
|
6779
|
+
if (this._pendingProgress === null || this.abortController.signal.aborted || this._streamEnded || this._terminalResult !== null) {
|
|
5791
6780
|
this._pendingProgress = null;
|
|
6781
|
+
return;
|
|
5792
6782
|
}
|
|
5793
|
-
|
|
6783
|
+
const pending = this._pendingProgress;
|
|
6784
|
+
this._pendingProgress = null;
|
|
6785
|
+
onProgress(pending);
|
|
5794
6786
|
}, _ClaudeCodeWorker.PROGRESS_DEBOUNCE_MS);
|
|
5795
6787
|
}
|
|
5796
6788
|
}
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
6789
|
+
/**
|
|
6790
|
+
* SDK user messages carry the tool_result blocks for the preceding
|
|
6791
|
+
* assistant step's tool_use calls. Each result is merged into the buffered
|
|
6792
|
+
* entry that issued the matching id; completed steps are then written,
|
|
6793
|
+
* oldest first.
|
|
6794
|
+
*
|
|
6795
|
+
* User messages WITHOUT tool_result blocks (echoes of the task prompt or of
|
|
6796
|
+
* controller messages this worker itself injected) are ignored — the task
|
|
6797
|
+
* entry is written by run() and controller messages are audited elsewhere,
|
|
6798
|
+
* so writing them here would duplicate entries.
|
|
6799
|
+
*/
|
|
6800
|
+
handleUserMessage(msg) {
|
|
6801
|
+
const content = msg.message.content;
|
|
6802
|
+
if (!Array.isArray(content)) return;
|
|
6803
|
+
const ts = nowISO();
|
|
6804
|
+
for (const block of content) {
|
|
6805
|
+
if (!block || block.type !== "tool_result") continue;
|
|
6806
|
+
const resultBlock = block;
|
|
6807
|
+
const owner = this._pendingToolSteps.find(
|
|
6808
|
+
(step) => step.toolCalls?.some((tc) => tc.toolCallId === resultBlock.tool_use_id)
|
|
6809
|
+
);
|
|
6810
|
+
if (!owner) {
|
|
6811
|
+
this.logger.debug(
|
|
6812
|
+
{ toolUseId: resultBlock.tool_use_id },
|
|
6813
|
+
"tool_result with no matching pending tool_use \u2014 dropped"
|
|
6814
|
+
);
|
|
6815
|
+
continue;
|
|
6816
|
+
}
|
|
6817
|
+
const call = owner.toolCalls.find((tc) => tc.toolCallId === resultBlock.tool_use_id);
|
|
6818
|
+
const result = toolResultBlockToRecord(resultBlock, call.toolName, ts);
|
|
6819
|
+
(owner.toolResults ??= []).push(result);
|
|
6820
|
+
call.status = result.status;
|
|
6821
|
+
call.endedAt = ts;
|
|
6822
|
+
}
|
|
6823
|
+
this.flushCompletedToolSteps();
|
|
6824
|
+
}
|
|
6825
|
+
/**
|
|
6826
|
+
* Write buffered steps whose tool calls have ALL received results, from the
|
|
6827
|
+
* head of the queue (order-preserving). Steps still missing results stay
|
|
6828
|
+
* buffered — a later user message or the end-of-stream flush resolves them.
|
|
6829
|
+
*/
|
|
6830
|
+
flushCompletedToolSteps() {
|
|
6831
|
+
while (this._pendingToolSteps.length > 0) {
|
|
6832
|
+
const head = this._pendingToolSteps[0];
|
|
6833
|
+
const callIds = new Set(head.toolCalls?.map((tc) => tc.toolCallId) ?? []);
|
|
6834
|
+
const resultIds = new Set(head.toolResults?.map((tr) => tr.toolCallId) ?? []);
|
|
6835
|
+
let complete = true;
|
|
6836
|
+
for (const id of callIds) {
|
|
6837
|
+
if (!resultIds.has(id)) {
|
|
6838
|
+
complete = false;
|
|
6839
|
+
break;
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
if (!complete) break;
|
|
6843
|
+
this._pendingToolSteps.shift();
|
|
6844
|
+
head.endedAt = nowISO();
|
|
6845
|
+
this.appendConversationEntry(head);
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
/**
|
|
6849
|
+
* End-of-stream fallback: write every still-buffered step so nothing is
|
|
6850
|
+
* lost. Tool calls that never received a result (abort, CLI death mid-tool)
|
|
6851
|
+
* are marked 'cancelled' instead of claiming to run forever.
|
|
6852
|
+
*/
|
|
6853
|
+
flushAllToolSteps() {
|
|
6854
|
+
if (this._pendingToolSteps.length === 0) return;
|
|
6855
|
+
const ts = nowISO();
|
|
6856
|
+
for (const entry of this._pendingToolSteps) {
|
|
6857
|
+
const resultIds = new Set(entry.toolResults?.map((tr) => tr.toolCallId) ?? []);
|
|
6858
|
+
for (const tc of entry.toolCalls ?? []) {
|
|
6859
|
+
if (!resultIds.has(tc.toolCallId)) {
|
|
6860
|
+
tc.status = "cancelled";
|
|
6861
|
+
if (tc.endedAt === void 0) tc.endedAt = ts;
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
entry.endedAt = ts;
|
|
6865
|
+
this.appendConversationEntry(entry);
|
|
5802
6866
|
}
|
|
6867
|
+
this._pendingToolSteps = [];
|
|
6868
|
+
}
|
|
6869
|
+
handleResultMessage(msg) {
|
|
6870
|
+
this.clearPendingProgress();
|
|
5803
6871
|
if (msg.subtype === "success") {
|
|
5804
6872
|
this._terminalResult = {
|
|
5805
6873
|
status: "completed",
|
|
@@ -5812,9 +6880,9 @@ controller: ${answer}`,
|
|
|
5812
6880
|
};
|
|
5813
6881
|
}
|
|
5814
6882
|
if (this._messageResolve) {
|
|
5815
|
-
const
|
|
6883
|
+
const resolve12 = this._messageResolve;
|
|
5816
6884
|
this._messageResolve = null;
|
|
5817
|
-
|
|
6885
|
+
resolve12();
|
|
5818
6886
|
}
|
|
5819
6887
|
const prev = this.state.get("runtimeState");
|
|
5820
6888
|
this.state.set("runtimeState", {
|
|
@@ -5954,13 +7022,21 @@ function disposeWorkerTool(deps) {
|
|
|
5954
7022
|
|
|
5955
7023
|
// src/services/tools/get-task-detail.ts
|
|
5956
7024
|
import { z as z7 } from "zod";
|
|
7025
|
+
var OUTPUT_OVERHEAD_CHARS = 1500;
|
|
7026
|
+
var DEFAULT_LIMIT = 10;
|
|
7027
|
+
var DEFAULT_MAX_CHARS = 200;
|
|
5957
7028
|
function getTaskDetailTool(deps) {
|
|
5958
7029
|
return defineTool({
|
|
5959
|
-
description: "Get detailed information about a specific task",
|
|
7030
|
+
description: "Get detailed information about a specific task, including its worker conversation. The conversation comes from the worker's live in-memory state while it runs, and from its on-disk transcript after the worker ended (marked [from transcript]). The conversation is paginated and truncation is always marked explicitly: offset skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination. limit (default 10 entries, counted back from the newest), maxChars (per-entry character cap, default 200) and full=true (complete text, all entries). A machine-readable meta line reports totalEntries / returnedEntries / offset / truncated.",
|
|
5960
7031
|
parameters: z7.object({
|
|
5961
|
-
taskId: z7.string().describe("Task ID")
|
|
7032
|
+
taskId: z7.string().describe("Task ID"),
|
|
7033
|
+
limit: z7.number().int().min(1).optional().describe(`Max entries to return (default ${DEFAULT_LIMIT})`),
|
|
7034
|
+
offset: z7.number().int().min(0).optional().describe("Skip the newest N entries (default 0)"),
|
|
7035
|
+
maxChars: z7.number().int().min(1).optional().describe(`Per-entry character cap (default ${DEFAULT_MAX_CHARS})`),
|
|
7036
|
+
full: z7.boolean().optional().describe("Return complete entry text and all entries (ignores limit/maxChars)")
|
|
5962
7037
|
}),
|
|
5963
|
-
|
|
7038
|
+
// Full reports can be far longer than the default tool-output cap.
|
|
7039
|
+
execute: safeExecute("get_task_detail", async ({ taskId, limit, offset, maxChars, full }) => {
|
|
5964
7040
|
const task = deps.getTask(taskId);
|
|
5965
7041
|
if (!task) return `Task ${taskId} not found`;
|
|
5966
7042
|
const lines = [
|
|
@@ -5975,30 +7051,313 @@ function getTaskDetailTool(deps) {
|
|
|
5975
7051
|
if (task.result) lines.push(`Result: ${task.result}`);
|
|
5976
7052
|
if (task.error) lines.push(`Error: ${task.error}`);
|
|
5977
7053
|
if (task.workerSessionId) lines.push(`Worker: ${task.workerSessionId}`);
|
|
7054
|
+
const isFull = full === true;
|
|
7055
|
+
const requestedLimit = limit ?? DEFAULT_LIMIT;
|
|
7056
|
+
let entries = [];
|
|
7057
|
+
let source = "none";
|
|
7058
|
+
let incompleteTail = false;
|
|
7059
|
+
let readWindowTruncated = false;
|
|
5978
7060
|
if (deps.getWorkerConversation) {
|
|
5979
|
-
|
|
5980
|
-
if (entries.length > 0)
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
7061
|
+
entries = deps.getWorkerConversation(taskId);
|
|
7062
|
+
if (entries.length > 0) source = "memory";
|
|
7063
|
+
}
|
|
7064
|
+
if (entries.length === 0 && deps.readTranscriptWindow) {
|
|
7065
|
+
const diskIds = [task.workerTranscriptId, task.workerSessionId].filter((id) => typeof id === "string");
|
|
7066
|
+
for (const id of [...new Set(diskIds)]) {
|
|
7067
|
+
const win2 = deps.readTranscriptWindow(id, {
|
|
7068
|
+
need: isFull ? void 0 : (offset ?? 0) + requestedLimit
|
|
7069
|
+
});
|
|
7070
|
+
if (win2 && win2.entries.length > 0) {
|
|
7071
|
+
entries = win2.entries;
|
|
7072
|
+
source = "transcript";
|
|
7073
|
+
incompleteTail = win2.incompleteTail;
|
|
7074
|
+
readWindowTruncated = win2.windowTruncated;
|
|
7075
|
+
break;
|
|
5985
7076
|
}
|
|
5986
7077
|
}
|
|
5987
7078
|
}
|
|
7079
|
+
if (entries.length === 0) return lines.join("\n");
|
|
7080
|
+
const perEntryCap = isFull ? null : maxChars ?? DEFAULT_MAX_CHARS;
|
|
7081
|
+
const requested = selectWindow(entries, offset ?? 0, isFull ? entries.length : requestedLimit);
|
|
7082
|
+
const rendered = renderEntriesFitting(
|
|
7083
|
+
requested.entries,
|
|
7084
|
+
requested.startIndex,
|
|
7085
|
+
{ maxChars: perEntryCap, includeToolDetail: isFull },
|
|
7086
|
+
MAX_TRANSCRIPT_RESULT_CHARS - OUTPUT_OVERHEAD_CHARS
|
|
7087
|
+
);
|
|
7088
|
+
const win = shrinkWindow(requested, rendered.dropped);
|
|
7089
|
+
const body = rendered.lines;
|
|
7090
|
+
const base = buildWindowMeta(win, { maxChars: perEntryCap, full: isFull, shortenedEntries: rendered.shortened });
|
|
7091
|
+
const meta = {
|
|
7092
|
+
...base,
|
|
7093
|
+
source,
|
|
7094
|
+
incompleteTail,
|
|
7095
|
+
readWindowTruncated,
|
|
7096
|
+
windowTruncated: base.windowTruncated || readWindowTruncated,
|
|
7097
|
+
truncated: base.truncated || readWindowTruncated
|
|
7098
|
+
};
|
|
7099
|
+
lines.push("");
|
|
7100
|
+
let header = buildTranscriptHeader(meta);
|
|
7101
|
+
if (readWindowTruncated) {
|
|
7102
|
+
header = `Conversation (${meta.totalEntries} entries in read window, showing ${meta.startIndex}-${meta.endIndex}; older entries exist beyond the read-window cap)`;
|
|
7103
|
+
}
|
|
7104
|
+
lines.push(`${header}${source === "transcript" ? " [from transcript]" : ""}`);
|
|
7105
|
+
lines.push(...body);
|
|
7106
|
+
if (incompleteTail) {
|
|
7107
|
+
lines.push("[note: last entry is incomplete \u2014 the transcript is still being written]");
|
|
7108
|
+
}
|
|
7109
|
+
if (readWindowTruncated) {
|
|
7110
|
+
lines.push("[note: transcript read window hit its byte cap \u2014 older entries were not read; page back with offset/limit]");
|
|
7111
|
+
}
|
|
7112
|
+
const notice = buildTruncationNotice(meta);
|
|
7113
|
+
if (notice) lines.push(notice);
|
|
7114
|
+
lines.push(`[conversation-meta] ${JSON.stringify(meta)}`);
|
|
5988
7115
|
return lines.join("\n");
|
|
5989
|
-
})
|
|
7116
|
+
}, { maxResultChars: MAX_TRANSCRIPT_RESULT_CHARS })
|
|
5990
7117
|
});
|
|
5991
7118
|
}
|
|
5992
7119
|
|
|
5993
|
-
// src/services/tools/
|
|
7120
|
+
// src/services/tools/read-worker-conversation.ts
|
|
5994
7121
|
import { z as z8 } from "zod";
|
|
7122
|
+
var OUTPUT_OVERHEAD_CHARS2 = 1500;
|
|
7123
|
+
var DEFAULT_LIMIT2 = 20;
|
|
7124
|
+
var DEFAULT_MAX_CHARS2 = 200;
|
|
7125
|
+
var FILTER_TYPES = ["user", "assistant", "system", "signal", "tool"];
|
|
7126
|
+
function typeMatches(entry, filter) {
|
|
7127
|
+
if (filter === "tool") {
|
|
7128
|
+
const calls = entry.toolCalls;
|
|
7129
|
+
return Array.isArray(calls) && calls.length > 0;
|
|
7130
|
+
}
|
|
7131
|
+
return entry.type === filter;
|
|
7132
|
+
}
|
|
7133
|
+
function readWorkerConversationTool(deps) {
|
|
7134
|
+
return defineTool({
|
|
7135
|
+
description: `Read a worker's conversation transcript. Layered source: while the engine still tracks the worker (running or idle), its live in-memory conversation is returned (meta source="memory"); once the worker finished/failed/was cancelled or disposed, the append-only on-disk file <sessionDir>/workers/<transcript id>/conversation.jsonl is read instead (meta source="disk") \u2014 so it works for running AND completed/failed/cancelled workers. Pagination: offset skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination. limit (default 20, max entries), maxChars (per-entry cap, default 200), full=true (complete text, all entries \u2014 still bounded by a 2 MB read window), type (filter: user/assistant/system/signal/tool). Truncation is always marked explicitly and reported as totalEntries / returnedEntries / offset / truncated, plus incompleteTail (last entry still being written) and readWindowTruncated (older entries exist beyond the read-window cap \u2014 page back with offset).`,
|
|
7136
|
+
parameters: z8.object({
|
|
7137
|
+
workerId: z8.string().describe("Worker ID (e.g. w-1793587065421-6h37)"),
|
|
7138
|
+
limit: z8.number().int().min(1).optional().describe(`Max entries to return (default ${DEFAULT_LIMIT2})`),
|
|
7139
|
+
offset: z8.number().int().min(0).optional().describe("Skip the newest N entries (default 0)"),
|
|
7140
|
+
maxChars: z8.number().int().min(1).optional().describe(`Per-entry character cap (default ${DEFAULT_MAX_CHARS2})`),
|
|
7141
|
+
full: z8.boolean().optional().describe("Return complete entry text and all entries (ignores limit/maxChars)"),
|
|
7142
|
+
type: z8.enum(FILTER_TYPES).optional().describe('Only entries of this type; "tool" = assistant entries that made tool calls')
|
|
7143
|
+
}),
|
|
7144
|
+
execute: safeExecute("read_worker_conversation", async ({ workerId, limit, offset, maxChars, full, type }) => {
|
|
7145
|
+
const now = deps.now?.() ?? Date.now();
|
|
7146
|
+
if (!isValidWorkerId(workerId)) {
|
|
7147
|
+
return `Invalid worker id "${workerId}" \u2014 worker ids are alphanumeric with dashes/underscores only.`;
|
|
7148
|
+
}
|
|
7149
|
+
const task = deps.listTasks?.().find((t) => t.workerSessionId === workerId);
|
|
7150
|
+
const isActive = deps.listWorkers?.().some((w) => w.workerId === workerId) ?? false;
|
|
7151
|
+
const liveTranscriptId = deps.getWorkerTranscriptId?.(workerId);
|
|
7152
|
+
const isFull = full === true;
|
|
7153
|
+
const requestedLimit = limit ?? DEFAULT_LIMIT2;
|
|
7154
|
+
const need = isFull ? void 0 : (offset ?? 0) + requestedLimit;
|
|
7155
|
+
const matches = type === void 0 ? void 0 : (e) => typeMatches(e, type);
|
|
7156
|
+
const memory = deps.getWorkerConversationByWorkerId?.(workerId);
|
|
7157
|
+
if (memory !== void 0 && memory.length > 0) {
|
|
7158
|
+
return render2({ source: "memory", entries: memory });
|
|
7159
|
+
}
|
|
7160
|
+
const candidates = [];
|
|
7161
|
+
for (const id of [task?.workerTranscriptId, liveTranscriptId, workerId]) {
|
|
7162
|
+
if (typeof id === "string" && !candidates.includes(id)) candidates.push(id);
|
|
7163
|
+
}
|
|
7164
|
+
let disk = null;
|
|
7165
|
+
for (const id of candidates) {
|
|
7166
|
+
const win = deps.readTranscriptWindow(id, { need, matches });
|
|
7167
|
+
if (win !== null) {
|
|
7168
|
+
disk = { id, win };
|
|
7169
|
+
break;
|
|
7170
|
+
}
|
|
7171
|
+
}
|
|
7172
|
+
if (disk !== null) {
|
|
7173
|
+
return render2({
|
|
7174
|
+
source: "disk",
|
|
7175
|
+
entries: disk.win.entries,
|
|
7176
|
+
transcriptId: disk.id,
|
|
7177
|
+
incompleteTail: disk.win.incompleteTail,
|
|
7178
|
+
readWindowTruncated: disk.win.windowTruncated
|
|
7179
|
+
});
|
|
7180
|
+
}
|
|
7181
|
+
const known = isActive || task !== void 0 || memory !== void 0;
|
|
7182
|
+
if (!known) {
|
|
7183
|
+
return `Unknown worker ${workerId}: no active worker and no task references it.`;
|
|
7184
|
+
}
|
|
7185
|
+
if (isActive) {
|
|
7186
|
+
return `Worker ${workerId} is running but has not written any transcript entries yet.`;
|
|
7187
|
+
}
|
|
7188
|
+
const resolved = task?.workerTranscriptId ?? liveTranscriptId;
|
|
7189
|
+
if (resolved !== void 0) {
|
|
7190
|
+
return `Worker ${workerId} exists but its transcript directory is missing on disk (expected ${workerTranscriptPath(deps.sessionDir, resolved)}).`;
|
|
7191
|
+
}
|
|
7192
|
+
return `Worker ${workerId} exists but its transcript cannot be located on disk (legacy session id layout?).`;
|
|
7193
|
+
function render2(src) {
|
|
7194
|
+
const { source, entries } = src;
|
|
7195
|
+
const incompleteTail = src.incompleteTail ?? false;
|
|
7196
|
+
const readWindowTruncated = src.readWindowTruncated ?? false;
|
|
7197
|
+
if (entries.length === 0) {
|
|
7198
|
+
const emptyMsg = `Transcript for worker ${workerId} exists but contains no complete entries yet.`;
|
|
7199
|
+
return incompleteTail ? `${emptyMsg}
|
|
7200
|
+
[note: last entry is incomplete \u2014 the transcript is still being written]` : emptyMsg;
|
|
7201
|
+
}
|
|
7202
|
+
const filter = type;
|
|
7203
|
+
const population = filter === void 0 ? entries : entries.filter((e) => typeMatches(e, filter));
|
|
7204
|
+
const reqLimit = isFull ? population.length : requestedLimit;
|
|
7205
|
+
const perEntryCap = isFull ? null : maxChars ?? DEFAULT_MAX_CHARS2;
|
|
7206
|
+
const requested = selectWindow(population, offset ?? 0, reqLimit);
|
|
7207
|
+
const rendered = renderEntriesFitting(
|
|
7208
|
+
requested.entries,
|
|
7209
|
+
requested.startIndex,
|
|
7210
|
+
{ maxChars: perEntryCap, includeToolDetail: isFull },
|
|
7211
|
+
MAX_TRANSCRIPT_RESULT_CHARS - OUTPUT_OVERHEAD_CHARS2
|
|
7212
|
+
);
|
|
7213
|
+
const win = shrinkWindow(requested, rendered.dropped);
|
|
7214
|
+
const body = rendered.lines;
|
|
7215
|
+
const base = buildWindowMeta(win, {
|
|
7216
|
+
maxChars: perEntryCap,
|
|
7217
|
+
full: isFull,
|
|
7218
|
+
shortenedEntries: rendered.shortened
|
|
7219
|
+
});
|
|
7220
|
+
const meta = {
|
|
7221
|
+
workerId,
|
|
7222
|
+
source,
|
|
7223
|
+
...src.transcriptId !== void 0 && { transcriptId: src.transcriptId },
|
|
7224
|
+
...base,
|
|
7225
|
+
incompleteTail,
|
|
7226
|
+
readWindowTruncated,
|
|
7227
|
+
windowTruncated: base.windowTruncated || readWindowTruncated,
|
|
7228
|
+
truncated: base.truncated || readWindowTruncated,
|
|
7229
|
+
...filter !== void 0 && { filter, totalEntriesBeforeFilter: entries.length }
|
|
7230
|
+
};
|
|
7231
|
+
const summary = summarizeTranscript(entries);
|
|
7232
|
+
const headLine = source === "disk" ? `Worker ${workerId} transcript (source=disk, dir id ${src.transcriptId}): ` + workerTranscriptPath(deps.sessionDir, src.transcriptId ?? workerId) : `Worker ${workerId} conversation (source=memory \u2014 live in-memory state of the tracked worker)`;
|
|
7233
|
+
const lines = [
|
|
7234
|
+
headLine,
|
|
7235
|
+
`Last activity: ${formatLastActivity(summary.lastActivityAt, now)}; currentTool: ${summary.currentTool ?? "-"}; terminal marker: ${summary.terminalSubtype ?? "-"}`
|
|
7236
|
+
];
|
|
7237
|
+
let header = buildTranscriptHeader(meta);
|
|
7238
|
+
if (readWindowTruncated) {
|
|
7239
|
+
header = `Conversation (${meta.totalEntries} entries in read window, showing ${meta.startIndex}-${meta.endIndex}; older entries exist beyond the read-window cap):`;
|
|
7240
|
+
}
|
|
7241
|
+
lines.push(header + (filter !== void 0 ? ` [type=${filter}; ${entries.length} entries in transcript]` : ""));
|
|
7242
|
+
lines.push(...body);
|
|
7243
|
+
if (incompleteTail) {
|
|
7244
|
+
lines.push("[note: last entry is incomplete \u2014 the transcript is still being written]");
|
|
7245
|
+
}
|
|
7246
|
+
if (readWindowTruncated) {
|
|
7247
|
+
lines.push("[note: transcript read window hit its byte cap \u2014 older entries were not read; page back with offset/limit]");
|
|
7248
|
+
}
|
|
7249
|
+
const notice = buildTruncationNotice(meta);
|
|
7250
|
+
if (notice) lines.push(notice);
|
|
7251
|
+
lines.push(`[transcript-meta] ${JSON.stringify(meta)}`);
|
|
7252
|
+
return lines.join("\n");
|
|
7253
|
+
}
|
|
7254
|
+
}, { maxResultChars: MAX_TRANSCRIPT_RESULT_CHARS })
|
|
7255
|
+
});
|
|
7256
|
+
}
|
|
7257
|
+
|
|
7258
|
+
// src/services/tools/get-worker-status.ts
|
|
7259
|
+
import { z as z9 } from "zod";
|
|
7260
|
+
var SETTLED_TASK_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
7261
|
+
function taskStatusToWorkerStatus(status) {
|
|
7262
|
+
if (status === "running") return "running";
|
|
7263
|
+
if (SETTLED_TASK_STATUSES.has(status)) return status;
|
|
7264
|
+
return "unknown";
|
|
7265
|
+
}
|
|
7266
|
+
function buildSnapshot(workerId, deps, now) {
|
|
7267
|
+
const active = deps.listWorkers().find((w) => w.workerId === workerId);
|
|
7268
|
+
const tasks = deps.listTasks();
|
|
7269
|
+
const task = active ? tasks.find((t) => t.id === active.taskId) : tasks.find((t) => t.workerSessionId === workerId);
|
|
7270
|
+
const entries = deps.readTranscript(workerId);
|
|
7271
|
+
if (!active && !task && entries === null) return null;
|
|
7272
|
+
const summary = summarizeTranscript(entries ?? []);
|
|
7273
|
+
const live = deps.getLiveStatus?.(workerId);
|
|
7274
|
+
const workerType = active?.workerType ?? task?.workerType ?? "unknown";
|
|
7275
|
+
const settled = task ? SETTLED_TASK_STATUSES.has(task.status) : false;
|
|
7276
|
+
const terminalTracked = workerType !== "puppet";
|
|
7277
|
+
const inferredTerminal = summary.hasTerminalEntry || settled;
|
|
7278
|
+
let runningForMs = null;
|
|
7279
|
+
if (active) {
|
|
7280
|
+
runningForMs = Math.max(0, now - active.startedAt);
|
|
7281
|
+
} else if (task?.startedAt) {
|
|
7282
|
+
runningForMs = Math.max(0, (task.completedAt ?? now) - task.startedAt);
|
|
7283
|
+
}
|
|
7284
|
+
return {
|
|
7285
|
+
workerId,
|
|
7286
|
+
type: workerType,
|
|
7287
|
+
taskId: active?.taskId ?? task?.id ?? null,
|
|
7288
|
+
status: active?.status ?? (task ? taskStatusToWorkerStatus(task.status) : "unknown"),
|
|
7289
|
+
runningForMs,
|
|
7290
|
+
lastActivityAt: summary.lastActivityAt,
|
|
7291
|
+
lastActivityAgoMs: summary.lastActivityAt === null ? null : Math.max(0, now - summary.lastActivityAt),
|
|
7292
|
+
currentTool: summary.currentTool,
|
|
7293
|
+
streamEnded: live?.streamEnded ?? (inferredTerminal ? true : terminalTracked ? false : null),
|
|
7294
|
+
resultReceived: live?.resultReceived ?? (inferredTerminal ? true : terminalTracked ? false : null),
|
|
7295
|
+
idleSince: active?.status === "idle" ? active.idleSinceMs ?? null : null,
|
|
7296
|
+
live: active !== void 0,
|
|
7297
|
+
lastEntryType: summary.lastEntryType,
|
|
7298
|
+
entries: summary.totalEntries
|
|
7299
|
+
};
|
|
7300
|
+
}
|
|
7301
|
+
function boolLabel(v) {
|
|
7302
|
+
return v === null ? "unknown" : String(v);
|
|
7303
|
+
}
|
|
7304
|
+
function formatSnapshot(s, now) {
|
|
7305
|
+
return [
|
|
7306
|
+
`Worker ${s.workerId} (${s.type})`,
|
|
7307
|
+
` task: ${s.taskId ?? "-"}`,
|
|
7308
|
+
` status: ${s.status}${s.live ? " (tracked)" : " (no longer tracked)"}`,
|
|
7309
|
+
` runningForMs: ${s.runningForMs === null ? "unknown" : `${s.runningForMs} (${formatDuration(s.runningForMs)})`}`,
|
|
7310
|
+
` lastActivityAt: ${formatLastActivity(s.lastActivityAt, now)}`,
|
|
7311
|
+
` currentTool: ${s.currentTool ?? "-"}`,
|
|
7312
|
+
` streamEnded: ${boolLabel(s.streamEnded)}`,
|
|
7313
|
+
` resultReceived: ${boolLabel(s.resultReceived)}`,
|
|
7314
|
+
` idleSince: ${s.idleSince === null ? "-" : formatLastActivity(s.idleSince, now)}`,
|
|
7315
|
+
` entries: ${s.entries} (last: ${s.lastEntryType ?? "-"})`
|
|
7316
|
+
];
|
|
7317
|
+
}
|
|
7318
|
+
function formatSnapshotLine(s) {
|
|
7319
|
+
const activity = s.lastActivityAgoMs === null ? "none" : `${formatDuration(s.lastActivityAgoMs)} ago`;
|
|
7320
|
+
return ` [${s.status}] ${s.workerId} (${s.type}) task=${s.taskId ?? "-"} runningFor=${s.runningForMs === null ? "unknown" : formatDuration(s.runningForMs)} lastActivity=${activity} tool=${s.currentTool ?? "-"} streamEnded=${boolLabel(s.streamEnded)} resultReceived=${boolLabel(s.resultReceived)}`;
|
|
7321
|
+
}
|
|
7322
|
+
function getWorkerStatusTool(deps) {
|
|
7323
|
+
return defineTool({
|
|
7324
|
+
description: 'Get the liveness of one worker (workerId given) or of all current workers (omitted). Reports status, runningForMs, lastActivityAt (epoch ms of the newest transcript entry; lastActivityAgoMs is how long ago that was), currentTool, streamEnded, resultReceived and idleSince \u2014 use it to tell "still working" from "stuck / already finished and just not released" before deciding to wait, chat, or cancel. Unknown worker ids return a not-found message instead of throwing. A machine-readable [worker-status] line carries the same data as JSON.',
|
|
7325
|
+
parameters: z9.object({
|
|
7326
|
+
workerId: z9.string().optional().describe("Worker ID; omit to list all current workers")
|
|
7327
|
+
}),
|
|
7328
|
+
execute: safeExecute("get_worker_status", async ({ workerId }) => {
|
|
7329
|
+
const now = deps.now?.() ?? Date.now();
|
|
7330
|
+
if (workerId !== void 0 && !isValidWorkerId(workerId)) {
|
|
7331
|
+
return `Invalid worker id "${workerId}" \u2014 worker ids are alphanumeric with dashes/underscores only.`;
|
|
7332
|
+
}
|
|
7333
|
+
if (workerId === void 0) {
|
|
7334
|
+
const workers = deps.listWorkers();
|
|
7335
|
+
if (workers.length === 0) {
|
|
7336
|
+
return "No workers are running or idle.\n[worker-status] []";
|
|
7337
|
+
}
|
|
7338
|
+
const snapshots = workers.map((w) => buildSnapshot(w.workerId, deps, now)).filter((s) => s !== null);
|
|
7339
|
+
const lines = [`Workers (${snapshots.length}):`, ...snapshots.map(formatSnapshotLine)];
|
|
7340
|
+
lines.push(`[worker-status] ${JSON.stringify(snapshots)}`);
|
|
7341
|
+
return lines.join("\n");
|
|
7342
|
+
}
|
|
7343
|
+
const snapshot = buildSnapshot(workerId, deps, now);
|
|
7344
|
+
if (!snapshot) {
|
|
7345
|
+
return `Worker ${workerId} not found (not tracked, no task references it, no transcript on disk).`;
|
|
7346
|
+
}
|
|
7347
|
+
return [...formatSnapshot(snapshot, now), `[worker-status] ${JSON.stringify(snapshot)}`].join("\n");
|
|
7348
|
+
}, { maxResultChars: MAX_TRANSCRIPT_RESULT_CHARS })
|
|
7349
|
+
});
|
|
7350
|
+
}
|
|
7351
|
+
|
|
7352
|
+
// src/services/tools/update-task.ts
|
|
7353
|
+
import { z as z10 } from "zod";
|
|
5995
7354
|
function updateTaskTool(deps) {
|
|
5996
7355
|
return defineTool({
|
|
5997
7356
|
description: "Update a pending task description or priority",
|
|
5998
|
-
parameters:
|
|
5999
|
-
taskId:
|
|
6000
|
-
description:
|
|
6001
|
-
priority:
|
|
7357
|
+
parameters: z10.object({
|
|
7358
|
+
taskId: z10.string().describe("Task ID to update"),
|
|
7359
|
+
description: z10.string().optional().describe("New task description"),
|
|
7360
|
+
priority: z10.enum(["normal", "high"]).optional().describe("New priority")
|
|
6002
7361
|
}),
|
|
6003
7362
|
execute: safeExecute("update_task", async ({ taskId, description, priority }) => {
|
|
6004
7363
|
if (!description && !priority) {
|
|
@@ -6012,14 +7371,14 @@ function updateTaskTool(deps) {
|
|
|
6012
7371
|
}
|
|
6013
7372
|
|
|
6014
7373
|
// src/services/tools/query-signals.ts
|
|
6015
|
-
import { z as
|
|
7374
|
+
import { z as z11 } from "zod";
|
|
6016
7375
|
function querySignalsTool(deps) {
|
|
6017
7376
|
return defineTool({
|
|
6018
7377
|
description: "Query signal history by status, source, or type",
|
|
6019
|
-
parameters:
|
|
6020
|
-
status:
|
|
6021
|
-
source:
|
|
6022
|
-
type:
|
|
7378
|
+
parameters: z11.object({
|
|
7379
|
+
status: z11.enum(["unread", "read", "discarded"]).optional().describe("Filter by status"),
|
|
7380
|
+
source: z11.enum(["user", "worker", "system"]).optional().describe("Filter by source"),
|
|
7381
|
+
type: z11.enum(["input", "completed", "failed", "progress", "cancelled"]).optional().describe("Filter by type")
|
|
6023
7382
|
}),
|
|
6024
7383
|
execute: safeExecute("query_signals", async (filter) => {
|
|
6025
7384
|
const signals = deps.querySignals(filter);
|
|
@@ -6032,11 +7391,11 @@ function querySignalsTool(deps) {
|
|
|
6032
7391
|
}
|
|
6033
7392
|
|
|
6034
7393
|
// src/services/tools/skip-reply.ts
|
|
6035
|
-
import { z as
|
|
7394
|
+
import { z as z12 } from "zod";
|
|
6036
7395
|
function skipReplyTool() {
|
|
6037
7396
|
return defineTool({
|
|
6038
7397
|
description: "Acknowledge a signal without responding to the user. IMPORTANT: this tool terminates the turn immediately. You MUST NOT output any text in the same response \u2014 your response must contain ONLY this tool call. Any co-emitted text will leak to the user as a broken message.",
|
|
6039
|
-
parameters:
|
|
7398
|
+
parameters: z12.object({}),
|
|
6040
7399
|
execute: safeExecute("skip_reply", async () => {
|
|
6041
7400
|
return "(skipped)";
|
|
6042
7401
|
})
|
|
@@ -6044,13 +7403,13 @@ function skipReplyTool() {
|
|
|
6044
7403
|
}
|
|
6045
7404
|
|
|
6046
7405
|
// src/services/tools/bash.ts
|
|
6047
|
-
import { z as
|
|
7406
|
+
import { z as z13 } from "zod";
|
|
6048
7407
|
import { execSync as execSync2 } from "child_process";
|
|
6049
7408
|
function bashTool(deps) {
|
|
6050
7409
|
return defineTool({
|
|
6051
7410
|
description: "Execute a shell command and return its output",
|
|
6052
|
-
parameters:
|
|
6053
|
-
command:
|
|
7411
|
+
parameters: z13.object({
|
|
7412
|
+
command: z13.string().describe("Shell command to execute")
|
|
6054
7413
|
}),
|
|
6055
7414
|
execute: safeExecute("bash", async ({ command }) => {
|
|
6056
7415
|
try {
|
|
@@ -6072,22 +7431,22 @@ ${detail}`;
|
|
|
6072
7431
|
}
|
|
6073
7432
|
|
|
6074
7433
|
// src/services/tools/read-file.ts
|
|
6075
|
-
import { z as
|
|
6076
|
-
import { readFileSync as
|
|
6077
|
-
import { resolve as
|
|
7434
|
+
import { z as z14 } from "zod";
|
|
7435
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
7436
|
+
import { resolve as resolve3 } from "path";
|
|
6078
7437
|
var DEFAULT_MAX_LINES = 500;
|
|
6079
7438
|
function readFileTool(deps = {}) {
|
|
6080
7439
|
const workspace = deps.cwd ?? process.cwd();
|
|
6081
7440
|
return defineTool({
|
|
6082
7441
|
description: `Read file content (with line numbers). Returns at most ${DEFAULT_MAX_LINES} lines by default. Use offset (1-based start line) and limit to read specific sections of large files.`,
|
|
6083
|
-
parameters:
|
|
6084
|
-
path:
|
|
6085
|
-
offset:
|
|
6086
|
-
limit:
|
|
7442
|
+
parameters: z14.object({
|
|
7443
|
+
path: z14.string().describe("File path (relative to workspace or absolute)"),
|
|
7444
|
+
offset: z14.number().int().min(1).optional().describe("Start line number (1-based)"),
|
|
7445
|
+
limit: z14.number().int().min(1).optional().describe("Number of lines to read")
|
|
6087
7446
|
}),
|
|
6088
7447
|
execute: safeExecute("read_file", async ({ path: path7, offset, limit }) => {
|
|
6089
|
-
const resolved =
|
|
6090
|
-
const content =
|
|
7448
|
+
const resolved = resolve3(workspace, path7);
|
|
7449
|
+
const content = readFileSync7(resolved, "utf-8");
|
|
6091
7450
|
const lines = content.split("\n");
|
|
6092
7451
|
const totalLines = lines.length;
|
|
6093
7452
|
const start = offset ? offset - 1 : 0;
|
|
@@ -6106,23 +7465,23 @@ function readFileTool(deps = {}) {
|
|
|
6106
7465
|
}
|
|
6107
7466
|
|
|
6108
7467
|
// src/services/tools/write-file.ts
|
|
6109
|
-
import { z as
|
|
6110
|
-
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as
|
|
6111
|
-
import { dirname as
|
|
7468
|
+
import { z as z15 } from "zod";
|
|
7469
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
|
|
7470
|
+
import { dirname as dirname5, resolve as resolve4 } from "path";
|
|
6112
7471
|
function writeFileTool(deps = {}) {
|
|
6113
7472
|
const workspace = deps.cwd ?? process.cwd();
|
|
6114
7473
|
return defineTool({
|
|
6115
7474
|
description: "Create a new file with the given content. Creates parent directories as needed. Use edit_file to modify existing files.",
|
|
6116
|
-
parameters:
|
|
6117
|
-
path:
|
|
6118
|
-
content:
|
|
7475
|
+
parameters: z15.object({
|
|
7476
|
+
path: z15.string().describe("File path (relative to workspace or absolute)"),
|
|
7477
|
+
content: z15.string().describe("File content")
|
|
6119
7478
|
}),
|
|
6120
7479
|
execute: safeExecute("write_file", async ({ path: path7, content }) => {
|
|
6121
|
-
const resolved =
|
|
6122
|
-
if (
|
|
7480
|
+
const resolved = resolve4(workspace, path7);
|
|
7481
|
+
if (existsSync7(resolved)) {
|
|
6123
7482
|
return `[ERROR] write_file: File already exists: ${path7}. Use edit_file to modify existing files.`;
|
|
6124
7483
|
}
|
|
6125
|
-
mkdirSync4(
|
|
7484
|
+
mkdirSync4(dirname5(resolved), { recursive: true });
|
|
6126
7485
|
writeFileSync3(resolved, content, "utf-8");
|
|
6127
7486
|
return `Written ${content.length} chars to ${resolved}`;
|
|
6128
7487
|
})
|
|
@@ -6130,23 +7489,23 @@ function writeFileTool(deps = {}) {
|
|
|
6130
7489
|
}
|
|
6131
7490
|
|
|
6132
7491
|
// src/services/tools/edit-file.ts
|
|
6133
|
-
import { readFileSync as
|
|
6134
|
-
import { resolve as
|
|
6135
|
-
import { z as
|
|
7492
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
7493
|
+
import { resolve as resolve5 } from "path";
|
|
7494
|
+
import { z as z16 } from "zod";
|
|
6136
7495
|
function editFileTool(deps) {
|
|
6137
7496
|
const workspace = deps.cwd ?? process.cwd();
|
|
6138
7497
|
return defineTool({
|
|
6139
7498
|
description: "Edit a file by replacing a unique string. old_string must appear exactly once in the file \u2014 if it matches multiple times, expand the surrounding context to make it unique.",
|
|
6140
|
-
parameters:
|
|
6141
|
-
path:
|
|
6142
|
-
old_string:
|
|
6143
|
-
new_string:
|
|
7499
|
+
parameters: z16.object({
|
|
7500
|
+
path: z16.string().describe("File path (relative to workspace or absolute)"),
|
|
7501
|
+
old_string: z16.string().describe("Exact text to find (must be unique in file)"),
|
|
7502
|
+
new_string: z16.string().describe("Replacement text")
|
|
6144
7503
|
}),
|
|
6145
7504
|
execute: safeExecute("edit_file", async ({ path: path7, old_string, new_string }) => {
|
|
6146
|
-
const resolved =
|
|
7505
|
+
const resolved = resolve5(workspace, path7);
|
|
6147
7506
|
let content;
|
|
6148
7507
|
try {
|
|
6149
|
-
content =
|
|
7508
|
+
content = readFileSync8(resolved, "utf-8");
|
|
6150
7509
|
} catch {
|
|
6151
7510
|
return `[ERROR] edit_file: File not found: ${path7}`;
|
|
6152
7511
|
}
|
|
@@ -6164,18 +7523,18 @@ function editFileTool(deps) {
|
|
|
6164
7523
|
|
|
6165
7524
|
// src/services/tools/search-files.ts
|
|
6166
7525
|
import fg from "fast-glob";
|
|
6167
|
-
import { resolve as
|
|
6168
|
-
import { z as
|
|
7526
|
+
import { resolve as resolve6 } from "path";
|
|
7527
|
+
import { z as z17 } from "zod";
|
|
6169
7528
|
function searchFilesTool(deps) {
|
|
6170
7529
|
const workspace = deps.cwd ?? process.cwd();
|
|
6171
7530
|
return defineTool({
|
|
6172
7531
|
description: "Search for files matching a glob pattern. Returns matching file paths relative to workspace.",
|
|
6173
|
-
parameters:
|
|
6174
|
-
pattern:
|
|
6175
|
-
path:
|
|
7532
|
+
parameters: z17.object({
|
|
7533
|
+
pattern: z17.string().describe('Glob pattern (e.g. "**/*.ts", "src/**/*.test.ts")'),
|
|
7534
|
+
path: z17.string().optional().describe("Base directory for search (default: workspace root)")
|
|
6176
7535
|
}),
|
|
6177
7536
|
execute: safeExecute("search_files", async ({ pattern, path: path7 }) => {
|
|
6178
|
-
const basePath = path7 ?
|
|
7537
|
+
const basePath = path7 ? resolve6(workspace, path7) : workspace;
|
|
6179
7538
|
const entries = await fg(pattern, {
|
|
6180
7539
|
cwd: basePath,
|
|
6181
7540
|
onlyFiles: true,
|
|
@@ -6189,8 +7548,8 @@ function searchFilesTool(deps) {
|
|
|
6189
7548
|
|
|
6190
7549
|
// src/services/tools/search-content.ts
|
|
6191
7550
|
import { execSync as execSync3 } from "child_process";
|
|
6192
|
-
import { resolve as
|
|
6193
|
-
import { z as
|
|
7551
|
+
import { resolve as resolve7 } from "path";
|
|
7552
|
+
import { z as z18 } from "zod";
|
|
6194
7553
|
function shellQuote(s) {
|
|
6195
7554
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
6196
7555
|
}
|
|
@@ -6198,13 +7557,13 @@ function searchContentTool(deps) {
|
|
|
6198
7557
|
const workspace = deps.cwd ?? process.cwd();
|
|
6199
7558
|
return defineTool({
|
|
6200
7559
|
description: "Search file contents for a text pattern. Returns matching lines in file:line:content format.",
|
|
6201
|
-
parameters:
|
|
6202
|
-
query:
|
|
6203
|
-
path:
|
|
6204
|
-
include:
|
|
7560
|
+
parameters: z18.object({
|
|
7561
|
+
query: z18.string().describe("Search text or regex pattern"),
|
|
7562
|
+
path: z18.string().optional().describe("Directory to search in (default: workspace root)"),
|
|
7563
|
+
include: z18.string().optional().describe('File pattern to include (e.g. "*.ts", "*.py")')
|
|
6205
7564
|
}),
|
|
6206
7565
|
execute: safeExecute("search_content", async ({ query: query2, path: path7, include }) => {
|
|
6207
|
-
const searchDir = path7 ?
|
|
7566
|
+
const searchDir = path7 ? resolve7(workspace, path7) : workspace;
|
|
6208
7567
|
const cmd = `grep -rn${include ? ` --include=${shellQuote(include)}` : ""} -- ${shellQuote(query2)} ${shellQuote(searchDir)}`;
|
|
6209
7568
|
try {
|
|
6210
7569
|
const output = execSync3(cmd, {
|
|
@@ -6223,13 +7582,13 @@ ${err.stderr?.toString() || ""}`;
|
|
|
6223
7582
|
}
|
|
6224
7583
|
|
|
6225
7584
|
// src/services/tools/web-search.ts
|
|
6226
|
-
import { z as
|
|
7585
|
+
import { z as z19 } from "zod";
|
|
6227
7586
|
function webSearchTool(deps) {
|
|
6228
7587
|
const apiKey = deps.tavilyApiKey;
|
|
6229
7588
|
return defineTool({
|
|
6230
7589
|
description: "Search the web using Tavily. Returns top results with title, URL, and content snippet.",
|
|
6231
|
-
parameters:
|
|
6232
|
-
query:
|
|
7590
|
+
parameters: z19.object({
|
|
7591
|
+
query: z19.string().describe("Search query")
|
|
6233
7592
|
}),
|
|
6234
7593
|
execute: safeExecute("web_search", async ({ query: query2 }) => {
|
|
6235
7594
|
if (!apiKey) {
|
|
@@ -6260,11 +7619,11 @@ function webSearchTool(deps) {
|
|
|
6260
7619
|
}
|
|
6261
7620
|
|
|
6262
7621
|
// src/services/tools/notebook.ts
|
|
6263
|
-
import { z as
|
|
7622
|
+
import { z as z20 } from "zod";
|
|
6264
7623
|
function readNotebookTool(deps) {
|
|
6265
7624
|
return defineTool({
|
|
6266
7625
|
description: "Read the current notebook content. The notebook is a private scratchpad that persists across context compaction \u2014 use it to store plans, notes, and working context.",
|
|
6267
|
-
parameters:
|
|
7626
|
+
parameters: z20.object({}),
|
|
6268
7627
|
execute: safeExecute("read_notebook", async () => {
|
|
6269
7628
|
const content = deps.getNotebook();
|
|
6270
7629
|
return content || "(notebook is empty)";
|
|
@@ -6274,9 +7633,9 @@ function readNotebookTool(deps) {
|
|
|
6274
7633
|
function updateNotebookTool(deps) {
|
|
6275
7634
|
return defineTool({
|
|
6276
7635
|
description: 'Update the notebook content. Use "replace" to overwrite the entire notebook, or "append" to add to the end. The notebook persists across context compaction.',
|
|
6277
|
-
parameters:
|
|
6278
|
-
content:
|
|
6279
|
-
mode:
|
|
7636
|
+
parameters: z20.object({
|
|
7637
|
+
content: z20.string().describe("Content to write"),
|
|
7638
|
+
mode: z20.enum(["replace", "append"]).optional().describe('Write mode: "replace" overwrites, "append" adds to the end (default: replace)')
|
|
6280
7639
|
}),
|
|
6281
7640
|
execute: safeExecute("update_notebook", async ({ content, mode }) => {
|
|
6282
7641
|
deps.setNotebook(content, mode ?? "replace");
|
|
@@ -6286,15 +7645,16 @@ function updateNotebookTool(deps) {
|
|
|
6286
7645
|
}
|
|
6287
7646
|
|
|
6288
7647
|
// src/services/tools/inquire.ts
|
|
6289
|
-
import { z as
|
|
7648
|
+
import { z as z21 } from "zod";
|
|
6290
7649
|
function inquireTool(deps) {
|
|
6291
7650
|
return defineTool({
|
|
6292
|
-
description:
|
|
6293
|
-
parameters:
|
|
6294
|
-
message:
|
|
6295
|
-
type:
|
|
6296
|
-
options:
|
|
6297
|
-
default:
|
|
7651
|
+
description: 'Ask the user a structured question and wait for their response. Use this to clarify ambiguous requests, confirm approach, or let the user choose between options. The tool blocks until the user answers. For "select" questions an "Other" choice is appended automatically so the user can always answer outside the preset options \u2014 do not add your own catch-all option. Answers typed via "Other" come back prefixed with "[Other] ".',
|
|
7652
|
+
parameters: z21.object({
|
|
7653
|
+
message: z21.string().describe("Question to ask the user"),
|
|
7654
|
+
type: z21.enum(["confirm", "select", "text"]).describe('Question type: "confirm" for yes/no, "select" for choosing from options, "text" for free-form input'),
|
|
7655
|
+
options: z21.array(z21.string()).optional().describe('Available choices (required for "select" type). Provide 2-5 clear preset options \u2014 an "Other" choice for free-form input is appended automatically'),
|
|
7656
|
+
default: z21.string().optional().describe("Default value or pre-selected option"),
|
|
7657
|
+
allowOther: z21.boolean().optional().describe('"select" only: append an "Other" choice letting the user type a custom answer (default true). Set false to restrict answers to the preset options')
|
|
6298
7658
|
}),
|
|
6299
7659
|
execute: safeExecute("inquire", async (args) => {
|
|
6300
7660
|
return deps.inquire(args);
|
|
@@ -6303,12 +7663,12 @@ function inquireTool(deps) {
|
|
|
6303
7663
|
}
|
|
6304
7664
|
|
|
6305
7665
|
// src/services/tools/done.ts
|
|
6306
|
-
import { z as
|
|
7666
|
+
import { z as z22 } from "zod";
|
|
6307
7667
|
function doneTool(deps) {
|
|
6308
7668
|
return defineTool({
|
|
6309
7669
|
description: "Report task completion. Worker stops executing after this call.",
|
|
6310
|
-
parameters:
|
|
6311
|
-
summary:
|
|
7670
|
+
parameters: z22.object({
|
|
7671
|
+
summary: z22.string().describe("Task completion summary")
|
|
6312
7672
|
}),
|
|
6313
7673
|
execute: safeExecute("done", async ({ summary }) => {
|
|
6314
7674
|
deps.onComplete(summary);
|
|
@@ -6318,12 +7678,12 @@ function doneTool(deps) {
|
|
|
6318
7678
|
}
|
|
6319
7679
|
|
|
6320
7680
|
// src/services/tools/fail.ts
|
|
6321
|
-
import { z as
|
|
7681
|
+
import { z as z23 } from "zod";
|
|
6322
7682
|
function failTool(deps) {
|
|
6323
7683
|
return defineTool({
|
|
6324
7684
|
description: "Report task failure. Worker stops executing after this call.",
|
|
6325
|
-
parameters:
|
|
6326
|
-
error:
|
|
7685
|
+
parameters: z23.object({
|
|
7686
|
+
error: z23.string().describe("Error description")
|
|
6327
7687
|
}),
|
|
6328
7688
|
execute: safeExecute("fail", async ({ error }) => {
|
|
6329
7689
|
deps.onFail(error);
|
|
@@ -6333,13 +7693,13 @@ function failTool(deps) {
|
|
|
6333
7693
|
}
|
|
6334
7694
|
|
|
6335
7695
|
// src/services/tools/update-progress.ts
|
|
6336
|
-
import { z as
|
|
7696
|
+
import { z as z24 } from "zod";
|
|
6337
7697
|
function updateProgressTool(deps) {
|
|
6338
7698
|
return defineTool({
|
|
6339
7699
|
description: "Report progress for the current task (replaces automatic step-based progress)",
|
|
6340
|
-
parameters:
|
|
6341
|
-
message:
|
|
6342
|
-
percent:
|
|
7700
|
+
parameters: z24.object({
|
|
7701
|
+
message: z24.string().describe("Progress description"),
|
|
7702
|
+
percent: z24.number().min(0).max(100).optional().describe("Completion percentage (0-100)")
|
|
6343
7703
|
}),
|
|
6344
7704
|
execute: safeExecute("update_progress", async ({ message, percent }) => {
|
|
6345
7705
|
deps.reportProgress(message, percent);
|
|
@@ -6350,21 +7710,24 @@ function updateProgressTool(deps) {
|
|
|
6350
7710
|
}
|
|
6351
7711
|
|
|
6352
7712
|
// src/services/tools/chat-worker.ts
|
|
6353
|
-
import { z as
|
|
7713
|
+
import { z as z25 } from "zod";
|
|
6354
7714
|
function chatWorkerTool(deps) {
|
|
6355
7715
|
return defineTool({
|
|
6356
|
-
description:
|
|
6357
|
-
parameters:
|
|
6358
|
-
workerId:
|
|
6359
|
-
message:
|
|
7716
|
+
description: `Send a message to a running worker. Use this to provide additional guidance, correct the worker's approach, or ask the worker to check something. The worker reads it at its next step (puppet) or turn boundary (third-party); idle multi-stage workers read it when their next stage starts. A third-party worker NEVER acts on the message mid-tool-call: it finishes the current turn first, so "queued" can mean a delay of minutes. If the worker has already finished, this tool fails explicitly \u2014 inspect its output instead.`,
|
|
7717
|
+
parameters: z25.object({
|
|
7718
|
+
workerId: z25.string().describe("ID of the target worker"),
|
|
7719
|
+
message: z25.string().describe("Message to send to the worker")
|
|
6360
7720
|
}),
|
|
6361
7721
|
execute: safeExecute("chat_worker", async ({ workerId, message }) => {
|
|
6362
7722
|
const status = deps.sendMessageToWorker(workerId, message);
|
|
6363
7723
|
if (status === "not_running") {
|
|
6364
|
-
return `[ERROR] Worker ${workerId} is not running (it may have just finished).`;
|
|
7724
|
+
return `[ERROR] Worker ${workerId} is not running (it may have just finished). Use get_worker_status("${workerId}") or read_worker_conversation("${workerId}") to inspect what it produced.`;
|
|
7725
|
+
}
|
|
7726
|
+
if (status === "finished") {
|
|
7727
|
+
return `[ERROR] Worker ${workerId} has already finished; message not delivered. Use read_worker_conversation("${workerId}") / get_task_detail to inspect its output.`;
|
|
6365
7728
|
}
|
|
6366
7729
|
if (status === "queued") {
|
|
6367
|
-
return `Message queued for worker ${workerId} \u2014 it
|
|
7730
|
+
return `Message queued for worker ${workerId} \u2014 QUEUED only, not delivered yet: the worker reads it at its next step (puppet), turn boundary (third-party \u2014 the current turn, including any running tool call, finishes first) or next stage start (idle). A stuck worker will never reach that point. Verify with get_worker_status("${workerId}") (lastActivityAt / currentTool); if it looks stuck, use cancel_task instead of waiting for this message.`;
|
|
6368
7731
|
}
|
|
6369
7732
|
return `Message delivered to worker ${workerId} \u2014 it will be read at the worker's next step.`;
|
|
6370
7733
|
})
|
|
@@ -6372,14 +7735,14 @@ function chatWorkerTool(deps) {
|
|
|
6372
7735
|
}
|
|
6373
7736
|
|
|
6374
7737
|
// src/services/tools/respond-permission.ts
|
|
6375
|
-
import { z as
|
|
7738
|
+
import { z as z26 } from "zod";
|
|
6376
7739
|
function respondPermissionTool(deps) {
|
|
6377
7740
|
return defineTool({
|
|
6378
7741
|
description: "Respond to a worker's permission request. Approve to allow the operation, deny to reject it.",
|
|
6379
|
-
parameters:
|
|
6380
|
-
requestId:
|
|
6381
|
-
approved:
|
|
6382
|
-
reason:
|
|
7742
|
+
parameters: z26.object({
|
|
7743
|
+
requestId: z26.string().describe("The request ID from the permission_request signal"),
|
|
7744
|
+
approved: z26.boolean().describe("true to approve, false to deny"),
|
|
7745
|
+
reason: z26.string().optional().describe("Reason for denial (shown to worker)")
|
|
6383
7746
|
}),
|
|
6384
7747
|
execute: safeExecute("respond_permission", async ({ requestId, approved, reason }) => {
|
|
6385
7748
|
const handled = deps.respondPermission(requestId, approved, reason);
|
|
@@ -6390,13 +7753,13 @@ function respondPermissionTool(deps) {
|
|
|
6390
7753
|
}
|
|
6391
7754
|
|
|
6392
7755
|
// src/services/tools/respond-inquiry.ts
|
|
6393
|
-
import { z as
|
|
7756
|
+
import { z as z27 } from "zod";
|
|
6394
7757
|
function respondInquiryTool(deps) {
|
|
6395
7758
|
return defineTool({
|
|
6396
7759
|
description: "Answer a worker's question (from a worker_inquiry signal). The blocked worker resumes with your answer as the tool result. If the decision belongs to the user, ask them first via inquire, then answer here.",
|
|
6397
|
-
parameters:
|
|
6398
|
-
inquiryId:
|
|
6399
|
-
answer:
|
|
7760
|
+
parameters: z27.object({
|
|
7761
|
+
inquiryId: z27.string().describe("The inquiryId from the worker_inquiry signal"),
|
|
7762
|
+
answer: z27.string().max(4e3).describe("The answer/instruction for the worker. Self-contained \u2014 the worker sees only this text.")
|
|
6400
7763
|
}),
|
|
6401
7764
|
execute: safeExecute("respond_inquiry", async ({ inquiryId, answer }) => {
|
|
6402
7765
|
const status = deps.respondInquiry(inquiryId, answer);
|
|
@@ -6412,7 +7775,7 @@ function respondInquiryTool(deps) {
|
|
|
6412
7775
|
}
|
|
6413
7776
|
|
|
6414
7777
|
// src/services/tools/use-skill.ts
|
|
6415
|
-
import { z as
|
|
7778
|
+
import { z as z28 } from "zod";
|
|
6416
7779
|
function formatSkillContent(skill) {
|
|
6417
7780
|
const header = `# Skill: ${skill.name}
|
|
6418
7781
|
|
|
@@ -6434,8 +7797,8 @@ ${skill.content}${resources}`;
|
|
|
6434
7797
|
function useSkillTool(deps) {
|
|
6435
7798
|
return defineTool({
|
|
6436
7799
|
description: `Load a skill's full instructions by name. Skills are on-demand expertise (workflows, conventions, scripts). Use this when a task matches a skill in the "Skills" section. Returns the skill body plus a list of its resource files (invoke those with your shell/file tools, not here).`,
|
|
6437
|
-
parameters:
|
|
6438
|
-
name:
|
|
7800
|
+
parameters: z28.object({
|
|
7801
|
+
name: z28.string().describe('Skill name, exactly as shown in the "Skills" section')
|
|
6439
7802
|
}),
|
|
6440
7803
|
execute: safeExecute("use-skill", async ({ name }) => {
|
|
6441
7804
|
const content = deps.registry.get(name);
|
|
@@ -6467,7 +7830,26 @@ function createControllerTools(deps) {
|
|
|
6467
7830
|
}),
|
|
6468
7831
|
get_task_detail: getTaskDetailTool({
|
|
6469
7832
|
getTask: deps.getTask,
|
|
6470
|
-
getWorkerConversation: deps.getWorkerConversation
|
|
7833
|
+
getWorkerConversation: deps.getWorkerConversation,
|
|
7834
|
+
readTranscriptWindow: deps.readTranscriptWindow
|
|
7835
|
+
}),
|
|
7836
|
+
read_worker_conversation: readWorkerConversationTool({
|
|
7837
|
+
sessionDir: deps.sessionDir,
|
|
7838
|
+
getWorkerConversationByWorkerId: deps.getWorkerConversationByWorkerId,
|
|
7839
|
+
getWorkerTranscriptId: deps.getWorkerTranscriptId,
|
|
7840
|
+
readTranscriptWindow: (workerId, opts) => {
|
|
7841
|
+
const reader = deps.readTranscriptWindow;
|
|
7842
|
+
if (!reader) throw new Error("readTranscriptWindow dep not wired");
|
|
7843
|
+
return reader(workerId, opts);
|
|
7844
|
+
},
|
|
7845
|
+
listWorkers: () => deps.getActiveWorkers?.() ?? [],
|
|
7846
|
+
listTasks: () => deps.listTasks()
|
|
7847
|
+
}),
|
|
7848
|
+
get_worker_status: getWorkerStatusTool({
|
|
7849
|
+
listWorkers: () => deps.getActiveWorkers?.() ?? [],
|
|
7850
|
+
listTasks: () => deps.listTasks(),
|
|
7851
|
+
readTranscript: deps.readTranscript,
|
|
7852
|
+
getLiveStatus: deps.getWorkerLiveStatus
|
|
6471
7853
|
}),
|
|
6472
7854
|
update_task: updateTaskTool({ updateTask: deps.updateTask }),
|
|
6473
7855
|
query_signals: querySignalsTool({ querySignals: deps.queryMessages }),
|
|
@@ -6507,13 +7889,13 @@ function createWorkerTools(deps) {
|
|
|
6507
7889
|
}
|
|
6508
7890
|
|
|
6509
7891
|
// src/services/tools/ask-controller.ts
|
|
6510
|
-
import { z as
|
|
7892
|
+
import { z as z29 } from "zod";
|
|
6511
7893
|
function askControllerTool(deps) {
|
|
6512
7894
|
return defineTool({
|
|
6513
7895
|
description: "Ask the controller a question and WAIT for the answer. Use ONLY when you are blocked: execution cannot continue as instructed (contradiction, missing resource, unexpected state), or continuing would risk significant wasted work. Do NOT use it for decisions you can make yourself \u2014 decide those autonomously and report via update_progress / done.",
|
|
6514
|
-
parameters:
|
|
6515
|
-
question:
|
|
6516
|
-
context:
|
|
7896
|
+
parameters: z29.object({
|
|
7897
|
+
question: z29.string().max(500).describe("The specific question you need answered to proceed. Be concise and self-contained."),
|
|
7898
|
+
context: z29.string().max(1e3).optional().describe("What you have tried and where you are stuck, so the controller can answer precisely.")
|
|
6517
7899
|
}),
|
|
6518
7900
|
execute: safeExecute("ask_controller", async ({ question, context }) => {
|
|
6519
7901
|
return deps.ask(question, context);
|
|
@@ -6555,16 +7937,16 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
6555
7937
|
},
|
|
6556
7938
|
...deps.turnLineage ?? {}
|
|
6557
7939
|
});
|
|
6558
|
-
const result = await new Promise((
|
|
7940
|
+
const result = await new Promise((resolve12) => {
|
|
6559
7941
|
const timer = setTimeout(() => {
|
|
6560
7942
|
pending.delete(requestId);
|
|
6561
|
-
|
|
7943
|
+
resolve12({ approved: false, reason: "timeout" });
|
|
6562
7944
|
}, timeoutMs);
|
|
6563
7945
|
pending.set(requestId, {
|
|
6564
7946
|
resolve: (ok, reason) => {
|
|
6565
7947
|
clearTimeout(timer);
|
|
6566
7948
|
pending.delete(requestId);
|
|
6567
|
-
|
|
7949
|
+
resolve12({ approved: ok, reason });
|
|
6568
7950
|
},
|
|
6569
7951
|
timer
|
|
6570
7952
|
});
|
|
@@ -6601,14 +7983,14 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
6601
7983
|
}
|
|
6602
7984
|
|
|
6603
7985
|
// src/services/tools/permission-rules.ts
|
|
6604
|
-
import { resolve as
|
|
7986
|
+
import { resolve as resolve8 } from "path";
|
|
6605
7987
|
import { homedir } from "os";
|
|
6606
7988
|
var FILE_TOOL_NAMES = ["read_file", "write_file", "edit_file", "search_files", "search_content"];
|
|
6607
7989
|
var DANGEROUS_BASH_PATTERN = /(?:^|\||;|&&)\s*(?:rm\s|sudo\b|chmod\b|chown\b|mkfs\b|dd\s|shutdown\b|reboot\b|kill\s+-9)/;
|
|
6608
7990
|
var URL_RE = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/\S+/g;
|
|
6609
7991
|
function expandHome(p) {
|
|
6610
7992
|
if (p === "~") return homedir();
|
|
6611
|
-
if (p.startsWith("~/")) return
|
|
7993
|
+
if (p.startsWith("~/")) return resolve8(homedir(), p.slice(2));
|
|
6612
7994
|
return p;
|
|
6613
7995
|
}
|
|
6614
7996
|
function extractPathCandidates(command) {
|
|
@@ -6623,7 +8005,7 @@ function extractPathCandidates(command) {
|
|
|
6623
8005
|
function needsFilePermission(workspace, args) {
|
|
6624
8006
|
const rawPath = args.path;
|
|
6625
8007
|
if (typeof rawPath !== "string" || !rawPath) return false;
|
|
6626
|
-
return !isInsideWorkspace(
|
|
8008
|
+
return !isInsideWorkspace(resolve8(workspace, expandHome(rawPath)), workspace);
|
|
6627
8009
|
}
|
|
6628
8010
|
function needsBashPermission(workspace, command) {
|
|
6629
8011
|
if (DANGEROUS_BASH_PATTERN.test(command)) return true;
|
|
@@ -6673,7 +8055,7 @@ function formatApiCallError(err) {
|
|
|
6673
8055
|
}
|
|
6674
8056
|
|
|
6675
8057
|
// src/services/task-execute-engine.ts
|
|
6676
|
-
import { resolve as
|
|
8058
|
+
import { resolve as resolve9 } from "path";
|
|
6677
8059
|
var RuntimeState = class extends BaseWayangState {
|
|
6678
8060
|
constructor(initialData) {
|
|
6679
8061
|
super(initialData, []);
|
|
@@ -6885,14 +8267,16 @@ var TaskExecuteEngine = class {
|
|
|
6885
8267
|
* Send a message to a worker. Delivery is judged by the activeWorkers
|
|
6886
8268
|
* runtime state — NOT by the workers Map, which lingers one tick past
|
|
6887
8269
|
* disposal (removeWorkerTracking defers) and would accept messages for a
|
|
6888
|
-
* worker that is already gone.
|
|
8270
|
+
* worker that is already gone. A worker that rejects the message (its run
|
|
8271
|
+
* can no longer take input) is reported as 'finished' — never a fake
|
|
8272
|
+
* 'queued'.
|
|
6889
8273
|
*/
|
|
6890
8274
|
sendMessageToWorker(workerId, message) {
|
|
6891
8275
|
const info = this.getActiveWorkers().find((w) => w.workerId === workerId);
|
|
6892
8276
|
if (!info) return "not_running";
|
|
6893
8277
|
const worker = this.workers.get(workerId);
|
|
6894
8278
|
if (!worker) return "not_running";
|
|
6895
|
-
worker.acceptMessage(message);
|
|
8279
|
+
if (!worker.acceptMessage(message)) return "finished";
|
|
6896
8280
|
if (info.status === "idle") return "queued";
|
|
6897
8281
|
return worker instanceof WorkerAgent ? "delivered" : "queued";
|
|
6898
8282
|
}
|
|
@@ -6948,6 +8332,24 @@ var TaskExecuteEngine = class {
|
|
|
6948
8332
|
}
|
|
6949
8333
|
});
|
|
6950
8334
|
}
|
|
8335
|
+
/**
|
|
8336
|
+
* Live run flags of a worker instance — "has the result arrived?" and "has
|
|
8337
|
+
* the stream closed?" (issue #16). Returns undefined for workers that do not
|
|
8338
|
+
* report them or are no longer tracked.
|
|
8339
|
+
*/
|
|
8340
|
+
getWorkerLiveStatus(workerId) {
|
|
8341
|
+
return this.workers.get(workerId)?.getLiveStatus?.();
|
|
8342
|
+
}
|
|
8343
|
+
/**
|
|
8344
|
+
* Per-worker activity derived from the in-memory conversation: when it last
|
|
8345
|
+
* produced an entry, and which tool it called last. Fed into heartbeat
|
|
8346
|
+
* signals so the controller can spot a stall without calling a tool.
|
|
8347
|
+
*/
|
|
8348
|
+
getWorkerActivity(workerId) {
|
|
8349
|
+
const entries = this.workers.get(workerId)?.getConversation() ?? [];
|
|
8350
|
+
const summary = summarizeTranscript(entries);
|
|
8351
|
+
return { lastActivityAt: summary.lastActivityAt, currentTool: summary.currentTool };
|
|
8352
|
+
}
|
|
6951
8353
|
getWorkerConversation(taskId) {
|
|
6952
8354
|
for (const [workerId, tId] of this.workerTaskMap) {
|
|
6953
8355
|
if (tId === taskId) {
|
|
@@ -6957,6 +8359,25 @@ var TaskExecuteEngine = class {
|
|
|
6957
8359
|
}
|
|
6958
8360
|
return [];
|
|
6959
8361
|
}
|
|
8362
|
+
/**
|
|
8363
|
+
* Live in-memory conversation of a worker looked up by worker id (not task
|
|
8364
|
+
* id). Undefined once the worker is no longer tracked — the on-disk
|
|
8365
|
+
* transcript (via the task's workerTranscriptId) takes over then.
|
|
8366
|
+
*/
|
|
8367
|
+
getWorkerConversationByWorkerId(workerId) {
|
|
8368
|
+
const worker = this.workers.get(workerId);
|
|
8369
|
+
return worker === void 0 ? void 0 : worker.getConversation();
|
|
8370
|
+
}
|
|
8371
|
+
/**
|
|
8372
|
+
* The worker instance's OWN id (agent-…/cw-…) — the directory name under
|
|
8373
|
+
* `<sessionDir>/workers/` where its conversation.jsonl lives. Distinct from
|
|
8374
|
+
* the engine worker id (w-…) used in tasks/signals. Only resolvable while
|
|
8375
|
+
* the engine tracks the worker; afterwards the task's persisted
|
|
8376
|
+
* `workerTranscriptId` is the source of truth.
|
|
8377
|
+
*/
|
|
8378
|
+
getWorkerTranscriptId(workerId) {
|
|
8379
|
+
return this.workers.get(workerId)?.id;
|
|
8380
|
+
}
|
|
6960
8381
|
getRunningCount() {
|
|
6961
8382
|
return this.taskState.get("tasks.running").length;
|
|
6962
8383
|
}
|
|
@@ -7113,6 +8534,20 @@ var TaskExecuteEngine = class {
|
|
|
7113
8534
|
this.taskState.append("tasks.running", updated);
|
|
7114
8535
|
this.logger.info({ taskId: id, workerId: workerSessionId }, "Task moved to running");
|
|
7115
8536
|
}
|
|
8537
|
+
/**
|
|
8538
|
+
* Record the worker instance's own id (the on-disk transcript directory
|
|
8539
|
+
* name) on the running task. Called right after the worker instance is
|
|
8540
|
+
* created, before its run starts, so the link is on disk even if the
|
|
8541
|
+
* process crashes mid-run.
|
|
8542
|
+
*/
|
|
8543
|
+
stampWorkerTranscriptId(taskId, transcriptId) {
|
|
8544
|
+
const running = [...this.taskState.get("tasks.running")];
|
|
8545
|
+
const idx = running.findIndex((t) => t.id === taskId);
|
|
8546
|
+
if (idx === -1) return;
|
|
8547
|
+
if (running[idx].workerTranscriptId === transcriptId) return;
|
|
8548
|
+
running[idx] = { ...running[idx], workerTranscriptId: transcriptId };
|
|
8549
|
+
this.taskState.set("tasks.running", running);
|
|
8550
|
+
}
|
|
7116
8551
|
completeTask(id, result) {
|
|
7117
8552
|
this.moveToHistory(id, "completed", { result });
|
|
7118
8553
|
this.logger.info({ taskId: id }, "Task completed");
|
|
@@ -7250,6 +8685,7 @@ var TaskExecuteEngine = class {
|
|
|
7250
8685
|
const worker = this.createWorker(workerType);
|
|
7251
8686
|
this.workers.set(workerId, worker);
|
|
7252
8687
|
this.workerTaskMap.set(workerId, task.id);
|
|
8688
|
+
this.stampWorkerTranscriptId(task.id, worker.id);
|
|
7253
8689
|
const executionTurnId = this.registerWorkerTurn(workerId, task);
|
|
7254
8690
|
const onProgress = this.buildProgressEmitter(workerId, task, workerType);
|
|
7255
8691
|
const tools = this.wireWorker(worker, task, workerId, workerType);
|
|
@@ -7280,7 +8716,9 @@ var TaskExecuteEngine = class {
|
|
|
7280
8716
|
assignedFromWorkerId: workerId,
|
|
7281
8717
|
status: "running",
|
|
7282
8718
|
startedAt: Date.now(),
|
|
7283
|
-
workerSessionId: workerId
|
|
8719
|
+
workerSessionId: workerId,
|
|
8720
|
+
// Same instance keeps writing the same conversation.jsonl.
|
|
8721
|
+
workerTranscriptId: worker.id
|
|
7284
8722
|
};
|
|
7285
8723
|
this.taskState.append("tasks.running", taskWithOrigin);
|
|
7286
8724
|
this.workerTaskMap.set(workerId, task.id);
|
|
@@ -7308,10 +8746,22 @@ var TaskExecuteEngine = class {
|
|
|
7308
8746
|
buildProgressEmitter(workerId, task, workerType) {
|
|
7309
8747
|
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
7310
8748
|
return (msg) => {
|
|
8749
|
+
const activity = this.getWorkerActivity(workerId);
|
|
7311
8750
|
this.enqueueWorkerSignal(workerId, {
|
|
7312
8751
|
source: "worker",
|
|
7313
8752
|
type: "progress",
|
|
7314
|
-
payload: {
|
|
8753
|
+
payload: {
|
|
8754
|
+
workerId,
|
|
8755
|
+
taskId: task.id,
|
|
8756
|
+
taskTitle: task.title,
|
|
8757
|
+
workerType,
|
|
8758
|
+
emoji: meta.emoji,
|
|
8759
|
+
message: msg,
|
|
8760
|
+
// Activity rides along (issue #16) so the controller sees liveness
|
|
8761
|
+
// from the signal itself instead of polling.
|
|
8762
|
+
lastActivityAt: activity.lastActivityAt,
|
|
8763
|
+
currentTool: activity.currentTool
|
|
8764
|
+
}
|
|
7315
8765
|
});
|
|
7316
8766
|
};
|
|
7317
8767
|
}
|
|
@@ -7361,10 +8811,20 @@ var TaskExecuteEngine = class {
|
|
|
7361
8811
|
tavilyApiKey: this.ctx.config.tavilyApiKey,
|
|
7362
8812
|
registry: this.skills,
|
|
7363
8813
|
reportProgress: (msg, _percent) => {
|
|
8814
|
+
const activity = this.getWorkerActivity(workerId);
|
|
7364
8815
|
this.enqueueWorkerSignal(workerId, {
|
|
7365
8816
|
source: "worker",
|
|
7366
8817
|
type: "progress",
|
|
7367
|
-
payload: {
|
|
8818
|
+
payload: {
|
|
8819
|
+
workerId,
|
|
8820
|
+
taskId: task.id,
|
|
8821
|
+
taskTitle: task.title,
|
|
8822
|
+
workerType,
|
|
8823
|
+
emoji: workerMeta.emoji,
|
|
8824
|
+
message: msg,
|
|
8825
|
+
lastActivityAt: activity.lastActivityAt,
|
|
8826
|
+
currentTool: activity.currentTool
|
|
8827
|
+
}
|
|
7368
8828
|
});
|
|
7369
8829
|
},
|
|
7370
8830
|
onComplete: (summary) => {
|
|
@@ -7396,7 +8856,7 @@ var TaskExecuteEngine = class {
|
|
|
7396
8856
|
if (toolName === "bash") return `Execute shell command: ${args.command}`;
|
|
7397
8857
|
if (FILE_TOOL_NAMES.includes(toolName)) {
|
|
7398
8858
|
const rawPath = typeof args.path === "string" ? args.path : "";
|
|
7399
|
-
const resolved = rawPath ?
|
|
8859
|
+
const resolved = rawPath ? resolve9(workspace, rawPath) : workspace;
|
|
7400
8860
|
return `${toolName}: ${rawPath || "(workspace root)"} (resolved: ${resolved})`;
|
|
7401
8861
|
}
|
|
7402
8862
|
return `${toolName}: ${JSON.stringify(args)}`;
|
|
@@ -7556,7 +9016,7 @@ var ControllerLoop = class {
|
|
|
7556
9016
|
this.signalQueue = signalQueue;
|
|
7557
9017
|
this.controllerAgent = controllerAgent;
|
|
7558
9018
|
this.heartbeatProvider = heartbeatProvider;
|
|
7559
|
-
this.heartbeatIntervalMs = options?.heartbeatIntervalMs ??
|
|
9019
|
+
this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 6e4;
|
|
7560
9020
|
}
|
|
7561
9021
|
ctx;
|
|
7562
9022
|
signalQueue;
|
|
@@ -7632,13 +9092,21 @@ var ControllerLoop = class {
|
|
|
7632
9092
|
payload: {
|
|
7633
9093
|
reason: "Workers are running but no signals for a while. Review status and decide if action is needed.",
|
|
7634
9094
|
idleSinceMs: now - lastWakeAt,
|
|
7635
|
-
workers: activeWorkers.map((w) =>
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
9095
|
+
workers: activeWorkers.map((w) => {
|
|
9096
|
+
const activity = this.heartbeatProvider.getWorkerActivity?.(w.workerId);
|
|
9097
|
+
return {
|
|
9098
|
+
workerId: w.workerId,
|
|
9099
|
+
taskId: w.taskId,
|
|
9100
|
+
taskTitle: w.taskTitle,
|
|
9101
|
+
workerType: w.workerType,
|
|
9102
|
+
runningForMs: now - w.startedAt,
|
|
9103
|
+
...activity ? {
|
|
9104
|
+
lastActivityAt: activity.lastActivityAt,
|
|
9105
|
+
lastActivityAgoMs: activity.lastActivityAt === null ? null : Math.max(0, now - activity.lastActivityAt),
|
|
9106
|
+
currentTool: activity.currentTool
|
|
9107
|
+
} : {}
|
|
9108
|
+
};
|
|
9109
|
+
}),
|
|
7642
9110
|
pendingTaskCount: this.heartbeatProvider.getPendingCount()
|
|
7643
9111
|
}
|
|
7644
9112
|
});
|
|
@@ -7655,11 +9123,11 @@ var ControllerLoop = class {
|
|
|
7655
9123
|
import * as path5 from "path";
|
|
7656
9124
|
|
|
7657
9125
|
// src/services/session/session-state.ts
|
|
7658
|
-
import { join as
|
|
9126
|
+
import { join as join10 } from "path";
|
|
7659
9127
|
var SessionState = class extends BaseWayangState {
|
|
7660
9128
|
jsonFile;
|
|
7661
9129
|
constructor(sessionDir) {
|
|
7662
|
-
const jsonFile = new JSONFileHelper(
|
|
9130
|
+
const jsonFile = new JSONFileHelper(join10(sessionDir, "meta.json"));
|
|
7663
9131
|
super(
|
|
7664
9132
|
{
|
|
7665
9133
|
meta: {
|
|
@@ -7765,7 +9233,7 @@ var SessionManager = class _SessionManager {
|
|
|
7765
9233
|
};
|
|
7766
9234
|
|
|
7767
9235
|
// src/services/agents/controller-state.ts
|
|
7768
|
-
import { join as
|
|
9236
|
+
import { join as join12 } from "path";
|
|
7769
9237
|
|
|
7770
9238
|
// src/services/conversation/conversation-index.ts
|
|
7771
9239
|
function emptyUsage() {
|
|
@@ -7986,8 +9454,8 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
7986
9454
|
/** In-memory resolve function for the current inquiry — never persisted. */
|
|
7987
9455
|
inquiryResolver = null;
|
|
7988
9456
|
constructor(ctx) {
|
|
7989
|
-
const jsonFile = new JSONFileHelper(
|
|
7990
|
-
const jsonlFile = new JSONLFileHelper(
|
|
9457
|
+
const jsonFile = new JSONFileHelper(join12(ctx.sessionDir, "runtime-state.json"));
|
|
9458
|
+
const jsonlFile = new JSONLFileHelper(join12(ctx.sessionDir, "conversation.jsonl"));
|
|
7991
9459
|
const initialData = {
|
|
7992
9460
|
runtimeState: {
|
|
7993
9461
|
session: { id: "", startedAt: 0 },
|
|
@@ -8062,8 +9530,8 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
8062
9530
|
}
|
|
8063
9531
|
/** Set a pending inquiry and return a Promise that resolves when the user answers. */
|
|
8064
9532
|
askInquiry(question) {
|
|
8065
|
-
return new Promise((
|
|
8066
|
-
this.inquiryResolver =
|
|
9533
|
+
return new Promise((resolve12) => {
|
|
9534
|
+
this.inquiryResolver = resolve12;
|
|
8067
9535
|
this.set("runtimeState.pendingInquiry", question);
|
|
8068
9536
|
});
|
|
8069
9537
|
}
|
|
@@ -8223,6 +9691,19 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
8223
9691
|
listTasks: (status) => engine.list(status),
|
|
8224
9692
|
getTask: (taskId) => engine.get(taskId),
|
|
8225
9693
|
getWorkerConversation: (taskId) => engine.getWorkerConversation(taskId),
|
|
9694
|
+
// On-disk transcript access (issue #16): survives worker disposal, so a
|
|
9695
|
+
// finished/cancelled worker's scene stays readable by the controller.
|
|
9696
|
+
readTranscript: (workerId) => readWorkerTranscript(ctx.sessionDir, workerId),
|
|
9697
|
+
// Layered transcript access (issue #16 review): while the engine tracks
|
|
9698
|
+
// the worker, its live in-memory conversation is the source; the on-disk
|
|
9699
|
+
// jsonl lives under the worker instance's OWN id (agent-/cw-…), which
|
|
9700
|
+
// the engine exposes and tasks persist as workerTranscriptId.
|
|
9701
|
+
getWorkerConversationByWorkerId: (workerId) => engine.getWorkerConversationByWorkerId(workerId),
|
|
9702
|
+
getWorkerTranscriptId: (workerId) => engine.getWorkerTranscriptId(workerId),
|
|
9703
|
+
readTranscriptWindow: (workerId, opts2) => readWorkerTranscriptWindow(ctx.sessionDir, workerId, opts2),
|
|
9704
|
+
sessionDir: ctx.sessionDir,
|
|
9705
|
+
getActiveWorkers: () => engine.getActiveWorkers(),
|
|
9706
|
+
getWorkerLiveStatus: (workerId) => engine.getWorkerLiveStatus(workerId),
|
|
8226
9707
|
cancelTask: (taskId) => engine.cancel(taskId),
|
|
8227
9708
|
abortWorker: (taskId) => engine.abortByTaskId(taskId),
|
|
8228
9709
|
updateTask: (taskId, updates) => engine.updatePending(taskId, updates),
|
|
@@ -8522,6 +10003,14 @@ function isoMs(ms) {
|
|
|
8522
10003
|
if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
|
|
8523
10004
|
return new Date(ms).toISOString();
|
|
8524
10005
|
}
|
|
10006
|
+
function signalActivityTail(currentTool, lastActivityAt) {
|
|
10007
|
+
const parts = [];
|
|
10008
|
+
if (currentTool) parts.push(`tool=${currentTool}`);
|
|
10009
|
+
if (typeof lastActivityAt === "number" && Number.isFinite(lastActivityAt)) {
|
|
10010
|
+
parts.push(`lastEntry=${new Date(lastActivityAt).toISOString().replace(/\.\d{3}Z$/, "Z")}`);
|
|
10011
|
+
}
|
|
10012
|
+
return parts.length > 0 ? `[activity: ${parts.join(", ")}]` : null;
|
|
10013
|
+
}
|
|
8525
10014
|
var signalConverters = {
|
|
8526
10015
|
input: (sig, ts, sessionId) => ({
|
|
8527
10016
|
type: "user" /* User */,
|
|
@@ -8563,6 +10052,7 @@ var signalConverters = {
|
|
|
8563
10052
|
},
|
|
8564
10053
|
progress: (sig, ts, sessionId) => {
|
|
8565
10054
|
const p = sig.payload;
|
|
10055
|
+
const activity = signalActivityTail(p.currentTool, p.lastActivityAt);
|
|
8566
10056
|
return {
|
|
8567
10057
|
...signalBase(ts, sessionId),
|
|
8568
10058
|
subtype: "worker_progress" /* WorkerProgress */,
|
|
@@ -8571,12 +10061,17 @@ var signalConverters = {
|
|
|
8571
10061
|
emoji: p.emoji,
|
|
8572
10062
|
taskId: p.taskId,
|
|
8573
10063
|
taskTitle: p.taskTitle,
|
|
8574
|
-
content: p.message
|
|
10064
|
+
content: activity ? `${p.message}
|
|
10065
|
+
${activity}` : p.message
|
|
8575
10066
|
};
|
|
8576
10067
|
},
|
|
8577
10068
|
heartbeat: (sig, ts, sessionId) => {
|
|
8578
10069
|
const p = sig.payload;
|
|
8579
|
-
const workerSummary = p.workers.map((w) =>
|
|
10070
|
+
const workerSummary = p.workers.map((w) => {
|
|
10071
|
+
const tool2 = w.currentTool ? `, tool ${w.currentTool}` : "";
|
|
10072
|
+
const idle = w.lastActivityAgoMs !== null && w.lastActivityAgoMs !== void 0 ? `, last activity ${Math.round(w.lastActivityAgoMs / 1e3)}s ago` : "";
|
|
10073
|
+
return `${w.taskTitle} [${w.workerId}] (${w.workerType}, running ${Math.round(w.runningForMs / 1e3)}s${tool2}${idle})`;
|
|
10074
|
+
}).join(", ");
|
|
8580
10075
|
return {
|
|
8581
10076
|
...signalBase(ts, sessionId),
|
|
8582
10077
|
subtype: "heartbeat" /* Heartbeat */,
|
|
@@ -8739,8 +10234,8 @@ function signalToEntry(sig, ts, sessionId) {
|
|
|
8739
10234
|
}
|
|
8740
10235
|
|
|
8741
10236
|
// src/services/skills/registry.ts
|
|
8742
|
-
import { readFileSync as
|
|
8743
|
-
import { join as
|
|
10237
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
10238
|
+
import { join as join13 } from "path";
|
|
8744
10239
|
import { parse as parseYaml } from "yaml";
|
|
8745
10240
|
var SKILL_FILE = "SKILL.md";
|
|
8746
10241
|
function parseFrontmatter(raw) {
|
|
@@ -8775,7 +10270,7 @@ function listResources(skillDir) {
|
|
|
8775
10270
|
const walk = (relDir) => {
|
|
8776
10271
|
let entries;
|
|
8777
10272
|
try {
|
|
8778
|
-
entries = readdirSync2(
|
|
10273
|
+
entries = readdirSync2(join13(skillDir, relDir), { withFileTypes: true });
|
|
8779
10274
|
} catch {
|
|
8780
10275
|
return;
|
|
8781
10276
|
}
|
|
@@ -8822,10 +10317,10 @@ var SkillRegistry = class {
|
|
|
8822
10317
|
if (existing) return existing;
|
|
8823
10318
|
const desc = this.descriptors.get(name);
|
|
8824
10319
|
if (!desc) return void 0;
|
|
8825
|
-
const filePath =
|
|
10320
|
+
const filePath = join13(desc.dir, SKILL_FILE);
|
|
8826
10321
|
let raw;
|
|
8827
10322
|
try {
|
|
8828
|
-
raw =
|
|
10323
|
+
raw = readFileSync9(filePath, "utf-8");
|
|
8829
10324
|
} catch (err) {
|
|
8830
10325
|
this.logger?.warn({ skill: name, error: err instanceof Error ? err.message : String(err) }, "Failed to read skill file");
|
|
8831
10326
|
return void 0;
|
|
@@ -8853,8 +10348,8 @@ var SkillRegistry = class {
|
|
|
8853
10348
|
for (const entry of entries) {
|
|
8854
10349
|
if (!entry.isDirectory()) continue;
|
|
8855
10350
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
8856
|
-
const skillDir =
|
|
8857
|
-
const skillFile =
|
|
10351
|
+
const skillDir = join13(dir, entry.name);
|
|
10352
|
+
const skillFile = join13(skillDir, SKILL_FILE);
|
|
8858
10353
|
if (!existsAsFile(skillFile)) continue;
|
|
8859
10354
|
const { frontmatter } = safeReadFrontmatter(skillFile);
|
|
8860
10355
|
const description = asString(frontmatter.description);
|
|
@@ -8869,35 +10364,35 @@ var SkillRegistry = class {
|
|
|
8869
10364
|
};
|
|
8870
10365
|
function existsAsFile(path7) {
|
|
8871
10366
|
try {
|
|
8872
|
-
return
|
|
10367
|
+
return statSync3(path7).isFile();
|
|
8873
10368
|
} catch {
|
|
8874
10369
|
return false;
|
|
8875
10370
|
}
|
|
8876
10371
|
}
|
|
8877
10372
|
function safeReadFrontmatter(path7) {
|
|
8878
10373
|
try {
|
|
8879
|
-
return parseFrontmatter(
|
|
10374
|
+
return parseFrontmatter(readFileSync9(path7, "utf-8"));
|
|
8880
10375
|
} catch {
|
|
8881
10376
|
return { frontmatter: {}, body: "" };
|
|
8882
10377
|
}
|
|
8883
10378
|
}
|
|
8884
10379
|
|
|
8885
10380
|
// src/services/skills/skill-dir.ts
|
|
8886
|
-
import { existsSync as
|
|
10381
|
+
import { existsSync as existsSync8 } from "fs";
|
|
8887
10382
|
import { homedir as homedir2 } from "os";
|
|
8888
|
-
import { join as
|
|
10383
|
+
import { join as join14, resolve as resolve10 } from "path";
|
|
8889
10384
|
function resolveSkillDirs(workspaceDir2, configDirs) {
|
|
8890
10385
|
const candidates = [
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
...(configDirs ?? []).map((d) =>
|
|
10386
|
+
join14(homedir2(), ".wayang", "skills"),
|
|
10387
|
+
join14(workspaceDir2, ".wayang", "skills"),
|
|
10388
|
+
...(configDirs ?? []).map((d) => resolve10(d))
|
|
8894
10389
|
];
|
|
8895
10390
|
const seen = /* @__PURE__ */ new Set();
|
|
8896
10391
|
const dirs = [];
|
|
8897
10392
|
for (const dir of candidates) {
|
|
8898
10393
|
if (seen.has(dir)) continue;
|
|
8899
10394
|
seen.add(dir);
|
|
8900
|
-
if (
|
|
10395
|
+
if (existsSync8(dir)) dirs.push(dir);
|
|
8901
10396
|
}
|
|
8902
10397
|
return dirs;
|
|
8903
10398
|
}
|
|
@@ -8956,7 +10451,8 @@ var Supervisor = class {
|
|
|
8956
10451
|
{
|
|
8957
10452
|
getRunningCount: () => this.engine.getRunningCount(),
|
|
8958
10453
|
getActiveWorkers: () => this.engine.getActiveWorkers(),
|
|
8959
|
-
getPendingCount: () => this.engine.list("pending").length
|
|
10454
|
+
getPendingCount: () => this.engine.list("pending").length,
|
|
10455
|
+
getWorkerActivity: (workerId) => this.engine.getWorkerActivity(workerId)
|
|
8960
10456
|
}
|
|
8961
10457
|
);
|
|
8962
10458
|
}
|
|
@@ -9245,8 +10741,11 @@ async function runWayangServer(options, io = process) {
|
|
|
9245
10741
|
return { status };
|
|
9246
10742
|
});
|
|
9247
10743
|
rpc.register(METHOD.CHAT_WORKER, (params) => {
|
|
9248
|
-
supervisor.engine.sendMessageToWorker(
|
|
9249
|
-
|
|
10744
|
+
const status = supervisor.engine.sendMessageToWorker(
|
|
10745
|
+
requiredString(params, "workerId"),
|
|
10746
|
+
requiredString(params, "message")
|
|
10747
|
+
);
|
|
10748
|
+
return { status };
|
|
9250
10749
|
});
|
|
9251
10750
|
rpc.register(METHOD.DISPOSE_WORKER, (params) => {
|
|
9252
10751
|
supervisor.engine.abortByWorkerId(requiredString(params, "workerId"));
|