How to Run the AI-OPS Agent Programmatically¶
You can execute the AI-OPS Agent programmatically. The Agent is driven by the AgentRunner (ai_ops.core.runner) class that isolates the concrete agent orchestrator/aorchestrator implementation (ai_ops.core.agent).
There are both sync and async paths, for general use stick to the asynchronous version (that's what the api uses). For other use cases (ex. benchmarks, prompt optimization) you may want to use the Synchronous Execution Model.
The AgentRunner needs a pre-existing session and an already configured InferenceClient.
Create an InferenceClient
from ai_ops.core.llm import ModelConfig, build_inference_client
# under the hood InferenceClient uses litellm.Router
agent_model = ModelConfig(
model="hosted_vllm/gemma4-31B-it-AWQ",
api_base="https://your-vllm-domain/v1/", # optional
api_key="you-should-have-one", # optional
)
llm_client = build_inference_client(config=agent_model)
Create a session
from ai_ops.core.storage import get_session_store
session_store = get_session_store()
session = session_store.create_session()
Configure the AgentRunner
from ai_ops.core.runner import AgentRunner, AgentConfig
from ai_ops.core.tools import DEFAULT_TOOLS
from ai_ops.core.tools.terminal import AllowListPolicy
from ai_ops.core.context_management import LayeredContextView
agent = AgentRunner(
session_id=session.uuid,
client=llm_client,
config=AgentConfig(
tools=DEFAULT_TOOLS,
context_fn=LayeredContextView(max_window_tokens=32768),
command_policies=(AllowListPolicy(allowlist=["nmap", "ffuf", "..."]),),
temperature=0.4, # default is AI_OPS_AGENT_TEMPERATURE / 0.4
prompt_extension=None, # optional string appended to the system prompt
confirmation_timeout_s=300, # default; a pending call is denied on timeout
agent_id="react", # default; selects the prompt/orchestrator variant
),
is_new_conversation=True, # default; writes the system prompt as the first message
extra_tool_ctx=None, # optional dict passed to ToolContext.extra
)
is_new_conversation=Falseskips writing the system prompt (used to resume a conversation).File-writing and terminal tools operate in a per-session workspace under the base directory (
~/.local/share/ai_ops/workspace/<session_id>/), created automatically.
Consume the Event Stream
import asyncio
from ai_ops.core.schema import (
UserMessageEvent, AgentMode,
ReasoningEvent, TextEvent, ToolCallEvent, ToolConfirmationEvent,
ToolResultEvent, ToolErrorEvent, StopEvent,
)
async def main():
event_stream = agent.arun(
user_message=UserMessageEvent(content="say deez"),
mode=AgentMode.SUPERVISED, # default; UNSUPERVISED raises the iteration cap
max_iterations=None, # defaults to 30 (SUPERVISED) / 60 (UNSUPERVISED)
)
async for event in event_stream:
if isinstance(event, ReasoningEvent):
print(f"<think>{event.chunk}</think>", end="")
elif isinstance(event, TextEvent):
print(f"Assistant: {event.chunk}", end="")
elif isinstance(event, ToolCallEvent):
print(f"\n[Tool Call] {event.name}({event.args})")
if event.requires_confirmation:
approved = input(f"Allow {event.name}? [y/N] ").strip().lower() == "y"
agent.confirm(ToolConfirmationEvent(call_id=event.call_id, approved=approved))
elif isinstance(event, ToolResultEvent):
print(f"[Tool Result] {event.name} -> {event.result}")
elif isinstance(event, ToolErrorEvent):
print(f"[Tool Error] {event.name} ({event.failure}): {event.error}")
elif isinstance(event, StopEvent):
if event.error:
print(f"\n[Fatal Error] {event.error}")
else:
print(f"\n[Execution Stopped] Reason: {event.reason}")
asyncio.run(main())
arun() raises RuntimeError if the runner is already running, and ValueError if the persisted conversation isn't a valid [system, user, ...] message list.
Note:
ToolCallEvent.requires_confirmationis only set in the async path.
Multi-turn and Interruption¶
agent.send(UserMessageEvent(content="follow-up message")) # queue next user turn
agent.stop() # non-preemptive stop
send()only has an effect whilearun()is actively running, it checks an internal flag that only the async path sets. Calling it before starting, after a stop, or during a syncrun()call returnsFalse.- It returns
Trueif the message was queued,Falseotherwise (e.g. a message was already pending). Check the return value if you need to know whether it landed. - The queued message isn't necessarily held until the very end of the turn: it's spliced in as soon as there are no pending tool calls, which can happen right after any tool-call-free assistant message, not only after a
StopEvent. stop()flags the runner to stop after the current event finishes (e.g. an in-flight tool call completes).
Synchronous Execution Model¶
A synchronous AgentRunner.run also exists, however:
- It never evaluates command policy or confirmation (requires_confirmation is never true).
- It doesn't append events to the session, only messages are persisted, not the structured event stream. If you need events.jsonl for a session, drive it through arun().
- send() will never successfully queue a message against a sync run. stop() still works, since it flips a flag shared by both paths.
Note: I may want to change some of this behaviour (specifically event persistence and send) but it's not currently a priority.