@testchimp/cli 0.1.60 → 0.1.62
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/chimphands/run.js +190 -25
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -168,6 +168,8 @@ class AgentEventPoster {
|
|
|
168
168
|
body.workingBranch = opts.workingBranch;
|
|
169
169
|
if (opts?.pullRequestUrl)
|
|
170
170
|
body.pullRequestUrl = opts.pullRequestUrl;
|
|
171
|
+
if (opts?.toolExecution)
|
|
172
|
+
body.toolExecution = opts.toolExecution;
|
|
171
173
|
const streamRole = isStreamFanoutRole(role);
|
|
172
174
|
this.chain = this.chain.then(async () => {
|
|
173
175
|
const live = !!opts?.liveStream && !opts?.durable;
|
|
@@ -193,6 +195,8 @@ class AgentEventPoster {
|
|
|
193
195
|
eph.messageId = opts.messageId;
|
|
194
196
|
if (opts?.opencodeSessionId)
|
|
195
197
|
eph.opencodeSessionId = opts.opencodeSessionId;
|
|
198
|
+
if (opts?.toolExecution)
|
|
199
|
+
eph.toolExecution = opts.toolExecution;
|
|
196
200
|
try {
|
|
197
201
|
await postJson(this.backend, this.apiKey, "/api/chimphands/post_ephemeral_agent_event", eph);
|
|
198
202
|
this.ephemeralFailCount = 0;
|
|
@@ -282,6 +286,62 @@ function extractOpencodeFatalError(raw) {
|
|
|
282
286
|
}
|
|
283
287
|
return null;
|
|
284
288
|
}
|
|
289
|
+
const TOOL_STATUS_RUNNING = "CHIMPHANDS_TOOL_EXECUTION_STATUS_RUNNING";
|
|
290
|
+
const TOOL_STATUS_PENDING = "CHIMPHANDS_TOOL_EXECUTION_STATUS_PENDING";
|
|
291
|
+
const TOOL_STATUS_COMPLETED = "CHIMPHANDS_TOOL_EXECUTION_STATUS_COMPLETED";
|
|
292
|
+
const TOOL_STATUS_ERROR = "CHIMPHANDS_TOOL_EXECUTION_STATUS_ERROR";
|
|
293
|
+
function normalizeEpochMillis(raw) {
|
|
294
|
+
if (raw == null || !Number.isFinite(raw) || raw <= 0)
|
|
295
|
+
return undefined;
|
|
296
|
+
// OpenCode uses epoch millis; guard seconds.
|
|
297
|
+
return raw < 1_000_000_000_000 ? Math.round(raw * 1000) : Math.round(raw);
|
|
298
|
+
}
|
|
299
|
+
function coerceTimeoutMillis(input) {
|
|
300
|
+
if (!input)
|
|
301
|
+
return undefined;
|
|
302
|
+
const raw = input.timeout ?? input.timeoutMs ?? input.timeout_ms;
|
|
303
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN;
|
|
304
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
305
|
+
return undefined;
|
|
306
|
+
return Math.round(n);
|
|
307
|
+
}
|
|
308
|
+
function mapToolExecutionStatus(status) {
|
|
309
|
+
const s = String(status || "").trim().toLowerCase();
|
|
310
|
+
if (!s)
|
|
311
|
+
return undefined;
|
|
312
|
+
if (s === "pending")
|
|
313
|
+
return TOOL_STATUS_PENDING;
|
|
314
|
+
if (s === "running")
|
|
315
|
+
return TOOL_STATUS_RUNNING;
|
|
316
|
+
if (s === "completed")
|
|
317
|
+
return TOOL_STATUS_COMPLETED;
|
|
318
|
+
if (s === "error" || s === "failed" || s === "cancelled")
|
|
319
|
+
return TOOL_STATUS_ERROR;
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
function toolExecutionFromOpencodePart(part) {
|
|
323
|
+
const status = mapToolExecutionStatus(part.state?.status);
|
|
324
|
+
if (!status)
|
|
325
|
+
return undefined;
|
|
326
|
+
const startedAtMillis = normalizeEpochMillis(part.state?.time?.start) ??
|
|
327
|
+
(status === TOOL_STATUS_RUNNING ? Date.now() : undefined);
|
|
328
|
+
const endedAtMillis = normalizeEpochMillis(part.state?.time?.end);
|
|
329
|
+
const toolName = String(part.state?.title || part.tool || "").trim() || undefined;
|
|
330
|
+
const timeoutMillis = coerceTimeoutMillis(part.state?.input);
|
|
331
|
+
return {
|
|
332
|
+
status,
|
|
333
|
+
toolName,
|
|
334
|
+
startedAtMillis,
|
|
335
|
+
timeoutMillis,
|
|
336
|
+
endedAtMillis,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function toolEventOptions(part, extra) {
|
|
340
|
+
const toolExecution = toolExecutionFromOpencodePart(part);
|
|
341
|
+
if (!toolExecution)
|
|
342
|
+
return extra ?? {};
|
|
343
|
+
return { ...extra, toolExecution };
|
|
344
|
+
}
|
|
285
345
|
/**
|
|
286
346
|
* OpenCode `message.part.delta` always sends `field: "text"` for both reasoning and
|
|
287
347
|
* answer parts. Resolve kind from partID → type learned via `message.part.updated`.
|
|
@@ -482,9 +542,9 @@ async function reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, po
|
|
|
482
542
|
if (!status || status === "pending" || status === "running")
|
|
483
543
|
continue;
|
|
484
544
|
const toolContent = formatToolUseContent(part);
|
|
485
|
-
postEvent(ROLE_TOOL, toolContent, {
|
|
545
|
+
postEvent(ROLE_TOOL, toolContent, toolEventOptions(part, {
|
|
486
546
|
messageId: opencodeMessageId("oc_tool_", part),
|
|
487
|
-
});
|
|
547
|
+
}));
|
|
488
548
|
posted += 1;
|
|
489
549
|
if (status === "completed") {
|
|
490
550
|
const detected = detectWorkingBranchFromToolOutput(toolContent);
|
|
@@ -663,17 +723,17 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
663
723
|
// Ephemeral "(running)" bubbles while UI attached (liveStream gated in poster).
|
|
664
724
|
if (status === "running") {
|
|
665
725
|
const toolContent = formatToolUseContent(part);
|
|
666
|
-
callbacks.postEvent(ROLE_TOOL, toolContent, liveOpts({
|
|
726
|
+
callbacks.postEvent(ROLE_TOOL, toolContent, toolEventOptions(part, liveOpts({
|
|
667
727
|
messageId: opencodeMessageId("oc_tool_", part),
|
|
668
728
|
throttle: false,
|
|
669
|
-
}));
|
|
729
|
+
})));
|
|
670
730
|
return;
|
|
671
731
|
}
|
|
672
732
|
const toolContent = formatToolUseContent(part);
|
|
673
|
-
callbacks.postEvent(ROLE_TOOL, toolContent, liveOpts({
|
|
733
|
+
callbacks.postEvent(ROLE_TOOL, toolContent, toolEventOptions(part, liveOpts({
|
|
674
734
|
messageId: opencodeMessageId("oc_tool_", part),
|
|
675
735
|
throttle: false,
|
|
676
|
-
}));
|
|
736
|
+
})));
|
|
677
737
|
if (status === "completed") {
|
|
678
738
|
const detected = detectWorkingBranchFromToolOutput(toolContent);
|
|
679
739
|
if (detected.branch) {
|
|
@@ -946,6 +1006,35 @@ function writeOpencodeConfig(backend, apiKey, boot) {
|
|
|
946
1006
|
}, null, 2));
|
|
947
1007
|
return model;
|
|
948
1008
|
}
|
|
1009
|
+
/** Abort in-flight tool/LLM work on the local OpenCode server (attach mode). */
|
|
1010
|
+
async function abortOpencodeSession(attachUrl, opencodeSessionId) {
|
|
1011
|
+
const base = attachUrl.replace(/\/$/, "");
|
|
1012
|
+
const url = `${base}/session/${encodeURIComponent(opencodeSessionId)}/abort`;
|
|
1013
|
+
try {
|
|
1014
|
+
const res = await fetch(url, {
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
headers: {
|
|
1017
|
+
Accept: "application/json",
|
|
1018
|
+
"x-opencode-directory": process.cwd(),
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
console.error(`ChimpHands OpenCode session abort: session=${opencodeSessionId} http=${res.status}`);
|
|
1022
|
+
}
|
|
1023
|
+
catch (err) {
|
|
1024
|
+
console.error(`ChimpHands OpenCode session abort failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function parseInboundUserMessage(data) {
|
|
1028
|
+
if (!data || typeof data !== "object")
|
|
1029
|
+
return null;
|
|
1030
|
+
const msg = data;
|
|
1031
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
1032
|
+
const idRaw = (typeof msg.id === "string" && msg.id) ||
|
|
1033
|
+
(typeof msg.message_id === "string" && msg.message_id) ||
|
|
1034
|
+
(typeof msg.messageId === "string" && msg.messageId) ||
|
|
1035
|
+
"";
|
|
1036
|
+
return { id: idRaw || undefined, content };
|
|
1037
|
+
}
|
|
949
1038
|
function buildOpencodeArgs(prompt, model, opencodeSessionId, attachUrl) {
|
|
950
1039
|
// --auto: headless CI must approve tool permissions (1.18+ otherwise auto-rejects).
|
|
951
1040
|
// --print-logs: surface server/client progress on stderr while waiting for first token.
|
|
@@ -1081,6 +1170,34 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1081
1170
|
const textByPartId = new Map();
|
|
1082
1171
|
const partTypeById = new Map();
|
|
1083
1172
|
let sawStdout = false;
|
|
1173
|
+
let cancelEscalationTimer = null;
|
|
1174
|
+
let cancelWatcher = null;
|
|
1175
|
+
const clearCancelWatcher = () => {
|
|
1176
|
+
if (cancelWatcher) {
|
|
1177
|
+
clearInterval(cancelWatcher);
|
|
1178
|
+
cancelWatcher = null;
|
|
1179
|
+
}
|
|
1180
|
+
if (cancelEscalationTimer) {
|
|
1181
|
+
clearTimeout(cancelEscalationTimer);
|
|
1182
|
+
cancelEscalationTimer = null;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
const killActiveChild = (signal) => {
|
|
1186
|
+
try {
|
|
1187
|
+
if (!child.killed)
|
|
1188
|
+
child.kill(signal);
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
/* ignore */
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
cancelWatcher = setInterval(() => {
|
|
1195
|
+
if (!callbacks.getCancelRequested?.())
|
|
1196
|
+
return;
|
|
1197
|
+
clearCancelWatcher();
|
|
1198
|
+
killActiveChild("SIGTERM");
|
|
1199
|
+
cancelEscalationTimer = setTimeout(() => killActiveChild("SIGKILL"), 2000);
|
|
1200
|
+
}, 500);
|
|
1084
1201
|
let progressTicker = setInterval(() => {
|
|
1085
1202
|
if (sawStdout) {
|
|
1086
1203
|
if (progressTicker) {
|
|
@@ -1200,9 +1317,9 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1200
1317
|
if (!status || status === "pending" || status === "running")
|
|
1201
1318
|
return;
|
|
1202
1319
|
const toolContent = formatToolUseContent(ev.part);
|
|
1203
|
-
callbacks.postEvent(ROLE_TOOL, toolContent, {
|
|
1320
|
+
callbacks.postEvent(ROLE_TOOL, toolContent, toolEventOptions(ev.part, {
|
|
1204
1321
|
messageId: opencodeMessageId("oc_tool_", ev.part),
|
|
1205
|
-
});
|
|
1322
|
+
}));
|
|
1206
1323
|
if (status === "completed") {
|
|
1207
1324
|
const detected = detectWorkingBranchFromToolOutput(toolContent);
|
|
1208
1325
|
if (detected.branch) {
|
|
@@ -1236,6 +1353,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1236
1353
|
});
|
|
1237
1354
|
child.on("close", (code) => {
|
|
1238
1355
|
callbacks.onActiveChild?.(null);
|
|
1356
|
+
clearCancelWatcher();
|
|
1239
1357
|
if (progressTicker) {
|
|
1240
1358
|
clearInterval(progressTicker);
|
|
1241
1359
|
progressTicker = null;
|
|
@@ -1380,6 +1498,7 @@ function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
|
1380
1498
|
catch {
|
|
1381
1499
|
/* ignore malformed payload */
|
|
1382
1500
|
}
|
|
1501
|
+
console.error(`ChimpHands inbound SSE cancel_turn for session ${sessionId}`);
|
|
1383
1502
|
handlers.onCancelTurn?.();
|
|
1384
1503
|
}
|
|
1385
1504
|
else if (eventName === "user_file_edit") {
|
|
@@ -1403,11 +1522,11 @@ function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
|
1403
1522
|
}
|
|
1404
1523
|
else if (eventName === "user_message" || eventName === "message") {
|
|
1405
1524
|
try {
|
|
1406
|
-
const msg = JSON.parse(data);
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1525
|
+
const msg = parseInboundUserMessage(JSON.parse(data));
|
|
1526
|
+
if (!msg)
|
|
1527
|
+
continue;
|
|
1528
|
+
console.error(`ChimpHands inbound SSE user_message for session ${sessionId}`);
|
|
1529
|
+
handlers.onUserMessage(msg);
|
|
1411
1530
|
}
|
|
1412
1531
|
catch {
|
|
1413
1532
|
/* ignore */
|
|
@@ -1614,6 +1733,8 @@ export async function runChimphands(opts) {
|
|
|
1614
1733
|
queue.push(content);
|
|
1615
1734
|
lastUserActivity = Date.now();
|
|
1616
1735
|
idle = false;
|
|
1736
|
+
const preview = content.length > 120 ? `${content.slice(0, 117)}...` : content;
|
|
1737
|
+
console.error(`ChimpHands queued user message (turnActive=${agentTurnInProgress}, depth=${queue.length}): ${JSON.stringify(preview)}`);
|
|
1617
1738
|
};
|
|
1618
1739
|
const pollPendingUserMessages = async () => {
|
|
1619
1740
|
try {
|
|
@@ -1674,33 +1795,56 @@ export async function runChimphands(opts) {
|
|
|
1674
1795
|
};
|
|
1675
1796
|
if (userId)
|
|
1676
1797
|
childEnv.TESTCHIMP_USER_ID = userId;
|
|
1798
|
+
const handleCancelTurn = () => {
|
|
1799
|
+
console.error(`ChimpHands cancel_turn received for session ${sessionId}`);
|
|
1800
|
+
cancelTurnRequested = true;
|
|
1801
|
+
const sessionToAbort = opencodeSessionId?.trim();
|
|
1802
|
+
if (attachUrl && sessionToAbort) {
|
|
1803
|
+
void abortOpencodeSession(attachUrl, sessionToAbort);
|
|
1804
|
+
}
|
|
1805
|
+
const ch = activeOpencodeChild;
|
|
1806
|
+
if (ch && !ch.killed) {
|
|
1807
|
+
try {
|
|
1808
|
+
ch.kill("SIGTERM");
|
|
1809
|
+
setTimeout(() => {
|
|
1810
|
+
try {
|
|
1811
|
+
if (!ch.killed)
|
|
1812
|
+
ch.kill("SIGKILL");
|
|
1813
|
+
}
|
|
1814
|
+
catch {
|
|
1815
|
+
/* ignore */
|
|
1816
|
+
}
|
|
1817
|
+
}, 2000);
|
|
1818
|
+
}
|
|
1819
|
+
catch {
|
|
1820
|
+
/* ignore */
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1677
1824
|
const stopInbound = connectInboundStream(backend, apiKey, sessionId, {
|
|
1678
1825
|
onUserMessage: enqueueUserMessage,
|
|
1679
1826
|
onIdle: () => {
|
|
1680
1827
|
idle = true;
|
|
1681
1828
|
},
|
|
1682
|
-
onCancelTurn:
|
|
1683
|
-
cancelTurnRequested = true;
|
|
1684
|
-
const ch = activeOpencodeChild;
|
|
1685
|
-
if (ch && !ch.killed) {
|
|
1686
|
-
try {
|
|
1687
|
-
ch.kill("SIGTERM");
|
|
1688
|
-
}
|
|
1689
|
-
catch {
|
|
1690
|
-
/* ignore */
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
},
|
|
1829
|
+
onCancelTurn: handleCancelTurn,
|
|
1694
1830
|
onUserFileEdit: handleUserFileEdit,
|
|
1695
1831
|
shouldRun: () => sessionActive,
|
|
1696
1832
|
});
|
|
1833
|
+
const inboundPollTimer = setInterval(() => {
|
|
1834
|
+
if (!sessionActive)
|
|
1835
|
+
return;
|
|
1836
|
+
void pollPendingUserMessages();
|
|
1837
|
+
}, 1500);
|
|
1697
1838
|
if (runtimeId && attachUrl) {
|
|
1698
1839
|
stopTunnel = startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, {
|
|
1699
1840
|
onUserFileEdit: handleUserFileEdit,
|
|
1841
|
+
onCancelTurn: handleCancelTurn,
|
|
1842
|
+
onUserMessage: enqueueUserMessage,
|
|
1700
1843
|
});
|
|
1701
1844
|
}
|
|
1702
1845
|
const shutdownRuntime = async () => {
|
|
1703
1846
|
sessionActive = false;
|
|
1847
|
+
clearInterval(inboundPollTimer);
|
|
1704
1848
|
stopInbound();
|
|
1705
1849
|
stopTunnel();
|
|
1706
1850
|
stopHeartbeat();
|
|
@@ -2109,6 +2253,27 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, han
|
|
|
2109
2253
|
socket.send(JSON.stringify({ type: "pong" }));
|
|
2110
2254
|
return;
|
|
2111
2255
|
}
|
|
2256
|
+
if (frame.type === "inbound_event" && frame.event === "cancel_turn") {
|
|
2257
|
+
const payload = frame.data;
|
|
2258
|
+
if (payload?.sessionId && payload.sessionId !== sessionId) {
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
console.error(`ChimpHands tunnel cancel_turn for session ${sessionId}`);
|
|
2262
|
+
handlers?.onCancelTurn?.();
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (frame.type === "inbound_event" && frame.event === "user_message" && frame.data) {
|
|
2266
|
+
const payload = frame.data;
|
|
2267
|
+
if (payload?.sessionId && payload.sessionId !== sessionId) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const msg = parseInboundUserMessage(frame.data);
|
|
2271
|
+
if (msg) {
|
|
2272
|
+
console.error(`ChimpHands tunnel user_message for session ${sessionId}`);
|
|
2273
|
+
handlers?.onUserMessage?.(msg);
|
|
2274
|
+
}
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2112
2277
|
if (frame.type === "inbound_event" && frame.event === "user_file_edit" && frame.data) {
|
|
2113
2278
|
const edit = frame.data;
|
|
2114
2279
|
if (edit.sessionId && edit.sessionId !== sessionId) {
|
package/package.json
CHANGED