@animalabs/agent-framework 0.5.6 → 0.6.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/src/agent.d.ts +20 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +45 -14
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +220 -5
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1489 -69
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +71 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +292 -9
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/gate-script-worker.d.ts +2 -0
- package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
- package/dist/src/gate/gate-script-worker.js +69 -0
- package/dist/src/gate/gate-script-worker.js.map +1 -0
- package/dist/src/gate/gate-script.d.ts +44 -0
- package/dist/src/gate/gate-script.d.ts.map +1 -0
- package/dist/src/gate/gate-script.js +164 -0
- package/dist/src/gate/gate-script.js.map +1 -0
- package/dist/src/gate/types.d.ts +35 -3
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +93 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +260 -6
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +15 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +161 -0
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -0
- package/dist/src/mcpl/conversation-router.js +179 -0
- package/dist/src/mcpl/conversation-router.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +10 -1
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +2 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +1 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +35 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +149 -206
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +98 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -0
- package/dist/src/mcpl/transport.js +281 -0
- package/dist/src/mcpl/transport.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +42 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +5 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +16 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +10 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +83 -0
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/prose-segments.d.ts +18 -0
- package/dist/src/prose-segments.d.ts.map +1 -0
- package/dist/src/prose-segments.js +39 -0
- package/dist/src/prose-segments.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
- package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog.js +106 -0
- package/dist/src/runtime/liveness-watchdog.js.map +1 -0
- package/dist/src/tool-result-history.d.ts +17 -0
- package/dist/src/tool-result-history.d.ts.map +1 -0
- package/dist/src/tool-result-history.js +60 -0
- package/dist/src/tool-result-history.js.map +1 -0
- package/dist/src/types/agent.d.ts +21 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +22 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +28 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.d.ts +2 -0
- package/dist/test/channel-registry-routing.test.d.ts.map +1 -0
- package/dist/test/channel-registry-routing.test.js +182 -0
- package/dist/test/channel-registry-routing.test.js.map +1 -0
- package/dist/test/checkpoint-attribution.test.d.ts +2 -0
- package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
- package/dist/test/checkpoint-attribution.test.js +40 -0
- package/dist/test/checkpoint-attribution.test.js.map +1 -0
- package/dist/test/conversation-router.test.d.ts +2 -0
- package/dist/test/conversation-router.test.d.ts.map +1 -0
- package/dist/test/conversation-router.test.js +160 -0
- package/dist/test/conversation-router.test.js.map +1 -0
- package/dist/test/conversation-routing.test.d.ts +7 -0
- package/dist/test/conversation-routing.test.d.ts.map +1 -0
- package/dist/test/conversation-routing.test.js +203 -0
- package/dist/test/conversation-routing.test.js.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.js +137 -0
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
- package/dist/test/event-gate-script.test.d.ts +2 -0
- package/dist/test/event-gate-script.test.d.ts.map +1 -0
- package/dist/test/event-gate-script.test.js +95 -0
- package/dist/test/event-gate-script.test.js.map +1 -0
- package/dist/test/event-gate-tags.test.d.ts +2 -0
- package/dist/test/event-gate-tags.test.d.ts.map +1 -0
- package/dist/test/event-gate-tags.test.js +76 -0
- package/dist/test/event-gate-tags.test.js.map +1 -0
- package/dist/test/framework.test.js +73 -1
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/helpers/mock-membrane.d.ts +41 -0
- package/dist/test/helpers/mock-membrane.d.ts.map +1 -0
- package/dist/test/helpers/mock-membrane.js +161 -0
- package/dist/test/helpers/mock-membrane.js.map +1 -0
- package/dist/test/inference-failure-observability.test.d.ts +2 -0
- package/dist/test/inference-failure-observability.test.d.ts.map +1 -0
- package/dist/test/inference-failure-observability.test.js +75 -0
- package/dist/test/inference-failure-observability.test.js.map +1 -0
- package/dist/test/liveness-watchdog.test.d.ts +2 -0
- package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
- package/dist/test/liveness-watchdog.test.js +78 -0
- package/dist/test/liveness-watchdog.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
- package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +155 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -0
- package/dist/test/prose-segments.test.d.ts +2 -0
- package/dist/test/prose-segments.test.d.ts.map +1 -0
- package/dist/test/prose-segments.test.js +52 -0
- package/dist/test/prose-segments.test.js.map +1 -0
- package/dist/test/tool-result-history.test.d.ts +7 -0
- package/dist/test/tool-result-history.test.d.ts.map +1 -0
- package/dist/test/tool-result-history.test.js +77 -0
- package/dist/test/tool-result-history.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.d.ts +19 -0
- package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +175 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/agent.ts +53 -18
- package/src/framework.ts +1685 -71
- package/src/gate/event-gate.ts +297 -9
- package/src/gate/gate-script-worker.ts +73 -0
- package/src/gate/gate-script.ts +180 -0
- package/src/gate/types.ts +36 -3
- package/src/index.ts +12 -0
- package/src/mcpl/channel-registry.ts +331 -6
- package/src/mcpl/checkpoint-manager.ts +16 -0
- package/src/mcpl/conversation-router.ts +284 -0
- package/src/mcpl/feature-set-manager.ts +14 -1
- package/src/mcpl/index.ts +12 -0
- package/src/mcpl/push-handler.ts +1 -0
- package/src/mcpl/server-connection.ts +157 -224
- package/src/mcpl/transport.ts +307 -0
- package/src/mcpl/types.ts +49 -2
- package/src/module-registry.ts +25 -2
- package/src/modules/discord/index.ts +93 -0
- package/src/prose-segments.ts +42 -0
- package/src/runtime/liveness-watchdog-worker.ts +66 -0
- package/src/runtime/liveness-watchdog.ts +122 -0
- package/src/tool-result-history.ts +57 -0
- package/src/types/agent.ts +21 -0
- package/src/types/framework.ts +24 -0
- package/src/types/module.ts +29 -0
- package/src/types/trace.ts +36 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Liveness watchdog worker — runs on its own thread, so it keeps ticking even
|
|
3
|
+
* when the main thread is wedged. Watches the shared heartbeat; if it goes stale
|
|
4
|
+
* past the threshold, writes a diagnostic and kills the whole process.
|
|
5
|
+
*
|
|
6
|
+
* Killing from a worker: process.kill(process.pid, signal) signals the whole
|
|
7
|
+
* process. SIGABRT → core dump (if enabled) + terminate; SIGKILL → immediate.
|
|
8
|
+
* Catchable signals (SIGTERM) are avoided — their handler would run on the
|
|
9
|
+
* wedged main thread and never fire.
|
|
10
|
+
*/
|
|
11
|
+
import { workerData } from 'node:worker_threads';
|
|
12
|
+
import { writeFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
interface Data {
|
|
15
|
+
sab: SharedArrayBuffer;
|
|
16
|
+
thresholdMs: number;
|
|
17
|
+
checkMs: number;
|
|
18
|
+
action: 'abort' | 'exit';
|
|
19
|
+
reportPath: string | null;
|
|
20
|
+
}
|
|
21
|
+
const { sab, thresholdMs, checkMs, action, reportPath } = workerData as Data;
|
|
22
|
+
|
|
23
|
+
const hb = new Float64Array(sab, 0, 1);
|
|
24
|
+
const phaseBuf = new Uint8Array(sab, 16, 48);
|
|
25
|
+
const dec = new TextDecoder();
|
|
26
|
+
|
|
27
|
+
function currentPhase(): string {
|
|
28
|
+
return dec.decode(phaseBuf).replace(/\0+$/, '') || 'unknown';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function check(): void {
|
|
32
|
+
const last = hb[0];
|
|
33
|
+
const age = Date.now() - last;
|
|
34
|
+
if (last > 0 && age > thresholdMs) {
|
|
35
|
+
const phase = currentPhase();
|
|
36
|
+
const report = {
|
|
37
|
+
ts: new Date().toISOString(),
|
|
38
|
+
event: 'main-thread-wedged',
|
|
39
|
+
ageMs: Math.round(age),
|
|
40
|
+
thresholdMs,
|
|
41
|
+
phase,
|
|
42
|
+
action,
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
};
|
|
45
|
+
const line = `[liveness-watchdog] main thread unresponsive ${Math.round(age)}ms ` +
|
|
46
|
+
`(phase=${phase}) — failing hard via ${action}`;
|
|
47
|
+
try {
|
|
48
|
+
console.error(line);
|
|
49
|
+
} catch {
|
|
50
|
+
/* ignore */
|
|
51
|
+
}
|
|
52
|
+
if (reportPath) {
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(reportPath, JSON.stringify(report) + '\n', { flag: 'a' });
|
|
55
|
+
} catch {
|
|
56
|
+
/* best effort */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Kill the whole process. The supervisor restarts a fresh one.
|
|
60
|
+
process.kill(process.pid, action === 'abort' ? 'SIGABRT' : 'SIGKILL');
|
|
61
|
+
return; // process is dying
|
|
62
|
+
}
|
|
63
|
+
setTimeout(check, checkMs);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setTimeout(check, checkMs);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LivenessWatchdog — detects a wedged (unresponsive) main thread and fails hard.
|
|
3
|
+
*
|
|
4
|
+
* The agent's event loop is single-threaded. A synchronous infinite loop or a
|
|
5
|
+
* self-rescheduling microtask flood (e.g. a pathological compression drain) can
|
|
6
|
+
* peg the thread for minutes: no events, no heartbeat, no recovery — the agent
|
|
7
|
+
* goes silently deaf. A same-thread timer can't catch this (it can't fire while
|
|
8
|
+
* the loop is blocked), so detection must live off-thread.
|
|
9
|
+
*
|
|
10
|
+
* Mechanism: the main thread stamps a heartbeat (and an optional phase label)
|
|
11
|
+
* into shared memory on a timer. A worker thread — which keeps running even when
|
|
12
|
+
* main is wedged — watches the heartbeat; if it goes stale past a threshold it
|
|
13
|
+
* writes a diagnostic and kills the whole process:
|
|
14
|
+
* - 'abort' → SIGABRT (core dump if enabled, for an offline stack) then dies
|
|
15
|
+
* - 'exit' → SIGKILL (clean, quiet; rely on the supervisor to restart)
|
|
16
|
+
* Either way the supervisor (launchd/systemd) restarts a fresh process instead
|
|
17
|
+
* of leaving a deaf one running.
|
|
18
|
+
*/
|
|
19
|
+
import { Worker } from 'node:worker_threads';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { phaseChannel } from '@animalabs/context-manager';
|
|
23
|
+
|
|
24
|
+
const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'liveness-watchdog-worker.js');
|
|
25
|
+
|
|
26
|
+
const HEARTBEAT_OFFSET = 0; // Float64: last heartbeat (ms since epoch)
|
|
27
|
+
const PHASE_OFFSET = 16; // Uint8[48]: current phase label (UTF-8)
|
|
28
|
+
const PHASE_LEN = 48;
|
|
29
|
+
const SAB_SIZE = 64;
|
|
30
|
+
|
|
31
|
+
export interface WatchdogOptions {
|
|
32
|
+
/** Wedge threshold: kill if no heartbeat for this long. Default 30000ms. */
|
|
33
|
+
thresholdMs?: number;
|
|
34
|
+
/** Heartbeat cadence. Default 1000ms. */
|
|
35
|
+
heartbeatMs?: number;
|
|
36
|
+
/** 'abort' → SIGABRT (core dump for debugging); 'exit' → SIGKILL. Default 'abort'. */
|
|
37
|
+
action?: 'abort' | 'exit';
|
|
38
|
+
/** Optional file the worker writes a JSON wedge report to before killing. */
|
|
39
|
+
reportPath?: string;
|
|
40
|
+
/** Disable entirely. */
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class LivenessWatchdog {
|
|
45
|
+
private readonly sab = new SharedArrayBuffer(SAB_SIZE);
|
|
46
|
+
private readonly hb = new Float64Array(this.sab, HEARTBEAT_OFFSET, 1);
|
|
47
|
+
private readonly phaseBuf = new Uint8Array(this.sab, PHASE_OFFSET, PHASE_LEN);
|
|
48
|
+
private readonly enc = new TextEncoder();
|
|
49
|
+
private timer?: ReturnType<typeof setInterval>;
|
|
50
|
+
private worker?: Worker;
|
|
51
|
+
private started = false;
|
|
52
|
+
|
|
53
|
+
constructor(private readonly opts: WatchdogOptions = {}) {}
|
|
54
|
+
|
|
55
|
+
start(): void {
|
|
56
|
+
if (this.started || this.opts.enabled === false) return;
|
|
57
|
+
this.started = true;
|
|
58
|
+
const heartbeatMs = this.opts.heartbeatMs ?? 1000;
|
|
59
|
+
this.setPhase('starting');
|
|
60
|
+
this.beat();
|
|
61
|
+
this.timer = setInterval(() => this.beat(), heartbeatMs);
|
|
62
|
+
this.timer.unref?.();
|
|
63
|
+
try {
|
|
64
|
+
this.worker = new Worker(WORKER_PATH, {
|
|
65
|
+
workerData: {
|
|
66
|
+
sab: this.sab,
|
|
67
|
+
thresholdMs: this.opts.thresholdMs ?? 30_000,
|
|
68
|
+
checkMs: Math.min(heartbeatMs, 2000),
|
|
69
|
+
action: this.opts.action ?? 'abort',
|
|
70
|
+
reportPath: this.opts.reportPath ?? null,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
this.worker.unref();
|
|
74
|
+
this.worker.on('error', (e) => console.error('[liveness-watchdog] worker error:', e.message));
|
|
75
|
+
// Wire the phase channel so labelled synchronous spans (compression,
|
|
76
|
+
// context-build, merge-graph walks) name themselves in a wedge report.
|
|
77
|
+
phaseChannel.report = (label: string) => this.setPhase(label);
|
|
78
|
+
console.error(
|
|
79
|
+
`[liveness-watchdog] armed: threshold=${this.opts.thresholdMs ?? 30_000}ms ` +
|
|
80
|
+
`action=${this.opts.action ?? 'abort'}`,
|
|
81
|
+
);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error('[liveness-watchdog] failed to start:', (e as Error).message);
|
|
84
|
+
}
|
|
85
|
+
this.setPhase('idle');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Stamp liveness. Called on a timer; the worker watches for staleness. */
|
|
89
|
+
private beat(): void {
|
|
90
|
+
this.hb[0] = Date.now();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Label the operation the main thread is about to run synchronously, so a
|
|
95
|
+
* wedge report names it (e.g. 'compression-tick', 'context-build'). Keep it
|
|
96
|
+
* short (truncated to 48 bytes). Reset to 'idle' when done.
|
|
97
|
+
*/
|
|
98
|
+
setPhase(label: string): void {
|
|
99
|
+
this.phaseBuf.fill(0);
|
|
100
|
+
const bytes = this.enc.encode(label);
|
|
101
|
+
this.phaseBuf.set(bytes.subarray(0, PHASE_LEN));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Run a synchronous section under a named phase, restoring the prior phase. */
|
|
105
|
+
withPhase<T>(label: string, fn: () => T): T {
|
|
106
|
+
this.setPhase(label);
|
|
107
|
+
try {
|
|
108
|
+
return fn();
|
|
109
|
+
} finally {
|
|
110
|
+
this.setPhase('idle');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
stop(): void {
|
|
115
|
+
if (this.timer) clearInterval(this.timer);
|
|
116
|
+
this.timer = undefined;
|
|
117
|
+
phaseChannel.report = () => {};
|
|
118
|
+
void this.worker?.terminate();
|
|
119
|
+
this.worker = undefined;
|
|
120
|
+
this.started = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert an AF `ToolResult.data` value into the string form that lives in
|
|
3
|
+
* the agent's conversation history.
|
|
4
|
+
*
|
|
5
|
+
* The live path (`toMembraneToolResult` in framework.ts) preserves MCP image
|
|
6
|
+
* blocks natively so the model sees the bytes on the current turn. The
|
|
7
|
+
* *persisted* copy needs to survive recompilation, compression, and
|
|
8
|
+
* `/restore` — storing megabytes of base64 there gets corrupted on the next
|
|
9
|
+
* `maxChars` slice and re-introduces, one turn deferred, exactly the silent
|
|
10
|
+
* hallucination this feature exists to kill.
|
|
11
|
+
*
|
|
12
|
+
* For MCP-shaped content arrays, this helper keeps text blocks verbatim and
|
|
13
|
+
* replaces image blocks with a short `[image: mimeType, ~NKB]` placeholder.
|
|
14
|
+
* For anything else it falls back to JSON.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { safeSlice } from './safe-slice.js';
|
|
18
|
+
|
|
19
|
+
export function toolResultDataToHistoryString(data: unknown, maxChars?: number): string {
|
|
20
|
+
const fromArray = tryHistoryStringFromContentArray(data);
|
|
21
|
+
const str = fromArray ?? JSON.stringify(data);
|
|
22
|
+
if (maxChars && str.length > maxChars) {
|
|
23
|
+
return safeSlice(str, 0, maxChars)
|
|
24
|
+
+ '\n\n[truncated — original was ' + str.length + ' chars]';
|
|
25
|
+
}
|
|
26
|
+
return str;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If `data` is an MCP content array whose every block has a shape we
|
|
31
|
+
* recognize, return a history-safe string (images → placeholders). Otherwise
|
|
32
|
+
* return `null` to defer to the caller's fallback (JSON, usually).
|
|
33
|
+
*/
|
|
34
|
+
function tryHistoryStringFromContentArray(data: unknown): string | null {
|
|
35
|
+
if (!Array.isArray(data)) return null;
|
|
36
|
+
const parts: string[] = [];
|
|
37
|
+
for (const raw of data) {
|
|
38
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
39
|
+
const b = raw as { type?: unknown; text?: unknown; data?: unknown; mimeType?: unknown };
|
|
40
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
41
|
+
parts.push(b.text);
|
|
42
|
+
} else if (b.type === 'image' && typeof b.data === 'string' && typeof b.mimeType === 'string') {
|
|
43
|
+
// Decoded byte estimate from base64 length (3/4 ratio, rounded).
|
|
44
|
+
const approxBytes = Math.floor(b.data.length * 3 / 4);
|
|
45
|
+
parts.push(`[image: ${b.mimeType}, ${formatSize(approxBytes)}]`);
|
|
46
|
+
} else {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatSize(bytes: number): string {
|
|
54
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
55
|
+
if (bytes < 1024 * 1024) return `~${Math.round(bytes / 1024)}KB`;
|
|
56
|
+
return `~${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
57
|
+
}
|
package/src/types/agent.ts
CHANGED
|
@@ -42,6 +42,9 @@ export interface AgentConfig {
|
|
|
42
42
|
* restarts with recompiled (compressed) context. Default: 150000. */
|
|
43
43
|
maxStreamTokens?: number;
|
|
44
44
|
|
|
45
|
+
/** Per-agent context compile budget (input tokens). When unset, the
|
|
46
|
+
* ContextManager's built-in default (100k) applies. */
|
|
47
|
+
contextBudgetTokens?: number;
|
|
45
48
|
/**
|
|
46
49
|
* Extended thinking config. When `enabled: true`, the agent runs with
|
|
47
50
|
* Anthropic's native extended thinking; responses include `thinking` blocks
|
|
@@ -51,6 +54,14 @@ export interface AgentConfig {
|
|
|
51
54
|
thinking?: {
|
|
52
55
|
enabled: boolean;
|
|
53
56
|
budgetTokens?: number;
|
|
57
|
+
/** 'enabled' (explicit budget, default) or 'adaptive' (model-managed) */
|
|
58
|
+
type?: 'enabled' | 'adaptive';
|
|
59
|
+
/**
|
|
60
|
+
* How thinking content is returned: 'summarized' (readable summary) or
|
|
61
|
+
* 'omitted' (empty thinking field, signature only). Fable 5 / Opus 4.7+
|
|
62
|
+
* default to 'omitted' — set 'summarized' to receive thinking text.
|
|
63
|
+
*/
|
|
64
|
+
display?: 'summarized' | 'omitted';
|
|
54
65
|
};
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -127,4 +138,14 @@ export interface InferenceRequest {
|
|
|
127
138
|
reason: string;
|
|
128
139
|
source: string;
|
|
129
140
|
timestamp: number;
|
|
141
|
+
/**
|
|
142
|
+
* The MCPL channel whose message triggered this inference, if any (composite
|
|
143
|
+
* id, e.g. `discord:guild:channel` / `discord:dm:id`). The framework routes
|
|
144
|
+
* the turn's auto-published plain-text speech here so a single TRUNK agent
|
|
145
|
+
* replies in the channel it is answering — not the process-global most-recent-
|
|
146
|
+
* inbound locus, which a concurrent message elsewhere can hijack (item-3
|
|
147
|
+
* redux). Undefined for non-channel wakes (heartbeat, timers, module events),
|
|
148
|
+
* which correctly fall back to the global default channel.
|
|
149
|
+
*/
|
|
150
|
+
channelId?: string;
|
|
130
151
|
}
|
package/src/types/framework.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Module, EventResponse } from './module.js';
|
|
|
4
4
|
import type { AgentConfig, InferenceRequest } from './agent.js';
|
|
5
5
|
import type { ProcessEvent } from './events.js';
|
|
6
6
|
import type { McplServerConfig, InferenceRoutingPolicy } from '../mcpl/types.js';
|
|
7
|
+
import type { ConversationRouterConfig } from '../mcpl/conversation-router.js';
|
|
7
8
|
import type { GateOptions } from '../gate/types.js';
|
|
8
9
|
|
|
9
10
|
// Re-export trace types
|
|
@@ -66,6 +67,29 @@ export interface FrameworkConfig {
|
|
|
66
67
|
|
|
67
68
|
/** EventGate config. If omitted, all events trigger inference (unchanged default). */
|
|
68
69
|
gate?: GateOptions;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Liveness watchdog: fail hard (and let the supervisor restart) if the main
|
|
73
|
+
* thread wedges — a synchronous infinite loop or microtask flood that would
|
|
74
|
+
* otherwise leave the agent silently deaf. Off unless `enabled` is set.
|
|
75
|
+
*/
|
|
76
|
+
watchdog?: {
|
|
77
|
+
enabled?: boolean;
|
|
78
|
+
/** Kill if no heartbeat for this long (ms). Default 30000. */
|
|
79
|
+
thresholdMs?: number;
|
|
80
|
+
/** 'abort' → SIGABRT (core dump for debugging); 'exit' → SIGKILL. Default 'abort'. */
|
|
81
|
+
action?: 'abort' | 'exit';
|
|
82
|
+
/** Optional path for a JSON wedge report written before the kill. */
|
|
83
|
+
reportPath?: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Per-channel conversation routing. When set, incoming MCPL channel
|
|
88
|
+
* messages route to per-channel fork agents spawned from the template
|
|
89
|
+
* agent instead of the primary conversation. If omitted, behavior is
|
|
90
|
+
* unchanged (all messages go to the primary agent).
|
|
91
|
+
*/
|
|
92
|
+
conversations?: ConversationRouterConfig;
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
/**
|
package/src/types/module.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ContextInjection,
|
|
7
7
|
} from '@animalabs/context-manager';
|
|
8
8
|
import type { ProcessEvent, ToolDefinition, ToolCall, ToolResult } from './events.js';
|
|
9
|
+
import type { TraceEventListener } from './trace.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Filter criteria for querying messages.
|
|
@@ -184,6 +185,34 @@ export interface ModuleContext {
|
|
|
184
185
|
* Unregister this module as a speech handler.
|
|
185
186
|
*/
|
|
186
187
|
unregisterSpeechHandler(): void;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Execute a tool call autonomously, without agent mediation.
|
|
191
|
+
*
|
|
192
|
+
* Routes through the framework's normal tool dispatch — MCPL-prefixed names
|
|
193
|
+
* (e.g. `mcpl--discord--unsubscribe_channel`) hit the MCPL subsystem; module
|
|
194
|
+
* names hit the owning module. Unlike pushing a `tool-call` event, this does
|
|
195
|
+
* NOT record tool_use/tool_result into any agent's context — it's a pure
|
|
196
|
+
* side-effecting call whose result is returned to the caller. Used by modules
|
|
197
|
+
* that need to act on their own (e.g. subscription GC unsubscribing a noisy
|
|
198
|
+
* channel) rather than asking the agent to call the tool.
|
|
199
|
+
*/
|
|
200
|
+
callTool(call: ToolCall): Promise<ToolResult>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Subscribe to the framework's trace event stream for observability.
|
|
204
|
+
*
|
|
205
|
+
* Modules use this to react to agent lifecycle without polling — e.g. an
|
|
206
|
+
* adapter that wants to show a "typing" indicator can bracket it between
|
|
207
|
+
* `inference:started` and the terminal events (`inference:turn_ended`,
|
|
208
|
+
* `inference:aborted`, `inference:failed`, `inference:exhausted`), all of
|
|
209
|
+
* which carry `agentName`. The terminal events fire once per turn, after any
|
|
210
|
+
* tool-call cycles, so a single start/stop bracket spans the whole turn.
|
|
211
|
+
*
|
|
212
|
+
* Returns an unsubscribe function. Call it in the module's `stop()` to avoid
|
|
213
|
+
* leaking listeners across teardown/recreate (e.g. session switch).
|
|
214
|
+
*/
|
|
215
|
+
onTrace(listener: TraceEventListener): () => void;
|
|
187
216
|
}
|
|
188
217
|
|
|
189
218
|
/**
|
package/src/types/trace.ts
CHANGED
|
@@ -234,6 +234,42 @@ export type TraceEvent =
|
|
|
234
234
|
type: 'mcpl:server-stderr';
|
|
235
235
|
serverId: string;
|
|
236
236
|
line: string;
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Per-channel conversation routing lifecycle
|
|
240
|
+
| (TraceEventBase & {
|
|
241
|
+
type: 'mcpl:conversation-spawned';
|
|
242
|
+
channelId: string;
|
|
243
|
+
agentName: string;
|
|
244
|
+
generation: number;
|
|
245
|
+
template: string;
|
|
246
|
+
})
|
|
247
|
+
| (TraceEventBase & {
|
|
248
|
+
type: 'mcpl:conversation-spawn-failed';
|
|
249
|
+
channelId: string;
|
|
250
|
+
agentName: string;
|
|
251
|
+
error: string;
|
|
252
|
+
})
|
|
253
|
+
| (TraceEventBase & {
|
|
254
|
+
type: 'mcpl:conversation-unrouted';
|
|
255
|
+
channelId: string;
|
|
256
|
+
messageId: string;
|
|
257
|
+
})
|
|
258
|
+
| (TraceEventBase & {
|
|
259
|
+
type: 'mcpl:conversation-binding-orphaned';
|
|
260
|
+
channelId: string;
|
|
261
|
+
agentName: string;
|
|
262
|
+
})
|
|
263
|
+
| (TraceEventBase & {
|
|
264
|
+
type: 'mcpl:conversation-closed';
|
|
265
|
+
channelId: string;
|
|
266
|
+
agentName: string;
|
|
267
|
+
reason: 'idle-ttl';
|
|
268
|
+
})
|
|
269
|
+
| (TraceEventBase & {
|
|
270
|
+
type: 'mcpl:conversation-disposed';
|
|
271
|
+
agentName: string;
|
|
272
|
+
channelId?: string;
|
|
237
273
|
});
|
|
238
274
|
|
|
239
275
|
/**
|