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

712 lines
24 KiB
Go

package agentsessionstore
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/tutti-os/tutti/packages/agent/daemon/httpx"
controlplanehttp "github.com/tutti-os/tutti/packages/agent/daemon/internal/httpclient"
)
func NewClient(cfg Config) *Client {
httpClient := cfg.HTTPClient
if httpClient == nil {
httpClient = httpx.NewClient(defaultTimeout)
}
return &Client{
cfg: cfg,
httpClient: httpClient,
}
}
func (c *Client) ReportActivity(ctx context.Context, input ReportActivityInput) (ReportActivityReply, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
if workspaceID == "" {
return ReportActivityReply{}, errors.New("workspace id is required")
}
// Provider observations are local Replay metadata. They never cross the
// canonical HTTP reporting boundary.
input.ProviderObservations = nil
return ReportActivityAsSessionUpdates(ctx, c, input)
}
func (c *Client) BindGoalProvenance(ctx context.Context, input BindGoalProvenanceInput) (GoalProvenanceBinding, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if workspaceID == "" || agentSessionID == "" {
return GoalProvenanceBinding{}, errors.New("workspace id and agent session id are required")
}
input.WorkspaceID = workspaceID
input.AgentSessionID = agentSessionID
requestBody, err := marshalRequestBody(input)
if err != nil {
return GoalProvenanceBinding{}, fmt.Errorf("prepare goal provenance bind request: %w", err)
}
endpoint := fmt.Sprintf("%s/rooms/%s/agents/sessions/%s/goal-provenance/bind",
resolveSessionAPIPrefix(c.cfg.BaseURL), url.PathEscape(workspaceID), url.PathEscape(agentSessionID))
var binding GoalProvenanceBinding
if err := c.postJSONWithTransientRemoteRetry(ctx, endpoint, requestBody, &binding); err != nil {
return GoalProvenanceBinding{}, WithRequestBodyBytes(err, len(requestBody))
}
return binding, nil
}
func (c *Client) LookupGoalProvenance(ctx context.Context, input LookupGoalProvenanceInput) (GoalProvenanceBinding, bool, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if workspaceID == "" || agentSessionID == "" {
return GoalProvenanceBinding{}, false, errors.New("workspace id and agent session id are required")
}
input.WorkspaceID = workspaceID
input.AgentSessionID = agentSessionID
requestBody, err := marshalRequestBody(input)
if err != nil {
return GoalProvenanceBinding{}, false, fmt.Errorf("prepare goal provenance lookup request: %w", err)
}
endpoint := fmt.Sprintf("%s/rooms/%s/agents/sessions/%s/goal-provenance/lookup",
resolveSessionAPIPrefix(c.cfg.BaseURL), url.PathEscape(workspaceID), url.PathEscape(agentSessionID))
var reply LookupGoalProvenanceReply
if err := c.postJSONWithTransientRemoteRetry(ctx, endpoint, requestBody, &reply); err != nil {
return GoalProvenanceBinding{}, false, WithRequestBodyBytes(err, len(requestBody))
}
return reply.Binding, reply.Found, nil
}
func (c *Client) ReportGoalReconcileRequired(ctx context.Context, input ReportGoalReconcileRequiredInput) (ReportGoalReconcileRequiredReply, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.Request.AgentSessionID)
if workspaceID == "" || agentSessionID == "" {
return ReportGoalReconcileRequiredReply{}, errors.New("workspace id and agent session id are required")
}
input.WorkspaceID = workspaceID
input.Request.AgentSessionID = agentSessionID
requestBody, err := marshalRequestBody(input.Request)
if err != nil {
return ReportGoalReconcileRequiredReply{}, fmt.Errorf("prepare goal reconcile request: %w", err)
}
endpoint := fmt.Sprintf("%s/rooms/%s/agents/sessions/%s/goal-reconcile-required",
resolveSessionAPIPrefix(c.cfg.BaseURL), url.PathEscape(workspaceID), url.PathEscape(agentSessionID))
var reply ReportGoalReconcileRequiredReply
if err := c.postJSONWithTransientRemoteRetry(ctx, endpoint, requestBody, &reply); err != nil {
return ReportGoalReconcileRequiredReply{}, WithRequestBodyBytes(err, len(requestBody))
}
return reply, nil
}
func (c *Client) ReportSessionState(ctx context.Context, input ReportSessionStateInput) (ReportSessionStateReply, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
if workspaceID == "" {
return ReportSessionStateReply{}, errors.New("workspace id is required")
}
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if agentSessionID == "" {
return ReportSessionStateReply{}, errors.New("agent session id is required")
}
endpoint := fmt.Sprintf(
"%s/rooms/%s/agents/sessions/%s/state",
resolveSessionAPIPrefix(c.cfg.BaseURL),
url.PathEscape(workspaceID),
url.PathEscape(agentSessionID),
)
// Metadata resolution: explicit input fields win, then the source, then
// (for the state document) the state itself. Existing non-empty values
// are left byte-identical in place; resolution only fills empty slots, so
// callers that carry no metadata anywhere produce an unchanged request.
agentTargetID := firstNonEmptyString(input.AgentTargetID, input.Source.AgentTargetID, input.State.AgentTargetID)
deviceID := firstNonEmptyString(input.DeviceID, input.Source.DeviceID, input.State.DeviceID)
source := input.Source
if agentTargetID != "" && strings.TrimSpace(source.AgentTargetID) == "" {
source.AgentTargetID = agentTargetID
}
if deviceID != "" && strings.TrimSpace(source.DeviceID) == "" {
source.DeviceID = deviceID
}
state := sanitizeSessionStateUpdateForUpstream(input.State)
if agentTargetID != "" && strings.TrimSpace(state.AgentTargetID) == "" {
state.AgentTargetID = agentTargetID
}
if deviceID != "" && strings.TrimSpace(state.DeviceID) == "" {
state.DeviceID = deviceID
}
requestBody, err := marshalRequestBody(reportSessionStateRequest{
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
AgentTargetID: agentTargetID,
DeviceID: deviceID,
SessionOrigin: sessionOriginCanonicalRequestValue(input.SessionOrigin),
Connector: input.Connector,
Source: &source,
State: state,
})
if err != nil {
return ReportSessionStateReply{}, fmt.Errorf("prepare agent session state request: %w", err)
}
var reply ReportSessionStateReply
reply.RequestBodyBytes = len(requestBody)
if err := c.postJSONWithTransientRemoteRetry(ctx, endpoint, requestBody, &reply); err != nil {
return reply, WithRequestBodyBytes(err, reply.RequestBodyBytes)
}
reply.RequestBodyBytes = len(requestBody)
return reply, nil
}
func (c *Client) ReportSessionMessages(ctx context.Context, input ReportSessionMessagesInput) (ReportSessionMessagesReply, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
if workspaceID == "" {
return ReportSessionMessagesReply{}, errors.New("workspace id is required")
}
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if agentSessionID == "" {
return ReportSessionMessagesReply{}, errors.New("agent session id is required")
}
endpoint := fmt.Sprintf(
"%s/rooms/%s/agents/sessions/%s/messages",
resolveSessionAPIPrefix(c.cfg.BaseURL),
url.PathEscape(workspaceID),
url.PathEscape(agentSessionID),
)
// Metadata resolution mirrors ReportSessionState: explicit input fields
// win, then the source; only empty slots are filled.
agentTargetID := firstNonEmptyString(input.AgentTargetID, input.Source.AgentTargetID)
deviceID := firstNonEmptyString(input.DeviceID, input.Source.DeviceID)
source := input.Source
if agentTargetID != "" && strings.TrimSpace(source.AgentTargetID) == "" {
source.AgentTargetID = agentTargetID
}
if deviceID != "" && strings.TrimSpace(source.DeviceID) == "" {
source.DeviceID = deviceID
}
requestBatches, err := marshalReportSessionMessagesRequestsForUpload(reportSessionMessagesRequest{
WorkspaceID: workspaceID,
AgentTargetID: agentTargetID,
DeviceID: deviceID,
SessionOrigin: sessionOriginCanonicalRequestValue(input.SessionOrigin),
Connector: input.Connector,
Source: &source,
Updates: sanitizeSessionMessageUpdatesForUpstream(input.Updates),
})
if err != nil {
return ReportSessionMessagesReply{}, fmt.Errorf("prepare agent session messages request: %w", err)
}
var reply ReportSessionMessagesReply
for _, batch := range requestBatches {
reply.RequestBodyBytes += len(batch)
var batchReply ReportSessionMessagesReply
err = c.postJSONWithTransientRemoteRetry(ctx, endpoint, batch, &batchReply)
if err != nil {
return reply, WithRequestBodyBytes(err, len(batch))
}
reply.AcceptedCount += batchReply.AcceptedCount
if batchReply.LatestVersion > reply.LatestVersion {
reply.LatestVersion = batchReply.LatestVersion
}
}
return reply, nil
}
func (c *Client) ReportActivityJSON(ctx context.Context, workspaceID string, payload json.RawMessage) (ReportActivityReply, error) {
workspaceID = strings.TrimSpace(workspaceID)
if workspaceID == "" {
return ReportActivityReply{}, errors.New("workspace id is required")
}
input, err := DecodeReportActivityJSON(payload)
if err != nil {
return ReportActivityReply{}, fmt.Errorf("decode agent activity request: %w", err)
}
input.WorkspaceID = workspaceID
return ReportActivityAsSessionUpdates(ctx, c, input)
}
func DecodeReportActivityJSON(payload json.RawMessage) (ReportActivityInput, error) {
var request reportActivityRequest
if err := json.Unmarshal(payload, &request); err != nil {
return ReportActivityInput{}, err
}
input := ReportActivityInput{
TimelineItems: request.TimelineItems,
StatePatches: request.StatePatches,
MessageUpdates: request.MessageUpdates,
SessionAudits: request.SessionAudits,
GoalReconcileRequests: request.GoalReconcileRequests,
}
if request.Connector != nil {
connector := *request.Connector
input.Connector = &connector
}
if request.Source != nil {
input.Source = *request.Source
}
return input, nil
}
func (c *Client) ListAgents(ctx context.Context, workspaceID string) (*WorkspaceAgentSnapshot, error) {
return c.ListAgentsWithOrigin(ctx, workspaceID, "")
}
func (c *Client) ListAgentsWithOrigin(ctx context.Context, workspaceID string, sessionOrigin string) (*WorkspaceAgentSnapshot, error) {
return c.ListAgentsWithFilter(ctx, ListAgentsInput{
WorkspaceID: workspaceID,
SessionOrigin: sessionOrigin,
})
}
func (c *Client) ListAgentsWithFilter(ctx context.Context, input ListAgentsInput) (*WorkspaceAgentSnapshot, error) {
var snapshot WorkspaceAgentSnapshot
endpoint := fmt.Sprintf(
"%s/rooms/%s/agents/list",
resolveAPIPrefix(c.cfg.BaseURL),
url.PathEscape(input.WorkspaceID),
)
query := url.Values{}
if origin := sessionOriginCanonicalQueryValue(input.SessionOrigin); origin != "" {
query.Set("session_origin", origin)
}
if userID := strings.TrimSpace(input.UserID); userID != "" {
query.Set("user_id", userID)
}
if deviceID := strings.TrimSpace(input.DeviceID); deviceID != "" {
query.Set("device_id", deviceID)
}
if encoded := query.Encode(); encoded != "" {
endpoint += "?" + encoded
}
err := c.doJSONWithTransientRemoteRetry(ctx, http.MethodGet, endpoint, nil, &snapshot)
if err != nil {
return nil, err
}
return &snapshot, nil
}
func (c *Client) GetGlobalAgentActivityFilterOptions(ctx context.Context) (*GlobalAgentActivityFilterOptions, error) {
var options GlobalAgentActivityFilterOptions
if err := c.doJSONWithTransientRemoteRetry(ctx, http.MethodGet, "/v2/agent-activity/filter-options", nil, &options); err != nil {
return nil, err
}
return &options, nil
}
func (c *Client) ListGlobalAgentActivitySessions(ctx context.Context, input ListGlobalAgentActivitySessionsInput) (*ListGlobalAgentActivitySessionsReply, error) {
roomIDs := normalizeGlobalAgentActivityFilterValues(input.RoomIDs)
ownerUserIDs := normalizeGlobalAgentActivityFilterValues(input.SessionOwnerUserIDs)
agentKeys := normalizeGlobalAgentActivityFilterValues(input.AgentKeys)
if input.ActivityFromUnixMS < 0 || input.ActivityToUnixMS < 0 {
return nil, errors.New("global agent activity time filters must be non-negative")
}
if input.ActivityFromUnixMS > 0 && input.ActivityToUnixMS > 0 && input.ActivityFromUnixMS >= input.ActivityToUnixMS {
return nil, errors.New("global agent activity start time must be before end time")
}
if len(roomIDs) == 0 && len(ownerUserIDs) == 0 && len(agentKeys) == 0 && input.ActivityFromUnixMS == 0 && input.ActivityToUnixMS == 0 {
return nil, errors.New("at least one global agent activity filter is required")
}
query := url.Values{}
for _, roomID := range roomIDs {
query.Add("roomIds", roomID)
}
for _, userID := range ownerUserIDs {
query.Add("sessionOwnerUserIds", userID)
}
for _, agentKey := range agentKeys {
query.Add("agentKeys", agentKey)
}
if input.ActivityFromUnixMS > 0 {
query.Set("activityFromUnixMs", strconv.FormatInt(input.ActivityFromUnixMS, 10))
}
if input.ActivityToUnixMS > 0 {
query.Set("activityToUnixMs", strconv.FormatInt(input.ActivityToUnixMS, 10))
}
endpoint := "/v2/agent-activity/sessions"
if encoded := query.Encode(); encoded != "" {
endpoint += "?" + encoded
}
var reply ListGlobalAgentActivitySessionsReply
if err := c.doJSONWithTransientRemoteRetry(ctx, http.MethodGet, endpoint, nil, &reply); err != nil {
return nil, err
}
return &reply, nil
}
func normalizeGlobalAgentActivityFilterValues(values []string) []string {
normalized := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
normalized = append(normalized, value)
}
return normalized
}
type DeleteAgentSessionInput struct {
WorkspaceID string
AgentSessionID string
SessionOrigin string
}
func (c *Client) DeleteAgentSession(ctx context.Context, input DeleteAgentSessionInput) error {
query := url.Values{}
if origin := sessionOriginCanonicalQueryValue(input.SessionOrigin); origin != "" {
query.Set("session_origin", origin)
}
endpoint := fmt.Sprintf(
"%s/rooms/%s/agents/%s",
resolveAPIPrefix(c.cfg.BaseURL),
url.PathEscape(input.WorkspaceID),
url.PathEscape(input.AgentSessionID),
)
if encoded := query.Encode(); encoded != "" {
endpoint += "?" + encoded
}
return c.doJSON(ctx, http.MethodDelete, endpoint, nil, nil)
}
func (c *Client) ListSessionMessages(ctx context.Context, input ListSessionMessagesInput) (*ListSessionMessagesReply, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
if workspaceID == "" {
return nil, errors.New("workspace id is required")
}
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if agentSessionID == "" {
return nil, errors.New("agent session id is required")
}
query := url.Values{}
if input.AfterVersion > 0 {
query.Set("after_version", strconv.FormatUint(input.AfterVersion, 10))
}
if input.Limit > 0 {
query.Set("limit", strconv.Itoa(input.Limit))
}
if origin := sessionOriginCanonicalQueryValue(input.SessionOrigin); origin != "" {
query.Set("session_origin", origin)
}
if deviceID := strings.TrimSpace(input.DeviceID); deviceID != "" {
query.Set("device_id", deviceID)
}
endpoint := fmt.Sprintf(
"%s/rooms/%s/agents/sessions/%s/messages",
resolveSessionAPIPrefix(c.cfg.BaseURL),
url.PathEscape(workspaceID),
url.PathEscape(agentSessionID),
)
if encoded := query.Encode(); encoded != "" {
endpoint += "?" + encoded
}
var messages ListSessionMessagesReply
if err := c.doJSONWithTransientRemoteRetry(ctx, http.MethodGet, endpoint, nil, &messages); err != nil {
return nil, err
}
return &messages, nil
}
func (c *Client) postJSON(ctx context.Context, endpoint string, requestBody any, responseBody any) error {
return c.doJSON(ctx, http.MethodPost, endpoint, requestBody, responseBody)
}
func (c *Client) postJSONWithTransientRemoteRetry(
ctx context.Context,
endpoint string,
requestBody any,
responseBody any,
) error {
var err error
for attempt := 0; ; attempt++ {
err = c.postJSON(ctx, endpoint, requestBody, responseBody)
if err == nil || !shouldRetryTransientRemotePost(c.cfg.BaseURL, err) {
return err
}
if attempt >= len(controlplanehttp.DefaultTransientBackoffs) || ctx.Err() != nil {
return err
}
if waitErr := sleepWithContext(ctx, controlplanehttp.DefaultTransientBackoffs[attempt]); waitErr != nil {
return err
}
}
}
func (c *Client) doJSONWithTransientRemoteRetry(
ctx context.Context,
method string,
endpoint string,
requestBody any,
responseBody any,
) error {
var err error
for attempt := 0; ; attempt++ {
err = c.doJSON(ctx, method, endpoint, requestBody, responseBody)
if err == nil || !shouldRetryTransientRemoteRead(c.cfg.BaseURL, method, err) {
return err
}
if attempt >= len(controlplanehttp.DefaultTransientBackoffs) || ctx.Err() != nil {
return err
}
if waitErr := sleepWithContext(ctx, controlplanehttp.DefaultTransientBackoffs[attempt]); waitErr != nil {
return err
}
}
}
func sessionOriginCanonicalQueryValue(origin string) string {
return canonicalSessionOriginValue(origin)
}
func sessionOriginCanonicalRequestValue(origin string) string {
return canonicalSessionOriginValue(origin)
}
func (c *Client) doJSON(ctx context.Context, method, endpoint string, requestBody any, responseBody any) error {
baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.BaseURL), "/")
if baseURL == "" {
return fmt.Errorf("agent activity base url is required")
}
var body io.Reader
if requestBody != nil {
raw, err := marshalRequestBody(requestBody)
if err != nil {
return fmt.Errorf("marshal agent activity request: %w", err)
}
body = bytes.NewReader(raw)
}
req, err := http.NewRequestWithContext(ctx, method, baseURL+endpoint, body)
if err != nil {
return err
}
c.setHeaders(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer func() { _ = res.Body.Close() }()
raw, err := io.ReadAll(io.LimitReader(res.Body, 2<<20))
if err != nil {
return err
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return HTTPError{StatusCode: res.StatusCode, Body: strings.TrimSpace(string(raw)), Header: res.Header.Clone()}
}
if responseBody == nil || len(bytes.TrimSpace(raw)) == 0 {
return nil
}
if err := json.Unmarshal(raw, responseBody); err != nil {
return fmt.Errorf("decode agent activity response: %w", err)
}
return nil
}
func marshalRequestBody(requestBody any) ([]byte, error) {
switch value := requestBody.(type) {
case json.RawMessage:
return value, nil
case []byte:
return value, nil
default:
return json.Marshal(requestBody)
}
}
// SanitizeTimelineItemsForRelay reuses the upstream payload sanitation rules for
// local relay transports so oversized tool outputs do not block activity reporting.
func (c *Client) setHeaders(req *http.Request) {
req.Header.Set("Accept", "application/json")
if req.Body != nil {
req.Header.Set("Content-Type", "application/json")
}
if cookie := strings.TrimSpace(c.cfg.SessionCookie); cookie != "" {
req.Header.Set("Cookie", cookie)
}
if userID := strings.TrimSpace(c.cfg.UserID); userID != "" {
req.Header.Set("X-User-Id", userID)
}
if token := strings.TrimSpace(c.cfg.Token); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
if lane := strings.TrimSpace(c.cfg.PPELane); lane != "" {
req.Header.Set("x-zk-ppe-lane", lane)
}
}
func resolveAPIPrefix(baseURL string) string {
if isLoopbackBaseURL(baseURL) {
return localAPIPrefix
}
return remoteAPIPrefix
}
func resolveSessionAPIPrefix(baseURL string) string {
if isLoopbackBaseURL(baseURL) {
return localAPIPrefix
}
return remoteAPIPrefix
}
func shouldRetryTransientRemotePost(baseURL string, err error) bool {
if err == nil || resolveAPIPrefix(baseURL) != remoteAPIPrefix {
return false
}
return controlplanehttp.IsTransientNetworkError(err)
}
func shouldRetryTransientRemoteRead(baseURL string, method string, err error) bool {
if err == nil || resolveAPIPrefix(baseURL) != remoteAPIPrefix {
return false
}
if method != http.MethodGet {
return false
}
return controlplanehttp.IsTransientNetworkError(err)
}
func sleepWithContext(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func isLoopbackBaseURL(raw string) bool {
host := baseURLHostname(raw)
if host == "" {
return false
}
if strings.EqualFold(host, "localhost") || host == "0.0.0.0" {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func baseURLHostname(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
parsed, err := url.Parse(raw)
if err == nil && parsed.Hostname() != "" {
return strings.TrimSpace(parsed.Hostname())
}
if strings.Contains(raw, "://") {
return ""
}
parsed, err = url.Parse("http://" + raw)
if err != nil {
return ""
}
return strings.TrimSpace(parsed.Hostname())
}
type reportActivityRequest struct {
Connector *ConnectorInfo `json:"connector,omitempty"`
Source *EventSource `json:"source,omitempty"`
TimelineItems []WorkspaceAgentTimelineItem `json:"timelineItems,omitempty"`
StatePatches []WorkspaceAgentStatePatch `json:"statePatches,omitempty"`
MessageUpdates []WorkspaceAgentMessageUpdate `json:"messageUpdates,omitempty"`
SessionAudits []WorkspaceAgentSessionAuditUpdate `json:"sessionAudits,omitempty"`
GoalReconcileRequests []WorkspaceAgentGoalReconcileRequest `json:"goalReconcileRequests,omitempty"`
}
type reportSessionStateRequest struct {
WorkspaceID string `json:"roomId,omitempty"`
AgentSessionID string `json:"agentSessionId,omitempty"`
AgentTargetID string `json:"agentTargetId,omitempty"`
DeviceID string `json:"deviceId,omitempty"`
SessionOrigin string `json:"sessionOrigin,omitempty"`
Source *EventSource `json:"source,omitempty"`
Connector *ConnectorInfo `json:"connector,omitempty"`
State WorkspaceAgentSessionStateUpdate `json:"state,omitempty"`
}
type reportSessionMessagesRequest struct {
WorkspaceID string `json:"roomId,omitempty"`
AgentTargetID string `json:"agentTargetId,omitempty"`
DeviceID string `json:"deviceId,omitempty"`
SessionOrigin string `json:"sessionOrigin,omitempty"`
Source *EventSource `json:"source,omitempty"`
Connector *ConnectorInfo `json:"connector,omitempty"`
Updates []WorkspaceAgentSessionMessageUpdate `json:"updates,omitempty"`
}
type flexibleUint64 uint64
func (v *flexibleUint64) UnmarshalJSON(data []byte) error {
parsed, err := parseFlexibleUint64(data)
if err != nil {
return err
}
*v = flexibleUint64(parsed)
return nil
}
type flexibleInt64 int64
func (v *flexibleInt64) UnmarshalJSON(data []byte) error {
parsed, err := parseFlexibleInt64(data)
if err != nil {
return err
}
*v = flexibleInt64(parsed)
return nil
}
func parseFlexibleUint64(data []byte) (uint64, error) {
text := strings.TrimSpace(string(data))
if text == "" || text == "null" {
return 0, nil
}
if strings.HasPrefix(text, `"`) {
var value string
if err := json.Unmarshal(data, &value); err != nil {
return 0, err
}
text = strings.TrimSpace(value)
if text == "" {
return 0, nil
}
}
return strconv.ParseUint(text, 10, 64)
}
func parseFlexibleInt64(data []byte) (int64, error) {
text := strings.TrimSpace(string(data))
if text == "" || text == "null" {
return 0, nil
}
if strings.HasPrefix(text, `"`) {
var value string
if err := json.Unmarshal(data, &value); err != nil {
return 0, err
}
text = strings.TrimSpace(value)
if text == "" {
return 0, nil
}
}
return strconv.ParseInt(text, 10, 64)
}