U1 — Agentic core (victor/agent/ + victor/runtime/)¶
412 modules / 202,631 LOC / 5,295 touching commits (12m). Reviewer: parallel unit review, 2026-09-03.
Overview¶
victor/agent/ (+victor/runtime/, 6 files) is the execution heart: orchestrator facade → 6 canonical services → 24 *_runtime.py helpers → pipeline/store/streaming subsystems. Health: mixed — effects are genuinely out of the facade (no direct provider/DB calls in orchestrator.py), but the Chat service is a handler-injection shell, so decomposition is ~half real. Two hot-path data-movement defects (per-iteration full-history model_dump(), sequential tool execution behind a dead parallel flag) are the top perf risks; churn concentrates in exactly the files the decomposition was supposed to shrink. Prior backlog F-002/F-005/F-008 confirmed still open.
Findings¶
| ID | Sev | Location | Finding | Why it matters | Fix sketch | Effort |
|---|---|---|---|---|---|---|
| U1-1 | P1 | victor/agent/services/turn_execution_runtime.py:1610-1614 | Every loop iteration does [msg.model_dump() for msg in self._chat_context.messages] — pydantic-serializes the ENTIRE history for tool selection; the consumer uses only last 2 messages (victor/storage/cache/generic_result_cache.py:127-160) |
O(history) CPU + alloc per model turn on the hottest path; pure data movement across a service seam | Pass messages[-2:] (or (depth, recent2_tuple)) — selector signature change, internal to agent |
S |
| U1-2 | P1 | victor/agent/tool_pipeline.py:2237 (seq loop); victor/config/settings.py:1783 | Canonical loop executes tool calls strictly sequentially; parallel_tool_execution=True (default) + max_concurrent_tools are dead — execute_tool_calls_parallel, ParallelToolExecutor.execute_parallel, AsyncToolExecutor have zero live-path callers; docstring at :2457 falsely claims internal parallelization |
Tool-heavy turns are latency-bound (subprocess/HTTP), wall-clock = sum of N tool latencies; settings promise silently unfulfilled | Route batch through execute_tool_calls_parallel gated by read-only/intent classification (action_authorizer already models intents); fix docstring |
M |
| U1-3 | P1 | victor/agent/services/turn_execution_runtime.py:442,444,446,457,466 | Background-task completion messages use \\n (literal backslash-n) in f-strings — model receives literal \n sequences, not newlines |
Corrupts model-visible formatting exactly when long-running tasks finish; 5 copy-propagated sites | Replace \\n→\n; test asserting no literal \n in injected content |
S |
| U1-4 | P2 | victor/agent/orchestrator.py:766-782; services/chat_service.py:128-181,238-243 | ChatService is nominal: bind_runtime_components injects 8 orchestrator handlers; "runtimes" are built with the orchestrator itself and call facade privates (orch._parse_and_validate_tool_calls) |
Facade-only law holds textually but not structurally; every chat feature re-touches orchestrator+service+runtime (churn=330) | Invert: ChatService owns turn lifecycle, receives frozen per-turn state; move _prepare/_teardown_chat_service_turn_runtime into ChatService. Guard-test upgrade (U1-11) first |
L |
| U1-5 | P2 | victor/agent/conversation/assembler.py:365 (also :339 per-iteration rescore) | older_messages.index(msg) inside selection loop — O(n²) with pydantic == per LLM call; full re-score/re-sort every iteration, no cross-iteration reuse |
CPU grows quadratically in long sessions exactly when context is largest | Enumerate with indices before sorting; cache older-message scores per turn, invalidate on compaction | S |
| U1-6 | P2 | victor/agent/conversation/store.py:212-213,1142,1386; services/chat_service.py:1410-1425 | Split-lock hazard: async writers take asyncio.Lock, sync writers take threading.Lock — no cross-domain exclusion on the same sqlite file; live persistence is unordered run_in_executor(None,…) fire-and-forget; the properly-locked add_message_async has zero production callers |
Concurrent turns can interleave session token read-modify-writes and persist messages out of order | Adopt add_message_async on the live path or delete it; single lock domain |
M |
| U1-7 | P2 | victor/agent/orchestrator.py (whole; :472-687) | God-file persists: 4,225 LOC, ~230 defs, 9 _initialize_* methods (F-002), 115 getattr(self,…) probes, 322 commits/12m, fan_in=39 |
Highest churn×size in the repo; every feature PR risks facade re-growth | Ratchet guard on def-count; move property blocks (~800 lines) to mixins | M |
| U1-8 | P2 | victor/agent/orchestrator.py:791; :3105,3114 | DI init writes a service private (self._provider_service._current_provider = self.provider); add_message reaches into conversation._messages with its own O(n) trim at ceiling 100 vs MessageHistory's 100,000 (message_history.py:130) |
Two divergent trim invariants; private reach-ins defeat the protocol layer | Add ProviderService.bind; expose MessageHistory.trim(max) |
S |
| U1-9 | P2 | victor/agent/message_history.py:36-111,196-208; orchestrator.py:2096-2117 | Triple shipped defensive type-scanning: _TrackedList ("temporary instrumentation") + full-history isinstance scan after every add + re-normalization on every assembled request |
O(history) waste per message and per LLM call paying forever for a one-time historical corruption | Assert Message type at the 2 write boundaries; delete all three scans | S |
| U1-10 | P2 | chat_stream_helpers.py (1,994, fan_in=1) + chat_stream_executor.py (1,657) + chat_stream_runtime.py (547) + streaming/handler.py (1,652) | Streaming ACT spread over 4 files as a mixin-with-orchestrator cluster; combined churn 170+/12m | Primary UX surface is the least coherent module family | Fold helpers into executor as explicit functions taking a context struct | M |
| U1-11 | P3 | tests/unit/agent/services/test_service_layer_validation.py:116-206 | Facade guard is string-grep: counts _chat_service occurrences, asserts protocol names in source |
Cannot detect handler-injection (U1-4), service-private writes (U1-8), or effectful additions | AST/import-based guard: forbid orchestrator imports of DB/provider effect modules; ratchet handler-injection kwargs | M |
| U1-12 | P3 | victor/agent/* (132 hits getattr(self.settings,…)) |
Settings read defensively with inline defaults everywhere | Typo'd flag names silently take the default — flag-drift bugs invisible (a real flag was deleted in #806) | Typed accessor helpers on services; lint rule for new raw getattrs | M |
| U1-13 | P3 | victor/agent/conversation/assembler.py:397 | result.insert(len(result), msg) — obfuscated append |
Readability | result.append(msg) |
S |
Co-design opportunities¶
- Kill per-iteration serialization at the tool-selection seam (U1-1 + U1-5): selector wants
(query, last-2, depth, stage); give it exactly that and cache scores per turn. - Parallelize the ACT tool batch behind intent classification — reuse
action_authorizer(fan_in=23) as the parallel-safety gate for U1-2. - Single per-turn state handoff: one frozen
TurnStatereplacing the setup/teardown handler pair and per-call dependency gathering. - Assembly memoization keyed on
(history_len, budget, query), invalidated on compaction/edit — amortized O(new messages). - Guard-test upgrade (U1-11) as the ratchet for U1-4/U1-7 before the L refactor.
- Streaming ACT consolidation (U1-10).
Keep intact¶
- KV/prompt-cache stability machinery: frozen system prompt,
_sort_tools_for_kv_stability, system-nudge interception into the user prefix (orchestrator.py:3103-3109, 3447-3465) — load-bearing for byte-identical prefixes. enhancedcompletion default + judge pinning gate (judge_calibration_gate.py:188-213); effect-gated completion default OFF — concluded programs; no graduation proposals.- LRUToolCache with edit-driven invalidation (tool_pipeline.py:305-421) — bounded, TTL'd, file-state-aware.
_ProgressiveToolsProxylazy registration (orchestrator.py:348-407) — amortizes registry cost out of the turn path.- Deprecation warnings on orchestrator.chat/stream_chat (orchestrator.py:3171, 3688) — deliberate v2.0 removal path.
Quick metrics¶
- services/: 57 files, 36,537 LOC; 24 are
*_runtime.pytwins (F-005 said 23 — grew); 28/57 reference "orchestrator"; orchestrator attr-derefs inside services ≈ 218. - Orchestrator: 4,225 LOC, ~230 defs, 9
_initialize_*, 115getattr(self,…), 18hasattr; zero directprovider.chat/DB calls. - Dead-on-live-path:
execute_tool_calls_parallel,AsyncToolExecutor,add_message_async,parallel_tool_execution/max_concurrent_toolssettings. - Bounded collections verified: usage_analytics (1,000/tool, 500/provider), streaming session history (100), LRUToolCache (50+TTL).
- Brief correction:
victor.agent.streaming.pipeline(churn=60, loc=11) is a tombstone raising ImportError — historical-churn artifact, not a live hotspot.