@animalabs/agent-framework 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -15
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +69 -11
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +4 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +3 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +4 -3
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +304 -89
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +2577 -678
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +99 -3
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +538 -13
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +2 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -0
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +161 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +184 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +752 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +2 -1
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +26 -10
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +40 -69
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +50 -10
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +134 -65
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +47 -18
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -37
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +207 -108
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -11
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +141 -49
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +3 -1
- 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 +4 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +9 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +22 -11
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/workspace/answers.d.ts +22 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +123 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +57 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +20 -13
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +62 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +934 -531
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +75 -63
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +366 -180
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +26 -16
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +50 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +35 -1
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +40 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +15 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +5 -2
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +72 -11
- package/src/api/server.ts +4 -2
- package/src/auto-continue.ts +4 -3
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +2694 -700
- package/src/gate/event-gate.ts +579 -15
- package/src/gate/index.ts +8 -0
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +164 -1
- package/src/heartbeat/heartbeat.ts +804 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/index.ts +4 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +2 -1
- package/src/mcpl/inbound-source.ts +42 -69
- package/src/mcpl/server-connection.ts +56 -16
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +133 -67
- package/src/mcpl/tool-signature.ts +51 -20
- package/src/mcpl/toolset-host.ts +197 -107
- package/src/mcpl/toolsets-module.ts +143 -49
- package/src/mcpl/types.ts +3 -1
- package/src/module-registry.ts +8 -2
- package/src/modules/history/index.ts +22 -12
- package/src/modules/workspace/answers.ts +133 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +58 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +21 -12
- package/src/modules/workspace/index.ts +927 -541
- package/src/modules/workspace/observe.ts +381 -177
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +27 -15
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +62 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +34 -1
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +40 -9
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +5 -2
- package/src/types/trace.ts +36 -1
|
@@ -40,6 +40,15 @@ function parseGateJson(text) {
|
|
|
40
40
|
const MIN_DEBOUNCE_MS = 100;
|
|
41
41
|
/** Maximum debounce value (ms). */
|
|
42
42
|
const MAX_DEBOUNCE_MS = 300_000;
|
|
43
|
+
/** Maximum `judge.maxWaitMs` (ms): an hour. */
|
|
44
|
+
const MAX_JUDGE_WAIT_MS = 3_600_000;
|
|
45
|
+
/** Longest `judge.instructions` (characters) a rule may hold: they go to the
|
|
46
|
+
* judge with every batch, so they're kept short. */
|
|
47
|
+
const MAX_JUDGE_INSTRUCTIONS = 4_000;
|
|
48
|
+
/** How long a judge request may take by default (ms) before onError decides. */
|
|
49
|
+
const DEFAULT_JUDGE_TIMEOUT_MS = 20_000;
|
|
50
|
+
/** Judge failures in a row after which gate_status names the judge as failing. */
|
|
51
|
+
const JUDGE_FAILING_STREAK = 3;
|
|
43
52
|
/** Whether a check made at `last` is still fresh at `now`, less than the
|
|
44
53
|
* reload throttle ago. A clock stepped backwards since makes it stale, so
|
|
45
54
|
* the next check runs rather than wait for the clock to catch up. */
|
|
@@ -176,8 +185,22 @@ function formatBehavior(b) {
|
|
|
176
185
|
const k = b.passive_sample.keyBy ? `,keyBy:${b.passive_sample.keyBy}` : '';
|
|
177
186
|
return `passive_sample:${b.passive_sample.every}${k}`;
|
|
178
187
|
}
|
|
188
|
+
if ('judge' in b) {
|
|
189
|
+
const w = b.judge.maxWaitMs !== undefined ? `,maxWait:${b.judge.maxWaitMs}` : '';
|
|
190
|
+
return `judge:${b.judge.debounceMs}${w}`;
|
|
191
|
+
}
|
|
179
192
|
return 'unknown';
|
|
180
193
|
}
|
|
194
|
+
/** The quiet period (ms) of a batching behavior: a debounce's, or a judge's. */
|
|
195
|
+
function batchQuietMs(b) {
|
|
196
|
+
if (typeof b !== 'object')
|
|
197
|
+
return undefined;
|
|
198
|
+
if ('debounce' in b)
|
|
199
|
+
return b.debounce;
|
|
200
|
+
if ('judge' in b)
|
|
201
|
+
return b.judge.debounceMs;
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
181
204
|
/**
|
|
182
205
|
* Stable fingerprint for a GateBehavior, used during reload to detect
|
|
183
206
|
* config changes that invalidate per-policy state. JSON.stringify is
|
|
@@ -232,9 +255,10 @@ const MATCH_FIELDS = [
|
|
|
232
255
|
'scope', 'source', 'channel', 'mount', 'pathGlob', 'tagsAny', 'tagsAll', 'tagsNone', 'metadataTrue', 'filter',
|
|
233
256
|
];
|
|
234
257
|
const FILTER_FIELDS = ['type', 'pattern'];
|
|
235
|
-
const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample'];
|
|
258
|
+
const BEHAVIOR_KINDS = ['debounce', 'rate_limit', 'passive_sample', 'judge'];
|
|
236
259
|
const RATE_LIMIT_FIELDS = ['tokens', 'refillIntervalMs', 'keyBy'];
|
|
237
260
|
const PASSIVE_SAMPLE_FIELDS = ['every', 'keyBy'];
|
|
261
|
+
const JUDGE_FIELDS = ['debounceMs', 'maxWaitMs', 'instructions'];
|
|
238
262
|
function isPlainObject(value) {
|
|
239
263
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
240
264
|
}
|
|
@@ -410,6 +434,7 @@ function fieldLevels(file) {
|
|
|
410
434
|
{ name: '"behavior"', fields: BEHAVIOR_KINDS },
|
|
411
435
|
{ name: '"behavior.rate_limit"', fields: RATE_LIMIT_FIELDS },
|
|
412
436
|
{ name: '"behavior.passive_sample"', fields: PASSIVE_SAMPLE_FIELDS },
|
|
437
|
+
{ name: '"behavior.judge"', fields: JUDGE_FIELDS },
|
|
413
438
|
];
|
|
414
439
|
}
|
|
415
440
|
/** Where a name is a field the gate reads, at a level other than the one whose fields are `here`. */
|
|
@@ -589,7 +614,7 @@ function readRule(raw, unnamed, file = 'gate.json') {
|
|
|
589
614
|
const of = (fields) => m.keys.some((key) => fields.includes(key));
|
|
590
615
|
if (obj.match === undefined && (of(MATCH_FIELDS) || of(FILTER_FIELDS)))
|
|
591
616
|
return 'so the rule matches every event';
|
|
592
|
-
if (obj.behavior === undefined && (of(BEHAVIOR_KINDS) || of(RATE_LIMIT_FIELDS) || of(PASSIVE_SAMPLE_FIELDS))) {
|
|
617
|
+
if (obj.behavior === undefined && (of(BEHAVIOR_KINDS) || of(RATE_LIMIT_FIELDS) || of(PASSIVE_SAMPLE_FIELDS) || of(JUDGE_FIELDS))) {
|
|
593
618
|
return 'so the rule\'s behavior is "always"';
|
|
594
619
|
}
|
|
595
620
|
return undefined;
|
|
@@ -618,7 +643,7 @@ function readRule(raw, unnamed, file = 'gate.json') {
|
|
|
618
643
|
}
|
|
619
644
|
return reading;
|
|
620
645
|
}
|
|
621
|
-
const BEHAVIORS_NEEDED = '"always", "defer" or "skip", or an object holding one of "debounce", "rate_limit" and "
|
|
646
|
+
const BEHAVIORS_NEEDED = '"always", "defer" or "skip", or an object holding one of "debounce", "rate_limit", "passive_sample" and "judge"';
|
|
622
647
|
/**
|
|
623
648
|
* A rule's behavior: "always" when left out; one of the three names; or an
|
|
624
649
|
* object holding one of the timing kinds. Anything else but a list or an
|
|
@@ -626,7 +651,8 @@ const BEHAVIORS_NEEDED = '"always", "defer" or "skip", or an object holding one
|
|
|
626
651
|
* and is read so; a list, or an object holding none of the kinds, was
|
|
627
652
|
* refused, and the rule is left out. Of
|
|
628
653
|
* two kinds or more, "debounce", then "rate_limit", then "passive_sample" was
|
|
629
|
-
* the one read, whatever their order, and is read so
|
|
654
|
+
* the one read, whatever their order, and is read so; "judge", newer than
|
|
655
|
+
* those, is read only where none of them is given. Each other kind is read
|
|
630
656
|
* on its own as well, so that a refusal names its problems too, and a load
|
|
631
657
|
* reads past them, since it isn't read either way. `kind` is the behavior's
|
|
632
658
|
* kind even where it can't be read, for passthrough's check.
|
|
@@ -649,7 +675,7 @@ function readBehavior(r, value) {
|
|
|
649
675
|
const kinds = BEHAVIOR_KINDS.filter((k) => b[k] !== undefined);
|
|
650
676
|
const kind = kinds[0];
|
|
651
677
|
if (kind === undefined) {
|
|
652
|
-
r.problems.push({ text: 'behavior holds none of "debounce", "rate_limit" and "
|
|
678
|
+
r.problems.push({ text: 'behavior holds none of "debounce", "rate_limit", "passive_sample" and "judge"' });
|
|
653
679
|
return {};
|
|
654
680
|
}
|
|
655
681
|
if (kinds.length > 1) {
|
|
@@ -777,11 +803,109 @@ function readPassiveSample(r, value) {
|
|
|
777
803
|
out.keyBy = keyBy;
|
|
778
804
|
return { passive_sample: out };
|
|
779
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* A judge: a debounce's quiet period, read as `debounce` is, with at its
|
|
808
|
+
* nearest bound one out of range; an optional longest wait, from that
|
|
809
|
+
* quiet period to an hour, likewise; and the agent's optional instructions to
|
|
810
|
+
* the judge, text of at most MAX_JUDGE_INSTRUCTIONS characters. Earlier
|
|
811
|
+
* releases knew no judge, so nothing deployed has one: a rule whose judge
|
|
812
|
+
* can't be read is left out, and its instructions, which go to a model with
|
|
813
|
+
* every batch, are never cut to fit.
|
|
814
|
+
*/
|
|
815
|
+
function readJudge(r, value) {
|
|
816
|
+
if (!isPlainObject(value)) {
|
|
817
|
+
r.problems.push({
|
|
818
|
+
text: `behavior.judge is ${shownValue(value)}, where an object is needed, such as {"debounceMs": 30000, "instructions": "..."}`,
|
|
819
|
+
});
|
|
820
|
+
return undefined;
|
|
821
|
+
}
|
|
822
|
+
const conf = withoutEmpty(`${r.where}: behavior.judge`, value, r.notes, {
|
|
823
|
+
maxWaitMs: 'a batch is judged only after its quiet period',
|
|
824
|
+
instructions: 'the judge decides without the agent\'s word',
|
|
825
|
+
});
|
|
826
|
+
readFieldNames(r, 'behavior.judge', conf, JUDGE_FIELDS);
|
|
827
|
+
let ok = true;
|
|
828
|
+
const ms = conf.debounceMs;
|
|
829
|
+
let debounceMs;
|
|
830
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
|
|
831
|
+
r.problems.push({ text: `behavior.judge.debounceMs is ${shownValue(ms)}, where a number of milliseconds is needed, such as 30000` });
|
|
832
|
+
ok = false;
|
|
833
|
+
}
|
|
834
|
+
else if (ms < MIN_DEBOUNCE_MS) {
|
|
835
|
+
r.problems.push({
|
|
836
|
+
text: `behavior.judge.debounceMs is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
|
|
837
|
+
});
|
|
838
|
+
debounceMs = MIN_DEBOUNCE_MS;
|
|
839
|
+
}
|
|
840
|
+
else if (ms > MAX_DEBOUNCE_MS) {
|
|
841
|
+
r.problems.push({
|
|
842
|
+
text: `behavior.judge.debounceMs is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
|
|
843
|
+
});
|
|
844
|
+
debounceMs = MAX_DEBOUNCE_MS;
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
debounceMs = ms;
|
|
848
|
+
}
|
|
849
|
+
let maxWaitMs;
|
|
850
|
+
const wait = conf.maxWaitMs;
|
|
851
|
+
if (wait !== undefined) {
|
|
852
|
+
if (typeof wait !== 'number' || !Number.isFinite(wait)) {
|
|
853
|
+
r.problems.push({ text: `behavior.judge.maxWaitMs is ${shownValue(wait)}, where a number of milliseconds is needed, such as 120000` });
|
|
854
|
+
ok = false;
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
const floor = debounceMs ?? MIN_DEBOUNCE_MS;
|
|
858
|
+
if (wait < floor) {
|
|
859
|
+
r.problems.push({
|
|
860
|
+
text: `behavior.judge.maxWaitMs is ${wait} ms, under the rule's ${floor} ms quiet period`,
|
|
861
|
+
read: `so it's read as ${floor} ms`,
|
|
862
|
+
});
|
|
863
|
+
maxWaitMs = floor;
|
|
864
|
+
}
|
|
865
|
+
else if (wait > MAX_JUDGE_WAIT_MS) {
|
|
866
|
+
r.problems.push({
|
|
867
|
+
text: `behavior.judge.maxWaitMs is ${wait} ms, over the ${MAX_JUDGE_WAIT_MS} ms maximum`,
|
|
868
|
+
read: `so it's read as ${MAX_JUDGE_WAIT_MS} ms`,
|
|
869
|
+
});
|
|
870
|
+
maxWaitMs = MAX_JUDGE_WAIT_MS;
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
maxWaitMs = wait;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const said = conf.instructions;
|
|
878
|
+
let instructions;
|
|
879
|
+
if (said !== undefined) {
|
|
880
|
+
if (typeof said !== 'string') {
|
|
881
|
+
r.problems.push({ text: `behavior.judge.instructions is ${shownValue(said)}, where text is needed` });
|
|
882
|
+
ok = false;
|
|
883
|
+
}
|
|
884
|
+
else if (said.length > MAX_JUDGE_INSTRUCTIONS) {
|
|
885
|
+
r.problems.push({
|
|
886
|
+
text: `behavior.judge.instructions is ${said.length} characters long, over the ${MAX_JUDGE_INSTRUCTIONS}-character maximum`,
|
|
887
|
+
});
|
|
888
|
+
ok = false;
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
instructions = said;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (!ok || debounceMs === undefined)
|
|
895
|
+
return undefined;
|
|
896
|
+
const out = { debounceMs };
|
|
897
|
+
if (maxWaitMs !== undefined)
|
|
898
|
+
out.maxWaitMs = maxWaitMs;
|
|
899
|
+
if (instructions !== undefined)
|
|
900
|
+
out.instructions = instructions;
|
|
901
|
+
return { judge: out };
|
|
902
|
+
}
|
|
780
903
|
/** How each kind of behavior object is read. */
|
|
781
904
|
const READ_KIND = {
|
|
782
905
|
debounce: readDebounce,
|
|
783
906
|
rate_limit: readRateLimit,
|
|
784
907
|
passive_sample: readPassiveSample,
|
|
908
|
+
judge: readJudge,
|
|
785
909
|
};
|
|
786
910
|
/** A timing kind's keyBy: a field name. One of another type was read as none. */
|
|
787
911
|
function readKeyBy(r, kind, value) {
|
|
@@ -1521,6 +1645,27 @@ export class EventGate {
|
|
|
1521
1645
|
startUnsavedMerged = false;
|
|
1522
1646
|
// Debounce state
|
|
1523
1647
|
debounceTimers = new Map();
|
|
1648
|
+
// Reply judge state (`judge` rules). The judge itself, when the host wires
|
|
1649
|
+
// one; without it a judge rule's batch is delivered as a debounce's.
|
|
1650
|
+
judgeFn;
|
|
1651
|
+
judgeTimeoutMs;
|
|
1652
|
+
judgeOnError;
|
|
1653
|
+
/** Per rule, the batch the judge is deciding now. */
|
|
1654
|
+
judging = new Map();
|
|
1655
|
+
/** Per rule, a batch that fired while a turn changing the judged context ran or the host was
|
|
1656
|
+
* quiesced, held to be judged once both are clear: the judge reads the
|
|
1657
|
+
* context the turn leaves, which may already answer it. */
|
|
1658
|
+
judgeHeld = new Map();
|
|
1659
|
+
judgeRecords = new Map();
|
|
1660
|
+
changesJudgedContextFn;
|
|
1661
|
+
/** Lifetime count of events dropped from batches waiting on the judge, at
|
|
1662
|
+
* the inference buffer's cap (MAX_INFERENCE_BUFFER per rule). */
|
|
1663
|
+
droppedJudgeEvents = 0;
|
|
1664
|
+
/** Judge requests failed in a row, and the newest failure's reason: a host
|
|
1665
|
+
* whose provider can't serve the judge's model fails every time, which
|
|
1666
|
+
* gate_status says once it's a streak (judgeFailingNotice). */
|
|
1667
|
+
judgeFailStreak = 0;
|
|
1668
|
+
judgeLastFailure = '';
|
|
1524
1669
|
// Rate-limit state — policy name → key → bucket.
|
|
1525
1670
|
rateLimitBuckets = new Map();
|
|
1526
1671
|
// Passive-sample state — policy name → key → counter (count since last fire).
|
|
@@ -1565,6 +1710,10 @@ export class EventGate {
|
|
|
1565
1710
|
// self-wake (an external wake supersedes it; the timer would otherwise
|
|
1566
1711
|
// land as a redundant empty wake right after the turn).
|
|
1567
1712
|
selfWakeTimers = new Map();
|
|
1713
|
+
/** When each agent's pending self-wake fires, and what armed it
|
|
1714
|
+
* (upcomingWakes). Read only while its timer is in selfWakeTimers, so an
|
|
1715
|
+
* entry a fire or a cancel left behind says nothing. */
|
|
1716
|
+
selfWakeDue = new Map();
|
|
1568
1717
|
// Privileged users who may wake the agent through sleep. Hot-reloaded from
|
|
1569
1718
|
// privilegedUsersPath on change.
|
|
1570
1719
|
privilegedUsersPath;
|
|
@@ -1595,6 +1744,12 @@ export class EventGate {
|
|
|
1595
1744
|
this.channelShownFn = opts.channelShown;
|
|
1596
1745
|
this.isNewestEntryFn = opts.isNewestEntry;
|
|
1597
1746
|
this.now = opts.now ?? (() => Date.now());
|
|
1747
|
+
this.judgeFn = opts.judge;
|
|
1748
|
+
this.changesJudgedContextFn = opts.changesJudgedContext;
|
|
1749
|
+
this.judgeTimeoutMs = opts.judgeTimeoutMs !== undefined && Number.isFinite(opts.judgeTimeoutMs) && opts.judgeTimeoutMs > 0
|
|
1750
|
+
? Math.min(opts.judgeTimeoutMs, MAX_TIMER_MS)
|
|
1751
|
+
: DEFAULT_JUDGE_TIMEOUT_MS;
|
|
1752
|
+
this.judgeOnError = opts.judgeOnError === 'skip' ? 'skip' : 'wake';
|
|
1598
1753
|
this.loadPrivileged();
|
|
1599
1754
|
// Seed or reconcile the on-disk config from the host's starting rules
|
|
1600
1755
|
// (`initialConfig`), which are read as gate.json is, rule by rule: a rule
|
|
@@ -2110,6 +2265,7 @@ export class EventGate {
|
|
|
2110
2265
|
for (const name of removed) {
|
|
2111
2266
|
this.clearPolicyState(name);
|
|
2112
2267
|
this.stats.delete(name);
|
|
2268
|
+
this.judgeRecords.delete(name);
|
|
2113
2269
|
}
|
|
2114
2270
|
const cleared = [...after.keys()].filter((name) => before.has(name) && before.get(name) !== after.get(name));
|
|
2115
2271
|
for (const name of cleared)
|
|
@@ -2568,7 +2724,7 @@ export class EventGate {
|
|
|
2568
2724
|
return { trigger: true, policyName: policy.name, behavior: 'always' };
|
|
2569
2725
|
}
|
|
2570
2726
|
if (typeof policy.behavior === 'object') {
|
|
2571
|
-
if ('debounce' in policy.behavior) {
|
|
2727
|
+
if ('debounce' in policy.behavior || 'judge' in policy.behavior) {
|
|
2572
2728
|
this.handleDebounce(policy, info);
|
|
2573
2729
|
return { trigger: false, policyName: policy.name, behavior: policy.behavior };
|
|
2574
2730
|
}
|
|
@@ -2709,7 +2865,7 @@ export class EventGate {
|
|
|
2709
2865
|
if (b === 'defer' || b === 'skip')
|
|
2710
2866
|
return false;
|
|
2711
2867
|
if (typeof b === 'object') {
|
|
2712
|
-
if ('debounce' in b)
|
|
2868
|
+
if ('debounce' in b || 'judge' in b)
|
|
2713
2869
|
return false; // batches; never triggers inline
|
|
2714
2870
|
if ('rate_limit' in b) {
|
|
2715
2871
|
const cfg = b.rate_limit;
|
|
@@ -2889,7 +3045,8 @@ export class EventGate {
|
|
|
2889
3045
|
}
|
|
2890
3046
|
}
|
|
2891
3047
|
handleDebounce(policy, info) {
|
|
2892
|
-
const debounceMs = policy.behavior
|
|
3048
|
+
const debounceMs = batchQuietMs(policy.behavior) ?? MIN_DEBOUNCE_MS;
|
|
3049
|
+
const judge = typeof policy.behavior === 'object' && 'judge' in policy.behavior ? policy.behavior.judge : undefined;
|
|
2893
3050
|
const event = {
|
|
2894
3051
|
policyName: policy.name,
|
|
2895
3052
|
content: info.content.length > MAX_CONTENT_SNIPPET
|
|
@@ -2910,14 +3067,24 @@ export class EventGate {
|
|
|
2910
3067
|
...(typeof info.metadata?.eventId === 'string' ? { eventId: info.metadata.eventId } : {}),
|
|
2911
3068
|
};
|
|
2912
3069
|
const existing = this.debounceTimers.get(policy.name);
|
|
3070
|
+
// A judge's maxWaitMs caps how far new events push the batch back: it's
|
|
3071
|
+
// judged by then, from its first event, quiet or not.
|
|
3072
|
+
const delay = (firstAt, batchJudge) => {
|
|
3073
|
+
const cap = batchJudge?.maxWaitMs;
|
|
3074
|
+
return cap === undefined ? debounceMs : Math.max(0, Math.min(debounceMs, firstAt + cap - this.now()));
|
|
3075
|
+
};
|
|
2913
3076
|
if (existing) {
|
|
2914
3077
|
clearTimeout(existing.timer);
|
|
2915
3078
|
existing.events.push(event);
|
|
2916
|
-
|
|
3079
|
+
const ms = delay(existing.firstAt, existing.judge);
|
|
3080
|
+
existing.timer = setTimeout(() => this.fireDebounce(policy.name), ms);
|
|
3081
|
+
existing.dueAt = this.now() + ms;
|
|
2917
3082
|
}
|
|
2918
3083
|
else {
|
|
2919
|
-
const
|
|
2920
|
-
|
|
3084
|
+
const firstAt = this.now();
|
|
3085
|
+
const ms = delay(firstAt, judge);
|
|
3086
|
+
const timer = setTimeout(() => this.fireDebounce(policy.name), ms);
|
|
3087
|
+
this.debounceTimers.set(policy.name, { timer, events: [event], firstAt, dueAt: firstAt + ms, ...(judge ? { judge } : {}) });
|
|
2921
3088
|
}
|
|
2922
3089
|
}
|
|
2923
3090
|
fireDebounce(policyName) {
|
|
@@ -2928,6 +3095,13 @@ export class EventGate {
|
|
|
2928
3095
|
}
|
|
2929
3096
|
const events = state.events;
|
|
2930
3097
|
this.debounceTimers.delete(policyName);
|
|
3098
|
+
// A judge rule's batch goes to the reply judge, which delivers it, or
|
|
3099
|
+
// doesn't, once it has decided (judgeBatch). Without a judge wired it's
|
|
3100
|
+
// delivered below, as a debounce's batch is.
|
|
3101
|
+
if (state.judge && this.judgeFn) {
|
|
3102
|
+
this.judgeBatch(policyName, events, state.judge);
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
2931
3105
|
// If any agent is currently inferring — or the host is quiesced — buffer
|
|
2932
3106
|
// for later (with cap). While quiesced, delivering would write a
|
|
2933
3107
|
// "[Gate: N events]" message into the very context being operated on
|
|
@@ -3060,10 +3234,299 @@ export class EventGate {
|
|
|
3060
3234
|
}
|
|
3061
3235
|
}
|
|
3062
3236
|
// =========================================================================
|
|
3237
|
+
// Reply judge (`judge` rules)
|
|
3238
|
+
// =========================================================================
|
|
3239
|
+
/**
|
|
3240
|
+
* Send one of a judge rule's batches to the reply judge. While a resident's
|
|
3241
|
+
* turn runs or the host is quiesced the batch is held instead, merged with
|
|
3242
|
+
* any the rule already holds, and judged once both are clear
|
|
3243
|
+
* (flushJudgeHeldIfClear): the judge then reads the context the turn left,
|
|
3244
|
+
* which may already have answered it. While the judge decides one of the
|
|
3245
|
+
* rule's batches, a later one waits for that verdict (finishJudging), so the
|
|
3246
|
+
* judge never decides two of a rule's batches at once.
|
|
3247
|
+
*/
|
|
3248
|
+
judgeBatch(policyName, events, judge) {
|
|
3249
|
+
if (this.disposed)
|
|
3250
|
+
return;
|
|
3251
|
+
const deciding = this.judging.get(policyName);
|
|
3252
|
+
if (deciding) {
|
|
3253
|
+
deciding.queued.push(...events);
|
|
3254
|
+
this.capJudgeEvents(deciding.queued);
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
if (this.judgedTurnRunning() || this.quiesced) {
|
|
3258
|
+
this.holdForJudge(policyName, events, judge);
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
const fn = this.judgeFn;
|
|
3262
|
+
if (!fn) {
|
|
3263
|
+
this.deliverOrBuffer(events);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
const abort = new AbortController();
|
|
3267
|
+
const agentNames = this.getAgentNamesFn();
|
|
3268
|
+
const state = { events, judge, started: Date.now(), abort, queued: [] };
|
|
3269
|
+
this.judging.set(policyName, state);
|
|
3270
|
+
const timedOut = new Promise((_, reject) => {
|
|
3271
|
+
state.timer = setTimeout(() => {
|
|
3272
|
+
abort.abort();
|
|
3273
|
+
reject(new Error(`the judge took more than ${this.judgeTimeoutMs} ms`));
|
|
3274
|
+
}, this.judgeTimeoutMs);
|
|
3275
|
+
// Never keep the process alive on the judge's account.
|
|
3276
|
+
state.timer.unref?.();
|
|
3277
|
+
});
|
|
3278
|
+
let asked;
|
|
3279
|
+
try {
|
|
3280
|
+
asked = fn({
|
|
3281
|
+
policyName,
|
|
3282
|
+
...(judge.instructions !== undefined ? { instructions: judge.instructions } : {}),
|
|
3283
|
+
events: events.map((e) => this.judgeEventSummary(e)),
|
|
3284
|
+
agentNames,
|
|
3285
|
+
upcomingWakes: this.upcomingWakes(agentNames, policyName),
|
|
3286
|
+
signal: abort.signal,
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
catch (err) {
|
|
3290
|
+
asked = Promise.reject(err);
|
|
3291
|
+
}
|
|
3292
|
+
Promise.race([asked, timedOut]).then((verdict) => this.finishJudging(policyName, state, verdict), (err) => this.finishJudging(policyName, state, undefined, err)).finally(() => {
|
|
3293
|
+
if (state.timer)
|
|
3294
|
+
clearTimeout(state.timer);
|
|
3295
|
+
}).catch((err) => {
|
|
3296
|
+
console.error(`[gate] Policy "${policyName}": the judge's verdict couldn't be applied (${errorText(err)})`);
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
/** Hold a batch for the judge until no turn changing the context it reads
|
|
3300
|
+
* runs and the host isn't quiesced, merged with what the rule already holds. */
|
|
3301
|
+
holdForJudge(policyName, events, judge) {
|
|
3302
|
+
const held = this.judgeHeld.get(policyName);
|
|
3303
|
+
if (held) {
|
|
3304
|
+
held.events.push(...events);
|
|
3305
|
+
held.judge = judge;
|
|
3306
|
+
this.capJudgeEvents(held.events);
|
|
3307
|
+
}
|
|
3308
|
+
else {
|
|
3309
|
+
const fresh = [...events];
|
|
3310
|
+
this.capJudgeEvents(fresh);
|
|
3311
|
+
this.judgeHeld.set(policyName, { events: fresh, judge });
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Keep a rule's events waiting on the judge to the inference buffer's cap,
|
|
3316
|
+
* dropping the oldest, counted and logged as bufferForInference counts its
|
|
3317
|
+
* drops: a turn that never ends (the wake-wedge) or a long quiesce on a busy
|
|
3318
|
+
* channel would otherwise grow them without bound. A queue beside a batch
|
|
3319
|
+
* the judge is deciding is capped on its own, so its newest events are never
|
|
3320
|
+
* the ones dropped for the batch ahead of it.
|
|
3321
|
+
*/
|
|
3322
|
+
capJudgeEvents(events) {
|
|
3323
|
+
const drop = events.length - MAX_INFERENCE_BUFFER;
|
|
3324
|
+
if (drop <= 0)
|
|
3325
|
+
return;
|
|
3326
|
+
events.splice(0, drop);
|
|
3327
|
+
const before = this.droppedJudgeEvents;
|
|
3328
|
+
this.droppedJudgeEvents += drop;
|
|
3329
|
+
if (Math.floor(before / 50) !== Math.floor(this.droppedJudgeEvents / 50) || before === 0) {
|
|
3330
|
+
console.error(`[gate] events waiting on the reply judge reached the cap (${MAX_INFERENCE_BUFFER} per rule): dropped `
|
|
3331
|
+
+ `${this.droppedJudgeEvents} oldest so far${this.quiesced ? ' during quiesce' : ''}`);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
/** The rule's judge as it is now, when the rule is still a judge rule; else the one the batch was asked with. */
|
|
3335
|
+
currentJudge(policyName, asked) {
|
|
3336
|
+
const now = this.config.policies.find((p) => p.name === policyName)?.behavior;
|
|
3337
|
+
return typeof now === 'object' && 'judge' in now ? now.judge : asked;
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* The gate's own timers that will wake `agentNames` with no new event, for
|
|
3341
|
+
* the judge to weigh what waiting costs: each agent's pending self-wake
|
|
3342
|
+
* (skip_reply's wake_in_seconds), the end of a sleep, and the pending
|
|
3343
|
+
* batches of rules other than `exceptPolicy` (a debounce's wakes the agent
|
|
3344
|
+
* when it fires; a judge rule's goes to the judge, so it only may).
|
|
3345
|
+
*/
|
|
3346
|
+
upcomingWakes(agentNames, exceptPolicy) {
|
|
3347
|
+
const wakes = [];
|
|
3348
|
+
const now = this.now();
|
|
3349
|
+
for (const name of agentNames) {
|
|
3350
|
+
const due = this.selfWakeDue.get(name);
|
|
3351
|
+
if (due && this.selfWakeTimers.has(name)) {
|
|
3352
|
+
wakes.push({ at: due.at, certain: true, what: `${agentNames.length > 1 ? `${name}'s` : 'its'} own ${due.source} timer` });
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
if (this.sleepUntil > now && (this.sleepAgent === undefined || agentNames.includes(this.sleepAgent))) {
|
|
3356
|
+
wakes.push({ at: this.sleepUntil, certain: true, what: 'the end of its sleep' });
|
|
3357
|
+
}
|
|
3358
|
+
for (const [name, state] of this.debounceTimers) {
|
|
3359
|
+
if (name === exceptPolicy || state.events.length === 0)
|
|
3360
|
+
continue;
|
|
3361
|
+
const n = state.events.length;
|
|
3362
|
+
const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
|
|
3363
|
+
wakes.push(state.judge
|
|
3364
|
+
? { at: state.dueAt, certain: false, what: `rule "${name}"'s batch of ${batch} goes to the judge` }
|
|
3365
|
+
: { at: state.dueAt, certain: true, what: `rule "${name}"'s debounced batch of ${batch}` });
|
|
3366
|
+
}
|
|
3367
|
+
return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
3368
|
+
}
|
|
3369
|
+
/** A batch's event as the judge is shown it. */
|
|
3370
|
+
judgeEventSummary(e) {
|
|
3371
|
+
const id = e.routeChannelId ?? e.channelId;
|
|
3372
|
+
let channel;
|
|
3373
|
+
if (id !== undefined) {
|
|
3374
|
+
let name = id;
|
|
3375
|
+
if (e.routeChannelId !== undefined && e.serverId !== undefined && this.channelShownFn) {
|
|
3376
|
+
try {
|
|
3377
|
+
name = this.channelShownFn({ serverId: e.serverId, channelId: e.routeChannelId });
|
|
3378
|
+
}
|
|
3379
|
+
catch {
|
|
3380
|
+
name = id;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
channel = e.channelLabel ? `${e.channelLabel.startsWith('#') ? e.channelLabel : `#${e.channelLabel}`} (${name})` : name;
|
|
3384
|
+
}
|
|
3385
|
+
return {
|
|
3386
|
+
eventType: e.eventType,
|
|
3387
|
+
...(channel !== undefined ? { channel } : {}),
|
|
3388
|
+
...(e.authorId !== undefined ? { authorId: e.authorId } : {}),
|
|
3389
|
+
addressed: e.addressed,
|
|
3390
|
+
content: e.content,
|
|
3391
|
+
at: e.timestamp,
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Apply the judge's verdict on a batch. Reply: the batch, with any of the
|
|
3396
|
+
* rule's batches that fired meanwhile, is delivered as a debounce's is
|
|
3397
|
+
* (held for a turn that started meanwhile, as fireDebounce holds one). Wait:
|
|
3398
|
+
* nothing is delivered, and batches that fired meanwhile go to the judge
|
|
3399
|
+
* afresh. A judge that failed, timed out or answered nothing usable decides
|
|
3400
|
+
* by onError: "wake" delivers, "skip" doesn't. Each verdict is traced and
|
|
3401
|
+
* counted for gate_status, and a failure is logged.
|
|
3402
|
+
*/
|
|
3403
|
+
finishJudging(policyName, state, verdict, err) {
|
|
3404
|
+
// A verdict for a batch the gate let go of (dispose() clears what the
|
|
3405
|
+
// judge was deciding) applies to nothing, even after a start() since.
|
|
3406
|
+
if (this.judging.get(policyName) !== state)
|
|
3407
|
+
return;
|
|
3408
|
+
this.judging.delete(policyName);
|
|
3409
|
+
if (state.timer)
|
|
3410
|
+
clearTimeout(state.timer);
|
|
3411
|
+
const durationMs = Date.now() - state.started;
|
|
3412
|
+
const failed = verdict == null || typeof verdict !== 'object' || typeof verdict.reply !== 'boolean';
|
|
3413
|
+
if (failed && err?.providerAdmission === 'deferred') {
|
|
3414
|
+
const wakes = this.judgeOnError === 'wake';
|
|
3415
|
+
console.error(`[gate] Policy "${policyName}": the reply judge made no call (${errorText(err)}), so its batch of ${state.events.length} `
|
|
3416
|
+
+ `${state.events.length === 1 ? 'event' : 'events'} ${wakes ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`);
|
|
3417
|
+
if (this.disposed)
|
|
3418
|
+
return;
|
|
3419
|
+
if (wakes)
|
|
3420
|
+
this.deliverOrBuffer([...state.events, ...state.queued]);
|
|
3421
|
+
else if (state.queued.length > 0)
|
|
3422
|
+
this.judgeBatch(policyName, state.queued, this.currentJudge(policyName, state.judge));
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
const outcome = failed ? 'error' : verdict.reply ? 'reply' : 'wait';
|
|
3426
|
+
const reason = failed
|
|
3427
|
+
? (err !== undefined ? errorText(err) : 'the judge gave no verdict')
|
|
3428
|
+
: typeof verdict.reason === 'string' ? verdict.reason : '';
|
|
3429
|
+
const record = this.judgeRecords.get(policyName) ?? { verdicts: { reply: 0, wait: 0, error: 0 } };
|
|
3430
|
+
record.verdicts[outcome]++;
|
|
3431
|
+
record.last = {
|
|
3432
|
+
at: Date.now(),
|
|
3433
|
+
verdict: outcome,
|
|
3434
|
+
reason: safeSlice(reason, 0, 300),
|
|
3435
|
+
eventCount: state.events.length,
|
|
3436
|
+
durationMs,
|
|
3437
|
+
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
3438
|
+
};
|
|
3439
|
+
if (this.config.policies.some((p) => p.name === policyName))
|
|
3440
|
+
this.judgeRecords.set(policyName, record);
|
|
3441
|
+
if (failed) {
|
|
3442
|
+
this.judgeFailStreak++;
|
|
3443
|
+
this.judgeLastFailure = safeSlice(reason, 0, 200);
|
|
3444
|
+
}
|
|
3445
|
+
else {
|
|
3446
|
+
this.judgeFailStreak = 0;
|
|
3447
|
+
}
|
|
3448
|
+
const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
|
|
3449
|
+
if (failed) {
|
|
3450
|
+
console.error(`[gate] Policy "${policyName}": the reply judge failed (${reason}), so its batch of ${state.events.length} `
|
|
3451
|
+
+ `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`);
|
|
3452
|
+
}
|
|
3453
|
+
this.emitTrace({
|
|
3454
|
+
type: 'gate:judge-verdict',
|
|
3455
|
+
policyName,
|
|
3456
|
+
verdict: outcome,
|
|
3457
|
+
woke: wake,
|
|
3458
|
+
eventCount: state.events.length,
|
|
3459
|
+
durationMs,
|
|
3460
|
+
...(!failed && verdict.model ? { model: verdict.model } : {}),
|
|
3461
|
+
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
3462
|
+
...(!failed && typeof verdict.cacheReadTokens === 'number' ? { cacheReadTokens: verdict.cacheReadTokens } : {}),
|
|
3463
|
+
...(!failed && typeof verdict.outputTokens === 'number' ? { outputTokens: verdict.outputTokens } : {}),
|
|
3464
|
+
timestamp: Date.now(),
|
|
3465
|
+
});
|
|
3466
|
+
if (this.disposed)
|
|
3467
|
+
return;
|
|
3468
|
+
const judge = this.currentJudge(policyName, state.judge);
|
|
3469
|
+
if (wake) {
|
|
3470
|
+
this.deliverOrBuffer([...state.events, ...state.queued]);
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
if (state.queued.length > 0)
|
|
3474
|
+
this.judgeBatch(policyName, state.queued, judge);
|
|
3475
|
+
}
|
|
3476
|
+
/** Deliver a batch now, or hold it for the agents' turns and the host's
|
|
3477
|
+
* quiesce to end, as fireDebounce does; a delivery that throws drops it,
|
|
3478
|
+
* named in the log. */
|
|
3479
|
+
deliverOrBuffer(events) {
|
|
3480
|
+
if (events.length === 0)
|
|
3481
|
+
return;
|
|
3482
|
+
if (this.inferring.size > 0 || this.quiesced) {
|
|
3483
|
+
this.bufferForInference(events);
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
try {
|
|
3487
|
+
this.deliverEvents(events);
|
|
3488
|
+
}
|
|
3489
|
+
catch (err) {
|
|
3490
|
+
const one = events.length === 1;
|
|
3491
|
+
console.error(`[gate] a judged batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
|
|
3492
|
+
+ `(${errorText(err)}), so the batch is dropped`);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
changesJudgedContext(agentName) {
|
|
3496
|
+
return this.changesJudgedContextFn?.(agentName) ?? true;
|
|
3497
|
+
}
|
|
3498
|
+
judgedTurnRunning() {
|
|
3499
|
+
return [...this.inferring].some((name) => this.changesJudgedContext(name));
|
|
3500
|
+
}
|
|
3501
|
+
stopJudging() {
|
|
3502
|
+
for (const [policyName, state] of [...this.judging]) {
|
|
3503
|
+
this.judging.delete(policyName);
|
|
3504
|
+
if (state.timer)
|
|
3505
|
+
clearTimeout(state.timer);
|
|
3506
|
+
state.abort.abort();
|
|
3507
|
+
this.holdForJudge(policyName, [...state.events, ...state.queued], state.judge);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
/** Send the batches held for the judge once no turn changing the context it reads runs and the host isn't quiesced. */
|
|
3511
|
+
flushJudgeHeldIfClear() {
|
|
3512
|
+
if (this.quiesced || this.judgedTurnRunning() || this.judgeHeld.size === 0)
|
|
3513
|
+
return;
|
|
3514
|
+
const held = [...this.judgeHeld];
|
|
3515
|
+
this.judgeHeld.clear();
|
|
3516
|
+
// Deferred, as the inference buffer's flush is, to stay out of the
|
|
3517
|
+
// turn-end callback that called this.
|
|
3518
|
+
queueMicrotask(() => {
|
|
3519
|
+
for (const [policyName, { events, judge }] of held)
|
|
3520
|
+
this.judgeBatch(policyName, events, this.currentJudge(policyName, judge));
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
// =========================================================================
|
|
3063
3524
|
// Inference lifecycle
|
|
3064
3525
|
// =========================================================================
|
|
3065
3526
|
onInferenceStarted(agentName) {
|
|
3066
3527
|
this.inferring.add(agentName);
|
|
3528
|
+
if (this.changesJudgedContext(agentName))
|
|
3529
|
+
this.stopJudging();
|
|
3067
3530
|
// A turn is starting — whatever caused it supersedes any pending
|
|
3068
3531
|
// self-wake. (Arming happens MID-turn via skip_reply, i.e. after this
|
|
3069
3532
|
// ran for the arming turn, so a fresh arm always survives its own turn.)
|
|
@@ -3082,10 +3545,13 @@ export class EventGate {
|
|
|
3082
3545
|
* still running. */
|
|
3083
3546
|
onInferenceResumed(agentName) {
|
|
3084
3547
|
this.inferring.add(agentName);
|
|
3548
|
+
if (this.changesJudgedContext(agentName))
|
|
3549
|
+
this.stopJudging();
|
|
3085
3550
|
}
|
|
3086
3551
|
onInferenceEnded(agentName) {
|
|
3087
3552
|
this.inferring.delete(agentName);
|
|
3088
3553
|
this.flushInferenceBufferIfClear();
|
|
3554
|
+
this.flushJudgeHeldIfClear();
|
|
3089
3555
|
}
|
|
3090
3556
|
/**
|
|
3091
3557
|
* Host quiesce suppression (issue #122). While set, debounce firings buffer
|
|
@@ -3100,8 +3566,16 @@ export class EventGate {
|
|
|
3100
3566
|
if (this.quiesced === quiesced)
|
|
3101
3567
|
return;
|
|
3102
3568
|
this.quiesced = quiesced;
|
|
3103
|
-
if (
|
|
3569
|
+
if (quiesced) {
|
|
3570
|
+
// A judge reading the context now would read it mid-surgery, and call
|
|
3571
|
+
// its provider while the host holds still: it's stopped, and its batch
|
|
3572
|
+
// held to be judged at resume.
|
|
3573
|
+
this.stopJudging();
|
|
3574
|
+
}
|
|
3575
|
+
else {
|
|
3104
3576
|
this.flushInferenceBufferIfClear();
|
|
3577
|
+
this.flushJudgeHeldIfClear();
|
|
3578
|
+
}
|
|
3105
3579
|
}
|
|
3106
3580
|
flushInferenceBufferIfClear() {
|
|
3107
3581
|
if (this.quiesced)
|
|
@@ -3128,11 +3602,16 @@ export class EventGate {
|
|
|
3128
3602
|
* is the signature of the wake-wedge: an agent stuck mid-"inference" so all
|
|
3129
3603
|
* incoming events buffer and never trigger. */
|
|
3130
3604
|
inferenceDiagnostics() {
|
|
3605
|
+
let judgeHeldEvents = 0;
|
|
3606
|
+
for (const held of this.judgeHeld.values())
|
|
3607
|
+
judgeHeldEvents += held.events.length;
|
|
3131
3608
|
return {
|
|
3132
3609
|
inferring: [...this.inferring],
|
|
3133
3610
|
bufferedEvents: this.inferenceBuffer.length,
|
|
3134
3611
|
droppedBufferedEvents: this.droppedBufferedEvents,
|
|
3135
3612
|
sleepUntil: this.sleepUntil,
|
|
3613
|
+
judgeHeldEvents,
|
|
3614
|
+
droppedJudgeEvents: this.droppedJudgeEvents,
|
|
3136
3615
|
};
|
|
3137
3616
|
}
|
|
3138
3617
|
// =========================================================================
|
|
@@ -3278,6 +3757,7 @@ export class EventGate {
|
|
|
3278
3757
|
this.requestInferenceFn(agentName, `self-scheduled wake (${source}, ${Math.round(ms / 1000)}s)`, 'self-wake', undefined, placement);
|
|
3279
3758
|
}, ms);
|
|
3280
3759
|
this.selfWakeTimers.set(agentName, timer);
|
|
3760
|
+
this.selfWakeDue.set(agentName, { at: this.now() + ms, source });
|
|
3281
3761
|
this.emitTrace({
|
|
3282
3762
|
type: 'gate:decision',
|
|
3283
3763
|
eventType: 'selfwake:set',
|
|
@@ -3489,6 +3969,16 @@ export class EventGate {
|
|
|
3489
3969
|
fireCount: this.passiveSampleFires.get(p.name) ?? 0,
|
|
3490
3970
|
};
|
|
3491
3971
|
}
|
|
3972
|
+
if (typeof p.behavior === 'object' && 'judge' in p.behavior) {
|
|
3973
|
+
const record = this.judgeRecords.get(p.name);
|
|
3974
|
+
result.judgeState = {
|
|
3975
|
+
pendingCount: debounce?.events.length ?? 0,
|
|
3976
|
+
judging: this.judging.has(p.name),
|
|
3977
|
+
heldCount: this.judgeHeld.get(p.name)?.events.length ?? 0,
|
|
3978
|
+
verdicts: { ...(record?.verdicts ?? { reply: 0, wait: 0, error: 0 }) },
|
|
3979
|
+
...(record?.last ? { last: { ...record.last } } : {}),
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3492
3982
|
return result;
|
|
3493
3983
|
});
|
|
3494
3984
|
const byEventType = {};
|
|
@@ -3513,7 +4003,10 @@ export class EventGate {
|
|
|
3513
4003
|
default: this.config.default ?? 'always',
|
|
3514
4004
|
script: this.gateScript.status(),
|
|
3515
4005
|
policies,
|
|
3516
|
-
errors: [
|
|
4006
|
+
errors: [
|
|
4007
|
+
this.configErrorShown(), this.startNotice, this.startUnsaved, this.privilegedError,
|
|
4008
|
+
this.judgeUnwiredNotice(), this.judgeFailingNotice(),
|
|
4009
|
+
]
|
|
3517
4010
|
.filter((e) => e !== null),
|
|
3518
4011
|
shadowWarnings: this.shadowWarningsCache.map(formatShadowWarning),
|
|
3519
4012
|
totalEvaluations: this.totalEvaluations,
|
|
@@ -3524,6 +4017,28 @@ export class EventGate {
|
|
|
3524
4017
|
},
|
|
3525
4018
|
};
|
|
3526
4019
|
}
|
|
4020
|
+
/** gate_status's word on judge rules in force with no judge wired. */
|
|
4021
|
+
judgeUnwiredNotice() {
|
|
4022
|
+
if (this.judgeFn)
|
|
4023
|
+
return null;
|
|
4024
|
+
const names = this.config.policies
|
|
4025
|
+
.filter((p) => typeof p.behavior === 'object' && 'judge' in p.behavior)
|
|
4026
|
+
.map((p) => p.name);
|
|
4027
|
+
if (names.length === 0)
|
|
4028
|
+
return null;
|
|
4029
|
+
return `${quotedList(names)} ${names.length === 1 ? 'is a judge rule' : 'are judge rules'}, and this host wires no reply `
|
|
4030
|
+
+ `judge, so ${names.length === 1 ? 'its batches wake' : 'their batches wake'} the agent as a debounce of the same quiet period would`;
|
|
4031
|
+
}
|
|
4032
|
+
/** gate_status's word on a judge that keeps failing (JUDGE_FAILING_STREAK in a row). */
|
|
4033
|
+
judgeFailingNotice() {
|
|
4034
|
+
if (this.judgeFailStreak < JUDGE_FAILING_STREAK)
|
|
4035
|
+
return null;
|
|
4036
|
+
return `the reply judge has failed its last ${this.judgeFailStreak} requests (the newest: ${this.judgeLastFailure}), `
|
|
4037
|
+
+ (this.judgeOnError === 'wake'
|
|
4038
|
+
? 'so judge rules wake the agent as debounces of the same quiet period would'
|
|
4039
|
+
: "so judge rules' batches don't wake the agent")
|
|
4040
|
+
+ "; the host's judge model or provider may not serve it (GateOptions.judge)";
|
|
4041
|
+
}
|
|
3527
4042
|
// =========================================================================
|
|
3528
4043
|
// Cleanup
|
|
3529
4044
|
// =========================================================================
|
|
@@ -3533,10 +4048,20 @@ export class EventGate {
|
|
|
3533
4048
|
clearTimeout(state.timer);
|
|
3534
4049
|
}
|
|
3535
4050
|
this.debounceTimers.clear();
|
|
4051
|
+
// A judge deciding now is told to stop, and its verdict, when it comes,
|
|
4052
|
+
// delivers nothing (finishJudging); held batches go with the buffer.
|
|
4053
|
+
for (const state of this.judging.values()) {
|
|
4054
|
+
if (state.timer)
|
|
4055
|
+
clearTimeout(state.timer);
|
|
4056
|
+
state.abort.abort();
|
|
4057
|
+
}
|
|
4058
|
+
this.judging.clear();
|
|
4059
|
+
this.judgeHeld.clear();
|
|
3536
4060
|
for (const timer of this.selfWakeTimers.values()) {
|
|
3537
4061
|
clearTimeout(timer);
|
|
3538
4062
|
}
|
|
3539
4063
|
this.selfWakeTimers.clear();
|
|
4064
|
+
this.selfWakeDue.clear();
|
|
3540
4065
|
// The sleep stays, with its note and agent, and its timer goes: fired
|
|
3541
4066
|
// after a stop(), its wake reached a framework that had stopped, and the
|
|
3542
4067
|
// timer held the process until the sleep's end. The next start() arms it
|