Key takeaways
- The Enter input is not a model call but a turn scheduling event.
- The submit boundary branches to empty input, exit command, local command, queued input, and model query.
- If you don't decide how to handle new input when there is a turn in progress, the runtime state can easily be broken.
- Direct input and queued input must be normalized to the same internal type.
The topic of this article is the Enter key. To exaggerate a bit, the stability of a product-type AI agent begins with how the Enter key is interpreted.
For the user, all it means is “I typed the prompt and pressed Enter.” However, from the runtime perspective, this event should not be a direct model API call. First, we need to decide whether this input is an empty input, a termination command, a local command, a queue item to be appended to an ongoing turn, or a new model query.
This boundary will be called submit boundary here.
1. Why submit boundary is needed
Suppose the model stream is still in progress and the user enters a new prompt. By sending this input directly to a new model call, two loops can simultaneously modify the same message history and screen state.
The results are usually not good.
- The second response is mixed in between the first model response.
- The tool request/result order is broken.
- It becomes unclear which turn the user-approved task belongs to.
- The transcript is stored in an unrecoverable order.
Therefore, the submit boundary is a gate that determines “Can it be executed now?”
2. Atomicity in one turn
In agent, turn is not just a simple user message. The turn includes input normalization, model stream, tool request, permission decision, tool result, final assistant message, and cost history.
# How to read: The items below are summarized examples to quickly check the action flow.raw prompt→ normalized turn→ model stream→ tool request→ permission decision→ tool result→ model continuation→ final answer→ ledger flush
If a new turn modifies the same state before this flow completes, atomicity is broken. So the submit boundary should have states likeidle,dispatching,running, andfinishing.
3. Input branching rules
At the submit boundary, the input roughly branches to one of the following:
| branch | example | Processing method |
|---|---|---|
| ignore | empty input | Do nothing |
| local action | help, clear, settings | Process without model call |
| interrupt | cancel, stop | Deliver stop signal to active turn |
| queued turn | Normal prompt that appears while running | Save to pending queue |
| model query | normal prompt | Start model loop after input normalization |
If you don't make this branch clear, your command and prompt will get mixed up. In particular, sending a slash-style command as a normal prompt or, conversely, misinterpreting a normal prompt as a local command breaks the user experience.
4. Guard and queue design
The most important thing in submit boundary is guard. A guard protects “whether the current runtime can start a new turn.”
Another important thing is queue. If a new input is unconditionally discarded when there is a turn in progress, usability is bad, and if it is executed immediately, the state will be broken. Therefore, a queue is needed.
| component | role |
|---|---|
ExecutionGuard | Determines whether a new turn can be started |
PendingTurnQueue | Keep input in order while in progress |
TurnDraft | Save original input, input mode, attachment, source |
SubmissionRouter | Local action, interrupt, model query branching |
5. Submission flow through concept code
The code below is not the original implementation, but newly written code for explanation purposes.
# How to read: Conceptual code that describes runtime boundaries, not a clone of the actual implementation.class ExecutionGuard: # Initializes runtime dependencies and state storage that the object will reference in later steps. def __init__(self): self.state = "idle" self.owner = None # Checks if the current turn can take execution and locks it into the dispatching state. def reserve(self, turn_id): if self.state != "idle": return False self.state = "dispatching" self.owner = turn_id return True # Converts the turn that is ready for input to the actual running state. def mark_running(self): self.state = "running" # When the turn with execution rights ends, the guard returns to the idle state. def release(self, turn_id): if self.owner == turn_id: self.state = "idle" self.owner = None # Branches raw input to one of ignore, interrupt, local action, or model turn.async def submit_input(raw_entry, runtime): draft = TurnDraft.from_raw(raw_entry) route = classify_submission(draft, runtime) if route.kind == "ignore": return if route.kind == "interrupt": runtime.abort_current_turn(route.reason) return if route.kind == "local_action": await runtime.local_actions.run(route.action) return if runtime.guard.state != "idle": runtime.pending_turns.append(draft) runtime.shell.notice("Executes after the current turn ends.") return if not runtime.guard.reserve(draft.turn_id): runtime.pending_turns.append(draft) return try: runtime.guard.mark_running() prepared = await runtime.input_pipeline.prepare(draft) await runtime.agent_loop.run(prepared) finally: runtime.guard.release(draft.turn_id) await drain_pending_turns(runtime)
The important part here is thatreserve()is called before input normalization. This is because the normalization process itself can be asynchronous. If another input is received in the meantime, both turns can start simultaneously.
6. AI utilization developer perspective
Developers using AI coding agents must check what policy the tool uses when new input is entered.
| situation | good UX |
|---|---|
| Enter new prompt during model response | Shows “Added to queue” |
| Enter stop during long-term operation | Sending cancellation signals to running models and tools |
| Enter local command | Processed without polluting the model context |
| Input with attachments queued | Attached snapshots are kept together |
In particular, attachments and editor selections must be left as snapshots at the time of queue. If the file state at the time of execution is re-read, it may differ from the user's intended context.
7. Agent developer perspective
When creating an agent, problems quickly arise if direct input and queued input are handled through different code paths. The first prompt you enter will process the attachment, the queued prompt will lose the attachment, and so on.
Therefore, it is recommended to convert both toTurnDraftand then make them pass through the sameexecute_turn().
# How to read: Conceptual code that describes runtime boundaries, not a clone of the actual implementation.def to_turn_draft(text, source, attachments=None, mode="chat"): return TurnDraft( id=new_id(), text=text, source=source, attachments=list(attachments or []), mode=mode, captured_at=now(), )
Additionally, you must distinguish between the context scope of the first input and subsequent queued input. Context pollution can occur if you blindly attach rich context, such as editor selections, current diffs, or temporary attachments, to all queued inputs.
8. Practical checklist
# How to read: The items below are summarized examples to quickly check the action flow.Submit Boundary Checklist [ ] Enter input does not immediately lead to a model API call.[ ] Input branches to local action, interrupt, queue, and model query.[ ] When there is an active turn, the new input processing policy is clear.[ ] Direct input and queued input use the same TurnDraft type.[ ] guard reservation occurs before asynchronous normalization.[ ] Attachments and editor context are preserved as snapshots when needed.[ ] There is logic to drain the pending queue after the turn ends.
Failure case: When Enter immediately makes a model call
Without a submit boundary, even the simplest input window can create complex bugs. Let's say the user sends the first prompt and enters a second prompt while the model response is being streamed. If the runtime connects Enter directly tocreateModelCall(), two calls read and write the same history at the same time. The second call starts before the tool result of the first call has come in, the two responses are mixed on the screen, and the cancel button doesn't know which call to stop.
This problem may not be visible in testing. This is because happy paths that are entered only one at a time will pass. Actual users do not wait and add additional comments, cancel incorrectly sent prompts, or enter local commands such as/status. Submit boundary does not consider all of these inputs as the same event, but branches to an action that matches the current runtime state.
Implementation example: submit decision table
| current status | Input example | decision | reason |
|---|---|---|---|
| idle | normal prompt | start_turn | Can start a new model turn |
| idle | /status | run_local_command | No need to call model |
| streaming | normal prompt | enqueue_turn | Protect active turn history |
| streaming | Escape | cancel_active_turn | Control input, not a new turn |
| awaiting_approval | OK button | resume_turn | Release pending status of existing turn |
This table is not a UI document but a runtime contract. The input window, command parser, and model loop must share the same decision. In particular, local commands should not enter the model context, and queued prompts should only have the necessary context snapshots. Otherwise, when the user says, “Continue based on the code I selected earlier,” there will be a problem of reading the editor selection that has already changed.
Checklist application results
When reviewing submission boundaries, we check three things: First, see if we schedule an active turn guard before input normalization. Second, check whether the queued input passes the sameTurnDrafttype as the direct input. Third, check that cancel and local commands do not pollute the model message history. If these three things are correct, the agent will not lose its order even if the number of inputs increases.
finish
The Submit boundary may seem small, but it is the central boundary of the agent runtime. With this layer, input, command, queue, cancel, and model loop will not conflict with each other.
In the next article, we will see how raw input that passes the submit boundary is converted into a model-visible message. This process is input normalization.

댓글
GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.