fix codegen timeout

This commit is contained in:
Denton Social
2026-09-24 03:39:48 -05:00
parent 300c20714b
commit bb97a09caa
5 changed files with 44 additions and 5 deletions
+5 -2
View File
@@ -209,8 +209,11 @@ unit tests (24) + box integration tests (2).
body…" badge) → sync codegen write → `materialize_skill` → hot-merge into the body…" badge) → sync codegen write → `materialize_skill` → hot-merge into the
tree → the new leaf runs directly so the request is answered. `create_category` tree → the new leaf runs directly so the request is answered. `create_category`
runs the same chain after authoring the category (`create_category` → runs the same chain after authoring the category (`create_category` →
`create_skill` → run). Codegen failure leaves a navigable stub and returns a `create_skill` → run). Codegen failure — including a request timeout — leaves
graceful `create_skill` result. a navigable stub and returns a graceful `create_skill` result; a timeout is
raised as `CodegenError` by the client, never a raw `TimeoutError`. The
default codegen timeout is 1200s (`cli.build_scheduler`); raise
`codegen.timeout` in config for harder prompts.
### Code principles ### Code principles
- **No mocking.** The decision engine is always real SemIf; the LLM is always a - **No mocking.** The decision engine is always real SemIf; the LLM is always a
+1 -1
View File
@@ -52,7 +52,7 @@ def build_scheduler(config: dict) -> tuple[Scheduler, dict]:
"base_url", config.get("llm", {}).get("base_url", "http://localhost:11434/v1") "base_url", config.get("llm", {}).get("base_url", "http://localhost:11434/v1")
), ),
model=codegen_cfg.get("model", "qwen38-iq3s"), model=codegen_cfg.get("model", "qwen38-iq3s"),
timeout=float(codegen_cfg.get("timeout", 600.0)), timeout=float(codegen_cfg.get("timeout", 1200.0)),
) )
log = DecisionLog(config.get("log", "data/decisions.jsonl")) log = DecisionLog(config.get("log", "data/decisions.jsonl"))
trace = TraceLog(config.get("trace", "data/runs.jsonl")) trace = TraceLog(config.get("trace", "data/runs.jsonl"))
+4
View File
@@ -72,6 +72,10 @@ class CodegenClient:
raise CodegenError( raise CodegenError(
f"codegen endpoint unreachable at {url}: {exc}. Is your local server running?" f"codegen endpoint unreachable at {url}: {exc}. Is your local server running?"
) from exc ) from exc
except TimeoutError as exc:
raise CodegenError(
f"codegen request timed out after {self.timeout}s at {url}"
) from exc
return payload["choices"][0]["message"]["content"] return payload["choices"][0]["message"]["content"]
+2 -1
View File
@@ -162,7 +162,7 @@ def test_generate_skill_body_codegen(tmp_path):
client = CodegenClient( client = CodegenClient(
base_url=codegen_cfg.get("base_url", "http://localhost:11434/v1"), base_url=codegen_cfg.get("base_url", "http://localhost:11434/v1"),
model=codegen_cfg.get("model", "qwen38-iq3s"), model=codegen_cfg.get("model", "qwen38-iq3s"),
timeout=float(codegen_cfg.get("timeout", 600.0)), timeout=float(codegen_cfg.get("timeout", 1200.0)),
) )
tree = build_tree(build_skills({"skills": {}})) tree = build_tree(build_skills({"skills": {}}))
draft = SkillDraft( draft = SkillDraft(
@@ -196,6 +196,7 @@ def test_create_skill_empty_category_does_not_wedge(tmp_path):
config["log"] = str(tmp_path / "decisions.jsonl") config["log"] = str(tmp_path / "decisions.jsonl")
config["trace"] = str(tmp_path / "runs.jsonl") config["trace"] = str(tmp_path / "runs.jsonl")
scheduler, config = build_scheduler(config) scheduler, config = build_scheduler(config)
scheduler.codegen = None # wedge regression only; skip the ~7min codegen body write
scheduler.tree["travel_planning"] = [] scheduler.tree["travel_planning"] = []
for _ in range(2): for _ in range(2):
+32 -1
View File
@@ -8,12 +8,14 @@ the CodegenClient itself is real, not mocked.
import json import json
import threading import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import pytest import pytest
from semif_agent.codegen import ( from semif_agent.codegen import (
CodegenClient, CodegenClient,
CodegenError,
build_skill_body_prompt, build_skill_body_prompt,
generate_skill_body, generate_skill_body,
parse_skill_body, parse_skill_body,
@@ -217,10 +219,39 @@ def test_codegen_client_unreachable_raises(tmp_path):
client = CodegenClient(base_url="http://127.0.0.1:1/v1", model="test", timeout=2) client = CodegenClient(base_url="http://127.0.0.1:1/v1", model="test", timeout=2)
tree = build_tree(build_skills({"skills": {}})) tree = build_tree(build_skills({"skills": {}}))
draft = SkillDraft(name="probe", description="Probe the service.") draft = SkillDraft(name="probe", description="Probe the service.")
with pytest.raises(Exception): with pytest.raises(CodegenError):
generate_skill_body(client, Request("is the service up?"), "tracking", draft, tree) generate_skill_body(client, Request("is the service up?"), "tracking", draft, tree)
class _SilentOpenAI(BaseHTTPRequestHandler):
"""Accepts the request but never replies; the client must time out."""
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
self.rfile.read(length)
time.sleep(5)
def log_message(self, format, *args):
pass
def test_chat_timeout_raises_codegen_error():
handler = type("Handler", (_SilentOpenAI,), {})
httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
try:
client = CodegenClient(
base_url=f"http://127.0.0.1:{httpd.server_address[1]}/v1",
model="test",
timeout=0.5,
)
with pytest.raises(CodegenError, match="timed out"):
client.chat([{"role": "user", "content": "hi"}])
finally:
httpd.shutdown()
httpd.server_close()
def test_chat_omits_max_tokens_by_default(): def test_chat_omits_max_tokens_by_default():
httpd, base = _fake_server(GOOD_BODY) httpd, base = _fake_server(GOOD_BODY)
try: try: