AI · Python · LangGraph · Orchestration
Cloud coding agent: roles, context and execution control
Designing a cloud agent that receives a task, investigates a repository, plans changes and takes them to a verifiable result. This article covers the roles, tools, memory and limits that govern the process.
What counts as a completed task
Generating a patch is one part of the work. Before it, the agent must understand existing code and choose where to make a change; afterwards, it must verify the implementation, handle findings and decide whether a merge request can be created. Between these stages, a model can lose a requirement, repeat a failed call or declare completion prematurely.
The implementation uses Python, LangGraph and a shared TaskState. RabbitMQ connects the worker to an external task source; Git stores changes, while SQLite stores graph checkpoints and search indexes. PHP is the primary target stack for modified projects. Roles, model providers, the prompts catalog, project standards and check commands come from configuration: the same graph can target another repository without rewriting orchestration.
One long agent with a shared context poorly separates investigation from write authority, mixes tool budgets and makes targeted feedback loops harder. Separate nodes with different tools, memory units and limits define explicit phase boundaries; between them, code enforces invariants and mandatory gates that a model cannot override with prose.
The selected operation defines the deliverable: changes with an MR, an analysis report or a review report. Each has a route, output fields and completion conditions. The shorter analysis and review modes are still evolving; their routes and tests are already implemented. No mode treats a model’s judgment as proof of code correctness.
Roles connected by contracts and feedback paths
The decomposer turns a description into acceptance criteria. The analyzer investigates the code and returns impact, files and supporting context. The planner defines steps, and the architect checks the plan before changes begin. The coder implements it; the reviewer checks the result against acceptance criteria, architecture and security requirements.
These are logical roles in a graph, not a promise of a parallel team of independent agents. They can use different models and settings. The separation determines each stage’s input, available tools and decision authority. Investigation roles, for example, do not receive patch application tools, while the coder has no arbitrary terminal tool.
Roles exchange structured results: criteria, paths, checklist items, findings and check statuses. Schemas and Pydantic validators check their shape and selected invariants. The architect judges semantic plan coverage and returns feedback to the planner on rejection. After implementation, a unified reviewer checks both requirements and architecture; there is no separate post-code architect stage.
- Decomposer → AnalyzerAcceptance criteria, repository investigation, change scope and context
- Planner ↔ ArchitectPlan, approach review and bounded revisions before code generation
- Coder sessionChecklist, patch application, item checks and commits
- semantic_sync → gates → ReviewerIndex sync when semantic search is enabled; blocked or looping sessions → planner_amend; completed sessions → configured gates and reviewer
- MR or feedbackAn approved result proceeds to MR creation; findings trigger fixes or plan amendments
Three modes on shared infrastructure
Task analysis and review of existing code are useful on their own. The operation therefore selects a route before generation starts. Investigation need not produce a patch, and review need not automatically fix its findings.
Short routes have their own incompleteness contract. Missing information produces a blocked report with questions. The full route can return need_info, retain state and continue after clarification. Review-only requires an existing target_branch: the code under review must already exist in Git.
| Mode | Work | Deliverable |
|---|---|---|
| full | Investigation, planning, implementation, checks and review | Changes and an MR on successful completion |
| analyze_only · evolving | Decomposer → init_env → analyzer → report | Criteria, impact, files and context; no coder or MR |
| review_only · evolving | Decomposer (or inbound criteria) → init_env → tests → smoke → syntax gate → reviewer | Findings and a verdict on an existing branch; no coder, semantic_sync or MR |
“No code generation” describes the route, not filesystem isolation: init_env prepares the environment, and checks may create auxiliary files. Role permissions and process isolation are separate layers.
Searching by meaning, symbols and actual code
One search method does not answer every repository question well. A known method name calls for symbol and relationship lookup. A known configuration string needs exact text search. When only the purpose is known, such as where retries are handled, semantic_search is useful.
Semantic search combines SQLite FTS5 full-text search with sqlite-vec vector search. Weighted Reciprocal Rank Fusion merges the rankings: a candidate’s position in each result list contributes to its combined score. The index retains the snippet, path, line range and symbol, so a hit can be checked against source. This selects context; it is not exhaustive behavioral analysis.
CodeGraph adds definitions, callers, callees, impact and batched exploration. search_code finds exact text, get_file_outline exposes the structure of a large file, and read_file_snippet retrieves a specific body. With a configured connection, list_database_tables, get_table_schema and run_readonly_sql let investigation use the actual database schema instead of guessing from the ORM.
A base semantic index is prepared for the worker, then copied for each task. Changes are synchronized into the task index, separating its working state from the base. The index remains derived data: freshness needs to be controlled, and critical conclusions tied to current source.
Live injection: context follows changes
After several patches, a previously read file may no longer match disk. Reusing old tool output can make the model construct a replacement for a state that no longer exists. Live injection places current target files into the system context.
The mechanism is not limited to the coder. Tool-loop roles (analyzer, architect, reviewer and the coder subgraph) receive the same suffix each turn; planner and planner_refine get one injection per node invoke — no tool loop, but the same tier rules. Disk state changes most often in the coder session, so injection refreshes before every model call there; reviewer and architect usually keep a fixed path set for the session, and the analyzer’s first inject is often a snapshot from task seed and accumulated context. Live injection does not mean continuous observation of the entire repository.
There are two tiers: a small file is injected in full, while a large one receives an outline. Re-reading a fully injected file is blocked with an explanation of where its contents already appear; outline-only files still allow method-body reads. File-count and size limits, together with injection metrics, expose the cost of this mechanism.
In the shared invoke_llm_with_tools loop, the previous reminder about files already in context is removed and one current reminder is appended before the next call. The current coder run_session_loop has no separate reminder of this kind: it updates the system message with current files before each model call.
Investigation memory and execution memory
Tool history grows faster than useful conclusions. Analyzer, architect and reviewer roles have write_scratchpad to retain facts, paths and open questions outside the sliding message window. Pruning removes older call–response pairs together; a tombstone records their removal and points to the scratchpad.
A tombstone marks the removal of older tool calls and responses without summarizing their contents. Findings are retained separately in the scratchpad; an empty scratchpad triggers a warning before pruning. For final synthesis, code assembles a handoff summary containing acceptance criteria, saved notes, read ranges and declared files. The analyzer uses this summary to produce a structured result without receiving the entire tool history.
The coder session uses a different memory unit: a closed checklist item. Once it closes, its history is replaced by a progress summary containing the checklist, completed items and their notes, files read and the next item. Code assembles this summary without a separate LLM summarization call. Changed files are injected again from disk; any needed details can be re-read.
The plan as an executable checklist
The current coder executes the plan as a checklist within one session. complete_item checks an item and, when step commits are enabled, creates a commit. A failure returns to the same session; edits remain on disk for an in-place fix. Normally, amending the remaining plan does not require regenerating completed items.
Adaptation has explicit actions. update_plan adds, drops or reorders an item with a reason; request_file requests a broader write scope. report_plan_blocker reports a false plan premise with evidence in source. These decisions are retained and passed to the reviewer.
Finish is allowed only when no items remain open and the worktree is clean. Already_done is checked too: a statement that everything is already implemented is insufficient. The system distinguishes code that predates the task from work committed earlier within the same task.
- Open item + current filesThe checklist defines current work; live injection supplies the visible code state
- Tools → complete_itemPatches land on disk; the item check returns a specific failure reason
- Failure → in-session fixEdits remain on disk; a loop breaker tracks repetition without changes
- Success → commit → progress noteWhen step commits are enabled, work is committed and the closed item transcript compacted
- Next item or finishFinishing requires a closed checklist and a clean worktree
What code checks and what remains a model judgment
Some failures are detected without an LLM. Before architectural review, code checks empty steps, a missing plan despite acceptance criteria, and references to nonexistent files within the modification scope. Schema validation checks result structure and separation of writable and context-only paths. A model call is unnecessary for a condition code can check unambiguously.
Complete_item checks actual changes, lint on modified files and disallowed TODO-only patches. A report item needs a report and no outstanding source edits. Plan-target checks can block or warn; a symbol’s presence does not prove correct behavior. Findings continue downstream for semantic review.
A separate mechanism addresses method overrides. The code index locates an ancestor method and extracts visible exits: value returns, early returns without a value, exceptions and parent delegation. The contract is surfaced during planning and change verification. If the parent stops without returning false, an override must not accidentally continue processing.
This targets a particular risk; it is not complete execution-path analysis. Dynamic relationships and unrecognized constructs remain outside its scope. If the index is unavailable, the context explicitly says the check was not performed. For a discovered contract, the coder must explain how exits are handled; tests and review check the final behavior.
After the coder, the graph passes through semantic_sync. When semantic search is enabled, this node updates the task index; a synchronization failure is recorded as degraded search. Blocked or looping coder sessions pass open items to planner_amend while retaining completed items. Successfully completed sessions proceed to configured gates: test_runner, smoke when configured, the syntax/lint gate, then the reviewer. Reviewer approval cannot replace a failed mandatory gate. Tests may be disabled by configuration or not run; those states are distinguished in the result rather than described as a passing suite.
Orchestration prompts as part of the protocol
A system prompt defines a role and persistent constraints. An orchestration fragment communicates a particular execution event: investigation budget exhaustion, a rejected patch, a required plan amendment or a structured-output handoff. These messages are stored separately from code and inserted when the event occurs.
For example, ADJUST_PLAN tells the planner that completed steps are already applied and supplies completed_plan_steps with failed_step_context. The planner must describe only remaining work. FULL_RESET needs different context: the state after a reset is different, so reusing the same message would introduce false premises.
When the reviewer exhausts its inspection budget, it receives the call limit and an instruction to list unverified criteria. An unfinished inspection must not become approved. Code also handles limit exhaustion; the text explains the reason and the expected response format to the model.
Patch validation errors, tool repetitions and an empty scratchpad before pruning follow the same pattern. They provide a corrective action rather than merely asking for another attempt. Shared rules need not be copied into every fragment; tests check alignment with tools and validators.
ADJUST_PLAN
completed_plan_steps → preserve applied work
failed_step_context → revise remaining work
INSPECTION BUDGET EXHAUSTED
unverified criteria → incomplete review
approved → false
Roles, pipeline rules, project standards and investigated data are separated explicitly. Repository content and tool output must not change a role’s authority or cancel checks. XML boundaries help identify data provenance; tool selection, path validation and execution code also enforce limits. Markup alone does not guarantee protection against prompt injection.
Dynamic limits and circuit breakers
A short investigation and a task with several criteria need different budgets. Analyzer, architect and reviewer tool limits use base + per_criterion × criterion count, capped at a maximum. The current coder session’s budget depends on open checklist items. This is a configurable resource-allocation heuristic; criterion count is not an exact measure of complexity.
A limit bounds cost but does not recognize a dead end. ToolLoopGuard tracks repeated tool-and-argument signatures, including alternating sequences. Separate rules detect identical tool errors, redundant reads and sequential micro-reads. Rules account for role and disk changes so a retry after a patch is not confused with repetition against unchanged state.
The coder has another signal: the same complete_item failure against an unchanged diff. The session ends as looping and passes the problem to planner_amend. Review findings track lack of progress across fix rounds; closing other findings is not treated as the same dead end.
The response depends on the cause. Investigation can synthesize accumulated evidence without further tools. Incomplete inspection receives no automatic approval. A false plan item returns for amendment; an infrastructure failure is not disguised as a need to rewrite an algorithm. Restarts, amendments and review rounds are bounded, alongside a total token budget and graph recursion limit.
research_budget = min(max_calls, base + per_criterion * criteria_count)
coder_budget = min(max_calls, base + per_item * open_items)
Early exits and retaining useful work
Successful investigation ends through finish_exploration: the declared file scope is validated, then handed to structured synthesis. There is no requirement to consume every tool call. On the full route, missing information can yield need_info; a false plan premise can yield a blocker grounded in source.
Planner_amend retains completed items and changes open ones. Strategic ADJUST_PLAN preserves implemented code, while FULL_RESET resets working state and plans again. These decisions have different costs; a routine check failure should not automatically destroy all progress.
Clarification pauses and result delivery add another layer. A graph checkpoint allows the full route to continue after a user response. RabbitMQ input is acknowledged after successful result publication; a transient publication failure can cause redelivery. This is at-least-once delivery, so the consuming system needs task_id as an idempotency key, not as a promise of exactly-once execution.
Scaling uses multiple worker processes with separate Git environments. Each process executes one task at a time; parallel graph.invoke calls within it are not a supported model. That boundary matters for state, ContextVar and the checkpoint connection.
How the system itself is checked
Testing generated application code and testing the worker are different jobs. Worker tests cover graph routing, output schemas, loop breakers, pruning, injected-file freshness, item completion and tool restrictions. CI includes pytest, Ruff, Mypy and Gitleaks; ordinary tests mock LLMs and embeddings.
These tests reproducibly check that review_only never enters the coder, repeated failure stops a session, and an open checklist prevents finish. They do not measure how well a real model will find a defect or implement an unfamiliar task. That requires a separate evaluation with verifiable task outcomes.
Evaluation reports include fingerprints of code, prompts and configuration. Comparing pass rates is meaningful only with a known task set, model, checks and settings. Sample reports in the repository are synthetic: they illustrate the format, not measured effectiveness.
A separate test caps the fixed prompt text for each role. A new rule competes with task code for context and must fit a budget. Prompt growth, like retry growth, should not be an invisible way to compensate for every failure.
End-to-end run tracing is available for debugging: node transitions, model and tool calls, and state snapshots at boundaries. The log records the call sequence, supplied context, tool results and reasons for transitions between stages. Langfuse integration is planned; it will extend the existing picture rather than replace the meaning of gates and structured reports.
Boundaries and trade-offs
This is not a universal single-prompt agent for every task, not a substitute for human code review, and not proof of correctness by model judgment. The coder has no arbitrary shell; graph roles run under contracts and gates, not as a parallel team of independent agents. Language and stack are a target-repo profile: project config sets dependency install, the test command, smoke, inline-lint discovery and the test-failure classifier preset; prompts and standards carry language rules and conventions. PHP is the primary profile today, but the graph stays the same — you change config plus language standards, not the orchestrator.
A multi-role graph requires extra calls and time. Early exploration completion and configurable budgets limit expenditure within the selected route. The analyze_only and review_only modes are selected by the required deliverable: task analysis or review of existing changes without code generation. Role separation does not guarantee independent errors, especially when roles share a model and source context.
For work on the target project, responsibilities are divided: deterministic checks validate plan structure, file changes and command results; the architect evaluates the proposed approach before implementation; the reviewer checks whether changes meet requirements, architecture and security constraints, and whether tests address the relevant behavior. Heuristic findings and warnings remain visible in the report separately from mandatory check results.
What I value in this design is the link between an action and its basis: which code was investigated, which item closed, which check passed, why an amendment was needed and why execution stopped. That link makes failures diagnosable and lets a specific mechanism change instead of adding another broad prohibition to a prompt.