Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

842 lines
22 KiB
Go

package canonical
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
var ErrToolCallPayloadTooLarge = errors.New("canonical tool call payload exceeds byte budget")
// ToolCallPayloadBudgetError reports a canonical payload that cannot fit even
// after every truncatable output string has been reduced to its marker.
type ToolCallPayloadBudgetError struct {
EncodedBytes int
MaxBytes int
}
func (e *ToolCallPayloadBudgetError) Error() string {
return fmt.Sprintf("%v: encoded bytes=%d max bytes=%d", ErrToolCallPayloadTooLarge, e.EncodedBytes, e.MaxBytes)
}
func (*ToolCallPayloadBudgetError) Unwrap() error { return ErrToolCallPayloadTooLarge }
func IsToolCallPayloadTooLarge(err error) bool {
return errors.Is(err, ErrToolCallPayloadTooLarge)
}
var canonicalToolPayloadKeys = map[string]struct{}{
"activityKind": {},
"approvalPurpose": {},
"callId": {},
"callType": {},
"changes": {},
"command": {},
"cwd": {},
"description": {},
"detailedContent": {},
"error": {},
"exitCode": {},
"fileChangeKind": {},
"fileChanges": {},
"filePath": {},
"input": {},
"locations": {},
"metadata": {},
"name": {},
"output": {},
"ownerCallId": {},
"ownerThreadId": {},
"options": {},
"parentCallId": {},
"paths": {},
"plan": {},
"provider": {},
"requestId": {},
"rootCallId": {},
"seq": {},
"sessionID": {},
"source": {},
"status": {},
"steps": {},
"structuredPatch": {},
"summary": {},
"title": {},
"toolName": {},
}
var canonicalToolBodyKeys = map[string]struct{}{
"action": {},
"answers": {},
"answersByQuestionId": {},
"changes": {},
"commandName": {},
"cwd": {},
"detailedContent": {},
"diff": {},
"duration": {},
"durationMs": {},
"exitCode": {},
"file": {},
"fileChanges": {},
"filePath": {},
"filenames": {},
"files": {},
"imageMimeType": {},
"isError": {},
"links": {},
"matches": {},
"message": {},
"mode": {},
"newString": {},
"oldString": {},
"patch": {},
"payload": {},
"requestId": {},
"reason": {},
"savedPath": {},
"savedPaths": {},
"selectedId": {},
"status": {},
"stderr": {},
"stdout": {},
"steps": {},
"structuredPatch": {},
"structuredContent": {},
"success": {},
"summary": {},
"text": {},
"totalDeferredTools": {},
"type": {},
}
var canonicalToolMetadataKeys = map[string]struct{}{
"agentId": {},
"approvalPurpose": {},
"async": {},
"callType": {},
"childSessionID": {},
"child_session_id": {},
"cwd": {},
"durationMs": {},
"interactiveKind": {},
"kind": {},
"mcpServer": {},
"options": {},
"outputFile": {},
"parentToolUseId": {},
"server": {},
"serverName": {},
"steps": {},
"subagentAgentId": {},
"subagentAsync": {},
"subagentOutputFile": {},
"subagentSessionID": {},
"subagentStatus": {},
"subagent_session_id": {},
"taskId": {},
"taskStatus": {},
"tool": {},
"toolName": {},
}
// CompactToolCallPayload turns provider-shaped tool data into the canonical
// stored business projection. Callers that persist its result must use
// CompactToolCallPayloadChecked so an impossible budget is not ignored.
func CompactToolCallPayload(status string, payload map[string]any) map[string]any {
result, _ := CompactToolCallPayloadChecked(status, payload)
return result
}
// CompactToolCallPayloadChecked returns an error instead of allowing a
// replication-unsafe payload to be persisted when required non-truncatable
// data alone exceeds the byte budget.
func CompactToolCallPayloadChecked(status string, payload map[string]any) (map[string]any, error) {
if len(payload) == 0 {
return payload, nil
}
result := cloneToolMap(payload)
input := compactToolInput(result["input"])
output := compactToolBody(result["output"])
toolError := compactToolBody(result["error"])
metadata := compactToolMetadata(result["metadata"])
steps := compactToolSteps(result["steps"])
if len(steps) == 0 {
steps = compactToolSteps(output["steps"])
}
if len(steps) == 0 {
steps = compactToolSteps(metadata["steps"])
}
delete(output, "steps")
delete(toolError, "steps")
delete(metadata, "steps")
projection := toolContentProjection{}
projection.add(result["content"])
if len(projection.texts) > 0 {
target := &output
if isFailedToolStatus(status) {
target = &toolError
}
if *target == nil {
*target = map[string]any{}
}
if toolString((*target)["text"]) == "" {
(*target)["text"] = strings.Join(projection.texts, "\n")
}
}
if len(projection.toolReferences) > 0 {
if output == nil {
output = map[string]any{}
}
if len(toolStringSlice(output["matches"])) == 0 {
output["matches"] = stringsToAny(projection.toolReferences)
}
if _, exists := output["totalDeferredTools"]; !exists {
output["totalDeferredTools"] = len(projection.toolReferences)
}
}
if len(projection.imagePaths) > 0 {
if output == nil {
output = map[string]any{}
}
savedPaths := append(toolStringSlice(output["savedPaths"]), projection.imagePaths...)
savedPaths = uniqueToolStrings(savedPaths)
output["savedPaths"] = stringsToAny(savedPaths)
if toolString(output["savedPath"]) == "" {
output["savedPath"] = savedPaths[0]
}
if toolString(output["imageMimeType"]) == "" && projection.imageMimeType != "" {
output["imageMimeType"] = projection.imageMimeType
}
}
if len(projection.fileChanges) > 0 {
result["fileChanges"] = mergeToolFileChanges(result["fileChanges"], projection.fileChanges)
}
if fileChanges := normalizeToolFileChanges(result["fileChanges"]); fileChanges != nil {
result["fileChanges"] = fileChanges
} else {
delete(result, "fileChanges")
}
// Compare aliases before per-field truncation can make equal source
// strings diverge at the byte boundary.
if isTerminalToolStatus(status) && isTerminalCommandPayload(result, input) {
compactTerminalCommandBodyAlias(output)
compactTerminalCommandBodyAlias(toolError)
}
CompactToolStructuredContentAliases(map[string]any{
"output": output,
"error": toolError,
"steps": steps,
})
if output != nil {
delete(output, "content")
output = TruncateToolOutputBody(selectToolKeys(output, canonicalToolBodyKeys))
}
if toolError != nil {
delete(toolError, "content")
toolError = TruncateToolOutputBody(selectToolKeys(toolError, canonicalToolBodyKeys))
}
delete(result, "content")
delete(result, "error")
delete(result, "input")
delete(result, "metadata")
delete(result, "output")
delete(result, "steps")
if len(input) > 0 {
result["input"] = input
}
if len(output) > 0 {
result["output"] = output
}
if len(toolError) > 0 && !isCompletedToolStatus(status) {
result["error"] = toolError
}
if len(metadata) > 0 {
result["metadata"] = metadata
}
if len(steps) > 0 {
result["steps"] = steps
}
result = selectToolKeys(result, canonicalToolPayloadKeys)
CompactTerminalCommandOutputAliases(status, result)
CompactToolStructuredContentAliases(result)
_, fits := FitToolCallPayloadOutputBudget(result, ToolCallPayloadMaxBytes)
encoded, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("encode canonical tool call payload: %w", err)
}
if !fits || len(encoded) > ToolCallPayloadMaxBytes {
return nil, &ToolCallPayloadBudgetError{
EncodedBytes: len(encoded),
MaxBytes: ToolCallPayloadMaxBytes,
}
}
return result, nil
}
func compactToolSteps(value any) []any {
rawSteps, ok := value.([]any)
if !ok {
return nil
}
steps := make([]any, 0, len(rawSteps))
for _, value := range rawSteps {
rawStep := toolMap(value)
if rawStep == nil {
continue
}
payload := toolMap(rawStep["payload"])
input := compactToolInput(firstToolValue(
rawStep["toolInput"],
rawStep["tool_input"],
payload["input"],
))
output := compactToolBody(firstToolValue(
rawStep["toolResult"],
rawStep["tool_result"],
payload["output"],
))
toolError := compactToolBody(firstToolValue(
rawStep["toolError"],
rawStep["tool_error"],
payload["error"],
))
metadata := compactToolMetadata(firstToolValue(
rawStep["metadata"],
payload["metadata"],
))
status := firstToolString(rawStep["status"], output["status"], toolError["status"])
step := map[string]any{}
for _, key := range []string{
"id",
"toolUseId",
"name",
"toolName",
"callType",
"status",
"summary",
"locations",
"occurredAtUnixMs",
} {
if rawStep[key] != nil {
step[key] = cloneToolValue(rawStep[key])
}
}
if step["toolUseId"] == nil && rawStep["tool_use_id"] != nil {
step["toolUseId"] = cloneToolValue(rawStep["tool_use_id"])
}
if step["toolName"] == nil && rawStep["tool_name"] != nil {
step["toolName"] = cloneToolValue(rawStep["tool_name"])
}
if step["callType"] == nil && rawStep["call_type"] != nil {
step["callType"] = cloneToolValue(rawStep["call_type"])
}
if status != "" {
step["status"] = status
}
if len(input) > 0 {
step["toolInput"] = input
}
if len(metadata) > 0 {
step["metadata"] = metadata
}
if isTerminalToolStatus(status) && isTerminalCommandPayload(step, input) {
compactTerminalCommandBodyAlias(output)
compactTerminalCommandBodyAlias(toolError)
}
output = TruncateToolOutputBody(output)
toolError = TruncateToolOutputBody(toolError)
if len(output) > 0 {
step["toolResult"] = output
}
if len(toolError) > 0 && !isCompletedToolStatus(status) {
step["toolError"] = toolError
}
if fileChanges := normalizeToolFileChanges(firstToolValue(rawStep["fileChanges"], payload["fileChanges"])); fileChanges != nil {
step["fileChanges"] = fileChanges
}
if len(step) > 0 {
steps = append(steps, step)
}
}
return steps
}
func compactToolBody(value any) map[string]any {
body := toolBodyMap(value)
if body == nil {
return nil
}
for _, rawKey := range []string{"rawOutput", "raw_output"} {
switch raw := body[rawKey].(type) {
case map[string]any:
body = mergeMissingToolValues(body, raw)
case string:
if toolString(body["text"]) == "" && strings.TrimSpace(raw) != "" {
body["text"] = raw
}
}
delete(body, rawKey)
}
if metadata := toolMap(body["metadata"]); len(metadata) > 0 {
for _, key := range []string{"changes", "detailedContent", "diff", "files", "steps", "structuredPatch"} {
if _, exists := body[key]; !exists && metadata[key] != nil {
body[key] = cloneToolValue(metadata[key])
}
}
if _, exists := body["exitCode"]; !exists && metadata["exit"] != nil {
body["exitCode"] = cloneToolValue(metadata["exit"])
}
}
projection := toolContentProjection{}
projection.add(body["content"])
if toolString(body["text"]) == "" {
if text := firstToolBodyText(body, projection.texts); text != "" {
body["text"] = text
}
}
if len(toolStringSlice(body["matches"])) == 0 && len(projection.toolReferences) > 0 {
body["matches"] = stringsToAny(projection.toolReferences)
if _, exists := body["totalDeferredTools"]; !exists {
body["totalDeferredTools"] = len(projection.toolReferences)
}
}
if len(projection.imagePaths) > 0 {
savedPaths := append(toolStringSlice(body["savedPaths"]), projection.imagePaths...)
savedPaths = uniqueToolStrings(savedPaths)
body["savedPaths"] = stringsToAny(savedPaths)
if toolString(body["savedPath"]) == "" {
body["savedPath"] = savedPaths[0]
}
if projection.imageMimeType != "" {
body["imageMimeType"] = projection.imageMimeType
}
}
if len(projection.fileChanges) > 0 {
body["fileChanges"] = mergeToolFileChanges(body["fileChanges"], projection.fileChanges)
}
if fileChanges := normalizeToolFileChanges(body["fileChanges"]); fileChanges != nil {
body["fileChanges"] = fileChanges
} else {
delete(body, "fileChanges")
}
if value, exists := body["exit_code"]; exists {
if _, canonicalExists := body["exitCode"]; !canonicalExists {
body["exitCode"] = cloneToolValue(value)
}
}
if value, exists := body["duration_ms"]; exists {
if _, canonicalExists := body["durationMs"]; !canonicalExists {
body["durationMs"] = cloneToolValue(value)
}
}
if value, exists := body["saved_path"]; exists && toolString(body["savedPath"]) == "" {
body["savedPath"] = cloneToolValue(value)
}
if value, exists := body["total_deferred_tools"]; exists {
if _, canonicalExists := body["totalDeferredTools"]; !canonicalExists {
body["totalDeferredTools"] = cloneToolValue(value)
}
}
if steps := compactToolSteps(body["steps"]); len(steps) > 0 {
body["steps"] = steps
} else {
delete(body, "steps")
}
delete(body, "_meta")
delete(body, "content")
delete(body, "metadata")
delete(body, "toolResponse")
return selectToolKeys(body, canonicalToolBodyKeys)
}
func compactToolMetadata(value any) map[string]any {
metadata := toolMap(value)
if metadata == nil {
return nil
}
delete(metadata, "adapter")
delete(metadata, "fileChange")
if steps := compactToolSteps(metadata["steps"]); len(steps) > 0 {
metadata["steps"] = steps
} else {
delete(metadata, "steps")
}
return selectToolKeys(metadata, canonicalToolMetadataKeys)
}
func firstToolBodyText(body map[string]any, contentTexts []string) string {
for _, key := range []string{
"output",
"aggregated_output",
"formatted_output",
"result",
} {
if text := toolString(body[key]); text != "" {
return text
}
}
if len(contentTexts) > 0 {
return strings.Join(contentTexts, "\n")
}
return ""
}
type toolContentProjection struct {
texts []string
toolReferences []string
imagePaths []string
imageMimeType string
fileChanges []map[string]any
}
func (projection *toolContentProjection) add(value any) {
switch typed := value.(type) {
case []any:
for _, item := range typed {
projection.add(item)
}
case map[string]any:
itemType := strings.ToLower(toolString(typed["type"]))
nested := toolMap(typed["content"])
nestedType := strings.ToLower(toolString(nested["type"]))
if text := toolString(typed["text"]); text != "" {
projection.texts = appendUniqueToolString(projection.texts, text)
}
if text := toolString(typed["content"]); text != "" {
projection.texts = appendUniqueToolString(projection.texts, text)
}
if nested != nil {
if text := toolString(nested["text"]); text != "" {
projection.texts = appendUniqueToolString(projection.texts, text)
}
}
if itemType == "tool_reference" {
if name := firstToolString(typed["tool_name"], typed["toolName"], typed["name"]); name != "" {
projection.toolReferences = appendUniqueToolString(projection.toolReferences, name)
}
}
if itemType == "image" || nestedType == "image" {
image := typed
if nestedType == "image" {
image = nested
}
if path := firstToolString(image["uri"], image["path"]); path != "" && !strings.HasPrefix(strings.ToLower(path), "data:image/") {
projection.imagePaths = appendUniqueToolString(projection.imagePaths, path)
}
if projection.imageMimeType == "" {
projection.imageMimeType = firstToolString(image["mimeType"], image["media_type"], image["mediaType"])
}
}
if itemType == "file_change" || itemType == "diff" {
projection.fileChanges = append(projection.fileChanges, toolFileChangesFromContent(typed)...)
}
if content, ok := typed["content"].([]any); ok {
projection.add(content)
}
}
}
func toolFileChangesFromContent(value map[string]any) []map[string]any {
paths := toolStringSlice(value["paths"])
if path := firstToolString(value["path"], value["filePath"], value["file_path"]); path != "" {
paths = append([]string{path}, paths...)
}
paths = uniqueToolStrings(paths)
if len(paths) == 0 {
return nil
}
oldString, hasOld := firstPresentToolString(value["oldString"], value["old_string"], value["oldText"])
newString, hasNew := firstPresentToolString(value["newString"], value["new_string"], value["newText"])
content, hasContent := firstPresentToolString(value["content"])
change := firstToolChange(value["change"], value["status"], value["kind"], value["type"])
if change == "" {
switch {
case hasOld && !hasNew:
change = "deleted"
case hasNew && (!hasOld || oldString == ""):
change = "added"
case hasContent:
change = "added"
default:
change = "modified"
}
}
files := make([]map[string]any, 0, len(paths))
for _, path := range paths {
file := map[string]any{"path": path, "change": change}
if hasOld {
file["oldString"] = oldString
}
if hasNew {
file["newString"] = newString
}
if hasContent {
file["content"] = content
}
for _, key := range []string{"diff", "patch", "unifiedDiff", "unified_diff"} {
if text, ok := value[key].(string); ok {
file[key] = text
}
}
files = append(files, file)
}
return files
}
func mergeToolFileChanges(existing any, incoming []map[string]any) map[string]any {
filesByPath := map[string]map[string]any{}
order := make([]string, 0)
if existingMap := normalizeToolFileChanges(existing); existingMap != nil {
for _, raw := range toolFileChangeMaps(existingMap["files"]) {
path := toolString(raw["path"])
if path == "" {
continue
}
order = append(order, path)
filesByPath[path] = raw
}
}
for _, raw := range incoming {
file := normalizeToolFileChange(raw)
if file == nil {
continue
}
path := toolString(file["path"])
if path == "" {
continue
}
if current, exists := filesByPath[path]; exists {
filesByPath[path] = mergeToolFileChangeValues(current, file)
continue
}
order = append(order, path)
filesByPath[path] = cloneToolMap(file)
}
files := make([]any, 0, len(order))
for _, path := range order {
files = append(files, filesByPath[path])
}
if len(files) == 0 {
return nil
}
return map[string]any{"files": files}
}
func toolBodyMap(value any) map[string]any {
switch typed := value.(type) {
case map[string]any:
return cloneToolMap(typed)
case string:
if strings.TrimSpace(typed) == "" {
return nil
}
return map[string]any{"text": typed}
case nil:
return nil
default:
return map[string]any{"text": fmt.Sprint(typed)}
}
}
func toolMap(value any) map[string]any {
typed, ok := value.(map[string]any)
if !ok || len(typed) == 0 {
return nil
}
return cloneToolMap(typed)
}
func cloneToolMap(input map[string]any) map[string]any {
if len(input) == 0 {
return nil
}
output := make(map[string]any, len(input))
for key, value := range input {
output[key] = cloneToolValue(value)
}
return output
}
func cloneToolValue(value any) any {
switch typed := value.(type) {
case map[string]any:
return cloneToolMap(typed)
case []any:
output := make([]any, len(typed))
for index, item := range typed {
output[index] = cloneToolValue(item)
}
return output
default:
return typed
}
}
func mergeMissingToolValues(current map[string]any, fallback map[string]any) map[string]any {
result := cloneToolMap(current)
if result == nil {
result = map[string]any{}
}
for key, value := range fallback {
if _, exists := result[key]; !exists {
result[key] = cloneToolValue(value)
}
}
return result
}
func selectToolKeys(input map[string]any, allowed map[string]struct{}) map[string]any {
if len(input) == 0 {
return nil
}
output := make(map[string]any)
for key, value := range input {
if _, ok := allowed[key]; !ok || toolValueEmpty(value) {
continue
}
output[key] = value
}
if len(output) == 0 {
return nil
}
return output
}
func toolValueEmpty(value any) bool {
switch typed := value.(type) {
case nil:
return true
case string:
return strings.TrimSpace(typed) == ""
case map[string]any:
return len(typed) == 0
case []any:
return len(typed) == 0
default:
return false
}
}
func toolString(value any) string {
text, ok := value.(string)
if !ok {
return ""
}
return strings.TrimSpace(text)
}
func firstToolString(values ...any) string {
for _, value := range values {
if text := toolString(value); text != "" {
return text
}
}
return ""
}
func firstToolValue(values ...any) any {
for _, value := range values {
if !toolValueEmpty(value) {
return value
}
}
return nil
}
func firstPresentToolString(values ...any) (string, bool) {
for _, value := range values {
if text, ok := value.(string); ok {
return text, true
}
}
return "", false
}
func toolStringSlice(value any) []string {
switch typed := value.(type) {
case []any:
values := make([]string, 0, len(typed))
for _, item := range typed {
if text := toolString(item); text != "" {
values = appendUniqueToolString(values, text)
}
}
return values
case []string:
return uniqueToolStrings(typed)
default:
return nil
}
}
func appendUniqueToolString(values []string, value string) []string {
value = strings.TrimSpace(value)
if value == "" {
return values
}
for _, current := range values {
if current == value {
return values
}
}
return append(values, value)
}
func uniqueToolStrings(values []string) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = appendUniqueToolString(result, value)
}
return result
}
func stringsToAny(values []string) []any {
result := make([]any, len(values))
for index, value := range values {
result[index] = value
}
return result
}
func normalizeToolChange(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "add", "added", "create", "created", "new", "write_file":
return "added"
case "delete", "deleted", "remove", "removed", "delete_file":
return "deleted"
case "modify", "modified", "update", "updated", "edit", "edited", "change", "changed", "edit_file":
return "modified"
default:
return ""
}
}
func isCompletedToolStatus(status string) bool {
return strings.TrimSpace(status) == "completed"
}
func isFailedToolStatus(status string) bool {
switch strings.TrimSpace(status) {
case "failed", "errored", "canceled":
return true
default:
return false
}
}