@animalabs/agent-framework 0.5.1 → 0.5.4
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 +9 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +9 -0
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +73 -11
- package/dist/src/framework.js.map +1 -1
- package/dist/src/modules/health/index.d.ts +59 -0
- package/dist/src/modules/health/index.d.ts.map +1 -0
- package/dist/src/modules/health/index.js +213 -0
- package/dist/src/modules/health/index.js.map +1 -0
- package/dist/src/modules/index.d.ts +2 -0
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -0
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/types/agent.d.ts +10 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/agent.ts +9 -0
- package/src/framework.ts +78 -11
- package/src/modules/health/index.ts +275 -0
- package/src/modules/index.ts +3 -0
- package/src/types/agent.ts +11 -0
package/src/framework.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { JsStore } from '@animalabs/chronicle';
|
|
3
3
|
import type { Membrane, ContentBlock, NormalizedRequest, YieldingStream, ToolResult as MembraneToolResult } from '@animalabs/membrane';
|
|
4
|
+
import { MembraneError } from '@animalabs/membrane';
|
|
4
5
|
import { ContextManager, PassthroughStrategy } from '@animalabs/context-manager';
|
|
5
6
|
import type {
|
|
6
7
|
MessageId,
|
|
@@ -108,13 +109,28 @@ class DefaultInferencePolicy implements InferencePolicy {
|
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
111
|
* Default error policy - retry with exponential backoff.
|
|
112
|
+
*
|
|
113
|
+
* Respects MembraneError.retryable: non-retryable errors (400 invalid_request,
|
|
114
|
+
* 401 auth, context_length, safety) are terminal on attempt 0. Retrying them
|
|
115
|
+
* burns API quota without any chance of success — the payload doesn't change
|
|
116
|
+
* between attempts. Production traces showed clerk + reviewer wasting 4
|
|
117
|
+
* inferences on each 400 due to blind retries.
|
|
118
|
+
*
|
|
119
|
+
* For retryable errors, honors retryAfterMs when present (rate limits),
|
|
120
|
+
* otherwise falls back to exponential backoff capped at maxRetries.
|
|
111
121
|
*/
|
|
112
122
|
class DefaultErrorPolicy implements ErrorPolicy {
|
|
113
123
|
maxRetries = 3;
|
|
114
124
|
|
|
115
125
|
onInferenceError(error: Error, _agentName: string, attempt: number): ErrorAction {
|
|
126
|
+
if (error instanceof MembraneError && !error.retryable) {
|
|
127
|
+
return { retry: false };
|
|
128
|
+
}
|
|
116
129
|
if (attempt < this.maxRetries) {
|
|
117
|
-
|
|
130
|
+
const delayMs = error instanceof MembraneError && error.retryAfterMs !== undefined
|
|
131
|
+
? error.retryAfterMs
|
|
132
|
+
: Math.pow(2, attempt) * 1000;
|
|
133
|
+
return { retry: true, delayMs };
|
|
118
134
|
}
|
|
119
135
|
return { retry: false };
|
|
120
136
|
}
|
|
@@ -680,13 +696,22 @@ export class AgentFramework {
|
|
|
680
696
|
let toolCallsCount = 0;
|
|
681
697
|
let settled = false;
|
|
682
698
|
let inferenceStarted = false;
|
|
699
|
+
let lastActivity = Date.now();
|
|
683
700
|
|
|
684
701
|
const STARTUP_TIMEOUT_MS = 30_000;
|
|
702
|
+
// After inference has started, give it 15 minutes of activity-bounded
|
|
703
|
+
// life. Each addressed trace event refreshes the deadline; only
|
|
704
|
+
// sustained silence trips it. Subagents that legitimately stream for
|
|
705
|
+
// 10+ minutes (e.g. long fork investigations) get the extra slack but
|
|
706
|
+
// stalled streams that emit `inference:completed` and then go quiet —
|
|
707
|
+
// the exact zombie shape we saw in production — are caught here.
|
|
708
|
+
const COMPLETION_IDLE_TIMEOUT_MS = 15 * 60_000;
|
|
685
709
|
|
|
686
710
|
const cleanup = () => {
|
|
687
711
|
if (settled) return;
|
|
688
712
|
settled = true;
|
|
689
713
|
clearTimeout(startupWatchdog);
|
|
714
|
+
clearInterval(completionWatchdog);
|
|
690
715
|
this.offTrace(traceListener);
|
|
691
716
|
this.agents.delete(agent.name);
|
|
692
717
|
};
|
|
@@ -705,12 +730,34 @@ export class AgentFramework {
|
|
|
705
730
|
}
|
|
706
731
|
}, STARTUP_TIMEOUT_MS);
|
|
707
732
|
|
|
733
|
+
// Completion watchdog: once inference has started, reject if no trace
|
|
734
|
+
// activity for COMPLETION_IDLE_TIMEOUT_MS. Polls every 30s. Without
|
|
735
|
+
// this, an ephemeral that loses its terminal event (e.g. due to a
|
|
736
|
+
// future ordering regression or an upstream stream drop) hangs forever
|
|
737
|
+
// and keeps its concurrency slot pinned.
|
|
738
|
+
const completionWatchdog = setInterval(() => {
|
|
739
|
+
if (settled || !inferenceStarted) return;
|
|
740
|
+
const idle = Date.now() - lastActivity;
|
|
741
|
+
if (idle > COMPLETION_IDLE_TIMEOUT_MS) {
|
|
742
|
+
cleanup();
|
|
743
|
+
reject(new Error(
|
|
744
|
+
`Ephemeral agent "${agent.name}" stalled: no trace activity for ` +
|
|
745
|
+
`${Math.round(idle / 1000)}s after inference started (threshold ` +
|
|
746
|
+
`${Math.round(COMPLETION_IDLE_TIMEOUT_MS / 1000)}s). Stream likely ` +
|
|
747
|
+
`dropped or terminal event was lost.`
|
|
748
|
+
));
|
|
749
|
+
}
|
|
750
|
+
}, 30_000);
|
|
751
|
+
|
|
708
752
|
const traceListener = (event: TraceEvent) => {
|
|
709
753
|
if (settled) return;
|
|
710
754
|
// Only track events for our ephemeral agent
|
|
711
755
|
const agentName = 'agentName' in event ? (event as { agentName: string }).agentName : null;
|
|
712
756
|
if (agentName !== agent.name) return;
|
|
713
757
|
|
|
758
|
+
// Any addressed event counts as liveness for the completion watchdog.
|
|
759
|
+
lastActivity = Date.now();
|
|
760
|
+
|
|
714
761
|
switch (event.type) {
|
|
715
762
|
case 'inference:started':
|
|
716
763
|
inferenceStarted = true;
|
|
@@ -732,13 +779,23 @@ export class AgentFramework {
|
|
|
732
779
|
speech = '';
|
|
733
780
|
break;
|
|
734
781
|
case 'inference:completed': {
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
782
|
+
// `inference:completed` is terminal — driveStream only emits it
|
|
783
|
+
// from `case 'complete'`, which fires after the full tool
|
|
784
|
+
// round-trip has finished and the model has stopped. Intermediate
|
|
785
|
+
// tool-cycle transitions use `inference:stream_resumed`, not
|
|
786
|
+
// `inference:completed`. So we resolve unconditionally here.
|
|
787
|
+
//
|
|
788
|
+
// Previously this branch gated on `agent.state.status === 'idle'`,
|
|
789
|
+
// which was correct in intent but order-fragile: the framework
|
|
790
|
+
// emitted `inference:completed` BEFORE calling `agent.reset()`,
|
|
791
|
+
// so synchronous listeners observed status === 'streaming' at
|
|
792
|
+
// the trace boundary. The gate failed silently and the promise
|
|
793
|
+
// hung forever — every production zombie subagent originated
|
|
794
|
+
// here. The reset+emit order has been fixed in driveStream's
|
|
795
|
+
// `case 'complete'` handler; the gate is dropped here as
|
|
796
|
+
// belt-and-suspenders.
|
|
797
|
+
cleanup();
|
|
798
|
+
resolve({ speech, toolCallsCount });
|
|
742
799
|
break;
|
|
743
800
|
}
|
|
744
801
|
case 'inference:turn_ended': {
|
|
@@ -1920,6 +1977,19 @@ export class AgentFramework {
|
|
|
1920
1977
|
cacheRead: du.cacheReadTokens,
|
|
1921
1978
|
}
|
|
1922
1979
|
: undefined;
|
|
1980
|
+
|
|
1981
|
+
// Reset agent state BEFORE emitting inference:completed so that
|
|
1982
|
+
// synchronous listeners (e.g. runEphemeralToCompletion) observe
|
|
1983
|
+
// status === 'idle' the moment the event fires. Previously
|
|
1984
|
+
// agent.reset() ran after `await dispatchSpeech`, which meant the
|
|
1985
|
+
// ephemeral-completion listener saw status === 'streaming' at the
|
|
1986
|
+
// trace boundary, failed its idle gate, and the promise hung
|
|
1987
|
+
// forever — every "zombie subagent" in production tracked back
|
|
1988
|
+
// here. Speech dispatch happens after but doesn't depend on the
|
|
1989
|
+
// status field.
|
|
1990
|
+
agent.reset();
|
|
1991
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
1992
|
+
|
|
1923
1993
|
this.emitTrace({
|
|
1924
1994
|
type: 'inference:completed',
|
|
1925
1995
|
agentName: agent.name,
|
|
@@ -1968,9 +2038,6 @@ export class AgentFramework {
|
|
|
1968
2038
|
);
|
|
1969
2039
|
}
|
|
1970
2040
|
|
|
1971
|
-
// Done — reset to idle
|
|
1972
|
-
agent.reset();
|
|
1973
|
-
this.eventGate?.onInferenceEnded(agent.name);
|
|
1974
2041
|
break;
|
|
1975
2042
|
}
|
|
1976
2043
|
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthModule — minimal framework self-introspection.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a single tool, `health--snapshot`, that returns a structured
|
|
5
|
+
* snapshot of recent framework activity. Useful when an agent gets a
|
|
6
|
+
* concurrency timeout, a 400 from the API, or wants to verify it's not
|
|
7
|
+
* silently failing.
|
|
8
|
+
*
|
|
9
|
+
* Sources:
|
|
10
|
+
* - `framework/inference-log` (queryInferenceLogs) — recent inference
|
|
11
|
+
* success/error counts, last N errors, per-agent token totals.
|
|
12
|
+
* - `modules/subagent/state` — best-effort. The slot is owned by
|
|
13
|
+
* `connectome-host`'s SubagentModule, not by this module. We read it
|
|
14
|
+
* directly from the chronicle store so the tool works even when the
|
|
15
|
+
* HealthModule has no awareness of SubagentModule's internals.
|
|
16
|
+
* - Registered module names.
|
|
17
|
+
*
|
|
18
|
+
* Kept deliberately read-only: no side effects, no kill switches.
|
|
19
|
+
* Reaping/cancellation lives in the SubagentModule.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { JsStore } from '@animalabs/chronicle';
|
|
23
|
+
import type { Module, ModuleContext } from '../../types/module.js';
|
|
24
|
+
import type { ToolDefinition, ToolCall, ToolResult, ProcessEvent } from '../../types/events.js';
|
|
25
|
+
import type { EventResponse, ProcessState } from '../../types/module.js';
|
|
26
|
+
import type { AgentFramework } from '../../framework.js';
|
|
27
|
+
|
|
28
|
+
export interface HealthModuleConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Default number of recent inferences to summarize when the tool is
|
|
31
|
+
* called without an explicit limit. Default: 20.
|
|
32
|
+
*/
|
|
33
|
+
defaultLookback?: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* State slot to read for the subagent registry. Allows hosts that name
|
|
37
|
+
* their subagent module differently to point this at the right slot.
|
|
38
|
+
* Default: `modules/subagent/state`.
|
|
39
|
+
*/
|
|
40
|
+
subagentStateId?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SnapshotInput {
|
|
44
|
+
/** How many recent inferences to summarize. Default config.defaultLookback. */
|
|
45
|
+
lookback?: number;
|
|
46
|
+
/** Include per-agent token totals across all-time. Default true. */
|
|
47
|
+
includeTokens?: boolean;
|
|
48
|
+
/** Include subagent registry summary. Default true. */
|
|
49
|
+
includeSubagents?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface PersistedSubagentRecord {
|
|
53
|
+
name: string;
|
|
54
|
+
type?: string;
|
|
55
|
+
status?: string;
|
|
56
|
+
startedAt?: number;
|
|
57
|
+
lastActivityAt?: number;
|
|
58
|
+
completedAt?: number;
|
|
59
|
+
toolCallsCount?: number;
|
|
60
|
+
statusMessage?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class HealthModule implements Module {
|
|
64
|
+
readonly name = 'health';
|
|
65
|
+
|
|
66
|
+
private ctx: ModuleContext | null = null;
|
|
67
|
+
private framework: AgentFramework | null = null;
|
|
68
|
+
private store: JsStore | null = null;
|
|
69
|
+
private config: Required<HealthModuleConfig>;
|
|
70
|
+
|
|
71
|
+
constructor(config: HealthModuleConfig = {}) {
|
|
72
|
+
this.config = {
|
|
73
|
+
defaultLookback: config.defaultLookback ?? 20,
|
|
74
|
+
subagentStateId: config.subagentStateId ?? 'modules/subagent/state',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wire framework + store. Host calls this after AgentFramework.create()
|
|
80
|
+
* so the module can query inference logs and chronicle state.
|
|
81
|
+
*/
|
|
82
|
+
bind(framework: AgentFramework, store: JsStore): void {
|
|
83
|
+
this.framework = framework;
|
|
84
|
+
this.store = store;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async start(ctx: ModuleContext): Promise<void> {
|
|
88
|
+
this.ctx = ctx;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async stop(): Promise<void> {
|
|
92
|
+
this.ctx = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getTools(): ToolDefinition[] {
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
name: 'snapshot',
|
|
99
|
+
description:
|
|
100
|
+
'Return a structured snapshot of framework health: recent inference success/error counts, last errors, per-agent token totals, active subagent registry, and registered modules. Read-only. Useful when diagnosing why a spawn timed out, why an inference failed, or whether the framework is silently degraded.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object' as const,
|
|
103
|
+
properties: {
|
|
104
|
+
lookback: {
|
|
105
|
+
type: 'number',
|
|
106
|
+
description: `How many recent inferences to summarize (default ${this.config.defaultLookback}).`,
|
|
107
|
+
},
|
|
108
|
+
includeTokens: {
|
|
109
|
+
type: 'boolean',
|
|
110
|
+
description: 'Include per-agent token totals (default true).',
|
|
111
|
+
},
|
|
112
|
+
includeSubagents: {
|
|
113
|
+
type: 'boolean',
|
|
114
|
+
description: 'Include subagent registry summary (default true).',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
123
|
+
if (call.name !== 'snapshot') {
|
|
124
|
+
return { success: false, isError: true, error: `Unknown tool: ${call.name}` };
|
|
125
|
+
}
|
|
126
|
+
const input = (call.input ?? {}) as SnapshotInput;
|
|
127
|
+
const lookback = input.lookback ?? this.config.defaultLookback;
|
|
128
|
+
const includeTokens = input.includeTokens ?? true;
|
|
129
|
+
const includeSubagents = input.includeSubagents ?? true;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const snapshot = this.buildSnapshot(lookback, includeTokens, includeSubagents);
|
|
133
|
+
return { success: true, data: snapshot };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
isError: true,
|
|
138
|
+
error: error instanceof Error ? error.message : String(error),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async onProcess(_event: ProcessEvent, _state: ProcessState): Promise<EventResponse> {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private buildSnapshot(
|
|
148
|
+
lookback: number,
|
|
149
|
+
includeTokens: boolean,
|
|
150
|
+
includeSubagents: boolean,
|
|
151
|
+
): Record<string, unknown> {
|
|
152
|
+
if (!this.framework || !this.store) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'HealthModule not bound — host must call bind(framework, store) before tool dispatch.',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Inference log summary ───────────────────────────────────────────
|
|
159
|
+
// queryInferenceLogs returns InferenceLogEntryWithId. Prefer the
|
|
160
|
+
// `summary` field (no blob deref) but fall back to `entry` for older
|
|
161
|
+
// records or if the summarizer wasn't populated.
|
|
162
|
+
const recent = this.framework.queryInferenceLogs({ limit: lookback });
|
|
163
|
+
const recentEntries = recent.entries ?? [];
|
|
164
|
+
|
|
165
|
+
const projectSummary = (e: typeof recentEntries[number]) => {
|
|
166
|
+
const s = e.summary ?? e.entry;
|
|
167
|
+
return {
|
|
168
|
+
timestamp: s.timestamp,
|
|
169
|
+
agentName: s.agentName,
|
|
170
|
+
success: s.success,
|
|
171
|
+
error: s.error,
|
|
172
|
+
requestId: s.requestId,
|
|
173
|
+
tokenUsage: s.tokenUsage,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const recentErrors = recentEntries
|
|
178
|
+
.map(projectSummary)
|
|
179
|
+
.filter((s) => s.success === false)
|
|
180
|
+
.slice(0, 10)
|
|
181
|
+
.map((s) => ({
|
|
182
|
+
timestamp: s.timestamp,
|
|
183
|
+
agentName: s.agentName,
|
|
184
|
+
error: typeof s.error === 'string' ? s.error.slice(0, 400) : s.error,
|
|
185
|
+
requestId: s.requestId,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
let tokenTotals: Record<string, { input: number; output: number; cacheRead: number; inferences: number }> | undefined;
|
|
189
|
+
if (includeTokens) {
|
|
190
|
+
// Pull a larger window for totals — defaults to 1000 entries which is
|
|
191
|
+
// enough to characterize ongoing usage without dragging the entire log.
|
|
192
|
+
const wide = this.framework.queryInferenceLogs({ limit: 1000 });
|
|
193
|
+
tokenTotals = {};
|
|
194
|
+
for (const raw of wide.entries ?? []) {
|
|
195
|
+
const s = projectSummary(raw);
|
|
196
|
+
const agent = s.agentName ?? 'unknown';
|
|
197
|
+
const t = tokenTotals[agent] ?? { input: 0, output: 0, cacheRead: 0, inferences: 0 };
|
|
198
|
+
t.inferences++;
|
|
199
|
+
const u = s.tokenUsage;
|
|
200
|
+
if (u) {
|
|
201
|
+
t.input += u.input ?? 0;
|
|
202
|
+
t.output += u.output ?? 0;
|
|
203
|
+
t.cacheRead += u.cacheRead ?? 0;
|
|
204
|
+
}
|
|
205
|
+
tokenTotals[agent] = t;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Subagent registry summary (best-effort) ────────────────────────
|
|
210
|
+
let subagents: Array<Record<string, unknown>> | string | undefined;
|
|
211
|
+
if (includeSubagents) {
|
|
212
|
+
try {
|
|
213
|
+
const raw = this.store.getStateJson(this.config.subagentStateId);
|
|
214
|
+
if (raw) {
|
|
215
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
216
|
+
const agents = (parsed as { agents?: Record<string, PersistedSubagentRecord> })?.agents
|
|
217
|
+
?? {};
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
subagents = Object.entries(agents).map(([key, sa]) => {
|
|
220
|
+
const startedAt = sa.startedAt ?? 0;
|
|
221
|
+
const lastActivityAt = sa.lastActivityAt ?? startedAt;
|
|
222
|
+
return {
|
|
223
|
+
key,
|
|
224
|
+
name: sa.name,
|
|
225
|
+
type: sa.type,
|
|
226
|
+
status: sa.status,
|
|
227
|
+
startedAt: startedAt > 0 ? new Date(startedAt).toISOString() : null,
|
|
228
|
+
lastActivityAt: lastActivityAt > 0 ? new Date(lastActivityAt).toISOString() : null,
|
|
229
|
+
runtimeSeconds: startedAt > 0 ? Math.floor((now - startedAt) / 1000) : null,
|
|
230
|
+
silentSeconds: lastActivityAt > 0 ? Math.floor((now - lastActivityAt) / 1000) : null,
|
|
231
|
+
toolCallsCount: sa.toolCallsCount,
|
|
232
|
+
statusMessage: sa.statusMessage,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
subagents = 'no subagent state registered';
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
subagents = `error reading subagent state: ${error instanceof Error ? error.message : String(error)}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Module list ────────────────────────────────────────────────────
|
|
244
|
+
const modules = this.ctx ? this.listModulesViaContext() : [];
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
timestamp: new Date().toISOString(),
|
|
248
|
+
window: {
|
|
249
|
+
lookback,
|
|
250
|
+
inferencesInWindow: recentEntries.length,
|
|
251
|
+
},
|
|
252
|
+
inferences: {
|
|
253
|
+
successCount: recentEntries.filter((e) => projectSummary(e).success).length,
|
|
254
|
+
errorCount: recentEntries.filter((e) => projectSummary(e).success === false).length,
|
|
255
|
+
recentErrors,
|
|
256
|
+
},
|
|
257
|
+
tokenTotalsByAgent: tokenTotals,
|
|
258
|
+
subagents,
|
|
259
|
+
modules,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private listModulesViaContext(): string[] {
|
|
264
|
+
// ModuleContext.getActiveTools returns flat tool names; derive module set
|
|
265
|
+
// from the prefix before '--'. This avoids needing a dedicated module-list
|
|
266
|
+
// accessor on ModuleContext (kept narrow on purpose).
|
|
267
|
+
const tools = this.ctx?.getActiveTools() ?? [];
|
|
268
|
+
const set = new Set<string>();
|
|
269
|
+
for (const t of tools) {
|
|
270
|
+
const i = t.name.indexOf('--');
|
|
271
|
+
if (i > 0) set.add(t.name.slice(0, i));
|
|
272
|
+
}
|
|
273
|
+
return [...set].sort();
|
|
274
|
+
}
|
|
275
|
+
}
|
package/src/modules/index.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
export { ApiModule } from './api/index.js';
|
|
6
6
|
export type { ApiEvent } from './api/index.js';
|
|
7
7
|
|
|
8
|
+
export { HealthModule } from './health/index.js';
|
|
9
|
+
export type { HealthModuleConfig } from './health/index.js';
|
|
10
|
+
|
|
8
11
|
export { WorkspaceModule } from './workspace/index.js';
|
|
9
12
|
export type {
|
|
10
13
|
WorkspaceConfig,
|
package/src/types/agent.ts
CHANGED
|
@@ -41,6 +41,17 @@ export interface AgentConfig {
|
|
|
41
41
|
/** Max input tokens before framework breaks a yielding stream and
|
|
42
42
|
* restarts with recompiled (compressed) context. Default: 150000. */
|
|
43
43
|
maxStreamTokens?: number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extended thinking config. When `enabled: true`, the agent runs with
|
|
47
|
+
* Anthropic's native extended thinking; responses include `thinking` blocks
|
|
48
|
+
* with cryptographic signatures, and the API enforces `temperature: 1`
|
|
49
|
+
* (Membrane handles this). Omit or set `enabled: false` to disable.
|
|
50
|
+
*/
|
|
51
|
+
thinking?: {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
budgetTokens?: number;
|
|
54
|
+
};
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
/**
|