Skip to content

Commit 9822e10

Browse files
Add agent harness state (#79)
1 parent 1ef3b17 commit 9822e10

4 files changed

Lines changed: 148 additions & 5 deletions

File tree

src/ucode/databricks.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
import platform
1111
import re
12+
import shlex
1213
import shutil
1314
import subprocess
1415
from typing import cast
@@ -527,9 +528,16 @@ def list_databricks_apps(workspace: str) -> list[dict]:
527528

528529

529530
def build_auth_shell_command(workspace: str) -> str:
531+
workspace_arg = shlex.quote(workspace.rstrip("/"))
532+
cli_command = (
533+
"env -u DATABRICKS_CONFIG_PROFILE "
534+
f"databricks auth token --host {workspace_arg} --force-refresh --output json "
535+
"| jq -r '.access_token'"
536+
)
530537
return (
531-
f"databricks auth token --host {workspace} --force-refresh --output json "
532-
f"| jq -r '.access_token'"
538+
'if [ -n "${DATABRICKS_BEARER:-}" ]; then '
539+
'printf "%s\\n" "$DATABRICKS_BEARER"; '
540+
f"else {cli_command}; fi"
533541
)
534542

535543

src/ucode/state.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import json
66

77
from ucode.config_io import APP_DIR, is_dry_run
8-
from ucode.databricks import build_shared_base_urls
8+
from ucode.databricks import build_auth_shell_command, build_shared_base_urls
99

1010
STATE_PATH = APP_DIR / "state.json"
1111
STATE_VERSION = 3
12+
AUTH_COMMAND_TIMEOUT_MS = 5000
13+
AUTH_REFRESH_INTERVAL_MS = 900_000
1214

1315

1416
def load_full_state() -> dict:
@@ -52,6 +54,12 @@ def save_state(state: dict) -> None:
5254

5355

5456
def hydrate_state(state: dict) -> dict:
57+
"""Normalize a workspace state entry and add derived harness config.
58+
59+
:param state: Raw workspace state entry from ``state.json``.
60+
:returns: Hydrated workspace state with stable ``managed_configs``,
61+
``base_urls``, and per-agent ``agents`` entries.
62+
"""
5563
if not isinstance(state, dict):
5664
return {}
5765

@@ -71,12 +79,81 @@ def hydrate_state(state: dict) -> dict:
7179
workspace = hydrated.get("workspace")
7280
if workspace:
7381
hydrated["base_urls"] = build_shared_base_urls(workspace)
82+
hydrated["agents"] = build_agent_state(hydrated)
7483
else:
7584
hydrated["base_urls"] = {}
85+
hydrated["agents"] = {}
7686

7787
return hydrated
7888

7989

90+
def build_agent_state(state: dict) -> dict[str, dict]:
91+
"""Build per-agent harness configuration for a workspace.
92+
93+
The returned shape is intended for downstream tools that want to reuse
94+
ucode's configured gateway URLs and auth command without duplicating
95+
endpoint construction logic.
96+
97+
:param state: Hydrated workspace state containing ``workspace``,
98+
``base_urls``, and discovered model lists.
99+
:returns: Mapping from agent name to its reusable configuration.
100+
"""
101+
workspace = state.get("workspace")
102+
if not isinstance(workspace, str) or not workspace:
103+
return {}
104+
105+
base_urls_value = state.get("base_urls")
106+
base_urls = base_urls_value if isinstance(base_urls_value, dict) else {}
107+
auth_command = build_auth_shell_command(workspace)
108+
claude_models_value = state.get("claude_models")
109+
claude_models: dict = claude_models_value if isinstance(claude_models_value, dict) else {}
110+
codex_models_value = state.get("codex_models")
111+
codex_models = codex_models_value if isinstance(codex_models_value, list) else []
112+
gemini_models_value = state.get("gemini_models")
113+
gemini_models = gemini_models_value if isinstance(gemini_models_value, list) else []
114+
115+
claude_model = (
116+
claude_models.get("opus") or claude_models.get("sonnet") or claude_models.get("haiku")
117+
)
118+
codex_model = codex_models[0] if codex_models else None
119+
pi_model = claude_model or codex_model or (gemini_models[0] if gemini_models else None)
120+
121+
agents: dict[str, dict] = {
122+
"claude": {
123+
"model": claude_model,
124+
"base_url": base_urls.get("claude"),
125+
"auth_command": auth_command,
126+
"auth_refresh_interval_ms": AUTH_REFRESH_INTERVAL_MS,
127+
"env": {
128+
"ANTHROPIC_BASE_URL": base_urls.get("claude"),
129+
"CLAUDE_CODE_API_KEY_HELPER_TTL_MS": str(AUTH_REFRESH_INTERVAL_MS),
130+
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1",
131+
},
132+
},
133+
"codex": {
134+
"model": codex_model,
135+
"base_url": base_urls.get("codex"),
136+
"auth_command": auth_command,
137+
"auth": {
138+
"command": "sh",
139+
"args": ["-c", auth_command],
140+
"timeout_ms": AUTH_COMMAND_TIMEOUT_MS,
141+
"refresh_interval_ms": AUTH_REFRESH_INTERVAL_MS,
142+
},
143+
},
144+
"pi": {
145+
"model": pi_model,
146+
"base_urls": base_urls.get("pi") if isinstance(base_urls.get("pi"), dict) else {},
147+
"auth_command": auth_command,
148+
"auth_refresh_interval_ms": AUTH_REFRESH_INTERVAL_MS,
149+
},
150+
}
151+
return {
152+
name: {key: value for key, value in config.items() if value is not None}
153+
for name, config in agents.items()
154+
}
155+
156+
80157
def clear_state() -> None:
81158
"""Remove the current workspace entry from state."""
82159
full = load_full_state()

tests/test_databricks.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def test_parses_access_token(self):
105105
assert "jq" in cmd
106106
assert ".access_token" in cmd
107107
assert "--force-refresh" in cmd
108+
assert "DATABRICKS_BEARER" in cmd
109+
assert "DATABRICKS_CONFIG_PROFILE" in cmd
108110

109111
def test_returns_token_when_auth_succeeds(self, tmp_path):
110112
# Fake databricks binary that always returns a valid token JSON.
@@ -118,10 +120,31 @@ def test_returns_token_when_auth_succeeds(self, tmp_path):
118120
["sh", "-c", cmd],
119121
capture_output=True,
120122
text=True,
121-
env={**os.environ, "PATH": f"{tmp_path}:{os.environ['PATH']}"},
123+
env={
124+
**os.environ,
125+
"PATH": f"{tmp_path}:{os.environ['PATH']}",
126+
"DATABRICKS_BEARER": "",
127+
},
122128
)
123129
assert result.stdout.strip() == "good-token"
124130

131+
def test_prefers_databricks_bearer(self, tmp_path):
132+
fake = tmp_path / "databricks"
133+
fake.write_text("#!/bin/sh\nexit 1\n")
134+
fake.chmod(0o755)
135+
cmd = build_auth_shell_command(WS)
136+
result = subprocess.run(
137+
["sh", "-c", cmd],
138+
capture_output=True,
139+
text=True,
140+
env={
141+
**os.environ,
142+
"PATH": f"{tmp_path}:{os.environ['PATH']}",
143+
"DATABRICKS_BEARER": "bearer-token",
144+
},
145+
)
146+
assert result.stdout.strip() == "bearer-token"
147+
125148

126149
class TestGetDatabricksToken:
127150
def _fake_databricks(self, tmp_path, script: str) -> dict:

tests/test_state.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ucode.state as state_mod
1111
from ucode.state import (
1212
STATE_VERSION,
13+
build_agent_state,
1314
clear_state,
1415
hydrate_state,
1516
load_full_state,
@@ -27,6 +28,12 @@
2728
"anthropic": f"{FAKE_WS}/ai-gateway/anthropic/v1",
2829
"gemini": f"{FAKE_WS}/ai-gateway/gemini/v1beta",
2930
},
31+
"copilot": f"{FAKE_WS}/ai-gateway/mlflow/v1",
32+
"pi": {
33+
"claude": f"{FAKE_WS}/ai-gateway/anthropic",
34+
"openai": f"{FAKE_WS}/ai-gateway/codex/v1",
35+
"gemini": f"{FAKE_WS}/ai-gateway/gemini/v1beta",
36+
},
3037
}
3138

3239

@@ -140,7 +147,7 @@ def test_clear_when_no_state_is_noop(self):
140147
class TestHydrateState:
141148
def test_empty_input_returns_empty(self):
142149
result = hydrate_state({})
143-
assert result == {"managed_configs": {}, "base_urls": {}}
150+
assert result == {"managed_configs": {}, "base_urls": {}, "agents": {}}
144151

145152
def test_non_dict_returns_empty(self):
146153
assert hydrate_state(None) == {} # type: ignore[arg-type]
@@ -153,6 +160,28 @@ def test_populates_base_urls_when_workspace_present(self):
153160
def test_no_base_urls_when_no_workspace(self):
154161
result = hydrate_state({"claude_models": {}})
155162
assert result["base_urls"] == {}
163+
assert result["agents"] == {}
164+
165+
def test_populates_agent_state_when_workspace_present(self):
166+
result = hydrate_state(
167+
{
168+
"workspace": FAKE_WS,
169+
"claude_models": {"opus": "claude-opus"},
170+
"codex_models": ["gpt-5"],
171+
}
172+
)
173+
174+
assert result["agents"]["claude"]["model"] == "claude-opus"
175+
assert result["agents"]["claude"]["base_url"] == FAKE_URLS["claude"]
176+
assert result["agents"]["claude"]["auth_command"].startswith("if [ -n")
177+
assert result["agents"]["codex"]["model"] == "gpt-5"
178+
assert result["agents"]["codex"]["base_url"] == FAKE_URLS["codex"]
179+
assert (
180+
result["agents"]["codex"]["auth"]["args"][1]
181+
== result["agents"]["codex"]["auth_command"]
182+
)
183+
assert result["agents"]["pi"]["model"] == "claude-opus"
184+
assert result["agents"]["pi"]["base_urls"] == FAKE_URLS["pi"]
156185

157186
def test_normalizes_managed_configs_dict_entry(self):
158187
state = {"managed_configs": {"claude": {"keys": [["env", "X"]]}}}
@@ -171,6 +200,12 @@ def test_drops_falsy_managed_configs(self):
171200
assert "claude" not in result["managed_configs"]
172201

173202

203+
class TestBuildAgentState:
204+
def test_returns_empty_without_workspace(self):
205+
result = build_agent_state({"base_urls": FAKE_URLS})
206+
assert result == {}
207+
208+
174209
# ---------------------------------------------------------------------------
175210
# mark_tool_managed
176211
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)