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
808 lines
28 KiB
Go
808 lines
28 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
)
|
|
|
|
func (s *claudeSDKAdapterSession) applySessionPayload(session *Session, payload map[string]any) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
if providerSessionID := payloadString(payload, "providerSessionId"); providerSessionID != "" {
|
|
s.providerSessionID = providerSessionID
|
|
if session != nil {
|
|
session.ProviderSessionID = providerSessionID
|
|
}
|
|
}
|
|
if resumeCursor := payloadMap(payload, "resumeCursor"); len(resumeCursor) > 0 {
|
|
s.resumeCursor = clonePayload(resumeCursor)
|
|
}
|
|
previousModel := s.currentUsageModel(nil)
|
|
if descriptors := configOptionDescriptors(payload["configOptions"]); len(descriptors) > 0 {
|
|
applyClaudeSDKConfigOptionDescriptors(&s.liveState, descriptors)
|
|
}
|
|
if model := payloadString(payload, "model"); model != "" {
|
|
_ = s.applyConfigOption("model", model)
|
|
}
|
|
s.invalidateContextUsageForModelChange(previousModel, s.currentUsageModel(nil))
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) assistantMessageID(turnID string) string {
|
|
if s.assistantMessages == nil {
|
|
s.assistantMessages = make(map[string]string)
|
|
}
|
|
if messageID := s.assistantMessages[turnID]; messageID != "" {
|
|
return messageID
|
|
}
|
|
messageID := "claude-sdk:assistant:" + turnID
|
|
s.assistantMessages[turnID] = messageID
|
|
return messageID
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) thinkingMessageID(turnID string) string {
|
|
if s.thinkingMessages == nil {
|
|
s.thinkingMessages = make(map[string]string)
|
|
}
|
|
if messageID := s.thinkingMessages[turnID]; messageID != "" {
|
|
return messageID
|
|
}
|
|
messageID := "claude-sdk:thinking:" + turnID
|
|
s.thinkingMessages[turnID] = messageID
|
|
return messageID
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKToolEvents(session Session, turnID string, payload map[string]any, eventType string, status string, sidecarType string) []activityshared.Event {
|
|
turnID = strings.TrimSpace(turnID)
|
|
if s == nil || turnID == "" {
|
|
return nil
|
|
}
|
|
isDelegation := payloadString(payload, "callType") == "subagent"
|
|
owner := s.claudeSDKToolEventOwner(turnID, payload)
|
|
events := []activityshared.Event{owner.event(session, payload, eventType, status)}
|
|
if isDelegation {
|
|
return append(events, s.updateClaudeSDKChildFromTool(session, turnID, payload, eventType, sidecarType)...)
|
|
}
|
|
return events
|
|
}
|
|
|
|
func markClaudeSDKToolProgressUpdate(event *activityshared.Event) {
|
|
if event == nil {
|
|
return
|
|
}
|
|
if event.Payload.Metadata == nil {
|
|
event.Payload.Metadata = map[string]any{}
|
|
}
|
|
event.Payload.Metadata[liveToolProgressMetadataKey] = true
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKTaskLifecycleEvents(session Session, turnID string, sidecarType string, payload map[string]any) []activityshared.Event {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
child, created, titleChanged, ok := s.updateClaudeSDKChild(session, claudeSDKChildUpdate{
|
|
Key: firstNonEmptyString(payloadString(payload, "parentToolUseId"), payloadString(payload, "toolCallId"), payloadString(payload, "agentId"), payloadString(payload, "taskId"), payloadString(payload, "task_id")),
|
|
ParentToolUseID: firstNonEmptyString(payloadString(payload, "parentToolUseId"), payloadString(payload, "toolCallId"), payloadString(payload, "callId")),
|
|
RootTurnID: turnID,
|
|
TaskID: firstNonEmptyString(payloadString(payload, "taskId"), payloadString(payload, "task_id")),
|
|
AgentID: payloadString(payload, "agentId"),
|
|
Description: payloadString(payload, "description"),
|
|
Summary: payloadString(payload, "summary"),
|
|
LastToolName: firstNonEmptyString(payloadString(payload, "lastToolName"), payloadString(payload, "last_tool_name")),
|
|
Status: claudeSDKTaskStatus(sidecarType, payloadString(payload, "status")),
|
|
Async: true,
|
|
Started: sidecarType == "task_started",
|
|
})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return claudeSDKChildEvents(session, child, created, titleChanged, sidecarType)
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKTaskResultUpdatedEvents(session Session, payload map[string]any) []activityshared.Event {
|
|
child, ok := s.claudeSDKChildForPayload(payload)
|
|
summary := strings.TrimSpace(payloadString(payload, "summary"))
|
|
if !ok || summary == "" {
|
|
return nil
|
|
}
|
|
child.Summary = summary
|
|
child.UpdatedAtUnixMS = unixMS(now())
|
|
s.childSessions[child.Key] = child
|
|
return claudeSDKChildResultEvents(session, child)
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) endUnresolvedClaudeSDKBackgroundChildren(session Session) []activityshared.Event {
|
|
if s == nil || len(s.childSessions) == 0 {
|
|
return nil
|
|
}
|
|
keys := make([]string, 0, len(s.childSessions))
|
|
for key := range s.childSessions {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
events := make([]activityshared.Event, 0)
|
|
for _, key := range keys {
|
|
child := s.childSessions[key]
|
|
if !child.Async || claudeSDKChildStatusIsTerminal(child.Status) {
|
|
continue
|
|
}
|
|
updatedAt := unixMS(now())
|
|
child.Status = "interrupted"
|
|
child.UpdatedAtUnixMS = updatedAt
|
|
child.CompletedAtUnixMS = updatedAt
|
|
s.childSessions[key] = child
|
|
events = append(events, claudeSDKChildEvents(
|
|
session,
|
|
child,
|
|
false,
|
|
false,
|
|
"background_level_ended",
|
|
)...)
|
|
}
|
|
return events
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) updateClaudeSDKChildFromTool(session Session, turnID string, payload map[string]any, _ string, sidecarType string) []activityshared.Event {
|
|
metadata := payloadMap(payload, "metadata")
|
|
if payloadString(payload, "callType") != "subagent" {
|
|
return nil
|
|
}
|
|
input := payloadMap(payload, "input")
|
|
parentToolUseID := firstNonEmptyString(payloadString(payload, "toolCallId"), payloadString(payload, "callId"))
|
|
backgroundProcess := payloadMap(metadata, "backgroundProcess")
|
|
// Sync Agent launches may still emit tool_completed with a running
|
|
// backgroundProcess while the child is waiting on approval. Keep the
|
|
// child "running" so the subagent card matches the pending interaction.
|
|
async := metadata["subagentAsync"] == true ||
|
|
payloadBoolValue(input, "run_in_background") ||
|
|
payloadString(backgroundProcess, "status") == "running"
|
|
status := string(activityshared.ActivityStatusRunning)
|
|
if sidecarType == "tool_failed" {
|
|
status = string(activityshared.ActivityStatusFailed)
|
|
} else if sidecarType == "tool_completed" && !async {
|
|
status = string(activityshared.ActivityStatusCompleted)
|
|
}
|
|
child, created, titleChanged, ok := s.updateClaudeSDKChild(session, claudeSDKChildUpdate{
|
|
Key: firstNonEmptyString(parentToolUseID, payloadString(metadata, "taskId"), payloadString(metadata, "agentId"), payloadString(metadata, "subagentAgentId")),
|
|
ParentToolUseID: parentToolUseID,
|
|
ParentChildKey: firstNonEmptyString(payloadString(metadata, "parentToolUseId"), payloadString(payload, "parentToolUseId")),
|
|
RootTurnID: turnID,
|
|
TaskID: payloadString(metadata, "taskId"),
|
|
AgentID: firstNonEmptyString(payloadString(metadata, "agentId"), payloadString(metadata, "subagentAgentId")),
|
|
Description: firstNonEmptyString(payloadString(input, "description"), payloadString(input, "prompt"), payloadString(payload, "name")),
|
|
Summary: payloadString(payloadMap(payload, "output"), "text"),
|
|
Status: status,
|
|
Async: async,
|
|
Started: true,
|
|
})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return claudeSDKChildEvents(session, child, created, titleChanged, sidecarType)
|
|
}
|
|
|
|
type claudeSDKChildUpdate struct {
|
|
Key string
|
|
ParentToolUseID string
|
|
ParentChildKey string
|
|
RootTurnID string
|
|
TaskID string
|
|
AgentID string
|
|
Description string
|
|
Status string
|
|
Summary string
|
|
LastToolName string
|
|
Async bool
|
|
Started bool
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) updateClaudeSDKChild(session Session, update claudeSDKChildUpdate) (claudeSDKChildSession, bool, bool, bool) {
|
|
if s == nil {
|
|
return claudeSDKChildSession{}, false, false, false
|
|
}
|
|
key := strings.TrimSpace(update.Key)
|
|
if key == "" {
|
|
return claudeSDKChildSession{}, false, false, false
|
|
}
|
|
if s.childSessions == nil {
|
|
s.childSessions = make(map[string]claudeSDKChildSession)
|
|
}
|
|
key = s.resolveClaudeSDKChildKey(update, key)
|
|
updatedAt := unixMS(now())
|
|
child := s.childSessions[key]
|
|
created := child.Key == ""
|
|
if created {
|
|
child.Key = key
|
|
child.AgentSessionID = newID()
|
|
child.TurnID = newID()
|
|
if parent := s.claudeSDKChildByKey(update.ParentChildKey); parent.AgentSessionID != "" {
|
|
child.RootAgentSessionID = parent.RootAgentSessionID
|
|
child.RootTurnID = parent.RootTurnID
|
|
child.ParentAgentSessionID = parent.AgentSessionID
|
|
child.ParentTurnID = parent.TurnID
|
|
} else {
|
|
child.RootAgentSessionID = strings.TrimSpace(session.AgentSessionID)
|
|
child.RootTurnID = strings.TrimSpace(update.RootTurnID)
|
|
child.ParentAgentSessionID = strings.TrimSpace(session.AgentSessionID)
|
|
child.ParentTurnID = strings.TrimSpace(update.RootTurnID)
|
|
}
|
|
}
|
|
child.UpdatedAtUnixMS = updatedAt
|
|
if update.ParentToolUseID != "" && (child.ParentToolUseID == "" || child.ParentToolUseID == update.ParentToolUseID) {
|
|
child.ParentToolUseID = update.ParentToolUseID
|
|
}
|
|
if update.TaskID != "" && (child.TaskID == "" || child.TaskID == update.TaskID) && !s.claudeSDKChildAliasBelongsToOtherKey(key, update.TaskID, func(child claudeSDKChildSession) string {
|
|
return child.TaskID
|
|
}) {
|
|
child.TaskID = update.TaskID
|
|
}
|
|
if update.AgentID != "" && (child.AgentID == "" || child.AgentID == update.AgentID) && !s.claudeSDKChildAliasBelongsToOtherKey(key, update.AgentID, func(child claudeSDKChildSession) string {
|
|
return child.AgentID
|
|
}) {
|
|
child.AgentID = update.AgentID
|
|
}
|
|
previousDescription := strings.TrimSpace(child.Description)
|
|
nextDescription := strings.TrimSpace(update.Description)
|
|
titleChanged := nextDescription != "" && nextDescription != previousDescription
|
|
if nextDescription != "" {
|
|
child.Description = nextDescription
|
|
}
|
|
if update.Summary != "" {
|
|
child.Summary = update.Summary
|
|
}
|
|
if update.LastToolName != "" {
|
|
child.LastToolName = update.LastToolName
|
|
}
|
|
child.Async = child.Async || update.Async
|
|
child.Status = claudeSDKMergeChildStatus(child.Status, update.Status)
|
|
if update.Started && child.StartedAtUnixMS == 0 {
|
|
child.StartedAtUnixMS = updatedAt
|
|
}
|
|
if claudeSDKChildStatusIsTerminal(child.Status) && child.CompletedAtUnixMS == 0 {
|
|
child.CompletedAtUnixMS = updatedAt
|
|
}
|
|
s.childSessions[key] = child
|
|
return child, created, titleChanged && !created, true
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) resolveClaudeSDKChildKey(update claudeSDKChildUpdate, fallback string) string {
|
|
parentID := strings.TrimSpace(update.ParentToolUseID)
|
|
if parentID != "" {
|
|
if resolved := s.claudeSDKChildKeyByAlias(parentID); resolved != "" {
|
|
return resolved
|
|
}
|
|
// The Task tool call id is the canonical child-session key. An
|
|
// update that carries one may merge through weaker task/agent aliases
|
|
// only into an entry that does not already belong to a different
|
|
// parent tool call; otherwise a poisoned alias would fold two
|
|
// concurrent child sessions into one entry.
|
|
for _, alias := range []string{update.AgentID, update.TaskID, update.Key} {
|
|
resolved := s.claudeSDKChildKeyByAlias(alias)
|
|
if resolved == "" {
|
|
continue
|
|
}
|
|
existingParent := strings.TrimSpace(s.childSessions[resolved].ParentToolUseID)
|
|
if existingParent == "" || existingParent == parentID {
|
|
return resolved
|
|
}
|
|
}
|
|
return parentID
|
|
}
|
|
keys := []string{
|
|
update.AgentID,
|
|
update.TaskID,
|
|
update.Key,
|
|
}
|
|
for _, key := range keys {
|
|
if resolved := s.claudeSDKChildKeyByAlias(key); resolved != "" {
|
|
return resolved
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKChildKeyByAlias(alias string) string {
|
|
alias = strings.TrimSpace(alias)
|
|
if alias == "" || s == nil {
|
|
return ""
|
|
}
|
|
if child := s.childSessions[alias]; child.TurnID != "" || child.Key != "" {
|
|
return alias
|
|
}
|
|
for key, child := range s.childSessions {
|
|
if alias == child.ParentToolUseID || alias == child.AgentID || alias == child.TaskID {
|
|
return key
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKChildAliasBelongsToOtherKey(currentKey string, alias string, selectAlias func(claudeSDKChildSession) string) bool {
|
|
alias = strings.TrimSpace(alias)
|
|
if alias == "" || s == nil {
|
|
return false
|
|
}
|
|
for key, child := range s.childSessions {
|
|
if key == currentKey {
|
|
continue
|
|
}
|
|
if alias == strings.TrimSpace(selectAlias(child)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKChildForPayload(payload map[string]any) (claudeSDKChildSession, bool) {
|
|
if s == nil || len(s.childSessions) == 0 {
|
|
return claudeSDKChildSession{}, false
|
|
}
|
|
metadata := payloadMap(payload, "metadata")
|
|
keys := []string{
|
|
payloadString(payload, "taskId"),
|
|
payloadString(payload, "task_id"),
|
|
payloadString(metadata, "taskId"),
|
|
payloadString(payload, "agentId"),
|
|
payloadString(metadata, "agentId"),
|
|
payloadString(metadata, "subagentAgentId"),
|
|
payloadString(metadata, "parentToolUseId"),
|
|
payloadString(payload, "parentToolUseId"),
|
|
payloadString(payload, "toolCallId"),
|
|
payloadString(payload, "callId"),
|
|
}
|
|
for _, key := range keys {
|
|
if child := s.claudeSDKChildByKey(key); child.TurnID != "" {
|
|
return child, true
|
|
}
|
|
}
|
|
return claudeSDKChildSession{}, false
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKDelegationParentForPayload(payload map[string]any) (claudeSDKChildSession, bool) {
|
|
if s == nil || len(s.childSessions) == 0 {
|
|
return claudeSDKChildSession{}, false
|
|
}
|
|
metadata := payloadMap(payload, "metadata")
|
|
parentToolUseID := firstNonEmptyString(
|
|
payloadString(metadata, "parentToolUseId"),
|
|
payloadString(payload, "parentToolUseId"),
|
|
)
|
|
if parentToolUseID == "" {
|
|
return claudeSDKChildSession{}, false
|
|
}
|
|
parent := s.claudeSDKChildByKey(parentToolUseID)
|
|
return parent, parent.TurnID != ""
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKChildByKey(key string) claudeSDKChildSession {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" || s == nil {
|
|
return claudeSDKChildSession{}
|
|
}
|
|
if resolved := s.claudeSDKChildKeyByAlias(key); resolved != "" {
|
|
return s.childSessions[resolved]
|
|
}
|
|
return claudeSDKChildSession{}
|
|
}
|
|
|
|
func (s *claudeSDKAdapterSession) claudeSDKChildByAgentSessionID(agentSessionID string) (claudeSDKChildSession, bool) {
|
|
agentSessionID = strings.TrimSpace(agentSessionID)
|
|
if s == nil || agentSessionID == "" {
|
|
return claudeSDKChildSession{}, false
|
|
}
|
|
for _, child := range s.childSessions {
|
|
if child.AgentSessionID == agentSessionID {
|
|
return child, true
|
|
}
|
|
}
|
|
return claudeSDKChildSession{}, false
|
|
}
|
|
|
|
func claudeSDKChildRuntimeSession(root Session, child claudeSDKChildSession) Session {
|
|
root.AgentSessionID = child.AgentSessionID
|
|
root.ProviderSessionID = firstNonEmptyString(child.TaskID, child.AgentID, child.Key)
|
|
root.Title = child.Description
|
|
root.Status = SessionStatusWorking
|
|
root.TurnLifecycle = nil
|
|
root.SubmitAvailability = blockedSubmitAvailability("active_turn")
|
|
return root
|
|
}
|
|
|
|
func claudeSDKChildEventContext(session Session, child claudeSDKChildSession, eventID string) (activityshared.EventContext, bool) {
|
|
ctx, ok := activityEventContext(claudeSDKChildRuntimeSession(session, child), eventID, child.TurnID)
|
|
if !ok {
|
|
return activityshared.EventContext{}, false
|
|
}
|
|
ctx.SessionKind = "child"
|
|
ctx.RootAgentSessionID = child.RootAgentSessionID
|
|
ctx.RootTurnID = child.RootTurnID
|
|
ctx.ParentAgentSessionID = child.ParentAgentSessionID
|
|
ctx.ParentTurnID = child.ParentTurnID
|
|
ctx.ParentToolCallID = child.ParentToolUseID
|
|
return ctx, true
|
|
}
|
|
|
|
func claudeSDKEventForChild(event activityshared.Event, child claudeSDKChildSession) activityshared.Event {
|
|
event.AgentSessionID = child.AgentSessionID
|
|
event.ProviderSessionID = firstNonEmptyString(child.TaskID, child.AgentID, child.Key)
|
|
event.SessionKind = "child"
|
|
event.RootAgentSessionID = child.RootAgentSessionID
|
|
event.RootTurnID = child.RootTurnID
|
|
event.ParentAgentSessionID = child.ParentAgentSessionID
|
|
event.ParentTurnID = child.ParentTurnID
|
|
event.ParentToolCallID = child.ParentToolUseID
|
|
return event
|
|
}
|
|
|
|
func claudeSDKChildEvents(session Session, child claudeSDKChildSession, created bool, titleChanged bool, sidecarType string) []activityshared.Event {
|
|
baseEventID := "claude-child:" + child.Key + ":" + sidecarType + ":" + newID()
|
|
ctx, ok := claudeSDKChildEventContext(session, child, baseEventID)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
eventContext := func(suffix string) activityshared.EventContext {
|
|
next := ctx
|
|
next.EventID = baseEventID + ":" + suffix
|
|
return next
|
|
}
|
|
events := make([]activityshared.Event, 0, 4)
|
|
if created {
|
|
sessionContext := eventContext("session-started")
|
|
sessionContext.Title = child.Description
|
|
events = append(events, activityshared.NewChildSessionStarted(sessionContext, child.TurnID))
|
|
} else if titleChanged {
|
|
titleContext := eventContext("session-title-updated")
|
|
titleContext.Title = child.Description
|
|
events = append(events, activityshared.NewSessionTitleUpdated(titleContext))
|
|
}
|
|
metadata := claudeSDKChildMetadata(child)
|
|
activityKey := "claude-sdk-child:" + child.Key
|
|
switch sidecarType {
|
|
case "task_started":
|
|
if created {
|
|
events = append(events, activityshared.NewTurnStarted(eventContext("turn-started"), child.TurnID))
|
|
}
|
|
events = append(events, activityshared.NewActivityStarted(eventContext("activity-started"), activityKey, metadata))
|
|
case "task_progress":
|
|
events = append(events, activityshared.NewTurnUpdated(eventContext("turn-updated"), child.TurnID, activityshared.TurnPhaseWorking))
|
|
events = append(events, activityshared.NewActivityUpdated(eventContext("activity-updated"), activityKey, metadata))
|
|
case "task_completed":
|
|
events = append(events, claudeSDKChildResultEvents(session, child)...)
|
|
switch claudeSDKNormalizeTaskStatus(child.Status) {
|
|
case string(activityshared.ActivityStatusFailed):
|
|
events = append(events, activityshared.NewActivityFailed(eventContext("activity-failed"), activityKey, metadata))
|
|
events = append(events, activityshared.NewTurnFailed(eventContext("turn-failed"), child.TurnID))
|
|
case "stopped":
|
|
events = append(events, activityshared.NewActivityCompleted(eventContext("activity-completed"), activityKey, metadata))
|
|
events = append(events, activityshared.NewTurnCanceled(eventContext("turn-canceled"), child.TurnID))
|
|
default:
|
|
events = append(events, activityshared.NewActivityCompleted(eventContext("activity-completed"), activityKey, metadata))
|
|
events = append(events, activityshared.NewTurnCompleted(eventContext("turn-completed"), child.TurnID, activityshared.TurnOutcomeCompleted))
|
|
}
|
|
case "background_level_ended":
|
|
events = append(events, activityshared.NewActivityCompleted(eventContext("activity-completed"), activityKey, metadata))
|
|
events = append(events, activityshared.NewTurnCompleted(eventContext("turn-completed"), child.TurnID, activityshared.TurnOutcomeInterrupted))
|
|
case "tool_started", "tool_updated":
|
|
if created {
|
|
events = append(events, activityshared.NewTurnStarted(eventContext("turn-started"), child.TurnID))
|
|
}
|
|
case "tool_completed":
|
|
if created {
|
|
events = append(events, activityshared.NewTurnStarted(eventContext("turn-started"), child.TurnID))
|
|
}
|
|
if !child.Async {
|
|
events = append(events, activityshared.NewTurnCompleted(eventContext("turn-completed"), child.TurnID, activityshared.TurnOutcomeCompleted))
|
|
}
|
|
case "tool_failed":
|
|
if created {
|
|
events = append(events, activityshared.NewTurnStarted(eventContext("turn-started"), child.TurnID))
|
|
}
|
|
events = append(events, activityshared.NewTurnFailed(eventContext("turn-failed"), child.TurnID))
|
|
}
|
|
for index := range events {
|
|
events[index] = claudeSDKEventForChild(events[index], child)
|
|
}
|
|
return events
|
|
}
|
|
|
|
func claudeSDKChildResultEvents(session Session, child claudeSDKChildSession) []activityshared.Event {
|
|
if strings.TrimSpace(child.Summary) == "" {
|
|
return nil
|
|
}
|
|
messageID := "claude-sdk:child-result:" + child.Key
|
|
resultEvent := newTurnActivityEventWithIDAt(
|
|
claudeSDKChildRuntimeSession(session, child),
|
|
"claude-sdk:child-result-event:"+newID(),
|
|
EventMessage,
|
|
child.TurnID,
|
|
messageStreamStateCompleted,
|
|
RoleAssistant,
|
|
child.Summary,
|
|
map[string]any{
|
|
"messageId": messageID,
|
|
"contentMode": messageContentModeSnapshot,
|
|
"streamState": messageStreamStateCompleted,
|
|
},
|
|
child.UpdatedAtUnixMS,
|
|
)
|
|
return []activityshared.Event{claudeSDKEventForChild(resultEvent, child)}
|
|
}
|
|
|
|
func claudeSDKChildStatusIsTerminal(status string) bool {
|
|
switch claudeSDKNormalizeTaskStatus(status) {
|
|
case string(activityshared.ActivityStatusCompleted), string(activityshared.ActivityStatusFailed), "stopped", "interrupted":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func claudeSDKChildMetadata(child claudeSDKChildSession) map[string]any {
|
|
metadata := map[string]any{
|
|
"kind": "child_agent",
|
|
"taskId": firstNonEmptyString(child.TaskID, child.Key),
|
|
"description": child.Description,
|
|
"status": firstNonEmptyString(child.Status, string(activityshared.ActivityStatusRunning)),
|
|
"title": child.Description,
|
|
}
|
|
if child.ParentToolUseID != "" {
|
|
metadata["parentToolUseId"] = child.ParentToolUseID
|
|
}
|
|
if child.AgentID != "" {
|
|
metadata["agentId"] = child.AgentID
|
|
}
|
|
if child.Summary != "" {
|
|
metadata["summary"] = child.Summary
|
|
}
|
|
if child.LastToolName != "" {
|
|
metadata["lastToolName"] = child.LastToolName
|
|
}
|
|
if child.StartedAtUnixMS > 0 {
|
|
metadata["startedAtUnixMs"] = child.StartedAtUnixMS
|
|
}
|
|
if child.UpdatedAtUnixMS > 0 {
|
|
metadata["updatedAtUnixMs"] = child.UpdatedAtUnixMS
|
|
}
|
|
if child.CompletedAtUnixMS > 0 {
|
|
metadata["completedAtUnixMs"] = child.CompletedAtUnixMS
|
|
}
|
|
return metadata
|
|
}
|
|
|
|
func claudeSDKTaskStatus(sidecarType string, status string) string {
|
|
if normalized := claudeSDKNormalizeTaskStatus(status); normalized != "" {
|
|
return normalized
|
|
}
|
|
switch sidecarType {
|
|
case "task_completed":
|
|
return string(activityshared.ActivityStatusCompleted)
|
|
default:
|
|
return string(activityshared.ActivityStatusRunning)
|
|
}
|
|
}
|
|
|
|
func claudeSDKNormalizeTaskStatus(status string) string {
|
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
|
case "failed", "error", "errored":
|
|
return string(activityshared.ActivityStatusFailed)
|
|
case "completed", "done", "success", "succeeded":
|
|
return string(activityshared.ActivityStatusCompleted)
|
|
case "stopped", "cancelled", "canceled":
|
|
return "stopped"
|
|
case "interrupted":
|
|
return "interrupted"
|
|
case "running", "in_progress", "pending":
|
|
return string(activityshared.ActivityStatusRunning)
|
|
default:
|
|
return strings.TrimSpace(status)
|
|
}
|
|
}
|
|
|
|
// claudeSDKPlanEvents maps the sidecar's plan_updated entries (SDK task list)
|
|
// onto the same synthesized update_todo tool call codex publishes for plan
|
|
// updates, so the GUI plan rendering works identically across providers.
|
|
func claudeSDKPlanEvents(session Session, turnID string, payload map[string]any) []activityshared.Event {
|
|
entries, _ := payload["entries"].([]any)
|
|
if len(entries) == 0 || strings.TrimSpace(turnID) == "" {
|
|
return nil
|
|
}
|
|
todos := make([]any, 0, len(entries))
|
|
for _, entry := range entries {
|
|
item := payloadObject(entry)
|
|
text := asStringRaw(item["content"])
|
|
if text == "" {
|
|
continue
|
|
}
|
|
todos = append(todos, map[string]any{
|
|
"content": text,
|
|
"status": appServerItemStatus(asString(item["status"])),
|
|
})
|
|
}
|
|
if len(todos) == 0 {
|
|
return nil
|
|
}
|
|
return []activityshared.Event{claudeSDKToolActivityEvent(session, turnID, map[string]any{
|
|
"toolCallId": "plan:" + strings.TrimSpace(turnID),
|
|
"name": "update_todo",
|
|
"input": map[string]any{"todos": todos},
|
|
"metadata": map[string]any{"kind": "think"},
|
|
}, EventCallCompleted, messageStreamStateCompleted)}
|
|
}
|
|
|
|
func claudeSDKToolActivityEvent(session Session, turnID string, payload map[string]any, eventType string, status string) activityshared.Event {
|
|
payload = normalizeClaudeSDKToolPayload(payload)
|
|
callID := firstNonEmpty(
|
|
payloadString(payload, "toolCallId"),
|
|
payloadString(payload, "callId"),
|
|
payloadString(payload, "id"),
|
|
newID(),
|
|
)
|
|
name := firstNonEmpty(payloadString(payload, "name"), payloadString(payload, "toolName"), callID)
|
|
metadata := map[string]any{
|
|
"adapter": claudeSDKSidecarAdapterName,
|
|
"callId": callID,
|
|
"callType": firstNonEmpty(payloadString(payload, "callType"), "tool"),
|
|
"name": name,
|
|
"status": status,
|
|
}
|
|
if toolName := payloadString(payload, "toolName"); toolName != "" {
|
|
metadata["toolName"] = toolName
|
|
}
|
|
if input := payloadMap(payload, "input"); len(input) > 0 {
|
|
metadata["input"] = input
|
|
}
|
|
if output := payloadMap(payload, "output"); len(output) > 0 {
|
|
metadata["output"] = output
|
|
}
|
|
if errorPayload := payloadMap(payload, "error"); len(errorPayload) > 0 {
|
|
metadata["error"] = errorPayload
|
|
}
|
|
if locations, ok := payload["locations"].([]any); ok && len(locations) > 0 {
|
|
metadata["locations"] = locations
|
|
}
|
|
if content, ok := payload["content"].([]any); ok && len(content) > 0 {
|
|
metadata["content"] = content
|
|
}
|
|
if sidecarMetadata := claudeSDKCanonicalToolMetadata(payloadMap(payload, "metadata")); len(sidecarMetadata) > 0 {
|
|
metadata["metadata"] = sidecarMetadata
|
|
if parentToolUseID := payloadString(sidecarMetadata, "parentToolUseId"); parentToolUseID != "" {
|
|
metadata["parentToolUseId"] = parentToolUseID
|
|
}
|
|
}
|
|
body := map[string]any(nil)
|
|
switch eventType {
|
|
case EventCallCompleted:
|
|
body = payloadMap(payload, "output")
|
|
case EventCallFailed:
|
|
body = payloadMap(payload, "error")
|
|
default:
|
|
body = payloadMap(payload, "input")
|
|
}
|
|
return newTurnActivityEventWithID(session, "claude-sdk:tool:"+callID, eventType, turnID, status, "", name, payloadWithCallBody(claudeSDKCallBodyKey(eventType), body, metadata))
|
|
}
|
|
|
|
func normalizeClaudeSDKToolPayload(payload map[string]any) map[string]any {
|
|
normalized, _ := normalizeClaudeSDKToolPayloadValue(payload, "").(map[string]any)
|
|
return normalized
|
|
}
|
|
|
|
func normalizeClaudeSDKToolPayloadValue(value any, field string) any {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
normalized := make(map[string]any, len(typed))
|
|
for key, entry := range typed {
|
|
normalized[key] = normalizeClaudeSDKToolPayloadValue(entry, key)
|
|
}
|
|
return normalized
|
|
case []any:
|
|
normalized := make([]any, len(typed))
|
|
for index, entry := range typed {
|
|
normalized[index] = normalizeClaudeSDKToolPayloadValue(entry, field)
|
|
}
|
|
return normalized
|
|
case string:
|
|
if !claudeSDKDiffField(field) {
|
|
return typed
|
|
}
|
|
return normalizeClaudeSDKUnifiedDiff(typed)
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
func claudeSDKDiffField(field string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(field)) {
|
|
case "diff", "patch", "unifieddiff", "unified_diff":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func normalizeClaudeSDKUnifiedDiff(diff string) string {
|
|
const noNewlineMarker = `\ No newline at end of file`
|
|
lines := strings.Split(strings.ReplaceAll(diff, "\r\n", "\n"), "\n")
|
|
for index, line := range lines {
|
|
if strings.TrimLeft(line, " \t") == noNewlineMarker {
|
|
lines[index] = noNewlineMarker
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func claudeSDKCallBodyKey(eventType string) string {
|
|
switch eventType {
|
|
case EventCallCompleted:
|
|
return "output"
|
|
case EventCallFailed:
|
|
return "error"
|
|
default:
|
|
return "input"
|
|
}
|
|
}
|
|
|
|
func (a *ClaudeCodeSDKAdapter) compactMessageEvent(
|
|
adapterSession *claudeSDKAdapterSession,
|
|
session Session,
|
|
turnID string,
|
|
streamState string,
|
|
noticeStatus string,
|
|
detail string,
|
|
) (activityshared.Event, bool) {
|
|
a.mu.Lock()
|
|
compact := adapterSession.compactMessages[turnID]
|
|
if compact.messageID == "" {
|
|
compact.messageID = "claude-sdk:compact:" + turnID
|
|
}
|
|
if compact.terminalStatus != "" {
|
|
a.mu.Unlock()
|
|
return activityshared.Event{}, false
|
|
}
|
|
if noticeStatus == "running" {
|
|
compact.active = true
|
|
} else {
|
|
compact.active = false
|
|
compact.terminalStatus = noticeStatus
|
|
}
|
|
if adapterSession.compactMessages == nil {
|
|
adapterSession.compactMessages = make(map[string]claudeSDKCompactMessage)
|
|
}
|
|
adapterSession.compactMessages[turnID] = compact
|
|
a.mu.Unlock()
|
|
return claudeSDKCompactMessageEvent(session, turnID, compact.messageID, streamState, noticeStatus, detail), true
|
|
}
|
|
|
|
func claudeSDKCompactMessageEvent(
|
|
session Session,
|
|
turnID string,
|
|
messageID string,
|
|
streamState string,
|
|
noticeStatus string,
|
|
detail string,
|
|
) activityshared.Event {
|
|
title := appServerCompactingContextTitle
|
|
if noticeStatus == "completed" {
|
|
title = appServerContextCompactedTitle
|
|
}
|
|
if noticeStatus == "failed" || noticeStatus == "canceled" {
|
|
title = appServerCompactionInterruptedTitle
|
|
}
|
|
metadata := map[string]any{
|
|
"adapter": claudeSDKSidecarAdapterName,
|
|
"messageId": messageID,
|
|
"contentMode": messageContentModeSnapshot,
|
|
"source": "compact",
|
|
"kind": "agent_system_notice",
|
|
"noticeKind": "system_notice",
|
|
"noticeCommand": "compact",
|
|
"noticeCommandStatus": noticeStatus,
|
|
"title": title,
|
|
}
|
|
if strings.TrimSpace(detail) != "" {
|
|
metadata["detail"] = strings.TrimSpace(detail)
|
|
}
|
|
return newTurnActivityEventWithID(session, messageID, EventMessage, turnID, streamState, RoleAssistant, title, metadata)
|
|
}
|