feat(ai): Add agents defined as flat markdown files#189
Conversation
Code Review SummaryThis PR introduces the concept of AI agents defined as flat markdown files. It provides a storage layer ( 🚀 Key Improvements
💡 Minor Suggestions
|
| // 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() |
There was a problem hiding this comment.
For consistency with other AI-related endpoints (like runRuntime), this handler should check if AI features are enabled before processing the request.
| runtimes, err := s.aiRuntimes.List() | |
| + if !s.aiRequireEnabled(c) { | |
| + return | |
| + } | |
| + runtimes, err := s.aiRuntimes.List() |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
| return | ||
| } | ||
| c.JSON(http.StatusOK, gin.H{"runtimes": runtimes, "dir": s.aiRuntimes.Dir()}) |
There was a problem hiding this comment.
Exposing the internal server filesystem path (dir) in the API response is an unnecessary information disclosure. It's safer to remove it.
| c.JSON(http.StatusOK, gin.H{"runtimes": runtimes, "dir": s.aiRuntimes.Dir()}) | |
| + c.JSON(http.StatusOK, gin.H{"runtimes": runtimes}) |
269f4c4 to
1846160
Compare
| func (s *Server) listAgents(c *gin.Context) { | ||
| agents, err := s.aiAgents.List() |
There was a problem hiding this comment.
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.
| 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()}) |
There was a problem hiding this comment.
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.
| c.JSON(http.StatusOK, gin.H{"agents": agents, "dir": s.aiAgents.Dir()}) | |
| c.JSON(http.StatusOK, gin.H{"agents": agents}) |
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.
1846160 to
304d6c5
Compare
|
|
||
| // getAgent returns one agent's parsed metadata and raw file content, for the | ||
| // panel's editor. | ||
| func (s *Server) getAgent(c *gin.Context) { |
There was a problem hiding this comment.
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.
| 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) { |
There was a problem hiding this comment.
To maintain consistency with other AI-related endpoints, this handler should check if AI features are enabled before processing the request.
| 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) { |
There was a problem hiding this comment.
To maintain consistency with other AI-related endpoints, this handler should check if AI features are enabled before processing the request.
| 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 { |
| if a.Deployment != "" { | ||
| scope += ":" + a.Deployment | ||
| } | ||
| fmt.Fprintf(&b, "- %s (%s): %s\n", a.Name, scope, a.Description) |
There was a problem hiding this comment.
Handle cases where an agent has no description to avoid a trailing colon in the output for the AI model.
| 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) | |
| } |
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.