-
Notifications
You must be signed in to change notification settings - Fork 3.6k
feat(tools): add SpawnStatusTool for reporting subagent statuses #1540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e60b456
feat(tools): add SpawnStatusTool for reporting subagent statuses
SHINE-six 2ce7aeb
feat(tools): enhance SpawnStatusTool to restrict task visibility by cβ¦
SHINE-six af6ef94
feat(tests): add Unicode result truncation and channel filtering testβ¦
SHINE-six 100da45
Potential fix for pull request finding
SHINE-six 6d37001
feat(tools): enhance SpawnStatusTool with task ID validation and sortβ¦
SHINE-six eb8c193
Merge branch 'main' of https://github.com/SHINE-six/picoclaw
SHINE-six e7518e8
feat(tools): update SpawnStatusTool description and parameter documenβ¦
SHINE-six de6a04b
refactor(tests): improve comments for clarity in ChannelFiltering tesβ¦
SHINE-six 9a2cf38
fix(tools): update no subagents message for clarity and remove unneceβ¦
SHINE-six 12f570e
fix(tools): improve description clarity for SpawnStatusTool regardingβ¦
SHINE-six 70b9676
feat(tools): add spawn_status tool configuration and registration
SHINE-six 7614f03
Potential fix for pull request finding
SHINE-six 521db34
fix(agent): improve subagent management for spawn and spawn_status tools
SHINE-six a8f7c86
Potential fix for pull request finding
SHINE-six 04d7d03
Merge branch 'main' of https://github.com/SHINE-six/picoclaw
SHINE-six c908b84
Potential fix for pull request finding
lxowalle 4f3b7c6
fix(tests): update ResultTruncation_Unicode test to use valid CJK chaβ¦
SHINE-six File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| package tools | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| // SpawnStatusTool reports the status of subagents that were spawned via the | ||
| // spawn tool. It can query a specific task by ID, or list every known task with | ||
| // a summary count broken-down by status. | ||
| type SpawnStatusTool struct { | ||
| manager *SubagentManager | ||
| } | ||
|
|
||
| // NewSpawnStatusTool creates a SpawnStatusTool backed by the given manager. | ||
| func NewSpawnStatusTool(manager *SubagentManager) *SpawnStatusTool { | ||
| return &SpawnStatusTool{manager: manager} | ||
| } | ||
|
|
||
| func (t *SpawnStatusTool) Name() string { | ||
| return "spawn_status" | ||
| } | ||
|
|
||
| func (t *SpawnStatusTool) Description() string { | ||
| return "Get the status of spawned subagents. " + | ||
| "Returns a list of all subagents and their current state " + | ||
| "(running, completed, failed, or canceled), or retrieves details " + | ||
| "for a specific subagent task when task_id is provided. " + | ||
| "Results are scoped to the current conversation's channel and chat ID; " + | ||
| "all tasks are listed only when no channel/chat context is injected " + | ||
| "(e.g. direct programmatic calls via Execute)." | ||
| } | ||
SHINE-six marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| func (t *SpawnStatusTool) Parameters() map[string]any { | ||
| return map[string]any{ | ||
| "type": "object", | ||
| "properties": map[string]any{ | ||
| "task_id": map[string]any{ | ||
| "type": "string", | ||
| "description": "Optional task ID (e.g. \"subagent-1\") to inspect a specific " + | ||
| "subagent. When omitted, all visible subagents are listed.", | ||
| }, | ||
SHINE-six marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| "required": []string{}, | ||
| } | ||
| } | ||
|
|
||
| func (t *SpawnStatusTool) Execute(ctx context.Context, args map[string]any) *ToolResult { | ||
| if t.manager == nil { | ||
| return ErrorResult("Subagent manager not configured") | ||
| } | ||
|
|
||
| // Derive the calling conversation's identity so we can scope results to the | ||
| // current chat only β preventing cross-conversation task leakage in | ||
| // multi-user deployments. | ||
| callerChannel := ToolChannel(ctx) | ||
| callerChatID := ToolChatID(ctx) | ||
|
|
||
SHINE-six marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| var taskID string | ||
| if rawTaskID, ok := args["task_id"]; ok && rawTaskID != nil { | ||
| taskIDStr, ok := rawTaskID.(string) | ||
| if !ok { | ||
| return ErrorResult("task_id must be a string") | ||
| } | ||
| taskID = strings.TrimSpace(taskIDStr) | ||
| } | ||
|
|
||
| if taskID != "" { | ||
| // GetTaskCopy returns a consistent snapshot under the manager lock, | ||
| // eliminating any data race with the concurrent subagent goroutine. | ||
| taskCopy, ok := t.manager.GetTaskCopy(taskID) | ||
| if !ok { | ||
| return ErrorResult(fmt.Sprintf("No subagent found with task ID: %s", taskID)) | ||
| } | ||
|
|
||
| // Restrict lookup to tasks that belong to this conversation. | ||
| if callerChannel != "" && taskCopy.OriginChannel != "" && taskCopy.OriginChannel != callerChannel { | ||
| return ErrorResult(fmt.Sprintf("No subagent found with task ID: %s", taskID)) | ||
| } | ||
| if callerChatID != "" && taskCopy.OriginChatID != "" && taskCopy.OriginChatID != callerChatID { | ||
| return ErrorResult(fmt.Sprintf("No subagent found with task ID: %s", taskID)) | ||
| } | ||
|
|
||
| return NewToolResult(spawnStatusFormatTask(&taskCopy)) | ||
| } | ||
SHINE-six marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // ListTaskCopies returns consistent snapshots under the manager lock. | ||
| origTasks := t.manager.ListTaskCopies() | ||
| if len(origTasks) == 0 { | ||
| return NewToolResult("No subagents have been spawned yet.") | ||
| } | ||
|
|
||
| tasks := make([]*SubagentTask, 0, len(origTasks)) | ||
| for i := range origTasks { | ||
| cpy := &origTasks[i] | ||
|
|
||
| // Filter to tasks that originate from the current conversation only. | ||
| if callerChannel != "" && cpy.OriginChannel != "" && cpy.OriginChannel != callerChannel { | ||
| continue | ||
| } | ||
| if callerChatID != "" && cpy.OriginChatID != "" && cpy.OriginChatID != callerChatID { | ||
| continue | ||
| } | ||
|
|
||
| tasks = append(tasks, cpy) | ||
| } | ||
|
|
||
| if len(tasks) == 0 { | ||
| return NewToolResult("No subagents found for this conversation.") | ||
| } | ||
|
|
||
| // Order by creation time (ascending) so spawning order is preserved. | ||
| // Fall back to ID string for tasks created in the same millisecond. | ||
| sort.Slice(tasks, func(i, j int) bool { | ||
| if tasks[i].Created != tasks[j].Created { | ||
| return tasks[i].Created < tasks[j].Created | ||
| } | ||
| return tasks[i].ID < tasks[j].ID | ||
| }) | ||
|
|
||
| counts := map[string]int{} | ||
| for _, task := range tasks { | ||
| counts[task.Status]++ | ||
| } | ||
|
|
||
| var sb strings.Builder | ||
| sb.WriteString(fmt.Sprintf("Subagent status report (%d total):\n", len(tasks))) | ||
| for _, status := range []string{"running", "completed", "failed", "canceled"} { | ||
| if n := counts[status]; n > 0 { | ||
| label := strings.ToUpper(status[:1]) + status[1:] + ":" | ||
| sb.WriteString(fmt.Sprintf(" %-10s %d\n", label, n)) | ||
| } | ||
| } | ||
| sb.WriteString("\n") | ||
|
|
||
| for _, task := range tasks { | ||
| sb.WriteString(spawnStatusFormatTask(task)) | ||
| sb.WriteString("\n\n") | ||
| } | ||
|
|
||
| return NewToolResult(strings.TrimRight(sb.String(), "\n")) | ||
| } | ||
|
|
||
| // spawnStatusFormatTask renders a single SubagentTask as a human-readable block. | ||
| func spawnStatusFormatTask(task *SubagentTask) string { | ||
| var sb strings.Builder | ||
|
|
||
| header := fmt.Sprintf("[%s] status=%s", task.ID, task.Status) | ||
| if task.Label != "" { | ||
| header += fmt.Sprintf(" label=%q", task.Label) | ||
| } | ||
| if task.AgentID != "" { | ||
| header += fmt.Sprintf(" agent=%s", task.AgentID) | ||
| } | ||
| if task.Created > 0 { | ||
| created := time.UnixMilli(task.Created).UTC().Format("2006-01-02 15:04:05 UTC") | ||
| header += fmt.Sprintf(" created=%s", created) | ||
| } | ||
| sb.WriteString(header) | ||
|
|
||
| if task.Task != "" { | ||
| sb.WriteString(fmt.Sprintf("\n task: %s", task.Task)) | ||
| } | ||
| if task.Result != "" { | ||
| result := task.Result | ||
| const maxResultLen = 300 | ||
| runes := []rune(result) | ||
| if len(runes) > maxResultLen { | ||
| result = string(runes[:maxResultLen]) + "β¦" | ||
| } | ||
| sb.WriteString(fmt.Sprintf("\n result: %s", result)) | ||
| } | ||
|
|
||
| return sb.String() | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say this is a deliberate design choice, not a bug. spawn_status is a read-only companion to spawn β it has no independent purpose without the spawn tool. The pattern of
registering a status/query tool alongside its parent tool is reasonable. Other examples in the codebase show similar patterns (e.g., find_skills is tightly coupled to skills).
Adding a separate config toggle for spawn_status would be over-engineering β no user would want spawn enabled but spawn_status disabled. The current behavior
(implicitly enabled with spawn) is the correct default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @SHINE-six , this tool is not necessary for all users. Please add a configuration option to enable whether this tool should be used and default is false.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lxowalle Since spawn and subagent is default to true, I think for this to default to true will be better? Often time usage with spawn (async), user/agents would need to check on it for task status progress
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hope tool list can be kept as small as possible, retaining only the necessary tools. I believe that in most cases, retrieving the status of subagents is only necessary when encountering issues. @SHINE-six