@testchimp/cli 0.1.38 → 0.1.40
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 +150 -62
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -11,17 +11,14 @@ import { URL } from "node:url";
|
|
|
11
11
|
import { getBackendUrl, requireApiKey } from "../core/client.js";
|
|
12
12
|
const ROLE_ASSISTANT = "CHIMPHANDS_MESSAGE_ROLE_ASSISTANT";
|
|
13
13
|
const ROLE_TOOL = "CHIMPHANDS_MESSAGE_ROLE_TOOL";
|
|
14
|
+
const ROLE_REASONING = "CHIMPHANDS_MESSAGE_ROLE_REASONING";
|
|
14
15
|
const ROLE_STATUS = "CHIMPHANDS_MESSAGE_ROLE_STATUS";
|
|
15
16
|
const STATUS_RUNNING = "CHIMPHANDS_SESSION_STATUS_RUNNING";
|
|
16
17
|
const STATUS_WAITING_USER = "CHIMPHANDS_SESSION_STATUS_WAITING_USER";
|
|
17
18
|
const STATUS_IDLE = "CHIMPHANDS_SESSION_STATUS_IDLE";
|
|
18
19
|
const STATUS_FAILED = "CHIMPHANDS_SESSION_STATUS_FAILED";
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
if (!trimmed)
|
|
22
|
-
return trimmed;
|
|
23
|
-
const rest = trimmed.replace(/^\/testchimp\s*/i, "").trim();
|
|
24
|
-
return rest ? `/testchimp ${rest}` : "/testchimp";
|
|
20
|
+
function normalizeUserMessage(content) {
|
|
21
|
+
return content.trim();
|
|
25
22
|
}
|
|
26
23
|
function apiHeaders(apiKey) {
|
|
27
24
|
return {
|
|
@@ -221,6 +218,21 @@ function runOpencode(prompt, model, childEnv, postEvent) {
|
|
|
221
218
|
postEvent(ROLE_ASSISTANT, next, undefined, opencodeMessageId("oc_text_", ev.part));
|
|
222
219
|
return;
|
|
223
220
|
}
|
|
221
|
+
case "reasoning": {
|
|
222
|
+
const chunk = ev.part?.text;
|
|
223
|
+
if (!chunk)
|
|
224
|
+
return;
|
|
225
|
+
const partId = ev.part?.id || ev.part?.messageID;
|
|
226
|
+
if (!partId) {
|
|
227
|
+
postEvent(ROLE_REASONING, chunk);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const reasoningKey = `reasoning:${partId}`;
|
|
231
|
+
const next = (textByPartId.get(reasoningKey) || "") + chunk;
|
|
232
|
+
textByPartId.set(reasoningKey, next);
|
|
233
|
+
postEvent(ROLE_REASONING, next, undefined, opencodeMessageId("oc_reasoning_", ev.part));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
224
236
|
case "tool_use": {
|
|
225
237
|
if (ev.part?.state?.status !== "completed")
|
|
226
238
|
return;
|
|
@@ -291,57 +303,83 @@ function runOpencode(prompt, model, childEnv, postEvent) {
|
|
|
291
303
|
return Promise.resolve({ code: errObj.status || 1, err: fatal });
|
|
292
304
|
}
|
|
293
305
|
}
|
|
294
|
-
function
|
|
306
|
+
function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
295
307
|
const url = new URL(`${backend}/api/chimphands/sessions/${encodeURIComponent(sessionId)}/inbound`);
|
|
296
308
|
const lib = url.protocol === "https:" ? https : http;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
309
|
+
let stopped = false;
|
|
310
|
+
let reconnectTimer = null;
|
|
311
|
+
let reconnectDelayMs = 1000;
|
|
312
|
+
const scheduleReconnect = () => {
|
|
313
|
+
if (stopped || !handlers.shouldRun())
|
|
314
|
+
return;
|
|
315
|
+
if (reconnectTimer)
|
|
316
|
+
return;
|
|
317
|
+
reconnectTimer = setTimeout(() => {
|
|
318
|
+
reconnectTimer = null;
|
|
319
|
+
connect();
|
|
320
|
+
}, reconnectDelayMs);
|
|
321
|
+
reconnectDelayMs = Math.min(reconnectDelayMs * 2, 15_000);
|
|
322
|
+
};
|
|
323
|
+
const connect = () => {
|
|
324
|
+
if (stopped || !handlers.shouldRun())
|
|
325
|
+
return;
|
|
326
|
+
const req = lib.request({
|
|
327
|
+
hostname: url.hostname,
|
|
328
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
329
|
+
path: url.pathname + url.search,
|
|
330
|
+
method: "GET",
|
|
331
|
+
headers: {
|
|
332
|
+
"TestChimp-Api-Key": apiKey,
|
|
333
|
+
Accept: "text/event-stream",
|
|
334
|
+
"Cache-Control": "no-cache",
|
|
335
|
+
},
|
|
336
|
+
}, (res) => {
|
|
337
|
+
reconnectDelayMs = 1000;
|
|
338
|
+
let buf = "";
|
|
339
|
+
let eventName = "message";
|
|
340
|
+
res.on("data", (chunk) => {
|
|
341
|
+
buf += chunk.toString();
|
|
342
|
+
const parts = buf.split("\n");
|
|
343
|
+
buf = parts.pop() || "";
|
|
344
|
+
for (const line of parts) {
|
|
345
|
+
if (line.startsWith("event:")) {
|
|
346
|
+
eventName = line.slice(6).trim() || "message";
|
|
322
347
|
}
|
|
323
|
-
else if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (content)
|
|
328
|
-
onUserMessage(content);
|
|
348
|
+
else if (line.startsWith("data:")) {
|
|
349
|
+
const data = line.slice(5).trim();
|
|
350
|
+
if (eventName === "idle") {
|
|
351
|
+
handlers.onIdle();
|
|
329
352
|
}
|
|
330
|
-
|
|
331
|
-
|
|
353
|
+
else if (eventName === "user_message" || eventName === "message") {
|
|
354
|
+
try {
|
|
355
|
+
const msg = JSON.parse(data);
|
|
356
|
+
handlers.onUserMessage({
|
|
357
|
+
id: msg.id || msg.message_id,
|
|
358
|
+
content: msg.content || "",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
/* ignore */
|
|
363
|
+
}
|
|
332
364
|
}
|
|
365
|
+
eventName = "message";
|
|
366
|
+
}
|
|
367
|
+
else if (line === "") {
|
|
368
|
+
eventName = "message";
|
|
333
369
|
}
|
|
334
|
-
eventName = "message";
|
|
335
|
-
}
|
|
336
|
-
else if (line === "") {
|
|
337
|
-
eventName = "message";
|
|
338
370
|
}
|
|
339
|
-
}
|
|
371
|
+
});
|
|
372
|
+
res.on("end", () => scheduleReconnect());
|
|
340
373
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
374
|
+
req.on("error", () => scheduleReconnect());
|
|
375
|
+
req.end();
|
|
376
|
+
};
|
|
377
|
+
connect();
|
|
378
|
+
return () => {
|
|
379
|
+
stopped = true;
|
|
380
|
+
if (reconnectTimer)
|
|
381
|
+
clearTimeout(reconnectTimer);
|
|
382
|
+
};
|
|
345
383
|
}
|
|
346
384
|
export async function runChimphands(opts) {
|
|
347
385
|
const apiKey = requireApiKey();
|
|
@@ -373,9 +411,41 @@ export async function runChimphands(opts) {
|
|
|
373
411
|
console.error(`ChimpHands OpenCode model: ${opencodeModel}`);
|
|
374
412
|
const idleMs = (Number(boot.idle_timeout_seconds) || 600) * 1000;
|
|
375
413
|
const queue = [];
|
|
414
|
+
const seenUserMessageIds = new Set();
|
|
376
415
|
let idle = false;
|
|
377
|
-
let
|
|
416
|
+
let sessionActive = true;
|
|
378
417
|
let lastUserActivity = Date.now();
|
|
418
|
+
const enqueueUserMessage = (msg) => {
|
|
419
|
+
const id = msg.id?.trim();
|
|
420
|
+
if (id) {
|
|
421
|
+
if (seenUserMessageIds.has(id))
|
|
422
|
+
return;
|
|
423
|
+
seenUserMessageIds.add(id);
|
|
424
|
+
}
|
|
425
|
+
const content = normalizeUserMessage(msg.content || "");
|
|
426
|
+
if (!content)
|
|
427
|
+
return;
|
|
428
|
+
queue.push(content);
|
|
429
|
+
lastUserActivity = Date.now();
|
|
430
|
+
idle = false;
|
|
431
|
+
};
|
|
432
|
+
const pollPendingUserMessages = async () => {
|
|
433
|
+
try {
|
|
434
|
+
const text = await postJson(backend, apiKey, "/api/chimphands/consume_pending_user_messages", {
|
|
435
|
+
sessionId,
|
|
436
|
+
});
|
|
437
|
+
const data = JSON.parse(text);
|
|
438
|
+
for (const msg of data.messages || []) {
|
|
439
|
+
enqueueUserMessage({
|
|
440
|
+
id: msg.id || msg.message_id,
|
|
441
|
+
content: msg.content || "",
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Polling is best-effort when inbound SSE misses an event.
|
|
447
|
+
}
|
|
448
|
+
};
|
|
379
449
|
const postEvent = (role, content, status, messageId) => {
|
|
380
450
|
const body = {
|
|
381
451
|
sessionId,
|
|
@@ -403,30 +473,46 @@ export async function runChimphands(opts) {
|
|
|
403
473
|
};
|
|
404
474
|
if (userId)
|
|
405
475
|
childEnv.TESTCHIMP_USER_ID = userId;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
closed = true;
|
|
476
|
+
const stopInbound = connectInboundStream(backend, apiKey, sessionId, {
|
|
477
|
+
onUserMessage: enqueueUserMessage,
|
|
478
|
+
onIdle: () => {
|
|
479
|
+
idle = true;
|
|
480
|
+
},
|
|
481
|
+
shouldRun: () => sessionActive,
|
|
413
482
|
});
|
|
414
483
|
postEvent(ROLE_STATUS, "Agent ready", STATUS_RUNNING);
|
|
415
|
-
let prompt =
|
|
484
|
+
let prompt = normalizeUserMessage(promptInput || boot.initial_prompt || "");
|
|
416
485
|
if (boot.conversation_summary) {
|
|
417
486
|
prompt = `Conversation so far:\n${boot.conversation_summary}\n\nCurrent task:\n${prompt}`;
|
|
418
487
|
}
|
|
419
488
|
for (const m of boot.pending_user_messages || []) {
|
|
420
489
|
if (m?.content)
|
|
421
|
-
|
|
490
|
+
enqueueUserMessage({ content: m.content });
|
|
422
491
|
}
|
|
423
492
|
const waitForNextPrompt = () => new Promise((resolve) => {
|
|
493
|
+
let lastPollAt = 0;
|
|
424
494
|
const tick = () => {
|
|
425
495
|
if (queue.length) {
|
|
426
|
-
resolve(
|
|
496
|
+
resolve(normalizeUserMessage(queue.shift()));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
if (now - lastPollAt >= 1500) {
|
|
501
|
+
lastPollAt = now;
|
|
502
|
+
void pollPendingUserMessages().then(() => {
|
|
503
|
+
if (queue.length) {
|
|
504
|
+
resolve(normalizeUserMessage(queue.shift()));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (idle || now - lastUserActivity >= idleMs) {
|
|
508
|
+
resolve(null);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
setTimeout(tick, 500);
|
|
512
|
+
});
|
|
427
513
|
return;
|
|
428
514
|
}
|
|
429
|
-
if (idle ||
|
|
515
|
+
if (idle || now - lastUserActivity >= idleMs) {
|
|
430
516
|
resolve(null);
|
|
431
517
|
return;
|
|
432
518
|
}
|
|
@@ -435,7 +521,7 @@ export async function runChimphands(opts) {
|
|
|
435
521
|
tick();
|
|
436
522
|
});
|
|
437
523
|
while (prompt) {
|
|
438
|
-
const result = await runOpencode(
|
|
524
|
+
const result = await runOpencode(prompt, opencodeModel, childEnv, postEvent);
|
|
439
525
|
if (result.code !== 0) {
|
|
440
526
|
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
441
527
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
@@ -468,6 +554,8 @@ export async function runChimphands(opts) {
|
|
|
468
554
|
idle = false;
|
|
469
555
|
prompt = (await waitForNextPrompt()) || "";
|
|
470
556
|
}
|
|
557
|
+
sessionActive = false;
|
|
558
|
+
stopInbound();
|
|
471
559
|
console.error("ChimpHands session idle — no user input before timeout; completing.");
|
|
472
560
|
complete(STATUS_IDLE);
|
|
473
561
|
}
|
package/package.json
CHANGED