@matatbread/matbot-core 0.4.8 → 0.4.9
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/package.json +2 -2
- package/src/runner.ts +47 -1
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"node",
|
|
5
5
|
"browser"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.4.
|
|
7
|
+
"version": "0.4.9",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24.0.0"
|
|
10
10
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@matatbread/matbot-plugin-api": "^0.4.
|
|
38
|
+
"@matatbread/matbot-plugin-api": "^0.4.9"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^6.0.3"
|
package/src/runner.ts
CHANGED
|
@@ -44,6 +44,34 @@ const TRUNCATION_CREATOR = 'matbot-truncation';
|
|
|
44
44
|
const abortReason = (signal: AbortSignal): string =>
|
|
45
45
|
typeof signal.reason === 'string' ? signal.reason : 'user-abort';
|
|
46
46
|
|
|
47
|
+
/** How long a tool executor may keep the turn alive AFTER the turn was aborted, before the runner stops
|
|
48
|
+
* reading it. An abort must abort: any executor can fail to end — a child process whose stdout was
|
|
49
|
+
* inherited by an orphan, a generator awaiting something that never settles, a bridged remote that went
|
|
50
|
+
* away — and the runner awaiting it is then the only thing holding a turn that nothing left in the
|
|
51
|
+
* process can end. Abandoning is bounded rather than immediate because a well-behaved tool cleaning up
|
|
52
|
+
* after a cut-off (flushing a partial result, closing a handle) is the common case and worth waiting for.
|
|
53
|
+
* Only armed once `signal` is aborted, so a long-running tool on a healthy turn is never cut short. */
|
|
54
|
+
const ABANDONED_TOOL_GRACE_MS = 30_000;
|
|
55
|
+
|
|
56
|
+
/** Resolves `ABANDON` once the signal has been aborted for `ms`, and never otherwise. */
|
|
57
|
+
const ABANDON = Symbol('abandon');
|
|
58
|
+
function abandonDeadline(signal: AbortSignal, ms: number): { wait: Promise<typeof ABANDON>; dispose: () => void } {
|
|
59
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
let arm: (() => void) | undefined;
|
|
61
|
+
const wait = new Promise<typeof ABANDON>(resolve => {
|
|
62
|
+
arm = () => { timer = setTimeout(() => resolve(ABANDON), ms); };
|
|
63
|
+
if (signal.aborted) arm();
|
|
64
|
+
else signal.addEventListener('abort', arm, { once: true });
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
wait,
|
|
68
|
+
dispose: () => {
|
|
69
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
70
|
+
if (arm !== undefined) signal.removeEventListener('abort', arm);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
const INTERRUPTED_TOOL_RESULT = {
|
|
48
76
|
error: 'Tool call interrupted before completion — the turn was interrupted while it was running. It may not have run to completion, and any side effect may or may not have occurred. Re-run it only if you still need its result.',
|
|
49
77
|
} as const;
|
|
@@ -562,8 +590,24 @@ export async function* runSession(opts: RunSessionOpts): AsyncIterable<TurnEvent
|
|
|
562
590
|
...(opts.files !== undefined ? { files: opts.files } : {}),
|
|
563
591
|
};
|
|
564
592
|
|
|
593
|
+
// Iterated by hand rather than with `for await`, so the read can be bounded once the turn is
|
|
594
|
+
// aborted (see ABANDONED_TOOL_GRACE_MS): an executor that never returns would otherwise hold this
|
|
595
|
+
// turn open for ever, with no path back for the session.
|
|
596
|
+
const events = siteScoped(site, tool.executor.execute(tc.input, toolCtx))[Symbol.asyncIterator]();
|
|
597
|
+
const deadline = abandonDeadline(signal, ABANDONED_TOOL_GRACE_MS);
|
|
565
598
|
try {
|
|
566
|
-
for
|
|
599
|
+
for (;;) {
|
|
600
|
+
const step = await Promise.race([events.next(), deadline.wait]);
|
|
601
|
+
if (step === ABANDON) {
|
|
602
|
+
console.warn(`[matbot] tool "${tc.name}" did not return ${ABANDONED_TOOL_GRACE_MS}ms after the turn was aborted — abandoned.`);
|
|
603
|
+
// Not awaited: a tool that ignored the abort may well not return from `return()` either, and
|
|
604
|
+
// awaiting it here is the same hang one frame further out.
|
|
605
|
+
void events.return?.(undefined as never);
|
|
606
|
+
isError = true;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
if (step.done === true) break;
|
|
610
|
+
const toolEv = step.value;
|
|
567
611
|
switch (toolEv.type) {
|
|
568
612
|
case 'stdout': yield { type: 'tool:stdout', callId: tc.id, chunk: toolEv.chunk, traceId }; break;
|
|
569
613
|
case 'stderr': yield { type: 'tool:stderr', callId: tc.id, chunk: toolEv.chunk, traceId }; break;
|
|
@@ -582,6 +626,8 @@ export async function* runSession(opts: RunSessionOpts): AsyncIterable<TurnEvent
|
|
|
582
626
|
} catch (e) {
|
|
583
627
|
result = { error: String(e) };
|
|
584
628
|
isError = true;
|
|
629
|
+
} finally {
|
|
630
|
+
deadline.dispose();
|
|
585
631
|
}
|
|
586
632
|
|
|
587
633
|
// A tool that errored under an aborted signal did so because it was cut off (a steer interrupt,
|