@animalabs/agent-framework 0.5.7 → 0.6.2
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 +4 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +15 -0
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +243 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1378 -114
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +64 -2
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +212 -12
- 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 +18 -3
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +60 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +114 -9
- 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/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +5 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +64 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +247 -210
- 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 +59 -3
- 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 +17 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +101 -6
- package/dist/src/module-registry.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/types/agent.d.ts +47 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +58 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +42 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-cache-ttl.test.d.ts +2 -0
- package/dist/test/agent-cache-ttl.test.d.ts.map +1 -0
- package/dist/test/agent-cache-ttl.test.js +25 -0
- package/dist/test/agent-cache-ttl.test.js.map +1 -0
- package/dist/test/channel-registry-routing.test.js +108 -1
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- 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-fork-injection-scoping.test.d.ts +2 -0
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +1 -0
- package/dist/test/conversation-fork-injection-scoping.test.js +184 -0
- package/dist/test/conversation-fork-injection-scoping.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/gather-context-timeout.test.d.ts +2 -0
- package/dist/test/gather-context-timeout.test.d.ts.map +1 -0
- package/dist/test/gather-context-timeout.test.js +120 -0
- package/dist/test/gather-context-timeout.test.js.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts +2 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.js +181 -0
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -0
- package/dist/test/inference-failure-observability.test.js +4 -0
- package/dist/test/inference-failure-observability.test.js.map +1 -1
- 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-reconnect-events.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +119 -0
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js +181 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -0
- package/dist/test/mcpl-request-timeout.test.d.ts +2 -0
- package/dist/test/mcpl-request-timeout.test.d.ts.map +1 -0
- package/dist/test/mcpl-request-timeout.test.js +123 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +7 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.js +142 -0
- package/dist/test/mcpl-runtime-lifecycle.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/refusal-rewind.test.d.ts +2 -0
- package/dist/test/refusal-rewind.test.d.ts.map +1 -0
- package/dist/test/refusal-rewind.test.js +108 -0
- package/dist/test/refusal-rewind.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.d.ts +22 -0
- package/dist/test/state-scaling-e2e.test.d.ts.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +200 -0
- package/dist/test/state-scaling-e2e.test.js.map +1 -0
- package/dist/test/state-scaling.test.d.ts +12 -0
- package/dist/test/state-scaling.test.d.ts.map +1 -0
- package/dist/test/state-scaling.test.js +243 -0
- package/dist/test/state-scaling.test.js.map +1 -0
- package/dist/test/tool-result-content.test.d.ts +2 -0
- package/dist/test/tool-result-content.test.d.ts.map +1 -0
- package/dist/test/tool-result-content.test.js +37 -0
- package/dist/test/tool-result-content.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 +4 -3
- package/src/agent.ts +16 -0
- package/src/framework.ts +1576 -121
- package/src/gate/event-gate.ts +209 -12
- package/src/gate/gate-script-worker.ts +73 -0
- package/src/gate/gate-script.ts +180 -0
- package/src/gate/types.ts +19 -3
- package/src/index.ts +10 -0
- package/src/mcpl/channel-registry.ts +143 -8
- package/src/mcpl/checkpoint-manager.ts +16 -0
- package/src/mcpl/push-handler.ts +5 -0
- package/src/mcpl/server-connection.ts +262 -227
- package/src/mcpl/transport.ts +307 -0
- package/src/mcpl/types.ts +68 -3
- package/src/module-registry.ts +126 -6
- 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/types/agent.ts +48 -0
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +65 -0
- package/src/types/trace.ts +51 -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
|
+
}
|
package/src/types/agent.ts
CHANGED
|
@@ -45,6 +45,22 @@ export interface AgentConfig {
|
|
|
45
45
|
/** Per-agent context compile budget (input tokens). When unset, the
|
|
46
46
|
* ContextManager's built-in default (100k) applies. */
|
|
47
47
|
contextBudgetTokens?: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Prompt-cache TTL forwarded to the provider (Anthropic `cache_control.ttl`).
|
|
51
|
+
* '5m' (provider default) or '1h'. When unset, Membrane's default applies.
|
|
52
|
+
*
|
|
53
|
+
* Why you'd set '1h': for persistent agents whose conversational cadence is
|
|
54
|
+
* slower than 5 minutes, the default TTL expires between turns and the full
|
|
55
|
+
* context is re-WRITTEN to cache on nearly every call. Cache writes carry a
|
|
56
|
+
* premium (1.25x base input for 5m, 2x for 1h) while reads cost 0.1x — so a
|
|
57
|
+
* chatty-but-not-rapid agent pays the write premium over and over. With '1h'
|
|
58
|
+
* the write happens once per idle-hour and subsequent turns hit cache reads;
|
|
59
|
+
* in practice cache writes can dominate spend for slow-cadence agents.
|
|
60
|
+
* Keep '5m' (or unset) for high-frequency loops with sub-5-minute cadence,
|
|
61
|
+
* where the cheaper write premium wins.
|
|
62
|
+
*/
|
|
63
|
+
cacheTtl?: '5m' | '1h';
|
|
48
64
|
/**
|
|
49
65
|
* Extended thinking config. When `enabled: true`, the agent runs with
|
|
50
66
|
* Anthropic's native extended thinking; responses include `thinking` blocks
|
|
@@ -63,6 +79,28 @@ export interface AgentConfig {
|
|
|
63
79
|
*/
|
|
64
80
|
display?: 'summarized' | 'omitted';
|
|
65
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* How to handle a model content-policy refusal (`stop_reason: refusal`).
|
|
84
|
+
* When `autoRewind` is on, a refused turn triggers an automatic rewind: the
|
|
85
|
+
* framework redacts the triggering turn (the tool result or message that
|
|
86
|
+
* tripped the classifier), injects a metadata-only marker in its place (which
|
|
87
|
+
* carries none of the offending content, so it cannot itself re-trip), and
|
|
88
|
+
* re-runs — up to `maxRewinds` times before giving up. This keeps the agent
|
|
89
|
+
* on its own model (no fallback-model substitution) while self-healing around
|
|
90
|
+
* a poison turn. Default: off (a refusal just surfaces a marker + reaction).
|
|
91
|
+
*/
|
|
92
|
+
refusalHandling?: {
|
|
93
|
+
/** Auto-rewind the triggering turn on refusal and retry. Default false. */
|
|
94
|
+
autoRewind?: boolean;
|
|
95
|
+
/** Max consecutive rewinds before giving up a turn. Default 3. */
|
|
96
|
+
maxRewinds?: number;
|
|
97
|
+
/**
|
|
98
|
+
* When the rewound turn is a *human* message (not a machine tool result),
|
|
99
|
+
* announce the withholding on the conversational surface (Discord) rather
|
|
100
|
+
* than dropping it silently. Default true.
|
|
101
|
+
*/
|
|
102
|
+
announceHumanTurns?: boolean;
|
|
103
|
+
};
|
|
66
104
|
}
|
|
67
105
|
|
|
68
106
|
/**
|
|
@@ -138,4 +176,14 @@ export interface InferenceRequest {
|
|
|
138
176
|
reason: string;
|
|
139
177
|
source: string;
|
|
140
178
|
timestamp: number;
|
|
179
|
+
/**
|
|
180
|
+
* The MCPL channel whose message triggered this inference, if any (composite
|
|
181
|
+
* id, e.g. `discord:guild:channel` / `discord:dm:id`). The framework routes
|
|
182
|
+
* the turn's auto-published plain-text speech here so a single TRUNK agent
|
|
183
|
+
* replies in the channel it is answering — not the process-global most-recent-
|
|
184
|
+
* inbound locus, which a concurrent message elsewhere can hijack (item-3
|
|
185
|
+
* redux). Undefined for non-channel wakes (heartbeat, timers, module events),
|
|
186
|
+
* which correctly fall back to the global default channel.
|
|
187
|
+
*/
|
|
188
|
+
channelId?: string;
|
|
141
189
|
}
|
package/src/types/framework.ts
CHANGED
|
@@ -68,6 +68,21 @@ export interface FrameworkConfig {
|
|
|
68
68
|
/** EventGate config. If omitted, all events trigger inference (unchanged default). */
|
|
69
69
|
gate?: GateOptions;
|
|
70
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
|
+
|
|
71
86
|
/**
|
|
72
87
|
* Per-channel conversation routing. When set, incoming MCPL channel
|
|
73
88
|
* messages route to per-channel fork agents spawned from the template
|
package/src/types/module.ts
CHANGED
|
@@ -36,6 +36,15 @@ export interface Module {
|
|
|
36
36
|
/** Unique name, used for tool namespacing and state storage */
|
|
37
37
|
readonly name: string;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Per-module budget (ms) for gatherContext() before the framework skips this
|
|
41
|
+
* module's injection for the turn (fail-open). Defaults to the registry-wide
|
|
42
|
+
* default (15s) when omitted. Declare a larger budget when gatherContext
|
|
43
|
+
* does real work — e.g. sequential LLM calls with provider backoff — so the
|
|
44
|
+
* injection isn't silently dropped every turn.
|
|
45
|
+
*/
|
|
46
|
+
readonly contextTimeoutMs?: number;
|
|
47
|
+
|
|
39
48
|
/**
|
|
40
49
|
* Start the module.
|
|
41
50
|
* Called when the framework starts or when the module is added.
|
|
@@ -101,9 +110,52 @@ export interface ModuleContext {
|
|
|
101
110
|
|
|
102
111
|
/**
|
|
103
112
|
* Set persistent state for this module.
|
|
113
|
+
*
|
|
114
|
+
* This rewrites the module's full state object every call — appropriate for
|
|
115
|
+
* small, bounded state only. For accumulating collections (registries,
|
|
116
|
+
* corpora, histories) use a log state (`registerLogState`/`appendToLog`)
|
|
117
|
+
* instead: a setState of an ever-growing object makes every persisted record
|
|
118
|
+
* proportional to accumulated content, i.e. O(n²) aggregate disk.
|
|
104
119
|
*/
|
|
105
120
|
setState<T>(state: T): void;
|
|
106
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Register an auxiliary append-log state for this module, stored as
|
|
124
|
+
* `modules/<module>/<name>`. Idempotent — safe to call on every start().
|
|
125
|
+
* Items are appended with `appendToLog` (one O(item) record per append) and
|
|
126
|
+
* point-edited with `editLogItem`; Chronicle's append_log snapshot cadence
|
|
127
|
+
* handles consolidation. `name` must not be 'state' (reserved for the main
|
|
128
|
+
* snapshot state) and must not contain '/'.
|
|
129
|
+
*/
|
|
130
|
+
registerLogState(
|
|
131
|
+
name: string,
|
|
132
|
+
opts?: { deltaSnapshotEvery?: number; fullSnapshotEvery?: number }
|
|
133
|
+
): void;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Append one item to a log state registered via registerLogState.
|
|
137
|
+
* Record size is O(item), independent of accumulated log length.
|
|
138
|
+
*/
|
|
139
|
+
appendToLog<T>(name: string, item: T): void;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Overwrite the item at `index` in a log state. Indices are stable —
|
|
143
|
+
* appends assign the next index and nothing is ever removed.
|
|
144
|
+
*/
|
|
145
|
+
editLogItem<T>(name: string, index: number, item: T): void;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read the full contents of a log state (empty array if never written).
|
|
149
|
+
* Intended for one-shot reconstruction at start(); keep hot-path reads
|
|
150
|
+
* against the in-memory copy.
|
|
151
|
+
*/
|
|
152
|
+
getLog<T>(name: string): T[];
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Number of items in a log state (0 if never written).
|
|
156
|
+
*/
|
|
157
|
+
getLogLength(name: string): number;
|
|
158
|
+
|
|
107
159
|
/**
|
|
108
160
|
* Process queue for pushing events from external listeners.
|
|
109
161
|
* @deprecated Use pushEvent() instead — it also emits process:received traces.
|
|
@@ -186,6 +238,19 @@ export interface ModuleContext {
|
|
|
186
238
|
*/
|
|
187
239
|
unregisterSpeechHandler(): void;
|
|
188
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Execute a tool call autonomously, without agent mediation.
|
|
243
|
+
*
|
|
244
|
+
* Routes through the framework's normal tool dispatch — MCPL-prefixed names
|
|
245
|
+
* (e.g. `mcpl--discord--unsubscribe_channel`) hit the MCPL subsystem; module
|
|
246
|
+
* names hit the owning module. Unlike pushing a `tool-call` event, this does
|
|
247
|
+
* NOT record tool_use/tool_result into any agent's context — it's a pure
|
|
248
|
+
* side-effecting call whose result is returned to the caller. Used by modules
|
|
249
|
+
* that need to act on their own (e.g. subscription GC unsubscribing a noisy
|
|
250
|
+
* channel) rather than asking the agent to call the tool.
|
|
251
|
+
*/
|
|
252
|
+
callTool(call: ToolCall): Promise<ToolResult>;
|
|
253
|
+
|
|
189
254
|
/**
|
|
190
255
|
* Subscribe to the framework's trace event stream for observability.
|
|
191
256
|
*
|
package/src/types/trace.ts
CHANGED
|
@@ -60,6 +60,13 @@ export type TraceEvent =
|
|
|
60
60
|
type: 'inference:exhausted';
|
|
61
61
|
agentName: string;
|
|
62
62
|
error: string;
|
|
63
|
+
/** Whether membrane classified the failure as retryable (observability). */
|
|
64
|
+
retryable?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The membrane error type (e.g. 'invalid_request', 'auth', 'context_length').
|
|
67
|
+
* The poison-history breaker fires only on 'invalid_request'.
|
|
68
|
+
*/
|
|
69
|
+
errorType?: string;
|
|
63
70
|
})
|
|
64
71
|
|
|
65
72
|
// Streaming inference lifecycle
|
|
@@ -270,6 +277,50 @@ export type TraceEvent =
|
|
|
270
277
|
type: 'mcpl:conversation-disposed';
|
|
271
278
|
agentName: string;
|
|
272
279
|
channelId?: string;
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// MCPL server connection lifecycle (spawn / handshake / reconnect health).
|
|
283
|
+
// Previously these outcomes were visible only on the host process's own
|
|
284
|
+
// stderr, so a server that lost the boot handshake race just went missing.
|
|
285
|
+
| (TraceEventBase & {
|
|
286
|
+
type: 'mcpl:server-connect-failed';
|
|
287
|
+
serverId: string;
|
|
288
|
+
error: string;
|
|
289
|
+
/** Ordinal of the failed attempt: 0 is the initial connect, N is the Nth background retry. */
|
|
290
|
+
attempt: number;
|
|
291
|
+
/** True when a background reconnect loop will keep retrying. */
|
|
292
|
+
willRetry: boolean;
|
|
293
|
+
})
|
|
294
|
+
| (TraceEventBase & {
|
|
295
|
+
type: 'mcpl:server-reconnected';
|
|
296
|
+
serverId: string;
|
|
297
|
+
/** How many attempts the reconnect loop needed (1 = first retry succeeded). */
|
|
298
|
+
attempts: number;
|
|
299
|
+
})
|
|
300
|
+
| (TraceEventBase & {
|
|
301
|
+
type: 'mcpl:server-closed';
|
|
302
|
+
serverId: string;
|
|
303
|
+
/** Child process exit code, or null when killed by signal / closed explicitly. */
|
|
304
|
+
code: number | null;
|
|
305
|
+
/** Signal that terminated the child, if any. */
|
|
306
|
+
signal: string | null;
|
|
307
|
+
/** True when a background reconnect loop will revive the connection. */
|
|
308
|
+
willReconnect: boolean;
|
|
309
|
+
})
|
|
310
|
+
| (TraceEventBase & {
|
|
311
|
+
type: 'mcpl:server-error';
|
|
312
|
+
serverId: string;
|
|
313
|
+
error: string;
|
|
314
|
+
})
|
|
315
|
+
| (TraceEventBase & {
|
|
316
|
+
type: 'mcpl:orphaned-response';
|
|
317
|
+
serverId: string;
|
|
318
|
+
/** The JSON-RPC id of the late response whose pending request already timed out. */
|
|
319
|
+
responseId: string | number;
|
|
320
|
+
/** True when the dropped response carried host-managed `state`. */
|
|
321
|
+
hadState: boolean;
|
|
322
|
+
/** True when the dropped response carried a server-managed `checkpoint`. */
|
|
323
|
+
hadCheckpoint: boolean;
|
|
273
324
|
});
|
|
274
325
|
|
|
275
326
|
/**
|