From 72005ae06ce2387cfba5c2761c4b17991f57f40c Mon Sep 17 00:00:00 2001 From: maxiaoyang <2768753269@qq.com> Date: Wed, 29 Apr 2026 20:54:11 +0800 Subject: [PATCH] feat(protocol): add Name field to providers.Message Adds an optional Name field on providers.Message so per-message sender attribution can be carried through the persistence layer and onto the wire. The field is JSON-tagged `name,omitempty`, so: - the OpenAI Chat Completions wire format will be able to emit the native `name` field that disambiguates participants in multi-user conversations; - session stores that round-trip messages via json.Marshal preserve the field with no extra plumbing; - existing session files load cleanly with Name = "". This is the schema-only change. Adapters and the agent pipeline are updated in subsequent commits. Refs #2702. --- pkg/providers/protocoltypes/types.go | 12 ++++- pkg/providers/protocoltypes/types_test.go | 66 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 pkg/providers/protocoltypes/types_test.go diff --git a/pkg/providers/protocoltypes/types.go b/pkg/providers/protocoltypes/types.go index bab4433e7..a7996feb3 100644 --- a/pkg/providers/protocoltypes/types.go +++ b/pkg/providers/protocoltypes/types.go @@ -79,8 +79,16 @@ type Attachment struct { } type Message struct { - Role string `json:"role"` - Content string `json:"content"` + Role string `json:"role"` + Content string `json:"content"` + // Name carries per-message sender attribution for multi-user sessions + // (Discord/Telegram/Slack groups, etc.). It is the sanitized SenderID + // from the inbound channel and is OpenAI-Chat-Completions wire-safe + // (matches `^[a-zA-Z0-9_-]{1,64}$`). Adapters that natively support a + // per-message name field (OpenAI) emit it directly; adapters that do + // not (Anthropic, Bedrock) prefix it onto user content at marshal + // time without mutating the persisted message. + Name string `json:"name,omitempty"` Media []string `json:"media,omitempty"` Attachments []Attachment `json:"attachments,omitempty"` ReasoningContent string `json:"reasoning_content,omitempty"` diff --git a/pkg/providers/protocoltypes/types_test.go b/pkg/providers/protocoltypes/types_test.go new file mode 100644 index 000000000..6e551cc0b --- /dev/null +++ b/pkg/providers/protocoltypes/types_test.go @@ -0,0 +1,66 @@ +// PicoClaw - Ultra-lightweight personal AI agent +// License: MIT +// +// Copyright (c) 2026 PicoClaw contributors + +package protocoltypes + +import ( + "encoding/json" + "strings" + "testing" +) + +// TestMessage_NameJSONRoundtrip verifies that the Name field — added for +// multi-user sender attribution (issue #2702) — round-trips through +// json.Marshal/Unmarshal. The session persistence layer (JSONL append + +// in-memory snapshot via json.MarshalIndent) relies on this transparency +// to carry sender attribution into stored history without bespoke code. +func TestMessage_NameJSONRoundtrip(t *testing.T) { + in := Message{Role: "user", Content: "hi", Name: "U_alice"} + data, err := json.Marshal(in) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + if !strings.Contains(string(data), `"name":"U_alice"`) { + t.Errorf("expected name in marshaled JSON, got: %s", string(data)) + } + var out Message + if err := json.Unmarshal(data, &out); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + if out.Name != "U_alice" { + t.Errorf("Name after roundtrip = %q, want U_alice", out.Name) + } +} + +// TestMessage_NameOmittedWhenEmpty preserves wire/persistence backward +// compatibility for direct-channel turns and pre-2702 session files: an +// empty Name must not appear as `"name":""` in either marshaled output. +func TestMessage_NameOmittedWhenEmpty(t *testing.T) { + in := Message{Role: "user", Content: "hi"} + data, err := json.Marshal(in) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + if strings.Contains(string(data), `"name"`) { + t.Errorf("empty Name should be omitted; got: %s", string(data)) + } +} + +// TestMessage_LegacyHistoryUnmarshalsToEmptyName verifies that historical +// session files written before this field existed still load cleanly: the +// Name field is simply zero-valued. +func TestMessage_LegacyHistoryUnmarshalsToEmptyName(t *testing.T) { + legacy := []byte(`{"role":"user","content":"old message"}`) + var msg Message + if err := json.Unmarshal(legacy, &msg); err != nil { + t.Fatalf("json.Unmarshal legacy: %v", err) + } + if msg.Name != "" { + t.Errorf("legacy Name = %q, want empty", msg.Name) + } + if msg.Content != "old message" { + t.Errorf("legacy Content = %q, want preserved", msg.Content) + } +}