Scope lead notes by project and share them with task agents

This commit is contained in:
Ian Gulliver
2026-10-16 23:12:14 -07:00
parent 165266f25d
commit 35184dc081
13 changed files with 395 additions and 251 deletions
+94 -10
View File
@@ -16,15 +16,18 @@ func (m *Manager) noteTools(tool func(name, description, schema string, run func
return []mcp.Tool{
tool("remember",
"Save a note about how the human works: a priority, a preference, or reasoning behind a choice, worded so you can apply it later. "+
"Without id it adds a note; with id it replaces that note's text. byre gives you every note at the start of each new lead session.",
"Give project_id when the note is about one project (its code, UI, docs or process); omit it when it holds for the human in every project. "+
"Without id it adds a note; with id it replaces that note's text and project. byre gives you every note at the start of each new lead session.",
`{"type": "object", "properties": {
"text": {"type": "string"},
"project_id": {"type": "string", "description": "The project the note applies to; omit for every project."},
"id": {"type": "string", "description": "The note to replace."}
}, "required": ["text"]}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
args := struct {
Text string `json:"text"`
ID string `json:"id"`
Text string `json:"text"`
ProjectID string `json:"project_id"`
ID string `json:"id"`
}{}
err := json.Unmarshal(raw, &args)
if err != nil {
@@ -34,7 +37,13 @@ func (m *Manager) noteTools(tool func(name, description, schema string, run func
if text == "" {
return "", errors.New("text is empty")
}
n, err := m.store.SaveLeadNote(ctx, user.Login, args.ID, text, leadActor(user.Login))
if args.ProjectID != "" {
_, err = m.store.GetProject(ctx, args.ProjectID)
if err != nil {
return "", err
}
}
n, err := m.store.SaveLeadNote(ctx, user.Login, args.ID, args.ProjectID, text, leadActor(user.Login))
if err != nil {
return "", err
}
@@ -66,11 +75,63 @@ func (m *Manager) noteTools(tool func(name, description, schema string, run func
"List your notes about how the human works, with their ids, as they are now. The human can delete notes in byre's Settings tab.",
`{"type": "object", "properties": {}}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
return m.notesText(ctx, user.Login)
return m.notesText(ctx, user.Login, "")
}),
}
}
const taskNotesGuidance = "The human's lead agent keeps the notes below on how the human works, in every project and in this one. Follow them in this task. " +
"When the human shows you a preference, a priority or the reasoning behind a choice that will recur, such as a correction or an overrule, suggest a note with suggest_note; the lead decides whether to keep it."
func (m *Manager) suggestNoteTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "suggest_note",
Description: "Suggest a note to the human's lead agent about how the human works: the choice and the why, in their terms, general enough to apply to the next case. " +
"The lead decides whether to save it, and for this project or every project. Don't suggest one-off facts about this task or what the notes already say.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"text": {"type": "string", "description": "The note, as you would want it worded."},
"evidence": {"type": "string", "description": "What the human said or did that shows it."}
},
"required": ["text", "evidence"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
args := struct {
Text string `json:"text"`
Evidence string `json:"evidence"`
}{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed suggest_note call: %w", err)
}
text := strings.TrimSpace(args.Text)
if text == "" {
return "", fmt.Errorf("%w: text must not be empty", ErrInvalid)
}
task, err := m.store.GetTask(ctx, l.taskID)
if err != nil {
return "", err
}
_, found, err := m.activeLead(ctx, task.CreatedBy)
if err != nil {
return "", err
}
if !found {
return "", fmt.Errorf("%w: %s has no lead agent to take the suggestion", ErrConflict, task.CreatedBy)
}
signal := fmt.Sprintf("%s suggests a note on how %s works:\n%s\nEvidence: %s\nSave it with remember if it holds and your notes don't already cover it, under project_id %s or for every project as fits; otherwise drop it.",
m.taskLabel(ctx, task.Id), task.CreatedBy, text, strings.TrimSpace(args.Evidence), task.ProjectId)
err = m.store.AddLeadInput(ctx, store.LeadInput{Owner: task.CreatedBy, Kind: store.LeadSignal, Actor: "byre", Text: signal})
if err != nil {
return "", err
}
m.deliverLead(task.CreatedBy)
return "Suggestion sent to the lead.", nil
},
}
}
func (m *Manager) DeleteLeadNote(ctx context.Context, login, id string) error {
text, err := m.store.DeleteLeadNote(ctx, login, id, login)
if err != nil {
@@ -89,18 +150,41 @@ func (m *Manager) DeleteLeadNote(ctx context.Context, login, id string) error {
return nil
}
func (m *Manager) notesText(ctx context.Context, owner string) (string, error) {
func (m *Manager) notesText(ctx context.Context, owner, onlyProject string) (string, error) {
notes, err := m.store.LeadNotes(ctx, owner)
if err != nil {
return "", err
}
if len(notes) == 0 {
return fmt.Sprintf("You have no notes yet on how %s works.", owner), nil
projects, err := m.store.ListProjects(ctx)
if err != nil {
return "", err
}
b := &strings.Builder{}
fmt.Fprintf(b, "Your notes on how %s works (%d):", owner, len(notes))
writeNotes(b, notes, "", fmt.Sprintf("Notes on how %s works in every project", owner))
for _, p := range projects {
if onlyProject != "" && p.Id != onlyProject {
continue
}
writeNotes(b, notes, p.Id, fmt.Sprintf("Notes on how %s works in project %s (%s)", owner, p.Name, p.Id))
}
if b.Len() == 0 {
return fmt.Sprintf("There are no notes yet on how %s works.", owner), nil
}
return strings.TrimPrefix(b.String(), "\n\n"), nil
}
func writeNotes(b *strings.Builder, notes []api.LeadNote, projectID, heading string) {
scoped := []api.LeadNote{}
for _, n := range notes {
if deref(n.ProjectId) == projectID {
scoped = append(scoped, n)
}
}
if len(scoped) == 0 {
return
}
fmt.Fprintf(b, "\n\n%s (%d):", heading, len(scoped))
for _, n := range scoped {
fmt.Fprintf(b, "\n- [%s] %s", n.Id, n.Text)
}
return b.String(), nil
}