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.
This commit is contained in:
parent
62d0e34ec9
commit
72005ae06c
2 changed files with 76 additions and 2 deletions
|
|
@ -79,8 +79,16 @@ type Attachment struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
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"`
|
Media []string `json:"media,omitempty"`
|
||||||
Attachments []Attachment `json:"attachments,omitempty"`
|
Attachments []Attachment `json:"attachments,omitempty"`
|
||||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
|
|
|
||||||
66
pkg/providers/protocoltypes/types_test.go
Normal file
66
pkg/providers/protocoltypes/types_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue