wayang-ai 0.6.3 → 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 +1993 -537
- 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";
|
|
@@ -1372,6 +1372,7 @@ function WorkerMessageRow({ section: section2, result, lookups }) {
|
|
|
1372
1372
|
const worker = summary.workerId ? lookups.workers.get(summary.workerId) : void 0;
|
|
1373
1373
|
const target = formatWorkerTag(worker ?? { workerId: summary.workerId }) || "(\u672A\u77E5 worker)";
|
|
1374
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";
|
|
1375
1376
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1376
1377
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1377
1378
|
/* @__PURE__ */ jsxs2(Text2, { color: iconColor, bold: true, children: [
|
|
@@ -1382,7 +1383,7 @@ function WorkerMessageRow({ section: section2, result, lookups }) {
|
|
|
1382
1383
|
" ",
|
|
1383
1384
|
target
|
|
1384
1385
|
] }),
|
|
1385
|
-
/* @__PURE__ */ jsx3(OutcomeMark, { outcome: summary.outcome, ok:
|
|
1386
|
+
/* @__PURE__ */ jsx3(OutcomeMark, { outcome: summary.outcome, ok: delivered, failed: "\u672A\u9001\u8FBE" })
|
|
1386
1387
|
] }),
|
|
1387
1388
|
/* @__PURE__ */ jsx3(PreviewLines, { lines: summary.lines, hidden: summary.hiddenLines }),
|
|
1388
1389
|
summary.failure && /* @__PURE__ */ jsx3(SecondaryLine, { color: theme.action.failColor, children: summary.failure })
|
|
@@ -1654,7 +1655,7 @@ var ChatArea = React2.memo(function ChatArea2({ items, busy, streamingItems, con
|
|
|
1654
1655
|
});
|
|
1655
1656
|
|
|
1656
1657
|
// src/tui/components/input-area.tsx
|
|
1657
|
-
import { useState as useState2, useMemo as useMemo2 } from "react";
|
|
1658
|
+
import { useState as useState2, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
1658
1659
|
|
|
1659
1660
|
// src/tui/utils/format-tokens.ts
|
|
1660
1661
|
function formatTokenCount(tokens) {
|
|
@@ -1801,21 +1802,302 @@ function useSlashCommands(supervisor) {
|
|
|
1801
1802
|
|
|
1802
1803
|
// src/tui/components/input-area.tsx
|
|
1803
1804
|
import { Box as Box6, Text as Text7, useInput } from "ink";
|
|
1804
|
-
|
|
1805
|
-
|
|
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 }) {
|
|
1806
2055
|
const [input, setInput] = useState2("");
|
|
1807
2056
|
const [cursor, setCursor] = useState2(0);
|
|
2057
|
+
const [anchor, setAnchor] = useState2(null);
|
|
1808
2058
|
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
2059
|
+
const historyRef = useRef2(initInputHistory(""));
|
|
1809
2060
|
const suggestions = useMemo2(() => {
|
|
1810
2061
|
if (!input.startsWith("/") || input.includes(" ")) return [];
|
|
1811
2062
|
const typed = input.slice(1).toLowerCase();
|
|
1812
2063
|
return SLASH_COMMANDS.filter((c) => c.name.startsWith(typed));
|
|
1813
2064
|
}, [input]);
|
|
1814
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;
|
|
1815
2072
|
const clampedIndex = showSuggestions ? Math.min(selectedIndex, suggestions.length - 1) : 0;
|
|
1816
2073
|
const reset = useMemoizedFn(() => {
|
|
1817
2074
|
setInput("");
|
|
1818
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);
|
|
1819
2101
|
setSelectedIndex(0);
|
|
1820
2102
|
});
|
|
1821
2103
|
const applySuggestion = useMemoizedFn((index) => {
|
|
@@ -1823,9 +2105,7 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1823
2105
|
if (!cmd) return;
|
|
1824
2106
|
const text = `/${cmd.name}`;
|
|
1825
2107
|
const filled = "args" in cmd ? `${text} ` : text;
|
|
1826
|
-
|
|
1827
|
-
setCursor(filled.length);
|
|
1828
|
-
setSelectedIndex(0);
|
|
2108
|
+
applyEdit(replaceRange(input, 0, input.length, filled), "replace");
|
|
1829
2109
|
});
|
|
1830
2110
|
useInput((ch, key) => {
|
|
1831
2111
|
if (key.return) {
|
|
@@ -1875,39 +2155,133 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1875
2155
|
return;
|
|
1876
2156
|
}
|
|
1877
2157
|
if (key.escape) {
|
|
2158
|
+
if (hasSelection) {
|
|
2159
|
+
setAnchor(null);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
1878
2162
|
if (showSuggestions) {
|
|
1879
|
-
|
|
1880
|
-
setCursor(0);
|
|
1881
|
-
setSelectedIndex(0);
|
|
2163
|
+
reset();
|
|
1882
2164
|
} else {
|
|
1883
2165
|
onEscape?.();
|
|
1884
2166
|
reset();
|
|
1885
2167
|
}
|
|
1886
2168
|
return;
|
|
1887
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
|
+
}
|
|
1888
2250
|
if (key.backspace || key.delete) {
|
|
2251
|
+
if (hasSelection) {
|
|
2252
|
+
applyEdit(replaceRange(input, sel.start, sel.end, ""), "delete");
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
1889
2255
|
if (cursor === 0) return;
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2256
|
+
const len = prevCodePointLength(input, cursor);
|
|
2257
|
+
applyEdit(
|
|
2258
|
+
{ value: input.slice(0, cursor - len) + input.slice(cursor), cursor: cursor - len },
|
|
2259
|
+
"delete"
|
|
2260
|
+
);
|
|
1893
2261
|
return;
|
|
1894
2262
|
}
|
|
1895
2263
|
if (key.leftArrow) {
|
|
1896
|
-
|
|
2264
|
+
setAnchor(null);
|
|
2265
|
+
setCursor((c) => Math.max(0, c - prevCodePointLength(input, c)));
|
|
1897
2266
|
return;
|
|
1898
2267
|
}
|
|
1899
2268
|
if (key.rightArrow) {
|
|
1900
|
-
|
|
2269
|
+
setAnchor(null);
|
|
2270
|
+
setCursor((c) => Math.min(input.length, c + nextCodePointLength(input, c)));
|
|
1901
2271
|
return;
|
|
1902
2272
|
}
|
|
1903
2273
|
if (ch && !key.ctrl && !key.meta) {
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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");
|
|
1907
2278
|
}
|
|
1908
2279
|
});
|
|
1909
|
-
const
|
|
1910
|
-
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
|
+
);
|
|
1911
2285
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1912
2286
|
showSuggestions && /* @__PURE__ */ jsx9(Box6, { flexDirection: "column", paddingX: 1, marginBottom: 0, children: suggestions.map((cmd, i) => {
|
|
1913
2287
|
const isSelected = i === clampedIndex;
|
|
@@ -1921,7 +2295,7 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1921
2295
|
] })
|
|
1922
2296
|
] }, cmd.name);
|
|
1923
2297
|
}) }),
|
|
1924
|
-
/* @__PURE__ */
|
|
2298
|
+
/* @__PURE__ */ jsx9(
|
|
1925
2299
|
Box6,
|
|
1926
2300
|
{
|
|
1927
2301
|
borderStyle: "single",
|
|
@@ -1929,16 +2303,26 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1929
2303
|
borderLeft: false,
|
|
1930
2304
|
borderRight: false,
|
|
1931
2305
|
paddingX: 1,
|
|
1932
|
-
children:
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
/* @__PURE__ */
|
|
1940
|
-
|
|
1941
|
-
|
|
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
|
+
}) })
|
|
1942
2326
|
}
|
|
1943
2327
|
)
|
|
1944
2328
|
] });
|
|
@@ -1947,13 +2331,59 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
|
|
|
1947
2331
|
// src/tui/components/inquiry-prompt.tsx
|
|
1948
2332
|
import React4, { useState as useState3 } from "react";
|
|
1949
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
|
|
1950
2372
|
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1951
2373
|
function InquiryPrompt({ inquiry, onAnswer }) {
|
|
1952
2374
|
if (inquiry.type === "confirm") {
|
|
1953
2375
|
return /* @__PURE__ */ jsx10(ConfirmPrompt, { message: inquiry.message, defaultVal: inquiry.default, onAnswer });
|
|
1954
2376
|
}
|
|
1955
2377
|
if (inquiry.type === "select") {
|
|
1956
|
-
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
|
+
);
|
|
1957
2387
|
}
|
|
1958
2388
|
return /* @__PURE__ */ jsx10(TextPrompt, { message: inquiry.message, onAnswer });
|
|
1959
2389
|
}
|
|
@@ -1977,25 +2407,53 @@ function ConfirmPrompt({ message, defaultVal, onAnswer }) {
|
|
|
1977
2407
|
] }, opt)) })
|
|
1978
2408
|
] });
|
|
1979
2409
|
}
|
|
1980
|
-
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);
|
|
1981
2435
|
const [selected, setSelected] = useState3(0);
|
|
1982
2436
|
useInput2((_ch, key) => {
|
|
1983
|
-
if (key.upArrow) setSelected((i) => (i - 1 +
|
|
1984
|
-
if (key.downArrow) setSelected((i) => (i + 1) %
|
|
1985
|
-
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
|
+
}
|
|
1986
2444
|
});
|
|
1987
2445
|
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1988
2446
|
/* @__PURE__ */ jsxs7(Box7, { paddingX: 1, borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: [
|
|
1989
2447
|
/* @__PURE__ */ jsx10(Text8, { color: "cyan", children: "? " }),
|
|
1990
2448
|
/* @__PURE__ */ jsx10(Text8, { bold: true, children: message })
|
|
1991
2449
|
] }),
|
|
1992
|
-
/* @__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: [
|
|
1993
2451
|
/* @__PURE__ */ jsx10(Text8, { color: i === selected ? theme.baseToken.color.accent : void 0, children: i === selected ? "\u25B8 " : " " }),
|
|
1994
|
-
/* @__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 })
|
|
1995
2453
|
] }, i)) })
|
|
1996
2454
|
] });
|
|
1997
2455
|
}
|
|
1998
|
-
function TextPrompt({ message, onAnswer }) {
|
|
2456
|
+
function TextPrompt({ message, onAnswer, inputLabel }) {
|
|
1999
2457
|
const [input, setInput] = useState3("");
|
|
2000
2458
|
const [cursor, setCursor] = useState3(0);
|
|
2001
2459
|
useInput2((ch, key) => {
|
|
@@ -2031,10 +2489,12 @@ function TextPrompt({ message, onAnswer }) {
|
|
|
2031
2489
|
/* @__PURE__ */ jsx10(Text8, { bold: true, children: message })
|
|
2032
2490
|
] }),
|
|
2033
2491
|
/* @__PURE__ */ jsxs7(Box7, { paddingX: 1, borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: [
|
|
2034
|
-
/* @__PURE__ */ jsxs7(Text8, { children: [
|
|
2492
|
+
inputLabel ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
2035
2493
|
" ",
|
|
2036
|
-
|
|
2037
|
-
|
|
2494
|
+
inputLabel,
|
|
2495
|
+
": "
|
|
2496
|
+
] }) : /* @__PURE__ */ jsx10(Text8, { children: " " }),
|
|
2497
|
+
/* @__PURE__ */ jsx10(Text8, { children: before }),
|
|
2038
2498
|
/* @__PURE__ */ jsx10(Text8, { color: "white", inverse: true, children: after.length > 0 ? after[0] : " " }),
|
|
2039
2499
|
/* @__PURE__ */ jsx10(Text8, { children: after.slice(1) })
|
|
2040
2500
|
] })
|
|
@@ -2173,11 +2633,6 @@ function OverlayPanel({ overlay }) {
|
|
|
2173
2633
|
] });
|
|
2174
2634
|
}
|
|
2175
2635
|
|
|
2176
|
-
// src/types/signal.ts
|
|
2177
|
-
function getSignalField(sig, field) {
|
|
2178
|
-
return sig.payload[field];
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
2636
|
// src/tui/types/display-item.ts
|
|
2182
2637
|
function entryToDisplayItem(entry) {
|
|
2183
2638
|
const base = {
|
|
@@ -2405,7 +2860,7 @@ function ControllerPage({ onExit }) {
|
|
|
2405
2860
|
}
|
|
2406
2861
|
) }),
|
|
2407
2862
|
overlay && /* @__PURE__ */ jsx13(OverlayPanel, { overlay }),
|
|
2408
|
-
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 }),
|
|
2409
2864
|
/* @__PURE__ */ jsx13(
|
|
2410
2865
|
StatusBar,
|
|
2411
2866
|
{
|
|
@@ -2427,7 +2882,7 @@ function AppContent() {
|
|
|
2427
2882
|
const { exit } = useApp();
|
|
2428
2883
|
const supervisor = useSupervisor();
|
|
2429
2884
|
const busy = useWayangState(supervisor.controllerAgent, "dynamicState.busy") ?? false;
|
|
2430
|
-
const lastCtrlC =
|
|
2885
|
+
const lastCtrlC = useRef3(0);
|
|
2431
2886
|
const handleExit = useMemoizedFn(() => {
|
|
2432
2887
|
exit();
|
|
2433
2888
|
});
|
|
@@ -2451,7 +2906,7 @@ function AppContent() {
|
|
|
2451
2906
|
// package.json
|
|
2452
2907
|
var package_default = {
|
|
2453
2908
|
name: "wayang-ai",
|
|
2454
|
-
version: "0.
|
|
2909
|
+
version: "0.7.0",
|
|
2455
2910
|
description: "Multi-agent orchestration platform",
|
|
2456
2911
|
type: "module",
|
|
2457
2912
|
license: "MIT",
|
|
@@ -2553,13 +3008,13 @@ function writeWelcome(workspace) {
|
|
|
2553
3008
|
|
|
2554
3009
|
// src/tui/render.ts
|
|
2555
3010
|
function renderInkUI(supervisor) {
|
|
2556
|
-
return new Promise((
|
|
3011
|
+
return new Promise((resolve12) => {
|
|
2557
3012
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
2558
3013
|
writeWelcome(supervisor.ctx.workspaceDir);
|
|
2559
3014
|
const { waitUntilExit } = render(
|
|
2560
3015
|
React6.createElement(App, { supervisor })
|
|
2561
3016
|
);
|
|
2562
|
-
waitUntilExit().then(() =>
|
|
3017
|
+
waitUntilExit().then(() => resolve12());
|
|
2563
3018
|
});
|
|
2564
3019
|
}
|
|
2565
3020
|
|
|
@@ -2568,26 +3023,6 @@ import * as fs3 from "fs";
|
|
|
2568
3023
|
import * as path3 from "path";
|
|
2569
3024
|
import * as childProcess from "child_process";
|
|
2570
3025
|
import prompts from "prompts";
|
|
2571
|
-
|
|
2572
|
-
// src/types/config.ts
|
|
2573
|
-
function validateConfig(config) {
|
|
2574
|
-
for (const [name, provider] of Object.entries(config.providers)) {
|
|
2575
|
-
if (!provider.endpoint) return `providers.${name}.endpoint is required`;
|
|
2576
|
-
if (!provider.apiKey) return `providers.${name}.apiKey is required (set WAYANG_LLM_API_KEY or configure in config file)`;
|
|
2577
|
-
if (!provider.modelName) return `providers.${name}.modelName is required`;
|
|
2578
|
-
const window = provider.contextWindowTokens;
|
|
2579
|
-
if (window !== void 0 && !(Number.isInteger(window) && window > 0)) {
|
|
2580
|
-
return `providers.${name}.contextWindowTokens must be a positive integer (got ${JSON.stringify(window)})`;
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
const cp = config.controller.provider;
|
|
2584
|
-
if (!config.providers[cp]) return `controller.provider "${cp}" not found`;
|
|
2585
|
-
const wp = config.worker.provider;
|
|
2586
|
-
if (!config.providers[wp]) return `worker.provider "${wp}" not found`;
|
|
2587
|
-
return null;
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
// src/onboard.ts
|
|
2591
3026
|
function fillApiKeys(config) {
|
|
2592
3027
|
const envKey = process.env.WAYANG_LLM_API_KEY || "";
|
|
2593
3028
|
for (const provider of Object.values(config.providers)) {
|
|
@@ -3511,10 +3946,10 @@ var SignalQueue = class {
|
|
|
3511
3946
|
waitForSignal() {
|
|
3512
3947
|
const unread = this.state.get("signals").filter((s) => s.status === "unread");
|
|
3513
3948
|
if (unread.length > 0) return Promise.resolve();
|
|
3514
|
-
return new Promise((
|
|
3949
|
+
return new Promise((resolve12) => {
|
|
3515
3950
|
this.resolveWait = () => {
|
|
3516
3951
|
this.logger.debug("waitForSignal resolved");
|
|
3517
|
-
|
|
3952
|
+
resolve12();
|
|
3518
3953
|
};
|
|
3519
3954
|
});
|
|
3520
3955
|
}
|
|
@@ -3574,267 +4009,12 @@ var SignalQueue = class {
|
|
|
3574
4009
|
}
|
|
3575
4010
|
};
|
|
3576
4011
|
|
|
3577
|
-
// src/services/
|
|
3578
|
-
import {
|
|
3579
|
-
|
|
3580
|
-
jsonFile;
|
|
3581
|
-
logger;
|
|
3582
|
-
constructor(ctx) {
|
|
3583
|
-
const jsonFile = new JSONFileHelper(join6(ctx.sessionDir, "tasks.json"));
|
|
3584
|
-
super(
|
|
3585
|
-
{
|
|
3586
|
-
tasks: {
|
|
3587
|
-
pending: [],
|
|
3588
|
-
running: [],
|
|
3589
|
-
history: []
|
|
3590
|
-
}
|
|
3591
|
-
},
|
|
3592
|
-
[{ path: "tasks", helper: jsonFile }]
|
|
3593
|
-
);
|
|
3594
|
-
this.jsonFile = jsonFile;
|
|
3595
|
-
this.logger = ctx.logger;
|
|
3596
|
-
}
|
|
3597
|
-
async restore() {
|
|
3598
|
-
const data = this.jsonFile.read();
|
|
3599
|
-
if (data) {
|
|
3600
|
-
this.data.tasks = data;
|
|
3601
|
-
}
|
|
3602
|
-
this.logger.info("TaskPoolState restored");
|
|
3603
|
-
}
|
|
3604
|
-
};
|
|
3605
|
-
|
|
3606
|
-
// src/services/agents/base-agent.ts
|
|
3607
|
-
import { streamText, stepCountIs, hasToolCall } from "ai";
|
|
3608
|
-
|
|
3609
|
-
// src/services/agents/model-factory.ts
|
|
3610
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
3611
|
-
function createModel(provider) {
|
|
3612
|
-
const compatible = createOpenAICompatible({
|
|
3613
|
-
name: "wayang",
|
|
3614
|
-
baseURL: provider.endpoint,
|
|
3615
|
-
apiKey: provider.apiKey
|
|
3616
|
-
});
|
|
3617
|
-
return compatible.chatModel(provider.modelName);
|
|
3618
|
-
}
|
|
3619
|
-
|
|
3620
|
-
// src/utils/id.ts
|
|
3621
|
-
function generateId(prefix) {
|
|
3622
|
-
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
3623
|
-
}
|
|
3624
|
-
function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
3625
|
-
const Y = date.getFullYear();
|
|
3626
|
-
const M = String(date.getMonth() + 1).padStart(2, "0");
|
|
3627
|
-
const D = String(date.getDate()).padStart(2, "0");
|
|
3628
|
-
const h = String(date.getHours()).padStart(2, "0");
|
|
3629
|
-
const m = String(date.getMinutes()).padStart(2, "0");
|
|
3630
|
-
const s = String(date.getSeconds()).padStart(2, "0");
|
|
3631
|
-
return `${Y}${M}${D}-${h}${m}${s}`;
|
|
3632
|
-
}
|
|
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";
|
|
3633
4015
|
|
|
3634
|
-
// src/
|
|
3635
|
-
|
|
3636
|
-
const now = /* @__PURE__ */ new Date();
|
|
3637
|
-
const off = -now.getTimezoneOffset();
|
|
3638
|
-
const sign = off >= 0 ? "+" : "-";
|
|
3639
|
-
const absOff = Math.abs(off);
|
|
3640
|
-
const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
|
|
3641
|
-
const mm = String(absOff % 60).padStart(2, "0");
|
|
3642
|
-
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
3643
|
-
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}`;
|
|
3644
|
-
}
|
|
3645
|
-
|
|
3646
|
-
// src/services/agents/base-agent.ts
|
|
3647
|
-
var DEFAULT_LLM_IDLE_TIMEOUT_MS = 3e5;
|
|
3648
|
-
var BaseAgent = class {
|
|
3649
|
-
id;
|
|
3650
|
-
abortController;
|
|
3651
|
-
model;
|
|
3652
|
-
hooks = {};
|
|
3653
|
-
constructor(provider) {
|
|
3654
|
-
this.id = generateId("agent");
|
|
3655
|
-
this.abortController = new AbortController();
|
|
3656
|
-
this.model = createModel(provider);
|
|
3657
|
-
}
|
|
3658
|
-
/** Set lifecycle hooks. */
|
|
3659
|
-
setHooks(hooks) {
|
|
3660
|
-
this.hooks = hooks;
|
|
3661
|
-
}
|
|
3662
|
-
/** Core streaming primitive — yields text chunks, returns final result.
|
|
3663
|
-
* Catches abort signal internally so callers see a clean completion. */
|
|
3664
|
-
async *streamLoop(opts) {
|
|
3665
|
-
const step = 0;
|
|
3666
|
-
const idleTimeoutMs = opts.llmIdleTimeoutMs ?? DEFAULT_LLM_IDLE_TIMEOUT_MS;
|
|
3667
|
-
const callAbort = new AbortController();
|
|
3668
|
-
const forwardAbort = () => callAbort.abort();
|
|
3669
|
-
if (this.abortController.signal.aborted) {
|
|
3670
|
-
callAbort.abort();
|
|
3671
|
-
} else {
|
|
3672
|
-
this.abortController.signal.addEventListener("abort", forwardAbort);
|
|
3673
|
-
}
|
|
3674
|
-
let watchdog = null;
|
|
3675
|
-
let timedOut = false;
|
|
3676
|
-
let toolsInFlight = 0;
|
|
3677
|
-
let finished = false;
|
|
3678
|
-
const clearWatchdog = () => {
|
|
3679
|
-
if (watchdog !== null) {
|
|
3680
|
-
clearTimeout(watchdog);
|
|
3681
|
-
watchdog = null;
|
|
3682
|
-
}
|
|
3683
|
-
};
|
|
3684
|
-
const armWatchdog = () => {
|
|
3685
|
-
if (idleTimeoutMs <= 0 || finished || toolsInFlight > 0) return;
|
|
3686
|
-
clearWatchdog();
|
|
3687
|
-
watchdog = setTimeout(() => {
|
|
3688
|
-
timedOut = true;
|
|
3689
|
-
callAbort.abort();
|
|
3690
|
-
}, idleTimeoutMs);
|
|
3691
|
-
};
|
|
3692
|
-
const tools = pauseWatchdogDuringTools(
|
|
3693
|
-
opts.tools,
|
|
3694
|
-
() => {
|
|
3695
|
-
toolsInFlight++;
|
|
3696
|
-
clearWatchdog();
|
|
3697
|
-
},
|
|
3698
|
-
() => {
|
|
3699
|
-
toolsInFlight--;
|
|
3700
|
-
if (toolsInFlight === 0) armWatchdog();
|
|
3701
|
-
}
|
|
3702
|
-
);
|
|
3703
|
-
let stream;
|
|
3704
|
-
const stopConditions = [];
|
|
3705
|
-
if (opts.maxSteps) stopConditions.push(stepCountIs(opts.maxSteps));
|
|
3706
|
-
if (opts.stopTools?.length) {
|
|
3707
|
-
for (const name of opts.stopTools) {
|
|
3708
|
-
stopConditions.push(hasToolCall(name));
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
let startTime;
|
|
3712
|
-
let firstChunkAt;
|
|
3713
|
-
let stepStartedAt;
|
|
3714
|
-
const onStep = opts.onStep ? (event) => {
|
|
3715
|
-
armWatchdog();
|
|
3716
|
-
const meta = { startedAt: stepStartedAt, firstTokenAt: firstChunkAt };
|
|
3717
|
-
firstChunkAt = void 0;
|
|
3718
|
-
stepStartedAt = nowISO();
|
|
3719
|
-
opts.onStep(event, meta);
|
|
3720
|
-
} : void 0;
|
|
3721
|
-
try {
|
|
3722
|
-
this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
|
|
3723
|
-
startTime = Date.now();
|
|
3724
|
-
stepStartedAt = nowISO();
|
|
3725
|
-
armWatchdog();
|
|
3726
|
-
stream = streamText({
|
|
3727
|
-
model: this.model,
|
|
3728
|
-
system: opts.system,
|
|
3729
|
-
messages: opts.messages,
|
|
3730
|
-
tools,
|
|
3731
|
-
toolChoice: opts.toolChoice,
|
|
3732
|
-
stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
|
|
3733
|
-
abortSignal: callAbort.signal,
|
|
3734
|
-
...opts.prepareStep && { prepareStep: opts.prepareStep },
|
|
3735
|
-
onStepFinish: onStep,
|
|
3736
|
-
onError: ({ error }) => {
|
|
3737
|
-
opts.onStreamError?.(error);
|
|
3738
|
-
},
|
|
3739
|
-
// SignalEntry entries are trusted system-generated notifications emitted
|
|
3740
|
-
// as `system`-role messages (in-band, order-sensitive). AI SDK v6 warns
|
|
3741
|
-
// on any system message inside `messages` unless this is opted in.
|
|
3742
|
-
allowSystemInMessages: true
|
|
3743
|
-
});
|
|
3744
|
-
} catch (err) {
|
|
3745
|
-
finished = true;
|
|
3746
|
-
clearWatchdog();
|
|
3747
|
-
this.abortController.signal.removeEventListener("abort", forwardAbort);
|
|
3748
|
-
if (this.abortController.signal.aborted) {
|
|
3749
|
-
return { text: "", toolResults: [], usage: void 0 };
|
|
3750
|
-
}
|
|
3751
|
-
throw err;
|
|
3752
|
-
}
|
|
3753
|
-
try {
|
|
3754
|
-
for await (const chunk of stream.textStream) {
|
|
3755
|
-
armWatchdog();
|
|
3756
|
-
if (chunk && firstChunkAt === void 0) firstChunkAt = nowISO();
|
|
3757
|
-
yield chunk;
|
|
3758
|
-
}
|
|
3759
|
-
} catch (err) {
|
|
3760
|
-
if (this.abortController.signal.aborted) {
|
|
3761
|
-
return { text: "", toolResults: [], usage: void 0 };
|
|
3762
|
-
}
|
|
3763
|
-
if (timedOut) {
|
|
3764
|
-
throw new Error(
|
|
3765
|
-
`LLM call idle timeout: no stream progress (text chunk or step finish) for ${idleTimeoutMs}ms`
|
|
3766
|
-
);
|
|
3767
|
-
}
|
|
3768
|
-
if (this.abortController.signal.aborted) {
|
|
3769
|
-
return { text: "", toolResults: [], usage: void 0 };
|
|
3770
|
-
}
|
|
3771
|
-
throw err;
|
|
3772
|
-
} finally {
|
|
3773
|
-
finished = true;
|
|
3774
|
-
clearWatchdog();
|
|
3775
|
-
this.abortController.signal.removeEventListener("abort", forwardAbort);
|
|
3776
|
-
}
|
|
3777
|
-
const duration = Date.now() - startTime;
|
|
3778
|
-
const text = await stream.text;
|
|
3779
|
-
const reasoningText = await stream.reasoningText;
|
|
3780
|
-
const toolResults = await stream.toolResults;
|
|
3781
|
-
const rawUsage = await stream.usage;
|
|
3782
|
-
const finishReason = await stream.finishReason;
|
|
3783
|
-
const steps = await stream.steps;
|
|
3784
|
-
const usage = rawUsage ? { inputTokens: rawUsage.inputTokens ?? 0, outputTokens: rawUsage.outputTokens ?? 0 } : void 0;
|
|
3785
|
-
this.hooks.afterLLM?.({ step, usage, durationMs: duration });
|
|
3786
|
-
return {
|
|
3787
|
-
text,
|
|
3788
|
-
reasoningText,
|
|
3789
|
-
toolResults: toolResults?.map((tr) => ({
|
|
3790
|
-
toolCallId: tr.toolCallId,
|
|
3791
|
-
result: tr.result,
|
|
3792
|
-
isError: tr.isError
|
|
3793
|
-
})),
|
|
3794
|
-
usage,
|
|
3795
|
-
finishReason,
|
|
3796
|
-
steps: steps?.length
|
|
3797
|
-
};
|
|
3798
|
-
}
|
|
3799
|
-
/** Batch wrapper — iterates streamLoop and returns the final result. */
|
|
3800
|
-
async collectLoop(opts) {
|
|
3801
|
-
const gen = this.streamLoop(opts);
|
|
3802
|
-
let result;
|
|
3803
|
-
while (true) {
|
|
3804
|
-
const { done, value } = await gen.next();
|
|
3805
|
-
if (done) {
|
|
3806
|
-
result = value;
|
|
3807
|
-
break;
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
return result;
|
|
3811
|
-
}
|
|
3812
|
-
abort() {
|
|
3813
|
-
this.abortController.abort();
|
|
3814
|
-
}
|
|
3815
|
-
};
|
|
3816
|
-
function pauseWatchdogDuringTools(tools, onStart, onEnd) {
|
|
3817
|
-
const wrapped = {};
|
|
3818
|
-
for (const [name, def] of Object.entries(tools ?? {})) {
|
|
3819
|
-
const original = def?.execute;
|
|
3820
|
-
if (typeof original !== "function") {
|
|
3821
|
-
wrapped[name] = def;
|
|
3822
|
-
continue;
|
|
3823
|
-
}
|
|
3824
|
-
wrapped[name] = {
|
|
3825
|
-
...def,
|
|
3826
|
-
execute: async (...args) => {
|
|
3827
|
-
onStart();
|
|
3828
|
-
try {
|
|
3829
|
-
return await original(...args);
|
|
3830
|
-
} finally {
|
|
3831
|
-
onEnd();
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3834
|
-
};
|
|
3835
|
-
}
|
|
3836
|
-
return wrapped;
|
|
3837
|
-
}
|
|
4016
|
+
// src/services/agents/worker-state.ts
|
|
4017
|
+
import { join as join6 } from "path";
|
|
3838
4018
|
|
|
3839
4019
|
// src/services/agents/utils/conversation-to-sdk-messages.ts
|
|
3840
4020
|
function conversationToSdkMessages(entries) {
|
|
@@ -4008,6 +4188,32 @@ function toolOutputText(output) {
|
|
|
4008
4188
|
return toolPayloadText(output);
|
|
4009
4189
|
}
|
|
4010
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
|
+
|
|
4011
4217
|
// src/services/agents/context-manager.ts
|
|
4012
4218
|
var DEFAULT_CONTEXT_WINDOW_TOKENS = 256e3;
|
|
4013
4219
|
var COMPACT_THRESHOLD_RATIO = 0.9;
|
|
@@ -4247,12 +4453,14 @@ ${this.compactSummary}` : "";
|
|
|
4247
4453
|
};
|
|
4248
4454
|
|
|
4249
4455
|
// src/services/agents/worker-state.ts
|
|
4250
|
-
|
|
4456
|
+
function workerDir(sessionDir, workerId) {
|
|
4457
|
+
return join6(sessionDir, "workers", workerId);
|
|
4458
|
+
}
|
|
4251
4459
|
var WorkerState = class extends BaseWayangState {
|
|
4252
4460
|
constructor(sessionDir, workerId, logger) {
|
|
4253
|
-
const
|
|
4254
|
-
const jsonFile = new JSONFileHelper(
|
|
4255
|
-
const jsonlFile = new JSONLFileHelper(
|
|
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"));
|
|
4256
4464
|
const initialData = {
|
|
4257
4465
|
runtimeState: {
|
|
4258
4466
|
session: { id: workerId, startedAt: Date.now() },
|
|
@@ -4299,6 +4507,524 @@ var WorkerState = class extends BaseWayangState {
|
|
|
4299
4507
|
}
|
|
4300
4508
|
};
|
|
4301
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
|
+
};
|
|
4987
|
+
}
|
|
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;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
return result;
|
|
5000
|
+
}
|
|
5001
|
+
abort() {
|
|
5002
|
+
this.abortController.abort();
|
|
5003
|
+
}
|
|
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;
|
|
5012
|
+
}
|
|
5013
|
+
wrapped[name] = {
|
|
5014
|
+
...def,
|
|
5015
|
+
execute: async (...args) => {
|
|
5016
|
+
onStart();
|
|
5017
|
+
try {
|
|
5018
|
+
return await original(...args);
|
|
5019
|
+
} finally {
|
|
5020
|
+
onEnd();
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
};
|
|
5024
|
+
}
|
|
5025
|
+
return wrapped;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
4302
5028
|
// src/services/agents/utils/build-assistant-entry.ts
|
|
4303
5029
|
function buildAssistantEntry(event, sessionId, uuid, extras) {
|
|
4304
5030
|
const ts = nowISO();
|
|
@@ -4590,15 +5316,16 @@ function isInsideWorkspace(resolved, workspace) {
|
|
|
4590
5316
|
const rel = relative(workspace, resolved);
|
|
4591
5317
|
return !rel.startsWith("..") && resolve(workspace, rel) === resolved;
|
|
4592
5318
|
}
|
|
4593
|
-
function truncate(result) {
|
|
4594
|
-
if (result.length <=
|
|
4595
|
-
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)";
|
|
4596
5322
|
}
|
|
4597
|
-
function safeExecute(name, fn) {
|
|
4598
|
-
|
|
5323
|
+
function safeExecute(name, fn, options) {
|
|
5324
|
+
const maxChars = options?.maxResultChars ?? MAX_RESULT_CHARS;
|
|
5325
|
+
return async (args, callOptions) => {
|
|
4599
5326
|
try {
|
|
4600
|
-
const result = await fn(args,
|
|
4601
|
-
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);
|
|
4602
5329
|
} catch (err) {
|
|
4603
5330
|
return `[ERROR] ${name}: ${err.message ?? String(err)}`;
|
|
4604
5331
|
}
|
|
@@ -4752,12 +5479,12 @@ Check the "Available workers" section below for current options.
|
|
|
4752
5479
|
|
|
4753
5480
|
## How to write a good task description
|
|
4754
5481
|
|
|
4755
|
-
The worker only sees the task description \u2014 it has NO access to your conversation.
|
|
4756
5482
|
Write it as a **self-contained instruction**:
|
|
4757
5483
|
1. What to do (specific, unambiguous)
|
|
4758
5484
|
2. All context/data the worker needs
|
|
4759
5485
|
3. Expected output (file path, format, etc.)
|
|
4760
5486
|
4. Constraints (language, length, style)
|
|
5487
|
+
5. Acceptance criteria \u2014 what counts as done
|
|
4761
5488
|
|
|
4762
5489
|
Bad: "Write the essay the user asked for"
|
|
4763
5490
|
Good: "Write a 200-word Chinese prose essay about autumn. Save to sanwen.txt in UTF-8."
|
|
@@ -4823,7 +5550,29 @@ Use get_task_detail when you need more information about a specific task:
|
|
|
4823
5550
|
- You need to understand what a worker actually did before deciding next steps
|
|
4824
5551
|
- Checking the error details of a failed task before retrying
|
|
4825
5552
|
|
|
4826
|
-
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).
|
|
4827
5576
|
|
|
4828
5577
|
## Sending messages to workers (chat_worker)
|
|
4829
5578
|
|
|
@@ -4831,6 +5580,7 @@ Use chat_worker(workerId, message) to send guidance to a running worker.
|
|
|
4831
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.
|
|
4832
5581
|
Delivery is blind: "queued" only means the message sits in the worker's inbox until that boundary. A stuck worker never reads it.
|
|
4833
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).
|
|
4834
5584
|
Use sparingly \u2014 only when you need to:
|
|
4835
5585
|
- Correct a worker that is going in the wrong direction
|
|
4836
5586
|
- Provide additional context the worker did not have
|
|
@@ -4867,7 +5617,8 @@ Types:
|
|
|
4867
5617
|
Guidelines:
|
|
4868
5618
|
- Prefer confirm and select over text. Less typing = better experience.
|
|
4869
5619
|
- Keep messages short and specific.
|
|
4870
|
-
- 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.`
|
|
4871
5622
|
);
|
|
4872
5623
|
var SIGNAL_HANDLING = section(
|
|
4873
5624
|
"Handling signals",
|
|
@@ -4903,8 +5654,9 @@ Explain the error. Suggest a retry or alternative.
|
|
|
4903
5654
|
## [HEARTBEAT] \u2014 periodic check-in while workers are running
|
|
4904
5655
|
|
|
4905
5656
|
You were woken up because workers are running but no events arrived for a while.
|
|
4906
|
-
Review the worker status in the heartbeat message:
|
|
4907
|
-
- 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
|
|
4908
5660
|
- Everything looks normal \u2192 call skip_reply()
|
|
4909
5661
|
|
|
4910
5662
|
## [PERMISSION REQUEST ...] \u2014 worker needs approval
|
|
@@ -4927,6 +5679,39 @@ You resumed a previous session that had unfinished tasks. Workers were NOT autom
|
|
|
4927
5679
|
|
|
4928
5680
|
Do NOT blindly re-create everything. The user may have moved on. Prefer to summarize and ask unless the intent is obvious.`
|
|
4929
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
|
+
);
|
|
4930
5715
|
var HARD_CONSTRAINTS = section(
|
|
4931
5716
|
"Hard constraints",
|
|
4932
5717
|
`- NEVER execute commands or write files yourself. Delegate via add_task\uFF08worker will execute\uFF09.
|
|
@@ -4952,6 +5737,7 @@ function buildControllerSystemPrompt(ctx, skills) {
|
|
|
4952
5737
|
TOOL_USAGE,
|
|
4953
5738
|
PLANNING_TOOLS,
|
|
4954
5739
|
SIGNAL_HANDLING,
|
|
5740
|
+
WORKING_PRINCIPLES,
|
|
4955
5741
|
HARD_CONSTRAINTS,
|
|
4956
5742
|
buildWorkerList(ctx.config.workers),
|
|
4957
5743
|
buildSkillCatalog(skills),
|
|
@@ -4959,6 +5745,54 @@ function buildControllerSystemPrompt(ctx, skills) {
|
|
|
4959
5745
|
);
|
|
4960
5746
|
}
|
|
4961
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
|
+
|
|
4962
5796
|
// src/services/agents/prompts/worker-prompt.ts
|
|
4963
5797
|
var IDENTITY2 = section(
|
|
4964
5798
|
"Identity",
|
|
@@ -5110,6 +5944,10 @@ function buildWorkerSystemPrompt(ctx, skills) {
|
|
|
5110
5944
|
TOOL_USAGE2,
|
|
5111
5945
|
EXECUTION_STRATEGY,
|
|
5112
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())),
|
|
5113
5951
|
QUALITY_RULES,
|
|
5114
5952
|
HARD_CONSTRAINTS2,
|
|
5115
5953
|
CONTROLLER_COMMUNICATION,
|
|
@@ -5288,6 +6126,15 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
5288
6126
|
getState() {
|
|
5289
6127
|
return this.state;
|
|
5290
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
|
+
}
|
|
5291
6138
|
/** Get conversation entries. */
|
|
5292
6139
|
getConversation() {
|
|
5293
6140
|
return this.state.get("conversation") ?? [];
|
|
@@ -5350,6 +6197,56 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
5350
6197
|
import { query, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
5351
6198
|
import { z as z2 } from "zod";
|
|
5352
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
|
+
}
|
|
6249
|
+
|
|
5353
6250
|
// src/infra/blocking-requests.ts
|
|
5354
6251
|
var BlockingRequestMap = class {
|
|
5355
6252
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -5361,14 +6258,14 @@ var BlockingRequestMap = class {
|
|
|
5361
6258
|
if (this.pending.has(id)) {
|
|
5362
6259
|
throw new Error(`BlockingRequestMap: duplicate wait id "${id}"`);
|
|
5363
6260
|
}
|
|
5364
|
-
return new Promise((
|
|
6261
|
+
return new Promise((resolve12) => {
|
|
5365
6262
|
const settle = (value) => {
|
|
5366
6263
|
this.pending.delete(id);
|
|
5367
|
-
|
|
6264
|
+
resolve12(value);
|
|
5368
6265
|
};
|
|
5369
6266
|
const timer = setTimeout(() => {
|
|
5370
6267
|
this.pending.delete(id);
|
|
5371
|
-
|
|
6268
|
+
resolve12(makeTimeoutValue());
|
|
5372
6269
|
}, timeoutMs);
|
|
5373
6270
|
if (typeof timer === "object" && timer && "unref" in timer) {
|
|
5374
6271
|
timer.unref();
|
|
@@ -5405,6 +6302,8 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5405
6302
|
logger;
|
|
5406
6303
|
abortController = new AbortController();
|
|
5407
6304
|
_terminalResult = null;
|
|
6305
|
+
/** True once the SDK message stream of the current run has closed (issue #16). */
|
|
6306
|
+
_streamEnded = false;
|
|
5408
6307
|
// --- Controller messaging via streamInput ---
|
|
5409
6308
|
_messageQueue = [];
|
|
5410
6309
|
_messageResolve = null;
|
|
@@ -5422,6 +6321,16 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5422
6321
|
* explicitly instead of pretending to queue.
|
|
5423
6322
|
*/
|
|
5424
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.
|
|
6332
|
+
*/
|
|
6333
|
+
_pendingToolSteps = [];
|
|
5425
6334
|
// --- ask_controller via in-process SDK MCP server ---
|
|
5426
6335
|
_askController = null;
|
|
5427
6336
|
// --- Permission control via canUseTool ---
|
|
@@ -5487,8 +6396,10 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5487
6396
|
}
|
|
5488
6397
|
async run(task, _tools, onProgress) {
|
|
5489
6398
|
this._terminalResult = null;
|
|
6399
|
+
this._streamEnded = false;
|
|
5490
6400
|
this._inputClosed = false;
|
|
5491
6401
|
this._runInFlight = true;
|
|
6402
|
+
this._pendingToolSteps = [];
|
|
5492
6403
|
this.state.set("runtimeState.task", { id: task.id, description: task.description });
|
|
5493
6404
|
const turnEnv = this.buildTurnEnv(task);
|
|
5494
6405
|
this._currentTurnEnv = turnEnv;
|
|
@@ -5552,7 +6463,10 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5552
6463
|
this.processStreamMessage(message, onProgress);
|
|
5553
6464
|
}
|
|
5554
6465
|
} finally {
|
|
6466
|
+
this._streamEnded = true;
|
|
5555
6467
|
this._inputClosed = true;
|
|
6468
|
+
this.clearPendingProgress();
|
|
6469
|
+
this.flushAllToolSteps();
|
|
5556
6470
|
}
|
|
5557
6471
|
} catch (err) {
|
|
5558
6472
|
this.logger.error({ err, taskId: task.id }, "ClaudeCodeWorker query error");
|
|
@@ -5594,6 +6508,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5594
6508
|
}
|
|
5595
6509
|
abort() {
|
|
5596
6510
|
this.abortController.abort();
|
|
6511
|
+
this.clearPendingProgress();
|
|
5597
6512
|
this.permissionHandler.cleanup();
|
|
5598
6513
|
this.flushMessageQueue();
|
|
5599
6514
|
}
|
|
@@ -5601,6 +6516,14 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
5601
6516
|
getState() {
|
|
5602
6517
|
return this.state;
|
|
5603
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
|
+
}
|
|
5604
6527
|
/** Get conversation entries. */
|
|
5605
6528
|
getConversation() {
|
|
5606
6529
|
return this.state.get("conversation") ?? [];
|
|
@@ -5716,8 +6639,8 @@ ${leftoverText}` : `${buildThirdPartyPrompt(this.skills)}${initialPrompt}`;
|
|
|
5716
6639
|
return { value: void 0, done: true };
|
|
5717
6640
|
}
|
|
5718
6641
|
} else if (this._messageQueue.length === 0) {
|
|
5719
|
-
await new Promise((
|
|
5720
|
-
this._messageResolve =
|
|
6642
|
+
await new Promise((resolve12) => {
|
|
6643
|
+
this._messageResolve = resolve12;
|
|
5721
6644
|
});
|
|
5722
6645
|
continue;
|
|
5723
6646
|
}
|
|
@@ -5786,25 +6709,49 @@ controller: ${answer}`,
|
|
|
5786
6709
|
processStreamMessage(message, onProgress) {
|
|
5787
6710
|
if (message.type === "assistant") {
|
|
5788
6711
|
this.handleAssistantMessage(message, onProgress);
|
|
6712
|
+
} else if (message.type === "user") {
|
|
6713
|
+
this.handleUserMessage(message);
|
|
5789
6714
|
} else if (message.type === "result") {
|
|
5790
6715
|
this.handleResultMessage(message);
|
|
5791
6716
|
}
|
|
5792
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
|
+
}
|
|
5793
6735
|
handleAssistantMessage(msg, onProgress) {
|
|
5794
6736
|
const textParts = [];
|
|
6737
|
+
const toolCalls = [];
|
|
6738
|
+
const ts = nowISO();
|
|
5795
6739
|
for (const block of msg.message.content) {
|
|
5796
6740
|
if (block.type === "text") {
|
|
5797
6741
|
textParts.push(block.text);
|
|
6742
|
+
} else if (block.type === "tool_use") {
|
|
6743
|
+
toolCalls.push(toolUseBlockToRecord(block, ts));
|
|
5798
6744
|
}
|
|
5799
6745
|
}
|
|
5800
6746
|
const text = textParts.join("\n");
|
|
5801
|
-
if (!text) return;
|
|
5802
|
-
|
|
6747
|
+
if (!text && toolCalls.length === 0) return;
|
|
6748
|
+
const entry = {
|
|
5803
6749
|
type: "assistant" /* Assistant */,
|
|
5804
6750
|
uuid: generateId("a"),
|
|
5805
6751
|
parentUuid: null,
|
|
5806
6752
|
sessionId: this.ctx.sessionId,
|
|
5807
|
-
timestamp:
|
|
6753
|
+
timestamp: ts,
|
|
6754
|
+
startedAt: ts,
|
|
5808
6755
|
message: {
|
|
5809
6756
|
role: "assistant",
|
|
5810
6757
|
content: text,
|
|
@@ -5815,26 +6762,112 @@ controller: ${answer}`,
|
|
|
5815
6762
|
cachedInputTokens: msg.message.usage.cache_read_input_tokens
|
|
5816
6763
|
}
|
|
5817
6764
|
}
|
|
5818
|
-
}
|
|
5819
|
-
|
|
5820
|
-
|
|
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) {
|
|
5821
6775
|
this._pendingProgress = text.slice(0, 200);
|
|
5822
6776
|
if (this._progressTimer) clearTimeout(this._progressTimer);
|
|
5823
6777
|
this._progressTimer = setTimeout(() => {
|
|
5824
|
-
|
|
5825
|
-
|
|
6778
|
+
this._progressTimer = null;
|
|
6779
|
+
if (this._pendingProgress === null || this.abortController.signal.aborted || this._streamEnded || this._terminalResult !== null) {
|
|
5826
6780
|
this._pendingProgress = null;
|
|
6781
|
+
return;
|
|
5827
6782
|
}
|
|
5828
|
-
|
|
6783
|
+
const pending = this._pendingProgress;
|
|
6784
|
+
this._pendingProgress = null;
|
|
6785
|
+
onProgress(pending);
|
|
5829
6786
|
}, _ClaudeCodeWorker.PROGRESS_DEBOUNCE_MS);
|
|
5830
6787
|
}
|
|
5831
6788
|
}
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
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);
|
|
5837
6866
|
}
|
|
6867
|
+
this._pendingToolSteps = [];
|
|
6868
|
+
}
|
|
6869
|
+
handleResultMessage(msg) {
|
|
6870
|
+
this.clearPendingProgress();
|
|
5838
6871
|
if (msg.subtype === "success") {
|
|
5839
6872
|
this._terminalResult = {
|
|
5840
6873
|
status: "completed",
|
|
@@ -5847,9 +6880,9 @@ controller: ${answer}`,
|
|
|
5847
6880
|
};
|
|
5848
6881
|
}
|
|
5849
6882
|
if (this._messageResolve) {
|
|
5850
|
-
const
|
|
6883
|
+
const resolve12 = this._messageResolve;
|
|
5851
6884
|
this._messageResolve = null;
|
|
5852
|
-
|
|
6885
|
+
resolve12();
|
|
5853
6886
|
}
|
|
5854
6887
|
const prev = this.state.get("runtimeState");
|
|
5855
6888
|
this.state.set("runtimeState", {
|
|
@@ -5989,13 +7022,21 @@ function disposeWorkerTool(deps) {
|
|
|
5989
7022
|
|
|
5990
7023
|
// src/services/tools/get-task-detail.ts
|
|
5991
7024
|
import { z as z7 } from "zod";
|
|
7025
|
+
var OUTPUT_OVERHEAD_CHARS = 1500;
|
|
7026
|
+
var DEFAULT_LIMIT = 10;
|
|
7027
|
+
var DEFAULT_MAX_CHARS = 200;
|
|
5992
7028
|
function getTaskDetailTool(deps) {
|
|
5993
7029
|
return defineTool({
|
|
5994
|
-
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.",
|
|
5995
7031
|
parameters: z7.object({
|
|
5996
|
-
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)")
|
|
5997
7037
|
}),
|
|
5998
|
-
|
|
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 }) => {
|
|
5999
7040
|
const task = deps.getTask(taskId);
|
|
6000
7041
|
if (!task) return `Task ${taskId} not found`;
|
|
6001
7042
|
const lines = [
|
|
@@ -6010,30 +7051,313 @@ function getTaskDetailTool(deps) {
|
|
|
6010
7051
|
if (task.result) lines.push(`Result: ${task.result}`);
|
|
6011
7052
|
if (task.error) lines.push(`Error: ${task.error}`);
|
|
6012
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;
|
|
6013
7060
|
if (deps.getWorkerConversation) {
|
|
6014
|
-
|
|
6015
|
-
if (entries.length > 0)
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
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;
|
|
6020
7076
|
}
|
|
6021
7077
|
}
|
|
6022
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)}`);
|
|
6023
7115
|
return lines.join("\n");
|
|
6024
|
-
})
|
|
7116
|
+
}, { maxResultChars: MAX_TRANSCRIPT_RESULT_CHARS })
|
|
6025
7117
|
});
|
|
6026
7118
|
}
|
|
6027
7119
|
|
|
6028
|
-
// src/services/tools/
|
|
7120
|
+
// src/services/tools/read-worker-conversation.ts
|
|
6029
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";
|
|
6030
7354
|
function updateTaskTool(deps) {
|
|
6031
7355
|
return defineTool({
|
|
6032
7356
|
description: "Update a pending task description or priority",
|
|
6033
|
-
parameters:
|
|
6034
|
-
taskId:
|
|
6035
|
-
description:
|
|
6036
|
-
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")
|
|
6037
7361
|
}),
|
|
6038
7362
|
execute: safeExecute("update_task", async ({ taskId, description, priority }) => {
|
|
6039
7363
|
if (!description && !priority) {
|
|
@@ -6047,14 +7371,14 @@ function updateTaskTool(deps) {
|
|
|
6047
7371
|
}
|
|
6048
7372
|
|
|
6049
7373
|
// src/services/tools/query-signals.ts
|
|
6050
|
-
import { z as
|
|
7374
|
+
import { z as z11 } from "zod";
|
|
6051
7375
|
function querySignalsTool(deps) {
|
|
6052
7376
|
return defineTool({
|
|
6053
7377
|
description: "Query signal history by status, source, or type",
|
|
6054
|
-
parameters:
|
|
6055
|
-
status:
|
|
6056
|
-
source:
|
|
6057
|
-
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")
|
|
6058
7382
|
}),
|
|
6059
7383
|
execute: safeExecute("query_signals", async (filter) => {
|
|
6060
7384
|
const signals = deps.querySignals(filter);
|
|
@@ -6067,11 +7391,11 @@ function querySignalsTool(deps) {
|
|
|
6067
7391
|
}
|
|
6068
7392
|
|
|
6069
7393
|
// src/services/tools/skip-reply.ts
|
|
6070
|
-
import { z as
|
|
7394
|
+
import { z as z12 } from "zod";
|
|
6071
7395
|
function skipReplyTool() {
|
|
6072
7396
|
return defineTool({
|
|
6073
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.",
|
|
6074
|
-
parameters:
|
|
7398
|
+
parameters: z12.object({}),
|
|
6075
7399
|
execute: safeExecute("skip_reply", async () => {
|
|
6076
7400
|
return "(skipped)";
|
|
6077
7401
|
})
|
|
@@ -6079,13 +7403,13 @@ function skipReplyTool() {
|
|
|
6079
7403
|
}
|
|
6080
7404
|
|
|
6081
7405
|
// src/services/tools/bash.ts
|
|
6082
|
-
import { z as
|
|
7406
|
+
import { z as z13 } from "zod";
|
|
6083
7407
|
import { execSync as execSync2 } from "child_process";
|
|
6084
7408
|
function bashTool(deps) {
|
|
6085
7409
|
return defineTool({
|
|
6086
7410
|
description: "Execute a shell command and return its output",
|
|
6087
|
-
parameters:
|
|
6088
|
-
command:
|
|
7411
|
+
parameters: z13.object({
|
|
7412
|
+
command: z13.string().describe("Shell command to execute")
|
|
6089
7413
|
}),
|
|
6090
7414
|
execute: safeExecute("bash", async ({ command }) => {
|
|
6091
7415
|
try {
|
|
@@ -6107,22 +7431,22 @@ ${detail}`;
|
|
|
6107
7431
|
}
|
|
6108
7432
|
|
|
6109
7433
|
// src/services/tools/read-file.ts
|
|
6110
|
-
import { z as
|
|
6111
|
-
import { readFileSync as
|
|
6112
|
-
import { resolve as
|
|
7434
|
+
import { z as z14 } from "zod";
|
|
7435
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
7436
|
+
import { resolve as resolve3 } from "path";
|
|
6113
7437
|
var DEFAULT_MAX_LINES = 500;
|
|
6114
7438
|
function readFileTool(deps = {}) {
|
|
6115
7439
|
const workspace = deps.cwd ?? process.cwd();
|
|
6116
7440
|
return defineTool({
|
|
6117
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.`,
|
|
6118
|
-
parameters:
|
|
6119
|
-
path:
|
|
6120
|
-
offset:
|
|
6121
|
-
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")
|
|
6122
7446
|
}),
|
|
6123
7447
|
execute: safeExecute("read_file", async ({ path: path7, offset, limit }) => {
|
|
6124
|
-
const resolved =
|
|
6125
|
-
const content =
|
|
7448
|
+
const resolved = resolve3(workspace, path7);
|
|
7449
|
+
const content = readFileSync7(resolved, "utf-8");
|
|
6126
7450
|
const lines = content.split("\n");
|
|
6127
7451
|
const totalLines = lines.length;
|
|
6128
7452
|
const start = offset ? offset - 1 : 0;
|
|
@@ -6141,23 +7465,23 @@ function readFileTool(deps = {}) {
|
|
|
6141
7465
|
}
|
|
6142
7466
|
|
|
6143
7467
|
// src/services/tools/write-file.ts
|
|
6144
|
-
import { z as
|
|
6145
|
-
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as
|
|
6146
|
-
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";
|
|
6147
7471
|
function writeFileTool(deps = {}) {
|
|
6148
7472
|
const workspace = deps.cwd ?? process.cwd();
|
|
6149
7473
|
return defineTool({
|
|
6150
7474
|
description: "Create a new file with the given content. Creates parent directories as needed. Use edit_file to modify existing files.",
|
|
6151
|
-
parameters:
|
|
6152
|
-
path:
|
|
6153
|
-
content:
|
|
7475
|
+
parameters: z15.object({
|
|
7476
|
+
path: z15.string().describe("File path (relative to workspace or absolute)"),
|
|
7477
|
+
content: z15.string().describe("File content")
|
|
6154
7478
|
}),
|
|
6155
7479
|
execute: safeExecute("write_file", async ({ path: path7, content }) => {
|
|
6156
|
-
const resolved =
|
|
6157
|
-
if (
|
|
7480
|
+
const resolved = resolve4(workspace, path7);
|
|
7481
|
+
if (existsSync7(resolved)) {
|
|
6158
7482
|
return `[ERROR] write_file: File already exists: ${path7}. Use edit_file to modify existing files.`;
|
|
6159
7483
|
}
|
|
6160
|
-
mkdirSync4(
|
|
7484
|
+
mkdirSync4(dirname5(resolved), { recursive: true });
|
|
6161
7485
|
writeFileSync3(resolved, content, "utf-8");
|
|
6162
7486
|
return `Written ${content.length} chars to ${resolved}`;
|
|
6163
7487
|
})
|
|
@@ -6165,23 +7489,23 @@ function writeFileTool(deps = {}) {
|
|
|
6165
7489
|
}
|
|
6166
7490
|
|
|
6167
7491
|
// src/services/tools/edit-file.ts
|
|
6168
|
-
import { readFileSync as
|
|
6169
|
-
import { resolve as
|
|
6170
|
-
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";
|
|
6171
7495
|
function editFileTool(deps) {
|
|
6172
7496
|
const workspace = deps.cwd ?? process.cwd();
|
|
6173
7497
|
return defineTool({
|
|
6174
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.",
|
|
6175
|
-
parameters:
|
|
6176
|
-
path:
|
|
6177
|
-
old_string:
|
|
6178
|
-
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")
|
|
6179
7503
|
}),
|
|
6180
7504
|
execute: safeExecute("edit_file", async ({ path: path7, old_string, new_string }) => {
|
|
6181
|
-
const resolved =
|
|
7505
|
+
const resolved = resolve5(workspace, path7);
|
|
6182
7506
|
let content;
|
|
6183
7507
|
try {
|
|
6184
|
-
content =
|
|
7508
|
+
content = readFileSync8(resolved, "utf-8");
|
|
6185
7509
|
} catch {
|
|
6186
7510
|
return `[ERROR] edit_file: File not found: ${path7}`;
|
|
6187
7511
|
}
|
|
@@ -6199,18 +7523,18 @@ function editFileTool(deps) {
|
|
|
6199
7523
|
|
|
6200
7524
|
// src/services/tools/search-files.ts
|
|
6201
7525
|
import fg from "fast-glob";
|
|
6202
|
-
import { resolve as
|
|
6203
|
-
import { z as
|
|
7526
|
+
import { resolve as resolve6 } from "path";
|
|
7527
|
+
import { z as z17 } from "zod";
|
|
6204
7528
|
function searchFilesTool(deps) {
|
|
6205
7529
|
const workspace = deps.cwd ?? process.cwd();
|
|
6206
7530
|
return defineTool({
|
|
6207
7531
|
description: "Search for files matching a glob pattern. Returns matching file paths relative to workspace.",
|
|
6208
|
-
parameters:
|
|
6209
|
-
pattern:
|
|
6210
|
-
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)")
|
|
6211
7535
|
}),
|
|
6212
7536
|
execute: safeExecute("search_files", async ({ pattern, path: path7 }) => {
|
|
6213
|
-
const basePath = path7 ?
|
|
7537
|
+
const basePath = path7 ? resolve6(workspace, path7) : workspace;
|
|
6214
7538
|
const entries = await fg(pattern, {
|
|
6215
7539
|
cwd: basePath,
|
|
6216
7540
|
onlyFiles: true,
|
|
@@ -6224,8 +7548,8 @@ function searchFilesTool(deps) {
|
|
|
6224
7548
|
|
|
6225
7549
|
// src/services/tools/search-content.ts
|
|
6226
7550
|
import { execSync as execSync3 } from "child_process";
|
|
6227
|
-
import { resolve as
|
|
6228
|
-
import { z as
|
|
7551
|
+
import { resolve as resolve7 } from "path";
|
|
7552
|
+
import { z as z18 } from "zod";
|
|
6229
7553
|
function shellQuote(s) {
|
|
6230
7554
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
6231
7555
|
}
|
|
@@ -6233,13 +7557,13 @@ function searchContentTool(deps) {
|
|
|
6233
7557
|
const workspace = deps.cwd ?? process.cwd();
|
|
6234
7558
|
return defineTool({
|
|
6235
7559
|
description: "Search file contents for a text pattern. Returns matching lines in file:line:content format.",
|
|
6236
|
-
parameters:
|
|
6237
|
-
query:
|
|
6238
|
-
path:
|
|
6239
|
-
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")')
|
|
6240
7564
|
}),
|
|
6241
7565
|
execute: safeExecute("search_content", async ({ query: query2, path: path7, include }) => {
|
|
6242
|
-
const searchDir = path7 ?
|
|
7566
|
+
const searchDir = path7 ? resolve7(workspace, path7) : workspace;
|
|
6243
7567
|
const cmd = `grep -rn${include ? ` --include=${shellQuote(include)}` : ""} -- ${shellQuote(query2)} ${shellQuote(searchDir)}`;
|
|
6244
7568
|
try {
|
|
6245
7569
|
const output = execSync3(cmd, {
|
|
@@ -6258,13 +7582,13 @@ ${err.stderr?.toString() || ""}`;
|
|
|
6258
7582
|
}
|
|
6259
7583
|
|
|
6260
7584
|
// src/services/tools/web-search.ts
|
|
6261
|
-
import { z as
|
|
7585
|
+
import { z as z19 } from "zod";
|
|
6262
7586
|
function webSearchTool(deps) {
|
|
6263
7587
|
const apiKey = deps.tavilyApiKey;
|
|
6264
7588
|
return defineTool({
|
|
6265
7589
|
description: "Search the web using Tavily. Returns top results with title, URL, and content snippet.",
|
|
6266
|
-
parameters:
|
|
6267
|
-
query:
|
|
7590
|
+
parameters: z19.object({
|
|
7591
|
+
query: z19.string().describe("Search query")
|
|
6268
7592
|
}),
|
|
6269
7593
|
execute: safeExecute("web_search", async ({ query: query2 }) => {
|
|
6270
7594
|
if (!apiKey) {
|
|
@@ -6295,11 +7619,11 @@ function webSearchTool(deps) {
|
|
|
6295
7619
|
}
|
|
6296
7620
|
|
|
6297
7621
|
// src/services/tools/notebook.ts
|
|
6298
|
-
import { z as
|
|
7622
|
+
import { z as z20 } from "zod";
|
|
6299
7623
|
function readNotebookTool(deps) {
|
|
6300
7624
|
return defineTool({
|
|
6301
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.",
|
|
6302
|
-
parameters:
|
|
7626
|
+
parameters: z20.object({}),
|
|
6303
7627
|
execute: safeExecute("read_notebook", async () => {
|
|
6304
7628
|
const content = deps.getNotebook();
|
|
6305
7629
|
return content || "(notebook is empty)";
|
|
@@ -6309,9 +7633,9 @@ function readNotebookTool(deps) {
|
|
|
6309
7633
|
function updateNotebookTool(deps) {
|
|
6310
7634
|
return defineTool({
|
|
6311
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.',
|
|
6312
|
-
parameters:
|
|
6313
|
-
content:
|
|
6314
|
-
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)')
|
|
6315
7639
|
}),
|
|
6316
7640
|
execute: safeExecute("update_notebook", async ({ content, mode }) => {
|
|
6317
7641
|
deps.setNotebook(content, mode ?? "replace");
|
|
@@ -6321,15 +7645,16 @@ function updateNotebookTool(deps) {
|
|
|
6321
7645
|
}
|
|
6322
7646
|
|
|
6323
7647
|
// src/services/tools/inquire.ts
|
|
6324
|
-
import { z as
|
|
7648
|
+
import { z as z21 } from "zod";
|
|
6325
7649
|
function inquireTool(deps) {
|
|
6326
7650
|
return defineTool({
|
|
6327
|
-
description:
|
|
6328
|
-
parameters:
|
|
6329
|
-
message:
|
|
6330
|
-
type:
|
|
6331
|
-
options:
|
|
6332
|
-
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')
|
|
6333
7658
|
}),
|
|
6334
7659
|
execute: safeExecute("inquire", async (args) => {
|
|
6335
7660
|
return deps.inquire(args);
|
|
@@ -6338,12 +7663,12 @@ function inquireTool(deps) {
|
|
|
6338
7663
|
}
|
|
6339
7664
|
|
|
6340
7665
|
// src/services/tools/done.ts
|
|
6341
|
-
import { z as
|
|
7666
|
+
import { z as z22 } from "zod";
|
|
6342
7667
|
function doneTool(deps) {
|
|
6343
7668
|
return defineTool({
|
|
6344
7669
|
description: "Report task completion. Worker stops executing after this call.",
|
|
6345
|
-
parameters:
|
|
6346
|
-
summary:
|
|
7670
|
+
parameters: z22.object({
|
|
7671
|
+
summary: z22.string().describe("Task completion summary")
|
|
6347
7672
|
}),
|
|
6348
7673
|
execute: safeExecute("done", async ({ summary }) => {
|
|
6349
7674
|
deps.onComplete(summary);
|
|
@@ -6353,12 +7678,12 @@ function doneTool(deps) {
|
|
|
6353
7678
|
}
|
|
6354
7679
|
|
|
6355
7680
|
// src/services/tools/fail.ts
|
|
6356
|
-
import { z as
|
|
7681
|
+
import { z as z23 } from "zod";
|
|
6357
7682
|
function failTool(deps) {
|
|
6358
7683
|
return defineTool({
|
|
6359
7684
|
description: "Report task failure. Worker stops executing after this call.",
|
|
6360
|
-
parameters:
|
|
6361
|
-
error:
|
|
7685
|
+
parameters: z23.object({
|
|
7686
|
+
error: z23.string().describe("Error description")
|
|
6362
7687
|
}),
|
|
6363
7688
|
execute: safeExecute("fail", async ({ error }) => {
|
|
6364
7689
|
deps.onFail(error);
|
|
@@ -6368,13 +7693,13 @@ function failTool(deps) {
|
|
|
6368
7693
|
}
|
|
6369
7694
|
|
|
6370
7695
|
// src/services/tools/update-progress.ts
|
|
6371
|
-
import { z as
|
|
7696
|
+
import { z as z24 } from "zod";
|
|
6372
7697
|
function updateProgressTool(deps) {
|
|
6373
7698
|
return defineTool({
|
|
6374
7699
|
description: "Report progress for the current task (replaces automatic step-based progress)",
|
|
6375
|
-
parameters:
|
|
6376
|
-
message:
|
|
6377
|
-
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)")
|
|
6378
7703
|
}),
|
|
6379
7704
|
execute: safeExecute("update_progress", async ({ message, percent }) => {
|
|
6380
7705
|
deps.reportProgress(message, percent);
|
|
@@ -6385,24 +7710,24 @@ function updateProgressTool(deps) {
|
|
|
6385
7710
|
}
|
|
6386
7711
|
|
|
6387
7712
|
// src/services/tools/chat-worker.ts
|
|
6388
|
-
import { z as
|
|
7713
|
+
import { z as z25 } from "zod";
|
|
6389
7714
|
function chatWorkerTool(deps) {
|
|
6390
7715
|
return defineTool({
|
|
6391
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.`,
|
|
6392
|
-
parameters:
|
|
6393
|
-
workerId:
|
|
6394
|
-
message:
|
|
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")
|
|
6395
7720
|
}),
|
|
6396
7721
|
execute: safeExecute("chat_worker", async ({ workerId, message }) => {
|
|
6397
7722
|
const status = deps.sendMessageToWorker(workerId, message);
|
|
6398
7723
|
if (status === "not_running") {
|
|
6399
|
-
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.`;
|
|
6400
7725
|
}
|
|
6401
7726
|
if (status === "finished") {
|
|
6402
|
-
return `[ERROR] Worker ${workerId} has already finished; message not delivered. Use get_task_detail to inspect its output.`;
|
|
7727
|
+
return `[ERROR] Worker ${workerId} has already finished; message not delivered. Use read_worker_conversation("${workerId}") / get_task_detail to inspect its output.`;
|
|
6403
7728
|
}
|
|
6404
7729
|
if (status === "queued") {
|
|
6405
|
-
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
|
|
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.`;
|
|
6406
7731
|
}
|
|
6407
7732
|
return `Message delivered to worker ${workerId} \u2014 it will be read at the worker's next step.`;
|
|
6408
7733
|
})
|
|
@@ -6410,14 +7735,14 @@ function chatWorkerTool(deps) {
|
|
|
6410
7735
|
}
|
|
6411
7736
|
|
|
6412
7737
|
// src/services/tools/respond-permission.ts
|
|
6413
|
-
import { z as
|
|
7738
|
+
import { z as z26 } from "zod";
|
|
6414
7739
|
function respondPermissionTool(deps) {
|
|
6415
7740
|
return defineTool({
|
|
6416
7741
|
description: "Respond to a worker's permission request. Approve to allow the operation, deny to reject it.",
|
|
6417
|
-
parameters:
|
|
6418
|
-
requestId:
|
|
6419
|
-
approved:
|
|
6420
|
-
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)")
|
|
6421
7746
|
}),
|
|
6422
7747
|
execute: safeExecute("respond_permission", async ({ requestId, approved, reason }) => {
|
|
6423
7748
|
const handled = deps.respondPermission(requestId, approved, reason);
|
|
@@ -6428,13 +7753,13 @@ function respondPermissionTool(deps) {
|
|
|
6428
7753
|
}
|
|
6429
7754
|
|
|
6430
7755
|
// src/services/tools/respond-inquiry.ts
|
|
6431
|
-
import { z as
|
|
7756
|
+
import { z as z27 } from "zod";
|
|
6432
7757
|
function respondInquiryTool(deps) {
|
|
6433
7758
|
return defineTool({
|
|
6434
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.",
|
|
6435
|
-
parameters:
|
|
6436
|
-
inquiryId:
|
|
6437
|
-
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.")
|
|
6438
7763
|
}),
|
|
6439
7764
|
execute: safeExecute("respond_inquiry", async ({ inquiryId, answer }) => {
|
|
6440
7765
|
const status = deps.respondInquiry(inquiryId, answer);
|
|
@@ -6450,7 +7775,7 @@ function respondInquiryTool(deps) {
|
|
|
6450
7775
|
}
|
|
6451
7776
|
|
|
6452
7777
|
// src/services/tools/use-skill.ts
|
|
6453
|
-
import { z as
|
|
7778
|
+
import { z as z28 } from "zod";
|
|
6454
7779
|
function formatSkillContent(skill) {
|
|
6455
7780
|
const header = `# Skill: ${skill.name}
|
|
6456
7781
|
|
|
@@ -6472,8 +7797,8 @@ ${skill.content}${resources}`;
|
|
|
6472
7797
|
function useSkillTool(deps) {
|
|
6473
7798
|
return defineTool({
|
|
6474
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).`,
|
|
6475
|
-
parameters:
|
|
6476
|
-
name:
|
|
7800
|
+
parameters: z28.object({
|
|
7801
|
+
name: z28.string().describe('Skill name, exactly as shown in the "Skills" section')
|
|
6477
7802
|
}),
|
|
6478
7803
|
execute: safeExecute("use-skill", async ({ name }) => {
|
|
6479
7804
|
const content = deps.registry.get(name);
|
|
@@ -6505,7 +7830,26 @@ function createControllerTools(deps) {
|
|
|
6505
7830
|
}),
|
|
6506
7831
|
get_task_detail: getTaskDetailTool({
|
|
6507
7832
|
getTask: deps.getTask,
|
|
6508
|
-
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
|
|
6509
7853
|
}),
|
|
6510
7854
|
update_task: updateTaskTool({ updateTask: deps.updateTask }),
|
|
6511
7855
|
query_signals: querySignalsTool({ querySignals: deps.queryMessages }),
|
|
@@ -6545,13 +7889,13 @@ function createWorkerTools(deps) {
|
|
|
6545
7889
|
}
|
|
6546
7890
|
|
|
6547
7891
|
// src/services/tools/ask-controller.ts
|
|
6548
|
-
import { z as
|
|
7892
|
+
import { z as z29 } from "zod";
|
|
6549
7893
|
function askControllerTool(deps) {
|
|
6550
7894
|
return defineTool({
|
|
6551
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.",
|
|
6552
|
-
parameters:
|
|
6553
|
-
question:
|
|
6554
|
-
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.")
|
|
6555
7899
|
}),
|
|
6556
7900
|
execute: safeExecute("ask_controller", async ({ question, context }) => {
|
|
6557
7901
|
return deps.ask(question, context);
|
|
@@ -6593,16 +7937,16 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
6593
7937
|
},
|
|
6594
7938
|
...deps.turnLineage ?? {}
|
|
6595
7939
|
});
|
|
6596
|
-
const result = await new Promise((
|
|
7940
|
+
const result = await new Promise((resolve12) => {
|
|
6597
7941
|
const timer = setTimeout(() => {
|
|
6598
7942
|
pending.delete(requestId);
|
|
6599
|
-
|
|
7943
|
+
resolve12({ approved: false, reason: "timeout" });
|
|
6600
7944
|
}, timeoutMs);
|
|
6601
7945
|
pending.set(requestId, {
|
|
6602
7946
|
resolve: (ok, reason) => {
|
|
6603
7947
|
clearTimeout(timer);
|
|
6604
7948
|
pending.delete(requestId);
|
|
6605
|
-
|
|
7949
|
+
resolve12({ approved: ok, reason });
|
|
6606
7950
|
},
|
|
6607
7951
|
timer
|
|
6608
7952
|
});
|
|
@@ -6639,14 +7983,14 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
6639
7983
|
}
|
|
6640
7984
|
|
|
6641
7985
|
// src/services/tools/permission-rules.ts
|
|
6642
|
-
import { resolve as
|
|
7986
|
+
import { resolve as resolve8 } from "path";
|
|
6643
7987
|
import { homedir } from "os";
|
|
6644
7988
|
var FILE_TOOL_NAMES = ["read_file", "write_file", "edit_file", "search_files", "search_content"];
|
|
6645
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)/;
|
|
6646
7990
|
var URL_RE = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/\S+/g;
|
|
6647
7991
|
function expandHome(p) {
|
|
6648
7992
|
if (p === "~") return homedir();
|
|
6649
|
-
if (p.startsWith("~/")) return
|
|
7993
|
+
if (p.startsWith("~/")) return resolve8(homedir(), p.slice(2));
|
|
6650
7994
|
return p;
|
|
6651
7995
|
}
|
|
6652
7996
|
function extractPathCandidates(command) {
|
|
@@ -6661,7 +8005,7 @@ function extractPathCandidates(command) {
|
|
|
6661
8005
|
function needsFilePermission(workspace, args) {
|
|
6662
8006
|
const rawPath = args.path;
|
|
6663
8007
|
if (typeof rawPath !== "string" || !rawPath) return false;
|
|
6664
|
-
return !isInsideWorkspace(
|
|
8008
|
+
return !isInsideWorkspace(resolve8(workspace, expandHome(rawPath)), workspace);
|
|
6665
8009
|
}
|
|
6666
8010
|
function needsBashPermission(workspace, command) {
|
|
6667
8011
|
if (DANGEROUS_BASH_PATTERN.test(command)) return true;
|
|
@@ -6711,7 +8055,7 @@ function formatApiCallError(err) {
|
|
|
6711
8055
|
}
|
|
6712
8056
|
|
|
6713
8057
|
// src/services/task-execute-engine.ts
|
|
6714
|
-
import { resolve as
|
|
8058
|
+
import { resolve as resolve9 } from "path";
|
|
6715
8059
|
var RuntimeState = class extends BaseWayangState {
|
|
6716
8060
|
constructor(initialData) {
|
|
6717
8061
|
super(initialData, []);
|
|
@@ -6988,6 +8332,24 @@ var TaskExecuteEngine = class {
|
|
|
6988
8332
|
}
|
|
6989
8333
|
});
|
|
6990
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
|
+
}
|
|
6991
8353
|
getWorkerConversation(taskId) {
|
|
6992
8354
|
for (const [workerId, tId] of this.workerTaskMap) {
|
|
6993
8355
|
if (tId === taskId) {
|
|
@@ -6997,6 +8359,25 @@ var TaskExecuteEngine = class {
|
|
|
6997
8359
|
}
|
|
6998
8360
|
return [];
|
|
6999
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
|
+
}
|
|
7000
8381
|
getRunningCount() {
|
|
7001
8382
|
return this.taskState.get("tasks.running").length;
|
|
7002
8383
|
}
|
|
@@ -7153,6 +8534,20 @@ var TaskExecuteEngine = class {
|
|
|
7153
8534
|
this.taskState.append("tasks.running", updated);
|
|
7154
8535
|
this.logger.info({ taskId: id, workerId: workerSessionId }, "Task moved to running");
|
|
7155
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
|
+
}
|
|
7156
8551
|
completeTask(id, result) {
|
|
7157
8552
|
this.moveToHistory(id, "completed", { result });
|
|
7158
8553
|
this.logger.info({ taskId: id }, "Task completed");
|
|
@@ -7290,6 +8685,7 @@ var TaskExecuteEngine = class {
|
|
|
7290
8685
|
const worker = this.createWorker(workerType);
|
|
7291
8686
|
this.workers.set(workerId, worker);
|
|
7292
8687
|
this.workerTaskMap.set(workerId, task.id);
|
|
8688
|
+
this.stampWorkerTranscriptId(task.id, worker.id);
|
|
7293
8689
|
const executionTurnId = this.registerWorkerTurn(workerId, task);
|
|
7294
8690
|
const onProgress = this.buildProgressEmitter(workerId, task, workerType);
|
|
7295
8691
|
const tools = this.wireWorker(worker, task, workerId, workerType);
|
|
@@ -7320,7 +8716,9 @@ var TaskExecuteEngine = class {
|
|
|
7320
8716
|
assignedFromWorkerId: workerId,
|
|
7321
8717
|
status: "running",
|
|
7322
8718
|
startedAt: Date.now(),
|
|
7323
|
-
workerSessionId: workerId
|
|
8719
|
+
workerSessionId: workerId,
|
|
8720
|
+
// Same instance keeps writing the same conversation.jsonl.
|
|
8721
|
+
workerTranscriptId: worker.id
|
|
7324
8722
|
};
|
|
7325
8723
|
this.taskState.append("tasks.running", taskWithOrigin);
|
|
7326
8724
|
this.workerTaskMap.set(workerId, task.id);
|
|
@@ -7348,10 +8746,22 @@ var TaskExecuteEngine = class {
|
|
|
7348
8746
|
buildProgressEmitter(workerId, task, workerType) {
|
|
7349
8747
|
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
7350
8748
|
return (msg) => {
|
|
8749
|
+
const activity = this.getWorkerActivity(workerId);
|
|
7351
8750
|
this.enqueueWorkerSignal(workerId, {
|
|
7352
8751
|
source: "worker",
|
|
7353
8752
|
type: "progress",
|
|
7354
|
-
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
|
+
}
|
|
7355
8765
|
});
|
|
7356
8766
|
};
|
|
7357
8767
|
}
|
|
@@ -7401,10 +8811,20 @@ var TaskExecuteEngine = class {
|
|
|
7401
8811
|
tavilyApiKey: this.ctx.config.tavilyApiKey,
|
|
7402
8812
|
registry: this.skills,
|
|
7403
8813
|
reportProgress: (msg, _percent) => {
|
|
8814
|
+
const activity = this.getWorkerActivity(workerId);
|
|
7404
8815
|
this.enqueueWorkerSignal(workerId, {
|
|
7405
8816
|
source: "worker",
|
|
7406
8817
|
type: "progress",
|
|
7407
|
-
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
|
+
}
|
|
7408
8828
|
});
|
|
7409
8829
|
},
|
|
7410
8830
|
onComplete: (summary) => {
|
|
@@ -7436,7 +8856,7 @@ var TaskExecuteEngine = class {
|
|
|
7436
8856
|
if (toolName === "bash") return `Execute shell command: ${args.command}`;
|
|
7437
8857
|
if (FILE_TOOL_NAMES.includes(toolName)) {
|
|
7438
8858
|
const rawPath = typeof args.path === "string" ? args.path : "";
|
|
7439
|
-
const resolved = rawPath ?
|
|
8859
|
+
const resolved = rawPath ? resolve9(workspace, rawPath) : workspace;
|
|
7440
8860
|
return `${toolName}: ${rawPath || "(workspace root)"} (resolved: ${resolved})`;
|
|
7441
8861
|
}
|
|
7442
8862
|
return `${toolName}: ${JSON.stringify(args)}`;
|
|
@@ -7672,13 +9092,21 @@ var ControllerLoop = class {
|
|
|
7672
9092
|
payload: {
|
|
7673
9093
|
reason: "Workers are running but no signals for a while. Review status and decide if action is needed.",
|
|
7674
9094
|
idleSinceMs: now - lastWakeAt,
|
|
7675
|
-
workers: activeWorkers.map((w) =>
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
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
|
+
}),
|
|
7682
9110
|
pendingTaskCount: this.heartbeatProvider.getPendingCount()
|
|
7683
9111
|
}
|
|
7684
9112
|
});
|
|
@@ -7695,11 +9123,11 @@ var ControllerLoop = class {
|
|
|
7695
9123
|
import * as path5 from "path";
|
|
7696
9124
|
|
|
7697
9125
|
// src/services/session/session-state.ts
|
|
7698
|
-
import { join as
|
|
9126
|
+
import { join as join10 } from "path";
|
|
7699
9127
|
var SessionState = class extends BaseWayangState {
|
|
7700
9128
|
jsonFile;
|
|
7701
9129
|
constructor(sessionDir) {
|
|
7702
|
-
const jsonFile = new JSONFileHelper(
|
|
9130
|
+
const jsonFile = new JSONFileHelper(join10(sessionDir, "meta.json"));
|
|
7703
9131
|
super(
|
|
7704
9132
|
{
|
|
7705
9133
|
meta: {
|
|
@@ -7805,7 +9233,7 @@ var SessionManager = class _SessionManager {
|
|
|
7805
9233
|
};
|
|
7806
9234
|
|
|
7807
9235
|
// src/services/agents/controller-state.ts
|
|
7808
|
-
import { join as
|
|
9236
|
+
import { join as join12 } from "path";
|
|
7809
9237
|
|
|
7810
9238
|
// src/services/conversation/conversation-index.ts
|
|
7811
9239
|
function emptyUsage() {
|
|
@@ -8026,8 +9454,8 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
8026
9454
|
/** In-memory resolve function for the current inquiry — never persisted. */
|
|
8027
9455
|
inquiryResolver = null;
|
|
8028
9456
|
constructor(ctx) {
|
|
8029
|
-
const jsonFile = new JSONFileHelper(
|
|
8030
|
-
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"));
|
|
8031
9459
|
const initialData = {
|
|
8032
9460
|
runtimeState: {
|
|
8033
9461
|
session: { id: "", startedAt: 0 },
|
|
@@ -8102,8 +9530,8 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
8102
9530
|
}
|
|
8103
9531
|
/** Set a pending inquiry and return a Promise that resolves when the user answers. */
|
|
8104
9532
|
askInquiry(question) {
|
|
8105
|
-
return new Promise((
|
|
8106
|
-
this.inquiryResolver =
|
|
9533
|
+
return new Promise((resolve12) => {
|
|
9534
|
+
this.inquiryResolver = resolve12;
|
|
8107
9535
|
this.set("runtimeState.pendingInquiry", question);
|
|
8108
9536
|
});
|
|
8109
9537
|
}
|
|
@@ -8263,6 +9691,19 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
8263
9691
|
listTasks: (status) => engine.list(status),
|
|
8264
9692
|
getTask: (taskId) => engine.get(taskId),
|
|
8265
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),
|
|
8266
9707
|
cancelTask: (taskId) => engine.cancel(taskId),
|
|
8267
9708
|
abortWorker: (taskId) => engine.abortByTaskId(taskId),
|
|
8268
9709
|
updateTask: (taskId, updates) => engine.updatePending(taskId, updates),
|
|
@@ -8562,6 +10003,14 @@ function isoMs(ms) {
|
|
|
8562
10003
|
if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
|
|
8563
10004
|
return new Date(ms).toISOString();
|
|
8564
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
|
+
}
|
|
8565
10014
|
var signalConverters = {
|
|
8566
10015
|
input: (sig, ts, sessionId) => ({
|
|
8567
10016
|
type: "user" /* User */,
|
|
@@ -8603,6 +10052,7 @@ var signalConverters = {
|
|
|
8603
10052
|
},
|
|
8604
10053
|
progress: (sig, ts, sessionId) => {
|
|
8605
10054
|
const p = sig.payload;
|
|
10055
|
+
const activity = signalActivityTail(p.currentTool, p.lastActivityAt);
|
|
8606
10056
|
return {
|
|
8607
10057
|
...signalBase(ts, sessionId),
|
|
8608
10058
|
subtype: "worker_progress" /* WorkerProgress */,
|
|
@@ -8611,12 +10061,17 @@ var signalConverters = {
|
|
|
8611
10061
|
emoji: p.emoji,
|
|
8612
10062
|
taskId: p.taskId,
|
|
8613
10063
|
taskTitle: p.taskTitle,
|
|
8614
|
-
content: p.message
|
|
10064
|
+
content: activity ? `${p.message}
|
|
10065
|
+
${activity}` : p.message
|
|
8615
10066
|
};
|
|
8616
10067
|
},
|
|
8617
10068
|
heartbeat: (sig, ts, sessionId) => {
|
|
8618
10069
|
const p = sig.payload;
|
|
8619
|
-
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(", ");
|
|
8620
10075
|
return {
|
|
8621
10076
|
...signalBase(ts, sessionId),
|
|
8622
10077
|
subtype: "heartbeat" /* Heartbeat */,
|
|
@@ -8779,8 +10234,8 @@ function signalToEntry(sig, ts, sessionId) {
|
|
|
8779
10234
|
}
|
|
8780
10235
|
|
|
8781
10236
|
// src/services/skills/registry.ts
|
|
8782
|
-
import { readFileSync as
|
|
8783
|
-
import { join as
|
|
10237
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
10238
|
+
import { join as join13 } from "path";
|
|
8784
10239
|
import { parse as parseYaml } from "yaml";
|
|
8785
10240
|
var SKILL_FILE = "SKILL.md";
|
|
8786
10241
|
function parseFrontmatter(raw) {
|
|
@@ -8815,7 +10270,7 @@ function listResources(skillDir) {
|
|
|
8815
10270
|
const walk = (relDir) => {
|
|
8816
10271
|
let entries;
|
|
8817
10272
|
try {
|
|
8818
|
-
entries = readdirSync2(
|
|
10273
|
+
entries = readdirSync2(join13(skillDir, relDir), { withFileTypes: true });
|
|
8819
10274
|
} catch {
|
|
8820
10275
|
return;
|
|
8821
10276
|
}
|
|
@@ -8862,10 +10317,10 @@ var SkillRegistry = class {
|
|
|
8862
10317
|
if (existing) return existing;
|
|
8863
10318
|
const desc = this.descriptors.get(name);
|
|
8864
10319
|
if (!desc) return void 0;
|
|
8865
|
-
const filePath =
|
|
10320
|
+
const filePath = join13(desc.dir, SKILL_FILE);
|
|
8866
10321
|
let raw;
|
|
8867
10322
|
try {
|
|
8868
|
-
raw =
|
|
10323
|
+
raw = readFileSync9(filePath, "utf-8");
|
|
8869
10324
|
} catch (err) {
|
|
8870
10325
|
this.logger?.warn({ skill: name, error: err instanceof Error ? err.message : String(err) }, "Failed to read skill file");
|
|
8871
10326
|
return void 0;
|
|
@@ -8893,8 +10348,8 @@ var SkillRegistry = class {
|
|
|
8893
10348
|
for (const entry of entries) {
|
|
8894
10349
|
if (!entry.isDirectory()) continue;
|
|
8895
10350
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
8896
|
-
const skillDir =
|
|
8897
|
-
const skillFile =
|
|
10351
|
+
const skillDir = join13(dir, entry.name);
|
|
10352
|
+
const skillFile = join13(skillDir, SKILL_FILE);
|
|
8898
10353
|
if (!existsAsFile(skillFile)) continue;
|
|
8899
10354
|
const { frontmatter } = safeReadFrontmatter(skillFile);
|
|
8900
10355
|
const description = asString(frontmatter.description);
|
|
@@ -8909,35 +10364,35 @@ var SkillRegistry = class {
|
|
|
8909
10364
|
};
|
|
8910
10365
|
function existsAsFile(path7) {
|
|
8911
10366
|
try {
|
|
8912
|
-
return
|
|
10367
|
+
return statSync3(path7).isFile();
|
|
8913
10368
|
} catch {
|
|
8914
10369
|
return false;
|
|
8915
10370
|
}
|
|
8916
10371
|
}
|
|
8917
10372
|
function safeReadFrontmatter(path7) {
|
|
8918
10373
|
try {
|
|
8919
|
-
return parseFrontmatter(
|
|
10374
|
+
return parseFrontmatter(readFileSync9(path7, "utf-8"));
|
|
8920
10375
|
} catch {
|
|
8921
10376
|
return { frontmatter: {}, body: "" };
|
|
8922
10377
|
}
|
|
8923
10378
|
}
|
|
8924
10379
|
|
|
8925
10380
|
// src/services/skills/skill-dir.ts
|
|
8926
|
-
import { existsSync as
|
|
10381
|
+
import { existsSync as existsSync8 } from "fs";
|
|
8927
10382
|
import { homedir as homedir2 } from "os";
|
|
8928
|
-
import { join as
|
|
10383
|
+
import { join as join14, resolve as resolve10 } from "path";
|
|
8929
10384
|
function resolveSkillDirs(workspaceDir2, configDirs) {
|
|
8930
10385
|
const candidates = [
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
...(configDirs ?? []).map((d) =>
|
|
10386
|
+
join14(homedir2(), ".wayang", "skills"),
|
|
10387
|
+
join14(workspaceDir2, ".wayang", "skills"),
|
|
10388
|
+
...(configDirs ?? []).map((d) => resolve10(d))
|
|
8934
10389
|
];
|
|
8935
10390
|
const seen = /* @__PURE__ */ new Set();
|
|
8936
10391
|
const dirs = [];
|
|
8937
10392
|
for (const dir of candidates) {
|
|
8938
10393
|
if (seen.has(dir)) continue;
|
|
8939
10394
|
seen.add(dir);
|
|
8940
|
-
if (
|
|
10395
|
+
if (existsSync8(dir)) dirs.push(dir);
|
|
8941
10396
|
}
|
|
8942
10397
|
return dirs;
|
|
8943
10398
|
}
|
|
@@ -8996,7 +10451,8 @@ var Supervisor = class {
|
|
|
8996
10451
|
{
|
|
8997
10452
|
getRunningCount: () => this.engine.getRunningCount(),
|
|
8998
10453
|
getActiveWorkers: () => this.engine.getActiveWorkers(),
|
|
8999
|
-
getPendingCount: () => this.engine.list("pending").length
|
|
10454
|
+
getPendingCount: () => this.engine.list("pending").length,
|
|
10455
|
+
getWorkerActivity: (workerId) => this.engine.getWorkerActivity(workerId)
|
|
9000
10456
|
}
|
|
9001
10457
|
);
|
|
9002
10458
|
}
|