|
|
|
@@ -111,7 +111,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
tool("answer_question",
|
|
|
|
|
"Answer an open question on the human's behalf. Only when the answer is clear or the human told you it. "+
|
|
|
|
|
"type option: options holds the chosen choices' exact text (one for pick-one), and text may add notes."+
|
|
|
|
|
"type other: free text.",
|
|
|
|
|
"type other: free text."+stoppedRefusal,
|
|
|
|
|
`{"type": "object", "properties": {
|
|
|
|
|
"item_id": {"type": "string"},
|
|
|
|
|
"type": {"type": "string", "enum": ["option", "other"]},
|
|
|
|
@@ -124,6 +124,18 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
item, err := m.store.GetInboxItem(ctx, c.ItemId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
sess, err := m.store.GetSession(ctx, item.AgentSessionId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
err = m.checkNotStopped(ctx, sess)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
m.leadAnswered[c.ItemId] = true
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
@@ -141,7 +153,8 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
"Send a message to a task's agent; it starts a turn. Starts the agent when the task has none. "+
|
|
|
|
|
"If the agent is mid-turn, the message is queued like the human's: the agent reads it with its next tool call's result, "+
|
|
|
|
|
"or as its next turn if it makes no further tool call, and gets it only once. "+
|
|
|
|
|
"On a team task, the message is posted to the team's board instead, so every agent of the team gets it the same way; model is refused there.",
|
|
|
|
|
"On a team task, the message is posted to the team's board instead, so every agent of the team gets it the same way; model is refused there, "+
|
|
|
|
|
"and so is the post while any agent of the team is stopped."+stoppedRefusal,
|
|
|
|
|
`{"type": "object", "properties": {
|
|
|
|
|
"task_id": {"type": "string"},
|
|
|
|
|
"text": {"type": "string"},
|
|
|
|
@@ -172,6 +185,10 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
if args.Model != "" {
|
|
|
|
|
return "", fmt.Errorf("%w: model can't be given for a team task; its agents keep their own models", ErrInvalid)
|
|
|
|
|
}
|
|
|
|
|
err = m.checkTeamNotStopped(ctx, task.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
err = m.Post(ctx, task, leadActor(user.Login), args.Text)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
@@ -359,7 +376,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
return "Interrupted.", nil
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
tool("compact_task", "Have a task's idle agent write a handoff and continue in a fresh session.",
|
|
|
|
|
tool("compact_task", "Have a task's idle agent write a handoff and continue in a fresh session."+stoppedRefusal,
|
|
|
|
|
`{"type": "object", "properties": {
|
|
|
|
|
"task_id": {"type": "string"},
|
|
|
|
|
"model": {"type": "string", "description": "Start the fresh session on this model, on either vendor; the current agent still writes the handoff. Defaults to the current model."}
|
|
|
|
@@ -376,6 +393,10 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
err = m.checkNotStopped(ctx, sess)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
_, err = m.Compact(ctx, sess.Id, args.Model)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
@@ -387,7 +408,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
"Merge a task as the human: its agent merges the default branch in, then byre squashes it into one commit, pushes it to origin and closes the task. "+
|
|
|
|
|
"While another task in the project is merging, byre holds the merge and starts it when that one is done. "+
|
|
|
|
|
"For a subtask, target parent instead asks its parent task's agent to merge it into the parent's branch, and the subtask closes once the parent has. "+
|
|
|
|
|
"Refused for a task with active subtasks. Only when the human asks. You get a signal when it is merged; failures show as errors in the task's transcript, and you get a signal for each.",
|
|
|
|
|
"Refused for a task with active subtasks. Only when the human asks. You get a signal when it is merged; failures show as errors in the task's transcript, and you get a signal for each."+stoppedRefusal,
|
|
|
|
|
`{"type": "object", "properties": {
|
|
|
|
|
"task_id": {"type": "string"},
|
|
|
|
|
"target": {"type": "string", "enum": ["main", "parent"], "description": "Default main."}
|
|
|
|
@@ -406,7 +427,17 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
}
|
|
|
|
|
switch args.Target {
|
|
|
|
|
case "", "main":
|
|
|
|
|
err = m.checkTaskNotStopped(ctx, task.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
case "parent":
|
|
|
|
|
if task.ParentTaskId != nil {
|
|
|
|
|
err = m.checkTaskNotStopped(ctx, *task.ParentTaskId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_, err = m.MergeIntoParent(ctx, task, user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
@@ -440,13 +471,17 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
|
|
|
|
|
|
|
|
|
tool("discard_task",
|
|
|
|
|
"Discard a task: its agent gets one turn to write the task summary, then byre stops it and deletes its worktree and branch without merging. This destroys its unmerged work. Refused for a task with active subtasks. Only when the human asks. "+
|
|
|
|
|
"If the agent is mid-turn, the summary turn starts when that turn ends; no other turn runs in between.",
|
|
|
|
|
"If the agent is mid-turn, the summary turn starts when that turn ends; no other turn runs in between."+stoppedRefusal,
|
|
|
|
|
taskOnly,
|
|
|
|
|
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
|
|
|
|
task, _, err := m.taskArg(ctx, raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
err = m.checkTaskNotStopped(ctx, task.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
_, err = m.Discard(ctx, task, user.Login)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
@@ -784,6 +819,38 @@ func (m *Manager) liveSession(ctx context.Context, taskID string) (api.AgentSess
|
|
|
|
|
return sess, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stoppedRefusal = " Refused while the agent is stopped: the human interrupted its last turn, or that turn ended in an error, and no turn has started since. Then put it to the human in chat instead."
|
|
|
|
|
|
|
|
|
|
func (m *Manager) checkTaskNotStopped(ctx context.Context, taskID string) error {
|
|
|
|
|
sess, found, err := m.liveSession(ctx, taskID)
|
|
|
|
|
if err != nil || !found {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return m.checkNotStopped(ctx, sess)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) checkTeamNotStopped(ctx context.Context, taskID string) error {
|
|
|
|
|
sessions, err := m.store.ListSessions(ctx, taskID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for _, s := range teamSessions(sessions) {
|
|
|
|
|
err = m.checkNotStopped(ctx, s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) checkNotStopped(ctx context.Context, sess api.AgentSession) error {
|
|
|
|
|
stopped, err := m.store.SessionStopped(ctx, sess.TaskId, sess.Id)
|
|
|
|
|
if err != nil || !stopped {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("%w: the human interrupted task %s's agent (or its last turn ended in an error) and hasn't messaged it since, so byre won't start a turn on it for you; put what you wanted to send or do to the human in chat instead", ErrConflict, sess.TaskId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) taskArg(ctx context.Context, raw json.RawMessage) (api.Task, api.Project, error) {
|
|
|
|
|
args := struct {
|
|
|
|
|
TaskID string `json:"task_id"`
|
|
|
|
|