world-model-optimizer 0.2.0__py3-none-any.whl
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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/engine/grounding.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Web grounding: bounded search for entities the world model cannot ground internally.
|
|
2
|
+
|
|
3
|
+
When the env encounters a real-world entity outside its traces and knowledge base (an API's error
|
|
4
|
+
format, a package name, a flight code), it may emit a `ground_query` (see
|
|
5
|
+
`wmo.core.render.output_contract`) instead of hallucinating. A `Grounder` serves that query; the
|
|
6
|
+
engine caches results into the knowledge base (`grounded.md`) so an entity is searched at most
|
|
7
|
+
once per model, and re-completes the step with the results in context.
|
|
8
|
+
|
|
9
|
+
The default is `NullGrounder` — no network, tests and evals stay hermetic. Real backends:
|
|
10
|
+
|
|
11
|
+
- `BraveGrounder` (`BRAVE_SEARCH_API_KEY`; free tier at https://api-dashboard.search.brave.com/),
|
|
12
|
+
a plain keyed JSON API with no scraping fragility — for free-text entity queries.
|
|
13
|
+
- `FetchGrounder` (keyless): when the agent's action is itself a read-only `curl` GET of a public
|
|
14
|
+
URL, fetch that URL live and let the model shape the real body into the observation. Found
|
|
15
|
+
empirically: 42% of the terminal-tasks test slice is curl-with-URL, scoring ~0.10 below every
|
|
16
|
+
other step kind — the values (API payloads, search rankings) are unknowable without the network.
|
|
17
|
+
Live fetches are inherently non-hermetic (the web has moved since capture); use only in serve or
|
|
18
|
+
in explicitly-labeled eval modes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import ipaddress
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import socket
|
|
28
|
+
import urllib.parse
|
|
29
|
+
import urllib.request
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Protocol
|
|
34
|
+
|
|
35
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
36
|
+
|
|
37
|
+
from wmo.core.types import Action
|
|
38
|
+
|
|
39
|
+
GROUNDER_KINDS = ("none", "brave", "fetch")
|
|
40
|
+
_BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
|
|
41
|
+
_TIMEOUT_SECONDS = 15.0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GroundingResult(BaseModel):
|
|
45
|
+
"""One search hit: enough to ground an entity, small enough to cache in the KB."""
|
|
46
|
+
|
|
47
|
+
title: str = ""
|
|
48
|
+
url: str = ""
|
|
49
|
+
snippet: str = ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Grounder(Protocol):
|
|
53
|
+
"""Anything that can answer a grounding query with search results."""
|
|
54
|
+
|
|
55
|
+
def ground(self, query: str) -> list[GroundingResult]:
|
|
56
|
+
"""Return search results for `query` (empty when grounding is unavailable)."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NullGrounder:
|
|
61
|
+
"""The default: grounding disabled, never touches the network."""
|
|
62
|
+
|
|
63
|
+
def ground(self, query: str) -> list[GroundingResult]:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Injectable HTTP GET (url, headers) -> response body; lets tests exercise BraveGrounder offline.
|
|
68
|
+
FetchFn = Callable[[str, dict[str, str]], str]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _assert_public_http_url(url: str) -> None:
|
|
72
|
+
"""Reject URLs that could reach anything but a public web host (SSRF guard).
|
|
73
|
+
|
|
74
|
+
Fetch targets can derive from MODEL OUTPUT (FetchGrounder GETs the action's own curl URL;
|
|
75
|
+
`ground_query` is model-written), so before any request: scheme must be http(s) — no
|
|
76
|
+
file://, ftp:// or scheme-less tricks — and the host must not resolve into private,
|
|
77
|
+
loopback, or link-local space (cloud metadata endpoints live at 169.254.169.254).
|
|
78
|
+
"""
|
|
79
|
+
parsed = urllib.parse.urlparse(url)
|
|
80
|
+
if parsed.scheme not in ("http", "https"):
|
|
81
|
+
raise ValueError(f"grounding fetch requires http(s), got {parsed.scheme!r}: {url}")
|
|
82
|
+
host = parsed.hostname
|
|
83
|
+
if not host:
|
|
84
|
+
raise ValueError(f"grounding fetch URL has no host: {url}")
|
|
85
|
+
try:
|
|
86
|
+
infos = socket.getaddrinfo(host, None)
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
raise ValueError(f"grounding fetch host does not resolve: {host}") from exc
|
|
89
|
+
for info in infos:
|
|
90
|
+
address = ipaddress.ip_address(info[4][0])
|
|
91
|
+
if not address.is_global:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"grounding fetch host {host} resolves to non-public address {address}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _PublicOnlyRedirects(urllib.request.HTTPRedirectHandler):
|
|
98
|
+
"""Re-validate every 3xx hop: a public host 302-ing to 169.254.169.254 is the classic pivot.
|
|
99
|
+
|
|
100
|
+
(The remaining residue is DNS rebinding — resolve-at-check vs resolve-at-connect — which
|
|
101
|
+
urllib cannot pin without a custom transport; grounding fetches stay read-only GETs, so the
|
|
102
|
+
blast radius is exfil of a fetched body into the KB, not a write.)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001, ANN201, ANN202
|
|
106
|
+
_assert_public_http_url(newurl)
|
|
107
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_OPENER = urllib.request.build_opener(_PublicOnlyRedirects)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def http_get(url: str, headers: dict[str, str]) -> str:
|
|
114
|
+
"""Fetch a public http(s) URL as text: the default, SSRF-guarded `FetchFn`."""
|
|
115
|
+
_assert_public_http_url(url)
|
|
116
|
+
request = urllib.request.Request(url, headers=headers) # noqa: S310 — guarded above
|
|
117
|
+
with _OPENER.open(request, timeout=_TIMEOUT_SECONDS) as response: # noqa: S310
|
|
118
|
+
body: str = response.read().decode("utf-8")
|
|
119
|
+
return body
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class _BraveWeb(BaseModel):
|
|
123
|
+
results: list[GroundingResult] = Field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _BraveResponse(BaseModel):
|
|
127
|
+
web: _BraveWeb = Field(default_factory=_BraveWeb)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class BraveGrounder:
|
|
131
|
+
"""Brave Search API backend (`X-Subscription-Token` keyed GET, JSON response)."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, api_key: str, *, count: int = 5, fetch: FetchFn = http_get) -> None:
|
|
134
|
+
self._api_key = api_key
|
|
135
|
+
self._count = count
|
|
136
|
+
self._fetch = fetch
|
|
137
|
+
|
|
138
|
+
def ground(self, query: str) -> list[GroundingResult]:
|
|
139
|
+
params = urllib.parse.urlencode({"q": query, "count": str(self._count)})
|
|
140
|
+
headers = {"Accept": "application/json", "X-Subscription-Token": self._api_key}
|
|
141
|
+
body = self._fetch(f"{_BRAVE_ENDPOINT}?{params}", headers)
|
|
142
|
+
try:
|
|
143
|
+
payload = json.loads(body)
|
|
144
|
+
except json.JSONDecodeError as exc:
|
|
145
|
+
raise ValueError(f"Brave search returned non-JSON for {query!r}: {body[:200]}") from exc
|
|
146
|
+
try:
|
|
147
|
+
# Brave's result objects use `description` for the snippet; map it before validation.
|
|
148
|
+
raw_results = payload.get("web", {}).get("results", [])
|
|
149
|
+
for item in raw_results:
|
|
150
|
+
if isinstance(item, dict) and "description" in item and "snippet" not in item:
|
|
151
|
+
item["snippet"] = item.pop("description")
|
|
152
|
+
return _BraveResponse.model_validate(payload).web.results
|
|
153
|
+
except (ValidationError, AttributeError) as exc:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Brave search response for {query!r} did not match the expected shape: {exc}"
|
|
156
|
+
) from exc
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class FetchGrounder:
|
|
160
|
+
"""Keyless grounder for URL-shaped queries: GET the URL, return its (capped) body.
|
|
161
|
+
|
|
162
|
+
Non-URL queries yield no results — compose with a search backend if both are wanted. Fetches
|
|
163
|
+
are memoized per instance (an eval or session asks about the same endpoint repeatedly) and
|
|
164
|
+
failures return no results rather than raising: an unreachable URL should degrade to the
|
|
165
|
+
ungrounded prediction, never break the step.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, *, max_chars: int = 8_000, fetch: FetchFn = http_get) -> None:
|
|
169
|
+
self._max_chars = max_chars
|
|
170
|
+
self._fetch = fetch
|
|
171
|
+
self._memo: dict[str, list[GroundingResult]] = {}
|
|
172
|
+
|
|
173
|
+
def ground(self, query: str) -> list[GroundingResult]:
|
|
174
|
+
url = query.strip()
|
|
175
|
+
if not url.startswith(("http://", "https://")):
|
|
176
|
+
return []
|
|
177
|
+
if url in self._memo:
|
|
178
|
+
return self._memo[url]
|
|
179
|
+
try:
|
|
180
|
+
body = self._fetch(url, {"Accept": "*/*", "User-Agent": "wmo-grounder"})
|
|
181
|
+
except Exception: # noqa: BLE001 - any transport failure degrades to "no grounding"
|
|
182
|
+
results: list[GroundingResult] = []
|
|
183
|
+
else:
|
|
184
|
+
if len(body) > self._max_chars:
|
|
185
|
+
body = body[: self._max_chars] + "\n[truncated]"
|
|
186
|
+
results = [GroundingResult(title=url, url=url, snippet=body)]
|
|
187
|
+
self._memo[url] = results
|
|
188
|
+
return results
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# curl flags that mean the request mutates state (or uploads); those commands are never fetched.
|
|
192
|
+
_CURL_MUTATING_FLAGS = re.compile(
|
|
193
|
+
r"(^|\s)(-X\s*(?!GET\b)\w+|--request\s+(?!GET\b)\w+|-d\b|--data\b|--data-\w+|-F\b|--form\b"
|
|
194
|
+
r"|-T\b|--upload-file\b)"
|
|
195
|
+
)
|
|
196
|
+
_URL_IN_COMMAND = re.compile(r"https?://[^\s\"'|;>]+")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def extract_get_url(action: Action) -> str | None:
|
|
200
|
+
"""Return the URL a read-only `curl` GET in `action` targets, or None.
|
|
201
|
+
|
|
202
|
+
Conservative by design: only bash tool calls whose command invokes `curl` without any
|
|
203
|
+
mutating/upload flag qualify — a fetch must be side-effect-free to be safe to execute for
|
|
204
|
+
grounding. The first URL in the command is taken (pipes/filters after it are the model's job
|
|
205
|
+
to apply to the fetched body).
|
|
206
|
+
"""
|
|
207
|
+
if action.name != "bash":
|
|
208
|
+
return None
|
|
209
|
+
command = action.arguments.get("command")
|
|
210
|
+
if not isinstance(command, str) or "curl" not in command:
|
|
211
|
+
return None
|
|
212
|
+
if _CURL_MUTATING_FLAGS.search(command):
|
|
213
|
+
return None
|
|
214
|
+
match = _URL_IN_COMMAND.search(command)
|
|
215
|
+
return match.group(0) if match else None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def prefetched_knowledge(
|
|
219
|
+
knowledge: str | None, action: Action, grounder: Grounder | None
|
|
220
|
+
) -> str | None:
|
|
221
|
+
"""Append a live fetch of `action`'s read-only GET URL to the knowledge text, when possible.
|
|
222
|
+
|
|
223
|
+
The stateless prefetch used by replay experiments; the serving engine has its own budgeted,
|
|
224
|
+
KB-cached variant (`WorldModel._predict`). Returns `knowledge` unchanged when there is no
|
|
225
|
+
grounder, no fetchable URL, or the fetch yields nothing.
|
|
226
|
+
"""
|
|
227
|
+
if grounder is None:
|
|
228
|
+
return knowledge
|
|
229
|
+
url = extract_get_url(action)
|
|
230
|
+
if url is None:
|
|
231
|
+
return knowledge
|
|
232
|
+
results = grounder.ground(url)
|
|
233
|
+
if not results:
|
|
234
|
+
return knowledge
|
|
235
|
+
block = f"## live fetch: {url}\n{render_grounding(results)}"
|
|
236
|
+
return f"{knowledge}\n\n{block}" if knowledge else block
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# --- source grounding: pinned repo files as ground truth for read-verb actions -------------------
|
|
240
|
+
|
|
241
|
+
# Read-verb patterns whose observation is (a slice of) one file's true content. Any write/pipe
|
|
242
|
+
# transformation disqualifies — the observation would no longer be the raw file bytes.
|
|
243
|
+
_SED_RANGE = re.compile(r"\bsed\s+-n\s+'(\d+),(\d+)p'\s+(\S+)\s*$")
|
|
244
|
+
_CAT_FILE = re.compile(r"\bcat\s+(\S+)\s*$")
|
|
245
|
+
_HEAD_FILE = re.compile(r"\bhead\s+-(?:n\s*)?(\d+)\s+(\S+)\s*$")
|
|
246
|
+
_WRITE_MARKERS = re.compile(r"(>|>>|\bsed\s+-i\b|\btee\b|\bmv\b|\bcp\b|\bpatch\b|git\s+apply)")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True)
|
|
250
|
+
class FileRead:
|
|
251
|
+
"""One read-verb action: the repo-relative path and the requested 1-based line range."""
|
|
252
|
+
|
|
253
|
+
path: str
|
|
254
|
+
start: int | None
|
|
255
|
+
end: int | None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def extract_file_read(action: Action) -> FileRead | None:
|
|
259
|
+
"""Return the file slice a pure read action requests, or None.
|
|
260
|
+
|
|
261
|
+
Conservative by design (mirrors `extract_get_url`): only bash `sed -n 'A,Bp'`/`cat`/`head -N`
|
|
262
|
+
with the file as the FINAL token qualify — pipes, redirects, in-place edits, or interpreters
|
|
263
|
+
mean the observation is not the raw file content. Paths are made repo-relative by stripping
|
|
264
|
+
the conventional sandbox prefix.
|
|
265
|
+
"""
|
|
266
|
+
if action.name != "bash":
|
|
267
|
+
return None
|
|
268
|
+
command = action.arguments.get("command")
|
|
269
|
+
if not isinstance(command, str) or _WRITE_MARKERS.search(command) or "|" in command:
|
|
270
|
+
return None
|
|
271
|
+
command = command.strip()
|
|
272
|
+
if m := _SED_RANGE.search(command):
|
|
273
|
+
return FileRead(_repo_relative(m.group(3)), int(m.group(1)), int(m.group(2)))
|
|
274
|
+
if m := _HEAD_FILE.search(command):
|
|
275
|
+
return FileRead(_repo_relative(m.group(2)), 1, int(m.group(1)))
|
|
276
|
+
if m := _CAT_FILE.search(command):
|
|
277
|
+
return FileRead(_repo_relative(m.group(1)), None, None)
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _repo_relative(path: str) -> str:
|
|
282
|
+
return path.removeprefix("/testbed/").lstrip("/")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class SourceResolver:
|
|
286
|
+
"""Ground read-verb actions in the REAL file content at a pinned commit.
|
|
287
|
+
|
|
288
|
+
`pins` maps a trace's instance id -> {repo, base_commit} (for swe-bench this is the
|
|
289
|
+
committed `examples/swe-bench/instance_commits.json`, built once from the public dataset —
|
|
290
|
+
any corpus whose traces can be pinned the same way gets the same machinery). Files are
|
|
291
|
+
fetched keylessly from raw.githubusercontent.com, memoized per file, and sliced to the
|
|
292
|
+
requested line range. Callers own the STALENESS GATE: never resolve a path the session has
|
|
293
|
+
already touched — the pinned content is wrong the moment the agent edits the file (verified
|
|
294
|
+
live: first-touch reads match the pin exactly; post-edit re-reads do not).
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def __init__(self, pins: dict[str, dict[str, str]], *, fetch: FetchFn = http_get) -> None:
|
|
298
|
+
self._pins = pins
|
|
299
|
+
self._fetch = fetch
|
|
300
|
+
self._memo: dict[str, list[str] | None] = {}
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def from_file(cls, path: str | Path) -> SourceResolver:
|
|
304
|
+
pins = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
305
|
+
return cls(pins)
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def pins(self) -> dict[str, dict[str, str]]:
|
|
309
|
+
"""The instance pins (shared with RepoTreeResolver so both ground the same commit)."""
|
|
310
|
+
return self._pins
|
|
311
|
+
|
|
312
|
+
def resolve(self, instance_id: str, read: FileRead) -> str | None:
|
|
313
|
+
pin = self._pins.get(instance_id)
|
|
314
|
+
if pin is None:
|
|
315
|
+
return None
|
|
316
|
+
url = f"https://raw.githubusercontent.com/{pin['repo']}/{pin['base_commit']}/{read.path}"
|
|
317
|
+
if url not in self._memo:
|
|
318
|
+
try:
|
|
319
|
+
self._memo[url] = self._fetch(url, {"User-Agent": "wmo-grounder"}).splitlines()
|
|
320
|
+
except Exception: # noqa: BLE001 - unreachable file degrades to "no grounding"
|
|
321
|
+
self._memo[url] = None
|
|
322
|
+
lines = self._memo[url]
|
|
323
|
+
if lines is None:
|
|
324
|
+
return None
|
|
325
|
+
if read.start is not None:
|
|
326
|
+
lines = lines[read.start - 1 : read.end]
|
|
327
|
+
return "\n".join(lines)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def source_grounded_knowledge(
|
|
331
|
+
knowledge: str | None,
|
|
332
|
+
action: Action,
|
|
333
|
+
instance_id: str | None,
|
|
334
|
+
prior_actions: list[Action],
|
|
335
|
+
resolver: SourceResolver | None,
|
|
336
|
+
*,
|
|
337
|
+
annotate_stale: bool = False,
|
|
338
|
+
) -> str | None:
|
|
339
|
+
"""Append the pinned source slice for a read action to the knowledge text.
|
|
340
|
+
|
|
341
|
+
First-touch reads get the pin as ground truth. Previously-touched paths are refused by
|
|
342
|
+
default (the agent may have edited the file — a stale pin served as truth is wrong).
|
|
343
|
+
`annotate_stale=True` instead serves the BASE version explicitly labeled as pre-edit — the
|
|
344
|
+
model combines it with the session's in-context edits itself. Zero interpretation risk
|
|
345
|
+
(nothing replays the edits mechanically), and it unlocks the ~20%-of-steps population of
|
|
346
|
+
reads on edited files that the strict gate refuses.
|
|
347
|
+
"""
|
|
348
|
+
if resolver is None or instance_id is None:
|
|
349
|
+
return knowledge
|
|
350
|
+
read = extract_file_read(action)
|
|
351
|
+
if read is None:
|
|
352
|
+
return knowledge
|
|
353
|
+
basename = read.path.rsplit("/", 1)[-1]
|
|
354
|
+
touched = False
|
|
355
|
+
for prior in prior_actions:
|
|
356
|
+
prior_cmd = prior.arguments.get("command") if prior.name else prior.content
|
|
357
|
+
if isinstance(prior_cmd, str) and basename in prior_cmd:
|
|
358
|
+
touched = True
|
|
359
|
+
break
|
|
360
|
+
if touched and not annotate_stale:
|
|
361
|
+
return knowledge
|
|
362
|
+
content = resolver.resolve(instance_id, read)
|
|
363
|
+
if content is None:
|
|
364
|
+
return knowledge
|
|
365
|
+
span = f" lines {read.start}-{read.end}" if read.start is not None else ""
|
|
366
|
+
if touched:
|
|
367
|
+
header = (
|
|
368
|
+
f"## source file: {read.path}{span} (BASE-COMMIT version — this session has since"
|
|
369
|
+
" touched/edited this file; the current content is this base with the history's"
|
|
370
|
+
" edits applied)"
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
header = f"## source file: {read.path}{span} (ground truth at the base commit)"
|
|
374
|
+
block = f"{header}\n{content}"
|
|
375
|
+
return f"{knowledge}\n\n{block}" if knowledge else block
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# --- registry polling: package facts from PyPI/npm JSON (keyless, read-only) --------------------
|
|
379
|
+
|
|
380
|
+
# The args slice after `pip show|install`, up to the next shell operator. Flags are stripped
|
|
381
|
+
# token-by-token below (the corpus shape is flag soup: `pip install --break-system-packages X
|
|
382
|
+
# 2>&1 | tail -3`, `python3 -m pip install ...`).
|
|
383
|
+
_PIP_CMD = re.compile(r"\bpip3?\s+(?:show|install)\s+([^|;&\n]*)")
|
|
384
|
+
_NPM_QUERY = re.compile(r"\bnpm\s+(?:view|info)\s+([A-Za-z0-9@/_.-]+)")
|
|
385
|
+
# Flags whose argument replaces the registry as the install source: the action isn't asking
|
|
386
|
+
# the registry about a named package, so refuse.
|
|
387
|
+
_PIP_NON_REGISTRY_FLAGS = frozenset(
|
|
388
|
+
{"-r", "--requirement", "-e", "--editable", "-c", "--constraint"}
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def extract_package_query(action: Action) -> tuple[str, str] | None:
|
|
393
|
+
"""Return (registry, package) for a pip/npm package action, or None.
|
|
394
|
+
|
|
395
|
+
Version specifiers are stripped (the registry answer covers them); invocations that don't
|
|
396
|
+
name a registry package (`pip freeze`, `pip install -r ...`, `pip install -e .`) are refused.
|
|
397
|
+
"""
|
|
398
|
+
if action.name != "bash":
|
|
399
|
+
return None
|
|
400
|
+
command = action.arguments.get("command")
|
|
401
|
+
if not isinstance(command, str):
|
|
402
|
+
return None
|
|
403
|
+
if m := _PIP_CMD.search(command):
|
|
404
|
+
for token in m.group(1).split():
|
|
405
|
+
if token in _PIP_NON_REGISTRY_FLAGS:
|
|
406
|
+
return None
|
|
407
|
+
if token.startswith("-") or token.startswith("2>"):
|
|
408
|
+
continue
|
|
409
|
+
name = re.split(r"[=<>!\[]", token.strip("'\""))[0]
|
|
410
|
+
if not name or name.startswith((".", "/")):
|
|
411
|
+
return None # a path, not a registry package
|
|
412
|
+
return ("pypi", name)
|
|
413
|
+
return None
|
|
414
|
+
if m := _NPM_QUERY.search(command):
|
|
415
|
+
return ("npm", m.group(1))
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# (registry, package) -> rendered block, or None for a failed lookup. Failures are cached
|
|
420
|
+
# too: an unreachable registry must cost ONE timeout per package, not one per step.
|
|
421
|
+
_REGISTRY_MEMO: dict[tuple[str, str], str | None] = {}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def registry_grounded_knowledge(
|
|
425
|
+
knowledge: str | None, action: Action, *, fetch: FetchFn = http_get
|
|
426
|
+
) -> str | None:
|
|
427
|
+
"""Append the real registry record for a package action (harmless keyless poll).
|
|
428
|
+
|
|
429
|
+
PyPI's JSON API answers `pip show/install` with the actual current name/version/summary/
|
|
430
|
+
dependencies — the values the model otherwise invents. Unreachable registries degrade to
|
|
431
|
+
no grounding.
|
|
432
|
+
"""
|
|
433
|
+
query = extract_package_query(action)
|
|
434
|
+
if query is None:
|
|
435
|
+
return knowledge
|
|
436
|
+
registry, package = query
|
|
437
|
+
memoize = fetch is http_get # injected fetch fns (tests) must not share process state
|
|
438
|
+
if memoize and query in _REGISTRY_MEMO:
|
|
439
|
+
block = _REGISTRY_MEMO[query]
|
|
440
|
+
if block is None:
|
|
441
|
+
return knowledge
|
|
442
|
+
return f"{knowledge}\n\n{block}" if knowledge else block
|
|
443
|
+
if registry == "pypi":
|
|
444
|
+
url = f"https://pypi.org/pypi/{package}/json"
|
|
445
|
+
else:
|
|
446
|
+
url = f"https://registry.npmjs.org/{package}/latest"
|
|
447
|
+
try:
|
|
448
|
+
payload = json.loads(fetch(url, {"Accept": "application/json"}))
|
|
449
|
+
except Exception: # noqa: BLE001 - registry unreachable: degrade to no grounding
|
|
450
|
+
if memoize:
|
|
451
|
+
_REGISTRY_MEMO[query] = None
|
|
452
|
+
return knowledge
|
|
453
|
+
info = payload.get("info", payload)
|
|
454
|
+
requires = info.get("requires_dist") or info.get("dependencies") or []
|
|
455
|
+
if isinstance(requires, dict):
|
|
456
|
+
requires = [f"{k}{v}" for k, v in requires.items()]
|
|
457
|
+
block = (
|
|
458
|
+
f"## registry record: {package} ({registry}, live)\n"
|
|
459
|
+
f"name: {info.get('name', package)}\n"
|
|
460
|
+
f"version: {info.get('version', '?')}\n"
|
|
461
|
+
f"summary: {info.get('summary', '')}\n"
|
|
462
|
+
f"requires: {', '.join(str(r) for r in requires[:12])}\n"
|
|
463
|
+
f"requires_python: {info.get('requires_python', '')}"
|
|
464
|
+
)
|
|
465
|
+
if memoize:
|
|
466
|
+
_REGISTRY_MEMO[query] = block
|
|
467
|
+
return f"{knowledge}\n\n{block}" if knowledge else block
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def get_grounder(kind: str) -> Grounder:
|
|
471
|
+
"""Construct the configured grounder (`HarnessConfig.grounder`): none | brave | fetch."""
|
|
472
|
+
if kind == "none":
|
|
473
|
+
return NullGrounder()
|
|
474
|
+
if kind == "fetch":
|
|
475
|
+
return FetchGrounder()
|
|
476
|
+
if kind == "brave":
|
|
477
|
+
api_key = os.environ.get("BRAVE_SEARCH_API_KEY", "")
|
|
478
|
+
if not api_key:
|
|
479
|
+
raise ValueError(
|
|
480
|
+
"grounder 'brave' needs BRAVE_SEARCH_API_KEY set; get a free key at "
|
|
481
|
+
"https://api-dashboard.search.brave.com/ or set grounder = 'none'"
|
|
482
|
+
)
|
|
483
|
+
return BraveGrounder(api_key)
|
|
484
|
+
raise ValueError(f"unknown grounder {kind!r}; choose one of {', '.join(GROUNDER_KINDS)}")
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def render_grounding(results: list[GroundingResult]) -> str:
|
|
488
|
+
"""Render results as compact markdown for the KB cache and the re-completion prompt."""
|
|
489
|
+
if not results:
|
|
490
|
+
return "(no results)"
|
|
491
|
+
return "\n".join(f"- {r.title} ({r.url}): {r.snippet}".strip() for r in results)
|