Skip to content

refactor(llmist): unify llmist backend onto shared executeWithBackend adapter path#540

Merged
zbigniewsobiecki merged 2 commits intodevfrom
refactor/unify-llmist-backend-onto-adapter-path
Feb 24, 2026
Merged

refactor(llmist): unify llmist backend onto shared executeWithBackend adapter path#540
zbigniewsobiecki merged 2 commits intodevfrom
refactor/unify-llmist-backend-onto-adapter-path

Conversation

@aaight
Copy link
Copy Markdown
Collaborator

@aaight aaight commented Feb 24, 2026

Summary

Unifies the llmist backend onto the shared executeWithBackend adapter path that the claude-code backend already uses, eliminating the special-case bypass in registry.ts that caused the two execution paths to diverge.

Card: https://trello.com/c/FvhG64kh/112-find-top-candidate-for-refactoring-and-plan-clean-refactoring-of-it-look-for-god-classes-modules-functions-files-code-duplicatio

What changed

  • src/backends/llmist/index.ts — Rewrote LlmistBackend.execute() to receive a fully pre-resolved AgentBackendInput and run the llmist SDK directly (no delegation to legacy per-agent executors). Uses getAgentProfile().getLlmistGadgets() for gadget instantiation, converts ContextInjection[] to synthetic gadget calls via injectSyntheticCall, and builds the agent with createConfiguredBuilder which preserves all llmist-specific features (loop detection, compaction, iteration hints, rate limiting, retry).

  • src/agents/registry.ts — Removed the 27-line special-case that bypassed executeWithBackend for the llmist backend and passed a minimal stub input. All backends now go through executeWithBackend, which provides: repo setup, CWD change/restore, env var loading, run record creation, log finalization, progress monitor, and watchdog.

  • tests/unit/backends/llmist.test.ts — Rewrote tests to match the new implementation. Tests now verify the unified path: gadget loading from profiles, synthetic call injection from contextInjections, loop termination handling, PR URL extraction, and correct parameter passing to createConfiguredBuilder.

  • tests/unit/agents/registry.test.ts — Updated test for the llmist path to verify it now calls executeWithBackend (like all other backends), removing the assertion that it bypassed the adapter.

Key design decisions

  • No nested lifecycles: The llmist backend's execute() method does NOT call executeAgentLifecycle — that would nest two pipeline scaffolds. Instead, it runs the llmist SDK directly, leveraging the outer executeAgentPipeline (from adapter.ts) for lifecycle management.
  • LLM call logger: Created per-execution using createLLMCallLogger into os.tmpdir() since the outer pipeline's FileLogger is not exposed to backends. Real-time per-call metrics still work via the runId passed to createObserverHooks inside createConfiguredBuilder.
  • Single source of truth for gadgets: getAgentProfile().getLlmistGadgets() — previously the llmist executors in base.ts, review.ts, respond-to-ci.ts, etc. each had their own gadget lists; now one function covers all.

Preserved llmist features

  • Loop detection and hard-stop (via runAgentLoop + createObserverHooks)
  • Context compaction (via createConfiguredBuilder + getCompactionConfig)
  • Iteration hints (via createConfiguredBuilder + getIterationTrailingMessage)
  • Rate limiting and retry (via createConfiguredBuilder)
  • Synthetic gadget call injection (ContextInjection[] → injectSyntheticCall)

Test plan

  • All 190 test files pass (npm test)
  • TypeScript compiles without errors (npm run typecheck)
  • Lint passes (npm run lint)
  • Registry test verifies llmist now uses executeWithBackend
  • New llmist tests verify gadget profile integration, synthetic injection, and loop handling

🤖 Generated with Claude Code

@nhopeatall
Copy link
Copy Markdown
Collaborator

nhopeatall commented Feb 24, 2026

🤖 Just a sec, looking into that PR for you


Progress: [████░░░░░░] 44% (iteration 31/70)

🔍 Code Review Update (1 min)
I've been reviewing the new implementation, focusing on key supporting modules and potential dead code. I've identified a critical issue where process.env.LLMIST_LOG_FILE is being set in two different locations, which could lead to global side-effects and affect concurrent executions. I'm currently investigating how budgetUsd and runId are passed through the adapter to ensure proper data flow.

Last updated: iteration 31 · review

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Well-structured refactor that correctly unifies the llmist backend onto the shared executeWithBackend adapter path. The registry simplification and elimination of the special-case bypass are clean. However, there are two behavioral regressions where the new implementation doesn't wire up integrations that the old code path provided.

Code Issues

Should Fix

  • src/backends/llmist/index.ts:75-76process.env.LLMIST_LOG_FILE is set to a temp path in os.tmpdir(), but the outer pipeline's fileLogger.llmistLogPath (created by createFileLogger in executionPipeline.ts) points to the workspace directory. During run finalization, storeRunLogs reads from fileLogger.llmistLogPath (in runTracking.ts:62-63), and getZippedBuffer() also looks there. Since the llmist SDK writes to the temp path, the structured llmist log is orphaned — it won't appear in run records or log bundles. The old lifecycle.ts:259 code correctly set this to fileLogger.llmistLogPath. Fix: either expose llmistLogPath on AgentBackendInput, or have the adapter's execute callback set process.env.LLMIST_LOG_FILE = ctx.fileLogger.llmistLogPath before calling backend.execute().

  • src/backends/llmist/index.ts:82-107createConfiguredBuilder is called without a progressMonitor. The adapter creates a ProgressMonitor (at adapter.ts:210-231) and passes it as input.progressReporter, but the llmist backend never reads progressReporter and doesn't pass it to createConfiguredBuilder. This means the observer hooks in createObserverHooks (at hooks.ts:112-114) won't call progressMonitor.onIteration(), so the progress accumulator will have zero data — progress updates to Trello/GitHub will be empty. The old code path in lifecycle.ts:267 passed progressMonitor through to the builder. Fix: pass input.progressReporter as the progressMonitor option to createConfiguredBuilder (the ProgressMonitor class already implements the right interface).

Comment thread src/backends/llmist/index.ts Outdated

// Point llmist at a temp log file for its internal structured log
const llmistLogPath = path.join(os.tmpdir(), `llmist-${agentType}-${Date.now()}.log`);
process.env.LLMIST_LOG_FILE = llmistLogPath;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SHOULD_FIX] This sets LLMIST_LOG_FILE to a temp path, but the outer pipeline's fileLogger.llmistLogPath points to the workspace dir. During finalization (runTracking.ts:62-63), the log is read from fileLogger.llmistLogPath and included in getZippedBuffer(). Since the SDK writes to the temp path, the structured llmist log is lost from run records and log bundles.

The old lifecycle.ts:259 code set this to fileLogger.llmistLogPath. Since the backend doesn't have access to fileLogger, consider either:

  1. Adding a llmistLogPath field to AgentBackendInput, or
  2. Having the adapter's execute callback set the env var before calling backend.execute()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! Added llmistLogPath field to AgentBackendInput and passed fileLogger.llmistLogPath from the adapter. The llmist backend now sets process.env.LLMIST_LOG_FILE to this workspace path instead of a temp path, ensuring the structured llmist log is included in run records and log bundles.

const gadgets = profile.getLlmistGadgets(agentType);

// Build the configured agent builder with all llmist-specific features:
// rate limiting, retry, compaction, iteration hints, observer hooks
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SHOULD_FIX] Missing progressMonitor parameter. The adapter creates a ProgressMonitor and passes it as input.progressReporter, but this backend never passes it to createConfiguredBuilder. As a result, createObserverHooks won't call progressMonitor.onIteration() (hooks.ts:112-114), and the progress accumulator will have zero state — progress updates to Trello/GitHub will be empty.

The old code path (lifecycle.ts:267) passed the progress monitor through to the builder. Fix:

progressMonitor: input.progressReporter as ProgressMonitor,

(or pass it through a more type-safe way)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! Now passing input.progressReporter to createConfiguredBuilder as the progressMonitor parameter. This ensures createObserverHooks can call onIteration(), onToolCall(), and onText() to populate the progress accumulator, enabling progress updates to Trello/GitHub.

@aaight
Copy link
Copy Markdown
Collaborator Author

aaight commented Feb 24, 2026

Review Feedback Addressed ✅

Thank you for the thorough review! Both behavioral regressions have been fixed:

1. llmist structured log integration ✅

  • Added llmistLogPath field to AgentBackendInput interface
  • Adapter now passes fileLogger.llmistLogPath (workspace directory) to the backend
  • Backend sets process.env.LLMIST_LOG_FILE to this path instead of a temp path
  • Result: The llmist SDK now writes to the workspace directory, ensuring logs are included in run records and log bundles

2. Progress monitor integration ✅

  • Backend now passes input.progressReporter to createConfiguredBuilder as the progressMonitor parameter
  • This enables createObserverHooks to call onIteration(), onToolCall(), and onText()
  • Result: Progress updates to Trello/GitHub now work correctly

Verification

  • ✅ All 190 test files passing (3226 tests)
  • ✅ Type checks passing
  • ✅ Lint passing
  • ✅ Added tests verifying both integrations

Committed as: 60b8297

Fixes two behavioral regressions in the llmist backend unification:

1. **llmistLogPath integration** — The llmist SDK now writes its
   structured log to the workspace directory (provided via
   `input.llmistLogPath` from the outer pipeline's `fileLogger`).
   Previously it wrote to a temp path, which caused the log to be
   orphaned and excluded from run records and log bundles.

2. **progressMonitor integration** — The backend now passes
   `input.progressReporter` to `createConfiguredBuilder` as the
   `progressMonitor` parameter. This ensures `createObserverHooks`
   can call `onIteration()`/`onToolCall()`/`onText()`, enabling
   progress updates to Trello/GitHub. Previously the monitor was
   created by the adapter but never wired through to the builder.

Changes:
- Added `llmistLogPath` field to `AgentBackendInput` interface
- Adapter passes `fileLogger.llmistLogPath` when building backend input
- Llmist backend sets `process.env.LLMIST_LOG_FILE` to the provided path
- Llmist backend passes `progressReporter` as `progressMonitor` to builder
- Added tests verifying both integrations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@nhopeatall
Copy link
Copy Markdown
Collaborator

nhopeatall commented Feb 24, 2026

🤖 Just a sec, looking into that PR for you


Progress: [█████░░░░░] 46% (iteration 32/70)

🔍 Code Review Update (1 min)

I've completed a review of how the new llmist backend integrates with runAgentLoop, confirming it avoids nested lifecycles. I've also verified the fileLogger.llmistLogPath property exists. I'm currently examining the AgentBackendResult type and how the adapter handles logBuffer and runId to ensure compatibility with the new backend's return values.

Last updated: iteration 32 · review

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Clean, well-structured refactoring that eliminates the special-case bypass in registry.ts and unifies both backends onto the shared executeWithBackend adapter path. The design decisions are sound, CI passes, and tests are thorough.

Architecture & Design

The key design choice — having LlmistBackend.execute() call runAgentLoop directly instead of executeAgentLifecycle — correctly avoids nesting two pipeline scaffolds. The outer executeAgentPipeline (via adapter.ts) provides the lifecycle (repo setup, CWD, env, watchdog, logging, finalization), while the inner execute() only handles llmist-specific concerns (builder configuration, synthetic call injection, agent loop). This is the right split.

The removal of the specialized executor dispatch table (specializedExecutors) is justified — getAgentProfile().getLlmistGadgets() now serves as the single source of truth for per-agent-type tool sets, replacing the per-executor imports from base.ts, review.ts, respond-to-ci.ts, etc.

Minor Observations (non-blocking)

LLM call file logging to temp dir: The new LlmistBackend.execute() creates its own createLLMCallLogger(os.tmpdir(), ...) rather than using the outer fileLogger.llmCallLogger. This means per-call request/response text files won't be included in the ZIP bundle (fileLogger.getZippedBuffer()). In practice this is benign because: (1) real-time DB logging via storeLlmCall is active when runId is present, so the data is accessible from the dashboard; (2) the cascade.log and llmist.log files are included in the ZIP correctly via llmistLogPath. If you want parity with the old lifecycle path's ZIP contents, you could pass fileLogger.llmCallLogger through the AgentBackendInput in a follow-up, but this is not a regression that affects functionality.

process.env.LLMIST_LOG_FILE not cleaned up: The env var is set but never restored after execution. This matches the existing behavior in lifecycle.ts:259-260, so it's not a regression — just a pre-existing pattern worth noting for future cleanup.

@zbigniewsobiecki zbigniewsobiecki merged commit ef6207e into dev Feb 24, 2026
5 checks passed
@zbigniewsobiecki zbigniewsobiecki mentioned this pull request Feb 25, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants