The UserInputSystem enables agents to request and wait for human input during execution, supporting both timed and infinite waits.
For multi-turn interactive agents, UserInputSystem is often paired with the opt-in TerminalCleanupSystem. This keeps UserInputSystem focused on input futures while allowing interactive runtimes to clear TerminalComponent(reason="reasoning_complete") after reasoning completes.
| Field | Type | Default | Description |
|---|---|---|---|
prompt |
str |
"" |
Prompt text to display to the user |
future |
asyncio.Future[str] | None |
None |
Future that resolves with user input |
timeout |
float | None |
None |
Seconds to wait; None for infinite wait |
result |
str | None |
None |
The received user input |
The UserInputSystem runs at priority -10 (before reasoning) and processes entities with a UserInputComponent:
- Creates an
asyncio.Futureif none exists on the component. - Publishes
UserInputRequestedEvent(entity_id, prompt). - Awaits the future with
asyncio.wait_for(asyncio.shield(future), timeout=component.timeout). - When
timeout=None, the system waits indefinitely for input. - On resolve: stores the result in
UserInputComponent.resultand appends it as a user message toConversationComponent. - On timeout: adds
ErrorComponentandTerminalComponentto the entity.
from ecs_agent.components import UserInputComponent
from ecs_agent.systems.user_input import UserInputSystem
# Add input component to agent
world.add_component(agent, UserInputComponent(
prompt="What would you like to do next?",
timeout=None, # Wait indefinitely
))
# Register system
world.register_system(UserInputSystem(priority=-10), priority=-10)TerminalCleanupSystem is the recommended helper for interactive continuations that must continue after a successful reasoning turn. Register it after reasoning, typically with priority=1, so it can clear reasoning_complete before the next tick's Runner stop check.
from ecs_agent.systems import TerminalCleanupSystem
world.register_system(TerminalCleanupSystem(priority=1), priority=1)
world.register_system(UserInputSystem(priority=-10), priority=-10)This behavior is opt-in. Runner still stops on top-level TerminalComponent unless a cleanup system removes a selected reason first.
External code provides input by resolving the future:
from ecs_agent.types import UserInputRequestedEvent
async def handle_input(event: UserInputRequestedEvent):
# Get input from user (e.g., stdin, UI, API)
user_response = input(event.prompt)
event.future.set_result(user_response)
world.event_bus.subscribe(UserInputRequestedEvent, handle_input)Both the UserInputSystem and the Runner support infinite waiting:
- UserInputSystem: Set
timeout=NoneonUserInputComponentfor indefinite wait. - Runner: Set
max_ticks=Noneonrunner.run()for indefinite execution.
Together, these allow building interactive agents that wait for human input without artificial time limits:
runner = Runner()
await runner.run(world, max_ticks=None) # Run until TerminalComponentUserInputRequestedEvent(entity_id, prompt)— Published when input is needed.
from ecs_agent import UserInputComponent, UserInputRequestedEvent, UserInputSystemAll types are available from the top-level ecs_agent package.