Files
byre/internal/report/gate.go
T

95 lines
1.9 KiB
Go

package report
import (
"context"
"encoding/json"
"fmt"
"sync"
"g.fc.run/misc/byre/internal/mcp"
)
const maxNudges = 3
const nudge = "You must call the report tool (mcp__byre__report) before ending your turn."
type Report struct {
Done []string `json:"done"`
NextSteps []string `json:"next_steps"`
Risks []string `json:"risks"`
Deviations []string `json:"deviations"`
}
type Gate struct {
OnReport func(Report)
mu sync.Mutex
reported bool
nudges int
}
func (g *Gate) Reset() {
g.mu.Lock()
defer g.mu.Unlock()
g.reported = false
g.nudges = 0
}
func (g *Gate) Nudges() int {
g.mu.Lock()
defer g.mu.Unlock()
return g.nudges
}
func (g *Gate) Tool() mcp.Tool {
return mcp.Tool{
Name: "report",
Description: "Report what you did this turn. You must call this before ending every turn. List every departure from the plan in deviations.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"done": {"type": "array", "items": {"type": "string"}},
"next_steps": {"type": "array", "items": {"type": "string"}},
"risks": {"type": "array", "items": {"type": "string"}},
"deviations": {"type": "array", "items": {"type": "string"}}
},
"required": ["done"]
}`),
Handler: g.handle,
}
}
func (g *Gate) handle(ctx context.Context, args json.RawMessage) (string, error) {
r := Report{}
err := json.Unmarshal(args, &r)
if err != nil {
return "", fmt.Errorf("malformed report: %w", err)
}
if len(r.Done) == 0 {
return "", fmt.Errorf("report must list at least one item in done")
}
g.mu.Lock()
g.reported = true
g.mu.Unlock()
if g.OnReport != nil {
g.OnReport(r)
}
return "report filed", nil
}
func (g *Gate) StopHook(ctx context.Context, input json.RawMessage) (map[string]any, error) {
g.mu.Lock()
defer g.mu.Unlock()
if g.reported || g.nudges >= maxNudges {
return map[string]any{}, nil
}
g.nudges++
return map[string]any{"decision": "block", "reason": nudge}, nil
}