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
608 lines
20 KiB
Go
608 lines
20 KiB
Go
package agentsessionstore
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
replay "github.com/tutti-os/tutti/packages/agent/session-replay"
|
|
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
|
|
)
|
|
|
|
func ReportActivityAsSessionUpdates(
|
|
ctx context.Context,
|
|
reporter SessionActivityReporter,
|
|
input ReportActivityInput,
|
|
) (ReportActivityReply, error) {
|
|
reply := ReportActivityReply{
|
|
AcceptedTimelineItemCount: 0,
|
|
}
|
|
if reporter == nil {
|
|
return reply, nil
|
|
}
|
|
replayContext, err := providerObservationCommitContext(
|
|
input.ProviderObservations,
|
|
)
|
|
if err != nil {
|
|
return reply, err
|
|
}
|
|
if len(input.GoalReconcileRequests) > 0 {
|
|
goalReporter, ok := reporter.(GoalReconcileRequestReporter)
|
|
if !ok {
|
|
return reply, fmt.Errorf("agent activity reporter does not support goal reconcile requests")
|
|
}
|
|
for _, request := range input.GoalReconcileRequests {
|
|
requestReply, err := goalReporter.ReportGoalReconcileRequired(ctx, ReportGoalReconcileRequiredInput{
|
|
WorkspaceID: input.WorkspaceID,
|
|
Request: request,
|
|
})
|
|
if err != nil {
|
|
return reply, err
|
|
}
|
|
if requestReply.Accepted {
|
|
reply.AcceptedGoalReconcileRequestCount++
|
|
}
|
|
}
|
|
}
|
|
auditInputs := SessionAuditInputsFromActivity(input)
|
|
messageInputs, err := SessionMessageInputsFromActivity(input)
|
|
if err != nil {
|
|
return reply, err
|
|
}
|
|
reportState := func(stateInput ReportSessionStateInput) error {
|
|
var stateReply ReportSessionStateReply
|
|
var err error
|
|
if contextual, ok := reporter.(SessionActivityCommitReporter); ok {
|
|
stateReply, err =
|
|
contextual.ReportSessionStateWithCommitContext(
|
|
ctx,
|
|
stateInput,
|
|
replayContext,
|
|
)
|
|
} else if len(input.ProviderObservations) == 0 {
|
|
stateReply, err = reporter.ReportSessionState(ctx, stateInput)
|
|
} else {
|
|
return fmt.Errorf("agent activity reporter does not support Replay commit context")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if stateReply.Accepted {
|
|
reply.AcceptedStatePatchCount++
|
|
}
|
|
reply.RequestBodyBytes += stateReply.RequestBodyBytes
|
|
return nil
|
|
}
|
|
reportMessages := func(messagesInput ReportSessionMessagesInput, audit bool) error {
|
|
var messagesReply ReportSessionMessagesReply
|
|
var err error
|
|
if contextual, ok := reporter.(SessionActivityCommitReporter); ok {
|
|
messagesReply, err =
|
|
contextual.ReportSessionMessagesWithCommitContext(
|
|
ctx,
|
|
messagesInput,
|
|
replayContext,
|
|
)
|
|
} else if len(input.ProviderObservations) == 0 {
|
|
messagesReply, err =
|
|
reporter.ReportSessionMessages(ctx, messagesInput)
|
|
} else {
|
|
return fmt.Errorf("agent activity reporter does not support Replay commit context")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if audit {
|
|
reply.AcceptedSessionAuditCount += messagesReply.AcceptedCount
|
|
} else {
|
|
reply.AcceptedMessageUpdateCount += messagesReply.AcceptedCount
|
|
}
|
|
reply.RequestBodyBytes += messagesReply.RequestBodyBytes
|
|
return nil
|
|
}
|
|
flushAuditsForSession := func(agentSessionID string) error {
|
|
agentSessionID = strings.TrimSpace(agentSessionID)
|
|
for index := range auditInputs {
|
|
if strings.TrimSpace(auditInputs[index].AgentSessionID) != agentSessionID || len(auditInputs[index].Updates) == 0 {
|
|
continue
|
|
}
|
|
pending := auditInputs[index]
|
|
auditInputs[index].Updates = nil
|
|
if err := reportMessages(pending, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
flushMessagesForTurn := func(agentSessionID string, turnID string) ([]ReportSessionMessagesInput, error) {
|
|
agentSessionID = strings.TrimSpace(agentSessionID)
|
|
turnID = strings.TrimSpace(turnID)
|
|
flushed := make([]ReportSessionMessagesInput, 0, 1)
|
|
for index := range messageInputs {
|
|
if strings.TrimSpace(messageInputs[index].AgentSessionID) != agentSessionID || len(messageInputs[index].Updates) == 0 {
|
|
continue
|
|
}
|
|
matching := make([]WorkspaceAgentSessionMessageUpdate, 0, len(messageInputs[index].Updates))
|
|
remaining := make([]WorkspaceAgentSessionMessageUpdate, 0, len(messageInputs[index].Updates))
|
|
for _, update := range messageInputs[index].Updates {
|
|
if strings.TrimSpace(update.TurnID) == turnID {
|
|
matching = append(matching, update)
|
|
} else {
|
|
remaining = append(remaining, update)
|
|
}
|
|
}
|
|
messageInputs[index].Updates = remaining
|
|
if len(matching) == 0 {
|
|
continue
|
|
}
|
|
pending := messageInputs[index]
|
|
pending.Updates = matching
|
|
if err := reportMessages(pending, false); err != nil {
|
|
return nil, err
|
|
}
|
|
flushed = append(flushed, pending)
|
|
}
|
|
return flushed, nil
|
|
}
|
|
|
|
// Preserve state-patch causality. A terminal patch locally flushes only its
|
|
// own Turn's messages before settlement; later non-terminal patches remain
|
|
// after that settlement instead of being globally moved ahead of it.
|
|
stateInputs := SessionStateInputsFromActivity(input)
|
|
for index := range stateInputs {
|
|
stateInput := &stateInputs[index]
|
|
if turnID, terminal := settlementBarrierTurnID(*stateInput); terminal {
|
|
if err := flushAuditsForSession(stateInput.AgentSessionID); err != nil {
|
|
return reply, err
|
|
}
|
|
flushed, err := flushMessagesForTurn(stateInput.AgentSessionID, turnID)
|
|
if err != nil {
|
|
return reply, err
|
|
}
|
|
anchorFinalAssistantMessages(stateInputs[index:index+1], flushed)
|
|
}
|
|
if err := reportState(*stateInput); err != nil {
|
|
return reply, err
|
|
}
|
|
}
|
|
for _, auditInput := range auditInputs {
|
|
if len(auditInput.Updates) > 0 {
|
|
if err := reportMessages(auditInput, true); err != nil {
|
|
return reply, err
|
|
}
|
|
}
|
|
}
|
|
for _, messagesInput := range messageInputs {
|
|
if len(messagesInput.Updates) > 0 {
|
|
if err := reportMessages(messagesInput, false); err != nil {
|
|
return reply, err
|
|
}
|
|
}
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
func providerObservationCommitContext(
|
|
batches []replay.ProviderObservationBatch,
|
|
) (replay.ProviderObservationCommitContext, error) {
|
|
return replay.NewProviderObservationCommitContext(batches)
|
|
}
|
|
|
|
func settlementBarrierTurnID(input ReportSessionStateInput) (string, bool) {
|
|
if root := input.State.RootProviderTurn; root != nil &&
|
|
strings.EqualFold(strings.TrimSpace(root.Phase), RootProviderTurnPhaseCompleted) {
|
|
return strings.TrimSpace(root.RootTurnID), strings.TrimSpace(root.RootTurnID) != ""
|
|
}
|
|
turn := input.State.Turn
|
|
if turn == nil {
|
|
return "", false
|
|
}
|
|
phase := strings.ToLower(strings.TrimSpace(turn.Phase))
|
|
terminal := turn.Settling || phase == "settling" || phase == "settled"
|
|
return strings.TrimSpace(turn.TurnID), terminal && strings.TrimSpace(turn.TurnID) != ""
|
|
}
|
|
|
|
func anchorFinalAssistantMessages(states []ReportSessionStateInput, messages []ReportSessionMessagesInput) {
|
|
anchors := make(map[string]string)
|
|
for _, input := range messages {
|
|
for _, update := range input.Updates {
|
|
if !strings.EqualFold(strings.TrimSpace(update.Role), "assistant") ||
|
|
!strings.EqualFold(strings.TrimSpace(update.Kind), "text") {
|
|
continue
|
|
}
|
|
turnID := strings.TrimSpace(update.TurnID)
|
|
messageID := strings.TrimSpace(update.MessageID)
|
|
if turnID != "" && messageID != "" {
|
|
anchors[strings.TrimSpace(input.AgentSessionID)+"\x00"+turnID] = messageID
|
|
}
|
|
}
|
|
}
|
|
for index := range states {
|
|
turn := states[index].State.Turn
|
|
if turn == nil || !strings.EqualFold(strings.TrimSpace(turn.Phase), "settled") {
|
|
continue
|
|
}
|
|
turn.FinalAssistantMessageID = anchors[strings.TrimSpace(states[index].AgentSessionID)+"\x00"+strings.TrimSpace(turn.TurnID)]
|
|
}
|
|
}
|
|
|
|
func SessionAuditInputsFromActivity(input ReportActivityInput) []ReportSessionMessagesInput {
|
|
if len(input.SessionAudits) == 0 {
|
|
return nil
|
|
}
|
|
source := input.Source
|
|
source.SessionOrigin = canonicalSessionOriginValue(source.SessionOrigin)
|
|
indexBySession := make(map[string]int)
|
|
out := make([]ReportSessionMessagesInput, 0)
|
|
for _, audit := range input.SessionAudits {
|
|
agentSessionID := strings.TrimSpace(firstNonEmptyString(source.AgentID, source.ProviderSessionID))
|
|
auditID := strings.TrimSpace(audit.AuditID)
|
|
if agentSessionID == "" || auditID == "" {
|
|
continue
|
|
}
|
|
index, ok := indexBySession[agentSessionID]
|
|
if !ok {
|
|
index = len(out)
|
|
indexBySession[agentSessionID] = index
|
|
out = append(out, ReportSessionMessagesInput{
|
|
WorkspaceID: input.WorkspaceID, AgentSessionID: agentSessionID,
|
|
SessionOrigin: source.SessionOrigin, Connector: cloneConnector(input.Connector), Source: source,
|
|
})
|
|
}
|
|
payload := clonePayloadMap(audit.Payload)
|
|
if payload == nil {
|
|
payload = map[string]any{}
|
|
}
|
|
if strings.TrimSpace(audit.Content) != "" {
|
|
payload["content"] = audit.Content
|
|
payload["text"] = audit.Content
|
|
}
|
|
out[index].Updates = append(out[index].Updates, WorkspaceAgentSessionMessageUpdate{
|
|
MessageID: auditID, Role: strings.TrimSpace(audit.Role), Kind: "session_audit",
|
|
Status: "completed", Payload: payload, OccurredAtUnixMS: audit.OccurredAtUnixMS,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func SessionStateInputsFromActivity(input ReportActivityInput) []ReportSessionStateInput {
|
|
if len(input.StatePatches) == 0 {
|
|
return nil
|
|
}
|
|
source := input.Source
|
|
source.SessionOrigin = canonicalSessionOriginValue(source.SessionOrigin)
|
|
out := make([]ReportSessionStateInput, 0, len(input.StatePatches))
|
|
for _, patch := range input.StatePatches {
|
|
agentSessionID := strings.TrimSpace(firstNonEmptyString(
|
|
patch.AgentSessionID,
|
|
source.AgentID,
|
|
source.ProviderSessionID,
|
|
))
|
|
if agentSessionID == "" {
|
|
continue
|
|
}
|
|
out = append(out, ReportSessionStateInput{
|
|
WorkspaceID: input.WorkspaceID,
|
|
AgentSessionID: agentSessionID,
|
|
SessionOrigin: source.SessionOrigin,
|
|
Connector: cloneConnector(input.Connector),
|
|
Source: source,
|
|
State: sessionStateUpdateFromPatch(patch),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func SessionMessageInputsFromActivity(input ReportActivityInput) ([]ReportSessionMessagesInput, error) {
|
|
updates := mergeActivityMessageUpdates(nil, input.MessageUpdates)
|
|
if len(updates) == 0 {
|
|
return nil, nil
|
|
}
|
|
source := input.Source
|
|
source.SessionOrigin = canonicalSessionOriginValue(source.SessionOrigin)
|
|
indexBySession := make(map[string]int)
|
|
out := make([]ReportSessionMessagesInput, 0)
|
|
for _, update := range updates {
|
|
agentSessionID := strings.TrimSpace(firstNonEmptyString(
|
|
update.AgentSessionID,
|
|
source.AgentID,
|
|
source.ProviderSessionID,
|
|
))
|
|
if agentSessionID == "" {
|
|
continue
|
|
}
|
|
converted := SessionMessageUpdateFromActivityUpdate(update)
|
|
if strings.TrimSpace(converted.MessageID) == "" {
|
|
continue
|
|
}
|
|
turnID := strings.TrimSpace(converted.TurnID)
|
|
kind := strings.TrimSpace(converted.Kind)
|
|
if kind == "session_audit" {
|
|
return nil, fmt.Errorf("agent activity session_audit %q must use SessionAudits", converted.MessageID)
|
|
}
|
|
if turnID == "" && kind != "session_audit" {
|
|
return nil, fmt.Errorf("agent activity message_update %q is missing turnId", converted.MessageID)
|
|
}
|
|
index, ok := indexBySession[agentSessionID]
|
|
if !ok {
|
|
index = len(out)
|
|
indexBySession[agentSessionID] = index
|
|
out = append(out, ReportSessionMessagesInput{
|
|
WorkspaceID: input.WorkspaceID,
|
|
AgentSessionID: agentSessionID,
|
|
SessionOrigin: source.SessionOrigin,
|
|
Connector: cloneConnector(input.Connector),
|
|
Source: source,
|
|
})
|
|
}
|
|
out[index].Updates = append(out[index].Updates, converted)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func mergeActivityMessageUpdates(derived []WorkspaceAgentMessageUpdate, explicit []WorkspaceAgentMessageUpdate) []WorkspaceAgentMessageUpdate {
|
|
if len(derived) == 0 {
|
|
return append([]WorkspaceAgentMessageUpdate(nil), explicit...)
|
|
}
|
|
if len(explicit) == 0 {
|
|
return derived
|
|
}
|
|
explicitIDs := make(map[string]struct{}, len(explicit))
|
|
for _, update := range explicit {
|
|
agentSessionID := strings.TrimSpace(update.AgentSessionID)
|
|
messageID := strings.TrimSpace(update.MessageID)
|
|
if agentSessionID == "" || messageID == "" {
|
|
continue
|
|
}
|
|
explicitIDs[agentSessionID+"\x00"+messageID] = struct{}{}
|
|
}
|
|
out := make([]WorkspaceAgentMessageUpdate, 0, len(derived)+len(explicit))
|
|
for _, update := range derived {
|
|
key := strings.TrimSpace(update.AgentSessionID) + "\x00" + strings.TrimSpace(update.MessageID)
|
|
if _, ok := explicitIDs[key]; ok {
|
|
continue
|
|
}
|
|
out = append(out, update)
|
|
}
|
|
out = append(out, explicit...)
|
|
return out
|
|
}
|
|
|
|
func SessionMessageUpdateFromActivityUpdate(update WorkspaceAgentMessageUpdate) WorkspaceAgentSessionMessageUpdate {
|
|
payload := clonePayloadMap(update.Payload)
|
|
if payload == nil {
|
|
payload = map[string]any{}
|
|
}
|
|
if update.Seq > 0 {
|
|
payload["seq"] = update.Seq
|
|
}
|
|
if callID := strings.TrimSpace(update.CallID); callID != "" {
|
|
payload["callId"] = callID
|
|
}
|
|
if parentCallID := strings.TrimSpace(update.ParentCallID); parentCallID != "" {
|
|
payload["parentCallId"] = parentCallID
|
|
}
|
|
if rootCallID := strings.TrimSpace(update.RootCallID); rootCallID != "" {
|
|
payload["rootCallId"] = rootCallID
|
|
}
|
|
if title := strings.TrimSpace(update.Title); title != "" {
|
|
payload["title"] = title
|
|
}
|
|
if len(payload) == 0 {
|
|
payload = nil
|
|
}
|
|
return WorkspaceAgentSessionMessageUpdate{
|
|
MessageID: strings.TrimSpace(update.MessageID),
|
|
TurnID: strings.TrimSpace(update.TurnID),
|
|
Role: strings.TrimSpace(update.Role),
|
|
Kind: strings.TrimSpace(update.Kind),
|
|
Status: strings.TrimSpace(update.Status),
|
|
Semantics: cloneMessageSemantics(update.Semantics),
|
|
Payload: payload,
|
|
OccurredAtUnixMS: firstNonZeroInt64(update.OccurredAtUnixMS, update.StartedAtUnixMS, update.CompletedAtUnixMS),
|
|
StartedAtUnixMS: update.StartedAtUnixMS,
|
|
CompletedAtUnixMS: update.CompletedAtUnixMS,
|
|
}
|
|
}
|
|
|
|
func sessionStateUpdateFromPatch(patch WorkspaceAgentStatePatch) WorkspaceAgentSessionStateUpdate {
|
|
currentPhase := strings.TrimSpace(patch.CurrentPhase)
|
|
if currentPhase == "" {
|
|
currentPhase = deriveCurrentPhaseFromEntityPatches(patch.Entities)
|
|
}
|
|
out := WorkspaceAgentSessionStateUpdate{
|
|
Kind: strings.TrimSpace(patch.Kind),
|
|
RootAgentSessionID: strings.TrimSpace(patch.RootAgentSessionID),
|
|
RootTurnID: strings.TrimSpace(patch.RootTurnID),
|
|
ParentAgentSessionID: strings.TrimSpace(patch.ParentAgentSessionID),
|
|
ParentTurnID: strings.TrimSpace(patch.ParentTurnID),
|
|
ParentToolCallID: strings.TrimSpace(patch.ParentToolCallID),
|
|
AgentTargetID: strings.TrimSpace(patch.AgentTargetID),
|
|
DeviceID: strings.TrimSpace(patch.DeviceID),
|
|
Provider: strings.TrimSpace(patch.Provider),
|
|
ProviderSessionID: strings.TrimSpace(patch.ProviderSessionID),
|
|
Model: strings.TrimSpace(patch.Model),
|
|
Settings: clonePayloadMap(patch.Settings),
|
|
Capabilities: canonical.CloneCapabilitySnapshot(patch.Capabilities),
|
|
RuntimeContext: clonePayloadMap(patch.RuntimeContext),
|
|
RuntimeContextPatch: canonical.CloneRuntimeContextPatch(patch.RuntimeContextPatch),
|
|
RuntimeActivity: cloneRuntimeActivityObservation(patch.RuntimeActivity),
|
|
TurnLifecycle: cloneTurnLifecycle(patch.TurnLifecycle),
|
|
SubmitAvailability: cloneSubmitAvailability(patch.SubmitAvailability),
|
|
InteractionTransition: cloneInteractionTransition(patch.InteractionTransition),
|
|
CWD: strings.TrimSpace(patch.CWD),
|
|
Title: strings.TrimSpace(patch.Title),
|
|
LifecycleStatus: strings.TrimSpace(patch.LifecycleStatus),
|
|
CurrentPhase: currentPhase,
|
|
LastError: strings.TrimSpace(patch.LastError),
|
|
OccurredAtUnixMS: patch.OccurredAtUnixMS,
|
|
RootProviderTurn: cloneRootProviderTurnTransition(patch.RootProviderTurn),
|
|
}
|
|
if patch.Turn != nil {
|
|
out.Turn = &WorkspaceAgentTurnStateUpdate{
|
|
TurnID: strings.TrimSpace(patch.Turn.TurnID),
|
|
CapabilityRefs: cloneCapabilityReferences(patch.Turn.CapabilityRefs),
|
|
Origin: strings.TrimSpace(patch.Turn.Origin),
|
|
SourceGoalOperationID: strings.TrimSpace(patch.Turn.SourceGoalOperationID),
|
|
SourceGoalRevision: patch.Turn.SourceGoalRevision,
|
|
SourceGoalRepairEpoch: patch.Turn.SourceGoalRepairEpoch,
|
|
ActiveTurnID: cloneStringPointer(patch.Turn.ActiveTurnID),
|
|
Phase: strings.TrimSpace(patch.Turn.Phase),
|
|
Outcome: strings.TrimSpace(patch.Turn.Outcome),
|
|
ErrorCode: strings.TrimSpace(patch.Turn.ErrorCode),
|
|
ErrorMessage: strings.TrimSpace(patch.Turn.ErrorMessage),
|
|
Settling: patch.Turn.Settling,
|
|
CompletedCommand: cloneCompletedCommand(patch.Turn.CompletedCommand),
|
|
SubmitAvailability: cloneSubmitAvailability(patch.Turn.SubmitAvailability),
|
|
FileChanges: clonePayloadMap(patch.Turn.FileChanges),
|
|
StartedAtUnixMS: patch.Turn.StartedAtUnixMS,
|
|
CompletedAtUnixMS: patch.Turn.CompletedAtUnixMS,
|
|
FinalAssistantMessageID: strings.TrimSpace(patch.Turn.FinalAssistantMessageID),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneRuntimeActivityObservation(
|
|
value *canonical.WorkspaceAgentRuntimeActivityObservation,
|
|
) *canonical.WorkspaceAgentRuntimeActivityObservation {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
out := *value
|
|
return &out
|
|
}
|
|
|
|
func cloneCapabilityReferences(
|
|
references []WorkspaceAgentCapabilityReference,
|
|
) []WorkspaceAgentCapabilityReference {
|
|
if len(references) == 0 {
|
|
return nil
|
|
}
|
|
return append([]WorkspaceAgentCapabilityReference(nil), references...)
|
|
}
|
|
|
|
func cloneRootProviderTurnTransition(value *WorkspaceAgentRootProviderTurnTransition) *WorkspaceAgentRootProviderTurnTransition {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
cloned := *value
|
|
cloned.CompletedCommand = cloneCompletedCommand(value.CompletedCommand)
|
|
return &cloned
|
|
}
|
|
|
|
func cloneMessageSemantics(value *WorkspaceAgentMessageSemantics) *WorkspaceAgentMessageSemantics {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
cloned := *value
|
|
return &cloned
|
|
}
|
|
|
|
func cloneStringPointer(value *string) *string {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
cloned := strings.TrimSpace(*value)
|
|
return &cloned
|
|
}
|
|
|
|
func cloneCompletedCommand(value *WorkspaceAgentCompletedCommand) *WorkspaceAgentCompletedCommand {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return &WorkspaceAgentCompletedCommand{
|
|
Kind: strings.TrimSpace(value.Kind),
|
|
Status: strings.TrimSpace(value.Status),
|
|
}
|
|
}
|
|
|
|
func cloneSubmitAvailability(value *WorkspaceAgentSubmitAvailability) *WorkspaceAgentSubmitAvailability {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return &WorkspaceAgentSubmitAvailability{
|
|
State: strings.TrimSpace(value.State),
|
|
Reason: strings.TrimSpace(value.Reason),
|
|
}
|
|
}
|
|
|
|
func cloneTurnLifecycle(value *WorkspaceAgentTurnLifecycle) *WorkspaceAgentTurnLifecycle {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return &WorkspaceAgentTurnLifecycle{
|
|
ActiveTurnID: cloneStringPointer(value.ActiveTurnID),
|
|
Phase: strings.TrimSpace(value.Phase),
|
|
Settling: value.Settling,
|
|
Outcome: cloneStringPointer(value.Outcome),
|
|
CompletedCommand: cloneCompletedCommand(value.CompletedCommand),
|
|
}
|
|
}
|
|
|
|
func cloneInteractionTransition(value *WorkspaceAgentInteractionTransition) *WorkspaceAgentInteractionTransition {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return &WorkspaceAgentInteractionTransition{
|
|
RequestID: strings.TrimSpace(value.RequestID),
|
|
TurnID: strings.TrimSpace(value.TurnID),
|
|
Kind: strings.TrimSpace(value.Kind),
|
|
Status: strings.TrimSpace(value.Status),
|
|
ToolName: strings.TrimSpace(value.ToolName),
|
|
Input: clonePayloadMap(value.Input),
|
|
Metadata: clonePayloadMap(value.Metadata),
|
|
}
|
|
}
|
|
|
|
func deriveCurrentPhaseFromEntityPatches(entities []WorkspaceAgentEntityPatch) string {
|
|
for _, entity := range entities {
|
|
switch strings.ToLower(strings.TrimSpace(entity.Status)) {
|
|
case "waiting", "waiting_input", "waiting_approval", "awaiting_approval":
|
|
return "waiting_input"
|
|
case "running", "streaming", "in_progress":
|
|
return "working"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func stringValueFromPayloadMap(payload map[string]any, key string) string {
|
|
if len(payload) == 0 {
|
|
return ""
|
|
}
|
|
value, ok := payload[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return text
|
|
}
|
|
|
|
func int64ValueFromPayloadMap(payload map[string]any, key string) int64 {
|
|
if len(payload) == 0 {
|
|
return 0
|
|
}
|
|
switch value := payload[key].(type) {
|
|
case int:
|
|
return int64(value)
|
|
case int64:
|
|
return value
|
|
case float64:
|
|
return int64(value)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func cloneConnector(connector *ConnectorInfo) *ConnectorInfo {
|
|
if connector == nil {
|
|
return nil
|
|
}
|
|
cloned := *connector
|
|
return &cloned
|
|
}
|