fix(subagents): redact tool result snapshots
- commit
- 41cac35a205969cf5bb66c3ba66cc1989e6689bb
- author
- brice · · 2026-08-11 03:48 UTC
- parent
- 997f27c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +5 -33
M packages/agent/subagents/persist.go +1 -3
| @@ -790,9 +790,7 @@ func replayEventTranscript(a *Agent, ev Event) { | |||
| 790 | 790 | a.appendTranscript("tool: " + name) | |
| 791 | 791 | } | |
| 792 | 792 | case "tool_result", EventToolFinished: | |
| 793 | - | if text := eventContentText(ev.Data); text != "" { | |
| 794 | - | a.appendTranscript("tool result: " + truncate(strings.ReplaceAll(text, "\n", " "), 240)) | |
| 795 | - | } | |
| 793 | + | a.appendTranscript("tool result: completed") | |
| 796 | 794 | case "stdout": | |
| 797 | 795 | if txt, _ := ev.Data["text"].(string); txt != "" { | |
| 798 | 796 | a.appendTranscript(txt) | |
| @@ -790,9 +790,7 @@ func replayEventTranscript(a *Agent, ev Event) { | |||||
| 790 | a.appendTranscript("tool: " + name) | 790 | a.appendTranscript("tool: " + name) | ||
| 791 | } | 791 | } | ||
| 792 | case "tool_result", EventToolFinished: | 792 | case "tool_result", EventToolFinished: | ||
| 793 | - | if text := eventContentText(ev.Data); text != "" { | 793 | + | a.appendTranscript("tool result: completed") |
| 794 | - | a.appendTranscript("tool result: " + truncate(strings.ReplaceAll(text, "\n", " "), 240)) | |||
| 795 | - | } | |||
| 796 | case "stdout": | 794 | case "stdout": | ||
| 797 | if txt, _ := ev.Data["text"].(string); txt != "" { | 795 | if txt, _ := ev.Data["text"].(string); txt != "" { | ||
| 798 | a.appendTranscript(txt) | 796 | a.appendTranscript(txt) | ||
M packages/agent/subagents/runner.go +3 -29
| @@ -1009,9 +1009,9 @@ func applyEventToSink(ev Event, sink Sink) { | |||
| 1009 | 1009 | sink.Transcript("tool: " + name) | |
| 1010 | 1010 | } | |
| 1011 | 1011 | case "tool_result", EventToolFinished: | |
| 1012 | - | if text := eventContentText(ev.Data); text != "" { | |
| 1013 | - | sink.Transcript("tool result: " + truncate(strings.ReplaceAll(text, "\n", " "), 240)) | |
| 1014 | - | } | |
| 1012 | + | // Tool output can contain credentials, terminal controls, and arbitrary | |
| 1013 | + | // binary text. The dashboard only needs to show that the call completed. | |
| 1014 | + | sink.Transcript("tool result: completed") | |
| 1015 | 1015 | sink.Activity("idle") | |
| 1016 | 1016 | case "turn_end", EventTurnResult, EventAgentIdle: | |
| 1017 | 1017 | sink.Activity("idle") | |
| @@ -1028,32 +1028,6 @@ func applyEventToSink(ev Event, sink Sink) { | |||
| 1028 | 1028 | } | |
| 1029 | 1029 | } | |
| 1030 | 1030 | ||
| 1031 | - | // eventContentText extracts the human-readable text carried by a tool-result | |
| 1032 | - | // event. The event stream is permissive across provider adapters, so accept | |
| 1033 | - | // both the canonical block array and simple string payloads. | |
| 1034 | - | func eventContentText(data map[string]any) string { | |
| 1035 | - | for _, key := range []string{"content", "output", "text"} { | |
| 1036 | - | switch value := data[key].(type) { | |
| 1037 | - | case string: | |
| 1038 | - | if value != "" { | |
| 1039 | - | return value | |
| 1040 | - | } | |
| 1041 | - | case []any: | |
| 1042 | - | var text []string | |
| 1043 | - | for _, item := range value { | |
| 1044 | - | block, _ := item.(map[string]any) | |
| 1045 | - | if value, _ := block["text"].(string); value != "" { | |
| 1046 | - | text = append(text, value) | |
| 1047 | - | } | |
| 1048 | - | } | |
| 1049 | - | if len(text) > 0 { | |
| 1050 | - | return strings.Join(text, "\n") | |
| 1051 | - | } | |
| 1052 | - | } | |
| 1053 | - | } | |
| 1054 | - | return "" | |
| 1055 | - | } | |
| 1056 | - | ||
| 1057 | 1031 | // RunnerFunc adapts a plain function into a Runner. Useful for tests | |
| 1058 | 1032 | // and for callers who don't need their own type. | |
| 1059 | 1033 | type RunnerFunc func(ctx context.Context, sink Sink) error | |
| @@ -1009,9 +1009,9 @@ func applyEventToSink(ev Event, sink Sink) { | |||||
| 1009 | sink.Transcript("tool: " + name) | 1009 | sink.Transcript("tool: " + name) | ||
| 1010 | } | 1010 | } | ||
| 1011 | case "tool_result", EventToolFinished: | 1011 | case "tool_result", EventToolFinished: | ||
| 1012 | - | if text := eventContentText(ev.Data); text != "" { | 1012 | + | // Tool output can contain credentials, terminal controls, and arbitrary |
| 1013 | - | sink.Transcript("tool result: " + truncate(strings.ReplaceAll(text, "\n", " "), 240)) | 1013 | + | // binary text. The dashboard only needs to show that the call completed. |
| 1014 | - | } | 1014 | + | sink.Transcript("tool result: completed") |
| 1015 | sink.Activity("idle") | 1015 | sink.Activity("idle") | ||
| 1016 | case "turn_end", EventTurnResult, EventAgentIdle: | 1016 | case "turn_end", EventTurnResult, EventAgentIdle: | ||
| 1017 | sink.Activity("idle") | 1017 | sink.Activity("idle") | ||
| @@ -1028,32 +1028,6 @@ func applyEventToSink(ev Event, sink Sink) { | |||||
| 1028 | } | 1028 | } | ||
| 1029 | } | 1029 | } | ||
| 1030 | 1030 | ||||
| 1031 | - | // eventContentText extracts the human-readable text carried by a tool-result | |||
| 1032 | - | // event. The event stream is permissive across provider adapters, so accept | |||
| 1033 | - | // both the canonical block array and simple string payloads. | |||
| 1034 | - | func eventContentText(data map[string]any) string { | |||
| 1035 | - | for _, key := range []string{"content", "output", "text"} { | |||
| 1036 | - | switch value := data[key].(type) { | |||
| 1037 | - | case string: | |||
| 1038 | - | if value != "" { | |||
| 1039 | - | return value | |||
| 1040 | - | } | |||
| 1041 | - | case []any: | |||
| 1042 | - | var text []string | |||
| 1043 | - | for _, item := range value { | |||
| 1044 | - | block, _ := item.(map[string]any) | |||
| 1045 | - | if value, _ := block["text"].(string); value != "" { | |||
| 1046 | - | text = append(text, value) | |||
| 1047 | - | } | |||
| 1048 | - | } | |||
| 1049 | - | if len(text) > 0 { | |||
| 1050 | - | return strings.Join(text, "\n") | |||
| 1051 | - | } | |||
| 1052 | - | } | |||
| 1053 | - | } | |||
| 1054 | - | return "" | |||
| 1055 | - | } | |||
| 1056 | - | ||||
| 1057 | // RunnerFunc adapts a plain function into a Runner. Useful for tests | 1031 | // RunnerFunc adapts a plain function into a Runner. Useful for tests | ||
| 1058 | // and for callers who don't need their own type. | 1032 | // and for callers who don't need their own type. | ||
| 1059 | type RunnerFunc func(ctx context.Context, sink Sink) error | 1033 | type RunnerFunc func(ctx context.Context, sink Sink) error | ||
M packages/agent/subagents/runner_test.go +1 -1
| @@ -808,7 +808,7 @@ func TestApplyEventToSinkProjectsToolActivityIntoTranscript(t *testing.T) { | |||
| 808 | 808 | "content": []any{map[string]any{"text": "command output"}}, | |
| 809 | 809 | }}, | |
| 810 | 810 | } | |
| 811 | - | want := []string{"tool: bash", "tool result: command output"} | |
| 811 | + | want := []string{"tool: bash", "tool result: completed"} | |
| 812 | 812 | ||
| 813 | 813 | live := &Agent{} | |
| 814 | 814 | for _, event := range events { | |
| @@ -808,7 +808,7 @@ func TestApplyEventToSinkProjectsToolActivityIntoTranscript(t *testing.T) { | |||||
| 808 | "content": []any{map[string]any{"text": "command output"}}, | 808 | "content": []any{map[string]any{"text": "command output"}}, | ||
| 809 | }}, | 809 | }}, | ||
| 810 | } | 810 | } | ||
| 811 | - | want := []string{"tool: bash", "tool result: command output"} | 811 | + | want := []string{"tool: bash", "tool result: completed"} |
| 812 | 812 | ||||
| 813 | live := &Agent{} | 813 | live := &Agent{} | ||
| 814 | for _, event := range events { | 814 | for _, event := range events { | ||