@animalabs/agent-framework 0.27.0 → 0.29.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 +145 -41
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +73 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +11 -5
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +5 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +9 -5
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.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 +468 -125
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3651 -1111
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +133 -13
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +654 -59
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- 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 +173 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +202 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +778 -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/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- 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 +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +133 -54
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +29 -11
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +43 -70
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.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 +70 -30
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.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 +150 -80
- 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 +60 -30
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -55
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +220 -111
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +27 -35
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +313 -91
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +31 -13
- 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 +10 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +15 -9
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +30 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +26 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +138 -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 +59 -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 +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +67 -50
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +63 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +980 -547
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +125 -82
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +465 -241
- 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 +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +156 -50
- 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/notice-tags.d.ts +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -0
- package/dist/src/notice-tags.js.map +1 -0
- 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 +52 -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/silent-heartbeat.d.ts +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- 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 +39 -4
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.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/coordinator.d.ts +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.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 +63 -18
- 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 +58 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -13
- 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 +76 -12
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +11 -5
- package/src/auto-continue.ts +12 -5
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +3772 -1138
- package/src/gate/event-gate.ts +705 -60
- package/src/gate/index.ts +9 -1
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +176 -3
- package/src/heartbeat/heartbeat.ts +843 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +5 -1
- package/src/line-reader.ts +58 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +139 -53
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +45 -70
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +76 -34
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +149 -82
- package/src/mcpl/tool-signature.ts +64 -32
- package/src/mcpl/toolset-host.ts +212 -110
- package/src/mcpl/toolsets-module.ts +277 -91
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +32 -13
- package/src/module-registry.ts +14 -9
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +30 -18
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +151 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +59 -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 +69 -50
- package/src/modules/workspace/index.ts +966 -556
- package/src/modules/workspace/observe.ts +497 -242
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +184 -49
- 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/notice-tags.ts +84 -0
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +64 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +38 -4
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/coordinator.ts +25 -27
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +63 -18
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +59 -0
- package/src/types/module.ts +28 -13
- package/src/types/trace.ts +36 -1
package/src/gate/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { EventGate, wakeProvenance, wakeLineText, type WakeProvenance, type WritePlacement } from './event-gate.js';
|
|
1
|
+
export { EventGate, MCPL_GATE_EVENT_TYPES, wakeProvenance, wakeLineText, type EventTypesFrom, type WakeProvenance, type WritePlacement } from './event-gate.js';
|
|
2
2
|
export type {
|
|
3
3
|
GateConfig,
|
|
4
4
|
GateOptions,
|
|
@@ -10,4 +10,12 @@ export type {
|
|
|
10
10
|
GateStatus,
|
|
11
11
|
GatePolicyStats,
|
|
12
12
|
WakeLine,
|
|
13
|
+
JudgeConfig,
|
|
14
|
+
JudgeEventSummary,
|
|
15
|
+
JudgeFn,
|
|
16
|
+
JudgeRequest,
|
|
17
|
+
JudgeVerdict,
|
|
18
|
+
ReplyJudgeOptions,
|
|
19
|
+
UpcomingWake,
|
|
13
20
|
} from './types.js';
|
|
21
|
+
export { ReplyJudge, DEFAULT_JUDGE_MODEL, DEFAULT_JUDGE_MAX_INPUT_TOKENS, MIN_JUDGE_INPUT_TOKENS, type ReplyJudgeDeps } from './reply-judge.js';
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReplyJudge — the small model a `judge` wake rule asks whether now is a good
|
|
3
|
+
* time for the agent to reply (JudgeConfig).
|
|
4
|
+
*
|
|
5
|
+
* Per batch it takes the request the agent WOULD send if woken now (the
|
|
6
|
+
* framework's previewActivation, a dry run that commits nothing), renders it
|
|
7
|
+
* as one plain-text document (the agent's system prompt, abridged, then its
|
|
8
|
+
* conversation, oldest first), keeps the newest part of that document that
|
|
9
|
+
* fits the judge's input budget, and asks the model, with the agent's own
|
|
10
|
+
* instructions and the batch, for one line: `REPLY: <reason>` or
|
|
11
|
+
* `WAIT: <reason>`.
|
|
12
|
+
*
|
|
13
|
+
* Cost and latency. The budget (`maxInputTokens`, default 90,000) keeps every
|
|
14
|
+
* request under the 100,000-token line past which Claude Haiku 5.5 costs five
|
|
15
|
+
* times as much. Tokens are estimated from characters, at a ratio the judge
|
|
16
|
+
* corrects from every response's reported input, so the estimate tracks the
|
|
17
|
+
* model's tokenizer and the context's mix of prose and code. The document is
|
|
18
|
+
* sent as a run of chunks that start at an anchor, the first message shown,
|
|
19
|
+
* which stays put while the window grows into its budget: the chunks before
|
|
20
|
+
* the newest stay byte-identical from one request to the next, and a cache
|
|
21
|
+
* breakpoint after the last full one lets every later request read them from
|
|
22
|
+
* the prompt cache. Only when the window outgrows its budget does the anchor
|
|
23
|
+
* move, to leave the window a share of the budget to grow into again.
|
|
24
|
+
*
|
|
25
|
+
* The judge reads; it never writes into the agent's context and never speaks.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { NativeFormatter } from '@animalabs/membrane';
|
|
29
|
+
import type { ContentBlock, DetailedUsage, Membrane, NormalizedMessage, NormalizedRequest, NormalizedResponse } from '@animalabs/membrane';
|
|
30
|
+
import type { JudgeEventSummary, JudgeFn, JudgeRequest, JudgeVerdict, ReplyJudgeOptions, UpcomingWake } from './types.js';
|
|
31
|
+
|
|
32
|
+
/** The judge's model when the host names none. */
|
|
33
|
+
export const DEFAULT_JUDGE_MODEL = 'claude-haiku-5-5';
|
|
34
|
+
/** The judge's input budget when the host names none: under Haiku 5.5's 100K price line, with margin. */
|
|
35
|
+
export const DEFAULT_JUDGE_MAX_INPUT_TOKENS = 90_000;
|
|
36
|
+
/** The smallest input budget the judge takes: its instructions, the batch and a little context. A
|
|
37
|
+
* host that wants only the newest few messages read (a short tail, cheaper and quicker, with less
|
|
38
|
+
* of the room's history to weigh) sets a budget near it. */
|
|
39
|
+
export const MIN_JUDGE_INPUT_TOKENS = 4_000;
|
|
40
|
+
|
|
41
|
+
/** Characters per token the judge assumes before any response has told it better. Low, so a first estimate overshoots. */
|
|
42
|
+
const INITIAL_CHARS_PER_TOKEN = 3;
|
|
43
|
+
/** The share of each measured ratio the running estimate takes: more when the
|
|
44
|
+
* text proved denser than estimated (more tokens a character), which is the
|
|
45
|
+
* side that overruns the budget, less when sparser. */
|
|
46
|
+
const CALIBRATION_WEIGHT_DENSER = 0.6;
|
|
47
|
+
const CALIBRATION_WEIGHT_SPARSER = 0.2;
|
|
48
|
+
/** The share of the budget the system prompt (the agent's, abridged) may fill. */
|
|
49
|
+
const SYSTEM_SHARE = 0.08;
|
|
50
|
+
/** The share of the document budget a fresh window starts at, leaving the rest to grow into before the anchor moves. */
|
|
51
|
+
const FRESH_WINDOW_SHARE = 0.65;
|
|
52
|
+
/** Estimated tokens per cached chunk of the document: above every model's minimum cacheable prefix. */
|
|
53
|
+
const CHUNK_TOKENS = 6_000;
|
|
54
|
+
/** Tokens held back for the judge's own instructions, the batch and the question. */
|
|
55
|
+
const FIXED_TOKENS_RESERVE = 3_000;
|
|
56
|
+
/** Longest text block (characters) a message renders whole; a longer one keeps its head and tail. */
|
|
57
|
+
const MAX_TEXT_CHARS = 6_000;
|
|
58
|
+
/** Longest message (characters) rendered, all its blocks together. */
|
|
59
|
+
const MAX_MESSAGE_CHARS = 12_000;
|
|
60
|
+
/** Longest tool call input / tool result (characters) rendered. */
|
|
61
|
+
const MAX_TOOL_CHARS = 600;
|
|
62
|
+
/** Output tokens a verdict may take, adaptive thinking included: room enough
|
|
63
|
+
* that thinking at the model's default effort doesn't crowd out the line.
|
|
64
|
+
* Only what's generated is billed. */
|
|
65
|
+
const VERDICT_MAX_TOKENS = 4_096;
|
|
66
|
+
/** The share of the budget the batch's listing may fill; past it the oldest events are counted, not listed. */
|
|
67
|
+
const EVENTS_SHARE = 0.1;
|
|
68
|
+
/** The participant the judge's request names as the assistant: none of its messages is one. */
|
|
69
|
+
const JUDGE_PARTICIPANT = 'reply-judge';
|
|
70
|
+
|
|
71
|
+
/** What the framework gives the judge. */
|
|
72
|
+
export interface ReplyJudgeDeps {
|
|
73
|
+
/** The membrane the judge calls for an agent. The framework's binds the
|
|
74
|
+
* judge as it binds compression, refusing a call rather than waiting under
|
|
75
|
+
* a hold, and never waits on the agent's turn. */
|
|
76
|
+
membraneFor: (agentName: string) => Membrane;
|
|
77
|
+
options?: ReplyJudgeOptions;
|
|
78
|
+
/** Which of the batch's agents the judge reads the context of. */
|
|
79
|
+
chooseAgent: (agentNames: string[]) => string | undefined;
|
|
80
|
+
/** The request the agent would send if woken now, as a dry run (previewActivation). */
|
|
81
|
+
preview: (agentName: string) => Promise<NormalizedRequest>;
|
|
82
|
+
/** Count a judge request's usage as the agent's, where its turns' usage is counted. */
|
|
83
|
+
recordUsage?: (agentName: string, usage: DetailedUsage) => void;
|
|
84
|
+
/** What else will wake the agent with no new event, beyond the gate's own
|
|
85
|
+
* timers the request carries: wakes the framework has queued or holds. A
|
|
86
|
+
* provider that throws counts as knowing of none. */
|
|
87
|
+
upcomingWakes?: (agentName: string) => UpcomingWake[] | Promise<UpcomingWake[]>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One message of the agent's context, rendered. */
|
|
91
|
+
interface Entry {
|
|
92
|
+
text: string;
|
|
93
|
+
key: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Where the last request's window started, per agent. */
|
|
97
|
+
interface Anchor {
|
|
98
|
+
/** The keys of the window's first few entries, to find it again. */
|
|
99
|
+
keys: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A message as the document shows it: its participant, and each block as text. Thinking is the agent's own, and isn't shown. */
|
|
103
|
+
export function renderMessage(message: NormalizedMessage): string {
|
|
104
|
+
const parts: string[] = [];
|
|
105
|
+
for (const block of message.content as ContentBlock[]) {
|
|
106
|
+
const line = renderBlock(block);
|
|
107
|
+
if (line) parts.push(line);
|
|
108
|
+
}
|
|
109
|
+
if (parts.length === 0) return '';
|
|
110
|
+
return cut(`[${message.participant}] ${parts.join('\n')}`, MAX_MESSAGE_CHARS);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderBlock(block: ContentBlock): string {
|
|
114
|
+
switch (block.type) {
|
|
115
|
+
case 'text':
|
|
116
|
+
return cut(block.text, MAX_TEXT_CHARS);
|
|
117
|
+
case 'image':
|
|
118
|
+
case 'generated_image':
|
|
119
|
+
return '[image]';
|
|
120
|
+
case 'document':
|
|
121
|
+
return '[document]';
|
|
122
|
+
case 'audio':
|
|
123
|
+
return '[audio]';
|
|
124
|
+
case 'video':
|
|
125
|
+
return '[video]';
|
|
126
|
+
case 'tool_use':
|
|
127
|
+
return `[calls ${block.name}] ${cut(safeJson(block.input), MAX_TOOL_CHARS)}`;
|
|
128
|
+
case 'tool_result': {
|
|
129
|
+
const body = typeof block.content === 'string'
|
|
130
|
+
? block.content
|
|
131
|
+
: block.content.map((b) => (b.type === 'text' ? b.text : `[${b.type}]`)).join('\n');
|
|
132
|
+
return `[${block.toolName ?? 'tool'} ${block.isError ? 'failed' : 'returned'}] ${cut(body, MAX_TOOL_CHARS)}`;
|
|
133
|
+
}
|
|
134
|
+
default:
|
|
135
|
+
// thinking, redacted thinking, system changes: not the conversation.
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function safeJson(value: unknown): string {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.stringify(value) ?? '';
|
|
143
|
+
} catch {
|
|
144
|
+
return String(value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Text no longer than `max` characters: a longer one keeps its head and tail, the middle named as cut. */
|
|
149
|
+
export function cut(text: string, max: number): string {
|
|
150
|
+
if (text.length <= max) return text;
|
|
151
|
+
const keep = Math.floor((max - 40) / 2);
|
|
152
|
+
return `${text.slice(0, keep)} […${text.length - 2 * keep} characters cut…] ${text.slice(-keep)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A cheap, stable key for an entry (FNV-1a over its text, with its length). */
|
|
156
|
+
function keyOf(text: string): string {
|
|
157
|
+
let h = 0x811c9dc5;
|
|
158
|
+
for (let i = 0; i < text.length; i++) {
|
|
159
|
+
h ^= text.charCodeAt(i);
|
|
160
|
+
h = Math.imul(h, 0x01000193);
|
|
161
|
+
}
|
|
162
|
+
return `${(h >>> 0).toString(36)}:${text.length}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The text of a system prompt, as membrane's request may hold it. */
|
|
166
|
+
function systemText(system: unknown): string {
|
|
167
|
+
if (typeof system === 'string') return system;
|
|
168
|
+
if (Array.isArray(system)) {
|
|
169
|
+
return system.map((b) => (b && typeof b === 'object' && typeof (b as { text?: unknown }).text === 'string' ? (b as { text: string }).text : '')).join('\n');
|
|
170
|
+
}
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** In how long, as the judge is told an upcoming wake. */
|
|
175
|
+
function inWhile(at: number | undefined, now: number): string {
|
|
176
|
+
if (at === undefined) return 'as soon as it is free';
|
|
177
|
+
const s = Math.max(0, Math.round((at - now) / 1000));
|
|
178
|
+
if (s < 90) return `in ${s} s`;
|
|
179
|
+
const m = Math.round(s / 60);
|
|
180
|
+
if (m < 90) return `in ${m} min`;
|
|
181
|
+
const h = Math.floor(m / 60);
|
|
182
|
+
return `in ${h} h${m % 60 ? ` ${m % 60} min` : ''}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What waiting would leave the agent with: what wakes it anyway, soonest
|
|
187
|
+
* first, or that nothing the host knows of is scheduled. A server's own
|
|
188
|
+
* timers (a heartbeat, a reminder) are listed only where the host was told
|
|
189
|
+
* of them, so the empty case says so.
|
|
190
|
+
*/
|
|
191
|
+
export function ifYouWait(wakes: readonly UpcomingWake[], agentName: string, now: number): string {
|
|
192
|
+
const sorted = [...wakes].sort((a, b) => (a.at ?? now) - (b.at ?? now));
|
|
193
|
+
const lines = sorted.slice(0, 8).map((w) => `- ${inWhile(w.at, now)}${w.certain ? '' : ', maybe'}: ${w.what}`);
|
|
194
|
+
const more = sorted.length > 8 ? [`- and ${sorted.length - 8} more`] : [];
|
|
195
|
+
return [
|
|
196
|
+
'<if_you_wait>',
|
|
197
|
+
...(lines.length > 0
|
|
198
|
+
? [`If you answer WAIT and nothing new arrives, ${agentName} is next woken by:`, ...lines, ...more]
|
|
199
|
+
: [`Nothing the host knows of is scheduled to wake ${agentName}: if you answer WAIT, it isn't woken until a new event arrives (a server's own timers, such as a heartbeat, may not be listed).`]),
|
|
200
|
+
'</if_you_wait>',
|
|
201
|
+
].join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Seconds or minutes ago, as the batch shows each event. */
|
|
205
|
+
function ago(at: number, now: number): string {
|
|
206
|
+
const s = Math.max(0, Math.round((now - at) / 1000));
|
|
207
|
+
return s < 120 ? `${s}s ago` : `${Math.round(s / 60)}m ago`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The verdict in a response's text: its last line that starts with REPLY or
|
|
212
|
+
* WAIT in capitals followed by a colon or a dash, as the judge is asked to
|
|
213
|
+
* write it. A capital word without one, or a lower-case "reply" or "wait" in
|
|
214
|
+
* the judge's reasoning ("Wait, they did ask"), isn't a verdict.
|
|
215
|
+
*/
|
|
216
|
+
export function parseVerdict(text: string): { reply: boolean; reason: string } | undefined {
|
|
217
|
+
const re = /^[\s*_>`#]*(REPLY|WAIT)[*_`]*\s*[:\-–—]\s*(.*)$/gm;
|
|
218
|
+
let last: RegExpExecArray | undefined;
|
|
219
|
+
for (let m = re.exec(text); m; m = re.exec(text)) last = m;
|
|
220
|
+
if (!last) return undefined;
|
|
221
|
+
return { reply: last[1] === 'REPLY', reason: last[2]!.trim().replace(/^[*_`]+|[*_`]+$/g, '').trim() };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The judge's own system prompt: stable per agent, so it caches. */
|
|
225
|
+
export function judgeSystemPrompt(agentName: string): string {
|
|
226
|
+
return [
|
|
227
|
+
`You are the reply judge for an AI agent named ${agentName}. The agent isn't woken for every message: when new`,
|
|
228
|
+
'events arrive, you decide whether now is a good time to wake it so it can respond. Waking it costs it a turn;',
|
|
229
|
+
"letting the events go by means it won't act on them until something else wakes it (they stay in its context,",
|
|
230
|
+
'so it will see them then).',
|
|
231
|
+
'',
|
|
232
|
+
"You're given the agent's context as a document: its conversation, oldest first, cut from the oldest end to",
|
|
233
|
+
"fit, then its system prompt (abridged). The agent's own messages are those from the participant named",
|
|
234
|
+
`${agentName}. Then you're given the new events and, when it has written them, the agent's own instructions to you.`,
|
|
235
|
+
"Its instructions are its preferences about its own attention and come first. Where they don't decide, wake it",
|
|
236
|
+
'when it is addressed or asked something, when a reply from it is expected or would clearly help, or when',
|
|
237
|
+
'something it is following needs it; let events go by when the talk is between others and does not need it,',
|
|
238
|
+
'when it has already answered, or when replying would only add noise.',
|
|
239
|
+
'',
|
|
240
|
+
"You're also told what will wake the agent anyway if you let the events go by. Waiting costs little when it",
|
|
241
|
+
"will be woken soon regardless; when nothing is scheduled, waiting means it may not see these events for a",
|
|
242
|
+
'long time, so weigh how much they matter.',
|
|
243
|
+
'',
|
|
244
|
+
"Don't answer the messages yourself and don't speak as the agent. Answer with exactly one line:",
|
|
245
|
+
'"REPLY: <a short reason>" to wake it, or "WAIT: <a short reason>" to let the events go by.',
|
|
246
|
+
].join('\n');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The closing message: the batch, the agent's instructions, the question. */
|
|
250
|
+
export function judgeQuestion(
|
|
251
|
+
request: Pick<JudgeRequest, 'events' | 'instructions' | 'upcomingWakes'>, agentName: string, now: number, maxEventChars = Infinity,
|
|
252
|
+
): string {
|
|
253
|
+
// Newest first into the listing's share, shown oldest first; the rest are
|
|
254
|
+
// counted, so a long held batch can't crowd out the context.
|
|
255
|
+
const lines: string[] = [];
|
|
256
|
+
let used = 0;
|
|
257
|
+
let i = request.events.length;
|
|
258
|
+
while (i > 0) {
|
|
259
|
+
const e: JudgeEventSummary = request.events[i - 1]!;
|
|
260
|
+
const where = e.channel ? ` in ${e.channel}` : '';
|
|
261
|
+
const who = e.authorId ? ` from ${e.authorId}` : '';
|
|
262
|
+
const addressed = e.addressed ? ' (addresses the agent)' : '';
|
|
263
|
+
const line = `- ${ago(e.at, now)}${where}${who}${addressed}: ${e.content}`;
|
|
264
|
+
if (lines.length > 0 && used + line.length > maxEventChars) break;
|
|
265
|
+
lines.unshift(line);
|
|
266
|
+
used += line.length + 1;
|
|
267
|
+
i--;
|
|
268
|
+
}
|
|
269
|
+
const parts = [
|
|
270
|
+
`<new_events count="${request.events.length}">`,
|
|
271
|
+
...(i > 0 ? [`- (${i} earlier ${i === 1 ? 'event' : 'events'} in this batch not listed)`] : []),
|
|
272
|
+
...lines,
|
|
273
|
+
'</new_events>',
|
|
274
|
+
];
|
|
275
|
+
parts.push('', ifYouWait(request.upcomingWakes ?? [], agentName, now));
|
|
276
|
+
if (request.instructions && request.instructions.trim()) {
|
|
277
|
+
parts.push('', `<instructions_from_${agentName.replace(/[^A-Za-z0-9_-]/g, '_')}>`, request.instructions.trim(), '</instructions_from_' + agentName.replace(/[^A-Za-z0-9_-]/g, '_') + '>');
|
|
278
|
+
}
|
|
279
|
+
parts.push('', `Should ${agentName} be woken now to respond? One line: "REPLY: <reason>" or "WAIT: <reason>".`);
|
|
280
|
+
return parts.join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export class ReplyJudge {
|
|
284
|
+
private readonly membraneFor: ReplyJudgeDeps['membraneFor'];
|
|
285
|
+
private readonly cacheTtl: '5m' | '1h';
|
|
286
|
+
private readonly model: string;
|
|
287
|
+
private readonly maxInputTokens: number;
|
|
288
|
+
private readonly effort: 'low' | 'medium' | 'high' | 'none';
|
|
289
|
+
private readonly chooseAgent: ReplyJudgeDeps['chooseAgent'];
|
|
290
|
+
private readonly preview: ReplyJudgeDeps['preview'];
|
|
291
|
+
private readonly recordUsage: ReplyJudgeDeps['recordUsage'];
|
|
292
|
+
private readonly extraWakes: ReplyJudgeDeps['upcomingWakes'];
|
|
293
|
+
/** Characters per token, corrected from each response's reported input. */
|
|
294
|
+
private charsPerToken = INITIAL_CHARS_PER_TOKEN;
|
|
295
|
+
private readonly anchors = new Map<string, Anchor>();
|
|
296
|
+
|
|
297
|
+
constructor(deps: ReplyJudgeDeps) {
|
|
298
|
+
this.membraneFor = deps.membraneFor;
|
|
299
|
+
this.cacheTtl = deps.options?.cacheTtl === '1h' ? '1h' : '5m';
|
|
300
|
+
this.model = deps.options?.model || DEFAULT_JUDGE_MODEL;
|
|
301
|
+
const max = deps.options?.maxInputTokens;
|
|
302
|
+
if (max === undefined) {
|
|
303
|
+
this.maxInputTokens = DEFAULT_JUDGE_MAX_INPUT_TOKENS;
|
|
304
|
+
} else if (typeof max !== 'number' || !Number.isFinite(max)) {
|
|
305
|
+
this.maxInputTokens = DEFAULT_JUDGE_MAX_INPUT_TOKENS;
|
|
306
|
+
console.error(`[gate] the reply judge's maxInputTokens isn't a number, so it's read as ${DEFAULT_JUDGE_MAX_INPUT_TOKENS}`);
|
|
307
|
+
} else if (max < MIN_JUDGE_INPUT_TOKENS) {
|
|
308
|
+
// At its nearest bound, as the gate reads a value out of range: a small
|
|
309
|
+
// budget set to save money stays small.
|
|
310
|
+
this.maxInputTokens = MIN_JUDGE_INPUT_TOKENS;
|
|
311
|
+
console.error(`[gate] the reply judge's maxInputTokens is ${max}, under the ${MIN_JUDGE_INPUT_TOKENS} minimum, so it's read as ${MIN_JUDGE_INPUT_TOKENS}`);
|
|
312
|
+
} else {
|
|
313
|
+
this.maxInputTokens = Math.floor(max);
|
|
314
|
+
}
|
|
315
|
+
this.effort = deps.options?.effort ?? 'low';
|
|
316
|
+
this.chooseAgent = deps.chooseAgent;
|
|
317
|
+
this.preview = deps.preview;
|
|
318
|
+
this.recordUsage = deps.recordUsage;
|
|
319
|
+
this.extraWakes = deps.upcomingWakes;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The gate's judge function. */
|
|
323
|
+
readonly judge: JudgeFn = async (request) => this.decide(request);
|
|
324
|
+
|
|
325
|
+
/** The ratio the judge estimates tokens by now (for tests and status). */
|
|
326
|
+
estimateRatio(): number {
|
|
327
|
+
return this.charsPerToken;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private tokens(chars: number): number {
|
|
331
|
+
return Math.ceil(chars / this.charsPerToken);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Build the judge's request for one batch: the system prompt, the document in chunks, the question. */
|
|
335
|
+
async buildRequest(request: JudgeRequest, now = Date.now()): Promise<{ request: NormalizedRequest; agentName: string; chars: number }> {
|
|
336
|
+
const agentName = this.chooseAgent(request.agentNames);
|
|
337
|
+
if (!agentName) throw new Error('no agent to judge for');
|
|
338
|
+
const preview = await this.preview(agentName);
|
|
339
|
+
if (request.signal.aborted) throw new Error('the judge was stopped before its request');
|
|
340
|
+
|
|
341
|
+
let extra: UpcomingWake[] = [];
|
|
342
|
+
try {
|
|
343
|
+
extra = (await this.extraWakes?.(agentName)) ?? [];
|
|
344
|
+
} catch {
|
|
345
|
+
extra = [];
|
|
346
|
+
}
|
|
347
|
+
const upcoming = [...(request.upcomingWakes ?? []), ...extra];
|
|
348
|
+
|
|
349
|
+
const system = judgeSystemPrompt(agentName);
|
|
350
|
+
const question = judgeQuestion(
|
|
351
|
+
{ ...request, upcomingWakes: upcoming }, agentName, now, Math.floor(this.maxInputTokens * EVENTS_SHARE * this.charsPerToken),
|
|
352
|
+
);
|
|
353
|
+
const fixedChars = system.length + question.length;
|
|
354
|
+
|
|
355
|
+
// The budget the document may fill, in characters at the current ratio.
|
|
356
|
+
const docTokens = Math.max(1_000, this.maxInputTokens - FIXED_TOKENS_RESERVE - this.tokens(fixedChars));
|
|
357
|
+
const docChars = Math.floor(docTokens * this.charsPerToken);
|
|
358
|
+
|
|
359
|
+
// The agent's system prompt comes after the conversation, just before the
|
|
360
|
+
// question: a system prompt that changes between requests (a host that
|
|
361
|
+
// writes the time or a status into it) then costs only its own tokens,
|
|
362
|
+
// not the cached conversation before it.
|
|
363
|
+
const agentSystem = systemText(preview.system);
|
|
364
|
+
const systemChars = Math.floor(docChars * SYSTEM_SHARE);
|
|
365
|
+
const about = [
|
|
366
|
+
`## ${agentName}'s system prompt${agentSystem.length > systemChars ? ' (abridged)' : ''}`,
|
|
367
|
+
'',
|
|
368
|
+
agentSystem ? cut(agentSystem, systemChars) : '(none)',
|
|
369
|
+
].join('\n');
|
|
370
|
+
const header = `# ${agentName}'s context\n\n## Conversation (oldest shown first)`;
|
|
371
|
+
|
|
372
|
+
const entries: Entry[] = [];
|
|
373
|
+
for (const message of preview.messages) {
|
|
374
|
+
const text = renderMessage(message);
|
|
375
|
+
if (text) entries.push({ text, key: keyOf(text) });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const windowChars = Math.max(0, docChars - header.length - about.length);
|
|
379
|
+
const start = this.windowStart(agentName, entries, windowChars);
|
|
380
|
+
const shown = entries.slice(start);
|
|
381
|
+
if (shown.length > 0) {
|
|
382
|
+
this.anchors.set(agentName, { keys: shown.slice(0, 3).map((e) => e.key) });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Chunks of whole messages from the anchor, each closed at CHUNK_TOKENS:
|
|
386
|
+
// the same entries from the same anchor make the same chunks, so every
|
|
387
|
+
// chunk before the open one is the same bytes as last time.
|
|
388
|
+
const chunkChars = CHUNK_TOKENS * INITIAL_CHARS_PER_TOKEN;
|
|
389
|
+
const chunks: string[] = [];
|
|
390
|
+
let current = start > 0 ? `${header}\n[…earlier messages not shown…]\n\n` : `${header}\n\n`;
|
|
391
|
+
for (const entry of shown) {
|
|
392
|
+
if (current.length + entry.text.length > chunkChars && current.length > 0) {
|
|
393
|
+
chunks.push(current);
|
|
394
|
+
current = '';
|
|
395
|
+
}
|
|
396
|
+
current += `${entry.text}\n\n`;
|
|
397
|
+
}
|
|
398
|
+
const open = current;
|
|
399
|
+
|
|
400
|
+
const messages: NormalizedMessage[] = chunks.map((text, i) => ({
|
|
401
|
+
participant: 'user',
|
|
402
|
+
content: [{ type: 'text', text }],
|
|
403
|
+
// After the last closed chunk: the prefix every later request shares.
|
|
404
|
+
...(i === chunks.length - 1 ? { cacheBreakpoint: true } : {}),
|
|
405
|
+
}));
|
|
406
|
+
if (open) messages.push({ participant: 'user', content: [{ type: 'text', text: open }] });
|
|
407
|
+
messages.push({ participant: 'user', content: [{ type: 'text', text: about }] });
|
|
408
|
+
messages.push({ participant: 'user', content: [{ type: 'text', text: question }] });
|
|
409
|
+
|
|
410
|
+
const chars = system.length + messages.reduce((n, m) => n + (m.content[0] as { text: string }).text.length, 0);
|
|
411
|
+
return {
|
|
412
|
+
agentName,
|
|
413
|
+
chars,
|
|
414
|
+
request: {
|
|
415
|
+
system,
|
|
416
|
+
messages,
|
|
417
|
+
config: { model: this.model, maxTokens: VERDICT_MAX_TOKENS },
|
|
418
|
+
assistantParticipant: JUDGE_PARTICIPANT,
|
|
419
|
+
promptCaching: true,
|
|
420
|
+
cacheTtl: this.cacheTtl,
|
|
421
|
+
...(this.effort !== 'none' ? { providerParams: { output_config: { effort: this.effort } } } : {}),
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Where the window starts: at last request's anchor, while the window from
|
|
428
|
+
* there fits; otherwise where the newest messages fill FRESH_WINDOW_SHARE
|
|
429
|
+
* of the budget, leaving room to grow before it moves again.
|
|
430
|
+
*/
|
|
431
|
+
private windowStart(agentName: string, entries: Entry[], windowChars: number): number {
|
|
432
|
+
const tailFrom = (budget: number): number => {
|
|
433
|
+
let used = 0;
|
|
434
|
+
let i = entries.length;
|
|
435
|
+
while (i > 0 && used + entries[i - 1]!.text.length + 2 <= budget) {
|
|
436
|
+
used += entries[i - 1]!.text.length + 2;
|
|
437
|
+
i--;
|
|
438
|
+
}
|
|
439
|
+
// The newest message is shown even when it alone is over budget (it's cut to MAX_MESSAGE_CHARS already).
|
|
440
|
+
return Math.min(i, Math.max(0, entries.length - 1));
|
|
441
|
+
};
|
|
442
|
+
const anchor = this.anchors.get(agentName);
|
|
443
|
+
if (anchor && anchor.keys.length > 0) {
|
|
444
|
+
const at = this.findAnchor(entries, anchor.keys);
|
|
445
|
+
if (at !== undefined) {
|
|
446
|
+
let size = 0;
|
|
447
|
+
for (let i = at; i < entries.length; i++) size += entries[i]!.text.length + 2;
|
|
448
|
+
if (size <= windowChars) return at;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return tailFrom(Math.floor(windowChars * FRESH_WINDOW_SHARE));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private findAnchor(entries: Entry[], keys: string[]): number | undefined {
|
|
455
|
+
for (let i = 0; i + keys.length <= entries.length; i++) {
|
|
456
|
+
let all = true;
|
|
457
|
+
for (let k = 0; k < keys.length; k++) {
|
|
458
|
+
if (entries[i + k]!.key !== keys[k]) { all = false; break; }
|
|
459
|
+
}
|
|
460
|
+
if (all) return i;
|
|
461
|
+
}
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Learn the tokenizer's ratio from one response, leaning conservative after an overshoot. */
|
|
466
|
+
private calibrate(chars: number, inputTokens: number): void {
|
|
467
|
+
if (!(inputTokens > 0) || !(chars > 0)) return;
|
|
468
|
+
const measured = chars / inputTokens;
|
|
469
|
+
const weight = measured < this.charsPerToken ? CALIBRATION_WEIGHT_DENSER : CALIBRATION_WEIGHT_SPARSER;
|
|
470
|
+
const next = this.charsPerToken * (1 - weight) + measured * weight;
|
|
471
|
+
// Over budget: take the measured ratio at once, with a margin, rather than drift toward it.
|
|
472
|
+
this.charsPerToken = inputTokens > this.maxInputTokens ? Math.min(next, measured * 0.9) : next;
|
|
473
|
+
this.charsPerToken = Math.min(6, Math.max(1, this.charsPerToken));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async decide(request: JudgeRequest): Promise<JudgeVerdict> {
|
|
477
|
+
const built = await this.buildRequest(request);
|
|
478
|
+
// The native formatter whatever the host's agents use: an XML or
|
|
479
|
+
// completions formatter would end the judge's request in a prefill, which
|
|
480
|
+
// current Claude models refuse.
|
|
481
|
+
const response = await this.membraneFor(built.agentName).complete(built.request, {
|
|
482
|
+
signal: request.signal,
|
|
483
|
+
formatter: new NativeFormatter(),
|
|
484
|
+
});
|
|
485
|
+
const usage = (response as NormalizedResponse).details?.usage;
|
|
486
|
+
if (usage) this.recordUsage?.(built.agentName, usage);
|
|
487
|
+
const inputTokens = (response.usage?.inputTokens ?? 0) + (usage?.cacheReadTokens ?? 0) + (usage?.cacheCreationTokens ?? 0);
|
|
488
|
+
this.calibrate(built.chars, inputTokens);
|
|
489
|
+
if (inputTokens > this.maxInputTokens) {
|
|
490
|
+
console.error(
|
|
491
|
+
`[gate] the reply judge's request carried ${inputTokens} input tokens, over its ${this.maxInputTokens} budget; `
|
|
492
|
+
+ `its estimate is now ${this.charsPerToken.toFixed(2)} characters a token`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const text = response.content
|
|
496
|
+
.filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
|
|
497
|
+
.map((b) => b.text)
|
|
498
|
+
.join('\n');
|
|
499
|
+
const verdict = parseVerdict(text);
|
|
500
|
+
if (!verdict) {
|
|
501
|
+
const stop = response.stopReason;
|
|
502
|
+
throw new Error(`the judge's answer held no verdict (stop: ${stop}${text ? `, text: ${JSON.stringify(cut(text, 120))}` : ''})`);
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
...verdict,
|
|
506
|
+
model: response.details?.model?.actual || this.model,
|
|
507
|
+
inputTokens,
|
|
508
|
+
...(usage?.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
509
|
+
outputTokens: response.usage?.outputTokens,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|