Five-Layer Architecture for AI Coding Tools: Entry, Loop, Tools, Permissions, and Prompts
This article describes a maintainable five-layer architecture for AI coding tools: a thin entry layer, a turn-based loop layer, a contract-driven tool layer, an independent permission layer, and a composable prompt layer. It explains each layer’s responsibilities and boundaries, shows pseudocode for the main control flow, and outlines how the layers form a traceable request chain with clear testing and observability points.
Five-Layer Architecture for AI Coding Tools: Entry, Loop, Tools, Permissions, and Prompts
Whether the code of an AI coding tool is “easy to read” often depends less on the number of comments and more on whether the architecture assigns different concerns to different layers. A common maintainable design splits the system from the outside in into five layers: the entry layer handles startup and mode selection, the loop layer drives model calls and tool execution, the tool layer turns model intent into concrete operations, the permission layer decides whether an operation is allowed, and the prompt layer dynamically assembles instructions and context for the model. Each layer has clear inputs, outputs, and boundaries, so the call chain can be traced reliably.
| Layer | Core responsibility | Design keywords |
|---|---|---|
| Entry layer | Parse commands, select run mode, start optimization | Thin entry, fast path, lazy loading |
| Loop layer | Drive model calls and tool execution | Turn control, state progression, termination conditions |
| Tool layer | Turn model intent into concrete operations | Registry, argument validation, side-effect isolation |
| Permission layer | Decide whether each operation is allowed | Least privilege, default deny, auditing |
| Prompt layer | Dynamically assemble instructions and context | Layered templates, context trimming, versioning |
From the outside in, these five layers connect the entire chain: “user input → model decision → tool execution → result return.” The sections below examine each layer.
1. Entry Layer: Skip When Possible, Keep the Entry Thin
The entry layer is the first interface the user touches. It should not carry business logic. Instead, it should do only three things: read arguments, determine the run mode, and load the corresponding module. The thinner the entry layer, the shorter the startup path and the clearer the dependencies.
A common optimization is the “fast path”: for requests such as version queries or help information that do not need a full runtime environment, output the result directly and exit without loading heavy modules such as the interactive UI, model client, or tool registry. This reduces startup time and prevents unrelated dependency initialization failures from affecting simple commands.
The pseudocode can be abstracted as:
async function main() {const args = readCommandLineArgs();if (isVersionOnly(args)) {printVersion();return;}if (isSubcommand(args, "background")) {const mod = await importModule("background");return mod.main(args.slice(1));}const interactive = await importModule("interactive");return interactive.main();}
The readability of the entry layer comes from “seeing the branches at a glance”: version queries take the fast path, subcommands go to independent modules, and the default path enters the full interactive flow. Modes are isolated through lazy loading, so the dependencies of one subcommand do not pollute other paths. The entry layer should not contain complex nested conditions, nor should it directly call models or execute tools; it only hands the request to the correct next layer.
2. Loop Layer: Control Models and Tools with Turn-Based Execution
The loop layer is the central hub of an AI coding tool. It usually drives the process of “model call—tool call—result append” with a loop: the model responds or requests a tool call based on the current context, the loop layer parses the request, hands it to the tool layer for execution, appends the execution result back to the context, and continues to the next round until the model no longer requests tools or a termination condition is met.
This loop layer should remain a “scheduler,” not an “executor.” It cares about state progression: the current turn, executed tools, remaining budget, whether a timeout has occurred, and whether human confirmation is required. How a specific tool is executed, how permissions are checked, and how prompts are assembled should all be delegated to the corresponding layers.
Pseudocode:
async function runLoop(input, context) {let state = createState(input, context);while (!state.finished) {const prompt = promptLayer.build(state);const response = await model.call(prompt);if (!response.toolCalls?.length) {state.finish(response.text);break;}for (const call of response.toolCalls) {const decision = permissionLayer.check(call, state);if (!decision.allowed) {state.appendToolResult(call.id, decision.reason);continue;}const result = await toolLayer.execute(call, state);state.appendToolResult(call.id, result);}state.tick();}return state.output();}
The readability of the loop layer comes from “a single control flow”: there is only one main loop, and all branches revolve around turn state. Tool call results are appended in a uniform way, and permission denials are also returned as tool results so the model can see complete feedback. The loop layer should also define explicit termination conditions, such as reaching the maximum number of turns, timing out, user cancellation, or the model providing a final answer, to avoid infinite loops.
3. Tool Layer: Turn Intent into Operations Without Swallowing Permissions
The tool layer translates the model’s intent into concrete operations. It usually includes a tool registry, tool descriptions, parameter schemas, execution functions, and result formatting. Each tool should have a stable name, a clear input schema, explicit side-effect documentation, and a unified error return format.
The key design principle for the tool layer is “contract-based.” The model sees only the tool name, purpose description, and parameter schema; the tool layer validates arguments, executes operations, catches exceptions, and returns structured results. The tool layer should not directly decide “whether the user has permission,” because permission rules would then be scattered across every tool and become difficult to audit and reuse. Permission decisions should be made centrally by the permission layer, and the tool layer should execute only approved operations.
Pseudocode:
type Tool = {name: string;schema: unknown;execute: (args: unknown, ctx: Context) => Promise<ToolResult>;};const registry = new Map<string, Tool>();function register(tool: Tool) {registry.set(tool.name, tool);}async function execute(call: ToolCall, ctx: Context) {const tool = registry.get(call.name);if (!tool) return errorResult("unknown tool");const parsed = validate(tool.schema, call.args);if (!parsed.ok) return errorResult(parsed.message);try {return await tool.execute(parsed.value, ctx);} catch (err) {return errorResult(normalizeError(err));}}
The readability of the tool layer comes from a “unified entry point”: all tools are exposed through the registry, and all calls go through argument validation and unified error handling. Tool implementations can be tested independently, tool descriptions can independently generate prompts, and tool execution can independently observe latency and failure rates. Side effects are confined within the tool layer, so the loop layer and prompt layer do not need to understand the details of file operations, network requests, or command execution.
4. Permission Layer: Cross-Cutting Concerns Need Independent Decisions
The permission layer is the security boundary of an AI coding tool. It needs to decide whether each operation is allowed, such as reading a path, writing a file, executing a command, or accessing a network resource. The permission layer should not be attached to a specific tool. Instead, it should exist independently as a cross-cutting concern: tools propose operations, and the permission layer returns a decision to allow, deny, or require confirmation.
The design principles of the permission layer include least privilege, default deny, explainability, and auditability. Least privilege means granting only the capabilities required for the current task; default deny means operations that are not explicitly allowed should not run automatically; explainability means giving a reason when denying; auditability means recording every decision for traceability.
Pseudocode:
function check(call: ToolCall, state: State): Decision {const operation = normalizeOperation(call);const rule = policy.match(operation, state.mode);if (!rule) {return { allowed: false, reason: "no matching policy" };}if (rule.requiresConfirmation && !state.confirmed(operation)) {return { allowed: false, reason: "confirmation required" };}audit.log({ operation, rule: rule.id, decision: "allow" });return { allowed: true };}
Once the permission layer is independent, the tool layer does not need to repeat security checks, and the loop layer does not need to understand specific policies. Permission rules can be combined by dimensions such as run mode, working directory, tool type, and argument range, and can be extended later. Denial results are returned to the model as tool results, so the model can adjust its plan instead of letting the entire loop fail with an exception.
5. Prompt Layer: Make Instructions and Context Assembled Data
The prompt layer dynamically assembles the instructions and context given to the model. It should not be string concatenation scattered throughout the codebase. Instead, it should use layered templates: system rules, safety constraints, tool instructions, environment information, task context, and historical messages are maintained separately and assembled as needed.
The readability of the prompt layer comes from “dataization” and “trimming.” Tool instructions can be generated from the tool registry, environment information can be injected according to run mode, historical messages can be trimmed according to the token budget, and safety constraints can be versioned. In this way, the prompt layer maintains a stable structure while changing dynamically with the task.
Pseudocode:
function build(state: State): Prompt {return compose([systemRules(),safetyRules(state.mode),toolInstructions(registry.list()),environmentContext(state.env),taskContext(state.task),trimHistory(state.history, state.budget)]);}
The prompt layer should also avoid taking on business decisions. It is only responsible for “giving the model the information it should receive”; it is not responsible for deciding whether a tool can be executed, nor for executing operations. Prompt templates should be testable, comparable, and rollback-friendly, so that a modification does not unintentionally change system behavior.
6. How the Five Layers Form a Traceable Chain
When the boundaries of the five layers are clear, the chain for a request usually looks like this: the entry layer parses user input and selects a mode; the loop layer creates turn state; the prompt layer assembles the current context; the model returns text or a tool call; the loop layer parses the tool call; the permission layer decides whether it is allowed; the tool layer executes the operation and returns a structured result; the loop layer appends the result and moves to the next round; finally, the loop layer outputs the answer.
The value of this chain is that “each layer answers only one question”: the entry layer answers “how to start,” the loop layer answers “what to do next,” the tool layer answers “how to do it,” the permission layer answers “whether it can be done,” and the prompt layer answers “what to tell the model.” Once the questions are separated, the code reading path becomes stable: enter through the entry layer, follow the loop, check permissions when encountering tool calls, and inspect prompts when encountering context. Readability is not about writing shorter code; it is about giving every concern a single owner.
In engineering practice, observability and testing strategies can also be added: the entry layer tests mode dispatch and fast paths; the loop layer tests termination conditions and tool result appending; the tool layer tests argument validation and error normalization; the permission layer tests policy matching and denial reasons; the prompt layer tests template assembly and context trimming. When each of the five layers is independently testable, the overall chain remains maintainable.
