@agentdeck/bridge 0.2.2 → 1.0.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/assets/fm-helper/agentdeck-fm-helper +0 -0
- 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 +133 -6
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +471 -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 +117 -11
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +52 -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 +146 -9
- package/dist/apme/index.js.map +1 -1
- package/dist/apme/judge-detect.d.ts +26 -0
- package/dist/apme/judge-detect.d.ts.map +1 -0
- package/dist/apme/judge-detect.js +69 -0
- package/dist/apme/judge-detect.js.map +1 -0
- 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 +99 -4
- package/dist/apme/runner.d.ts.map +1 -1
- package/dist/apme/runner.js +743 -72
- 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 +17 -11
- package/dist/apme/settings.d.ts.map +1 -1
- package/dist/apme/settings.js +30 -9
- 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 +112 -0
- package/dist/awaiting-overlay.d.ts.map +1 -0
- package/dist/awaiting-overlay.js +179 -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 +34 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +207 -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/claude-permission-rules.d.ts +55 -0
- package/dist/claude-permission-rules.d.ts.map +1 -0
- package/dist/claude-permission-rules.js +197 -0
- package/dist/claude-permission-rules.js.map +1 -0
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1030 -93
- 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/d200h/image-renderer.d.ts +4 -16
- package/dist/d200h/image-renderer.d.ts.map +1 -1
- package/dist/d200h/image-renderer.js +46 -212
- package/dist/d200h/image-renderer.js.map +1 -1
- package/dist/daemon-server.d.ts +85 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +1975 -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 +190 -1
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +1090 -156
- 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 +119 -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 +23 -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 +217 -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 +3 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +18 -5
- 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/model-catalog.d.ts +1 -1
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +2 -4
- package/dist/model-catalog.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/d200h-module.d.ts +22 -0
- package/dist/modules/d200h-module.d.ts.map +1 -1
- package/dist/modules/d200h-module.js +200 -65
- package/dist/modules/d200h-module.js.map +1 -1
- 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/trmnl-module.d.ts +38 -0
- package/dist/modules/trmnl-module.d.ts.map +1 -0
- package/dist/modules/trmnl-module.js +111 -0
- package/dist/modules/trmnl-module.js.map +1 -0
- package/dist/modules/types.d.ts +4 -2
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/observed-steering.d.ts +93 -0
- package/dist/observed-steering.d.ts.map +1 -0
- package/dist/observed-steering.js +247 -0
- package/dist/observed-steering.js.map +1 -0
- 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/opencode-steering.d.ts +36 -0
- package/dist/opencode-steering.d.ts.map +1 -0
- package/dist/opencode-steering.js +84 -0
- package/dist/opencode-steering.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 +172 -18
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +32 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +288 -49
- package/dist/passive-observer.js.map +1 -1
- package/dist/permission-resolver.d.ts +55 -0
- package/dist/permission-resolver.d.ts.map +1 -0
- package/dist/permission-resolver.js +112 -0
- package/dist/permission-resolver.js.map +1 -0
- package/dist/pixoo/micro-glyphs.d.ts +18 -0
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
- package/dist/pixoo/micro-glyphs.js +143 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -0
- package/dist/pixoo/official-dot-glyphs.generated.d.ts +8 -0
- package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -0
- package/dist/pixoo/official-dot-glyphs.generated.js +247 -0
- package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +16 -8
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
- package/dist/pixoo/pixoo-bridge.js +127 -35
- 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 +27 -3
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +533 -135
- 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 +84 -11
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
- package/dist/pixoo/pixoo-sprites.js +620 -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/review-runner.d.ts +113 -0
- package/dist/review-runner.d.ts.map +1 -0
- package/dist/review-runner.js +428 -0
- package/dist/review-runner.js.map +1 -0
- 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 +25 -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-focus-relay.d.ts.map +1 -1
- package/dist/session-focus-relay.js +7 -1
- package/dist/session-focus-relay.js.map +1 -1
- package/dist/session-registry.d.ts +74 -3
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +140 -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 +112 -3
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +460 -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/trmnl/byos-server.d.ts +39 -0
- package/dist/trmnl/byos-server.d.ts.map +1 -0
- package/dist/trmnl/byos-server.js +301 -0
- package/dist/trmnl/byos-server.js.map +1 -0
- package/dist/trmnl/frame-cache.d.ts +37 -0
- package/dist/trmnl/frame-cache.d.ts.map +1 -0
- package/dist/trmnl/frame-cache.js +157 -0
- package/dist/trmnl/frame-cache.js.map +1 -0
- package/dist/trmnl/image-renderer.d.ts +27 -0
- package/dist/trmnl/image-renderer.d.ts.map +1 -0
- package/dist/trmnl/image-renderer.js +235 -0
- package/dist/trmnl/image-renderer.js.map +1 -0
- package/dist/trmnl/trmnl-settings.d.ts +96 -0
- package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
- package/dist/trmnl/trmnl-settings.js +177 -0
- package/dist/trmnl/trmnl-settings.js.map +1 -0
- package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
- package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
- package/dist/trmnl/trmnl-telemetry.js +56 -0
- package/dist/trmnl/trmnl-telemetry.js.map +1 -0
- 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 +38 -11
- 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 +355 -121
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/terrarium.d.ts.map +1 -1
- package/dist/tui/terrarium.js +44 -51
- 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 +98 -13
- 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/version-check.d.ts.map +1 -1
- package/dist/version-check.js +1 -17
- package/dist/version-check.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 +17 -3
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +78 -8
- package/dist/ws-server.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +112 -0
- package/package.json +10 -4
- 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/apme/runner.js
CHANGED
|
@@ -6,16 +6,34 @@
|
|
|
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
15
|
import { debug } from '../logger.js';
|
|
16
|
-
import { loadApmeConfig, shouldJudge } from './settings.js';
|
|
16
|
+
import { loadApmeConfig, shouldJudge, DEFAULT_APME_CONFIG } from './settings.js';
|
|
17
17
|
import { loadMlxSettings, mlxChatUrl } from '@agentdeck/shared';
|
|
18
|
+
import { callFoundationModelsHelper, probeFoundationModelsHelper } from '../foundation-models-helper.js';
|
|
19
|
+
import { runSampleScorers } from './scorers/index.js';
|
|
18
20
|
import { execSync } from 'child_process';
|
|
21
|
+
function deriveTaskOutcome(score) {
|
|
22
|
+
if (score == null)
|
|
23
|
+
return 'pending';
|
|
24
|
+
if (score >= 0.75)
|
|
25
|
+
return 'success';
|
|
26
|
+
if (score >= 0.5)
|
|
27
|
+
return 'partial';
|
|
28
|
+
return 'fail';
|
|
29
|
+
}
|
|
30
|
+
/** Narrow an arbitrary DB `outcome` string to the typed union, so a row
|
|
31
|
+
* written via the manual path (`closeTaskExternal` outcome override) can
|
|
32
|
+
* flow back into `TaskEvaluatedEvent` without losing type-safety. */
|
|
33
|
+
function isPreservableOutcome(value) {
|
|
34
|
+
return value === 'success' || value === 'partial' || value === 'fail'
|
|
35
|
+
|| value === 'pending' || value === 'abandoned';
|
|
36
|
+
}
|
|
19
37
|
const DEFAULT_COMMANDS = {
|
|
20
38
|
typescript: {
|
|
21
39
|
lint: 'pnpm -w lint',
|
|
@@ -38,13 +56,27 @@ export class ApmeRunner {
|
|
|
38
56
|
store;
|
|
39
57
|
queue = [];
|
|
40
58
|
drainPromise = null;
|
|
59
|
+
queuedRunIds = new Set();
|
|
60
|
+
runningRunIds = new Set();
|
|
61
|
+
runningTurnIds = new Set();
|
|
62
|
+
runningTaskIds = new Set();
|
|
41
63
|
listeners = new Set();
|
|
64
|
+
taskListeners = new Set();
|
|
42
65
|
configOverride = null;
|
|
43
66
|
judgeOverride = null;
|
|
44
67
|
detOverride = null;
|
|
68
|
+
/** Cached startup judge readiness probe. Populated by `refreshBackendProbe`,
|
|
69
|
+
* surfaced on /health. Null until the first probe completes. */
|
|
70
|
+
lastBackendProbe = null;
|
|
45
71
|
constructor(store) {
|
|
46
72
|
this.store = store;
|
|
47
73
|
}
|
|
74
|
+
/** Probe the configured judge backend and cache the result. Safe to call
|
|
75
|
+
* fire-and-forget at daemon startup — failures don't throw. */
|
|
76
|
+
async refreshBackendProbe(cfg) {
|
|
77
|
+
this.lastBackendProbe = await probeJudgeBackend(cfg);
|
|
78
|
+
return this.lastBackendProbe;
|
|
79
|
+
}
|
|
48
80
|
_setConfig(cfg) { this.configOverride = cfg; }
|
|
49
81
|
_setJudgeFn(fn) {
|
|
50
82
|
this.judgeOverride = fn;
|
|
@@ -56,9 +88,23 @@ export class ApmeRunner {
|
|
|
56
88
|
this.listeners.add(fn);
|
|
57
89
|
return () => this.listeners.delete(fn);
|
|
58
90
|
}
|
|
91
|
+
/** Subscribe to per-task judge completions. The event fires AFTER the
|
|
92
|
+
* task_judge axis scores and summary are persisted, so listeners can read
|
|
93
|
+
* the final state directly from the event payload without re-querying.
|
|
94
|
+
* Used by the timeline emitter to upsert the corresponding `task_end` row
|
|
95
|
+
* with score + outcome metadata. */
|
|
96
|
+
onTaskEvaluated(fn) {
|
|
97
|
+
this.taskListeners.add(fn);
|
|
98
|
+
return () => this.taskListeners.delete(fn);
|
|
99
|
+
}
|
|
59
100
|
enqueue(job) {
|
|
60
101
|
if (!this.store.enabled)
|
|
61
102
|
return;
|
|
103
|
+
if (this.queuedRunIds.has(job.runId) || this.runningRunIds.has(job.runId)) {
|
|
104
|
+
debug('APME', `skip duplicate eval enqueue runId=${job.runId}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.queuedRunIds.add(job.runId);
|
|
62
108
|
this.queue.push(job);
|
|
63
109
|
debug('APME', `enqueue eval runId=${job.runId} (queue=${this.queue.length})`);
|
|
64
110
|
void this.drain();
|
|
@@ -69,7 +115,14 @@ export class ApmeRunner {
|
|
|
69
115
|
enqueueTurn(job) {
|
|
70
116
|
if (!this.store.enabled)
|
|
71
117
|
return;
|
|
72
|
-
|
|
118
|
+
if (this.runningTurnIds.has(job.turnId)) {
|
|
119
|
+
debug('APME', `skip duplicate turn eval turnId=${job.turnId}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.runningTurnIds.add(job.turnId);
|
|
123
|
+
void this.runTurnEval(job).finally(() => {
|
|
124
|
+
this.runningTurnIds.delete(job.turnId);
|
|
125
|
+
});
|
|
73
126
|
}
|
|
74
127
|
/** Judge a closed task (group of turns between boundary signals —
|
|
75
128
|
* TodoWrite all-completed, /clear, session_end). Fires-and-forgets;
|
|
@@ -77,7 +130,14 @@ export class ApmeRunner {
|
|
|
77
130
|
enqueueTask(job) {
|
|
78
131
|
if (!this.store.enabled)
|
|
79
132
|
return;
|
|
80
|
-
|
|
133
|
+
if (this.runningTaskIds.has(job.taskId)) {
|
|
134
|
+
debug('APME', `skip duplicate task eval taskId=${job.taskId}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.runningTaskIds.add(job.taskId);
|
|
138
|
+
void this.runTaskEval(job).finally(() => {
|
|
139
|
+
this.runningTaskIds.delete(job.taskId);
|
|
140
|
+
});
|
|
81
141
|
}
|
|
82
142
|
async runTaskEval({ runId, taskId, category, boundarySignal }) {
|
|
83
143
|
const cfg = this.configOverride ?? loadApmeConfig();
|
|
@@ -121,6 +181,13 @@ export class ApmeRunner {
|
|
|
121
181
|
if (turns.length > TURN_CAP) {
|
|
122
182
|
lines.push(`… (${turns.length - TURN_CAP} more turns omitted)`);
|
|
123
183
|
}
|
|
184
|
+
// The canonical SessionSample gives the judge the typed tool trajectory
|
|
185
|
+
// (req #5/#6) and per-sample cost (req #7) — context the turn rows lack.
|
|
186
|
+
const sample = this.store.getSample(taskId);
|
|
187
|
+
const trajectoryLines = sample ? buildTrajectoryLines(sample) : [];
|
|
188
|
+
const costLine = sample
|
|
189
|
+
? `cost: ${sample.cost.inputTokens}in/${sample.cost.outputTokens}out tok, $${sample.cost.costUsd.toFixed(4)}, model ${sample.model.modelId}`
|
|
190
|
+
: '';
|
|
124
191
|
const judgePrompt = [
|
|
125
192
|
rubric.prompt,
|
|
126
193
|
'',
|
|
@@ -128,23 +195,28 @@ export class ApmeRunner {
|
|
|
128
195
|
`task_category: ${category ?? task.taskCategory ?? 'unknown'}`,
|
|
129
196
|
`turn_count: ${turns.length}`,
|
|
130
197
|
`boundary_signal: ${boundarySignal ?? task.boundarySignal}`,
|
|
198
|
+
...(costLine ? [costLine] : []),
|
|
131
199
|
'',
|
|
132
200
|
'--- TURNS ---',
|
|
133
201
|
...lines,
|
|
202
|
+
...(trajectoryLines.length ? ['', '--- TOOL TRAJECTORY ---', ...trajectoryLines] : []),
|
|
134
203
|
'',
|
|
135
204
|
'Respond with strict JSON only.',
|
|
136
205
|
].join('\n');
|
|
137
206
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
207
|
+
// callJudgeWithMeta carries the effective backend label across the
|
|
208
|
+
// FM→MLX fallback path. Without it, eval rows produced by the MLX
|
|
209
|
+
// fallback would be misattributed to foundationModels in the DB.
|
|
210
|
+
const judgeResult = this.judgeOverride
|
|
211
|
+
? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
|
|
212
|
+
: await callJudgeWithMeta(judgePrompt, cfg.judge);
|
|
213
|
+
const parsed = parseJudgeJson(judgeResult.text);
|
|
142
214
|
if (!parsed) {
|
|
143
215
|
debug('APME', `runTaskEval parse failed task=${taskId.slice(0, 8)}`);
|
|
144
216
|
return;
|
|
145
217
|
}
|
|
146
218
|
const now = Date.now();
|
|
147
|
-
const judgeModel =
|
|
219
|
+
const judgeModel = judgeResult.effectiveLabel;
|
|
148
220
|
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
149
221
|
this.store.insertEvalForTask({
|
|
150
222
|
id: 0,
|
|
@@ -165,22 +237,85 @@ export class ApmeRunner {
|
|
|
165
237
|
createdAt: now,
|
|
166
238
|
});
|
|
167
239
|
}
|
|
240
|
+
// Pure sample-trajectory scorers (tool churn, error rate) — they add
|
|
241
|
+
// signal the LLM judge can miss and are cheap/deterministic. Stored under
|
|
242
|
+
// layer='trajectory' so they don't collide with task_judge axes.
|
|
243
|
+
if (sample) {
|
|
244
|
+
try {
|
|
245
|
+
for (const r of runSampleScorers(sample)) {
|
|
246
|
+
this.store.insertEvalForTask({
|
|
247
|
+
id: 0, runId, taskId,
|
|
248
|
+
layer: r.layer,
|
|
249
|
+
metric: r.metric,
|
|
250
|
+
score: r.score,
|
|
251
|
+
raw: r.reasoning ? JSON.stringify({ reasoning: r.reasoning, scorer: r.scorer }) : null,
|
|
252
|
+
rubricVer: null,
|
|
253
|
+
judgeModel: `scorer:${r.scorer}`,
|
|
254
|
+
createdAt: now,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
debug('APME', `sample scorers failed task=${taskId.slice(0, 8)}: ${String(err)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const compositeScore = parsed.scores.overall ?? null;
|
|
263
|
+
const derivedOutcome = deriveTaskOutcome(compositeScore);
|
|
264
|
+
// Preserve a previously-set outcome — that only happens when the user
|
|
265
|
+
// explicitly closed the task via `agentdeck task done/cancel` (or the
|
|
266
|
+
// macOS detail-pane button) with an outcome override. Without this
|
|
267
|
+
// guard the async judge resolves 5–30 s after the manual close and
|
|
268
|
+
// overwrites e.g. `abandoned` with `partial`, silently losing the
|
|
269
|
+
// user's gesture. `closeTask` itself never writes outcome — only the
|
|
270
|
+
// manual path does — so a non-null read here is unambiguous.
|
|
271
|
+
const existingOutcome = this.store.getTask(taskId)?.outcome ?? null;
|
|
272
|
+
const taskOutcome = isPreservableOutcome(existingOutcome)
|
|
273
|
+
? existingOutcome
|
|
274
|
+
: derivedOutcome;
|
|
168
275
|
this.store.updateTask(taskId, {
|
|
169
276
|
summary: parsed.summary ?? null,
|
|
170
|
-
compositeScore
|
|
277
|
+
compositeScore,
|
|
278
|
+
outcome: taskOutcome,
|
|
171
279
|
notesJson: JSON.stringify({
|
|
172
280
|
reasoning: parsed.reasoning,
|
|
173
281
|
done: parsed.done,
|
|
174
282
|
missed: parsed.missed,
|
|
175
283
|
}),
|
|
176
284
|
});
|
|
177
|
-
debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${
|
|
285
|
+
debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${compositeScore} outcome=${taskOutcome} summary=${parsed.summary?.slice(0, 40) ?? '-'}`);
|
|
178
286
|
for (const fn of this.listeners) {
|
|
179
287
|
try {
|
|
180
|
-
fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall:
|
|
288
|
+
fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall: compositeScore ?? undefined });
|
|
181
289
|
}
|
|
182
290
|
catch { /* ignore */ }
|
|
183
291
|
}
|
|
292
|
+
if (this.taskListeners.size > 0) {
|
|
293
|
+
const run = this.store.getRun(runId);
|
|
294
|
+
const updatedTask = this.store.getTask(taskId) ?? task;
|
|
295
|
+
const event = {
|
|
296
|
+
runId,
|
|
297
|
+
taskId,
|
|
298
|
+
sessionId: run?.sessionId ?? '',
|
|
299
|
+
agentType: run?.agentType ?? undefined,
|
|
300
|
+
projectName: run?.projectName ?? undefined,
|
|
301
|
+
startedAt: updatedTask.startedAt,
|
|
302
|
+
endedAt: updatedTask.endedAt ?? Date.now(),
|
|
303
|
+
compositeScore,
|
|
304
|
+
outcome: taskOutcome,
|
|
305
|
+
taskCategory: category ?? updatedTask.taskCategory ?? undefined,
|
|
306
|
+
summary: parsed.summary ?? undefined,
|
|
307
|
+
boundarySignal: boundarySignal ?? updatedTask.boundarySignal,
|
|
308
|
+
turns: updatedTask.firstTurnIndex != null
|
|
309
|
+
? (updatedTask.lastTurnIndex ?? updatedTask.firstTurnIndex) - updatedTask.firstTurnIndex + 1
|
|
310
|
+
: undefined,
|
|
311
|
+
};
|
|
312
|
+
for (const fn of this.taskListeners) {
|
|
313
|
+
try {
|
|
314
|
+
fn(event);
|
|
315
|
+
}
|
|
316
|
+
catch { /* ignore */ }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
184
319
|
}
|
|
185
320
|
catch (err) {
|
|
186
321
|
debug('APME', `task eval error taskId=${taskId.slice(0, 8)}: ${String(err)}`);
|
|
@@ -226,14 +361,15 @@ export class ApmeRunner {
|
|
|
226
361
|
'Respond with strict JSON only.',
|
|
227
362
|
].join('\n');
|
|
228
363
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
: await
|
|
232
|
-
|
|
364
|
+
// Same fallback-aware labelling rule as runTaskEval — see comment there.
|
|
365
|
+
const judgeResult = this.judgeOverride
|
|
366
|
+
? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
|
|
367
|
+
: await callJudgeWithMeta(judgePrompt, cfg.judge);
|
|
368
|
+
const parsed = parseJudgeJson(judgeResult.text);
|
|
233
369
|
if (!parsed)
|
|
234
370
|
return;
|
|
235
371
|
const now = Date.now();
|
|
236
|
-
const judgeModel =
|
|
372
|
+
const judgeModel = judgeResult.effectiveLabel;
|
|
237
373
|
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
238
374
|
this.store.insertEvalForTurn({
|
|
239
375
|
runId, turnId,
|
|
@@ -274,8 +410,14 @@ export class ApmeRunner {
|
|
|
274
410
|
async doDrain() {
|
|
275
411
|
while (this.queue.length > 0) {
|
|
276
412
|
const job = this.queue.shift();
|
|
413
|
+
this.queuedRunIds.delete(job.runId);
|
|
414
|
+
this.runningRunIds.add(job.runId);
|
|
277
415
|
try {
|
|
278
416
|
const result = await this.runOne(job);
|
|
417
|
+
if (!result.layer1Ran && !result.layer2Ran && result.overall === undefined) {
|
|
418
|
+
debug('APME', `runner no-op runId=${job.runId} (no eval rows produced)`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
279
421
|
for (const fn of this.listeners) {
|
|
280
422
|
try {
|
|
281
423
|
fn(result);
|
|
@@ -286,6 +428,9 @@ export class ApmeRunner {
|
|
|
286
428
|
catch (err) {
|
|
287
429
|
debug('APME', `runner error runId=${job.runId}: ${String(err)}`);
|
|
288
430
|
}
|
|
431
|
+
finally {
|
|
432
|
+
this.runningRunIds.delete(job.runId);
|
|
433
|
+
}
|
|
289
434
|
}
|
|
290
435
|
}
|
|
291
436
|
async runOne(job) {
|
|
@@ -295,6 +440,10 @@ export class ApmeRunner {
|
|
|
295
440
|
debug('APME', `runOne: run ${job.runId} not found`);
|
|
296
441
|
return { runId: job.runId, layer1Ran: false, layer2Ran: false };
|
|
297
442
|
}
|
|
443
|
+
if (this.store.listEvalsForRun(run.id).length > 0) {
|
|
444
|
+
debug('APME', `runOne: run ${run.id} already has eval rows; skip duplicate`);
|
|
445
|
+
return { runId: job.runId, layer1Ran: false, layer2Ran: false };
|
|
446
|
+
}
|
|
298
447
|
// ── Layer 1 — deterministic ───────────────────────────────────────────────
|
|
299
448
|
let layer1Ran = false;
|
|
300
449
|
let layer1Passed = null;
|
|
@@ -339,13 +488,14 @@ export class ApmeRunner {
|
|
|
339
488
|
if (rubric) {
|
|
340
489
|
try {
|
|
341
490
|
const prompt = buildJudgePrompt(run, rubric.prompt, layer1Passed, this.store);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
: await
|
|
345
|
-
|
|
491
|
+
// Same fallback-aware labelling rule as runTaskEval — see comment there.
|
|
492
|
+
const judgeResult = this.judgeOverride
|
|
493
|
+
? { text: await this.judgeOverride(prompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
|
|
494
|
+
: await callJudgeWithMeta(prompt, cfg.judge);
|
|
495
|
+
const parsed = parseJudgeJson(judgeResult.text);
|
|
346
496
|
if (parsed) {
|
|
347
497
|
const now = Date.now();
|
|
348
|
-
const judgeModel =
|
|
498
|
+
const judgeModel = judgeResult.effectiveLabel;
|
|
349
499
|
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
350
500
|
this.store.insertEval({
|
|
351
501
|
runId: run.id,
|
|
@@ -530,6 +680,38 @@ function runCommand(command, cwd, timeoutMs) {
|
|
|
530
680
|
});
|
|
531
681
|
}
|
|
532
682
|
// ─── Layer 2 execution ────────────────────────────────────────────────────────
|
|
683
|
+
/** Render a SessionSample's typed trajectory as compact judge-prompt lines.
|
|
684
|
+
* Gives the judge tool-call sequences + model usage the turn rows omit. */
|
|
685
|
+
export function buildTrajectoryLines(sample, cap = 30) {
|
|
686
|
+
const lines = [];
|
|
687
|
+
const events = sample.events.slice(0, cap);
|
|
688
|
+
for (const e of events) {
|
|
689
|
+
switch (e.kind) {
|
|
690
|
+
case 'tool': {
|
|
691
|
+
let input = '';
|
|
692
|
+
try {
|
|
693
|
+
input = e.input == null ? '' : JSON.stringify(e.input).slice(0, 120);
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
input = '';
|
|
697
|
+
}
|
|
698
|
+
lines.push(` tool ${e.name}(${input})${e.status ? ` → ${e.status}` : ''}${e.error ? ` [err: ${String(e.error).slice(0, 80)}]` : ''}`);
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
case 'model':
|
|
702
|
+
lines.push(` model ${e.model}: ${e.inputTokens}in/${e.outputTokens}out tok${e.costUsd ? ` ($${e.costUsd.toFixed(4)})` : ''}`);
|
|
703
|
+
break;
|
|
704
|
+
case 'state':
|
|
705
|
+
lines.push(` state → ${e.to}`);
|
|
706
|
+
break;
|
|
707
|
+
default:
|
|
708
|
+
break; // user/assistant messages already shown in the TURNS section
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (sample.events.length > cap)
|
|
712
|
+
lines.push(` … (${sample.events.length - cap} more events)`);
|
|
713
|
+
return lines;
|
|
714
|
+
}
|
|
533
715
|
// `ParsedJudge` is the canonical eval v1 type imported from `@agentdeck/shared`.
|
|
534
716
|
const NON_CODE_CATEGORIES = new Set(['conversation', 'planning', 'research', 'review']);
|
|
535
717
|
export function buildJudgePrompt(run, rubricPrompt, layer1Passed, store) {
|
|
@@ -619,29 +801,351 @@ export function effectiveJudgeModelTag(cfg) {
|
|
|
619
801
|
return 'foundationModels:apple-intelligence';
|
|
620
802
|
return `${cfg.backend}:${cfg.model}`;
|
|
621
803
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
804
|
+
/** Strip backend-specific fields when forcing a cfg through a different
|
|
805
|
+
* adapter. Without this, a FM cfg (`endpoint:"http://.../apme/judge/foundation-models"`,
|
|
806
|
+
* `model:"apple-intelligence"`) handed to `callMlx` would POST to the FM URL
|
|
807
|
+
* and request a model the MLX server has never heard of — silent failure.
|
|
808
|
+
* Mirrors the `resetBackendCoupledFields` path inside `loadApmeConfig`. */
|
|
809
|
+
export function sanitizeForMlx(judgeCfg) {
|
|
810
|
+
if (judgeCfg.backend === 'mlx' && !judgeCfg.endpoint && (!judgeCfg.model || judgeCfg.model === DEFAULT_APME_CONFIG.judge.model)) {
|
|
811
|
+
return judgeCfg;
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
...judgeCfg,
|
|
815
|
+
backend: 'mlx',
|
|
816
|
+
endpoint: undefined,
|
|
817
|
+
model: DEFAULT_APME_CONFIG.judge.model,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS = 15_000;
|
|
821
|
+
const FOUNDATION_MODELS_UNAVAILABLE_TTL_MS = 60_000;
|
|
822
|
+
const FOUNDATION_MODELS_READY_TTL_MS = 60_000;
|
|
823
|
+
let foundationModelsAutoCache = null;
|
|
824
|
+
let foundationModelsResolveInFlight = null;
|
|
825
|
+
function getFoundationModelsAutoCache(now = Date.now()) {
|
|
826
|
+
if (!foundationModelsAutoCache)
|
|
827
|
+
return null;
|
|
828
|
+
if (foundationModelsAutoCache.expiresAt <= now) {
|
|
829
|
+
foundationModelsAutoCache = null;
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
return foundationModelsAutoCache;
|
|
833
|
+
}
|
|
834
|
+
function markFoundationModelsAutoReady(url) {
|
|
835
|
+
foundationModelsAutoCache = {
|
|
836
|
+
state: 'ready',
|
|
837
|
+
url,
|
|
838
|
+
expiresAt: Date.now() + FOUNDATION_MODELS_READY_TTL_MS,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function markFoundationModelsAutoUnavailable(reason, ttlMs = FOUNDATION_MODELS_UNAVAILABLE_TTL_MS) {
|
|
842
|
+
foundationModelsAutoCache = {
|
|
843
|
+
state: 'unavailable',
|
|
844
|
+
reason,
|
|
845
|
+
expiresAt: Date.now() + ttlMs,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/** Test hook for cache-sensitive runner tests. Production code should not call this. */
|
|
849
|
+
export function clearFoundationModelsAutoCacheForTests() {
|
|
850
|
+
foundationModelsAutoCache = null;
|
|
851
|
+
foundationModelsResolveInFlight = null;
|
|
852
|
+
}
|
|
853
|
+
/** Like `callJudge`, but returns the effective backend + label so callers
|
|
854
|
+
* can record `judge_model` correctly across fallback paths. */
|
|
855
|
+
export async function callJudgeWithMeta(prompt, judgeCfg) {
|
|
629
856
|
if (judgeCfg.backend === 'foundationModels') {
|
|
630
857
|
try {
|
|
631
|
-
|
|
858
|
+
const text = await callFoundationModels(prompt, judgeCfg);
|
|
859
|
+
return { text, effectiveBackend: 'foundationModels', effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
|
|
632
860
|
}
|
|
633
861
|
catch (err) {
|
|
634
|
-
// Cost-sensitive default:
|
|
635
|
-
//
|
|
862
|
+
// Cost-sensitive default: never route to a paid/network backend.
|
|
863
|
+
// Retry via local MLX only when fallbackToMlx is enabled; otherwise
|
|
636
864
|
// propagate the error so the runner's try/catch skips this eval.
|
|
637
865
|
if (judgeCfg.fallbackToMlx) {
|
|
638
866
|
debug('APME', `foundationModels unavailable, fallback to MLX: ${String(err)}`);
|
|
639
|
-
|
|
867
|
+
// sanitizeForMlx wipes the FM-specific endpoint/model so callMlx
|
|
868
|
+
// never POSTs to the FM endpoint or asks MLX for `apple-intelligence`.
|
|
869
|
+
// We then derive the label from the SANITIZED cfg — recording the
|
|
870
|
+
// effective backend, not the cfg the user originally requested.
|
|
871
|
+
const mlxCfg = sanitizeForMlx(judgeCfg);
|
|
872
|
+
const text = await callMlx(prompt, mlxCfg);
|
|
873
|
+
return { text, effectiveBackend: 'mlx', effectiveLabel: effectiveJudgeModelTag(mlxCfg) };
|
|
640
874
|
}
|
|
641
875
|
throw err;
|
|
642
876
|
}
|
|
643
877
|
}
|
|
644
|
-
|
|
878
|
+
let text;
|
|
879
|
+
if (judgeCfg.backend === 'mlx')
|
|
880
|
+
text = await callMlx(prompt, judgeCfg);
|
|
881
|
+
else if (judgeCfg.backend === 'openai')
|
|
882
|
+
text = await callOpenAICompatible(prompt, judgeCfg);
|
|
883
|
+
else if (judgeCfg.backend === 'openclaw')
|
|
884
|
+
text = await callOpenClaw(prompt, judgeCfg);
|
|
885
|
+
else if (judgeCfg.backend === 'api')
|
|
886
|
+
text = await callApi(prompt, judgeCfg);
|
|
887
|
+
else
|
|
888
|
+
throw new Error(`unknown judge backend: ${String(judgeCfg.backend)}`);
|
|
889
|
+
return { text, effectiveBackend: judgeCfg.backend, effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
|
|
890
|
+
}
|
|
891
|
+
/** Backwards-compatible thin wrapper. Internal callers that need the actual
|
|
892
|
+
* effective backend (for DB labelling) should call `callJudgeWithMeta`. */
|
|
893
|
+
export async function callJudge(prompt, judgeCfg) {
|
|
894
|
+
return (await callJudgeWithMeta(prompt, judgeCfg)).text;
|
|
895
|
+
}
|
|
896
|
+
export async function probeJudgeBackend(cfg) {
|
|
897
|
+
const start = Date.now();
|
|
898
|
+
const checkedAt = start;
|
|
899
|
+
// Each branch must establish that the backend can ACTUALLY produce a judge
|
|
900
|
+
// response — not just that some HTTP port answers. Reachability without a
|
|
901
|
+
// usable model / SDK / on-device LLM yields false-positive "ready" that
|
|
902
|
+
// hides the real failure mode (silent eval skip later). Follow the rule:
|
|
903
|
+
// either invoke (cheap ping) or downgrade to `unavailable` with a reason.
|
|
904
|
+
try {
|
|
905
|
+
if (cfg.backend === 'mlx') {
|
|
906
|
+
const mlx = loadMlxSettings();
|
|
907
|
+
const url = cfg.endpoint ?? mlx.endpoint;
|
|
908
|
+
const base = url.replace(/\/v1\/chat\/completions$/, '').replace(/\/chat\/completions$/, '');
|
|
909
|
+
let model;
|
|
910
|
+
let modelsReachable = false;
|
|
911
|
+
for (const path of ['/v1/models', '/models']) {
|
|
912
|
+
const resp = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
913
|
+
if (resp?.ok) {
|
|
914
|
+
modelsReachable = true;
|
|
915
|
+
const json = await resp.json().catch(() => ({}));
|
|
916
|
+
model = json.data?.find(m => m.id && !m.id.toLowerCase().includes('nanollava'))?.id;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (!modelsReachable) {
|
|
921
|
+
return {
|
|
922
|
+
backend: 'mlx', status: 'unavailable',
|
|
923
|
+
reason: `MLX server unreachable at ${base}. Start with \`mlx_lm.server\` or set apme.judge.endpoint.`,
|
|
924
|
+
endpoint: base, checkedAt,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
// Pinned/configured model overrides catalog discovery — the real call uses
|
|
928
|
+
// the same fallback chain as callMlx().
|
|
929
|
+
const pickedModel = mlx.model ?? cfg.model ?? model;
|
|
930
|
+
if (!pickedModel) {
|
|
931
|
+
return {
|
|
932
|
+
backend: 'mlx', status: 'unavailable',
|
|
933
|
+
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 …\`.`,
|
|
934
|
+
endpoint: base, checkedAt,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
// Cheapest possible inference probe: max_tokens=1, temperature=0. If MLX
|
|
938
|
+
// accepts this without a model error, callMlx() will succeed too.
|
|
939
|
+
const ping = await fetch(`${base}/v1/chat/completions`, {
|
|
940
|
+
method: 'POST',
|
|
941
|
+
headers: { 'Content-Type': 'application/json' },
|
|
942
|
+
body: JSON.stringify({
|
|
943
|
+
model: pickedModel,
|
|
944
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
945
|
+
max_tokens: 1, temperature: 0,
|
|
946
|
+
}),
|
|
947
|
+
signal: AbortSignal.timeout(8000),
|
|
948
|
+
}).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
|
|
949
|
+
if (!ping.ok) {
|
|
950
|
+
const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
|
|
951
|
+
return {
|
|
952
|
+
backend: 'mlx', status: 'unavailable',
|
|
953
|
+
reason: `MLX inference failed for model "${pickedModel}" (${detail}). Check that the model is actually loaded.`,
|
|
954
|
+
endpoint: base, model: pickedModel, checkedAt,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return { backend: 'mlx', status: 'ready', latencyMs: Date.now() - start, model: pickedModel, endpoint: base, checkedAt };
|
|
958
|
+
}
|
|
959
|
+
if (cfg.backend === 'openai') {
|
|
960
|
+
// Generic OpenAI-compatible (Ollama / OpenRouter / LM Studio / vLLM / …).
|
|
961
|
+
// "Ready" requires a reachable model catalog and a resolvable model id.
|
|
962
|
+
if (!cfg.endpoint) {
|
|
963
|
+
return {
|
|
964
|
+
backend: 'openai', status: 'unavailable',
|
|
965
|
+
reason: 'set apme.judge.endpoint (e.g. http://127.0.0.1:11434/v1 for Ollama, or https://openrouter.ai/api/v1 for OpenRouter)',
|
|
966
|
+
checkedAt,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const base = openAIBase(cfg.endpoint);
|
|
970
|
+
const isRemote = /^https?:\/\/(?!127\.0\.0\.1|localhost|\[::1\])/.test(cfg.endpoint);
|
|
971
|
+
if (isRemote && !cfg.apiKey) {
|
|
972
|
+
return {
|
|
973
|
+
backend: 'openai', status: 'unavailable',
|
|
974
|
+
reason: `remote endpoint ${base} needs an API key — set apme.judge.apiKey (OpenRouter etc.)`,
|
|
975
|
+
endpoint: base, checkedAt,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const model = await resolveOpenAIModel(base, cfg.apiKey, cfg.model);
|
|
979
|
+
if (!model || model === 'default') {
|
|
980
|
+
return {
|
|
981
|
+
backend: 'openai', status: 'unavailable',
|
|
982
|
+
reason: `endpoint ${base} unreachable or advertises no model — is the server running / the key valid?`,
|
|
983
|
+
endpoint: base, checkedAt,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
return { backend: 'openai', status: 'ready', latencyMs: Date.now() - start, model, endpoint: base, checkedAt };
|
|
987
|
+
}
|
|
988
|
+
if (cfg.backend === 'openclaw') {
|
|
989
|
+
// OpenClaw Gateway: /health proves the gateway socket is up but does NOT
|
|
990
|
+
// prove /chat will route. The bridge's own gateway adapter performs an
|
|
991
|
+
// Ed25519 handshake against the same gateway; we don't replicate it
|
|
992
|
+
// here, but we DO require both /health AND a model catalog response so
|
|
993
|
+
// a stub gateway with /health = 200 doesn't pass.
|
|
994
|
+
const url = cfg.endpoint ?? 'http://127.0.0.1:18789';
|
|
995
|
+
const base = url.replace(/\/chat$/, '');
|
|
996
|
+
const health = await fetch(`${base}/health`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
997
|
+
if (!health?.ok) {
|
|
998
|
+
return {
|
|
999
|
+
backend: 'openclaw', status: 'unavailable',
|
|
1000
|
+
reason: `OpenClaw Gateway /health unreachable at ${base}.`,
|
|
1001
|
+
endpoint: base, checkedAt,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
const models = await fetch(`${base}/models`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
1005
|
+
if (!models?.ok) {
|
|
1006
|
+
return {
|
|
1007
|
+
backend: 'openclaw', status: 'unavailable',
|
|
1008
|
+
reason: `OpenClaw Gateway /health responds but /models does not — gateway not fully initialised. Wait for handshake or check apme.judge.endpoint.`,
|
|
1009
|
+
endpoint: base, checkedAt,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const json = await models.json().catch(() => ({}));
|
|
1013
|
+
const list = json.data ?? json.models ?? [];
|
|
1014
|
+
const requested = cfg.model;
|
|
1015
|
+
if (requested && !list.some(m => m.id === requested)) {
|
|
1016
|
+
return {
|
|
1017
|
+
backend: 'openclaw', status: 'unavailable',
|
|
1018
|
+
reason: `OpenClaw Gateway is up but model "${requested}" is not advertised. Available: ${list.slice(0, 5).map(m => m.id).join(', ')}`,
|
|
1019
|
+
endpoint: base, checkedAt,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
backend: 'openclaw', status: 'ready',
|
|
1024
|
+
latencyMs: Date.now() - start,
|
|
1025
|
+
model: requested ?? list[0]?.id, endpoint: base, checkedAt,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (cfg.backend === 'foundationModels') {
|
|
1029
|
+
// Mirror callFoundationModels: explicit endpoint wins over auto-resolve.
|
|
1030
|
+
const url = cfg.endpoint ?? await resolveFoundationModelsUrl();
|
|
1031
|
+
if (!url) {
|
|
1032
|
+
const helper = await probeFoundationModelsHelper();
|
|
1033
|
+
if (helper.available) {
|
|
1034
|
+
return {
|
|
1035
|
+
backend: 'foundationModels',
|
|
1036
|
+
status: 'ready',
|
|
1037
|
+
latencyMs: Date.now() - start,
|
|
1038
|
+
endpoint: helper.path ? `helper:${helper.path}` : 'helper',
|
|
1039
|
+
checkedAt,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
backend: 'foundationModels', status: 'unavailable',
|
|
1044
|
+
reason: `Swift daemon not found and helper unavailable: ${helper.reason ?? 'unknown'}`,
|
|
1045
|
+
checkedAt,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
// Foundation Models adapter on the Swift side returns either { text }
|
|
1049
|
+
// (ready) or { error: "unavailable", reason } (Apple Intelligence not
|
|
1050
|
+
// downloaded, model still warming, etc). A trivial ping forces that
|
|
1051
|
+
// signal up to us so we don't claim ready when the on-device LLM is
|
|
1052
|
+
// actually unusable.
|
|
1053
|
+
const ping = await fetch(url, {
|
|
1054
|
+
method: 'POST',
|
|
1055
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1056
|
+
body: JSON.stringify({ prompt: 'ping' }),
|
|
1057
|
+
signal: AbortSignal.timeout(8000),
|
|
1058
|
+
}).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
|
|
1059
|
+
if (!ping.ok) {
|
|
1060
|
+
const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
|
|
1061
|
+
if (!cfg.endpoint) {
|
|
1062
|
+
markFoundationModelsAutoUnavailable(`Swift daemon FM endpoint did not accept probe (${detail}).`);
|
|
1063
|
+
const helper = await probeFoundationModelsHelper();
|
|
1064
|
+
if (helper.available) {
|
|
1065
|
+
return {
|
|
1066
|
+
backend: 'foundationModels',
|
|
1067
|
+
status: 'ready',
|
|
1068
|
+
latencyMs: Date.now() - start,
|
|
1069
|
+
endpoint: helper.path ? `helper:${helper.path}` : 'helper',
|
|
1070
|
+
checkedAt,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
backend: 'foundationModels', status: 'unavailable',
|
|
1076
|
+
reason: `Swift daemon FM endpoint did not accept probe (${detail}).`,
|
|
1077
|
+
endpoint: url, checkedAt,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
const json = await ping.json().catch(() => ({}));
|
|
1081
|
+
if (json.error) {
|
|
1082
|
+
if (!cfg.endpoint) {
|
|
1083
|
+
markFoundationModelsAutoUnavailable(`Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}.`);
|
|
1084
|
+
const helper = await probeFoundationModelsHelper();
|
|
1085
|
+
if (helper.available) {
|
|
1086
|
+
return {
|
|
1087
|
+
backend: 'foundationModels',
|
|
1088
|
+
status: 'ready',
|
|
1089
|
+
latencyMs: Date.now() - start,
|
|
1090
|
+
endpoint: helper.path ? `helper:${helper.path}` : 'helper',
|
|
1091
|
+
checkedAt,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
backend: 'foundationModels', status: 'unavailable',
|
|
1097
|
+
reason: `Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}. Apple Intelligence may not be downloaded yet.`,
|
|
1098
|
+
endpoint: url, checkedAt,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
if (!cfg.endpoint)
|
|
1102
|
+
markFoundationModelsAutoReady(url);
|
|
1103
|
+
return { backend: 'foundationModels', status: 'ready', latencyMs: Date.now() - start, endpoint: url, checkedAt };
|
|
1104
|
+
}
|
|
1105
|
+
if (cfg.backend === 'api') {
|
|
1106
|
+
// Opt-in Anthropic API judge. "Ready" must mean a judge call can
|
|
1107
|
+
// actually succeed, so verify a credential exists and the model id
|
|
1108
|
+
// resolves via the free Models endpoint (no token spend).
|
|
1109
|
+
const hasCredential = Boolean(cfg.apiKey || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN);
|
|
1110
|
+
if (!hasCredential) {
|
|
1111
|
+
return {
|
|
1112
|
+
backend: 'api', status: 'unavailable',
|
|
1113
|
+
reason: 'no Anthropic API credential — set apme.judge.apiKey in settings.json, export ANTHROPIC_API_KEY, or run `ant auth login`',
|
|
1114
|
+
checkedAt,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
try {
|
|
1118
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
1119
|
+
const client = new Anthropic({
|
|
1120
|
+
...(cfg.apiKey ? { apiKey: cfg.apiKey } : {}),
|
|
1121
|
+
timeout: 8_000,
|
|
1122
|
+
maxRetries: 0,
|
|
1123
|
+
});
|
|
1124
|
+
const model = apiJudgeModel(cfg);
|
|
1125
|
+
await client.models.retrieve(model);
|
|
1126
|
+
return {
|
|
1127
|
+
backend: 'api', status: 'ready', model,
|
|
1128
|
+
latencyMs: Date.now() - start, checkedAt,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
return {
|
|
1133
|
+
backend: 'api', status: 'unavailable',
|
|
1134
|
+
reason: `Anthropic API probe failed: ${String(err).slice(0, 200)}`,
|
|
1135
|
+
checkedAt,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return { backend: cfg.backend, status: 'unknown', checkedAt };
|
|
1140
|
+
}
|
|
1141
|
+
catch (err) {
|
|
1142
|
+
return {
|
|
1143
|
+
backend: cfg.backend, status: 'unavailable',
|
|
1144
|
+
reason: String(err).slice(0, 200),
|
|
1145
|
+
latencyMs: Date.now() - start,
|
|
1146
|
+
checkedAt,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
645
1149
|
}
|
|
646
1150
|
async function callMlx(prompt, cfg) {
|
|
647
1151
|
// MLX server speaks OpenAI chat-completions. The llm.mlx pin (shared with
|
|
@@ -691,6 +1195,95 @@ async function callMlx(prompt, cfg) {
|
|
|
691
1195
|
}
|
|
692
1196
|
return text;
|
|
693
1197
|
}
|
|
1198
|
+
/** Normalize a user-supplied base/endpoint to the chat-completions URL.
|
|
1199
|
+
* Accepts a bare host (`http://127.0.0.1:11434`), a base with `/v1`, or the
|
|
1200
|
+
* full `/v1/chat/completions` — all resolve to the same POST target. */
|
|
1201
|
+
export function openAIChatUrl(endpoint) {
|
|
1202
|
+
let e = endpoint.trim().replace(/\/+$/, '');
|
|
1203
|
+
if (/\/chat\/completions$/.test(e))
|
|
1204
|
+
return e;
|
|
1205
|
+
if (/\/v1$/.test(e))
|
|
1206
|
+
return `${e}/chat/completions`;
|
|
1207
|
+
return `${e}/v1/chat/completions`;
|
|
1208
|
+
}
|
|
1209
|
+
function openAIBase(endpoint) {
|
|
1210
|
+
return endpoint.trim().replace(/\/+$/, '')
|
|
1211
|
+
.replace(/\/chat\/completions$/, '')
|
|
1212
|
+
.replace(/\/v1$/, '');
|
|
1213
|
+
}
|
|
1214
|
+
/** Resolve a model id for an OpenAI-compatible server when the user left it
|
|
1215
|
+
* unset. Ollama exposes `/api/tags`; everything else exposes `/v1/models`. */
|
|
1216
|
+
async function resolveOpenAIModel(base, apiKey, configured) {
|
|
1217
|
+
if (configured && configured !== 'qwen3-30b' && configured !== 'default')
|
|
1218
|
+
return configured;
|
|
1219
|
+
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
|
1220
|
+
// Ollama first (its /v1/models also works, but /api/tags is the canonical list).
|
|
1221
|
+
try {
|
|
1222
|
+
const r = await fetch(`${base}/api/tags`, { headers, signal: AbortSignal.timeout(3000) }).catch(() => null);
|
|
1223
|
+
if (r?.ok) {
|
|
1224
|
+
const j = await r.json();
|
|
1225
|
+
const first = j.models?.find((m) => m.name)?.name;
|
|
1226
|
+
if (first)
|
|
1227
|
+
return first;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch { /* try openai path */ }
|
|
1231
|
+
for (const path of ['/v1/models', '/models']) {
|
|
1232
|
+
try {
|
|
1233
|
+
const r = await fetch(`${base}${path}`, { headers, signal: AbortSignal.timeout(3000) }).catch(() => null);
|
|
1234
|
+
if (r?.ok) {
|
|
1235
|
+
const j = await r.json();
|
|
1236
|
+
const first = j.data?.find((m) => m.id && !m.id.toLowerCase().includes('nanollava'))?.id;
|
|
1237
|
+
if (first)
|
|
1238
|
+
return first;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
catch { /* next */ }
|
|
1242
|
+
}
|
|
1243
|
+
return configured || 'default';
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Generic OpenAI-compatible chat-completions judge. One implementation covers
|
|
1247
|
+
* the de-facto standard local + cloud providers:
|
|
1248
|
+
* - Ollama endpoint http://127.0.0.1:11434/v1 (no key)
|
|
1249
|
+
* - LM Studio endpoint http://127.0.0.1:1234/v1 (no key)
|
|
1250
|
+
* - vLLM/llama.cpp/LiteLLM/MLX (local OpenAI servers, no key)
|
|
1251
|
+
* - OpenRouter endpoint https://openrouter.ai/api/v1 (Bearer apiKey)
|
|
1252
|
+
* - any other OpenAI-compatible endpoint
|
|
1253
|
+
* `apiKey` is sent as a Bearer only when set — local servers ignore it.
|
|
1254
|
+
*/
|
|
1255
|
+
async function callOpenAICompatible(prompt, cfg) {
|
|
1256
|
+
if (!cfg.endpoint) {
|
|
1257
|
+
throw new Error('openai judge: apme.judge.endpoint is required (e.g. http://127.0.0.1:11434/v1 for Ollama)');
|
|
1258
|
+
}
|
|
1259
|
+
const url = openAIChatUrl(cfg.endpoint);
|
|
1260
|
+
const base = openAIBase(cfg.endpoint);
|
|
1261
|
+
const model = await resolveOpenAIModel(base, cfg.apiKey, cfg.model);
|
|
1262
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
1263
|
+
if (cfg.apiKey)
|
|
1264
|
+
headers.Authorization = `Bearer ${cfg.apiKey}`;
|
|
1265
|
+
const resp = await fetch(url, {
|
|
1266
|
+
method: 'POST',
|
|
1267
|
+
headers,
|
|
1268
|
+
body: JSON.stringify({
|
|
1269
|
+
model,
|
|
1270
|
+
messages: [
|
|
1271
|
+
{ role: 'system', content: 'You are an exacting code evaluator. Reply with strict JSON only.' },
|
|
1272
|
+
{ role: 'user', content: prompt },
|
|
1273
|
+
],
|
|
1274
|
+
temperature: 0,
|
|
1275
|
+
max_tokens: 1024,
|
|
1276
|
+
}),
|
|
1277
|
+
signal: AbortSignal.timeout(90_000),
|
|
1278
|
+
});
|
|
1279
|
+
if (!resp.ok)
|
|
1280
|
+
throw new Error(`openai judge HTTP ${resp.status} (${url})`);
|
|
1281
|
+
const json = await resp.json();
|
|
1282
|
+
const text = json.choices?.[0]?.message?.content;
|
|
1283
|
+
if (typeof text !== 'string' || text.trim().length === 0)
|
|
1284
|
+
throw new Error('openai judge returned empty content');
|
|
1285
|
+
return text;
|
|
1286
|
+
}
|
|
694
1287
|
async function callOpenClaw(prompt, cfg) {
|
|
695
1288
|
// OpenClaw Gateway exposes the user's configured models. Route through it
|
|
696
1289
|
// when the user wants to reuse their existing subscription models for judge.
|
|
@@ -709,58 +1302,136 @@ async function callOpenClaw(prompt, cfg) {
|
|
|
709
1302
|
return json.text;
|
|
710
1303
|
}
|
|
711
1304
|
/**
|
|
712
|
-
* Route a judge call to
|
|
1305
|
+
* Route a judge call to Foundation Models.
|
|
713
1306
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
* ship Foundation Models — callers should either opt into `fallbackToMlx` or
|
|
719
|
-
* accept the resulting eval skip.
|
|
1307
|
+
* Prefer the Swift daemon HTTP adapter when it is running; otherwise use the
|
|
1308
|
+
* bundled CLI Swift helper process. Default CLI config enables
|
|
1309
|
+
* `fallbackToMlx`, while callers can set it false to force a skip when
|
|
1310
|
+
* neither Foundation Models path works.
|
|
720
1311
|
*
|
|
721
1312
|
* Shape contract:
|
|
722
1313
|
* Request : POST /apme/judge/foundation-models { prompt: string }
|
|
723
1314
|
* Response : { text: string } | { error: "unavailable", reason: string }
|
|
724
1315
|
*/
|
|
725
1316
|
async function callFoundationModels(prompt, cfg) {
|
|
1317
|
+
const explicitEndpoint = Boolean(cfg.endpoint);
|
|
1318
|
+
const cached = explicitEndpoint ? null : getFoundationModelsAutoCache();
|
|
1319
|
+
if (cached?.state === 'unavailable') {
|
|
1320
|
+
try {
|
|
1321
|
+
return await callFoundationModelsHelper(prompt);
|
|
1322
|
+
}
|
|
1323
|
+
catch (helperErr) {
|
|
1324
|
+
throw new Error(`foundationModels cached unavailable: ${cached.reason}; helper unavailable: ${String(helperErr)}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
726
1327
|
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'}`);
|
|
1328
|
+
if (!url) {
|
|
1329
|
+
try {
|
|
1330
|
+
return await callFoundationModelsHelper(prompt);
|
|
1331
|
+
}
|
|
1332
|
+
catch (helperErr) {
|
|
1333
|
+
throw new Error(`foundationModels: no Swift daemon found and helper unavailable: ${String(helperErr)}`);
|
|
1334
|
+
}
|
|
740
1335
|
}
|
|
741
|
-
|
|
742
|
-
|
|
1336
|
+
try {
|
|
1337
|
+
const resp = await fetch(url, {
|
|
1338
|
+
method: 'POST',
|
|
1339
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1340
|
+
body: JSON.stringify({ prompt }),
|
|
1341
|
+
signal: AbortSignal.timeout(60_000),
|
|
1342
|
+
});
|
|
1343
|
+
if (!resp.ok)
|
|
1344
|
+
throw new Error(`foundationModels HTTP ${resp.status}`);
|
|
1345
|
+
const json = await resp.json();
|
|
1346
|
+
if (json.error) {
|
|
1347
|
+
throw new Error(`foundationModels ${json.error}: ${json.reason ?? 'no reason'}`);
|
|
1348
|
+
}
|
|
1349
|
+
if (typeof json.text !== 'string' || json.text.length === 0) {
|
|
1350
|
+
throw new Error('foundationModels returned empty text');
|
|
1351
|
+
}
|
|
1352
|
+
if (!explicitEndpoint)
|
|
1353
|
+
markFoundationModelsAutoReady(url);
|
|
1354
|
+
return json.text;
|
|
1355
|
+
}
|
|
1356
|
+
catch (err) {
|
|
1357
|
+
if (!explicitEndpoint) {
|
|
1358
|
+
markFoundationModelsAutoUnavailable(String(err));
|
|
1359
|
+
try {
|
|
1360
|
+
return await callFoundationModelsHelper(prompt);
|
|
1361
|
+
}
|
|
1362
|
+
catch (helperErr) {
|
|
1363
|
+
throw new Error(`${String(err)}; helper unavailable: ${String(helperErr)}`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
throw err;
|
|
743
1367
|
}
|
|
744
|
-
return json.text;
|
|
745
1368
|
}
|
|
746
1369
|
/** Best-effort resolver for the Swift daemon's FM endpoint. Returns null when
|
|
747
1370
|
* no Swift daemon (httpPort ≠ port) is reachable. */
|
|
748
1371
|
async function resolveFoundationModelsUrl() {
|
|
1372
|
+
const cached = getFoundationModelsAutoCache();
|
|
1373
|
+
if (cached?.state === 'ready')
|
|
1374
|
+
return cached.url;
|
|
1375
|
+
if (cached?.state === 'unavailable')
|
|
1376
|
+
return null;
|
|
1377
|
+
if (foundationModelsResolveInFlight)
|
|
1378
|
+
return foundationModelsResolveInFlight;
|
|
749
1379
|
// Lazy-require to avoid pulling session-registry into every test bundle
|
|
750
1380
|
// that imports runner.ts for its pure helpers.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1381
|
+
foundationModelsResolveInFlight = (async () => {
|
|
1382
|
+
try {
|
|
1383
|
+
const { findDaemonPortAsync } = await import('../session-registry.js');
|
|
1384
|
+
const info = await findDaemonPortAsync();
|
|
1385
|
+
if (!info) {
|
|
1386
|
+
markFoundationModelsAutoUnavailable('Swift daemon not found', FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS);
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
const port = info.httpPort ?? info.port;
|
|
1390
|
+
const url = `http://127.0.0.1:${port}/apme/judge/foundation-models`;
|
|
1391
|
+
markFoundationModelsAutoReady(url);
|
|
1392
|
+
return url;
|
|
1393
|
+
}
|
|
1394
|
+
finally {
|
|
1395
|
+
foundationModelsResolveInFlight = null;
|
|
1396
|
+
}
|
|
1397
|
+
})();
|
|
1398
|
+
return foundationModelsResolveInFlight;
|
|
1399
|
+
}
|
|
1400
|
+
/** Default model for the opt-in Anthropic API judge when the configured
|
|
1401
|
+
* `model` belongs to another backend (e.g. an MLX id left over from a
|
|
1402
|
+
* backend switch). */
|
|
1403
|
+
const API_JUDGE_DEFAULT_MODEL = 'claude-opus-4-8';
|
|
1404
|
+
function apiJudgeModel(cfg) {
|
|
1405
|
+
return cfg.model && cfg.model.startsWith('claude') ? cfg.model : API_JUDGE_DEFAULT_MODEL;
|
|
1406
|
+
}
|
|
1407
|
+
async function callApi(prompt, cfg) {
|
|
1408
|
+
// Opt-in Anthropic API judge (mirrors the Swift ApmeJudgeApi adapter).
|
|
1409
|
+
// Credential chain: settings.json apme.judge.apiKey -> the SDK's standard
|
|
1410
|
+
// resolution (ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / `ant auth login`
|
|
1411
|
+
// profile). Hard client timeout — a wedged judge must never wedge an eval.
|
|
1412
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
1413
|
+
const client = new Anthropic({
|
|
1414
|
+
...(cfg.apiKey ? { apiKey: cfg.apiKey } : {}),
|
|
1415
|
+
timeout: 90_000,
|
|
1416
|
+
maxRetries: 1,
|
|
1417
|
+
});
|
|
1418
|
+
const response = await client.messages.create({
|
|
1419
|
+
model: apiJudgeModel(cfg),
|
|
1420
|
+
max_tokens: 8192,
|
|
1421
|
+
thinking: { type: 'adaptive' },
|
|
1422
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1423
|
+
});
|
|
1424
|
+
if (response.stop_reason === 'refusal') {
|
|
1425
|
+
throw new Error('API judge refused the request (stop_reason=refusal)');
|
|
1426
|
+
}
|
|
1427
|
+
const text = response.content
|
|
1428
|
+
.filter((b) => b.type === 'text')
|
|
1429
|
+
.map((b) => b.text)
|
|
1430
|
+
.join('\n')
|
|
1431
|
+
.trim();
|
|
1432
|
+
if (!text)
|
|
1433
|
+
throw new Error(`API judge returned no text (stop_reason=${response.stop_reason})`);
|
|
1434
|
+
return text;
|
|
764
1435
|
}
|
|
765
1436
|
export function parseJudgeJson(text) {
|
|
766
1437
|
// Models often wrap JSON in prose or code fences — grab the first {...} block.
|