-
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
+641
β11
Merged
Changes from all 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
Some comments aren't visible on the classic Files Changed page.
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)." | ||
| } | ||
|
|
||
| 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.
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.
Uh oh!
There was an error while loading. Please reload this page.