97 lines
2.1 KiB
Go
97 lines
2.1 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"`
|
||
|
|
OpenQuestions []string `json:"open_questions"`
|
||
|
|
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"}},
|
||
|
|
"open_questions": {"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
|
||
|
|
}
|