agent-hitch 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -3
- package/README.zh-CN.md +14 -2
- package/dist/bin/hitch.js +25 -3
- package/dist/bin/hitch.js.map +1 -1
- package/dist/scripts/canary-benchmark.js +54 -0
- package/dist/scripts/canary-benchmark.js.map +1 -0
- package/dist/scripts/canary-buildkit-secrets.js +166 -0
- package/dist/scripts/canary-buildkit-secrets.js.map +1 -0
- package/dist/scripts/canary-collision-domains.js +77 -0
- package/dist/scripts/canary-collision-domains.js.map +1 -0
- package/dist/scripts/canary-docker-image-observation.js +181 -0
- package/dist/scripts/canary-docker-image-observation.js.map +1 -0
- package/dist/scripts/canary-eval-scheduler-throughput.js +47 -0
- package/dist/scripts/canary-eval-scheduler-throughput.js.map +1 -0
- package/dist/scripts/canary-gpu-hardware.js +95 -0
- package/dist/scripts/canary-gpu-hardware.js.map +1 -0
- package/dist/scripts/canary-harbor-load.js +365 -0
- package/dist/scripts/canary-harbor-load.js.map +1 -0
- package/dist/scripts/canary-packaged-harness.js +75 -0
- package/dist/scripts/canary-packaged-harness.js.map +1 -0
- package/dist/scripts/canary-resource-load.js +188 -0
- package/dist/scripts/canary-resource-load.js.map +1 -0
- package/dist/scripts/check-architecture.js +10 -5
- package/dist/scripts/check-architecture.js.map +1 -1
- package/dist/scripts/docker-reaper-crash-child.js +21 -0
- package/dist/scripts/docker-reaper-crash-child.js.map +1 -0
- package/dist/src/adapters/catalog.js +3 -1
- package/dist/src/adapters/catalog.js.map +1 -1
- package/dist/src/adapters/discovery.js +1 -1
- package/dist/src/adapters/discovery.js.map +1 -1
- package/dist/src/adapters/providers/claude.js +6 -0
- package/dist/src/adapters/providers/claude.js.map +1 -1
- package/dist/src/adapters/providers/codex-auth.js +29 -0
- package/dist/src/adapters/providers/codex-auth.js.map +1 -0
- package/dist/src/adapters/providers/codex.js +29 -6
- package/dist/src/adapters/providers/codex.js.map +1 -1
- package/dist/src/adapters/providers/deepseek.js +6 -0
- package/dist/src/adapters/providers/deepseek.js.map +1 -1
- package/dist/src/adapters/providers/model-call.js +26 -0
- package/dist/src/adapters/providers/model-call.js.map +1 -0
- package/dist/src/adapters/providers/opencode.js +6 -0
- package/dist/src/adapters/providers/opencode.js.map +1 -1
- package/dist/src/adapters/providers/pi.js +6 -0
- package/dist/src/adapters/providers/pi.js.map +1 -1
- package/dist/src/artifacts/index.js +1 -0
- package/dist/src/artifacts/index.js.map +1 -1
- package/dist/src/artifacts/preparer.js +7 -2
- package/dist/src/artifacts/preparer.js.map +1 -1
- package/dist/src/backends/harbor/agent-budget.js +31 -0
- package/dist/src/backends/harbor/agent-budget.js.map +1 -0
- package/dist/src/backends/harbor/backend.js +59 -107
- package/dist/src/backends/harbor/backend.js.map +1 -1
- package/dist/src/backends/harbor/environment-config.js +92 -0
- package/dist/src/backends/harbor/environment-config.js.map +1 -0
- package/dist/src/backends/harbor/index.js +6 -1
- package/dist/src/backends/harbor/index.js.map +1 -1
- package/dist/src/backends/harbor/model-proxy-config.js +23 -0
- package/dist/src/backends/harbor/model-proxy-config.js.map +1 -0
- package/dist/src/backends/harbor/process.js +177 -0
- package/dist/src/backends/harbor/process.js.map +1 -0
- package/dist/src/backends/harbor/regrade.js +77 -0
- package/dist/src/backends/harbor/regrade.js.map +1 -0
- package/dist/src/backends/harbor/runtime-toolchain.js +30 -0
- package/dist/src/backends/harbor/runtime-toolchain.js.map +1 -0
- package/dist/src/backends/harbor/supervisor.js +74 -0
- package/dist/src/backends/harbor/supervisor.js.map +1 -0
- package/dist/src/backends/harbor/task-resources.js +206 -0
- package/dist/src/backends/harbor/task-resources.js.map +1 -0
- package/dist/src/backends/harbor/tools.js +6 -29
- package/dist/src/backends/harbor/tools.js.map +1 -1
- package/dist/src/backends/index.js +5 -1
- package/dist/src/backends/index.js.map +1 -1
- package/dist/src/benchmarks/index.js +3 -0
- package/dist/src/benchmarks/index.js.map +1 -0
- package/dist/src/benchmarks/loader.js +271 -0
- package/dist/src/benchmarks/loader.js.map +1 -0
- package/dist/src/benchmarks/metrics.js +16 -0
- package/dist/src/benchmarks/metrics.js.map +1 -0
- package/dist/src/benchmarks/toml.js +13 -0
- package/dist/src/benchmarks/toml.js.map +1 -0
- package/dist/src/benchmarks/validation.js +242 -0
- package/dist/src/benchmarks/validation.js.map +1 -0
- package/dist/src/cli/arguments.js +6 -4
- package/dist/src/cli/arguments.js.map +1 -1
- package/dist/src/cli/commands/benchmark.js +22 -0
- package/dist/src/cli/commands/benchmark.js.map +1 -0
- package/dist/src/cli/commands/capabilities.js +18 -0
- package/dist/src/cli/commands/capabilities.js.map +1 -0
- package/dist/src/cli/commands/daemon.js +135 -8
- package/dist/src/cli/commands/daemon.js.map +1 -1
- package/dist/src/cli/commands/eval.js +228 -6
- package/dist/src/cli/commands/eval.js.map +1 -1
- package/dist/src/cli/commands/images.js +40 -0
- package/dist/src/cli/commands/images.js.map +1 -0
- package/dist/src/cli/commands/run.js +42 -17
- package/dist/src/cli/commands/run.js.map +1 -1
- package/dist/src/cli/commands/runs.js +25 -3
- package/dist/src/cli/commands/runs.js.map +1 -1
- package/dist/src/cli/commands/trajectory.js +96 -5
- package/dist/src/cli/commands/trajectory.js.map +1 -1
- package/dist/src/cli/commands/verifier.js +36 -0
- package/dist/src/cli/commands/verifier.js.map +1 -0
- package/dist/src/cli/commands/worker.js +79 -0
- package/dist/src/cli/commands/worker.js.map +1 -0
- package/dist/src/cli/main.js +10 -0
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/output.js +84 -4
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/control-plane/build-admission.js +69 -0
- package/dist/src/control-plane/build-admission.js.map +1 -0
- package/dist/src/control-plane/build-records.js +6 -0
- package/dist/src/control-plane/build-records.js.map +1 -0
- package/dist/src/control-plane/collisions.js +40 -0
- package/dist/src/control-plane/collisions.js.map +1 -0
- package/dist/src/control-plane/eval-control-state.js +22 -0
- package/dist/src/control-plane/eval-control-state.js.map +1 -0
- package/dist/src/control-plane/eval-control-work.js +44 -0
- package/dist/src/control-plane/eval-control-work.js.map +1 -0
- package/dist/src/control-plane/eval-image-resolution.js +52 -0
- package/dist/src/control-plane/eval-image-resolution.js.map +1 -0
- package/dist/src/control-plane/eval-image-services.js +30 -0
- package/dist/src/control-plane/eval-image-services.js.map +1 -0
- package/dist/src/control-plane/eval-records.js +252 -0
- package/dist/src/control-plane/eval-records.js.map +1 -0
- package/dist/src/control-plane/eval-recovery.js +181 -0
- package/dist/src/control-plane/eval-recovery.js.map +1 -0
- package/dist/src/control-plane/eval-scheduler.js +450 -0
- package/dist/src/control-plane/eval-scheduler.js.map +1 -0
- package/dist/src/control-plane/index.js +20 -0
- package/dist/src/control-plane/index.js.map +1 -0
- package/dist/src/control-plane/local-worker.js +49 -0
- package/dist/src/control-plane/local-worker.js.map +1 -0
- package/dist/src/control-plane/model-capture-planning.js +12 -0
- package/dist/src/control-plane/model-capture-planning.js.map +1 -0
- package/dist/src/control-plane/remote-result-transport.js +175 -0
- package/dist/src/control-plane/remote-result-transport.js.map +1 -0
- package/dist/src/control-plane/remote-work-coordinator.js +333 -0
- package/dist/src/control-plane/remote-work-coordinator.js.map +1 -0
- package/dist/src/control-plane/remote-work-inputs.js +266 -0
- package/dist/src/control-plane/remote-work-inputs.js.map +1 -0
- package/dist/src/control-plane/remote-work-item.js +62 -0
- package/dist/src/control-plane/remote-work-item.js.map +1 -0
- package/dist/src/control-plane/remote-work-recovery.js +220 -0
- package/dist/src/control-plane/remote-work-recovery.js.map +1 -0
- package/dist/src/control-plane/remote-worker-artifacts.js +135 -0
- package/dist/src/control-plane/remote-worker-artifacts.js.map +1 -0
- package/dist/src/control-plane/remote-worker-client.js +202 -0
- package/dist/src/control-plane/remote-worker-client.js.map +1 -0
- package/dist/src/control-plane/remote-worker-configuration.js +34 -0
- package/dist/src/control-plane/remote-worker-configuration.js.map +1 -0
- package/dist/src/control-plane/remote-worker-credentials.js +58 -0
- package/dist/src/control-plane/remote-worker-credentials.js.map +1 -0
- package/dist/src/control-plane/remote-worker-protocol.js +489 -0
- package/dist/src/control-plane/remote-worker-protocol.js.map +1 -0
- package/dist/src/control-plane/remote-worker-runner.js +214 -0
- package/dist/src/control-plane/remote-worker-runner.js.map +1 -0
- package/dist/src/control-plane/remote-workers.js +377 -0
- package/dist/src/control-plane/remote-workers.js.map +1 -0
- package/dist/src/control-plane/rerun-scheduler.js +448 -0
- package/dist/src/control-plane/rerun-scheduler.js.map +1 -0
- package/dist/src/control-plane/rerun-submission.js +62 -0
- package/dist/src/control-plane/rerun-submission.js.map +1 -0
- package/dist/src/control-plane/resources.js +182 -0
- package/dist/src/control-plane/resources.js.map +1 -0
- package/dist/src/control-plane/scheduler-eval-entry.js +32 -0
- package/dist/src/control-plane/scheduler-eval-entry.js.map +1 -0
- package/dist/src/control-plane/synthetic-result.js +21 -0
- package/dist/src/control-plane/synthetic-result.js.map +1 -0
- package/dist/src/control-plane/work-admission.js +23 -0
- package/dist/src/control-plane/work-admission.js.map +1 -0
- package/dist/src/control-plane/work-dispatcher.js +237 -0
- package/dist/src/control-plane/work-dispatcher.js.map +1 -0
- package/dist/src/controller-runtime/hash.js +19 -3
- package/dist/src/controller-runtime/hash.js.map +1 -1
- package/dist/src/controller-runtime/index.js +1 -1
- package/dist/src/controller-runtime/index.js.map +1 -1
- package/dist/src/controller-runtime/store.js +15 -6
- package/dist/src/controller-runtime/store.js.map +1 -1
- package/dist/src/daemon/auth.js +5 -0
- package/dist/src/daemon/auth.js.map +1 -1
- package/dist/src/daemon/client.js.map +1 -1
- package/dist/src/daemon/event-stream.js +46 -0
- package/dist/src/daemon/event-stream.js.map +1 -0
- package/dist/src/daemon/health.js +66 -0
- package/dist/src/daemon/health.js.map +1 -0
- package/dist/src/daemon/launcher.js +28 -1
- package/dist/src/daemon/launcher.js.map +1 -1
- package/dist/src/daemon/logging.js +23 -0
- package/dist/src/daemon/logging.js.map +1 -0
- package/dist/src/daemon/scheduler.js +26 -5
- package/dist/src/daemon/scheduler.js.map +1 -1
- package/dist/src/daemon/server.js +255 -98
- package/dist/src/daemon/server.js.map +1 -1
- package/dist/src/daemon/telemetry.js +163 -0
- package/dist/src/daemon/telemetry.js.map +1 -0
- package/dist/src/daemon/worker-routes.js +219 -0
- package/dist/src/daemon/worker-routes.js.map +1 -0
- package/dist/src/domain/benchmarks.js +2 -0
- package/dist/src/domain/benchmarks.js.map +1 -0
- package/dist/src/domain/bundles.js +2 -0
- package/dist/src/domain/bundles.js.map +1 -0
- package/dist/src/domain/control-plane.js +2 -0
- package/dist/src/domain/control-plane.js.map +1 -0
- package/dist/src/domain/execution-evidence.js +2 -0
- package/dist/src/domain/execution-evidence.js.map +1 -0
- package/dist/src/domain/execution-plan.js +2 -0
- package/dist/src/domain/execution-plan.js.map +1 -0
- package/dist/src/domain/images.js +2 -0
- package/dist/src/domain/images.js.map +1 -0
- package/dist/src/domain/index.js +13 -0
- package/dist/src/domain/index.js.map +1 -1
- package/dist/src/domain/interactions.js +2 -0
- package/dist/src/domain/interactions.js.map +1 -0
- package/dist/src/domain/providers.js +2 -0
- package/dist/src/domain/providers.js.map +1 -0
- package/dist/src/domain/resources.js +2 -0
- package/dist/src/domain/resources.js.map +1 -0
- package/dist/src/domain/runs.js.map +1 -1
- package/dist/src/domain/validation.js +11 -4
- package/dist/src/domain/validation.js.map +1 -1
- package/dist/src/domain/verifier-evidence-validation.js +214 -0
- package/dist/src/domain/verifier-evidence-validation.js.map +1 -0
- package/dist/src/domain/verifier-evidence.js +3 -0
- package/dist/src/domain/verifier-evidence.js.map +1 -0
- package/dist/src/domain/verifier-score-contract.js +232 -0
- package/dist/src/domain/verifier-score-contract.js.map +1 -0
- package/dist/src/domain/workers.js +2 -0
- package/dist/src/domain/workers.js.map +1 -0
- package/dist/src/evals/benchmark-adapter-manifest.js +219 -0
- package/dist/src/evals/benchmark-adapter-manifest.js.map +1 -0
- package/dist/src/evals/benchmark-candidate.js +36 -0
- package/dist/src/evals/benchmark-candidate.js.map +1 -0
- package/dist/src/evals/benchmark-run.js +166 -0
- package/dist/src/evals/benchmark-run.js.map +1 -0
- package/dist/src/evals/collect-only-rerun.js +183 -0
- package/dist/src/evals/collect-only-rerun.js.map +1 -0
- package/dist/src/evals/directory.js +30 -0
- package/dist/src/evals/directory.js.map +1 -0
- package/dist/src/evals/docker-engine-stats.js +89 -0
- package/dist/src/evals/docker-engine-stats.js.map +1 -0
- package/dist/src/evals/docker-ownership.js +51 -0
- package/dist/src/evals/docker-ownership.js.map +1 -0
- package/dist/src/evals/docker-reaper.js +144 -0
- package/dist/src/evals/docker-reaper.js.map +1 -0
- package/dist/src/evals/docker-resource-observer.js +207 -0
- package/dist/src/evals/docker-resource-observer.js.map +1 -0
- package/dist/src/evals/duration-estimator.js +126 -0
- package/dist/src/evals/duration-estimator.js.map +1 -0
- package/dist/src/evals/environment-image-planning.js +164 -0
- package/dist/src/evals/environment-image-planning.js.map +1 -0
- package/dist/src/evals/eval-artifact-planning.js +56 -0
- package/dist/src/evals/eval-artifact-planning.js.map +1 -0
- package/dist/src/evals/eval-finalization.js +10 -0
- package/dist/src/evals/eval-finalization.js.map +1 -0
- package/dist/src/evals/eval-lifecycle-events.js +29 -0
- package/dist/src/evals/eval-lifecycle-events.js.map +1 -0
- package/dist/src/evals/eval-logical-plan.js +51 -0
- package/dist/src/evals/eval-logical-plan.js.map +1 -0
- package/dist/src/evals/eval-result-builder.js +81 -0
- package/dist/src/evals/eval-result-builder.js.map +1 -0
- package/dist/src/evals/events.js +53 -5
- package/dist/src/evals/events.js.map +1 -1
- package/dist/src/evals/evolution-baseline.js +191 -0
- package/dist/src/evals/evolution-baseline.js.map +1 -0
- package/dist/src/evals/execution-evidence.js +116 -0
- package/dist/src/evals/execution-evidence.js.map +1 -0
- package/dist/src/evals/execution-leases.js +342 -0
- package/dist/src/evals/execution-leases.js.map +1 -0
- package/dist/src/evals/execution-plan-artifacts.js +72 -0
- package/dist/src/evals/execution-plan-artifacts.js.map +1 -0
- package/dist/src/evals/execution-plan-images.js +69 -0
- package/dist/src/evals/execution-plan-images.js.map +1 -0
- package/dist/src/evals/execution-plan-resources.js +174 -0
- package/dist/src/evals/execution-plan-resources.js.map +1 -0
- package/dist/src/evals/execution-plan.js +451 -0
- package/dist/src/evals/execution-plan.js.map +1 -0
- package/dist/src/evals/failure-classifier.js +61 -0
- package/dist/src/evals/failure-classifier.js.map +1 -0
- package/dist/src/evals/harbor-artifact-builder-image.js +99 -0
- package/dist/src/evals/harbor-artifact-builder-image.js.map +1 -0
- package/dist/src/evals/harbor-artifact-builder.js +358 -0
- package/dist/src/evals/harbor-artifact-builder.js.map +1 -0
- package/dist/src/evals/harbor-bridge-error.js +58 -3
- package/dist/src/evals/harbor-bridge-error.js.map +1 -1
- package/dist/src/evals/harbor-node-runtime.js +146 -0
- package/dist/src/evals/harbor-node-runtime.js.map +1 -0
- package/dist/src/evals/harbor-phase-timings.js +60 -0
- package/dist/src/evals/harbor-phase-timings.js.map +1 -0
- package/dist/src/evals/index.js +40 -2
- package/dist/src/evals/index.js.map +1 -1
- package/dist/src/evals/infrastructure-retry.js +208 -84
- package/dist/src/evals/infrastructure-retry.js.map +1 -1
- package/dist/src/evals/interaction-capture-import.js +49 -0
- package/dist/src/evals/interaction-capture-import.js.map +1 -0
- package/dist/src/evals/local-docker-provider.js +372 -0
- package/dist/src/evals/local-docker-provider.js.map +1 -0
- package/dist/src/evals/local-eval-planning.js +80 -0
- package/dist/src/evals/local-eval-planning.js.map +1 -0
- package/dist/src/evals/model-capture-plan.js +22 -0
- package/dist/src/evals/model-capture-plan.js.map +1 -0
- package/dist/src/evals/model-capture-runtime.js +114 -0
- package/dist/src/evals/model-capture-runtime.js.map +1 -0
- package/dist/src/evals/model-proxy-runtime-state.js +53 -0
- package/dist/src/evals/model-proxy-runtime-state.js.map +1 -0
- package/dist/src/evals/native-phase-evidence.js +440 -0
- package/dist/src/evals/native-phase-evidence.js.map +1 -0
- package/dist/src/evals/physical-retry-work.js +31 -0
- package/dist/src/evals/physical-retry-work.js.map +1 -0
- package/dist/src/evals/planned-execution-support.js +66 -0
- package/dist/src/evals/planned-execution-support.js.map +1 -0
- package/dist/src/evals/planned-execution.js +469 -0
- package/dist/src/evals/planned-execution.js.map +1 -0
- package/dist/src/evals/planned-progress-publisher.js +62 -0
- package/dist/src/evals/planned-progress-publisher.js.map +1 -0
- package/dist/src/evals/planned-remote-execution.js +40 -0
- package/dist/src/evals/planned-remote-execution.js.map +1 -0
- package/dist/src/evals/planned-retry-execution.js +174 -0
- package/dist/src/evals/planned-retry-execution.js.map +1 -0
- package/dist/src/evals/planned-retry-lifecycle.js +188 -0
- package/dist/src/evals/planned-retry-lifecycle.js.map +1 -0
- package/dist/src/evals/preparation-rerun.js +225 -0
- package/dist/src/evals/preparation-rerun.js.map +1 -0
- package/dist/src/evals/prepared-harness.js +76 -0
- package/dist/src/evals/prepared-harness.js.map +1 -0
- package/dist/src/evals/progress.js +52 -12
- package/dist/src/evals/progress.js.map +1 -1
- package/dist/src/evals/records.js +20 -3
- package/dist/src/evals/records.js.map +1 -1
- package/dist/src/evals/recovery.js +242 -0
- package/dist/src/evals/recovery.js.map +1 -0
- package/dist/src/evals/regrade-evidence.js +77 -0
- package/dist/src/evals/regrade-evidence.js.map +1 -0
- package/dist/src/evals/remote-infrastructure-retry.js +165 -0
- package/dist/src/evals/remote-infrastructure-retry.js.map +1 -0
- package/dist/src/evals/request.js +21 -3
- package/dist/src/evals/request.js.map +1 -1
- package/dist/src/evals/rerun-inputs.js +108 -0
- package/dist/src/evals/rerun-inputs.js.map +1 -0
- package/dist/src/evals/rerun-slots.js +4 -3
- package/dist/src/evals/rerun-slots.js.map +1 -1
- package/dist/src/evals/rerun-types.js +45 -0
- package/dist/src/evals/rerun-types.js.map +1 -0
- package/dist/src/evals/rerun.js +133 -88
- package/dist/src/evals/rerun.js.map +1 -1
- package/dist/src/evals/result-helpers.js +78 -0
- package/dist/src/evals/result-helpers.js.map +1 -0
- package/dist/src/evals/resume-state.js +36 -0
- package/dist/src/evals/resume-state.js.map +1 -0
- package/dist/src/evals/retry-backoff.js +20 -0
- package/dist/src/evals/retry-backoff.js.map +1 -0
- package/dist/src/evals/retry-state.js +195 -0
- package/dist/src/evals/retry-state.js.map +1 -0
- package/dist/src/evals/scheduler-metrics.js +95 -0
- package/dist/src/evals/scheduler-metrics.js.map +1 -0
- package/dist/src/evals/scheduler-trace-replay.js +107 -0
- package/dist/src/evals/scheduler-trace-replay.js.map +1 -0
- package/dist/src/evals/service-types.js +2 -0
- package/dist/src/evals/service-types.js.map +1 -0
- package/dist/src/evals/service.js +331 -347
- package/dist/src/evals/service.js.map +1 -1
- package/dist/src/evals/task-resources.js +209 -0
- package/dist/src/evals/task-resources.js.map +1 -0
- package/dist/src/evals/trial-environment-evidence.js +87 -0
- package/dist/src/evals/trial-environment-evidence.js.map +1 -0
- package/dist/src/evals/trial-execution-evidence.js +13 -0
- package/dist/src/evals/trial-execution-evidence.js.map +1 -0
- package/dist/src/evals/trial-import-identity.js +31 -0
- package/dist/src/evals/trial-import-identity.js.map +1 -0
- package/dist/src/evals/trial-import.js +183 -188
- package/dist/src/evals/trial-import.js.map +1 -1
- package/dist/src/evals/trial-publication-recovery.js +144 -0
- package/dist/src/evals/trial-publication-recovery.js.map +1 -0
- package/dist/src/evals/trial-publication.js +25 -0
- package/dist/src/evals/trial-publication.js.map +1 -0
- package/dist/src/evals/trial-reference-validation.js +49 -0
- package/dist/src/evals/trial-reference-validation.js.map +1 -0
- package/dist/src/evals/verifier-artifacts.js +317 -0
- package/dist/src/evals/verifier-artifacts.js.map +1 -0
- package/dist/src/evals/verifier-eligibility.js +25 -0
- package/dist/src/evals/verifier-eligibility.js.map +1 -0
- package/dist/src/evals/verifier-only-rerun.js +210 -0
- package/dist/src/evals/verifier-only-rerun.js.map +1 -0
- package/dist/src/evals/verifier-runtime.js +61 -0
- package/dist/src/evals/verifier-runtime.js.map +1 -0
- package/dist/src/evals/verifier-score-artifacts.js +119 -0
- package/dist/src/evals/verifier-score-artifacts.js.map +1 -0
- package/dist/src/evals/work-item-artifacts.js +23 -0
- package/dist/src/evals/work-item-artifacts.js.map +1 -0
- package/dist/src/foundation/config.js +17 -0
- package/dist/src/foundation/config.js.map +1 -1
- package/dist/src/foundation/contained-file.js +85 -0
- package/dist/src/foundation/contained-file.js.map +1 -0
- package/dist/src/foundation/credential-redaction.js +162 -0
- package/dist/src/foundation/credential-redaction.js.map +1 -0
- package/dist/src/foundation/environment-image-references.js +59 -0
- package/dist/src/foundation/environment-image-references.js.map +1 -0
- package/dist/src/foundation/executable.js +45 -5
- package/dist/src/foundation/executable.js.map +1 -1
- package/dist/src/foundation/fs.js +2 -1
- package/dist/src/foundation/fs.js.map +1 -1
- package/dist/src/foundation/index.js +6 -2
- package/dist/src/foundation/index.js.map +1 -1
- package/dist/src/foundation/process-identity.js +69 -0
- package/dist/src/foundation/process-identity.js.map +1 -0
- package/dist/src/images/context.js +47 -0
- package/dist/src/images/context.js.map +1 -0
- package/dist/src/images/docker-buildkit.js +137 -0
- package/dist/src/images/docker-buildkit.js.map +1 -0
- package/dist/src/images/dockerfile.js +67 -0
- package/dist/src/images/dockerfile.js.map +1 -0
- package/dist/src/images/gc.js +261 -0
- package/dist/src/images/gc.js.map +1 -0
- package/dist/src/images/index.js +10 -0
- package/dist/src/images/index.js.map +1 -0
- package/dist/src/images/manifest.js +109 -0
- package/dist/src/images/manifest.js.map +1 -0
- package/dist/src/images/ownership.js +5 -0
- package/dist/src/images/ownership.js.map +1 -0
- package/dist/src/images/records.js +32 -0
- package/dist/src/images/records.js.map +1 -0
- package/dist/src/images/registry.js +154 -0
- package/dist/src/images/registry.js.map +1 -0
- package/dist/src/images/service.js +224 -0
- package/dist/src/images/service.js.map +1 -0
- package/dist/src/model-access/capture.js +255 -0
- package/dist/src/model-access/capture.js.map +1 -0
- package/dist/src/model-access/index.js +5 -0
- package/dist/src/model-access/index.js.map +1 -0
- package/dist/src/model-access/policy.js +49 -0
- package/dist/src/model-access/policy.js.map +1 -0
- package/dist/src/model-access/proxy.js +336 -0
- package/dist/src/model-access/proxy.js.map +1 -0
- package/dist/src/model-access/records.js +167 -0
- package/dist/src/model-access/records.js.map +1 -0
- package/dist/src/runs/adapter-process.js +12 -0
- package/dist/src/runs/adapter-process.js.map +1 -0
- package/dist/src/runs/bundle.js +450 -0
- package/dist/src/runs/bundle.js.map +1 -0
- package/dist/src/runs/events.js +12 -1
- package/dist/src/runs/events.js.map +1 -1
- package/dist/src/runs/executor.js +46 -45
- package/dist/src/runs/executor.js.map +1 -1
- package/dist/src/runs/finalizer.js +17 -0
- package/dist/src/runs/finalizer.js.map +1 -1
- package/dist/src/runs/index.js +8 -1
- package/dist/src/runs/index.js.map +1 -1
- package/dist/src/runs/manifest.js +5 -5
- package/dist/src/runs/manifest.js.map +1 -1
- package/dist/src/runs/phase-bundle.js +86 -0
- package/dist/src/runs/phase-bundle.js.map +1 -0
- package/dist/src/runs/phase-cancellation.js +107 -0
- package/dist/src/runs/phase-cancellation.js.map +1 -0
- package/dist/src/runs/phase-group.js +113 -0
- package/dist/src/runs/phase-group.js.map +1 -0
- package/dist/src/runs/query.js +1 -1
- package/dist/src/runs/query.js.map +1 -1
- package/dist/src/runs/records.js +3 -0
- package/dist/src/runs/records.js.map +1 -1
- package/dist/src/runs/request.js +10 -3
- package/dist/src/runs/request.js.map +1 -1
- package/dist/src/runs/training-candidate.js +161 -0
- package/dist/src/runs/training-candidate.js.map +1 -0
- package/dist/src/runs/verifier-evidence-redaction.js +61 -0
- package/dist/src/runs/verifier-evidence-redaction.js.map +1 -0
- package/dist/src/runs/verifier-evidence.js +459 -0
- package/dist/src/runs/verifier-evidence.js.map +1 -0
- package/dist/src/runs/verifier-structured-evidence.js +83 -0
- package/dist/src/runs/verifier-structured-evidence.js.map +1 -0
- package/dist/src/trajectories/analysis.js +288 -0
- package/dist/src/trajectories/analysis.js.map +1 -0
- package/dist/src/trajectories/chunk-projection.js +148 -0
- package/dist/src/trajectories/chunk-projection.js.map +1 -0
- package/dist/src/trajectories/content-projection.js +389 -0
- package/dist/src/trajectories/content-projection.js.map +1 -0
- package/dist/src/trajectories/dsh-chunk-contract.js +176 -0
- package/dist/src/trajectories/dsh-chunk-contract.js.map +1 -0
- package/dist/src/trajectories/dsh-contract.js +284 -0
- package/dist/src/trajectories/dsh-contract.js.map +1 -0
- package/dist/src/trajectories/events-chunk-drill.js +95 -0
- package/dist/src/trajectories/events-chunk-drill.js.map +1 -0
- package/dist/src/trajectories/events-page.js +367 -0
- package/dist/src/trajectories/events-page.js.map +1 -0
- package/dist/src/trajectories/index.js +3 -0
- package/dist/src/trajectories/index.js.map +1 -1
- package/dist/src/trajectories/provider-capture.js +15 -24
- package/dist/src/trajectories/provider-capture.js.map +1 -1
- package/dist/src/trajectories/providers/deepseek.js +3 -3
- package/dist/src/trajectories/providers/deepseek.js.map +1 -1
- package/dist/src/trajectories/request-attempt.js +47 -0
- package/dist/src/trajectories/request-attempt.js.map +1 -0
- package/dist/src/trajectories/stream-reader.js +311 -0
- package/dist/src/trajectories/stream-reader.js.map +1 -0
- package/dist/src/trajectories/surface-fold.js +126 -0
- package/dist/src/trajectories/surface-fold.js.map +1 -0
- package/dist/src/workers/index.js +3 -0
- package/dist/src/workers/index.js.map +1 -0
- package/dist/src/workers/remote-harbor-work-spec.js +97 -0
- package/dist/src/workers/remote-harbor-work-spec.js.map +1 -0
- package/dist/src/workers/remote-harbor-worker.js +223 -0
- package/dist/src/workers/remote-harbor-worker.js.map +1 -0
- package/docs/schemas/benchmark-hook-request-v1.schema.json +65 -0
- package/docs/schemas/benchmark-hook-response-v1.schema.json +75 -0
- package/docs/schemas/benchmark-hook-v1.schema.json +33 -0
- package/docs/schemas/benchmark-lock-v1.schema.json +351 -0
- package/docs/schemas/benchmark-metric-v1.schema.json +37 -0
- package/docs/schemas/benchmark-package-v1.schema.json +167 -0
- package/docs/schemas/benchmark-phase-group.schema.json +41 -0
- package/docs/schemas/benchmark-profile-v1.schema.json +141 -0
- package/docs/schemas/benchmark-task-v1.schema.json +384 -0
- package/docs/schemas/benchmark-tool-result-v1.schema.json +30 -0
- package/docs/schemas/environment-build-index.schema.json +13 -0
- package/docs/schemas/environment-build-record.schema.json +25 -0
- package/docs/schemas/environment-image-manifest.schema.json +62 -0
- package/docs/schemas/error.schema.json +22 -0
- package/docs/schemas/eval-control.schema.json +36 -0
- package/docs/schemas/eval-execution-plan.schema.json +216 -0
- package/docs/schemas/eval-progress.schema.json +1 -15
- package/docs/schemas/eval-rerun-result.schema.json +58 -0
- package/docs/schemas/eval-rerun-state.schema.json +61 -0
- package/docs/schemas/eval-rerun-submission.schema.json +60 -0
- package/docs/schemas/eval-result.schema.json +32 -13
- package/docs/schemas/eval-submission-input.schema.json +14 -0
- package/docs/schemas/eval-submission.schema.json +68 -0
- package/docs/schemas/eval-trial-publication.schema.json +15 -0
- package/docs/schemas/eval-trial-reference.schema.json +129 -0
- package/docs/schemas/execution-evidence.schema.json +96 -0
- package/docs/schemas/execution-lease.schema.json +50 -0
- package/docs/schemas/execution-provider-status.schema.json +65 -0
- package/docs/schemas/execution-worker.schema.json +53 -0
- package/docs/schemas/harbor-process-exit.schema.json +14 -0
- package/docs/schemas/interaction-capture-ref.schema.json +45 -0
- package/docs/schemas/local-provider-execution.schema.json +33 -0
- package/docs/schemas/model-interaction.schema.json +41 -0
- package/docs/schemas/regrade-assessment-reference.schema.json +11 -0
- package/docs/schemas/remote-result-envelope.schema.json +32 -0
- package/docs/schemas/remote-tree-envelope.schema.json +25 -0
- package/docs/schemas/remote-work-offer.schema.json +58 -0
- package/docs/schemas/remote-work-receipt.schema.json +50 -0
- package/docs/schemas/remote-worker-artifact.schema.json +17 -0
- package/docs/schemas/remote-worker-event.schema.json +18 -0
- package/docs/schemas/remote-worker-heartbeat.schema.json +37 -0
- package/docs/schemas/remote-worker-registration.schema.json +52 -0
- package/docs/schemas/result-bundle-index.schema.json +133 -0
- package/docs/schemas/run-context.schema.json +15 -0
- package/docs/schemas/run-environment-images.schema.json +39 -0
- package/docs/schemas/run-manifest.schema.json +4 -0
- package/docs/schemas/run-request.schema.json +4 -0
- package/docs/schemas/training-data-candidate.schema.json +46 -0
- package/docs/schemas/trajectory-analysis.schema.json +258 -0
- package/docs/schemas/trajectory-events-page.schema.json +40 -0
- package/docs/schemas/verifier-assessment.schema.json +77 -0
- package/docs/schemas/verifier-evidence.schema.json +276 -0
- package/integrations/harbor/hitch_benchmark.py +252 -0
- package/integrations/harbor/hitch_candidate_recycle.py +262 -0
- package/integrations/harbor/hitch_harbor_agent.py +698 -568
- package/integrations/harbor/hitch_harbor_environment.py +469 -0
- package/integrations/harbor/hitch_harbor_task_resources.py +275 -0
- package/integrations/harbor/hitch_harbor_verifier.py +83 -2
- package/integrations/harbor/hitch_phase_supervisor.py +452 -0
- package/integrations/harbor/hitch_tool_client.mjs +91 -0
- package/integrations/model-call/cli.js +57 -0
- package/package.json +30 -6
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Trusted native-phase orchestration; no SDK imports or benchmark scoring.
|
|
2
|
+
|
|
3
|
+
The private controller implements state/bind/cancel. Its native SDK owns task
|
|
4
|
+
progress and grading. This worker owns candidate run identities and container
|
|
5
|
+
replacement, and records candidate evidence only. Call after initial agent setup
|
|
6
|
+
and controller prepare. A failure requires whole-trial cleanup, never a retry of
|
|
7
|
+
the same conversation. Public package/assessment integration is a separate layer.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import re
|
|
18
|
+
import shlex
|
|
19
|
+
import shutil
|
|
20
|
+
import tempfile
|
|
21
|
+
import time
|
|
22
|
+
from urllib.parse import urlparse
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
from harbor.models.agent.context import AgentContext
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PhaseSupervisionError(RuntimeError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _json(text):
|
|
33
|
+
def pairs(items):
|
|
34
|
+
value = {}
|
|
35
|
+
for key, item in items:
|
|
36
|
+
if key in value:
|
|
37
|
+
raise ValueError("duplicate JSON field")
|
|
38
|
+
value[key] = item
|
|
39
|
+
return value
|
|
40
|
+
def constant(_):
|
|
41
|
+
raise ValueError("non-finite JSON number")
|
|
42
|
+
return json.loads(text, object_pairs_hook=pairs, parse_constant=constant)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _positive(value, maximum):
|
|
46
|
+
return type(value) is int and 1 <= value <= maximum
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _digest(value):
|
|
50
|
+
return isinstance(value, str) and re.fullmatch(r"sha256:[a-f0-9]{64}", value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _write(path, value):
|
|
54
|
+
temporary = path.with_suffix(".pending")
|
|
55
|
+
with temporary.open("x", encoding="utf-8") as stream:
|
|
56
|
+
os.chmod(temporary, 0o600)
|
|
57
|
+
json.dump(value, stream, sort_keys=True, ensure_ascii=False, allow_nan=False)
|
|
58
|
+
stream.write("\n")
|
|
59
|
+
stream.flush()
|
|
60
|
+
os.fsync(stream.fileno())
|
|
61
|
+
temporary.replace(path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NativePhaseSupervisor:
|
|
65
|
+
def __init__(self, agent, environment, *, controller, binding, task_digest,
|
|
66
|
+
timeout_ms, shutdown_timeout_ms=30_000, poll_interval_ms=250,
|
|
67
|
+
run_group_id=None, finalization_timeout_ms=None, task_instruction=""):
|
|
68
|
+
"""controller={service, argv}; binding={endpoint, tools}, both locked.
|
|
69
|
+
|
|
70
|
+
timeout_ms is the remaining *whole task* allowance, not a phase budget.
|
|
71
|
+
Shutdown/collection has a distinct allowance and cannot start a model.
|
|
72
|
+
Management credentials stay inside the controller's private session file.
|
|
73
|
+
"""
|
|
74
|
+
self.agent, self.env = agent, environment
|
|
75
|
+
if not agent._setup_complete or not agent._phase_supervision_available:
|
|
76
|
+
raise ValueError("native phase supervision requires setup with a compatible frozen runtime")
|
|
77
|
+
if (not isinstance(controller, dict) or set(controller) != {"service", "argv"}
|
|
78
|
+
or not isinstance(controller["service"], str) or controller["service"] == "main"
|
|
79
|
+
or not re.fullmatch(r"[a-z][a-z0-9_-]*", controller["service"])
|
|
80
|
+
or not isinstance(controller["argv"], list) or not 1 <= len(controller["argv"]) <= 32
|
|
81
|
+
or any(not isinstance(v, str) or not v or "\0" in v or len(v) > 8192 for v in controller["argv"])):
|
|
82
|
+
raise ValueError("invalid locked native controller command")
|
|
83
|
+
if not isinstance(binding, dict) or set(binding) != {"endpoint", "tools"}:
|
|
84
|
+
raise ValueError("invalid locked native tool definition")
|
|
85
|
+
endpoint = urlparse(binding["endpoint"])
|
|
86
|
+
if (endpoint.scheme != "http" or endpoint.hostname != controller["service"] or endpoint.path != "/"
|
|
87
|
+
or endpoint.username or endpoint.password or endpoint.query or endpoint.fragment
|
|
88
|
+
or not isinstance(binding["tools"], list) or not binding["tools"]):
|
|
89
|
+
raise ValueError("native tools must belong to the locked private controller service")
|
|
90
|
+
if (not _digest(task_digest) or not _positive(timeout_ms, 9007199254740991)
|
|
91
|
+
or not _positive(shutdown_timeout_ms, 600_000) or not _positive(poll_interval_ms, 5000)):
|
|
92
|
+
raise ValueError("invalid native task identity or budget")
|
|
93
|
+
self.group = run_group_id or "run_group_" + uuid.uuid4().hex
|
|
94
|
+
if not isinstance(self.group, str) or not re.fullmatch(r"run_group_[a-f0-9]{32}", self.group):
|
|
95
|
+
raise ValueError("invalid native run group identity")
|
|
96
|
+
if not environment._hitch_ownership_labels or not _digest(agent.revision_identity):
|
|
97
|
+
raise ValueError("native phase supervision requires a leased environment and immutable harness")
|
|
98
|
+
self.controller, self.binding = _json(json.dumps(controller)), _json(json.dumps(binding))
|
|
99
|
+
self.task_digest, self.timeout_ms = task_digest, timeout_ms
|
|
100
|
+
if not isinstance(task_instruction, str):
|
|
101
|
+
raise ValueError("invalid locked task instruction")
|
|
102
|
+
self.task_instruction = task_instruction
|
|
103
|
+
self.shutdown_timeout_ms, self.poll_interval_ms = shutdown_timeout_ms, poll_interval_ms
|
|
104
|
+
if finalization_timeout_ms is not None and not _positive(finalization_timeout_ms, 9007199254740991):
|
|
105
|
+
raise ValueError("invalid native finalization allowance")
|
|
106
|
+
self.finalization_timeout_ms = finalization_timeout_ms
|
|
107
|
+
self.root = Path(environment.trial_paths.trial_dir)
|
|
108
|
+
if self.root.is_symlink() or not self.root.is_dir():
|
|
109
|
+
raise ValueError("native phase evidence requires a real trial directory")
|
|
110
|
+
self.root = self.root.resolve(strict=True)
|
|
111
|
+
self.directory = self.root / "hitch-native-phases"
|
|
112
|
+
self.prepared = self.running = self.context = None
|
|
113
|
+
self.phase_index, self.sequence, self.generation = 0, 0, 0
|
|
114
|
+
self.deadline_ns = 0
|
|
115
|
+
self.used = False
|
|
116
|
+
self.record = None
|
|
117
|
+
self.native_state = None
|
|
118
|
+
self.cancel_reason = None
|
|
119
|
+
|
|
120
|
+
def _remaining_ms(self):
|
|
121
|
+
return (self.deadline_ns - time.monotonic_ns()) // 1_000_000
|
|
122
|
+
|
|
123
|
+
def _save(self):
|
|
124
|
+
_write(self.directory / "supervision.json", self.record)
|
|
125
|
+
|
|
126
|
+
async def _rpc(self, operation, parameters=None, *, cleanup=False):
|
|
127
|
+
remaining = self.shutdown_timeout_ms if cleanup else self._remaining_ms()
|
|
128
|
+
if remaining <= 0:
|
|
129
|
+
raise PhaseSupervisionError("native_task_budget_expired")
|
|
130
|
+
request = {"request_id": "phase_rpc_" + uuid.uuid4().hex, "operation": operation, "parameters": parameters or {}}
|
|
131
|
+
command = "printf %s " + shlex.quote(json.dumps(request)) + " | " + shlex.join(self.controller["argv"])
|
|
132
|
+
try:
|
|
133
|
+
result = await self.env.service_exec(command, service=self.controller["service"], timeout_sec=max(1, math.ceil(min(15000, remaining) / 1000)))
|
|
134
|
+
if result.return_code or not isinstance(result.stdout, str) or len(result.stdout.encode()) > 2 * 1024 * 1024:
|
|
135
|
+
raise ValueError("invalid management result")
|
|
136
|
+
output = _json(result.stdout)
|
|
137
|
+
if not isinstance(output, dict):
|
|
138
|
+
raise ValueError("invalid management output")
|
|
139
|
+
return output
|
|
140
|
+
except asyncio.CancelledError:
|
|
141
|
+
raise
|
|
142
|
+
except Exception:
|
|
143
|
+
# Bind output may contain a token. Never echo stdout/stderr or parse
|
|
144
|
+
# errors, including errors from a service_exec implementation.
|
|
145
|
+
raise PhaseSupervisionError("native_controller_rpc_failed") from None
|
|
146
|
+
|
|
147
|
+
async def _state(self, *, finalizing=False):
|
|
148
|
+
state = await self._rpc("state", cleanup=finalizing)
|
|
149
|
+
try:
|
|
150
|
+
if set(state) != {"state", "generation", "sequence", "run_id", "prediction", "task_current_date"}:
|
|
151
|
+
raise ValueError()
|
|
152
|
+
generation, sequence = state["generation"], state["sequence"]
|
|
153
|
+
if (type(generation) is not int or not self.generation <= generation <= self.phase_index + 1
|
|
154
|
+
or type(sequence) is not int or sequence < self.sequence or generation < 0):
|
|
155
|
+
raise ValueError()
|
|
156
|
+
name, run_id, prediction = state["state"], state["run_id"], state["prediction"]
|
|
157
|
+
if name not in {"created", "context_required", "awaiting_actions", "sdk_executing", "completed", "failed", "cancelled", "finalizing"}:
|
|
158
|
+
raise ValueError()
|
|
159
|
+
if state["task_current_date"] is not None and (not isinstance(state["task_current_date"], str) or len(state["task_current_date"]) > 1024):
|
|
160
|
+
raise ValueError()
|
|
161
|
+
if prediction is not None:
|
|
162
|
+
if (not isinstance(prediction, dict) or set(prediction) != {"sequence", "generation", "instruction", "screenshot_file", "screenshot_sha256", "user_response"}
|
|
163
|
+
or type(prediction["generation"]) is not int or prediction["generation"] != generation
|
|
164
|
+
or type(prediction["sequence"]) is not int or prediction["sequence"] != sequence or sequence < 1
|
|
165
|
+
or not isinstance(prediction["instruction"], str)
|
|
166
|
+
or prediction["user_response"] is not None and not isinstance(prediction["user_response"], str)
|
|
167
|
+
or not isinstance(prediction["screenshot_file"], str) or not re.fullmatch(r"observation-[0-9]{6,}\.png", prediction["screenshot_file"])
|
|
168
|
+
or not isinstance(prediction["screenshot_sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", prediction["screenshot_sha256"])):
|
|
169
|
+
raise ValueError()
|
|
170
|
+
if name == "created" and (generation != 0 or sequence != 0 or prediction is not None):
|
|
171
|
+
raise ValueError()
|
|
172
|
+
if name in {"awaiting_actions", "sdk_executing"}:
|
|
173
|
+
if self.prepared is None or generation != self.phase_index or run_id != self.prepared.run_id:
|
|
174
|
+
raise ValueError()
|
|
175
|
+
if name == "awaiting_actions" and prediction is None:
|
|
176
|
+
raise ValueError()
|
|
177
|
+
elif run_id is not None:
|
|
178
|
+
raise ValueError()
|
|
179
|
+
if name == "context_required" and (generation < 1 or generation != self.phase_index + 1):
|
|
180
|
+
raise ValueError()
|
|
181
|
+
if name == "finalizing" and (not finalizing or prediction is not None or not self.record.get("budget_finalization")):
|
|
182
|
+
raise ValueError()
|
|
183
|
+
if name == "completed" and (prediction is not None or not finalizing and (self.prepared is None or generation != self.phase_index)):
|
|
184
|
+
raise ValueError()
|
|
185
|
+
except (ValueError, TypeError, KeyError):
|
|
186
|
+
raise PhaseSupervisionError("native_controller_state_invalid") from None
|
|
187
|
+
self.generation, self.sequence = generation, sequence
|
|
188
|
+
self.native_state = state
|
|
189
|
+
if name in {"failed", "cancelled"}:
|
|
190
|
+
raise PhaseSupervisionError("native_controller_" + name)
|
|
191
|
+
return state
|
|
192
|
+
|
|
193
|
+
async def _start(self, state):
|
|
194
|
+
prediction = state["prediction"]
|
|
195
|
+
# Keep the locked task guidance on every fresh candidate, alongside the
|
|
196
|
+
# current native instruction/date. Never carry a prior phase's prompt.
|
|
197
|
+
instruction = ("Complete this native task using the locked tool bridge. "
|
|
198
|
+
"Run `node /tmp/hitch-tools.mjs list` for schemas and invoke "
|
|
199
|
+
"`node /tmp/hitch-tools.mjs TOOL_NAME 'JSON_ARGUMENTS'`. "
|
|
200
|
+
"Open returned image paths with your native image viewing tool.\n"
|
|
201
|
+
+ json.dumps({"task_instructions": self.task_instruction,
|
|
202
|
+
"instruction": prediction["instruction"],
|
|
203
|
+
"task_current_date": state["task_current_date"]}, ensure_ascii=False))
|
|
204
|
+
self.phase_index += 1
|
|
205
|
+
self.prepared = self.agent.prepare_phase(instruction=instruction, run_group_id=self.group, phase_index=self.phase_index,
|
|
206
|
+
task_digest=self.task_digest, remaining_timeout_ms=self._remaining_ms())
|
|
207
|
+
entry = {"phase_index": self.phase_index, "generation": state["generation"], "run_id": self.prepared.run_id,
|
|
208
|
+
"status": "prepared", "first_prediction_sequence": prediction["sequence"],
|
|
209
|
+
"first_screenshot_sha256": prediction["screenshot_sha256"]}
|
|
210
|
+
self.record["phases"].append(entry)
|
|
211
|
+
self._save()
|
|
212
|
+
response = await self._rpc("bind", {"generation": state["generation"], "run_id": self.prepared.run_id})
|
|
213
|
+
binding = response.get("binding")
|
|
214
|
+
if (set(response) != {"binding"} or not isinstance(binding, dict) or set(binding) != {"endpoint", "tools", "token"}
|
|
215
|
+
or binding["endpoint"] != self.binding["endpoint"] or binding["tools"] != self.binding["tools"]
|
|
216
|
+
or not isinstance(binding["token"], str) or not re.fullmatch(r"[a-f0-9]{64}", binding["token"])):
|
|
217
|
+
raise PhaseSupervisionError("native_binding_differs_from_locked_definition")
|
|
218
|
+
with tempfile.TemporaryDirectory(prefix="hitch-phase-binding-") as temporary:
|
|
219
|
+
file = Path(temporary) / "binding.json"
|
|
220
|
+
file.write_text(json.dumps(binding), encoding="utf-8"); file.chmod(0o600)
|
|
221
|
+
await self.env.upload_file(file, "/tmp/hitch-tool-binding.json")
|
|
222
|
+
protected = await self.env.exec("chmod 600 /tmp/hitch-tool-binding.json")
|
|
223
|
+
if protected.return_code:
|
|
224
|
+
raise PhaseSupervisionError("native_binding_protection_failed")
|
|
225
|
+
await self.env.upload_file(Path(__file__).with_name("hitch_tool_client.mjs"), "/tmp/hitch-tools.mjs")
|
|
226
|
+
self.context = AgentContext()
|
|
227
|
+
self.cancel_reason = None
|
|
228
|
+
self.running = asyncio.create_task(self.agent.run_phase(self.prepared, self.env, self.context))
|
|
229
|
+
entry["status"] = "candidate_running"
|
|
230
|
+
self._save()
|
|
231
|
+
# Give run_phase ownership before any cancellation can be requested.
|
|
232
|
+
await asyncio.sleep(0)
|
|
233
|
+
|
|
234
|
+
async def _settle(self, reason):
|
|
235
|
+
deadline = time.monotonic_ns() + self.shutdown_timeout_ms * 1_000_000
|
|
236
|
+
if reason == "task_budget_expired" and self.record.get("budget_finalization", {}).get("receipt"):
|
|
237
|
+
self.cancel_reason = reason
|
|
238
|
+
if not self.running.done():
|
|
239
|
+
self.cancel_reason = reason
|
|
240
|
+
try:
|
|
241
|
+
# An uncertain upload must settle as failure and whole-trial
|
|
242
|
+
# cleanup. Never retry or replace a container on that path.
|
|
243
|
+
await asyncio.wait_for(self.agent.request_phase_cancellation(self.prepared, self.env, reason=reason), self.shutdown_timeout_ms / 1000)
|
|
244
|
+
except asyncio.TimeoutError:
|
|
245
|
+
raise PhaseSupervisionError("native_candidate_shutdown_incomplete") from None
|
|
246
|
+
except asyncio.CancelledError:
|
|
247
|
+
raise
|
|
248
|
+
except Exception:
|
|
249
|
+
raise PhaseSupervisionError("native_candidate_cancellation_delivery_failed") from None
|
|
250
|
+
try:
|
|
251
|
+
await asyncio.wait_for(asyncio.shield(self.running), max(0, (deadline - time.monotonic_ns()) / 1_000_000_000))
|
|
252
|
+
except asyncio.TimeoutError:
|
|
253
|
+
raise PhaseSupervisionError("native_candidate_shutdown_incomplete") from None
|
|
254
|
+
except Exception as error:
|
|
255
|
+
# The bridge reports a nonzero executor exit for cancellation; only
|
|
256
|
+
# this specific case can continue to independent bundle inspection.
|
|
257
|
+
metadata = self.context.metadata or {}
|
|
258
|
+
allowed = {"cancelled", "timed_out"} if self.cancel_reason == "task_budget_expired" and self.record.get("budget_finalization", {}).get("receipt") else {"cancelled"}
|
|
259
|
+
if not (self.cancel_reason and getattr(error, "code", None) == "hitch_process_failed" and metadata.get("hitch_status") in allowed
|
|
260
|
+
and metadata.get("hitch_bridge_error_code") == "hitch_process_failed"):
|
|
261
|
+
raise PhaseSupervisionError("native_candidate_failed") from None
|
|
262
|
+
metadata = self.context.metadata or {}
|
|
263
|
+
if metadata.get("hitch_run_id") != self.prepared.run_id or metadata.get("hitch_phase_bundle_exported") is not True:
|
|
264
|
+
raise PhaseSupervisionError("native_candidate_export_incomplete")
|
|
265
|
+
|
|
266
|
+
async def _inspect(self, source):
|
|
267
|
+
module = (self.agent.hitch_runtime_dir / "payload/dist/src/runs/phase-bundle.js").resolve(strict=True)
|
|
268
|
+
node = shutil.which("node")
|
|
269
|
+
if not node:
|
|
270
|
+
raise PhaseSupervisionError("native_phase_inspection_node_missing")
|
|
271
|
+
payload = {"sourceDirectory": str(source), "expected": {"run_id": self.prepared.run_id,
|
|
272
|
+
"context": _json(self.prepared.context_json), "parent": _json(self.prepared.parent_json),
|
|
273
|
+
"revision_identity": self.agent.revision_identity}}
|
|
274
|
+
script = ("import {inspectSealedPhaseRunBundle} from " + json.dumps(module.as_uri()) + ";"
|
|
275
|
+
"let data='';for await(const c of process.stdin)data+=c;"
|
|
276
|
+
"try{process.stdout.write(JSON.stringify(await inspectSealedPhaseRunBundle(JSON.parse(data))))}"
|
|
277
|
+
"catch{process.stderr.write('phase evidence invalid');process.exitCode=1}")
|
|
278
|
+
process = await asyncio.create_subprocess_exec(node, "--input-type=module", "-e", script,
|
|
279
|
+
stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
280
|
+
try:
|
|
281
|
+
stdout, _ = await asyncio.wait_for(process.communicate(json.dumps(payload).encode()), self.shutdown_timeout_ms / 1000)
|
|
282
|
+
except BaseException:
|
|
283
|
+
if process.returncode is None:
|
|
284
|
+
process.kill()
|
|
285
|
+
await process.wait()
|
|
286
|
+
raise
|
|
287
|
+
if process.returncode or len(stdout) > 65536:
|
|
288
|
+
raise PhaseSupervisionError("native_phase_evidence_invalid")
|
|
289
|
+
proof = _json(stdout)
|
|
290
|
+
allowed = {"succeeded", "cancelled", "timed_out"} if self.record.get("budget_finalization", {}).get("receipt") else {"succeeded", "cancelled"}
|
|
291
|
+
if proof["process_status"] not in allowed or proof["process_status"] != self.context.metadata.get("hitch_status"):
|
|
292
|
+
raise PhaseSupervisionError("native_phase_process_status_mismatch")
|
|
293
|
+
prior = self.record["phases"][:-1]
|
|
294
|
+
for phase in prior:
|
|
295
|
+
previous = phase["evidence"]
|
|
296
|
+
if (previous["provider_session_id"] == proof["provider_session_id"]
|
|
297
|
+
or previous["harness"] != proof["harness"] or previous["model"] != proof["model"]
|
|
298
|
+
or datetime.fromisoformat(previous["completed_at"].replace("Z", "+00:00")) > datetime.fromisoformat(proof["started_at"].replace("Z", "+00:00"))):
|
|
299
|
+
raise PhaseSupervisionError("native_phase_candidate_or_conversation_changed")
|
|
300
|
+
return proof
|
|
301
|
+
|
|
302
|
+
async def _retire(self, state):
|
|
303
|
+
finished = state["state"] == "completed"
|
|
304
|
+
await self._settle("task_budget_expired" if self.record.get("budget_finalization", {}).get("receipt") else "native_task_finished" if finished else "native_phase_reset")
|
|
305
|
+
entry = self.record["phases"][-1]
|
|
306
|
+
entry.update(status="candidate_exported", boundary={"state": state["state"], "generation": state["generation"], "sequence": state["sequence"]})
|
|
307
|
+
self._save()
|
|
308
|
+
if finished:
|
|
309
|
+
# At task end this also invokes the normal benchmark snapshot hook.
|
|
310
|
+
await self.env.stop_service("main")
|
|
311
|
+
source = self.root / "agent/hitch-run-bundle"
|
|
312
|
+
else:
|
|
313
|
+
receipt = await self.env.recycle_candidate_phase(self.phase_index)
|
|
314
|
+
if receipt.get("status") != "completed" or receipt.get("phase_index") != self.phase_index:
|
|
315
|
+
raise PhaseSupervisionError("native_candidate_replacement_incomplete")
|
|
316
|
+
source = self.root / f"hitch-candidate-phases/phase-{self.phase_index:04d}/agent/hitch-run-bundle"
|
|
317
|
+
entry["replacement_receipt_ref"] = f"hitch-candidate-phases/phase-{self.phase_index:04d}/receipt.json"
|
|
318
|
+
# No candidate from the retired container can mutate the source now.
|
|
319
|
+
entry.update(status="sealed", bundle_ref=source.relative_to(self.root).as_posix(), evidence=await self._inspect(source))
|
|
320
|
+
self._save()
|
|
321
|
+
self.prepared = self.running = self.context = None
|
|
322
|
+
if not finished:
|
|
323
|
+
await self.agent.setup(self.env)
|
|
324
|
+
return finished
|
|
325
|
+
|
|
326
|
+
async def _finalize_budget(self):
|
|
327
|
+
if self._remaining_ms() > 0 or not self.record["phases"]:
|
|
328
|
+
raise PhaseSupervisionError("native_task_budget_expired")
|
|
329
|
+
self.record["budget_finalization"] = {"status": "requested", "timeout_ms": self.finalization_timeout_ms,
|
|
330
|
+
"elapsed_ms": self.timeout_ms - self._remaining_ms(),
|
|
331
|
+
"requested_at": datetime.now(timezone.utc).isoformat()}
|
|
332
|
+
self._save()
|
|
333
|
+
state = await self._state(finalizing=True)
|
|
334
|
+
if state["state"] != "completed":
|
|
335
|
+
receipt = await self._rpc("expire_budget", cleanup=True)
|
|
336
|
+
expected_run = self.prepared.run_id if self.prepared else None
|
|
337
|
+
if (set(receipt) != {"budget_exhausted", "generation", "sequence", "run_id", "pending_prediction", "action_submitted"}
|
|
338
|
+
or receipt["budget_exhausted"] is not True
|
|
339
|
+
or receipt["run_id"] != expected_run and not (receipt["run_id"] is None and receipt["generation"] == self.phase_index + 1)
|
|
340
|
+
or type(receipt["generation"]) is not int or not self.phase_index <= receipt["generation"] <= self.phase_index + 1
|
|
341
|
+
or type(receipt["sequence"]) is not int or receipt["sequence"] < self.sequence
|
|
342
|
+
or type(receipt["pending_prediction"]) is not bool or type(receipt["action_submitted"]) is not bool):
|
|
343
|
+
raise PhaseSupervisionError("native_budget_receipt_invalid")
|
|
344
|
+
self.record["budget_finalization"]["receipt"] = receipt
|
|
345
|
+
self._save()
|
|
346
|
+
if self.prepared:
|
|
347
|
+
await self._settle("task_budget_expired")
|
|
348
|
+
while True:
|
|
349
|
+
state = await self._state(finalizing=True)
|
|
350
|
+
if state["state"] == "completed":
|
|
351
|
+
break
|
|
352
|
+
if state["state"] != "finalizing":
|
|
353
|
+
raise PhaseSupervisionError("native_budget_did_not_revoke_candidate")
|
|
354
|
+
await asyncio.sleep(self.poll_interval_ms / 1000)
|
|
355
|
+
else:
|
|
356
|
+
# The SDK completed just before the watchdog was observed. Do not
|
|
357
|
+
# invent a deadline event or replace that native completion reason.
|
|
358
|
+
del self.record["budget_finalization"]
|
|
359
|
+
if self.prepared:
|
|
360
|
+
await self._retire(state)
|
|
361
|
+
else:
|
|
362
|
+
# A reset already retired the previous candidate. Stop the fresh,
|
|
363
|
+
# unused replacement; its archived bundle/receipt remains evidence.
|
|
364
|
+
await self.env.stop_service("main")
|
|
365
|
+
if self.record.get("budget_finalization"):
|
|
366
|
+
self.record["budget_finalization"]["status"] = "completed"
|
|
367
|
+
self.record.update(status="completed", completed_at=datetime.now(timezone.utc).isoformat(),
|
|
368
|
+
final_native_state={k: state[k] for k in ["state", "generation", "sequence"]})
|
|
369
|
+
self._save()
|
|
370
|
+
return _json(json.dumps(self.record))
|
|
371
|
+
|
|
372
|
+
async def _cleanup(self):
|
|
373
|
+
cleanup = {"controller_cancelled": False, "environment_stopped": False, "candidate_settled": self.running is None}
|
|
374
|
+
try:
|
|
375
|
+
cleanup["controller_cancelled"] = await self._rpc("cancel", cleanup=True) == {"cancelled": True}
|
|
376
|
+
except BaseException:
|
|
377
|
+
pass
|
|
378
|
+
if self.running is not None and not self.running.done() and not self.cancel_reason:
|
|
379
|
+
try:
|
|
380
|
+
self.cancel_reason = "task_budget_expired" if self._remaining_ms() <= 0 else "cancelled"
|
|
381
|
+
await asyncio.wait_for(self.agent.request_phase_cancellation(self.prepared, self.env, reason=self.cancel_reason), self.shutdown_timeout_ms / 1000)
|
|
382
|
+
except BaseException:
|
|
383
|
+
pass
|
|
384
|
+
if self.running is not None:
|
|
385
|
+
try:
|
|
386
|
+
await asyncio.wait_for(asyncio.shield(self.running), self.shutdown_timeout_ms / 1000)
|
|
387
|
+
except BaseException:
|
|
388
|
+
pass
|
|
389
|
+
try:
|
|
390
|
+
await self.env.stop(delete=True)
|
|
391
|
+
cleanup["environment_stopped"] = True
|
|
392
|
+
except BaseException:
|
|
393
|
+
pass
|
|
394
|
+
if self.running is not None:
|
|
395
|
+
# Only cancel the host awaiter after whole-environment teardown;
|
|
396
|
+
# cancelling docker-exec alone is not proof the model stopped.
|
|
397
|
+
if not self.running.done() and cleanup["environment_stopped"]:
|
|
398
|
+
self.running.cancel()
|
|
399
|
+
try:
|
|
400
|
+
await self.running
|
|
401
|
+
except BaseException:
|
|
402
|
+
pass
|
|
403
|
+
cleanup["candidate_settled"] = self.running.done()
|
|
404
|
+
if self.running.done() and not self.running.cancelled():
|
|
405
|
+
self.running.exception() # Consume the already recorded failure.
|
|
406
|
+
self.record["cleanup"] = cleanup
|
|
407
|
+
self.record["cleanup_required"] = not all(cleanup.values())
|
|
408
|
+
self._save()
|
|
409
|
+
|
|
410
|
+
async def run(self):
|
|
411
|
+
if self.used:
|
|
412
|
+
raise PhaseSupervisionError("native_phase_supervisor_is_single_use")
|
|
413
|
+
self.used = True
|
|
414
|
+
self.directory.mkdir(mode=0o700, exist_ok=False)
|
|
415
|
+
self.deadline_ns = time.monotonic_ns() + self.timeout_ms * 1_000_000
|
|
416
|
+
self.record = {"schema_version": "hitch-native-phase-supervision@1", "scope": "candidate-evidence-only",
|
|
417
|
+
"run_group_id": self.group, "task_digest": self.task_digest, "status": "running", "phases": [],
|
|
418
|
+
"started_at": datetime.now(timezone.utc).isoformat(), "timeout_ms": self.timeout_ms}
|
|
419
|
+
self._save()
|
|
420
|
+
try:
|
|
421
|
+
while True:
|
|
422
|
+
try:
|
|
423
|
+
state = await self._state()
|
|
424
|
+
except PhaseSupervisionError as error:
|
|
425
|
+
if str(error) == "native_task_budget_expired" and self.finalization_timeout_ms is not None:
|
|
426
|
+
return await asyncio.wait_for(self._finalize_budget(), self.finalization_timeout_ms / 1000)
|
|
427
|
+
raise
|
|
428
|
+
if self._remaining_ms() <= 0 and self.finalization_timeout_ms is not None:
|
|
429
|
+
return await asyncio.wait_for(self._finalize_budget(), self.finalization_timeout_ms / 1000)
|
|
430
|
+
if self.prepared is not None and (state["state"] == "completed" or state["generation"] == self.phase_index + 1):
|
|
431
|
+
if await self._retire(state):
|
|
432
|
+
self.record.update(status="completed", completed_at=datetime.now(timezone.utc).isoformat())
|
|
433
|
+
self._save()
|
|
434
|
+
return _json(json.dumps(self.record))
|
|
435
|
+
continue
|
|
436
|
+
if self.prepared is None and state["state"] == "context_required" and state["prediction"] is not None:
|
|
437
|
+
await self._start(state)
|
|
438
|
+
elif self.running is not None and self.running.done() and state["state"] == "awaiting_actions":
|
|
439
|
+
# submit atomically switches to sdk_executing. If another
|
|
440
|
+
# prediction awaits an exited model, no native boundary was
|
|
441
|
+
# reached; never create a replacement conversation here.
|
|
442
|
+
raise PhaseSupervisionError("native_candidate_exited_before_boundary")
|
|
443
|
+
elif self.running is not None and self.running.done() and (self.running.cancelled() or self.running.exception() is not None):
|
|
444
|
+
raise PhaseSupervisionError("native_candidate_failed")
|
|
445
|
+
await asyncio.sleep(min(self.poll_interval_ms, max(0, self._remaining_ms())) / 1000)
|
|
446
|
+
except BaseException as error:
|
|
447
|
+
self.record.update(status="failed", failure_code=str(error) if isinstance(error, PhaseSupervisionError) else type(error).__name__)
|
|
448
|
+
self._save()
|
|
449
|
+
await self._cleanup()
|
|
450
|
+
if isinstance(error, asyncio.CancelledError):
|
|
451
|
+
raise
|
|
452
|
+
raise PhaseSupervisionError(self.record["failure_code"]) from None
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Generic tool-server@1 CLI transport. All tool names and schemas are package data.
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
const IMAGE_TYPES = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
|
|
11
|
+
|
|
12
|
+
function decodeImage(block) {
|
|
13
|
+
const extension = IMAGE_TYPES[block.mimeType];
|
|
14
|
+
if (!extension || typeof block.data !== "string" || block.data.length > Math.ceil(MAX_IMAGE_BYTES / 3) * 4) throw new Error("Unsupported or oversized tool image");
|
|
15
|
+
const bytes = Buffer.from(block.data, "base64");
|
|
16
|
+
if (!bytes.length || bytes.length > MAX_IMAGE_BYTES || bytes.toString("base64") !== block.data) throw new Error("Invalid tool image encoding");
|
|
17
|
+
const valid = block.mimeType === "image/png" ? bytes.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))
|
|
18
|
+
: block.mimeType === "image/jpeg" ? bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff
|
|
19
|
+
: bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP";
|
|
20
|
+
if (!valid) throw new Error("Tool image MIME type differs from its bytes");
|
|
21
|
+
return { bytes, extension };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Only this explicit envelope opts in. Existing arbitrary JSON/text tool
|
|
25
|
+
// responses (including objects containing a content field) stay byte-for-byte.
|
|
26
|
+
export async function materializeToolResult(text) {
|
|
27
|
+
let result;
|
|
28
|
+
try { result = JSON.parse(text); } catch { return text; }
|
|
29
|
+
if (result?.protocol !== "hitch-tool-result@1") return text;
|
|
30
|
+
if (!Array.isArray(result.content) || result.content.length > 64) throw new Error("Invalid tool result content");
|
|
31
|
+
let imageCount = 0;
|
|
32
|
+
const decoded = result.content.map((block) => {
|
|
33
|
+
if (block?.type === "text" && typeof block.text === "string") return { text: block.text };
|
|
34
|
+
if (block?.type !== "image" || ++imageCount > 8) throw new Error("Unsupported tool content block");
|
|
35
|
+
return decodeImage(block);
|
|
36
|
+
});
|
|
37
|
+
const directory = imageCount ? await mkdtemp(path.join(tmpdir(), "hitch-observations-")) : null;
|
|
38
|
+
try {
|
|
39
|
+
const content = [];
|
|
40
|
+
for (const [index, block] of decoded.entries()) {
|
|
41
|
+
if ("text" in block) { content.push({ type: "text", text: block.text }); continue; }
|
|
42
|
+
const target = path.join(directory, `${index}.${block.extension}`);
|
|
43
|
+
await writeFile(target, block.bytes, { flag: "wx", mode: 0o600 });
|
|
44
|
+
content.push({ type: "image", path: target, mimeType: result.content[index].mimeType,
|
|
45
|
+
bytes: block.bytes.length, sha256: createHash("sha256").update(block.bytes).digest("hex") });
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify({ ...result, content });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (directory) await rm(directory, { recursive: true, force: true });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function invokeTool(binding, name, args) {
|
|
55
|
+
if (!binding.tools.some((tool) => tool.name === name)) throw new Error("Unknown tool name; use list");
|
|
56
|
+
const response = await fetch(new URL("call", binding.endpoint), {
|
|
57
|
+
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${binding.token}` },
|
|
58
|
+
body: JSON.stringify({ name, arguments: args }), signal: AbortSignal.timeout(60000), redirect: "error",
|
|
59
|
+
});
|
|
60
|
+
let size = 0;
|
|
61
|
+
const chunks = [];
|
|
62
|
+
for await (const chunk of response.body) {
|
|
63
|
+
size += chunk.length;
|
|
64
|
+
if (size > MAX_RESPONSE_BYTES) throw new Error("Tool response exceeds 16 MiB");
|
|
65
|
+
chunks.push(chunk);
|
|
66
|
+
}
|
|
67
|
+
const result = Buffer.concat(chunks).toString("utf8");
|
|
68
|
+
if (!response.ok) throw new Error(`Tool transport ${response.status}: ${result}`);
|
|
69
|
+
return materializeToolResult(result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readToolArguments(argument, input = process.stdin) {
|
|
73
|
+
if (argument !== "-") return JSON.parse(argument);
|
|
74
|
+
const chunks = [];
|
|
75
|
+
let size = 0;
|
|
76
|
+
for await (const chunk of input) {
|
|
77
|
+
const bytes = Buffer.from(chunk);
|
|
78
|
+
size += bytes.length;
|
|
79
|
+
if (size >= 1024 * 1024) throw new Error("Tool arguments exceed 1 MiB");
|
|
80
|
+
chunks.push(bytes);
|
|
81
|
+
}
|
|
82
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
86
|
+
const binding = JSON.parse(await readFile("/tmp/hitch-tool-binding.json", "utf8"));
|
|
87
|
+
const [name, argument = "{}"] = process.argv.slice(2);
|
|
88
|
+
const result = !name || name === "list" ? JSON.stringify(binding.tools, null, 2)
|
|
89
|
+
: await invokeTool(binding, name, await readToolArguments(argument));
|
|
90
|
+
process.stdout.write(`${result}\n`);
|
|
91
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Trusted no-tools harness. One Responses request, native text/image inputs,
|
|
3
|
+
// no tool executor, retries, subprocesses, browser, or continuation loop.
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
if (process.argv.includes('--version')) {
|
|
7
|
+
console.log('hitch-model-call 1.0.0');
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
if (args.length !== 2 || args[0] !== '--model' || !args[1]) throw new Error('Usage: model-call --model MODEL');
|
|
12
|
+
const model = args[1];
|
|
13
|
+
const key = process.env.OPENAI_API_KEY;
|
|
14
|
+
if (!key) throw new Error('OPENAI_API_KEY is required for the model-call harness');
|
|
15
|
+
const base = new URL(process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1/');
|
|
16
|
+
if (base.username || base.password || base.search || base.hash || (base.protocol !== 'https:' && !(base.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname)))) throw new Error('invalid model endpoint');
|
|
17
|
+
if (!base.pathname.endsWith('/')) base.pathname += '/';
|
|
18
|
+
let raw = '';
|
|
19
|
+
for await (const chunk of process.stdin) {
|
|
20
|
+
raw += chunk;
|
|
21
|
+
if (Buffer.byteLength(raw) > 30 * 1024 * 1024) throw new Error('model input exceeds 30 MB');
|
|
22
|
+
}
|
|
23
|
+
const input = JSON.parse(raw);
|
|
24
|
+
const allowed = new Set(['schema_version', 'messages', 'max_output_tokens', 'reasoning_effort']);
|
|
25
|
+
if (input.schema_version !== '1' || Object.keys(input).some(k => !allowed.has(k)) || !Array.isArray(input.messages) || !input.messages.length) throw new Error('invalid model input');
|
|
26
|
+
for (const message of input.messages) {
|
|
27
|
+
if (!['system', 'user'].includes(message.role) || Object.keys(message).some(k => !['role', 'content'].includes(k)) || !Array.isArray(message.content) || !message.content.length) throw new Error('invalid model message');
|
|
28
|
+
for (const part of message.content) {
|
|
29
|
+
if (part.type === 'input_text') {
|
|
30
|
+
if (typeof part.text !== 'string' || Object.keys(part).some(k => !['type', 'text'].includes(k))) throw new Error('invalid text part');
|
|
31
|
+
} else if (part.type === 'input_image') {
|
|
32
|
+
if (typeof part.image_url !== 'string' || !/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(part.image_url) || Object.keys(part).some(k => !['type', 'image_url'].includes(k))) throw new Error('images must be native, embedded PNG/JPEG/WebP');
|
|
33
|
+
} else throw new Error('unsupported input modality');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const maxTokens = input.max_output_tokens ?? 8192;
|
|
37
|
+
if (!Number.isSafeInteger(maxTokens) || maxTokens < 1 || maxTokens > 131072) throw new Error('invalid max_output_tokens');
|
|
38
|
+
if (input.reasoning_effort !== undefined && !['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(input.reasoning_effort)) throw new Error('invalid reasoning effort');
|
|
39
|
+
const emit = event => console.log(JSON.stringify(event));
|
|
40
|
+
emit({ type: 'session.created', session_id: randomUUID() });
|
|
41
|
+
const response = await fetch(new URL('responses', base), {
|
|
42
|
+
redirect: 'error',
|
|
43
|
+
method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ model, input: input.messages, tools: [], tool_choice: 'none', store: false,
|
|
45
|
+
max_output_tokens: maxTokens, ...(input.reasoning_effort ? { reasoning: { effort: input.reasoning_effort } } : {}) }),
|
|
46
|
+
signal: AbortSignal.timeout(3600000),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) throw new Error(`model API returned HTTP ${response.status}`);
|
|
49
|
+
const body = await response.json();
|
|
50
|
+
if (!['completed', 'incomplete'].includes(body.status) || !Array.isArray(body.output)) throw new Error('invalid model completion');
|
|
51
|
+
if (body.output.some(item => !['message', 'reasoning'].includes(item.type))) throw new Error('model returned a prohibited tool/action item');
|
|
52
|
+
const output = body.output.filter(item => item.type === 'message').flatMap(item => item.content || []).map(part => part.type === 'output_text' ? part.text : part.type === 'refusal' ? part.refusal : '').join('\n');
|
|
53
|
+
// An empty answer (including token exhaustion) remains a gradable model
|
|
54
|
+
// outcome. Provider/transport failures above remain infrastructure errors.
|
|
55
|
+
emit({ type: 'provider.response', response: body });
|
|
56
|
+
emit({ type: 'usage.updated', usage: body.usage || {} });
|
|
57
|
+
emit({ type: 'message.completed', text: output });
|