Agent Runtime (Observe → Think → Act)
This page explains what Agent does at runtime, with an emphasis on how LLM context is assembled: inputs, attributes, MessageFormatters, ContextBlocks (RAG / Memory / MCP), and tool calls.
Source of truth: masfactory/components/agents/agent.py
1) Observe: build system/user + chat messages
Agent.observe(input_dict) prepares:
system_prompt: str(written intomessages[0])user_prompt: str(written into the last user message)messages: list[dict](passed tomodel.invoke(...))
In MASFactory, context assembly is intentionally payload-first:
- Build a structured user payload (
dict) - Inject selected context blocks into the payload as a
CONTEXTfield - Let the input formatter
dump(...)convert that payload into the final user prompt text
1.1 Inputs come from
- Horizontal inputs (Edge keys):
input_dict - Vertical state (attributes): pulled via
pull_keys - Template placeholders:
{field}substitution insideinstructions/prompt_template - History messages: from
HistoryMemory.get_messages()(inserted as chat messages) - Context blocks: from Memory/RAG/MCP providers via
get_blocks(...)(injected intoCONTEXT)
1.2 Default user payload structure
If you don't pass formatters, MASFactory uses:
- input formatter:
ParagraphMessageFormatter - output formatter:
JsonMessageFormatter
The user payload typically contains:
MESSAGE TO YOU: renderedprompt_template- (optional) unused input fields (when
hide_unused_fields=False) RESPONSE FORMAT REQUIREMENTS: provided by the output formatterREQUIRED OUTPUT FIELDS AND THEIR DESCRIPTIONS: derived from outgoingoutput_keys+push_keysCONTEXT: injected context text (if any)
CONTEXTis injected before formatter dumping, so the same blocks will be represented differently depending on your input formatter.
2) Think: call the model adapter
Agent.think(messages, settings=...) calls:
model.invoke(messages=..., tools=..., settings=...)
and expects one of:
{"type": "content", "content": "<text>"}{"type": "tool_call", "content": [{ "id": "...", "name": "...", "arguments": {...} }, ...]}
3) Act: execute tools and append tool results
When the model returns tool_call:
Agent.act(...)usesToolAdapterto execute tools- builds tool messages shaped like
{"role":"tool","content":"...","tool_call_id":"..."} - appends them to
messagesand loops back tothink(...)until a finalcontentarrives
4) How context adapters affect Agents
RAG / Memory / MCP are unified as ContextProviders for Agents:
they only need to implement get_blocks(query, top_k=...) -> list[ContextBlock].
4.1 Passive (auto-inject)
When passive=True, the Agent calls get_blocks(...) during Observe and injects rendered blocks into the user payload as CONTEXT.
4.2 Active (on-demand retrieval via tools)
When active=True, the Agent provides two extra tools for the model:
list_context_sources()retrieve_context(source, query, top_k=...)
This enables “reason first, retrieve later” workflows.
See: /guide/context_adapters.
5) Examples: given inputs, what does the model receive?
These examples focus on prompt assembly, so they only call observe() (no model call required).
Example A: prompt_template only (no RAG/Memory)
from masfactory import Agent
agent = Agent(
name="writer",
model=object(), # observe() only
instructions="You are a concise writer.",
prompt_template="Topic: {topic}",
)
system_prompt, user_prompt, messages = agent.observe({"topic": "What is MASFactory?"})
print(system_prompt)
print("----")
print(user_prompt)
print("----")
print([m["role"] for m in messages])Example B: passive context injection (ContextBlock → CONTEXT)
from masfactory import Agent
from masfactory.adapters.context.types import ContextBlock, ContextQuery
class DummyProvider:
context_label = "MEM"
passive = True
active = False
supports_passive = True
supports_active = True
def get_blocks(self, query: ContextQuery, *, top_k: int = 8) -> list[ContextBlock]:
return [ContextBlock(text="memory snippet", score=0.9)]
agent = Agent(
name="writer",
model=object(),
instructions="You are a concise writer.",
prompt_template="Topic: {topic}",
memories=[DummyProvider()],
)
_, user_prompt, _ = agent.observe({"topic": "Explain DAG"})
print(user_prompt) # contains a CONTEXT field near the endYou will see a CONTEXT: field near the end of user_prompt, which contains:
[Context]
(MEM) memory snippetExample C: active provider (on-demand via tools)
from masfactory import Agent
from masfactory.adapters.context.types import ContextBlock, ContextQuery
class ActiveProvider:
context_label = "RAG"
passive = False
active = True
supports_passive = True
supports_active = True
def get_blocks(self, query: ContextQuery, *, top_k: int = 8) -> list[ContextBlock]:
return [ContextBlock(text=f"hit for: {query.query_text}", score=0.8)]
agent = Agent(
name="writer",
model=object(),
instructions="You are a concise writer.",
prompt_template="{query}",
retrievers=[ActiveProvider()],
)
agent.observe({"query": "MASFactory context blocks"})
print([t.__name__ for t in agent.tools]) # list_context_sources, retrieve_context, ...