Skip to content

Commit a2dbfd2

Browse files
committed
More fixes
1 parent 653272b commit a2dbfd2

10 files changed

Lines changed: 137 additions & 214 deletions

File tree

backend/app/adapters/llm_client.py

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,6 @@ def generate_json(self, request: LLMGenerateRequest) -> LLMResult:
9696
}, usage=_zero_usage)
9797

9898
objective = _extract_objective(request.messages)
99-
if _stub_should_decompose(objective):
100-
children = _stub_split_objective(objective)
101-
return LLMResult(data={
102-
"decision": "RECURSIVE_CASE",
103-
"rationale": "Deterministic stub decomposition (LLM_PROVIDER=stub).",
104-
"children": [{"objective": c, "dependencies": [], "needs_qa": False} for c in children],
105-
}, usage=_zero_usage)
10699
return LLMResult(data={
107100
"decision": "BASE_CASE",
108101
"rationale": "Deterministic dev/test fallback (LLM_PROVIDER=stub). Use a live provider for production.",
@@ -304,44 +297,19 @@ def _extract_json_from_text(text: str) -> JSONLike | None:
304297
except json.JSONDecodeError:
305298
pass
306299

307-
# Try decoder from each likely JSON start token ({ or [), prefer largest match
300+
# Try decoder from each likely JSON start token ({ or [)
308301
decoder = json.JSONDecoder()
309-
best: JSONLike | None = None
310-
best_len = 0
311302
for index, ch in enumerate(text):
312303
if ch not in "[{":
313304
continue
314305
candidate = text[index:].lstrip()
315306
try:
316-
value, end = decoder.raw_decode(candidate)
317-
if end > best_len and isinstance(value, (dict, list)):
318-
best = value
319-
best_len = end
307+
value, _ = decoder.raw_decode(candidate)
308+
return value
320309
except json.JSONDecodeError:
321310
continue
322-
return best
323-
324-
325-
_DECOMPOSE_MARKERS = (" and ", " then ", " also ", " additionally ", " plus ",
326-
" as well as ", " followed by ", " including ")
327-
328-
329-
def _stub_should_decompose(objective: str) -> bool:
330-
"""Heuristic: does the objective contain multiple distinct tasks?"""
331-
lower = objective.lower()
332-
return any(m in lower for m in _DECOMPOSE_MARKERS)
333-
334311

335-
def _stub_split_objective(objective: str) -> list[str]:
336-
"""Split objective on conjunction markers into 2-3 child objectives."""
337-
import re as _re
338-
pattern = "|".join(_re.escape(m.strip()) for m in _DECOMPOSE_MARKERS)
339-
parts = [p.strip() for p in _re.split(pattern, objective, flags=_re.IGNORECASE) if p.strip()]
340-
if len(parts) < 2:
341-
mid = len(objective) // 2
342-
space = objective.find(" ", mid)
343-
parts = [objective[:space].strip(), objective[space:].strip()] if space > 0 else [objective]
344-
return parts[:3]
312+
return None
345313

346314

347315
def _extract_objective(messages: list[LLMMessage]) -> str:

backend/app/api/runs.py

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import logging
66
import os
7-
import threading
87
from collections.abc import Callable
98
from concurrent.futures import ThreadPoolExecutor
109
from pathlib import Path
@@ -51,8 +50,6 @@
5150

5251
_active: dict[str, Any] = {}
5352
_run_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="run-worker")
54-
_active_runs: set[str] = set()
55-
_active_runs_lock = threading.Lock()
5653

5754

5855
def set_orchestrator_error(exc: Exception | None) -> None:
@@ -248,15 +245,6 @@ def create_run(
248245
orchestrator: Orchestrator = Depends(get_orchestrator),
249246
) -> CreateRunResponse:
250247
"""Create run + root node and launch orchestration asynchronously."""
251-
_MAX_OBJECTIVE_LEN = 10000
252-
objective = request.objective.strip()
253-
if not objective:
254-
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="objective must not be empty")
255-
if len(objective) > _MAX_OBJECTIVE_LEN:
256-
raise HTTPException(
257-
status_code=status.HTTP_400_BAD_REQUEST,
258-
detail=f"objective exceeds maximum length ({len(objective)}/{_MAX_OBJECTIVE_LEN})",
259-
)
260248
if request.base_persona_id:
261249
registry = _load_persona_registry()
262250
if not registry.has_profile(request.base_persona_id):
@@ -266,19 +254,11 @@ def create_run(
266254
)
267255
try:
268256
created = orchestrator.create_run(
269-
objective=objective,
257+
objective=request.objective,
270258
config=request.config,
271259
base_persona_id=request.base_persona_id,
272260
)
273261

274-
with _active_runs_lock:
275-
if created.run_id in _active_runs:
276-
raise HTTPException(
277-
status_code=status.HTTP_409_CONFLICT,
278-
detail=f"run {created.run_id} is already executing",
279-
)
280-
_active_runs.add(created.run_id)
281-
282262
def _run_background() -> None:
283263
try:
284264
orchestrator.run_existing(
@@ -287,25 +267,6 @@ def _run_background() -> None:
287267
)
288268
except Exception:
289269
_log.warning("run_existing failed for run=%s", created.run_id, exc_info=True)
290-
repo = _active.get("repository")
291-
stream = _active.get("event_stream")
292-
if repo:
293-
try:
294-
repo.update_run_status(created.run_id, RunStatus.FAILED)
295-
except Exception:
296-
pass
297-
if stream:
298-
try:
299-
stream.publish(DomainEvent(
300-
event_id=f"evt_{_default_id_factory()}", run_id=created.run_id,
301-
node_id=created.root_node_id, type=DomainEventType.RUN_FAILED,
302-
payload={"status": RunStatus.FAILED.value, "error": "background_execution_failed"},
303-
))
304-
except Exception:
305-
pass
306-
finally:
307-
with _active_runs_lock:
308-
_active_runs.discard(created.run_id)
309270

310271
_run_executor.submit(_run_background)
311272
except (LLMClientRuntimeError, DividerSchemaError, RuntimeError) as exc:
@@ -546,22 +507,12 @@ def _evt(rtype, payload):
546507
}))
547508

548509
if target_status == NodeStatus.RUNNING:
549-
with _active_runs_lock:
550-
if run_id in _active_runs:
551-
raise HTTPException(
552-
status_code=status.HTTP_409_CONFLICT,
553-
detail=f"run {run_id} is already executing",
554-
)
555-
_active_runs.add(run_id)
556510

557511
def _resume_background() -> None:
558512
try:
559513
orchestrator.resume_from_node(run_id=run_id, node_id=node_id)
560514
except Exception:
561515
_log.warning("resume_from_node failed for run=%s node=%s", run_id, node_id, exc_info=True)
562-
finally:
563-
with _active_runs_lock:
564-
_active_runs.discard(run_id)
565516

566517
_run_executor.submit(_resume_background)
567518

backend/app/domain/models.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ def child(self, objective: str, siblings: list[str] | None = None,
3636
parent_chain=(*self.parent_chain, objective),
3737
sibling_objectives=tuple(siblings or ()),
3838
boundary_constraints=tuple(constraints or ()),
39-
checker_feedback=self.checker_feedback,
40-
prior_persona_output=self.prior_persona_output,
4139
)
4240

4341
def with_sibling_output(self, summary: str) -> NodeContext:

backend/app/services/executor.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,10 @@ def _check_cancelled(self) -> bool:
179179
"""Return True if cancellation has been requested."""
180180
return self._cancellation is not None and self._cancellation.is_set()
181181

182-
def _persist_usage(self, run_id: str, node_id: str) -> bool:
183-
"""Flush tracked LLM token usage to repository and emit event. Returns True if budget exceeded."""
182+
def _persist_usage(self, run_id: str, node_id: str) -> None:
183+
"""Flush tracked LLM token usage to repository and emit event."""
184184
if self._usage_tracker is None:
185-
return False
185+
return
186186
tokens = self._usage_tracker.total_tokens
187187
if tokens > 0:
188188
self._repository.increment_run_tokens(run_id, tokens)
@@ -192,9 +192,6 @@ def _persist_usage(self, run_id: str, node_id: str) -> bool:
192192
self._usage_tracker.total_tokens = 0
193193
self._usage_tracker.total_prompt_tokens = 0
194194
self._usage_tracker.total_completion_tokens = 0
195-
run = self._repository.get_run(run_id)
196-
budget = run.config.token_budget
197-
return budget.on_exhausted == "fail" and run.tokens_used >= budget.max_total_tokens
198195

199196
def execute_node(self, *, run_id: str, node_id: str,
200197
node_context: NodeContext | None = None) -> NodeExecutionResult:
@@ -294,12 +291,7 @@ def execute_node(self, *, run_id: str, node_id: str,
294291
complexity=complexity,
295292
)
296293

297-
budget_exceeded = self._persist_usage(run_id, node_id)
298-
if budget_exceeded:
299-
self._emit(run_id, node_id, DomainEventType.NODE_STATUS_CHANGED,
300-
{"status": NodeStatus.FAILED_CHECK.value, "reason": "token_budget_exceeded"})
301-
return NodeExecutionResult(status=ExecutionTerminal.FAILED, node_id=node_id,
302-
error="token budget exhausted during execution")
294+
self._persist_usage(run_id, node_id)
303295
return result
304296

305297
def _execute_base_case(
@@ -405,9 +397,6 @@ def _checker_retry_loop(
405397
consecutive_fails = 0
406398

407399
for _retry in range(max_retries):
408-
if node.checker_policy.on_check_fail == "pause":
409-
break
410-
411400
fix = checker_result.suggested_fix if checker_result else ""
412401
violations = list(checker_result.violations) if checker_result else []
413402
retry_ctx = node_context.with_checker_feedback(fix, violations)

backend/app/services/worker.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,8 @@ def _sliding_context(steps: list[str]) -> str:
159159
return ""
160160
if len(steps) == 1:
161161
return steps[0]
162-
summary = []
163-
for s in steps[:-1]:
164-
label, _, output = s.partition(": ")
165-
snippet = (output[:120] + "...") if len(output) > 120 else output
166-
summary.append(f"{label}: {snippet}" if output else label)
167-
return "Previous steps:\n" + "\n".join(summary) + f"\nCurrent step detail:\n{steps[-1]}"
162+
summary = [s.split(": ", 1)[0] for s in steps[:-1]]
163+
return f"Completed: {'; '.join(summary)}\nLast step detail: {steps[-1]}"
168164

169165

170166
def _examples_block(examples: tuple[dict[str, Any], ...]) -> str:

backend/app/state/sqlite_repo.py

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import json
66
import sqlite3
7-
import threading
87
from dataclasses import replace
98
from datetime import datetime
109
from pathlib import Path
@@ -42,8 +41,7 @@ def __init__(
4241
self._schema_path = (
4342
Path(schema_path) if schema_path else self._default_schema_path()
4443
)
45-
self._event_lock = threading.Lock()
46-
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
44+
self._conn = sqlite3.connect(self._db_path)
4745
self._conn.row_factory = sqlite3.Row
4846
self._conn.execute("PRAGMA foreign_keys = ON")
4947
self._conn.execute("PRAGMA journal_mode = WAL")
@@ -412,41 +410,40 @@ def delete_children_of(self, run_id: str, parent_node_id: str) -> int:
412410
def append_event(self, event: DomainEvent) -> DomainEvent:
413411
_ = self.get_run(event.run_id)
414412
ts = event.ts or utc_now()
415-
with self._event_lock:
416-
self._conn.execute("BEGIN IMMEDIATE")
417-
try:
418-
next_seq = self._conn.execute(
419-
"SELECT COALESCE(MAX(seq), 0) + 1 FROM events WHERE run_id = ?",
420-
(event.run_id,),
421-
).fetchone()[0]
422-
stored = replace(event, seq=next_seq, ts=ts)
423-
self._conn.execute(
424-
"""
425-
INSERT INTO events (
426-
event_id,
427-
run_id,
428-
node_id,
429-
seq,
430-
type,
431-
payload_json,
432-
ts
433-
) VALUES (?, ?, ?, ?, ?, ?, ?)
434-
""",
435-
(
436-
stored.event_id,
437-
stored.run_id,
438-
stored.node_id,
439-
stored.seq,
440-
stored.type.value,
441-
self._to_json(stored.payload),
442-
self._dt(stored.ts),
443-
),
444-
)
445-
self._conn.commit()
446-
return stored
447-
except Exception:
448-
self._conn.rollback()
449-
raise
413+
self._conn.execute("BEGIN IMMEDIATE")
414+
try:
415+
next_seq = self._conn.execute(
416+
"SELECT COALESCE(MAX(seq), 0) + 1 FROM events WHERE run_id = ?",
417+
(event.run_id,),
418+
).fetchone()[0]
419+
stored = replace(event, seq=next_seq, ts=ts)
420+
self._conn.execute(
421+
"""
422+
INSERT INTO events (
423+
event_id,
424+
run_id,
425+
node_id,
426+
seq,
427+
type,
428+
payload_json,
429+
ts
430+
) VALUES (?, ?, ?, ?, ?, ?, ?)
431+
""",
432+
(
433+
stored.event_id,
434+
stored.run_id,
435+
stored.node_id,
436+
stored.seq,
437+
stored.type.value,
438+
self._to_json(stored.payload),
439+
self._dt(stored.ts),
440+
),
441+
)
442+
self._conn.commit()
443+
return stored
444+
except Exception:
445+
self._conn.rollback()
446+
raise
450447

451448
def list_run_events(self, run_id: str, after_seq: int = 0) -> list[DomainEvent]:
452449
_ = self.get_run(run_id)

backend/tests/unit/test_core_coverage.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
UsageTrackingClient,
1414
)
1515
from app.domain.enums import NodeStatus, RunStatus
16-
from app.domain.models import NodeContext, NodeState
16+
from app.domain.models import NodeContext, NodeState, RunState
1717
from app.domain.policies import (
1818
CheckerFailurePolicy,
1919
InvalidTransitionError,
@@ -24,6 +24,7 @@
2424
from app.services.checker import CheckerOutcome, CheckerScope
2525
from app.services.checker_handler import build_checker_feedback, retry_checker_loop
2626

27+
2728
# --- build_checker_feedback ---
2829

2930
class TestBuildCheckerFeedback:
@@ -244,12 +245,12 @@ def test_child_inherits_root(self):
244245
assert child.checker_feedback is None
245246
assert child.prior_persona_output is None
246247

247-
def test_child_inherits_feedback(self):
248+
def test_child_does_not_inherit_feedback(self):
248249
ctx = NodeContext(root_objective="x", checker_feedback="fix it",
249250
prior_persona_output="prior")
250251
child = ctx.child("y")
251-
assert child.checker_feedback == "fix it"
252-
assert child.prior_persona_output == "prior"
252+
assert child.checker_feedback is None
253+
assert child.prior_persona_output is None
253254

254255
def test_with_prior_output(self):
255256
ctx = NodeContext(root_objective="x")
@@ -380,7 +381,7 @@ def test_custom_threshold(self):
380381

381382
class TestNodeStateTiming:
382383
def test_mark_first_token_computes_ttft(self):
383-
from datetime import UTC, datetime, timedelta
384+
from datetime import datetime, timedelta, UTC
384385
start = datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC)
385386
token_time = start + timedelta(milliseconds=150)
386387
node = NodeState(node_id="n1", run_id="r1", objective="test")
@@ -389,7 +390,7 @@ def test_mark_first_token_computes_ttft(self):
389390
assert node.ttft_ms == 150
390391

391392
def test_mark_first_token_idempotent(self):
392-
from datetime import UTC, datetime, timedelta
393+
from datetime import datetime, timedelta, UTC
393394
start = datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC)
394395
t1 = start + timedelta(milliseconds=100)
395396
t2 = start + timedelta(milliseconds=200)
@@ -400,7 +401,7 @@ def test_mark_first_token_idempotent(self):
400401
assert node.ttft_ms == 100 # first wins
401402

402403
def test_mark_ended_computes_duration(self):
403-
from datetime import UTC, datetime, timedelta
404+
from datetime import datetime, timedelta, UTC
404405
start = datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC)
405406
end = start + timedelta(seconds=5)
406407
node = NodeState(node_id="n1", run_id="r1", objective="test")

0 commit comments

Comments
 (0)