Let agents see rendered diagrams and drop Mermaid support
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"g.fc.run/ai/byre/internal/mcp"
|
||||
)
|
||||
|
||||
func (m *Manager) renderDiagramTool() mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "render_diagram",
|
||||
Description: "See a diagram as byre's app draws it in the transcript: pass the body of a ```diagram block and get back a PNG of the result, drawn by the app's own renderer in light mode. " +
|
||||
"A source the app can't draw comes back as the reason instead.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {"type": "string", "description": "The block's body, without the fence lines."}
|
||||
},
|
||||
"required": ["source"]
|
||||
}`),
|
||||
Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
|
||||
args := struct {
|
||||
Source string `json:"source"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return mcp.TextResult(fmt.Sprintf("malformed render_diagram call: %v", err), true)
|
||||
}
|
||||
png, err := renderDiagram(ctx, args.Source)
|
||||
if err != nil {
|
||||
return mcp.TextResult(err.Error(), true)
|
||||
}
|
||||
return mcp.Result{Content: []mcp.Content{{Type: "image", Data: base64.StdEncoding.EncodeToString(png), MimeType: "image/png"}}}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func renderDiagram(ctx context.Context, source string) ([]byte, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := filepath.Join(filepath.Dir(exe), "..", "MacOS", "Byre")
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, app, "render-diagram")
|
||||
cmd.Stdin = strings.NewReader(source)
|
||||
stderr := bytes.Buffer{}
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render diagram: %w: %s", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
"end_line": {"type": "integer"},
|
||||
"ref": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"mermaid": {"type": "string"},
|
||||
"diagram": {"type": "string", "description": "For kind diagram: the body of a ` + "```diagram" + ` block, whose format is the diagrams knowledge-base article."},
|
||||
"task_id": {"type": "string"},
|
||||
"snapshot_id": {"type": "string"},
|
||||
"caption": {"type": "string"}
|
||||
|
||||
@@ -2,7 +2,7 @@ How to draw a box diagram (architecture, components, data flow) that byre's app
|
||||
|
||||
# Box diagrams
|
||||
|
||||
A fenced block with the language `diagram` draws boxes in rows with arrows between them. The app lays it out itself, so you never place coordinates. Clicking it opens a full-size view: pinch or scroll to zoom and pan; clicking a box highlights it, its arrows and its neighbors; each `highlight` line becomes a button there. Mermaid flowcharts (```` ```mermaid ````) still work for graphs whose shape the layout should find on its own.
|
||||
A fenced block with the language `diagram` draws boxes in rows with arrows between them. The app lays it out itself, so you never place coordinates. Clicking it opens a full-size view: pinch or scroll to zoom and pan; clicking a box highlights it, its arrows and its neighbors; each `highlight` line becomes a button there. It is the only diagram format byre draws; a ```` ```mermaid ```` block shows as plain code.
|
||||
|
||||
```diagram
|
||||
row
|
||||
@@ -31,3 +31,5 @@ highlight "Event loop" store server coord
|
||||
- Lines starting with `#` are comments. A mistake shows the source with the reason it wasn't drawn.
|
||||
|
||||
Keep each box to a title and one or two short detail lines, and draw only the arrows the reader needs; a picture with every dependency in it reads worse than prose.
|
||||
|
||||
Check a diagram before you send it: `render_diagram` takes the block's body and returns a PNG of what the app draws, or the reason it can't. Look for crossing or overlapping arrows and labels sitting on top of each other, and adjust the rows, `aside` or arrows until it reads cleanly.
|
||||
|
||||
@@ -33,7 +33,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
|
||||
- Dig into context with list_tasks, read_transcript, read_inbox and read_status (vendor rate limits and agent health), and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message.
|
||||
- You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it.
|
||||
- Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine.
|
||||
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix".
|
||||
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a fenced diagram block (its format is the diagrams knowledge-base article) of its stage names only, with no models: one row per step, a parallel's stages side by side in one row, and an arrow into each stage from the stages before it. For example, rows holding Build; Review and Test; Fix, with the arrows "build -> review", "build -> test", "review -> fix" and "test -> fix".
|
||||
- Schedules: when the human asks for work to run on a timer (every Monday, nightly), you manage it with list_schedules, save_schedule and delete_schedule; the human sees and edits the same schedules in the Project tab. On each run, byre creates a task from a schedule's prompt or workflow as create_task would, created by byre, and you get its created signal; a run that fails to start reaches you as a signal.
|
||||
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/tskDa7e4K7305e). byre draws it in that task's color, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer or a finished turn, link to #/task/<task_id>/transcript.
|
||||
- Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
|
||||
|
||||
@@ -847,7 +847,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool(), m.renderDiagramTool()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
|
||||
lead := true
|
||||
if sess.Role == api.Implementer {
|
||||
g += longTitleGuidance(task)
|
||||
@@ -918,7 +918,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
|
||||
return nil, err
|
||||
}
|
||||
l.notes = "\n\n" + notes
|
||||
tools = append(m.leadTools(l), m.readKnowledgeTool())
|
||||
tools = append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool())
|
||||
env = nil
|
||||
}
|
||||
env = append(git.IdentityEnv(l.gitName, l.gitEmail), env...)
|
||||
|
||||
@@ -30,7 +30,7 @@ const reviewGuidance = `You are a reviewer running inside byre. Another agent is
|
||||
- Start the review with ` + "`change`" + `: what the change does, in a few sentences, for someone who hasn't read it.
|
||||
- Each finding says what is wrong, where (path and line), why it matters and what to do about it. Leave out praise and nits. No findings is a fine result.
|
||||
- With no findings, the summary is exactly "No significant findings." and nothing else. With findings, the summary is a bulleted Markdown list, one line per finding, and nothing else.
|
||||
- Show rather than quote. Give a finding's file and lines and byre shows that part of the diff, so don't paste code. When a picture would explain something better than prose, such as a data flow, call graph, state machine or schema the change alters, draw it in a ` + "```mermaid" + ` block. When the change alters something you can see, you can run it (on any free port on 127.0.0.1) and attach screenshots taken with the Playwright tools. Most reviews need neither; leave out a diagram or screenshot that adds nothing to the text.`
|
||||
- Show rather than quote. Give a finding's file and lines and byre shows that part of the diff, so don't paste code. When a picture would explain something better than prose, such as a data flow, call graph, state machine or schema the change alters, draw it in a ` + "```diagram" + ` block (its format is the diagrams knowledge-base article) and check it with render_diagram. When the change alters something you can see, you can run it (on any free port on 127.0.0.1) and attach screenshots taken with the Playwright tools. Most reviews need neither; leave out a diagram or screenshot that adds nothing to the text.`
|
||||
|
||||
func reviewSchema() json.RawMessage {
|
||||
images := map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user