@animalabs/agent-framework 0.6.2 → 0.6.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 +3 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +7 -2
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +60 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +261 -20
- package/dist/src/framework.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +1 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +4 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/types.d.ts +2 -0
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts +2 -0
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +15 -4
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/timezone.d.ts +14 -0
- package/dist/src/timezone.d.ts.map +1 -0
- package/dist/src/timezone.js +74 -0
- package/dist/src/timezone.js.map +1 -0
- package/dist/src/types/agent.d.ts +5 -2
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +46 -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/agent-cache-ttl.test.js +18 -2
- package/dist/test/agent-cache-ttl.test.js.map +1 -1
- package/dist/test/health-snapshot.test.d.ts +2 -0
- package/dist/test/health-snapshot.test.d.ts.map +1 -0
- package/dist/test/health-snapshot.test.js +74 -0
- package/dist/test/health-snapshot.test.js.map +1 -0
- 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/test/timezone.test.d.ts +2 -0
- package/dist/test/timezone.test.d.ts.map +1 -0
- package/dist/test/timezone.test.js +18 -0
- package/dist/test/timezone.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/agent.ts +8 -3
- package/src/framework.ts +307 -22
- package/src/index.ts +1 -0
- package/src/modules/discord/index.ts +4 -1
- package/src/modules/discord/types.ts +3 -0
- package/src/modules/health/index.ts +20 -4
- package/src/timezone.ts +90 -0
- package/src/types/agent.ts +6 -2
- package/src/types/framework.ts +51 -0
- package/src/types/trace.ts +16 -0
package/dist/src/framework.js
CHANGED
|
@@ -21,6 +21,7 @@ import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
|
21
21
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
22
22
|
import { EventGate } from './gate/event-gate.js';
|
|
23
23
|
import { UsageTracker } from './usage/usage-tracker.js';
|
|
24
|
+
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
24
25
|
const FRAMEWORK_STATE_ID = 'framework/state';
|
|
25
26
|
const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
|
|
26
27
|
const INFERENCE_LOG_ID = 'framework/inference-log';
|
|
@@ -66,6 +67,10 @@ class DefaultErrorPolicy {
|
|
|
66
67
|
}
|
|
67
68
|
/** Default sync interval in milliseconds */
|
|
68
69
|
const DEFAULT_SYNC_INTERVAL_MS = 1000;
|
|
70
|
+
/** Poll pending context-strategy maintenance independently of user activity. */
|
|
71
|
+
const DEFAULT_MAINTENANCE_INTERVAL_MS = 5000;
|
|
72
|
+
/** Bound one pass so a large backlog yields to inference and other agents. */
|
|
73
|
+
const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
69
74
|
/**
|
|
70
75
|
* Extract fields the EventGate cares about from a ProcessEvent. The set of
|
|
71
76
|
* event variants that carry `content`/`mount`/`paths`/`metadata` is open-ended
|
|
@@ -128,6 +133,12 @@ export class AgentFramework {
|
|
|
128
133
|
traceListeners = [];
|
|
129
134
|
syncIntervalMs;
|
|
130
135
|
syncTimer = null;
|
|
136
|
+
maintenanceIntervalMs;
|
|
137
|
+
maintenanceTimer = null;
|
|
138
|
+
maintenancePass = null;
|
|
139
|
+
maintenanceRunId = 0;
|
|
140
|
+
currentMaintenanceRun = null;
|
|
141
|
+
maintenanceHistory = [];
|
|
131
142
|
/** Last time we console-warned about stale (busy-requeued) inference requests, per agent. */
|
|
132
143
|
staleWarnAt = new Map();
|
|
133
144
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -142,6 +153,19 @@ export class AgentFramework {
|
|
|
142
153
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
143
154
|
* escalated loudly to stderr. */
|
|
144
155
|
inferenceFailureEscalationThreshold = 3;
|
|
156
|
+
/** Per-(agent,kind) timestamp of the last ops webhook post. Throttles
|
|
157
|
+
* opsAlert() so a persistent failure re-posts once per cooldown window
|
|
158
|
+
* instead of on every occurrence. */
|
|
159
|
+
opsAlertLastSent = new Map();
|
|
160
|
+
/** Cooldown between webhook posts for the same (agent, kind). */
|
|
161
|
+
opsAlertCooldownMs = 15 * 60_000;
|
|
162
|
+
/** Per-agent refusal bookkeeping for observability — exposed via
|
|
163
|
+
* healthSnapshot() and mirrored to failures.log / ops alerts. */
|
|
164
|
+
refusalStats = new Map();
|
|
165
|
+
/** Per-agent count of consecutive refusals (reset on any non-refusal
|
|
166
|
+
* completion). Distinct from refusalRewinds, which budgets the auto-rewind
|
|
167
|
+
* loop — this one drives ops alerting. */
|
|
168
|
+
refusalStreak = new Map();
|
|
145
169
|
/** Per-agent count of consecutive refusal-driven rewinds in the current turn
|
|
146
170
|
* chain (reset when a turn completes without a refusal). Bounds the auto
|
|
147
171
|
* rewind loop — see refusalHandling + rewindTriggeringTurn. */
|
|
@@ -211,15 +235,19 @@ export class AgentFramework {
|
|
|
211
235
|
eventGate = null;
|
|
212
236
|
// Session-level token usage tracking (always-on)
|
|
213
237
|
usageTracker;
|
|
214
|
-
|
|
238
|
+
/** Presentation-only wall-clock zone; persistence remains UTC/epoch. */
|
|
239
|
+
timeZone;
|
|
240
|
+
constructor(store, ownsStore, membrane, inferencePolicy, errorPolicy, syncIntervalMs, maintenanceIntervalMs, processLoggingPersist, processLoggingBroadcast, timeZone) {
|
|
215
241
|
this.store = store;
|
|
216
242
|
this.ownsStore = ownsStore;
|
|
217
243
|
this.membrane = membrane;
|
|
218
244
|
this.inferencePolicy = inferencePolicy;
|
|
219
245
|
this.errorPolicy = errorPolicy;
|
|
220
246
|
this.syncIntervalMs = syncIntervalMs;
|
|
247
|
+
this.maintenanceIntervalMs = maintenanceIntervalMs;
|
|
221
248
|
this.processLoggingPersist = processLoggingPersist;
|
|
222
249
|
this.processLoggingBroadcast = processLoggingBroadcast;
|
|
250
|
+
this.timeZone = timeZone;
|
|
223
251
|
this.queue = new ProcessQueueImpl();
|
|
224
252
|
this.usageTracker = new UsageTracker({
|
|
225
253
|
emitTrace: (e) => this.emitTrace({ ...e }),
|
|
@@ -299,7 +327,7 @@ export class AgentFramework {
|
|
|
299
327
|
// Already registered
|
|
300
328
|
}
|
|
301
329
|
}
|
|
302
|
-
const framework = new AgentFramework(store, ownsStore, config.membrane, config.inferencePolicy ?? new DefaultInferencePolicy(), config.errorPolicy ?? new DefaultErrorPolicy(), config.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS, processLoggingPersist, processLoggingBroadcast);
|
|
330
|
+
const framework = new AgentFramework(store, ownsStore, config.membrane, config.inferencePolicy ?? new DefaultInferencePolicy(), config.errorPolicy ?? new DefaultErrorPolicy(), config.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS, config.maintenanceIntervalMs ?? DEFAULT_MAINTENANCE_INTERVAL_MS, processLoggingPersist, processLoggingBroadcast, resolveTimeZone(config.timeZone));
|
|
303
331
|
// Restore persisted usage data (if any) from prior session
|
|
304
332
|
framework.restoreUsageState();
|
|
305
333
|
// Create agents
|
|
@@ -433,6 +461,15 @@ export class AgentFramework {
|
|
|
433
461
|
}
|
|
434
462
|
}, this.syncIntervalMs);
|
|
435
463
|
}
|
|
464
|
+
if (this.maintenanceIntervalMs > 0) {
|
|
465
|
+
this.maintenanceTimer = setInterval(() => {
|
|
466
|
+
this.startQueuedMaintenance();
|
|
467
|
+
}, this.maintenanceIntervalMs);
|
|
468
|
+
this.maintenanceTimer.unref?.();
|
|
469
|
+
// Do not make a restored queue wait a full interval before its first
|
|
470
|
+
// attempt. MCPL/module initialization has completed before start().
|
|
471
|
+
this.startQueuedMaintenance();
|
|
472
|
+
}
|
|
436
473
|
}
|
|
437
474
|
/**
|
|
438
475
|
* Stop the event loop.
|
|
@@ -456,6 +493,13 @@ export class AgentFramework {
|
|
|
456
493
|
clearInterval(this.syncTimer);
|
|
457
494
|
this.syncTimer = null;
|
|
458
495
|
}
|
|
496
|
+
if (this.maintenanceTimer) {
|
|
497
|
+
clearInterval(this.maintenanceTimer);
|
|
498
|
+
this.maintenanceTimer = null;
|
|
499
|
+
}
|
|
500
|
+
if (this.maintenancePass) {
|
|
501
|
+
await this.maintenancePass;
|
|
502
|
+
}
|
|
459
503
|
if (this.loopPromise) {
|
|
460
504
|
await this.loopPromise;
|
|
461
505
|
}
|
|
@@ -481,6 +525,100 @@ export class AgentFramework {
|
|
|
481
525
|
this.store.close();
|
|
482
526
|
}
|
|
483
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Advance queued ContextManager work without requiring a new message.
|
|
530
|
+
*
|
|
531
|
+
* Tool definitions are refreshed first because compression replays stored
|
|
532
|
+
* tool cycles and providers reject those requests when the corresponding
|
|
533
|
+
* schemas are absent. Passes never overlap; each agent gets a bounded drain
|
|
534
|
+
* so maintenance cannot monopolize the framework event loop.
|
|
535
|
+
*/
|
|
536
|
+
startQueuedMaintenance() {
|
|
537
|
+
if (this.maintenancePass || !this.running)
|
|
538
|
+
return;
|
|
539
|
+
const pass = this.runQueuedMaintenance().catch((error) => {
|
|
540
|
+
console.error('[context-maintenance] pass failed:', error instanceof Error ? error.message : error);
|
|
541
|
+
});
|
|
542
|
+
this.maintenancePass = pass;
|
|
543
|
+
void pass.finally(() => {
|
|
544
|
+
if (this.maintenancePass === pass)
|
|
545
|
+
this.maintenancePass = null;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async runQueuedMaintenance() {
|
|
549
|
+
const allTools = this.getAllTools();
|
|
550
|
+
const queued = [...this.agents.values()].flatMap((agent) => {
|
|
551
|
+
const cm = agent.getContextManager();
|
|
552
|
+
const tools = allTools.filter((tool) => agent.canUseTool(tool.name));
|
|
553
|
+
cm.setToolDefinitions(tools);
|
|
554
|
+
if (cm.isReady())
|
|
555
|
+
return [];
|
|
556
|
+
const pending = cm.getPendingWork()?.description;
|
|
557
|
+
const progress = this.contextProgress(cm);
|
|
558
|
+
const record = {
|
|
559
|
+
agentName: agent.name,
|
|
560
|
+
startedAt: Date.now(),
|
|
561
|
+
ticks: 0,
|
|
562
|
+
readyBefore: false,
|
|
563
|
+
...(pending ? { pendingBefore: pending } : {}),
|
|
564
|
+
...(progress ? { progressBefore: progress } : {}),
|
|
565
|
+
};
|
|
566
|
+
return [{
|
|
567
|
+
agent,
|
|
568
|
+
cm,
|
|
569
|
+
record,
|
|
570
|
+
}];
|
|
571
|
+
});
|
|
572
|
+
if (queued.length === 0)
|
|
573
|
+
return;
|
|
574
|
+
const run = {
|
|
575
|
+
id: ++this.maintenanceRunId,
|
|
576
|
+
startedAt: Date.now(),
|
|
577
|
+
agents: queued.map(item => item.record),
|
|
578
|
+
};
|
|
579
|
+
this.currentMaintenanceRun = run;
|
|
580
|
+
try {
|
|
581
|
+
await Promise.all(queued.map(async ({ agent, cm, record }) => {
|
|
582
|
+
try {
|
|
583
|
+
for (let i = 0; i < MAINTENANCE_TICKS_PER_PASS && this.running && !cm.isReady(); i++) {
|
|
584
|
+
await cm.tick();
|
|
585
|
+
record.ticks++;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
590
|
+
console.error(`[context-maintenance] agent=${agent.name} failed:`, record.error);
|
|
591
|
+
}
|
|
592
|
+
finally {
|
|
593
|
+
const pending = cm.getPendingWork()?.description;
|
|
594
|
+
record.finishedAt = Date.now();
|
|
595
|
+
record.readyAfter = cm.isReady();
|
|
596
|
+
if (pending)
|
|
597
|
+
record.pendingAfter = pending;
|
|
598
|
+
const progress = this.contextProgress(cm);
|
|
599
|
+
if (progress)
|
|
600
|
+
record.progressAfter = progress;
|
|
601
|
+
}
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
run.finishedAt = Date.now();
|
|
606
|
+
run.agents.sort((a, b) => a.agentName.localeCompare(b.agentName));
|
|
607
|
+
this.currentMaintenanceRun = null;
|
|
608
|
+
this.maintenanceHistory.unshift(run);
|
|
609
|
+
if (this.maintenanceHistory.length > 50)
|
|
610
|
+
this.maintenanceHistory.length = 50;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
contextProgress(cm) {
|
|
614
|
+
const strategy = cm.getStrategy();
|
|
615
|
+
if (typeof strategy.getProgressSnapshot !== 'function')
|
|
616
|
+
return undefined;
|
|
617
|
+
const progress = strategy.getProgressSnapshot();
|
|
618
|
+
return progress && typeof progress === 'object'
|
|
619
|
+
? progress
|
|
620
|
+
: undefined;
|
|
621
|
+
}
|
|
484
622
|
/**
|
|
485
623
|
* Push a process event to the queue.
|
|
486
624
|
*/
|
|
@@ -592,6 +730,27 @@ export class AgentFramework {
|
|
|
592
730
|
}
|
|
593
731
|
return [...moduleTools, ...this.mcplTools, ...channelTools, ...gateTools];
|
|
594
732
|
}
|
|
733
|
+
/** Counts-only context-maintenance diagnostics for authenticated debug UIs. */
|
|
734
|
+
getContextMaintenanceSnapshot() {
|
|
735
|
+
const agents = [...this.agents.values()].map((agent) => {
|
|
736
|
+
const cm = agent.getContextManager();
|
|
737
|
+
const pending = cm.getPendingWork()?.description;
|
|
738
|
+
const progress = this.contextProgress(cm);
|
|
739
|
+
return {
|
|
740
|
+
agentName: agent.name,
|
|
741
|
+
ready: cm.isReady(),
|
|
742
|
+
...(pending ? { pending } : {}),
|
|
743
|
+
...(progress ? { progress } : {}),
|
|
744
|
+
};
|
|
745
|
+
});
|
|
746
|
+
return structuredClone({
|
|
747
|
+
intervalMs: this.maintenanceIntervalMs,
|
|
748
|
+
ticksPerPass: MAINTENANCE_TICKS_PER_PASS,
|
|
749
|
+
current: this.currentMaintenanceRun,
|
|
750
|
+
history: this.maintenanceHistory,
|
|
751
|
+
agents,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
595
754
|
/**
|
|
596
755
|
* Build the membrane-normalized request that WOULD be emitted if `agentName`
|
|
597
756
|
* were activated right now — WITHOUT running inference, opening a stream, or
|
|
@@ -2970,6 +3129,7 @@ export class AgentFramework {
|
|
|
2970
3129
|
const category = stopDetails?.category ?? 'unknown';
|
|
2971
3130
|
console.error(`[inference-refusal] agent=${agent.name} category=${category}` +
|
|
2972
3131
|
(stopDetails?.explanation ? ` explanation=${stopDetails.explanation}` : ''));
|
|
3132
|
+
this.noteRefusal(agent.name, category, tokenUsage);
|
|
2973
3133
|
// Rewind: excise the turn that fed the refusal, drop a
|
|
2974
3134
|
// metadata-only marker, and retry — keeping the agent on its own
|
|
2975
3135
|
// model instead of substituting a fallback. Driven either by an
|
|
@@ -3037,6 +3197,7 @@ export class AgentFramework {
|
|
|
3037
3197
|
this.refusalRewinds.set(agent.name, 0);
|
|
3038
3198
|
}
|
|
3039
3199
|
this.rewindEpisode.delete(agent.name);
|
|
3200
|
+
this.refusalStreak.delete(agent.name);
|
|
3040
3201
|
}
|
|
3041
3202
|
// Dispatch speech (and thoughts if any)
|
|
3042
3203
|
if (speechContent.length > 0 || thoughts.length > 0) {
|
|
@@ -3740,9 +3901,84 @@ export class AgentFramework {
|
|
|
3740
3901
|
status: agent.state.status,
|
|
3741
3902
|
consecutiveInferenceFailures: this.consecutiveInferenceFailures.get(name) ?? 0,
|
|
3742
3903
|
lastInference: this.lastInferenceAt.get(name) ?? null,
|
|
3904
|
+
refusalStats: this.refusalStats.get(name) ?? null,
|
|
3743
3905
|
})),
|
|
3744
3906
|
};
|
|
3745
3907
|
}
|
|
3908
|
+
/** Append a structured JSONL record to logs/failures.log (best-effort).
|
|
3909
|
+
* Durable and independent of journald/unit log redirects — this is what
|
|
3910
|
+
* connectome-doctor and fleet tooling read. Always stamps `at`. Legacy
|
|
3911
|
+
* records carry {at, agent, consecutive, reason}; new records add `kind`
|
|
3912
|
+
* and kind-specific fields (additive only — doctor parses by regex). */
|
|
3913
|
+
logFailure(record) {
|
|
3914
|
+
try {
|
|
3915
|
+
mkdirSync('logs', { recursive: true });
|
|
3916
|
+
appendFileSync('logs/failures.log', JSON.stringify({ at: new Date().toISOString(), ...record }) + '\n');
|
|
3917
|
+
}
|
|
3918
|
+
catch { /* best-effort */ }
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Ops alert — the one escalation path for "a human should hear about this":
|
|
3922
|
+
* (1) durable failures.log record (unless the caller already wrote one),
|
|
3923
|
+
* (2) an `ops:alert` trace so authorized observers get it on the wire,
|
|
3924
|
+
* (3) if CONNECTOME_OPS_WEBHOOK is set, a Discord post throttled to one per
|
|
3925
|
+
* (agent, kind) per cooldown window — a persistent failure re-posts
|
|
3926
|
+
* every ~15 min instead of flooding the channel on every occurrence.
|
|
3927
|
+
* See connectome docs/observability.md.
|
|
3928
|
+
*/
|
|
3929
|
+
opsAlert(kind, agentName, message, opts) {
|
|
3930
|
+
if (!opts?.skipLog) {
|
|
3931
|
+
this.logFailure({ agent: agentName, kind, reason: message, ...opts?.data });
|
|
3932
|
+
}
|
|
3933
|
+
this.emitTrace({ type: 'ops:alert', kind, agentName, message, data: opts?.data });
|
|
3934
|
+
const hook = process.env.CONNECTOME_OPS_WEBHOOK;
|
|
3935
|
+
if (!hook)
|
|
3936
|
+
return;
|
|
3937
|
+
const key = `${agentName}:${kind}`;
|
|
3938
|
+
const now = Date.now();
|
|
3939
|
+
if (now - (this.opsAlertLastSent.get(key) ?? 0) < this.opsAlertCooldownMs)
|
|
3940
|
+
return;
|
|
3941
|
+
// Stamp BEFORE the async post so a burst can't race past the cooldown.
|
|
3942
|
+
this.opsAlertLastSent.set(key, now);
|
|
3943
|
+
fetch(hook, {
|
|
3944
|
+
method: 'POST',
|
|
3945
|
+
headers: { 'content-type': 'application/json' },
|
|
3946
|
+
body: JSON.stringify({
|
|
3947
|
+
content: `\u{1F6A8} **${agentName}** ${kind}: ${message.slice(0, 500)}`,
|
|
3948
|
+
}),
|
|
3949
|
+
}).catch((err) => console.error(`[ops-alert] webhook post failed (${key}):`, err instanceof Error ? err.message : err));
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Record a model refusal for observability: per-agent stats + a
|
|
3953
|
+
* consecutive-refusal streak (exposed via healthSnapshot), a durable
|
|
3954
|
+
* failures.log record for EVERY refusal (previously invisible outside
|
|
3955
|
+
* stderr), and a throttled ops alert once refusals repeat — one refusal is
|
|
3956
|
+
* often survivable (auto-rewind may clear it); two in a row means stuck.
|
|
3957
|
+
* The streak is reset by any non-refusal completion in the stream driver.
|
|
3958
|
+
*/
|
|
3959
|
+
noteRefusal(agentName, category, tokens) {
|
|
3960
|
+
const stats = this.refusalStats.get(agentName)
|
|
3961
|
+
?? { total: 0, byCategory: {}, lastAt: 0, lastCategory: '' };
|
|
3962
|
+
stats.total += 1;
|
|
3963
|
+
stats.byCategory[category] = (stats.byCategory[category] ?? 0) + 1;
|
|
3964
|
+
stats.lastAt = Date.now();
|
|
3965
|
+
stats.lastCategory = category;
|
|
3966
|
+
this.refusalStats.set(agentName, stats);
|
|
3967
|
+
const streak = (this.refusalStreak.get(agentName) ?? 0) + 1;
|
|
3968
|
+
this.refusalStreak.set(agentName, streak);
|
|
3969
|
+
this.logFailure({
|
|
3970
|
+
agent: agentName,
|
|
3971
|
+
kind: 'refusal',
|
|
3972
|
+
category,
|
|
3973
|
+
streak,
|
|
3974
|
+
locus: this.channelRegistry?.buildChannelContext()?.incoming?.channelId ?? null,
|
|
3975
|
+
tokens: tokens ?? null,
|
|
3976
|
+
});
|
|
3977
|
+
if (streak >= 2) {
|
|
3978
|
+
this.opsAlert('refusal', agentName, `refusal streak ${streak}, category=${category}`, { skipLog: true, data: { category, streak } });
|
|
3979
|
+
}
|
|
3980
|
+
return streak;
|
|
3981
|
+
}
|
|
3746
3982
|
noteInferenceExhausted(agentName, reason, retryable, errorType) {
|
|
3747
3983
|
const streak = (this.consecutiveInferenceFailures.get(agentName) ?? 0) + 1;
|
|
3748
3984
|
this.consecutiveInferenceFailures.set(agentName, streak);
|
|
@@ -3751,12 +3987,8 @@ export class AgentFramework {
|
|
|
3751
3987
|
console.error(`[inference-failed] agent=${agentName} consecutive=${streak}: ${reason}`);
|
|
3752
3988
|
// (1b) Machine-greppable durable record, independent of journald/unit log
|
|
3753
3989
|
// redirects: logs/failures.log under the host's working directory. This is
|
|
3754
|
-
// what connectome-doctor reads.
|
|
3755
|
-
|
|
3756
|
-
mkdirSync('logs', { recursive: true });
|
|
3757
|
-
appendFileSync('logs/failures.log', JSON.stringify({ at: new Date().toISOString(), agent: agentName, consecutive: streak, reason }) + '\n');
|
|
3758
|
-
}
|
|
3759
|
-
catch { /* best-effort */ }
|
|
3990
|
+
// what connectome-doctor reads. Legacy fields kept; `kind` is additive.
|
|
3991
|
+
this.logFailure({ agent: agentName, consecutive: streak, reason, kind: 'inference-exhausted' });
|
|
3760
3992
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
3761
3993
|
const agent = this.agents.get(agentName);
|
|
3762
3994
|
if (agent && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
@@ -3778,17 +4010,10 @@ export class AgentFramework {
|
|
|
3778
4010
|
if (streak >= this.inferenceFailureEscalationThreshold) {
|
|
3779
4011
|
console.error(`[inference-hard-down] agent=${agentName} has FAILED ${streak} consecutive ` +
|
|
3780
4012
|
`inferences — it cannot complete a turn. Last reason: ${reason}`);
|
|
3781
|
-
//
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
method: 'POST',
|
|
3786
|
-
headers: { 'content-type': 'application/json' },
|
|
3787
|
-
body: JSON.stringify({
|
|
3788
|
-
content: `\u{1F6A8} **${agentName}** hard-down: ${streak} consecutive inference failures.\nLast reason: ${reason.slice(0, 500)}`,
|
|
3789
|
-
}),
|
|
3790
|
-
}).catch((err) => console.error('[inference-hard-down] webhook post failed:', err instanceof Error ? err.message : err));
|
|
3791
|
-
}
|
|
4013
|
+
// Ops alert (webhook + ops:alert trace), throttled per (agent, kind) —
|
|
4014
|
+
// a hard-down agent re-posts every ~15 min, not on every failed retry.
|
|
4015
|
+
// failures.log already got the per-exhaustion record above.
|
|
4016
|
+
this.opsAlert('hard-down', agentName, `${streak} consecutive inference failures. Last reason: ${reason}`, { skipLog: true });
|
|
3792
4017
|
// (4) Poison-history breaker: ONLY for `invalid_request` — a 400-class
|
|
3793
4018
|
// rejection of the history itself. Deliberately NOT keyed on
|
|
3794
4019
|
// `retryable === false`, which membrane also returns for auth (expired
|
|
@@ -4233,6 +4458,13 @@ export class AgentFramework {
|
|
|
4233
4458
|
attempt: params.attempt,
|
|
4234
4459
|
willRetry: connection.willReconnect,
|
|
4235
4460
|
});
|
|
4461
|
+
// The reconnect loop never gives up (backoff caps at ~300s), so
|
|
4462
|
+
// "the server is effectively down" is an attempt-count judgment:
|
|
4463
|
+
// 5 failed attempts ≈ a few minutes of outage. Throttled per
|
|
4464
|
+
// (serverId, kind), so a long outage re-posts every ~15 min.
|
|
4465
|
+
if (params.attempt >= 5) {
|
|
4466
|
+
this.opsAlert('mcpl-down', connection.id, `MCPL server unreachable (attempt ${params.attempt}): ${params.error}`);
|
|
4467
|
+
}
|
|
4236
4468
|
});
|
|
4237
4469
|
// A late response to a tools/call that already timed out, carrying
|
|
4238
4470
|
// stateful data the server advanced to. The host can't re-inject it (the
|
|
@@ -4657,7 +4889,16 @@ export class AgentFramework {
|
|
|
4657
4889
|
}
|
|
4658
4890
|
console.error(`[sleep] agent=${agentName} seconds=${seconds} announce=${announce} until=${new Date(until).toISOString()}`);
|
|
4659
4891
|
// endTurn: the agent stops here and goes idle for the duration.
|
|
4660
|
-
finish({
|
|
4892
|
+
finish({
|
|
4893
|
+
success: true,
|
|
4894
|
+
data: {
|
|
4895
|
+
sleepingFor: human,
|
|
4896
|
+
until,
|
|
4897
|
+
untilLocal: formatZonedDateTime(until, this.timeZone),
|
|
4898
|
+
timeZone: this.timeZone,
|
|
4899
|
+
},
|
|
4900
|
+
endTurn: true,
|
|
4901
|
+
});
|
|
4661
4902
|
}
|
|
4662
4903
|
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
4663
4904
|
* server's declared tag ontology + gate.js status. */
|