Exploring programmable agent objects, semantic self-containment, and a single-file approach inspired by Vue.
An application-specific agent can start with a fairly compact definition: a prompt describes its task, and a tool list identifies the operations it can use. At that stage, reviewing the definition can give us a useful picture of the agent.
As the application develops, the role may acquire more specific requirements. It might need to track a budget, react to execution results, preserve progress across restarts, or validate the decision it returns to the application. Those requirements bring programmatic logic into the picture alongside the model's instructions.
Someone reviewing the agent now needs to understand how these pieces fit together. What capabilities are available? What does the prompt ask the model to do? Which rules are enforced by code, and what state do those rules depend on?
Modern agent frameworks provide middleware, callbacks, hooks, and plugins for implementing such behavior. I've been interested in a question about its organization: when an agent acquires its own application-specific logic, where should that logic live?
Could we treat the agent as a programmable object, with attributes, methods, and state, and keep its local rules alongside its definition? Shared implementations could still live in reusable modules, while the choices particular to this agent remain visible together.
From configurable agents to programmable objects
Consider an agent that examines support tickets and drafts replies using a knowledge-base search tool. Its role and initial capability can be described together:
description: "Examines one support ticket and drafts a reply."
tools:
- search-knowledge
This declaration identifies the role and the search capability it uses. The search implementation can live elsewhere; reviewing the binding tells us which operation this agent can call.
Now suppose the application introduces another requirement:
Each ticket may use at most three knowledge-base searches. Once the budget is exhausted, the agent should use the evidence already collected or acknowledge what it cannot verify and request human assistance.
The agent needs a counter, a check before execution, and a way to handle the situation when another search is prohibited. The prompt can explain the intended behavior, while programmatic logic enforces the operational limit.
We could implement this through a tool wrapper, middleware, or application code. What makes agent-owned logic appealing here is the boundary between the search capability and the rules governing its use.
The tool has a semantic scope: search the knowledge base and return relevant information. This support role's conditions for using it can evolve independently. VIP tickets might get five attempts; tickets already handed to human support might stop searching; failed searches might consume the budget differently in another application.
A framework-provided limit may cover a simple case. As the conditions become more specific to the role, I sometimes find it more natural to express them as an ordinary method:
def _can_search(self):
if self.escalated:
return False
limit = 5 if self.ticket_type == "vip" else 3
return self.state.search_count < limit
Here, ticket_type and escalated represent application information supplied or maintained for the agent. The method reads that information and its own search count to express a rule. A change to the rule can be made in Python without introducing a configuration keyword for every variation.
This example assumes one agent per ticket; a quota shared across workers would need a shared source of truth. The scope of the state should follow the scope of the rule.
We can certainly put rules about customer status and query limits inside the search tool. That choice couples a reusable knowledge lookup operation to the application's classification and budget policies. Different control rules may then lead to separate tool variants or additional policy branches around the same search implementation.
A version that distinguishes VIP tickets would also need that classification supplied, for example through an extra ticket_type argument. This argument serves the control policy; it contributes no information to the knowledge query itself. As more conditions are added, the tool's interface and implementation take on more of the surrounding application's concerns.
For this role, I would prefer to keep the search tool focused on knowledge retrieval. The application can supply ticket facts to the agent, and the agent can maintain its own attempt count and decide whether its policy permits another search. Those rules can evolve while the search capability remains reusable.
An agent can be declarative where appropriate and directly programmable where useful. But giving an agent its own logic leaves another decision: where should we organize that logic?
If the prompt, state initialization, rule, and execution callback are assembled in different places, a reviewer needs to follow their connections. I would like the agent's definition to remain a useful place for reading those related decisions together.
An idea inspired by Vue's Single-File Components
Vue's Single-File Components bring a component's template, script, and styles into one .vue file. They describe different aspects of the same component, and a change to one aspect often needs to be understood in relation to the others.
A component can also have its own state and functions while importing shared behavior from other modules. That combination appeals to me: local logic stays near the definition it belongs to, and reusable implementations retain their own scope.
I wondered whether a similar organization could work for agents. Their instructions, capability bindings, attributes, persistent state, methods, and hooks collectively determine how a particular role behaves. When checking a search-budget rule, for example, I would like to see its state and enforcement point near the instructions and capabilities it affects.
There are two choices here: allowing the agent to own methods and state, and keeping those methods and state near the rest of its definition. Together, they could give a reviewer a coherent unit to examine as the agent gains application-specific behavior.
Larger implementations can still extract shared functions and services. What I want to preserve is a clear place for understanding how this particular agent is put together.
This is what I've been calling semantic self-containment.
Semantic self-containment
To make that idea concrete, consider this call:
use_retry(self, max_retries=3)
Its implementation can involve error classification, hook registration, and backoff. Including that implementation in every agent would create duplication, but this call already communicates a behavioral choice: the agent enables a retry policy with a limit of three.
The choice is locally visible, while its implementation remains reusable. A tool binding works similarly. The declaration identifies which search capability this agent uses; following the reference gives us its implementation details when needed.
A local method serves a complementary purpose:
def _search_budget_exhausted(self):
return self.state.search_count >= self.state.search_limit
Here the rule itself is short enough to inspect directly. It can sit beside the registration of the state it reads and the hook that uses its result.
Both forms help explain the agent: a meaningful call selects reusable behavior, and a local method expresses a rule particular to this role. A reviewer can inspect their relationship to the prompt and capability declarations in the same place.
By semantic self-containment, I mean keeping an agent's capabilities, state, policies, and local logic understandable together, while allowing reusable implementations to retain their own scope. A definition can depend on several modules and still make this agent's choices clear.
The quality of those abstractions matters. A vague call such as apply_policies(self, config) could conceal substantial behavior even inside a single file. Explicit bindings, meaningful names, and readable assembly are what make that file a useful unit of review.
I've been exploring this idea through semantic self-containment, inspired in part by Vue's Single-File Components. In my open-source Python agent runtime, Flowing, a reusable agent declaration provides a place for both capability bindings and local Python behavior. What interests me is whether that organization can keep a growing agent easy to review.
Putting the idea into practice: a support agent in Flowing
We can explore this organization by building the support agent's definition step by step. In Flowing, the .fya("flowing yaml") format brings declarative fields, instructions, and agent-specific Python logic into one file. That gives us a place to examine how a capability declaration, a rule, and its enforcement fit together.
Let's return to the support role. We want an agent that examines one ticket, searches for relevant information, and drafts a reply. We'll implement the three-search budget directly so we can follow its state, methods, and hooks together. Then we can consider how further requirements fit into the same definition.
The search engine remains external. The surrounding application supplies ticket information and handles actual delivery and handoff. The part we are organizing here is the behavior specific to the support agent.
The following sections build the same support.fya. It begins with YAML fields, followed by named sections separated by ---: $system_prompt contains the instructions, and $script contains Python code. The initialization snippets belong to one setup() function.
1. Make the role and initial capability visible
A .fya is a reusable Agent declaration. We begin with a description and one tool binding:
description: "Examines one support ticket and drafts a reply."
tools:
- search-knowledge
The tool list binds the operation the model can call. The application supplies its implementation; this file records that the support role uses it. A reviewer has a starting point for understanding the agent's purpose and capability.
2. Keep the instructions beside the capabilities
The next section describes how the model should perform that role:
---
$system_prompt:
You examine support ticket {{ticket_id}} and draft a reply.
Use verified knowledge-base information.
Do not invent policies, order details, or unsupported facts.
You have a limited search budget. When a search is blocked, use the
available evidence or explain what cannot be verified and request
human assistance.
The system prompt can also contain Jinja2 template expressions. Here, {{ticket_id}} is filled from the Agent's ticket_id attribute when context is assembled; the setup() function below supplies that attribute. The same definition can therefore serve different ticket instances.
The instructions explain how to use the search capability: collect evidence, draft a grounded reply, and acknowledge gaps when the available information is insufficient. They can be read alongside the binding immediately above them.
The budget instruction also gives the model guidance about what to do when it cannot search again. We still need code to check the count and stop a prohibited call. That is where the agent's own methods and state enter the definition.
3. Give the Agent its own methods
The $script block can define ordinary instance methods. For this policy, one method checks the budget and another provides the guidance to return when it is exhausted. setup() initializes the instance:
---
$script:
from flowing.errors import Intercepted
def _search_budget_exhausted(self):
return self.state.search_count >= self.state.search_limit
def _fallback_instruction(self):
return (
"The search budget is exhausted. Use the available evidence "
"or explain what cannot be verified and request human assistance."
)
async def setup(self, ticket_id: str):
self.ticket_id = ticket_id
self.state.register("search_limit", 3)
self.state.register("search_count", 0)
These functions become methods on the Agent object. They can read its attributes and state, and other methods or hooks can call them directly. The leading underscores are Python's convention for internal methods.
The ticket ID is an ordinary attribute supplied during initialization and used by the prompt template. The registered values are persistent state: the budget and the accumulated count.
Registration preserves a restored value instead of overwriting it with the default. That allows the agent to keep its accumulated count when the ticket continues across launches. The state registration and the method reading it are visible together, so their relationship can be reviewed without tracing a separate counter back to this instance.
4. Let a Hook enforce the rule
The methods need a point where they can affect execution. For this rule, that point is immediately before a search call. Inside the same setup(), attach a handler to that tool's pre-call hook:
def limit_search(agent, call):
if agent._search_budget_exhausted():
raise Intercepted(agent._fallback_instruction())
agent.state.search_count += 1
return call
self.hooks.before_tool_call["search-knowledge"](limit_search)
The handler receives the Agent and the pending call. It checks the agent's method, increments the counter when the call is allowed, and returns the call so execution can proceed. The name in the subscription scopes this handler to search-knowledge.
Raising Intercepted prevents execution and returns the reason to the model as a blocked tool result. The counter is incremented before execution, so an admitted call consumes an attempt even if execution subsequently fails.
The method, its state, and the enforcement point now appear together. A reviewer can see what the rule means and where it takes effect, while the search implementation retains its separate responsibility.
5. Intervene after the final search
The pre-call check blocks an attempt beyond the limit. We may also want to guide the model immediately after its final permitted attempt finishes, before it tries another search. To send that guidance once, we can register a notice flag and attach a second hook inside the same setup():
self.state.register("budget_notice_sent", False)
async def notify_limit(agent, result):
if (
agent._search_budget_exhausted()
and not agent.state.budget_notice_sent
):
agent.state.budget_notice_sent = True
await agent.steer(agent._fallback_instruction())
return result
self.hooks.after_tool_call["search-knowledge"](notify_limit)
This handler consults the same budget method and records that it has sent the guidance. The flag avoids repeatedly sending that notice. steer() supplies it for a subsequent model call in the active turn; it does not interrupt an in-flight request.
The two hooks serve related purposes: the pre-call hook enforces the limit, and the post-call hook guides the model after the budget is consumed. Both can use the agent's own methods and state, and both are attached in the same declaration. That makes it possible to review the constraint and the behavior surrounding it together.
This example uses a search tool that returns its result normally. Background tools have separate result-delivery timing to consider.
Reusable behavior through Composables
Retrying a failed model request is useful across many roles. I would like to reuse that policy while keeping the choice to enable it visible in this agent.
A Composable is an ordinary Python function that attaches behavior to an Agent. For retries, import use_retry into $script and add its call to the existing setup():
from flowing.composables import use_retry
use_retry(self, max_retries=3)
Like the search-budget handlers, this function uses the agent's hooks to intervene during execution. Its implementation is shared, while the decision to enable it and the configured limit are local to this definition.
I use the implementations in flowing.composables here as examples of what Composables can do. They express particular policy choices and may not be the best fit for a downstream application. Developers can use them as references, adapt them, or write their own Composables with the hooks, state, and methods their application needs. The behavior selected for an agent can still be made visible through an explicit call in its definition.
The same pattern can accommodate further requirements. Some reusable behaviors also need data describing what they should do. We can place that data in YAML and attach its consumer in setup(), keeping both choices visible together.
Procedure guidance through Skills
Suppose this agent now needs to follow delivery, refund, and escalation procedures maintained as separate documents. A Skill is a reusable document of task guidance that the agent can load when relevant.
We can extend the YAML with a skills field and add the loading tool to the existing tool list:
tools:
- search-knowledge
- skill-load
skills:
- ./skills/delivery-support.md
- ./skills/refund-handling.md
- ./skills/human-escalation.md
Together with that data, import the corresponding Composable into $script and call it inside the existing setup():
from flowing.plugins.skills import use_skill
use_skill(self)
skills is an ordinary, non-reserved field that becomes agent.skills. use_skill() reads it and assembles this instance's procedure catalog and loading methods. The extension also requires SkillPlugin to be installed on the Runtime; we will see that shared installation in the project entry point below.
Add the corresponding instruction to the existing system prompt:
Load the relevant Skill when handling delivery, refund, or
human-escalation questions.
A reviewer can inspect that instruction, the selected procedure documents, the loading-tool binding, and the call enabling the behavior in the same file.
Structured results for the host
Suppose the surrounding application also needs to distinguish a usable reply draft from a request for human assistance. A JSON result with named fields gives the host something it can consume programmatically.
We can describe the expected shape under another ordinary YAML field, output, and attach the Composable that checks it:
output:
reply:
type: string
description: "Customer-facing reply draft."
needs_human:
type: boolean
description: "Whether human assistance is required."
reason:
type: string
description: "Reason for requesting human assistance, if any."
Import use_validate_turn_result into $script and add its call to the existing setup():
from flowing.composables import use_validate_turn_result
use_validate_turn_result(self, on_fail="steer")
This function reads agent.output and registers a validation handler on the model-response hook. If the final response fails the schema check, on_fail="steer" supplies the validation feedback as a steering message and keeps the same turn running so the model can revise its response. The output contract is enforced by the attached behavior, including guidance toward a valid result.
The model also needs instructions to produce that result. Add to the existing system prompt:
Return JSON with reply (string), needs_human (boolean), and reason
(string). Set needs_human to true when human assistance is required.
Do not claim that a human handoff has already been completed.
A reviewer can now check the requested JSON, the expected shape, and the validator consuming it together. Both output and skills get their meaning from the code that reads them. Application-defined fields can follow the same pattern: each need introduces its data and the behavior using it, with reusable implementations remaining external.
From the Agent declaration to the host application
The support agent owns its search policy and produces a structured decision. The host runs it and performs the actual handoff.
A .fya defines an Agent class. The Runtime mounts it as a live object with its own attributes, state, hooks, and message history. One instance per ticket gives each ticket an independent search budget.
A project assembly function could look like this:
from flowing import Runtime
from flowing.plugins.skills import SkillPlugin
from app.support_tools import register_support_tools
async def main(ticket_id: str) -> Runtime:
runtime = Runtime()
runtime.set_model_tags("@/model-tags.yaml")
runtime.install(SkillPlugin())
register_support_tools(runtime)
await runtime.mount(
"@/support.fya", agent_id=f"ticket-{ticket_id}", ticket_id=ticket_id
)
return runtime
register_support_tools() represents application code registering the search implementation. The project supplies its Skill files and model configuration too. A stable Agent ID lets a later launch restore that ticket's messages and registered state. Normal shutdown drains pending persistence writes.
The host uses launch(project_dir, ticket_id=...), retrieves the Agent with get_agent(), and awaits agent.query(ticket_text). It handles an unsuccessful turn through its own fallback path. For a completed result, it consumes the validated JSON, reviews or delivers the draft, and acts on needs_human.
Requesting human assistance and completing a handoff are distinct operations. The application owns delivery and mandatory business escalation rules. Output validation establishes structure; it cannot establish that the model's judgment is correct.
Could this organization help coding agents?
Suppose the product team now asks for five searches on VIP tickets and no further searches after human handoff. A coding assistant modifying this agent has a natural starting point in support.fya: the state, budget method, fallback guidance, and hooks are locally visible.
It could introduce a method like _can_search() from the earlier example and have the hook consult it. Ticket classification and handoff status would need appropriate application sources, and the fallback guidance would need to cover both blocking conditions.
That makes the modification's related parts discoverable. It also exposes persistence questions: changing the default in state.register("search_limit", ...) preserves existing restored limits, so changing defaults and updating existing tickets are separate tasks.
I see a possible benefit here for coding assistants maintaining application-specific agents. Whether this organization improves modification accuracy would need evidence from actual tasks.
For humans: understanding, review, and monitoring
A human reviewing that change can examine the capability bindings, instructions, state registration, methods, and hook attachment together. The file provides a common point of reference for questions such as:
- Where does VIP status come from, and who updates handoff status?
- Is the constraint enforced, and does it count attempts or successes?
- Which values survive recovery?
- Does the output express the decision the host expects?
External implementations still need review where relevant. The declaration makes their relationship to this particular agent easier to follow.
Runtime observations can complement that static view. A hook can record the count, the decision, and agent.node_id at the point where it permits or blocks a search. The host can also inspect the live Agent's state and message history. Those observations help connect the rule being reviewed to what happened during execution.
Keeping related behavior nearby may make it easier to check consistency across code, instructions, and observations. Its usefulness depends on the quality of those abstractions and on the surrounding application.
Where I would draw the boundary
The convenience of writing methods and hooks directly in an agent definition can make it tempting to keep adding code there. I would use that flexibility while it keeps the agent's behavior easy to review. Keeping everything in one file can become a goal of its own, at the expense of that clarity.
For a small agent-specific rule, a method and a few hooks can be the clearest organization. As the implementation grows or more roles share the rule, extracting a Composable may improve understanding and reuse. Its invocation and parameters can remain in the declaration, so the agent's choice of behavior stays visible.
Responsibilities also need to retain their proper scope. A search engine can stay in a tool, detailed procedures in Skills, and application-wide workflows in the host. A quota shared by several workers needs a shared control point; placing a separate counter on every Agent would not enforce that quota. The ability to put logic on an Agent does not make it the right owner of every rule.
The same restraint applies to file size. A declaration crowded with unrelated implementation details can become harder to review than a few well-organized modules. I would extract those details when doing so makes the agent easier to understand, leaving its bindings, policy choices, and agent-specific logic together. That is the boundary I want to preserve: the file remains a coherent entry point for reviewing the agent's behavior.
After appropriate extraction, the definition can still be semantically self-contained. It can show which behaviors the agent adopts, how they are configured, and how they fit together, while encapsulating implementation details in external modules. A call such as use_retry(self, max_retries=3) makes the policy choice and limit visible while leaving hook registration and backoff details elsewhere. Those details may require following the reference, but the agent's behavioral assembly remains locally understandable.
Trade-offs and open questions
A programmable .fya contains executable application code. Its external tools, shared policies, and host-side hooks can all affect runtime behavior. Reviewing the file remains part of a broader process involving dependency review, testing, and observation.
An ordinary Python class can also collect related logic. Flowing supports handwritten Agent subclasses alongside .fya; both produce the same class model. The file format gives declarations, long prompts, and Python methods a common authoring structure that I am exploring as a default unit of review.
Other frameworks already offer state, middleware, hooks, plugins, and callbacks. The choice I am exploring is to make the reusable Agent definition a consistent place for discovering an agent's capabilities, policies, and application-specific logic.
For some applications, ordinary Python construction or a more distributed organization may be clearer. The useful question is how well the chosen boundary supports understanding as requirements accumulate.
Conclusion: a possible programming boundary for Agents
The support agent began with capabilities and instructions. Its search policy brought persistent state, methods, execution constraints, and fallback behavior into that definition. Keeping these related pieces together gives a reviewer a coherent place to examine how the role works.
That is the value I hope semantic self-containment can offer: one file where an agent's selected capabilities and its own behavioral rules can be reviewed together, with reusable implementations retaining their own scope.
I'm exploring this approach in Flowing. Its documentation covers declarations, hooks, Composables, and embedding the runtime in Python applications.
When reviewing an agent with application-specific logic, which parts of its behavior do you want to see together, and when does extracting a rule into another module make that review easier?
If this approach resonates with you, give Flowing a try. I'd love to hear what you build with it—and if you like the idea, a star on GitHub would mean a lot.
This is completed under help of AI. Thanks Codex for helping me drafting and revising this article and preparing the cover image.
Top comments (0)