Files
kandev/apps/backend/internal/task/models/message_shell_output.go
T
2026-07-16 21:44:24 +01:00

164 lines
4.5 KiB
Go

package models
import "github.com/kandev/kandev/internal/agentctl/types/streams"
// ShellExecOutputSnapshot is the full bounded output stored on a shell message.
type ShellExecOutputSnapshot struct {
ExitCode *int `json:"exit_code,omitempty"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
Truncated bool `json:"truncated,omitempty"`
}
// ProjectMessageMetadata removes shell output bodies from client message payloads.
// It copies the normalized shell path so repository-owned metadata is never mutated.
func ProjectMessageMetadata(metadata map[string]any) map[string]any {
output, ok := shellOutputFromMetadata(metadata)
if !ok {
return metadata
}
normalized, ok := projectNormalizedShell(metadata["normalized"], shellOutputSummary(output))
if !ok {
return metadata
}
projected := copyMetadata(metadata)
projected["normalized"] = normalized
return projected
}
// ExtractShellExecOutput returns the full stored snapshot for normalized shell metadata.
func ExtractShellExecOutput(metadata map[string]any) (ShellExecOutputSnapshot, bool) {
return shellOutputFromMetadata(metadata)
}
func shellOutputFromMetadata(metadata map[string]any) (ShellExecOutputSnapshot, bool) {
if metadata == nil {
return ShellExecOutputSnapshot{}, false
}
raw, ok := metadata["normalized"]
if !ok || raw == nil {
return ShellExecOutputSnapshot{}, false
}
switch normalized := raw.(type) {
case *streams.NormalizedPayload:
if normalized == nil || normalized.ShellExec() == nil {
return ShellExecOutputSnapshot{}, false
}
return shellOutputFromTyped(normalized.ShellExec().Output), true
case map[string]any:
shell, shellOK := normalized["shell_exec"].(map[string]any)
if !shellOK {
return ShellExecOutputSnapshot{}, false
}
return shellOutputFromMap(shell["output"]), true
default:
return ShellExecOutputSnapshot{}, false
}
}
func shellOutputFromTyped(output *streams.ShellExecOutput) ShellExecOutputSnapshot {
if output == nil {
return ShellExecOutputSnapshot{}
}
return ShellExecOutputSnapshot{
ExitCode: output.ExitCode,
Stdout: output.Stdout,
Stderr: output.Stderr,
Truncated: output.Truncated,
}
}
func shellOutputFromMap(raw any) ShellExecOutputSnapshot {
output, ok := raw.(map[string]any)
if !ok {
return ShellExecOutputSnapshot{}
}
result := ShellExecOutputSnapshot{
Stdout: StringFromAny(output["stdout"]),
Stderr: StringFromAny(output["stderr"]),
Truncated: boolFromAny(output["truncated"]),
}
if exitCode, ok := intFromAny(output["exit_code"]); ok {
result.ExitCode = &exitCode
}
return result
}
func shellOutputSummary(output ShellExecOutputSnapshot) map[string]any {
summary := map[string]any{
"has_output": output.Stdout != "" || output.Stderr != "",
"stdout_bytes": len(output.Stdout),
"stderr_bytes": len(output.Stderr),
"truncated": output.Truncated,
}
if output.ExitCode != nil {
summary["exit_code"] = *output.ExitCode
}
return summary
}
func projectNormalizedShell(raw any, summary map[string]any) (map[string]any, bool) {
switch normalized := raw.(type) {
case *streams.NormalizedPayload:
if normalized == nil || normalized.ShellExec() == nil {
return nil, false
}
return map[string]any{
"kind": string(streams.ToolKindShellExec),
"shell_exec": projectTypedShell(normalized.ShellExec(), summary),
}, true
case map[string]any:
shell, ok := normalized["shell_exec"].(map[string]any)
if !ok {
return nil, false
}
projectedShell := copyMetadata(shell)
projectedShell["output"] = summary
projected := copyMetadata(normalized)
projected["shell_exec"] = projectedShell
return projected, true
default:
return nil, false
}
}
func projectTypedShell(shell *streams.ShellExecPayload, summary map[string]any) map[string]any {
projected := map[string]any{
"command": shell.Command,
"output": summary,
}
if shell.WorkDir != "" {
projected["work_dir"] = shell.WorkDir
}
if shell.Description != "" {
projected["description"] = shell.Description
}
if shell.Timeout != 0 {
projected["timeout"] = shell.Timeout
}
if shell.Background {
projected["background"] = true
}
return projected
}
func intFromAny(raw any) (int, bool) {
switch value := raw.(type) {
case int:
return value, true
case int32:
return int(value), true
case int64:
return int(value), true
case float64:
return int(value), value == float64(int(value))
default:
return 0, false
}
}
func boolFromAny(raw any) bool {
value, _ := raw.(bool)
return value
}