Scope lead notes by project and share them with task agents
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user