@agentdeck/bridge 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/fonts/IBMPlexSans-Bold.ttf +0 -0
- package/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Bold.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/assets/fonts/LICENSES.md +22 -0
- package/dist/adapters/claude-code.d.ts +6 -0
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +8 -0
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/openclaw.d.ts +24 -0
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +291 -35
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adapters/opencode-adapter.d.ts +59 -0
- package/dist/adapters/opencode-adapter.d.ts.map +1 -1
- package/dist/adapters/opencode-adapter.js +297 -35
- package/dist/adapters/opencode-adapter.js.map +1 -1
- package/dist/adb-reverse.d.ts +1 -1
- package/dist/adb-reverse.js +1 -1
- package/dist/antigravity-local.d.ts +1 -0
- package/dist/antigravity-local.d.ts.map +1 -1
- package/dist/antigravity-local.js +33 -0
- package/dist/antigravity-local.js.map +1 -1
- package/dist/apme/adapters/claude-hook.d.ts.map +1 -1
- package/dist/apme/adapters/claude-hook.js +5 -0
- package/dist/apme/adapters/claude-hook.js.map +1 -1
- package/dist/apme/adapters/codex-hook.d.ts +17 -0
- package/dist/apme/adapters/codex-hook.d.ts.map +1 -0
- package/dist/apme/adapters/codex-hook.js +95 -0
- package/dist/apme/adapters/codex-hook.js.map +1 -0
- package/dist/apme/adapters/codex-turn-manager.d.ts +86 -0
- package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -0
- package/dist/apme/adapters/codex-turn-manager.js +496 -0
- package/dist/apme/adapters/codex-turn-manager.js.map +1 -0
- package/dist/apme/adapters/openclaw-hook.d.ts +99 -0
- package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -0
- package/dist/apme/adapters/openclaw-hook.js +234 -0
- package/dist/apme/adapters/openclaw-hook.js.map +1 -0
- package/dist/apme/adapters/opencode-hook.d.ts +56 -0
- package/dist/apme/adapters/opencode-hook.d.ts.map +1 -0
- package/dist/apme/adapters/opencode-hook.js +182 -0
- package/dist/apme/adapters/opencode-hook.js.map +1 -0
- package/dist/apme/adapters/timeline.d.ts.map +1 -1
- package/dist/apme/adapters/timeline.js +6 -1
- package/dist/apme/adapters/timeline.js.map +1 -1
- package/dist/apme/antigravity-transcript.d.ts +18 -0
- package/dist/apme/antigravity-transcript.d.ts.map +1 -0
- package/dist/apme/antigravity-transcript.js +94 -0
- package/dist/apme/antigravity-transcript.js.map +1 -0
- package/dist/apme/classify-turn.d.ts +13 -0
- package/dist/apme/classify-turn.d.ts.map +1 -0
- package/dist/apme/classify-turn.js +49 -0
- package/dist/apme/classify-turn.js.map +1 -0
- package/dist/apme/claude-transcript-reader.d.ts +10 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
- package/dist/apme/claude-transcript-reader.js +35 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -1
- package/dist/apme/collector.d.ts +129 -6
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +470 -23
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +87 -11
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +43 -6
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/index.d.ts +26 -5
- package/dist/apme/index.d.ts.map +1 -1
- package/dist/apme/index.js +135 -9
- package/dist/apme/index.js.map +1 -1
- package/dist/apme/pareto.d.ts +39 -0
- package/dist/apme/pareto.d.ts.map +1 -0
- package/dist/apme/pareto.js +59 -0
- package/dist/apme/pareto.js.map +1 -0
- package/dist/apme/recommend.d.ts +3 -0
- package/dist/apme/recommend.d.ts.map +1 -1
- package/dist/apme/recommend.js +43 -1
- package/dist/apme/recommend.js.map +1 -1
- package/dist/apme/runner.d.ts +92 -4
- package/dist/apme/runner.d.ts.map +1 -1
- package/dist/apme/runner.js +586 -70
- package/dist/apme/runner.js.map +1 -1
- package/dist/apme/sample-to-timeline.d.ts +26 -0
- package/dist/apme/sample-to-timeline.d.ts.map +1 -0
- package/dist/apme/sample-to-timeline.js +95 -0
- package/dist/apme/sample-to-timeline.js.map +1 -0
- package/dist/apme/scorers/index.d.ts +47 -0
- package/dist/apme/scorers/index.d.ts.map +1 -0
- package/dist/apme/scorers/index.js +96 -0
- package/dist/apme/scorers/index.js.map +1 -0
- package/dist/apme/settings.d.ts +14 -10
- package/dist/apme/settings.d.ts.map +1 -1
- package/dist/apme/settings.js +34 -8
- package/dist/apme/settings.js.map +1 -1
- package/dist/apme/store.d.ts +33 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +334 -25
- package/dist/apme/store.js.map +1 -1
- package/dist/awaiting-overlay.d.ts +99 -0
- package/dist/awaiting-overlay.d.ts.map +1 -0
- package/dist/awaiting-overlay.js +148 -0
- package/dist/awaiting-overlay.js.map +1 -0
- package/dist/ble-sync-spawn.d.ts +63 -0
- package/dist/ble-sync-spawn.d.ts.map +1 -0
- package/dist/ble-sync-spawn.js +196 -0
- package/dist/ble-sync-spawn.js.map +1 -0
- package/dist/bridge-core.d.ts +24 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +157 -24
- package/dist/bridge-core.js.map +1 -1
- package/dist/broadcast.d.ts +3 -0
- package/dist/broadcast.d.ts.map +1 -0
- package/dist/broadcast.js +122 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/check-deps.d.ts.map +1 -1
- package/dist/check-deps.js +25 -9
- package/dist/check-deps.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1025 -94
- package/dist/cli.js.map +1 -1
- package/dist/codex-auth.d.ts +21 -0
- package/dist/codex-auth.d.ts.map +1 -1
- package/dist/codex-auth.js +52 -1
- package/dist/codex-auth.js.map +1 -1
- package/dist/codex-output-parser.d.ts +8 -0
- package/dist/codex-output-parser.d.ts.map +1 -1
- package/dist/codex-output-parser.js +110 -25
- package/dist/codex-output-parser.js.map +1 -1
- package/dist/codex-rate-limits.d.ts +16 -0
- package/dist/codex-rate-limits.d.ts.map +1 -0
- package/dist/codex-rate-limits.js +197 -0
- package/dist/codex-rate-limits.js.map +1 -0
- package/dist/codex-rollout-response.d.ts +31 -0
- package/dist/codex-rollout-response.d.ts.map +1 -0
- package/dist/codex-rollout-response.js +135 -0
- package/dist/codex-rollout-response.js.map +1 -0
- package/dist/daemon-server.d.ts +85 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +1627 -159
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +15 -6
- package/dist/daemon.js.map +1 -1
- package/dist/display-dim.d.ts +9 -0
- package/dist/display-dim.d.ts.map +1 -0
- package/dist/display-dim.js +53 -0
- package/dist/display-dim.js.map +1 -0
- package/dist/display-monitor.d.ts +15 -2
- package/dist/display-monitor.d.ts.map +1 -1
- package/dist/display-monitor.js +104 -26
- package/dist/display-monitor.js.map +1 -1
- package/dist/esp32-serial.d.ts +160 -1
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +950 -153
- package/dist/esp32-serial.js.map +1 -1
- package/dist/fallback-task-timeline.d.ts +42 -0
- package/dist/fallback-task-timeline.d.ts.map +1 -0
- package/dist/fallback-task-timeline.js +112 -0
- package/dist/fallback-task-timeline.js.map +1 -0
- package/dist/foundation-models-helper.d.ts +11 -0
- package/dist/foundation-models-helper.d.ts.map +1 -0
- package/dist/foundation-models-helper.js +241 -0
- package/dist/foundation-models-helper.js.map +1 -0
- package/dist/hook-migration.js +1 -1
- package/dist/hook-migration.js.map +1 -1
- package/dist/hook-server.d.ts +2 -3
- package/dist/hook-server.d.ts.map +1 -1
- package/dist/hook-server.js +22 -10
- package/dist/hook-server.js.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +33 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +164 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-discover.d.ts +18 -0
- package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-discover.js +85 -0
- package/dist/idotmatrix/idotmatrix-discover.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts +20 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -0
- package/dist/idotmatrix/idotmatrix-settings.js +57 -0
- package/dist/idotmatrix/idotmatrix-settings.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +215 -179
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +15 -16
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +18 -96
- package/dist/log-stream.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +12 -3
- package/dist/logger.js.map +1 -1
- package/dist/mdns.d.ts +17 -0
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +52 -18
- package/dist/mdns.js.map +1 -1
- package/dist/modules/adb-module.d.ts +1 -1
- package/dist/modules/adb-module.d.ts.map +1 -1
- package/dist/modules/adb-module.js +4 -3
- package/dist/modules/adb-module.js.map +1 -1
- package/dist/modules/broadcast-module.d.ts +17 -0
- package/dist/modules/broadcast-module.d.ts.map +1 -0
- package/dist/modules/broadcast-module.js +26 -0
- package/dist/modules/broadcast-module.js.map +1 -0
- package/dist/modules/idotmatrix-module.d.ts +9 -0
- package/dist/modules/idotmatrix-module.d.ts.map +1 -0
- package/dist/modules/idotmatrix-module.js +46 -0
- package/dist/modules/idotmatrix-module.js.map +1 -0
- package/dist/modules/index.d.ts +3 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +9 -3
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/pixoo-module.d.ts.map +1 -1
- package/dist/modules/pixoo-module.js +14 -4
- package/dist/modules/pixoo-module.js.map +1 -1
- package/dist/modules/serial-module.d.ts +4 -0
- package/dist/modules/serial-module.d.ts.map +1 -1
- package/dist/modules/serial-module.js +14 -12
- package/dist/modules/serial-module.js.map +1 -1
- package/dist/modules/timebox-module.d.ts +9 -0
- package/dist/modules/timebox-module.d.ts.map +1 -0
- package/dist/modules/timebox-module.js +46 -0
- package/dist/modules/timebox-module.js.map +1 -0
- package/dist/modules/types.d.ts +4 -2
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/openclaw-session.d.ts +18 -0
- package/dist/openclaw-session.d.ts.map +1 -0
- package/dist/openclaw-session.js +34 -0
- package/dist/openclaw-session.js.map +1 -0
- package/dist/output-parser.d.ts +30 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +125 -9
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +26 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +258 -49
- package/dist/passive-observer.js.map +1 -1
- package/dist/permission-resolver.d.ts +47 -0
- package/dist/permission-resolver.d.ts.map +1 -0
- package/dist/permission-resolver.js +103 -0
- package/dist/permission-resolver.js.map +1 -0
- package/dist/pixoo/micro-glyphs.d.ts +49 -0
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
- package/dist/pixoo/micro-glyphs.js +276 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +2 -2
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
- package/dist/pixoo/pixoo-bridge.js +93 -25
- package/dist/pixoo/pixoo-bridge.js.map +1 -1
- package/dist/pixoo/pixoo-camera.d.ts +19 -3
- package/dist/pixoo/pixoo-camera.d.ts.map +1 -1
- package/dist/pixoo/pixoo-camera.js +62 -27
- package/dist/pixoo/pixoo-camera.js.map +1 -1
- package/dist/pixoo/pixoo-client.d.ts +13 -3
- package/dist/pixoo/pixoo-client.d.ts.map +1 -1
- package/dist/pixoo/pixoo-client.js +51 -22
- package/dist/pixoo/pixoo-client.js.map +1 -1
- package/dist/pixoo/pixoo-discover.d.ts +37 -0
- package/dist/pixoo/pixoo-discover.d.ts.map +1 -0
- package/dist/pixoo/pixoo-discover.js +105 -0
- package/dist/pixoo/pixoo-discover.js.map +1 -0
- package/dist/pixoo/pixoo-renderer.d.ts +8 -3
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +189 -60
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pixoo/pixoo-settings.d.ts +6 -0
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
- package/dist/pixoo/pixoo-settings.js +9 -0
- package/dist/pixoo/pixoo-settings.js.map +1 -1
- package/dist/pixoo/pixoo-sprites.d.ts +60 -11
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
- package/dist/pixoo/pixoo-sprites.js +512 -209
- package/dist/pixoo/pixoo-sprites.js.map +1 -1
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +8 -2
- package/dist/pty-manager.js.map +1 -1
- package/dist/session-activity.d.ts +14 -0
- package/dist/session-activity.d.ts.map +1 -0
- package/dist/session-activity.js +160 -0
- package/dist/session-activity.js.map +1 -0
- package/dist/session-aggregator.d.ts +17 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +48 -9
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-registry.d.ts +71 -3
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +137 -8
- package/dist/session-registry.js.map +1 -1
- package/dist/session-timeline-relay.d.ts.map +1 -1
- package/dist/session-timeline-relay.js +13 -4
- package/dist/session-timeline-relay.js.map +1 -1
- package/dist/session-transcript-timeline.d.ts +49 -0
- package/dist/session-transcript-timeline.d.ts.map +1 -0
- package/dist/session-transcript-timeline.js +276 -0
- package/dist/session-transcript-timeline.js.map +1 -0
- package/dist/state-machine.d.ts +6 -1
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +86 -28
- package/dist/state-machine.js.map +1 -1
- package/dist/timebox/timebox-daemon-sync.d.ts +19 -0
- package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -0
- package/dist/timebox/timebox-daemon-sync.js +135 -0
- package/dist/timebox/timebox-daemon-sync.js.map +1 -0
- package/dist/timebox/timebox-discover.d.ts +19 -0
- package/dist/timebox/timebox-discover.d.ts.map +1 -0
- package/dist/timebox/timebox-discover.js +87 -0
- package/dist/timebox/timebox-discover.js.map +1 -0
- package/dist/timebox/timebox-settings.d.ts +32 -0
- package/dist/timebox/timebox-settings.d.ts.map +1 -0
- package/dist/timebox/timebox-settings.js +80 -0
- package/dist/timebox/timebox-settings.js.map +1 -0
- package/dist/timeline-store.d.ts +63 -3
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +292 -14
- package/dist/timeline-store.js.map +1 -1
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +29 -5
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/tui/ansi.d.ts +0 -4
- package/dist/tui/ansi.d.ts.map +1 -1
- package/dist/tui/ansi.js +14 -7
- package/dist/tui/ansi.js.map +1 -1
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +24 -7
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/tui/renderer.d.ts +36 -0
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +303 -119
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/terrarium.d.ts.map +1 -1
- package/dist/tui/terrarium.js +37 -49
- package/dist/tui/terrarium.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +7 -1
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +10 -3
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +74 -12
- package/dist/usage-event.js.map +1 -1
- package/dist/utility-proxy.d.ts.map +1 -1
- package/dist/utility-proxy.js +8 -0
- package/dist/utility-proxy.js.map +1 -1
- package/dist/utils/project-name.d.ts +3 -0
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +44 -2
- package/dist/utils/project-name.js.map +1 -1
- package/dist/windows-service.d.ts +26 -0
- package/dist/windows-service.d.ts.map +1 -0
- package/dist/windows-service.js +145 -0
- package/dist/windows-service.js.map +1 -0
- package/dist/ws-server.d.ts +22 -1
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +89 -4
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +112 -0
- package/package.json +9 -3
- package/scripts/build-fm-helper.mjs +26 -0
- package/LICENSE +0 -21
- package/dist/apme/tuner.d.ts +0 -64
- package/dist/apme/tuner.d.ts.map +0 -1
- package/dist/apme/tuner.js +0 -297
- package/dist/apme/tuner.js.map +0 -1
- package/dist/d200h/hid-protocol.d.ts +0 -62
- package/dist/d200h/hid-protocol.d.ts.map +0 -1
- package/dist/d200h/hid-protocol.js +0 -124
- package/dist/d200h/hid-protocol.js.map +0 -1
- package/dist/d200h/image-renderer.d.ts +0 -36
- package/dist/d200h/image-renderer.d.ts.map +0 -1
- package/dist/d200h/image-renderer.js +0 -487
- package/dist/d200h/image-renderer.js.map +0 -1
- package/dist/modules/d200h-module.d.ts +0 -43
- package/dist/modules/d200h-module.d.ts.map +0 -1
- package/dist/modules/d200h-module.js +0 -360
- package/dist/modules/d200h-module.js.map +0 -1
- package/dist/modules/d200h-renderer.d.ts +0 -7
- package/dist/modules/d200h-renderer.d.ts.map +0 -1
- package/dist/modules/d200h-renderer.js +0 -321
- package/dist/modules/d200h-renderer.js.map +0 -1
package/dist/apme/runner.js
CHANGED
|
@@ -6,16 +6,35 @@
|
|
|
6
6
|
* 0/1 in `evals` (metrics: lint_clean, build_ok, tests_pass).
|
|
7
7
|
*
|
|
8
8
|
* Layer 2 (llm_judge): G-Eval style rubric against the latest `rubrics` row.
|
|
9
|
-
* Backend is pluggable — default is
|
|
10
|
-
*
|
|
11
|
-
* common "clear pass" case skips layer 2 entirely.
|
|
9
|
+
* Backend is pluggable — default is Foundation Models via the Swift daemon or
|
|
10
|
+
* bundled CLI Swift helper, with local MLX fallback for CLI-only runs. Gated
|
|
11
|
+
* by `shouldJudge()` so the common "clear pass" case skips layer 2 entirely.
|
|
12
12
|
*/
|
|
13
13
|
import { spawn } from 'child_process';
|
|
14
14
|
import { existsSync, readdirSync } from 'fs';
|
|
15
|
+
import { createRequire } from 'module';
|
|
15
16
|
import { debug } from '../logger.js';
|
|
16
|
-
import { loadApmeConfig, shouldJudge } from './settings.js';
|
|
17
|
+
import { loadApmeConfig, shouldJudge, DEFAULT_APME_CONFIG } from './settings.js';
|
|
17
18
|
import { loadMlxSettings, mlxChatUrl } from '@agentdeck/shared';
|
|
19
|
+
import { callFoundationModelsHelper, probeFoundationModelsHelper } from '../foundation-models-helper.js';
|
|
20
|
+
import { runSampleScorers } from './scorers/index.js';
|
|
18
21
|
import { execSync } from 'child_process';
|
|
22
|
+
function deriveTaskOutcome(score) {
|
|
23
|
+
if (score == null)
|
|
24
|
+
return 'pending';
|
|
25
|
+
if (score >= 0.75)
|
|
26
|
+
return 'success';
|
|
27
|
+
if (score >= 0.5)
|
|
28
|
+
return 'partial';
|
|
29
|
+
return 'fail';
|
|
30
|
+
}
|
|
31
|
+
/** Narrow an arbitrary DB `outcome` string to the typed union, so a row
|
|
32
|
+
* written via the manual path (`closeTaskExternal` outcome override) can
|
|
33
|
+
* flow back into `TaskEvaluatedEvent` without losing type-safety. */
|
|
34
|
+
function isPreservableOutcome(value) {
|
|
35
|
+
return value === 'success' || value === 'partial' || value === 'fail'
|
|
36
|
+
|| value === 'pending' || value === 'abandoned';
|
|
37
|
+
}
|
|
19
38
|
const DEFAULT_COMMANDS = {
|
|
20
39
|
typescript: {
|
|
21
40
|
lint: 'pnpm -w lint',
|
|
@@ -38,13 +57,27 @@ export class ApmeRunner {
|
|
|
38
57
|
store;
|
|
39
58
|
queue = [];
|
|
40
59
|
drainPromise = null;
|
|
60
|
+
queuedRunIds = new Set();
|
|
61
|
+
runningRunIds = new Set();
|
|
62
|
+
runningTurnIds = new Set();
|
|
63
|
+
runningTaskIds = new Set();
|
|
41
64
|
listeners = new Set();
|
|
65
|
+
taskListeners = new Set();
|
|
42
66
|
configOverride = null;
|
|
43
67
|
judgeOverride = null;
|
|
44
68
|
detOverride = null;
|
|
69
|
+
/** Cached startup judge readiness probe. Populated by `refreshBackendProbe`,
|
|
70
|
+
* surfaced on /health. Null until the first probe completes. */
|
|
71
|
+
lastBackendProbe = null;
|
|
45
72
|
constructor(store) {
|
|
46
73
|
this.store = store;
|
|
47
74
|
}
|
|
75
|
+
/** Probe the configured judge backend and cache the result. Safe to call
|
|
76
|
+
* fire-and-forget at daemon startup — failures don't throw. */
|
|
77
|
+
async refreshBackendProbe(cfg) {
|
|
78
|
+
this.lastBackendProbe = await probeJudgeBackend(cfg);
|
|
79
|
+
return this.lastBackendProbe;
|
|
80
|
+
}
|
|
48
81
|
_setConfig(cfg) { this.configOverride = cfg; }
|
|
49
82
|
_setJudgeFn(fn) {
|
|
50
83
|
this.judgeOverride = fn;
|
|
@@ -56,9 +89,23 @@ export class ApmeRunner {
|
|
|
56
89
|
this.listeners.add(fn);
|
|
57
90
|
return () => this.listeners.delete(fn);
|
|
58
91
|
}
|
|
92
|
+
/** Subscribe to per-task judge completions. The event fires AFTER the
|
|
93
|
+
* task_judge axis scores and summary are persisted, so listeners can read
|
|
94
|
+
* the final state directly from the event payload without re-querying.
|
|
95
|
+
* Used by the timeline emitter to upsert the corresponding `task_end` row
|
|
96
|
+
* with score + outcome metadata. */
|
|
97
|
+
onTaskEvaluated(fn) {
|
|
98
|
+
this.taskListeners.add(fn);
|
|
99
|
+
return () => this.taskListeners.delete(fn);
|
|
100
|
+
}
|
|
59
101
|
enqueue(job) {
|
|
60
102
|
if (!this.store.enabled)
|
|
61
103
|
return;
|
|
104
|
+
if (this.queuedRunIds.has(job.runId) || this.runningRunIds.has(job.runId)) {
|
|
105
|
+
debug('APME', `skip duplicate eval enqueue runId=${job.runId}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.queuedRunIds.add(job.runId);
|
|
62
109
|
this.queue.push(job);
|
|
63
110
|
debug('APME', `enqueue eval runId=${job.runId} (queue=${this.queue.length})`);
|
|
64
111
|
void this.drain();
|
|
@@ -69,7 +116,14 @@ export class ApmeRunner {
|
|
|
69
116
|
enqueueTurn(job) {
|
|
70
117
|
if (!this.store.enabled)
|
|
71
118
|
return;
|
|
72
|
-
|
|
119
|
+
if (this.runningTurnIds.has(job.turnId)) {
|
|
120
|
+
debug('APME', `skip duplicate turn eval turnId=${job.turnId}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.runningTurnIds.add(job.turnId);
|
|
124
|
+
void this.runTurnEval(job).finally(() => {
|
|
125
|
+
this.runningTurnIds.delete(job.turnId);
|
|
126
|
+
});
|
|
73
127
|
}
|
|
74
128
|
/** Judge a closed task (group of turns between boundary signals —
|
|
75
129
|
* TodoWrite all-completed, /clear, session_end). Fires-and-forgets;
|
|
@@ -77,7 +131,14 @@ export class ApmeRunner {
|
|
|
77
131
|
enqueueTask(job) {
|
|
78
132
|
if (!this.store.enabled)
|
|
79
133
|
return;
|
|
80
|
-
|
|
134
|
+
if (this.runningTaskIds.has(job.taskId)) {
|
|
135
|
+
debug('APME', `skip duplicate task eval taskId=${job.taskId}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.runningTaskIds.add(job.taskId);
|
|
139
|
+
void this.runTaskEval(job).finally(() => {
|
|
140
|
+
this.runningTaskIds.delete(job.taskId);
|
|
141
|
+
});
|
|
81
142
|
}
|
|
82
143
|
async runTaskEval({ runId, taskId, category, boundarySignal }) {
|
|
83
144
|
const cfg = this.configOverride ?? loadApmeConfig();
|
|
@@ -121,6 +182,13 @@ export class ApmeRunner {
|
|
|
121
182
|
if (turns.length > TURN_CAP) {
|
|
122
183
|
lines.push(`… (${turns.length - TURN_CAP} more turns omitted)`);
|
|
123
184
|
}
|
|
185
|
+
// The canonical SessionSample gives the judge the typed tool trajectory
|
|
186
|
+
// (req #5/#6) and per-sample cost (req #7) — context the turn rows lack.
|
|
187
|
+
const sample = this.store.getSample(taskId);
|
|
188
|
+
const trajectoryLines = sample ? buildTrajectoryLines(sample) : [];
|
|
189
|
+
const costLine = sample
|
|
190
|
+
? `cost: ${sample.cost.inputTokens}in/${sample.cost.outputTokens}out tok, $${sample.cost.costUsd.toFixed(4)}, model ${sample.model.modelId}`
|
|
191
|
+
: '';
|
|
124
192
|
const judgePrompt = [
|
|
125
193
|
rubric.prompt,
|
|
126
194
|
'',
|
|
@@ -128,23 +196,28 @@ export class ApmeRunner {
|
|
|
128
196
|
`task_category: ${category ?? task.taskCategory ?? 'unknown'}`,
|
|
129
197
|
`turn_count: ${turns.length}`,
|
|
130
198
|
`boundary_signal: ${boundarySignal ?? task.boundarySignal}`,
|
|
199
|
+
...(costLine ? [costLine] : []),
|
|
131
200
|
'',
|
|
132
201
|
'--- TURNS ---',
|
|
133
202
|
...lines,
|
|
203
|
+
...(trajectoryLines.length ? ['', '--- TOOL TRAJECTORY ---', ...trajectoryLines] : []),
|
|
134
204
|
'',
|
|
135
205
|
'Respond with strict JSON only.',
|
|
136
206
|
].join('\n');
|
|
137
207
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
208
|
+
// callJudgeWithMeta carries the effective backend label across the
|
|
209
|
+
// FM→MLX fallback path. Without it, eval rows produced by the MLX
|
|
210
|
+
// fallback would be misattributed to foundationModels in the DB.
|
|
211
|
+
const judgeResult = this.judgeOverride
|
|
212
|
+
? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
|
|
213
|
+
: await callJudgeWithMeta(judgePrompt, cfg.judge);
|
|
214
|
+
const parsed = parseJudgeJson(judgeResult.text);
|
|
142
215
|
if (!parsed) {
|
|
143
216
|
debug('APME', `runTaskEval parse failed task=${taskId.slice(0, 8)}`);
|
|
144
217
|
return;
|
|
145
218
|
}
|
|
146
219
|
const now = Date.now();
|
|
147
|
-
const judgeModel =
|
|
220
|
+
const judgeModel = judgeResult.effectiveLabel;
|
|
148
221
|
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
149
222
|
this.store.insertEvalForTask({
|
|
150
223
|
id: 0,
|
|
@@ -165,22 +238,82 @@ export class ApmeRunner {
|
|
|
165
238
|
createdAt: now,
|
|
166
239
|
});
|
|
167
240
|
}
|
|
241
|
+
// Pure sample-trajectory scorers (tool churn, error rate) — they add
|
|
242
|
+
// signal the LLM judge can miss and are cheap/deterministic. Stored under
|
|
243
|
+
// layer='trajectory' so they don't collide with task_judge axes.
|
|
244
|
+
if (sample) {
|
|
245
|
+
try {
|
|
246
|
+
for (const r of runSampleScorers(sample)) {
|
|
247
|
+
this.store.insertEvalForTask({
|
|
248
|
+
id: 0, runId, taskId,
|
|
249
|
+
layer: r.layer,
|
|
250
|
+
metric: r.metric,
|
|
251
|
+
score: r.score,
|
|
252
|
+
raw: r.reasoning ? JSON.stringify({ reasoning: r.reasoning, scorer: r.scorer }) : null,
|
|
253
|
+
rubricVer: null,
|
|
254
|
+
judgeModel: `scorer:${r.scorer}`,
|
|
255
|
+
createdAt: now,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
debug('APME', `sample scorers failed task=${taskId.slice(0, 8)}: ${String(err)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const compositeScore = parsed.scores.overall ?? null;
|
|
264
|
+
const derivedOutcome = deriveTaskOutcome(compositeScore);
|
|
265
|
+
// Preserve a previously-set outcome — that only happens when the user
|
|
266
|
+
// explicitly closed the task via `agentdeck task done/cancel` (or the
|
|
267
|
+
// macOS detail-pane button) with an outcome override. Without this
|
|
268
|
+
// guard the async judge resolves 5–30 s after the manual close and
|
|
269
|
+
// overwrites e.g. `abandoned` with `partial`, silently losing the
|
|
270
|
+
// user's gesture. `closeTask` itself never writes outcome — only the
|
|
271
|
+
// manual path does — so a non-null read here is unambiguous.
|
|
272
|
+
const existingOutcome = this.store.getTask(taskId)?.outcome ?? null;
|
|
273
|
+
const taskOutcome = isPreservableOutcome(existingOutcome)
|
|
274
|
+
? existingOutcome
|
|
275
|
+
: derivedOutcome;
|
|
168
276
|
this.store.updateTask(taskId, {
|
|
169
277
|
summary: parsed.summary ?? null,
|
|
170
|
-
compositeScore
|
|
278
|
+
compositeScore,
|
|
279
|
+
outcome: taskOutcome,
|
|
171
280
|
notesJson: JSON.stringify({
|
|
172
281
|
reasoning: parsed.reasoning,
|
|
173
282
|
done: parsed.done,
|
|
174
283
|
missed: parsed.missed,
|
|
175
284
|
}),
|
|
176
285
|
});
|
|
177
|
-
debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${
|
|
286
|
+
debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${compositeScore} outcome=${taskOutcome} summary=${parsed.summary?.slice(0, 40) ?? '-'}`);
|
|
178
287
|
for (const fn of this.listeners) {
|
|
179
288
|
try {
|
|
180
|
-
fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall:
|
|
289
|
+
fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall: compositeScore ?? undefined });
|
|
181
290
|
}
|
|
182
291
|
catch { /* ignore */ }
|
|
183
292
|
}
|
|
293
|
+
if (this.taskListeners.size > 0) {
|
|
294
|
+
const run = this.store.getRun(runId);
|
|
295
|
+
const updatedTask = this.store.getTask(taskId) ?? task;
|
|
296
|
+
const event = {
|
|
297
|
+
runId,
|
|
298
|
+
taskId,
|
|
299
|
+
sessionId: run?.sessionId ?? '',
|
|
300
|
+
agentType: run?.agentType ?? undefined,
|
|
301
|
+
projectName: run?.projectName ?? undefined,
|
|
302
|
+
startedAt: updatedTask.startedAt,
|
|
303
|
+
endedAt: updatedTask.endedAt ?? Date.now(),
|
|
304
|
+
compositeScore,
|
|
305
|
+
outcome: taskOutcome,
|
|
306
|
+
taskCategory: category ?? updatedTask.taskCategory ?? undefined,
|
|
307
|
+
summary: parsed.summary ?? undefined,
|
|
308
|
+
boundarySignal: boundarySignal ?? updatedTask.boundarySignal,
|
|
309
|
+
};
|
|
310
|
+
for (const fn of this.taskListeners) {
|
|
311
|
+
try {
|
|
312
|
+
fn(event);
|
|
313
|
+
}
|
|
314
|
+
catch { /* ignore */ }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
184
317
|
}
|
|
185
318
|
catch (err) {
|
|
186
319
|
debug('APME', `task eval error taskId=${taskId.slice(0, 8)}: ${String(err)}`);
|
|
@@ -226,14 +359,15 @@ export class ApmeRunner {
|
|
|
226
359
|
'Respond with strict JSON only.',
|
|
227
360
|
].join('\n');
|
|
228
361
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
: await
|
|
232
|
-
|
|
362
|
+
// Same fallback-aware labelling rule as runTaskEval — see comment there.
|
|
363
|
+
const judgeResult = this.judgeOverride
|
|
364
|
+
? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
|
|
365
|
+
: await callJudgeWithMeta(judgePrompt, cfg.judge);
|
|
366
|
+
const parsed = parseJudgeJson(judgeResult.text);
|
|
233
367
|
if (!parsed)
|
|
234
368
|
return;
|
|
235
369
|
const now = Date.now();
|
|
236
|
-
const judgeModel =
|
|
370
|
+
const judgeModel = judgeResult.effectiveLabel;
|
|
237
371
|
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
238
372
|
this.store.insertEvalForTurn({
|
|
239
373
|
runId, turnId,
|
|
@@ -274,8 +408,14 @@ export class ApmeRunner {
|
|
|
274
408
|
async doDrain() {
|
|
275
409
|
while (this.queue.length > 0) {
|
|
276
410
|
const job = this.queue.shift();
|
|
411
|
+
this.queuedRunIds.delete(job.runId);
|
|
412
|
+
this.runningRunIds.add(job.runId);
|
|
277
413
|
try {
|
|
278
414
|
const result = await this.runOne(job);
|
|
415
|
+
if (!result.layer1Ran && !result.layer2Ran && result.overall === undefined) {
|
|
416
|
+
debug('APME', `runner no-op runId=${job.runId} (no eval rows produced)`);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
279
419
|
for (const fn of this.listeners) {
|
|
280
420
|
try {
|
|
281
421
|
fn(result);
|
|
@@ -286,6 +426,9 @@ export class ApmeRunner {
|
|
|
286
426
|
catch (err) {
|
|
287
427
|
debug('APME', `runner error runId=${job.runId}: ${String(err)}`);
|
|
288
428
|
}
|
|
429
|
+
finally {
|
|
430
|
+
this.runningRunIds.delete(job.runId);
|
|
431
|
+
}
|
|
289
432
|
}
|
|
290
433
|
}
|
|
291
434
|
async runOne(job) {
|
|
@@ -295,6 +438,10 @@ export class ApmeRunner {
|
|
|
295
438
|
debug('APME', `runOne: run ${job.runId} not found`);
|
|
296
439
|
return { runId: job.runId, layer1Ran: false, layer2Ran: false };
|
|
297
440
|
}
|
|
441
|
+
if (this.store.listEvalsForRun(run.id).length > 0) {
|
|
442
|
+
debug('APME', `runOne: run ${run.id} already has eval rows; skip duplicate`);
|
|
443
|
+
return { runId: job.runId, layer1Ran: false, layer2Ran: false };
|
|
444
|
+
}
|
|
298
445
|
// ── Layer 1 — deterministic ───────────────────────────────────────────────
|
|
299
446
|
let layer1Ran = false;
|
|
300
447
|
let layer1Passed = null;
|
|
@@ -339,13 +486,14 @@ export class ApmeRunner {
|
|
|
339
486
|
if (rubric) {
|
|
340
487
|
try {
|
|
341
488
|
const prompt = buildJudgePrompt(run, rubric.prompt, layer1Passed, this.store);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
: await
|
|
345
|
-
|
|
489
|
+
// Same fallback-aware labelling rule as runTaskEval — see comment there.
|
|
490
|
+
const judgeResult = this.judgeOverride
|
|
491
|
+
? { text: await this.judgeOverride(prompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
|
|
492
|
+
: await callJudgeWithMeta(prompt, cfg.judge);
|
|
493
|
+
const parsed = parseJudgeJson(judgeResult.text);
|
|
346
494
|
if (parsed) {
|
|
347
495
|
const now = Date.now();
|
|
348
|
-
const judgeModel =
|
|
496
|
+
const judgeModel = judgeResult.effectiveLabel;
|
|
349
497
|
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
350
498
|
this.store.insertEval({
|
|
351
499
|
runId: run.id,
|
|
@@ -530,6 +678,38 @@ function runCommand(command, cwd, timeoutMs) {
|
|
|
530
678
|
});
|
|
531
679
|
}
|
|
532
680
|
// ─── Layer 2 execution ────────────────────────────────────────────────────────
|
|
681
|
+
/** Render a SessionSample's typed trajectory as compact judge-prompt lines.
|
|
682
|
+
* Gives the judge tool-call sequences + model usage the turn rows omit. */
|
|
683
|
+
export function buildTrajectoryLines(sample, cap = 30) {
|
|
684
|
+
const lines = [];
|
|
685
|
+
const events = sample.events.slice(0, cap);
|
|
686
|
+
for (const e of events) {
|
|
687
|
+
switch (e.kind) {
|
|
688
|
+
case 'tool': {
|
|
689
|
+
let input = '';
|
|
690
|
+
try {
|
|
691
|
+
input = e.input == null ? '' : JSON.stringify(e.input).slice(0, 120);
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
input = '';
|
|
695
|
+
}
|
|
696
|
+
lines.push(` tool ${e.name}(${input})${e.status ? ` → ${e.status}` : ''}${e.error ? ` [err: ${String(e.error).slice(0, 80)}]` : ''}`);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case 'model':
|
|
700
|
+
lines.push(` model ${e.model}: ${e.inputTokens}in/${e.outputTokens}out tok${e.costUsd ? ` ($${e.costUsd.toFixed(4)})` : ''}`);
|
|
701
|
+
break;
|
|
702
|
+
case 'state':
|
|
703
|
+
lines.push(` state → ${e.to}`);
|
|
704
|
+
break;
|
|
705
|
+
default:
|
|
706
|
+
break; // user/assistant messages already shown in the TURNS section
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (sample.events.length > cap)
|
|
710
|
+
lines.push(` … (${sample.events.length - cap} more events)`);
|
|
711
|
+
return lines;
|
|
712
|
+
}
|
|
533
713
|
// `ParsedJudge` is the canonical eval v1 type imported from `@agentdeck/shared`.
|
|
534
714
|
const NON_CODE_CATEGORIES = new Set(['conversation', 'planning', 'research', 'review']);
|
|
535
715
|
export function buildJudgePrompt(run, rubricPrompt, layer1Passed, store) {
|
|
@@ -619,29 +799,312 @@ export function effectiveJudgeModelTag(cfg) {
|
|
|
619
799
|
return 'foundationModels:apple-intelligence';
|
|
620
800
|
return `${cfg.backend}:${cfg.model}`;
|
|
621
801
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
802
|
+
/** Strip backend-specific fields when forcing a cfg through a different
|
|
803
|
+
* adapter. Without this, a FM cfg (`endpoint:"http://.../apme/judge/foundation-models"`,
|
|
804
|
+
* `model:"apple-intelligence"`) handed to `callMlx` would POST to the FM URL
|
|
805
|
+
* and request a model the MLX server has never heard of — silent failure.
|
|
806
|
+
* Mirrors the `resetBackendCoupledFields` path inside `loadApmeConfig`. */
|
|
807
|
+
export function sanitizeForMlx(judgeCfg) {
|
|
808
|
+
if (judgeCfg.backend === 'mlx' && !judgeCfg.endpoint && (!judgeCfg.model || judgeCfg.model === DEFAULT_APME_CONFIG.judge.model)) {
|
|
809
|
+
return judgeCfg;
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
...judgeCfg,
|
|
813
|
+
backend: 'mlx',
|
|
814
|
+
endpoint: undefined,
|
|
815
|
+
model: DEFAULT_APME_CONFIG.judge.model,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS = 15_000;
|
|
819
|
+
const FOUNDATION_MODELS_UNAVAILABLE_TTL_MS = 60_000;
|
|
820
|
+
const FOUNDATION_MODELS_READY_TTL_MS = 60_000;
|
|
821
|
+
let foundationModelsAutoCache = null;
|
|
822
|
+
let foundationModelsResolveInFlight = null;
|
|
823
|
+
function getFoundationModelsAutoCache(now = Date.now()) {
|
|
824
|
+
if (!foundationModelsAutoCache)
|
|
825
|
+
return null;
|
|
826
|
+
if (foundationModelsAutoCache.expiresAt <= now) {
|
|
827
|
+
foundationModelsAutoCache = null;
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
return foundationModelsAutoCache;
|
|
831
|
+
}
|
|
832
|
+
function markFoundationModelsAutoReady(url) {
|
|
833
|
+
foundationModelsAutoCache = {
|
|
834
|
+
state: 'ready',
|
|
835
|
+
url,
|
|
836
|
+
expiresAt: Date.now() + FOUNDATION_MODELS_READY_TTL_MS,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function markFoundationModelsAutoUnavailable(reason, ttlMs = FOUNDATION_MODELS_UNAVAILABLE_TTL_MS) {
|
|
840
|
+
foundationModelsAutoCache = {
|
|
841
|
+
state: 'unavailable',
|
|
842
|
+
reason,
|
|
843
|
+
expiresAt: Date.now() + ttlMs,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
/** Test hook for cache-sensitive runner tests. Production code should not call this. */
|
|
847
|
+
export function clearFoundationModelsAutoCacheForTests() {
|
|
848
|
+
foundationModelsAutoCache = null;
|
|
849
|
+
foundationModelsResolveInFlight = null;
|
|
850
|
+
}
|
|
851
|
+
/** Like `callJudge`, but returns the effective backend + label so callers
|
|
852
|
+
* can record `judge_model` correctly across fallback paths. */
|
|
853
|
+
export async function callJudgeWithMeta(prompt, judgeCfg) {
|
|
629
854
|
if (judgeCfg.backend === 'foundationModels') {
|
|
630
855
|
try {
|
|
631
|
-
|
|
856
|
+
const text = await callFoundationModels(prompt, judgeCfg);
|
|
857
|
+
return { text, effectiveBackend: 'foundationModels', effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
|
|
632
858
|
}
|
|
633
859
|
catch (err) {
|
|
634
|
-
// Cost-sensitive default:
|
|
635
|
-
//
|
|
860
|
+
// Cost-sensitive default: never route to a paid/network backend.
|
|
861
|
+
// Retry via local MLX only when fallbackToMlx is enabled; otherwise
|
|
636
862
|
// propagate the error so the runner's try/catch skips this eval.
|
|
637
863
|
if (judgeCfg.fallbackToMlx) {
|
|
638
864
|
debug('APME', `foundationModels unavailable, fallback to MLX: ${String(err)}`);
|
|
639
|
-
|
|
865
|
+
// sanitizeForMlx wipes the FM-specific endpoint/model so callMlx
|
|
866
|
+
// never POSTs to the FM endpoint or asks MLX for `apple-intelligence`.
|
|
867
|
+
// We then derive the label from the SANITIZED cfg — recording the
|
|
868
|
+
// effective backend, not the cfg the user originally requested.
|
|
869
|
+
const mlxCfg = sanitizeForMlx(judgeCfg);
|
|
870
|
+
const text = await callMlx(prompt, mlxCfg);
|
|
871
|
+
return { text, effectiveBackend: 'mlx', effectiveLabel: effectiveJudgeModelTag(mlxCfg) };
|
|
640
872
|
}
|
|
641
873
|
throw err;
|
|
642
874
|
}
|
|
643
875
|
}
|
|
644
|
-
|
|
876
|
+
let text;
|
|
877
|
+
if (judgeCfg.backend === 'mlx')
|
|
878
|
+
text = await callMlx(prompt, judgeCfg);
|
|
879
|
+
else if (judgeCfg.backend === 'openclaw')
|
|
880
|
+
text = await callOpenClaw(prompt, judgeCfg);
|
|
881
|
+
else if (judgeCfg.backend === 'api')
|
|
882
|
+
text = await callApi(prompt, judgeCfg);
|
|
883
|
+
else
|
|
884
|
+
throw new Error(`unknown judge backend: ${String(judgeCfg.backend)}`);
|
|
885
|
+
return { text, effectiveBackend: judgeCfg.backend, effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
|
|
886
|
+
}
|
|
887
|
+
/** Backwards-compatible thin wrapper. Internal callers that need the actual
|
|
888
|
+
* effective backend (for DB labelling) should call `callJudgeWithMeta`. */
|
|
889
|
+
export async function callJudge(prompt, judgeCfg) {
|
|
890
|
+
return (await callJudgeWithMeta(prompt, judgeCfg)).text;
|
|
891
|
+
}
|
|
892
|
+
export async function probeJudgeBackend(cfg) {
|
|
893
|
+
const start = Date.now();
|
|
894
|
+
const checkedAt = start;
|
|
895
|
+
// Each branch must establish that the backend can ACTUALLY produce a judge
|
|
896
|
+
// response — not just that some HTTP port answers. Reachability without a
|
|
897
|
+
// usable model / SDK / on-device LLM yields false-positive "ready" that
|
|
898
|
+
// hides the real failure mode (silent eval skip later). Follow the rule:
|
|
899
|
+
// either invoke (cheap ping) or downgrade to `unavailable` with a reason.
|
|
900
|
+
try {
|
|
901
|
+
if (cfg.backend === 'mlx') {
|
|
902
|
+
const mlx = loadMlxSettings();
|
|
903
|
+
const url = cfg.endpoint ?? mlx.endpoint;
|
|
904
|
+
const base = url.replace(/\/v1\/chat\/completions$/, '').replace(/\/chat\/completions$/, '');
|
|
905
|
+
let model;
|
|
906
|
+
let modelsReachable = false;
|
|
907
|
+
for (const path of ['/v1/models', '/models']) {
|
|
908
|
+
const resp = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
909
|
+
if (resp?.ok) {
|
|
910
|
+
modelsReachable = true;
|
|
911
|
+
const json = await resp.json().catch(() => ({}));
|
|
912
|
+
model = json.data?.find(m => m.id && !m.id.toLowerCase().includes('nanollava'))?.id;
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (!modelsReachable) {
|
|
917
|
+
return {
|
|
918
|
+
backend: 'mlx', status: 'unavailable',
|
|
919
|
+
reason: `MLX server unreachable at ${base}. Start with \`mlx_lm.server\` or set apme.judge.endpoint.`,
|
|
920
|
+
endpoint: base, checkedAt,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
// Pinned/configured model overrides catalog discovery — the real call uses
|
|
924
|
+
// the same fallback chain as callMlx().
|
|
925
|
+
const pickedModel = mlx.model ?? cfg.model ?? model;
|
|
926
|
+
if (!pickedModel) {
|
|
927
|
+
return {
|
|
928
|
+
backend: 'mlx', status: 'unavailable',
|
|
929
|
+
reason: `MLX server reachable at ${base} but advertises no chat-capable model (only nanollava-class found). Load a chat model with \`mlx_lm.server --model …\`.`,
|
|
930
|
+
endpoint: base, checkedAt,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
// Cheapest possible inference probe: max_tokens=1, temperature=0. If MLX
|
|
934
|
+
// accepts this without a model error, callMlx() will succeed too.
|
|
935
|
+
const ping = await fetch(`${base}/v1/chat/completions`, {
|
|
936
|
+
method: 'POST',
|
|
937
|
+
headers: { 'Content-Type': 'application/json' },
|
|
938
|
+
body: JSON.stringify({
|
|
939
|
+
model: pickedModel,
|
|
940
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
941
|
+
max_tokens: 1, temperature: 0,
|
|
942
|
+
}),
|
|
943
|
+
signal: AbortSignal.timeout(8000),
|
|
944
|
+
}).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
|
|
945
|
+
if (!ping.ok) {
|
|
946
|
+
const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
|
|
947
|
+
return {
|
|
948
|
+
backend: 'mlx', status: 'unavailable',
|
|
949
|
+
reason: `MLX inference failed for model "${pickedModel}" (${detail}). Check that the model is actually loaded.`,
|
|
950
|
+
endpoint: base, model: pickedModel, checkedAt,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
return { backend: 'mlx', status: 'ready', latencyMs: Date.now() - start, model: pickedModel, endpoint: base, checkedAt };
|
|
954
|
+
}
|
|
955
|
+
if (cfg.backend === 'openclaw') {
|
|
956
|
+
// OpenClaw Gateway: /health proves the gateway socket is up but does NOT
|
|
957
|
+
// prove /chat will route. The bridge's own gateway adapter performs an
|
|
958
|
+
// Ed25519 handshake against the same gateway; we don't replicate it
|
|
959
|
+
// here, but we DO require both /health AND a model catalog response so
|
|
960
|
+
// a stub gateway with /health = 200 doesn't pass.
|
|
961
|
+
const url = cfg.endpoint ?? 'http://127.0.0.1:18789';
|
|
962
|
+
const base = url.replace(/\/chat$/, '');
|
|
963
|
+
const health = await fetch(`${base}/health`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
964
|
+
if (!health?.ok) {
|
|
965
|
+
return {
|
|
966
|
+
backend: 'openclaw', status: 'unavailable',
|
|
967
|
+
reason: `OpenClaw Gateway /health unreachable at ${base}.`,
|
|
968
|
+
endpoint: base, checkedAt,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const models = await fetch(`${base}/models`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
972
|
+
if (!models?.ok) {
|
|
973
|
+
return {
|
|
974
|
+
backend: 'openclaw', status: 'unavailable',
|
|
975
|
+
reason: `OpenClaw Gateway /health responds but /models does not — gateway not fully initialised. Wait for handshake or check apme.judge.endpoint.`,
|
|
976
|
+
endpoint: base, checkedAt,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const json = await models.json().catch(() => ({}));
|
|
980
|
+
const list = json.data ?? json.models ?? [];
|
|
981
|
+
const requested = cfg.model;
|
|
982
|
+
if (requested && !list.some(m => m.id === requested)) {
|
|
983
|
+
return {
|
|
984
|
+
backend: 'openclaw', status: 'unavailable',
|
|
985
|
+
reason: `OpenClaw Gateway is up but model "${requested}" is not advertised. Available: ${list.slice(0, 5).map(m => m.id).join(', ')}`,
|
|
986
|
+
endpoint: base, checkedAt,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
backend: 'openclaw', status: 'ready',
|
|
991
|
+
latencyMs: Date.now() - start,
|
|
992
|
+
model: requested ?? list[0]?.id, endpoint: base, checkedAt,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (cfg.backend === 'foundationModels') {
|
|
996
|
+
// Mirror callFoundationModels: explicit endpoint wins over auto-resolve.
|
|
997
|
+
const url = cfg.endpoint ?? await resolveFoundationModelsUrl();
|
|
998
|
+
if (!url) {
|
|
999
|
+
const helper = await probeFoundationModelsHelper();
|
|
1000
|
+
if (helper.available) {
|
|
1001
|
+
return {
|
|
1002
|
+
backend: 'foundationModels',
|
|
1003
|
+
status: 'ready',
|
|
1004
|
+
latencyMs: Date.now() - start,
|
|
1005
|
+
endpoint: helper.path ? `helper:${helper.path}` : 'helper',
|
|
1006
|
+
checkedAt,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
backend: 'foundationModels', status: 'unavailable',
|
|
1011
|
+
reason: `Swift daemon not found and helper unavailable: ${helper.reason ?? 'unknown'}`,
|
|
1012
|
+
checkedAt,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
// Foundation Models adapter on the Swift side returns either { text }
|
|
1016
|
+
// (ready) or { error: "unavailable", reason } (Apple Intelligence not
|
|
1017
|
+
// downloaded, model still warming, etc). A trivial ping forces that
|
|
1018
|
+
// signal up to us so we don't claim ready when the on-device LLM is
|
|
1019
|
+
// actually unusable.
|
|
1020
|
+
const ping = await fetch(url, {
|
|
1021
|
+
method: 'POST',
|
|
1022
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1023
|
+
body: JSON.stringify({ prompt: 'ping' }),
|
|
1024
|
+
signal: AbortSignal.timeout(8000),
|
|
1025
|
+
}).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
|
|
1026
|
+
if (!ping.ok) {
|
|
1027
|
+
const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
|
|
1028
|
+
if (!cfg.endpoint) {
|
|
1029
|
+
markFoundationModelsAutoUnavailable(`Swift daemon FM endpoint did not accept probe (${detail}).`);
|
|
1030
|
+
const helper = await probeFoundationModelsHelper();
|
|
1031
|
+
if (helper.available) {
|
|
1032
|
+
return {
|
|
1033
|
+
backend: 'foundationModels',
|
|
1034
|
+
status: 'ready',
|
|
1035
|
+
latencyMs: Date.now() - start,
|
|
1036
|
+
endpoint: helper.path ? `helper:${helper.path}` : 'helper',
|
|
1037
|
+
checkedAt,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
backend: 'foundationModels', status: 'unavailable',
|
|
1043
|
+
reason: `Swift daemon FM endpoint did not accept probe (${detail}).`,
|
|
1044
|
+
endpoint: url, checkedAt,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
const json = await ping.json().catch(() => ({}));
|
|
1048
|
+
if (json.error) {
|
|
1049
|
+
if (!cfg.endpoint) {
|
|
1050
|
+
markFoundationModelsAutoUnavailable(`Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}.`);
|
|
1051
|
+
const helper = await probeFoundationModelsHelper();
|
|
1052
|
+
if (helper.available) {
|
|
1053
|
+
return {
|
|
1054
|
+
backend: 'foundationModels',
|
|
1055
|
+
status: 'ready',
|
|
1056
|
+
latencyMs: Date.now() - start,
|
|
1057
|
+
endpoint: helper.path ? `helper:${helper.path}` : 'helper',
|
|
1058
|
+
checkedAt,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
backend: 'foundationModels', status: 'unavailable',
|
|
1064
|
+
reason: `Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}. Apple Intelligence may not be downloaded yet.`,
|
|
1065
|
+
endpoint: url, checkedAt,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
if (!cfg.endpoint)
|
|
1069
|
+
markFoundationModelsAutoReady(url);
|
|
1070
|
+
return { backend: 'foundationModels', status: 'ready', latencyMs: Date.now() - start, endpoint: url, checkedAt };
|
|
1071
|
+
}
|
|
1072
|
+
if (cfg.backend === 'api') {
|
|
1073
|
+
// The 'api' backend is currently a STUB — callApi() always throws,
|
|
1074
|
+
// regardless of ANTHROPIC_API_KEY or @anthropic-ai/sdk presence. A probe
|
|
1075
|
+
// that returned 'ready' here would be a lie: the next judge call still
|
|
1076
|
+
// throws. Per cost-sensitive-defaults policy (memory:
|
|
1077
|
+
// feedback_cost_sensitive_defaults.md), local MLX is the supported path;
|
|
1078
|
+
// wiring real API judging is deliberately deferred. Surface that gap
|
|
1079
|
+
// honestly so users redirect to MLX/OpenClaw instead of debugging a
|
|
1080
|
+
// never-firing eval.
|
|
1081
|
+
const hasKey = !!process.env.ANTHROPIC_API_KEY;
|
|
1082
|
+
let sdkPresent = false;
|
|
1083
|
+
try {
|
|
1084
|
+
const r = createRequire(import.meta.url);
|
|
1085
|
+
r.resolve('@anthropic-ai/sdk');
|
|
1086
|
+
sdkPresent = true;
|
|
1087
|
+
}
|
|
1088
|
+
catch { /* not installed */ }
|
|
1089
|
+
const env = hasKey
|
|
1090
|
+
? (sdkPresent ? 'key+SDK present' : 'key set, SDK missing')
|
|
1091
|
+
: 'no ANTHROPIC_API_KEY';
|
|
1092
|
+
return {
|
|
1093
|
+
backend: 'api', status: 'unavailable',
|
|
1094
|
+
reason: `Anthropic API judge backend is not implemented — callApi() is a stub that always throws. Switch apme.judge.backend to "mlx" or "openclaw". Environment: ${env}.`,
|
|
1095
|
+
checkedAt,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return { backend: cfg.backend, status: 'unknown', checkedAt };
|
|
1099
|
+
}
|
|
1100
|
+
catch (err) {
|
|
1101
|
+
return {
|
|
1102
|
+
backend: cfg.backend, status: 'unavailable',
|
|
1103
|
+
reason: String(err).slice(0, 200),
|
|
1104
|
+
latencyMs: Date.now() - start,
|
|
1105
|
+
checkedAt,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
645
1108
|
}
|
|
646
1109
|
async function callMlx(prompt, cfg) {
|
|
647
1110
|
// MLX server speaks OpenAI chat-completions. The llm.mlx pin (shared with
|
|
@@ -709,58 +1172,111 @@ async function callOpenClaw(prompt, cfg) {
|
|
|
709
1172
|
return json.text;
|
|
710
1173
|
}
|
|
711
1174
|
/**
|
|
712
|
-
* Route a judge call to
|
|
1175
|
+
* Route a judge call to Foundation Models.
|
|
713
1176
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
* ship Foundation Models — callers should either opt into `fallbackToMlx` or
|
|
719
|
-
* accept the resulting eval skip.
|
|
1177
|
+
* Prefer the Swift daemon HTTP adapter when it is running; otherwise use the
|
|
1178
|
+
* bundled CLI Swift helper process. Default CLI config enables
|
|
1179
|
+
* `fallbackToMlx`, while callers can set it false to force a skip when
|
|
1180
|
+
* neither Foundation Models path works.
|
|
720
1181
|
*
|
|
721
1182
|
* Shape contract:
|
|
722
1183
|
* Request : POST /apme/judge/foundation-models { prompt: string }
|
|
723
1184
|
* Response : { text: string } | { error: "unavailable", reason: string }
|
|
724
1185
|
*/
|
|
725
1186
|
async function callFoundationModels(prompt, cfg) {
|
|
1187
|
+
const explicitEndpoint = Boolean(cfg.endpoint);
|
|
1188
|
+
const cached = explicitEndpoint ? null : getFoundationModelsAutoCache();
|
|
1189
|
+
if (cached?.state === 'unavailable') {
|
|
1190
|
+
try {
|
|
1191
|
+
return await callFoundationModelsHelper(prompt);
|
|
1192
|
+
}
|
|
1193
|
+
catch (helperErr) {
|
|
1194
|
+
throw new Error(`foundationModels cached unavailable: ${cached.reason}; helper unavailable: ${String(helperErr)}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
726
1197
|
const url = cfg.endpoint ?? await resolveFoundationModelsUrl();
|
|
727
|
-
if (!url)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
});
|
|
735
|
-
if (!resp.ok)
|
|
736
|
-
throw new Error(`foundationModels HTTP ${resp.status}`);
|
|
737
|
-
const json = await resp.json();
|
|
738
|
-
if (json.error) {
|
|
739
|
-
throw new Error(`foundationModels ${json.error}: ${json.reason ?? 'no reason'}`);
|
|
1198
|
+
if (!url) {
|
|
1199
|
+
try {
|
|
1200
|
+
return await callFoundationModelsHelper(prompt);
|
|
1201
|
+
}
|
|
1202
|
+
catch (helperErr) {
|
|
1203
|
+
throw new Error(`foundationModels: no Swift daemon found and helper unavailable: ${String(helperErr)}`);
|
|
1204
|
+
}
|
|
740
1205
|
}
|
|
741
|
-
|
|
742
|
-
|
|
1206
|
+
try {
|
|
1207
|
+
const resp = await fetch(url, {
|
|
1208
|
+
method: 'POST',
|
|
1209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1210
|
+
body: JSON.stringify({ prompt }),
|
|
1211
|
+
signal: AbortSignal.timeout(60_000),
|
|
1212
|
+
});
|
|
1213
|
+
if (!resp.ok)
|
|
1214
|
+
throw new Error(`foundationModels HTTP ${resp.status}`);
|
|
1215
|
+
const json = await resp.json();
|
|
1216
|
+
if (json.error) {
|
|
1217
|
+
throw new Error(`foundationModels ${json.error}: ${json.reason ?? 'no reason'}`);
|
|
1218
|
+
}
|
|
1219
|
+
if (typeof json.text !== 'string' || json.text.length === 0) {
|
|
1220
|
+
throw new Error('foundationModels returned empty text');
|
|
1221
|
+
}
|
|
1222
|
+
if (!explicitEndpoint)
|
|
1223
|
+
markFoundationModelsAutoReady(url);
|
|
1224
|
+
return json.text;
|
|
1225
|
+
}
|
|
1226
|
+
catch (err) {
|
|
1227
|
+
if (!explicitEndpoint) {
|
|
1228
|
+
markFoundationModelsAutoUnavailable(String(err));
|
|
1229
|
+
try {
|
|
1230
|
+
return await callFoundationModelsHelper(prompt);
|
|
1231
|
+
}
|
|
1232
|
+
catch (helperErr) {
|
|
1233
|
+
throw new Error(`${String(err)}; helper unavailable: ${String(helperErr)}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
throw err;
|
|
743
1237
|
}
|
|
744
|
-
return json.text;
|
|
745
1238
|
}
|
|
746
1239
|
/** Best-effort resolver for the Swift daemon's FM endpoint. Returns null when
|
|
747
1240
|
* no Swift daemon (httpPort ≠ port) is reachable. */
|
|
748
1241
|
async function resolveFoundationModelsUrl() {
|
|
1242
|
+
const cached = getFoundationModelsAutoCache();
|
|
1243
|
+
if (cached?.state === 'ready')
|
|
1244
|
+
return cached.url;
|
|
1245
|
+
if (cached?.state === 'unavailable')
|
|
1246
|
+
return null;
|
|
1247
|
+
if (foundationModelsResolveInFlight)
|
|
1248
|
+
return foundationModelsResolveInFlight;
|
|
749
1249
|
// Lazy-require to avoid pulling session-registry into every test bundle
|
|
750
1250
|
// that imports runner.ts for its pure helpers.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1251
|
+
foundationModelsResolveInFlight = (async () => {
|
|
1252
|
+
try {
|
|
1253
|
+
const { findDaemonPortAsync } = await import('../session-registry.js');
|
|
1254
|
+
const info = await findDaemonPortAsync();
|
|
1255
|
+
if (!info) {
|
|
1256
|
+
markFoundationModelsAutoUnavailable('Swift daemon not found', FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS);
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
const port = info.httpPort ?? info.port;
|
|
1260
|
+
const url = `http://127.0.0.1:${port}/apme/judge/foundation-models`;
|
|
1261
|
+
markFoundationModelsAutoReady(url);
|
|
1262
|
+
return url;
|
|
1263
|
+
}
|
|
1264
|
+
finally {
|
|
1265
|
+
foundationModelsResolveInFlight = null;
|
|
1266
|
+
}
|
|
1267
|
+
})();
|
|
1268
|
+
return foundationModelsResolveInFlight;
|
|
757
1269
|
}
|
|
758
1270
|
async function callApi(_prompt, _cfg) {
|
|
759
|
-
// Anthropic API
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
1271
|
+
// Anthropic API judging is NOT implemented on the Node bridge. The macOS
|
|
1272
|
+
// Swift daemon (apple/AgentDeck/Daemon/Apme/ApmeJudgeApi.swift) is the only
|
|
1273
|
+
// place this backend works today. `loadApmeConfig` silently downgrades
|
|
1274
|
+
// settings.json `backend:"api"` to `"mlx"`, so reaching this stub means
|
|
1275
|
+
// either (a) a programmatic caller bypassed loadApmeConfig, or (b) something
|
|
1276
|
+
// raced between settings load and dispatch. Either way, throw with the
|
|
1277
|
+
// SAME diagnostic the probe surfaces so logs remain consistent.
|
|
1278
|
+
throw new Error('APME judge backend "api" is a stub on the Node bridge — callApi() always throws. ' +
|
|
1279
|
+
'Use the macOS app for Anthropic API judging, or set apme.judge.backend to "mlx" / "openclaw" / "foundationModels".');
|
|
764
1280
|
}
|
|
765
1281
|
export function parseJudgeJson(text) {
|
|
766
1282
|
// Models often wrap JSON in prose or code fences — grab the first {...} block.
|