@animalabs/agent-framework 0.6.2 → 0.6.3
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/framework.d.ts +58 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +247 -19
- package/dist/src/framework.js.map +1 -1
- package/dist/src/types/framework.d.ts +40 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +9 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/inference-failure-observability.test.js +5 -0
- package/dist/test/inference-failure-observability.test.js.map +1 -1
- package/dist/test/maintenance-timer.test.d.ts +2 -0
- package/dist/test/maintenance-timer.test.d.ts.map +1 -0
- package/dist/test/maintenance-timer.test.js +114 -0
- package/dist/test/maintenance-timer.test.js.map +1 -0
- package/dist/test/ops-alert.test.d.ts +2 -0
- package/dist/test/ops-alert.test.d.ts.map +1 -0
- package/dist/test/ops-alert.test.js +152 -0
- package/dist/test/ops-alert.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/framework.ts +289 -19
- package/src/types/framework.ts +44 -0
- package/src/types/trace.ts +16 -0
package/package.json
CHANGED
package/src/framework.ts
CHANGED
|
@@ -40,6 +40,9 @@ import type {
|
|
|
40
40
|
AgentState,
|
|
41
41
|
Module,
|
|
42
42
|
SpeechContext,
|
|
43
|
+
ContextMaintenanceRun,
|
|
44
|
+
ContextMaintenanceAgentRun,
|
|
45
|
+
ContextMaintenanceSnapshot,
|
|
43
46
|
} from './types/index.js';
|
|
44
47
|
import { ProcessQueueImpl } from './queue.js';
|
|
45
48
|
import { Agent } from './agent.js';
|
|
@@ -144,6 +147,10 @@ class DefaultErrorPolicy implements ErrorPolicy {
|
|
|
144
147
|
|
|
145
148
|
/** Default sync interval in milliseconds */
|
|
146
149
|
const DEFAULT_SYNC_INTERVAL_MS = 1000;
|
|
150
|
+
/** Poll pending context-strategy maintenance independently of user activity. */
|
|
151
|
+
const DEFAULT_MAINTENANCE_INTERVAL_MS = 5000;
|
|
152
|
+
/** Bound one pass so a large backlog yields to inference and other agents. */
|
|
153
|
+
const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
147
154
|
|
|
148
155
|
/**
|
|
149
156
|
* Extract fields the EventGate cares about from a ProcessEvent. The set of
|
|
@@ -253,6 +260,12 @@ export class AgentFramework {
|
|
|
253
260
|
private traceListeners: TraceEventListener[] = [];
|
|
254
261
|
private syncIntervalMs: number;
|
|
255
262
|
private syncTimer: ReturnType<typeof setInterval> | null = null;
|
|
263
|
+
private maintenanceIntervalMs: number;
|
|
264
|
+
private maintenanceTimer: ReturnType<typeof setInterval> | null = null;
|
|
265
|
+
private maintenancePass: Promise<void> | null = null;
|
|
266
|
+
private maintenanceRunId = 0;
|
|
267
|
+
private currentMaintenanceRun: ContextMaintenanceRun | null = null;
|
|
268
|
+
private maintenanceHistory: ContextMaintenanceRun[] = [];
|
|
256
269
|
/** Last time we console-warned about stale (busy-requeued) inference requests, per agent. */
|
|
257
270
|
private staleWarnAt = new Map<string, number>();
|
|
258
271
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -267,6 +280,19 @@ export class AgentFramework {
|
|
|
267
280
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
268
281
|
* escalated loudly to stderr. */
|
|
269
282
|
private readonly inferenceFailureEscalationThreshold = 3;
|
|
283
|
+
/** Per-(agent,kind) timestamp of the last ops webhook post. Throttles
|
|
284
|
+
* opsAlert() so a persistent failure re-posts once per cooldown window
|
|
285
|
+
* instead of on every occurrence. */
|
|
286
|
+
private opsAlertLastSent: Map<string, number> = new Map();
|
|
287
|
+
/** Cooldown between webhook posts for the same (agent, kind). */
|
|
288
|
+
private readonly opsAlertCooldownMs = 15 * 60_000;
|
|
289
|
+
/** Per-agent refusal bookkeeping for observability — exposed via
|
|
290
|
+
* healthSnapshot() and mirrored to failures.log / ops alerts. */
|
|
291
|
+
private refusalStats: Map<string, { total: number; byCategory: Record<string, number>; lastAt: number; lastCategory: string }> = new Map();
|
|
292
|
+
/** Per-agent count of consecutive refusals (reset on any non-refusal
|
|
293
|
+
* completion). Distinct from refusalRewinds, which budgets the auto-rewind
|
|
294
|
+
* loop — this one drives ops alerting. */
|
|
295
|
+
private refusalStreak: Map<string, number> = new Map();
|
|
270
296
|
/** Per-agent count of consecutive refusal-driven rewinds in the current turn
|
|
271
297
|
* chain (reset when a turn completes without a refusal). Bounds the auto
|
|
272
298
|
* rewind loop — see refusalHandling + rewindTriggeringTurn. */
|
|
@@ -356,6 +382,7 @@ export class AgentFramework {
|
|
|
356
382
|
inferencePolicy: InferencePolicy,
|
|
357
383
|
errorPolicy: ErrorPolicy,
|
|
358
384
|
syncIntervalMs: number,
|
|
385
|
+
maintenanceIntervalMs: number,
|
|
359
386
|
processLoggingPersist: boolean,
|
|
360
387
|
processLoggingBroadcast: boolean
|
|
361
388
|
) {
|
|
@@ -365,6 +392,7 @@ export class AgentFramework {
|
|
|
365
392
|
this.inferencePolicy = inferencePolicy;
|
|
366
393
|
this.errorPolicy = errorPolicy;
|
|
367
394
|
this.syncIntervalMs = syncIntervalMs;
|
|
395
|
+
this.maintenanceIntervalMs = maintenanceIntervalMs;
|
|
368
396
|
this.processLoggingPersist = processLoggingPersist;
|
|
369
397
|
this.processLoggingBroadcast = processLoggingBroadcast;
|
|
370
398
|
this.queue = new ProcessQueueImpl();
|
|
@@ -455,6 +483,7 @@ export class AgentFramework {
|
|
|
455
483
|
config.inferencePolicy ?? new DefaultInferencePolicy(),
|
|
456
484
|
config.errorPolicy ?? new DefaultErrorPolicy(),
|
|
457
485
|
config.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS,
|
|
486
|
+
config.maintenanceIntervalMs ?? DEFAULT_MAINTENANCE_INTERVAL_MS,
|
|
458
487
|
processLoggingPersist,
|
|
459
488
|
processLoggingBroadcast
|
|
460
489
|
);
|
|
@@ -610,6 +639,16 @@ export class AgentFramework {
|
|
|
610
639
|
}
|
|
611
640
|
}, this.syncIntervalMs);
|
|
612
641
|
}
|
|
642
|
+
|
|
643
|
+
if (this.maintenanceIntervalMs > 0) {
|
|
644
|
+
this.maintenanceTimer = setInterval(() => {
|
|
645
|
+
this.startQueuedMaintenance();
|
|
646
|
+
}, this.maintenanceIntervalMs);
|
|
647
|
+
this.maintenanceTimer.unref?.();
|
|
648
|
+
// Do not make a restored queue wait a full interval before its first
|
|
649
|
+
// attempt. MCPL/module initialization has completed before start().
|
|
650
|
+
this.startQueuedMaintenance();
|
|
651
|
+
}
|
|
613
652
|
}
|
|
614
653
|
|
|
615
654
|
/**
|
|
@@ -638,6 +677,14 @@ export class AgentFramework {
|
|
|
638
677
|
this.syncTimer = null;
|
|
639
678
|
}
|
|
640
679
|
|
|
680
|
+
if (this.maintenanceTimer) {
|
|
681
|
+
clearInterval(this.maintenanceTimer);
|
|
682
|
+
this.maintenanceTimer = null;
|
|
683
|
+
}
|
|
684
|
+
if (this.maintenancePass) {
|
|
685
|
+
await this.maintenancePass;
|
|
686
|
+
}
|
|
687
|
+
|
|
641
688
|
if (this.loopPromise) {
|
|
642
689
|
await this.loopPromise;
|
|
643
690
|
}
|
|
@@ -668,6 +715,103 @@ export class AgentFramework {
|
|
|
668
715
|
}
|
|
669
716
|
}
|
|
670
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Advance queued ContextManager work without requiring a new message.
|
|
720
|
+
*
|
|
721
|
+
* Tool definitions are refreshed first because compression replays stored
|
|
722
|
+
* tool cycles and providers reject those requests when the corresponding
|
|
723
|
+
* schemas are absent. Passes never overlap; each agent gets a bounded drain
|
|
724
|
+
* so maintenance cannot monopolize the framework event loop.
|
|
725
|
+
*/
|
|
726
|
+
private startQueuedMaintenance(): void {
|
|
727
|
+
if (this.maintenancePass || !this.running) return;
|
|
728
|
+
const pass = this.runQueuedMaintenance().catch((error) => {
|
|
729
|
+
console.error(
|
|
730
|
+
'[context-maintenance] pass failed:',
|
|
731
|
+
error instanceof Error ? error.message : error,
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
this.maintenancePass = pass;
|
|
735
|
+
void pass.finally(() => {
|
|
736
|
+
if (this.maintenancePass === pass) this.maintenancePass = null;
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private async runQueuedMaintenance(): Promise<void> {
|
|
741
|
+
const allTools = this.getAllTools();
|
|
742
|
+
const queued = [...this.agents.values()].flatMap((agent) => {
|
|
743
|
+
const cm = agent.getContextManager();
|
|
744
|
+
const tools = allTools.filter((tool) => agent.canUseTool(tool.name));
|
|
745
|
+
cm.setToolDefinitions(tools);
|
|
746
|
+
if (cm.isReady()) return [];
|
|
747
|
+
const pending = cm.getPendingWork()?.description;
|
|
748
|
+
const progress = this.contextProgress(cm);
|
|
749
|
+
const record: ContextMaintenanceAgentRun = {
|
|
750
|
+
agentName: agent.name,
|
|
751
|
+
startedAt: Date.now(),
|
|
752
|
+
ticks: 0,
|
|
753
|
+
readyBefore: false,
|
|
754
|
+
...(pending ? { pendingBefore: pending } : {}),
|
|
755
|
+
...(progress ? { progressBefore: progress } : {}),
|
|
756
|
+
};
|
|
757
|
+
return [{
|
|
758
|
+
agent,
|
|
759
|
+
cm,
|
|
760
|
+
record,
|
|
761
|
+
}];
|
|
762
|
+
});
|
|
763
|
+
if (queued.length === 0) return;
|
|
764
|
+
|
|
765
|
+
const run: ContextMaintenanceRun = {
|
|
766
|
+
id: ++this.maintenanceRunId,
|
|
767
|
+
startedAt: Date.now(),
|
|
768
|
+
agents: queued.map(item => item.record),
|
|
769
|
+
};
|
|
770
|
+
this.currentMaintenanceRun = run;
|
|
771
|
+
|
|
772
|
+
try {
|
|
773
|
+
await Promise.all(queued.map(async ({ agent, cm, record }) => {
|
|
774
|
+
try {
|
|
775
|
+
for (
|
|
776
|
+
let i = 0;
|
|
777
|
+
i < MAINTENANCE_TICKS_PER_PASS && this.running && !cm.isReady();
|
|
778
|
+
i++
|
|
779
|
+
) {
|
|
780
|
+
await cm.tick();
|
|
781
|
+
record.ticks++;
|
|
782
|
+
}
|
|
783
|
+
} catch (error) {
|
|
784
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
785
|
+
console.error(`[context-maintenance] agent=${agent.name} failed:`, record.error);
|
|
786
|
+
} finally {
|
|
787
|
+
const pending = cm.getPendingWork()?.description;
|
|
788
|
+
record.finishedAt = Date.now();
|
|
789
|
+
record.readyAfter = cm.isReady();
|
|
790
|
+
if (pending) record.pendingAfter = pending;
|
|
791
|
+
const progress = this.contextProgress(cm);
|
|
792
|
+
if (progress) record.progressAfter = progress;
|
|
793
|
+
}
|
|
794
|
+
}));
|
|
795
|
+
} finally {
|
|
796
|
+
run.finishedAt = Date.now();
|
|
797
|
+
run.agents.sort((a, b) => a.agentName.localeCompare(b.agentName));
|
|
798
|
+
this.currentMaintenanceRun = null;
|
|
799
|
+
this.maintenanceHistory.unshift(run);
|
|
800
|
+
if (this.maintenanceHistory.length > 50) this.maintenanceHistory.length = 50;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private contextProgress(cm: ContextManager): Record<string, unknown> | undefined {
|
|
805
|
+
const strategy = cm.getStrategy() as {
|
|
806
|
+
getProgressSnapshot?: () => unknown;
|
|
807
|
+
};
|
|
808
|
+
if (typeof strategy.getProgressSnapshot !== 'function') return undefined;
|
|
809
|
+
const progress = strategy.getProgressSnapshot();
|
|
810
|
+
return progress && typeof progress === 'object'
|
|
811
|
+
? progress as Record<string, unknown>
|
|
812
|
+
: undefined;
|
|
813
|
+
}
|
|
814
|
+
|
|
671
815
|
/**
|
|
672
816
|
* Push a process event to the queue.
|
|
673
817
|
*/
|
|
@@ -790,6 +934,28 @@ export class AgentFramework {
|
|
|
790
934
|
return [...moduleTools, ...this.mcplTools, ...channelTools, ...gateTools];
|
|
791
935
|
}
|
|
792
936
|
|
|
937
|
+
/** Counts-only context-maintenance diagnostics for authenticated debug UIs. */
|
|
938
|
+
getContextMaintenanceSnapshot(): ContextMaintenanceSnapshot {
|
|
939
|
+
const agents = [...this.agents.values()].map((agent) => {
|
|
940
|
+
const cm = agent.getContextManager();
|
|
941
|
+
const pending = cm.getPendingWork()?.description;
|
|
942
|
+
const progress = this.contextProgress(cm);
|
|
943
|
+
return {
|
|
944
|
+
agentName: agent.name,
|
|
945
|
+
ready: cm.isReady(),
|
|
946
|
+
...(pending ? { pending } : {}),
|
|
947
|
+
...(progress ? { progress } : {}),
|
|
948
|
+
};
|
|
949
|
+
});
|
|
950
|
+
return structuredClone({
|
|
951
|
+
intervalMs: this.maintenanceIntervalMs,
|
|
952
|
+
ticksPerPass: MAINTENANCE_TICKS_PER_PASS,
|
|
953
|
+
current: this.currentMaintenanceRun,
|
|
954
|
+
history: this.maintenanceHistory,
|
|
955
|
+
agents,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
793
959
|
/**
|
|
794
960
|
* Build the membrane-normalized request that WOULD be emitted if `agentName`
|
|
795
961
|
* were activated right now — WITHOUT running inference, opening a stream, or
|
|
@@ -3544,6 +3710,8 @@ export class AgentFramework {
|
|
|
3544
3710
|
(stopDetails?.explanation ? ` explanation=${stopDetails.explanation}` : ''),
|
|
3545
3711
|
);
|
|
3546
3712
|
|
|
3713
|
+
this.noteRefusal(agent.name, category, tokenUsage);
|
|
3714
|
+
|
|
3547
3715
|
// Rewind: excise the turn that fed the refusal, drop a
|
|
3548
3716
|
// metadata-only marker, and retry — keeping the agent on its own
|
|
3549
3717
|
// model instead of substituting a fallback. Driven either by an
|
|
@@ -3621,6 +3789,7 @@ export class AgentFramework {
|
|
|
3621
3789
|
this.refusalRewinds.set(agent.name, 0);
|
|
3622
3790
|
}
|
|
3623
3791
|
this.rewindEpisode.delete(agent.name);
|
|
3792
|
+
this.refusalStreak.delete(agent.name);
|
|
3624
3793
|
}
|
|
3625
3794
|
|
|
3626
3795
|
// Dispatch speech (and thoughts if any)
|
|
@@ -4390,10 +4559,108 @@ export class AgentFramework {
|
|
|
4390
4559
|
status: agent.state.status,
|
|
4391
4560
|
consecutiveInferenceFailures: this.consecutiveInferenceFailures.get(name) ?? 0,
|
|
4392
4561
|
lastInference: this.lastInferenceAt.get(name) ?? null,
|
|
4562
|
+
refusalStats: this.refusalStats.get(name) ?? null,
|
|
4393
4563
|
})),
|
|
4394
4564
|
};
|
|
4395
4565
|
}
|
|
4396
4566
|
|
|
4567
|
+
/** Append a structured JSONL record to logs/failures.log (best-effort).
|
|
4568
|
+
* Durable and independent of journald/unit log redirects — this is what
|
|
4569
|
+
* connectome-doctor and fleet tooling read. Always stamps `at`. Legacy
|
|
4570
|
+
* records carry {at, agent, consecutive, reason}; new records add `kind`
|
|
4571
|
+
* and kind-specific fields (additive only — doctor parses by regex). */
|
|
4572
|
+
private logFailure(record: Record<string, unknown>): void {
|
|
4573
|
+
try {
|
|
4574
|
+
mkdirSync('logs', { recursive: true });
|
|
4575
|
+
appendFileSync(
|
|
4576
|
+
'logs/failures.log',
|
|
4577
|
+
JSON.stringify({ at: new Date().toISOString(), ...record }) + '\n',
|
|
4578
|
+
);
|
|
4579
|
+
} catch { /* best-effort */ }
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4582
|
+
/**
|
|
4583
|
+
* Ops alert — the one escalation path for "a human should hear about this":
|
|
4584
|
+
* (1) durable failures.log record (unless the caller already wrote one),
|
|
4585
|
+
* (2) an `ops:alert` trace so authorized observers get it on the wire,
|
|
4586
|
+
* (3) if CONNECTOME_OPS_WEBHOOK is set, a Discord post throttled to one per
|
|
4587
|
+
* (agent, kind) per cooldown window — a persistent failure re-posts
|
|
4588
|
+
* every ~15 min instead of flooding the channel on every occurrence.
|
|
4589
|
+
* See connectome docs/observability.md.
|
|
4590
|
+
*/
|
|
4591
|
+
private opsAlert(
|
|
4592
|
+
kind: string,
|
|
4593
|
+
agentName: string,
|
|
4594
|
+
message: string,
|
|
4595
|
+
opts?: { data?: Record<string, unknown>; skipLog?: boolean },
|
|
4596
|
+
): void {
|
|
4597
|
+
if (!opts?.skipLog) {
|
|
4598
|
+
this.logFailure({ agent: agentName, kind, reason: message, ...opts?.data });
|
|
4599
|
+
}
|
|
4600
|
+
this.emitTrace({ type: 'ops:alert', kind, agentName, message, data: opts?.data });
|
|
4601
|
+
|
|
4602
|
+
const hook = process.env.CONNECTOME_OPS_WEBHOOK;
|
|
4603
|
+
if (!hook) return;
|
|
4604
|
+
const key = `${agentName}:${kind}`;
|
|
4605
|
+
const now = Date.now();
|
|
4606
|
+
if (now - (this.opsAlertLastSent.get(key) ?? 0) < this.opsAlertCooldownMs) return;
|
|
4607
|
+
// Stamp BEFORE the async post so a burst can't race past the cooldown.
|
|
4608
|
+
this.opsAlertLastSent.set(key, now);
|
|
4609
|
+
fetch(hook, {
|
|
4610
|
+
method: 'POST',
|
|
4611
|
+
headers: { 'content-type': 'application/json' },
|
|
4612
|
+
body: JSON.stringify({
|
|
4613
|
+
content: `\u{1F6A8} **${agentName}** ${kind}: ${message.slice(0, 500)}`,
|
|
4614
|
+
}),
|
|
4615
|
+
}).catch((err) => console.error(
|
|
4616
|
+
`[ops-alert] webhook post failed (${key}):`,
|
|
4617
|
+
err instanceof Error ? err.message : err,
|
|
4618
|
+
));
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
/**
|
|
4622
|
+
* Record a model refusal for observability: per-agent stats + a
|
|
4623
|
+
* consecutive-refusal streak (exposed via healthSnapshot), a durable
|
|
4624
|
+
* failures.log record for EVERY refusal (previously invisible outside
|
|
4625
|
+
* stderr), and a throttled ops alert once refusals repeat — one refusal is
|
|
4626
|
+
* often survivable (auto-rewind may clear it); two in a row means stuck.
|
|
4627
|
+
* The streak is reset by any non-refusal completion in the stream driver.
|
|
4628
|
+
*/
|
|
4629
|
+
private noteRefusal(
|
|
4630
|
+
agentName: string,
|
|
4631
|
+
category: string,
|
|
4632
|
+
tokens?: { input: number; output: number },
|
|
4633
|
+
): number {
|
|
4634
|
+
const stats = this.refusalStats.get(agentName)
|
|
4635
|
+
?? { total: 0, byCategory: {}, lastAt: 0, lastCategory: '' };
|
|
4636
|
+
stats.total += 1;
|
|
4637
|
+
stats.byCategory[category] = (stats.byCategory[category] ?? 0) + 1;
|
|
4638
|
+
stats.lastAt = Date.now();
|
|
4639
|
+
stats.lastCategory = category;
|
|
4640
|
+
this.refusalStats.set(agentName, stats);
|
|
4641
|
+
|
|
4642
|
+
const streak = (this.refusalStreak.get(agentName) ?? 0) + 1;
|
|
4643
|
+
this.refusalStreak.set(agentName, streak);
|
|
4644
|
+
|
|
4645
|
+
this.logFailure({
|
|
4646
|
+
agent: agentName,
|
|
4647
|
+
kind: 'refusal',
|
|
4648
|
+
category,
|
|
4649
|
+
streak,
|
|
4650
|
+
locus: this.channelRegistry?.buildChannelContext()?.incoming?.channelId ?? null,
|
|
4651
|
+
tokens: tokens ?? null,
|
|
4652
|
+
});
|
|
4653
|
+
if (streak >= 2) {
|
|
4654
|
+
this.opsAlert(
|
|
4655
|
+
'refusal',
|
|
4656
|
+
agentName,
|
|
4657
|
+
`refusal streak ${streak}, category=${category}`,
|
|
4658
|
+
{ skipLog: true, data: { category, streak } },
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
return streak;
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4397
4664
|
private noteInferenceExhausted(
|
|
4398
4665
|
agentName: string,
|
|
4399
4666
|
reason: string,
|
|
@@ -4409,14 +4676,8 @@ export class AgentFramework {
|
|
|
4409
4676
|
|
|
4410
4677
|
// (1b) Machine-greppable durable record, independent of journald/unit log
|
|
4411
4678
|
// redirects: logs/failures.log under the host's working directory. This is
|
|
4412
|
-
// what connectome-doctor reads.
|
|
4413
|
-
|
|
4414
|
-
mkdirSync('logs', { recursive: true });
|
|
4415
|
-
appendFileSync(
|
|
4416
|
-
'logs/failures.log',
|
|
4417
|
-
JSON.stringify({ at: new Date().toISOString(), agent: agentName, consecutive: streak, reason }) + '\n',
|
|
4418
|
-
);
|
|
4419
|
-
} catch { /* best-effort */ }
|
|
4679
|
+
// what connectome-doctor reads. Legacy fields kept; `kind` is additive.
|
|
4680
|
+
this.logFailure({ agent: agentName, consecutive: streak, reason, kind: 'inference-exhausted' });
|
|
4420
4681
|
|
|
4421
4682
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
4422
4683
|
const agent = this.agents.get(agentName);
|
|
@@ -4447,17 +4708,15 @@ export class AgentFramework {
|
|
|
4447
4708
|
`inferences — it cannot complete a turn. Last reason: ${reason}`,
|
|
4448
4709
|
);
|
|
4449
4710
|
|
|
4450
|
-
//
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
}).catch((err) => console.error('[inference-hard-down] webhook post failed:', err instanceof Error ? err.message : err));
|
|
4460
|
-
}
|
|
4711
|
+
// Ops alert (webhook + ops:alert trace), throttled per (agent, kind) —
|
|
4712
|
+
// a hard-down agent re-posts every ~15 min, not on every failed retry.
|
|
4713
|
+
// failures.log already got the per-exhaustion record above.
|
|
4714
|
+
this.opsAlert(
|
|
4715
|
+
'hard-down',
|
|
4716
|
+
agentName,
|
|
4717
|
+
`${streak} consecutive inference failures. Last reason: ${reason}`,
|
|
4718
|
+
{ skipLog: true },
|
|
4719
|
+
);
|
|
4461
4720
|
|
|
4462
4721
|
// (4) Poison-history breaker: ONLY for `invalid_request` — a 400-class
|
|
4463
4722
|
// rejection of the history itself. Deliberately NOT keyed on
|
|
@@ -5030,6 +5289,17 @@ export class AgentFramework {
|
|
|
5030
5289
|
attempt: params.attempt,
|
|
5031
5290
|
willRetry: connection.willReconnect,
|
|
5032
5291
|
});
|
|
5292
|
+
// The reconnect loop never gives up (backoff caps at ~300s), so
|
|
5293
|
+
// "the server is effectively down" is an attempt-count judgment:
|
|
5294
|
+
// 5 failed attempts ≈ a few minutes of outage. Throttled per
|
|
5295
|
+
// (serverId, kind), so a long outage re-posts every ~15 min.
|
|
5296
|
+
if (params.attempt >= 5) {
|
|
5297
|
+
this.opsAlert(
|
|
5298
|
+
'mcpl-down',
|
|
5299
|
+
connection.id,
|
|
5300
|
+
`MCPL server unreachable (attempt ${params.attempt}): ${params.error}`,
|
|
5301
|
+
);
|
|
5302
|
+
}
|
|
5033
5303
|
});
|
|
5034
5304
|
|
|
5035
5305
|
// A late response to a tools/call that already timed out, carrying
|
package/src/types/framework.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface FrameworkConfig {
|
|
|
56
56
|
/** Interval for periodic store sync in milliseconds (default: 1000ms, 0 to disable) */
|
|
57
57
|
syncIntervalMs?: number;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Interval for queued context-maintenance polling in milliseconds
|
|
61
|
+
* (default: 5000ms, 0 to disable). Each pass refreshes the agent's live tool
|
|
62
|
+
* definitions, then advances pending compression/indexing work.
|
|
63
|
+
*/
|
|
64
|
+
maintenanceIntervalMs?: number;
|
|
65
|
+
|
|
59
66
|
/** Process logging configuration (default: disabled) */
|
|
60
67
|
processLogging?: ProcessLoggingConfig;
|
|
61
68
|
|
|
@@ -92,6 +99,43 @@ export interface FrameworkConfig {
|
|
|
92
99
|
conversations?: ConversationRouterConfig;
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/** One agent's outcome within a periodic context-maintenance pass. */
|
|
103
|
+
export interface ContextMaintenanceAgentRun {
|
|
104
|
+
agentName: string;
|
|
105
|
+
startedAt: number;
|
|
106
|
+
finishedAt?: number;
|
|
107
|
+
ticks: number;
|
|
108
|
+
readyBefore: boolean;
|
|
109
|
+
readyAfter?: boolean;
|
|
110
|
+
pendingBefore?: string;
|
|
111
|
+
pendingAfter?: string;
|
|
112
|
+
progressBefore?: Record<string, unknown>;
|
|
113
|
+
progressAfter?: Record<string, unknown>;
|
|
114
|
+
error?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A bounded periodic context-maintenance pass. */
|
|
118
|
+
export interface ContextMaintenanceRun {
|
|
119
|
+
id: number;
|
|
120
|
+
startedAt: number;
|
|
121
|
+
finishedAt?: number;
|
|
122
|
+
agents: ContextMaintenanceAgentRun[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Debug-safe maintenance state. Contains counts and errors, never messages. */
|
|
126
|
+
export interface ContextMaintenanceSnapshot {
|
|
127
|
+
intervalMs: number;
|
|
128
|
+
ticksPerPass: number;
|
|
129
|
+
current: ContextMaintenanceRun | null;
|
|
130
|
+
history: ContextMaintenanceRun[];
|
|
131
|
+
agents: Array<{
|
|
132
|
+
agentName: string;
|
|
133
|
+
ready: boolean;
|
|
134
|
+
pending?: string;
|
|
135
|
+
progress?: Record<string, unknown>;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
|
|
95
139
|
/**
|
|
96
140
|
* Policy for deciding when to run inference.
|
|
97
141
|
*/
|
package/src/types/trace.ts
CHANGED
|
@@ -321,6 +321,22 @@ export type TraceEvent =
|
|
|
321
321
|
hadState: boolean;
|
|
322
322
|
/** True when the dropped response carried a server-managed `checkpoint`. */
|
|
323
323
|
hadCheckpoint: boolean;
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Ops / fleet observability. Everything the framework's opsAlert() escalates
|
|
327
|
+
// (refusals, hard-down, MCPL unreachable, …) mirrored onto the trace bus so
|
|
328
|
+
// authorized observers see alerts on the same wire as the rest of the
|
|
329
|
+
// agent's internal life. Discord (CONNECTOME_OPS_WEBHOOK) is one sink of
|
|
330
|
+
// this stream, not a separate system. See connectome docs/observability.md.
|
|
331
|
+
| (TraceEventBase & {
|
|
332
|
+
type: 'ops:alert';
|
|
333
|
+
/** Alert kind — open set: 'refusal' | 'hard-down' | 'mcpl-down' | … */
|
|
334
|
+
kind: string;
|
|
335
|
+
/** Agent name, or a server/component id for non-agent alerts (e.g. mcpl-down). */
|
|
336
|
+
agentName: string;
|
|
337
|
+
message: string;
|
|
338
|
+
/** Kind-specific structured payload (mirrors the failures.log record). */
|
|
339
|
+
data?: Record<string, unknown>;
|
|
324
340
|
});
|
|
325
341
|
|
|
326
342
|
/**
|