Skip to content

feat(ai): Add agents defined as flat markdown files#189

Merged
nfebe merged 1 commit into
mainfrom
feat/agent-runtimes
Jul 23, 2026
Merged

feat(ai): Add agents defined as flat markdown files#189
nfebe merged 1 commit into
mainfrom
feat/agent-runtimes

Conversation

@nfebe

@nfebe nfebe commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

The direction announced on the site becomes runnable, with the naming corrected after research into how agent platforms draw this line: the markdown file is the agent (the definition), and the runtime is the layer that runs it, the shared runner, permission-gated tools, protected mode, redaction, and approvals built across this release.

An agent is a markdown file: optional frontmatter for its scope, a body of instructions. Running one executes the instructions as an AI session through that runtime, so every session guarantee carries over unchanged, including the pause for per-call approval before any state change. Each run records which agent it belongs to, so an agent's run history is its sessions.

The file is the whole definition. It lives in .flatrun/agents/, edits in any editor, versions in git, and needs no registration step: drop a file in, it lists; delete it, it is gone. This mirrors how Claude Code defines subagents and how LangGraph separates assistants from runs.

@sourceant

sourceant Bot commented Jul 23, 2026

Copy link
Copy Markdown

Code Review Summary

This PR introduces the concept of AI agents defined as flat markdown files. It provides a storage layer (AgentStore), API handlers for CRUD and execution, and integrated AI tools allowing agents to manage other agents. The architecture is robust and follows established project patterns.

🚀 Key Improvements

  • File-based persistence for agents in .flatrun/agents/, enabling git-managed agent definitions.
  • Integration of agents with the existing session runtime, preserving security gates and tool loops.
  • Addition of write_agent_file and read_agent_file tools, facilitating agent meta-programming.

💡 Minor Suggestions

  • Add aiRequireEnabled checks to getAgent, putAgent, and deleteAgent for API consistency.
  • Optimize redundant file I/O in the getAgent handler.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment thread internal/api/runtime_handlers.go Outdated
// listRuntimes returns every agent runtime defined as a markdown file under
// the runtimes directory.
func (s *Server) listRuntimes(c *gin.Context) {
runtimes, err := s.aiRuntimes.List()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with other AI-related endpoints (like runRuntime), this handler should check if AI features are enabled before processing the request.

Suggested change
runtimes, err := s.aiRuntimes.List()
+ if !s.aiRequireEnabled(c) {
+ return
+ }
+ runtimes, err := s.aiRuntimes.List()

Comment thread internal/api/runtime_handlers.go Outdated
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"runtimes": runtimes, "dir": s.aiRuntimes.Dir()})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposing the internal server filesystem path (dir) in the API response is an unnecessary information disclosure. It's safer to remove it.

Suggested change
c.JSON(http.StatusOK, gin.H{"runtimes": runtimes, "dir": s.aiRuntimes.Dir()})
+ c.JSON(http.StatusOK, gin.H{"runtimes": runtimes})

@nfebe
nfebe force-pushed the feat/agent-runtimes branch from 269f4c4 to 1846160 Compare July 23, 2026 12:32

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment on lines +14 to +15
func (s *Server) listAgents(c *gin.Context) {
agents, err := s.aiAgents.List()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To maintain consistency with other AI-related endpoints and ensure that AI features can be globally toggled, this handler should check if AI features are enabled before processing the request.

Suggested change
func (s *Server) listAgents(c *gin.Context) {
agents, err := s.aiAgents.List()
if !s.aiRequireEnabled(c) {
return
}
agents, err := s.aiAgents.List()

c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"agents": agents, "dir": s.aiAgents.Dir()})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposing the internal server filesystem path (dir) in the API response is an unnecessary information disclosure. It's safer to remove it as the client only needs the list of agents.

Suggested change
c.JSON(http.StatusOK, gin.H{"agents": agents, "dir": s.aiAgents.Dir()})
c.JSON(http.StatusOK, gin.H{"agents": agents})

@nfebe nfebe changed the title feat(ai): Add agent runtimes defined as flat markdown files feat(ai): Add agents defined as flat markdown files Jul 23, 2026
An agent can now be defined as a markdown file: optional frontmatter for
its scope, a body of instructions. Running one executes the instructions
as an AI session through the runtime's shared tool set, so every session
guarantee carries over: permission-gated tools, protected mode, secret
redaction, and the pause for per-call approval before any state change.
Each run records which agent it belongs to, so an agent's run history is
its sessions.

Agents can be created three ways: ask the assistant to write one (the
write tool validates the definition and waits for approval), edit the
file from the panel, or drop a file in the agents directory; there is no
registration step. The architecture for policy, external MCP tools,
triggers, and durable runs is documented in docs/AGENTS.md.
@nfebe
nfebe force-pushed the feat/agent-runtimes branch from 1846160 to 304d6c5 Compare July 23, 2026 13:01

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.


// getAgent returns one agent's parsed metadata and raw file content, for the
// panel's editor.
func (s *Server) getAgent(c *gin.Context) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handler can be optimized to read the file only once. Currently, it calls Get (which reads and parses) and then Raw (which reads again). Additionally, for consistency with other AI endpoints, it should check if AI features are enabled.

Suggested change
func (s *Server) getAgent(c *gin.Context) {
func (s *Server) getAgent(c *gin.Context) {
if !s.aiRequireEnabled(c) {
return
}
name := c.Param("name")
content, err := s.aiAgents.Raw(name)
if err == ai.ErrAgentNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Agent not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
agent, err := ai.ParseAgent(name, content)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"agent": agent, "content": content})
}

}

// putAgent validates and writes an agent definition from the panel's editor.
func (s *Server) putAgent(c *gin.Context) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To maintain consistency with other AI-related endpoints, this handler should check if AI features are enabled before processing the request.

Suggested change
func (s *Server) putAgent(c *gin.Context) {
func (s *Server) putAgent(c *gin.Context) {
if !s.aiRequireEnabled(c) {
return
}

}

// deleteAgent removes an agent definition.
func (s *Server) deleteAgent(c *gin.Context) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To maintain consistency with other AI-related endpoints, this handler should check if AI features are enabled before processing the request.

Suggested change
func (s *Server) deleteAgent(c *gin.Context) {
func (s *Server) deleteAgent(c *gin.Context) {
if !s.aiRequireEnabled(c) {
return
}
if err := s.aiAgents.Delete(c.Param("name")); err != nil {

Comment thread internal/api/ai_tools.go
if a.Deployment != "" {
scope += ":" + a.Deployment
}
fmt.Fprintf(&b, "- %s (%s): %s\n", a.Name, scope, a.Description)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle cases where an agent has no description to avoid a trailing colon in the output for the AI model.

Suggested change
fmt.Fprintf(&b, "- %s (%s): %s\n", a.Name, scope, a.Description)
if a.Description != "" {
fmt.Fprintf(&b, "- %s (%s): %s\n", a.Name, scope, a.Description)
} else {
fmt.Fprintf(&b, "- %s (%s)\n", a.Name, scope)
}

@nfebe
nfebe merged commit bfc4d76 into main Jul 23, 2026
6 checks passed
@nfebe
nfebe deleted the feat/agent-runtimes branch July 23, 2026 13:22
@nfebe nfebe mentioned this pull request Jul 23, 2026
43 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant