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

643 lines
18 KiB
Go

package agentruntime
import (
"encoding/json"
"errors"
"strings"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
)
func acpModeValue(update map[string]any) string {
return firstNonEmpty(
// `currentModeId` is the ACP-canonical field on a current_mode_update
// notification; the rest are tolerated fallbacks for other shapes.
asString(update["currentModeId"]),
asString(update["current_mode_id"]),
asString(update["mode"]),
asString(update["modeId"]),
asString(update["mode_id"]),
asString(update["name"]),
asString(update["value"]),
)
}
func acpCommandsValue(update map[string]any) ([]AgentSessionCommand, bool) {
commands := make([]AgentSessionCommand, 0)
found := false
entryCount := 0
appendCommand := func(command AgentSessionCommand) {
command.Name = strings.TrimSpace(command.Name)
command.Description = strings.TrimSpace(command.Description)
command.InputHint = strings.TrimSpace(command.InputHint)
if command.Name != "" {
commands = append(commands, command)
}
}
for _, key := range []string{"commands", "availableCommands", "available_commands"} {
values, ok := update[key].([]any)
if !ok {
continue
}
found = true
entryCount += len(values)
for _, value := range values {
switch typed := value.(type) {
case string:
appendCommand(AgentSessionCommand{Name: typed})
case map[string]any:
appendCommand(AgentSessionCommand{
Name: firstNonEmpty(
asString(typed["name"]),
asString(typed["id"]),
asString(typed["command"]),
),
Description: firstNonEmpty(
asString(typed["description"]),
asString(typed["summary"]),
),
InputHint: acpCommandInputHint(typed),
})
}
}
}
if !found {
return nil, false
}
commands = dedupeAgentSessionCommands(commands)
if entryCount > 0 && len(commands) == 0 {
return nil, false
}
return commands, true
}
func acpCommandInputHint(command map[string]any) string {
if hint := firstNonEmpty(
asString(command["inputHint"]),
asString(command["input_hint"]),
asString(command["hint"]),
); hint != "" {
return hint
}
if input, ok := command["input"].(map[string]any); ok {
return firstNonEmpty(
asString(input["hint"]),
asString(input["inputHint"]),
asString(input["input_hint"]),
)
}
return ""
}
func dedupeAgentSessionCommands(commands []AgentSessionCommand) []AgentSessionCommand {
if len(commands) == 0 {
return []AgentSessionCommand{}
}
seen := make(map[string]struct{}, len(commands))
out := make([]AgentSessionCommand, 0, len(commands))
for _, command := range commands {
command.Name = strings.TrimSpace(command.Name)
command.Description = strings.TrimSpace(command.Description)
command.InputHint = strings.TrimSpace(command.InputHint)
if command.Name == "" {
continue
}
if _, ok := seen[command.Name]; ok {
continue
}
seen[command.Name] = struct{}{}
out = append(out, command)
}
return out
}
func agentSessionCommandNames(commands []AgentSessionCommand) []string {
if len(commands) == 0 {
return []string{}
}
names := make([]string, 0, len(commands))
for _, command := range commands {
if name := strings.TrimSpace(command.Name); name != "" {
names = append(names, name)
}
}
return names
}
func agentSessionCommandsRuntimeContext(commands []AgentSessionCommand) []map[string]any {
if len(commands) == 0 {
return []map[string]any{}
}
result := make([]map[string]any, 0, len(commands))
for _, command := range commands {
name := strings.TrimSpace(command.Name)
if name == "" {
continue
}
value := map[string]any{"name": name}
if description := strings.TrimSpace(command.Description); description != "" {
value["description"] = description
}
if inputHint := strings.TrimSpace(command.InputHint); inputHint != "" {
value["inputHint"] = inputHint
}
result = append(result, value)
}
return result
}
func acpConfigValues(update map[string]any) map[string]any {
values := map[string]any{}
for _, key := range []string{"config", "option", "options"} {
if object, ok := update[key].(map[string]any); ok {
for objectKey, objectValue := range object {
if strings.TrimSpace(objectKey) != "" {
values[objectKey] = objectValue
}
}
}
}
configKey := firstNonEmpty(
asString(update["key"]),
asString(update["optionId"]),
asString(update["option_id"]),
asString(update["name"]),
)
if configKey != "" {
if value, ok := update["value"]; ok {
values[configKey] = value
}
}
return values
}
func dedupeStrings(values []string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for _, value := range values {
normalized := strings.TrimSpace(value)
if normalized == "" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
out = append(out, normalized)
}
return out
}
func codexACPEnv(session Session, host HostMetadata) []string {
env := []string{
codexAgentRoutingEnv,
codexRoutingPreload,
"NO_BROWSER=1",
}
env = append(env, workspaceEnv(session, host)...)
return env
}
func acpSessionID(raw json.RawMessage) (string, error) {
var result struct {
SessionID string `json:"sessionId"`
}
if err := json.Unmarshal(raw, &result); err != nil {
return "", err
}
if strings.TrimSpace(result.SessionID) == "" {
return "", errors.New("ACP session/new returned empty sessionId")
}
return strings.TrimSpace(result.SessionID), nil
}
func acpAgentInfo(raw json.RawMessage) map[string]any {
var result struct {
AgentInfo map[string]any `json:"agentInfo"`
}
if err := json.Unmarshal(raw, &result); err != nil || result.AgentInfo == nil {
return map[string]any{}
}
return result.AgentInfo
}
func acpResumeMethod(raw json.RawMessage) string {
var result map[string]any
if len(raw) == 0 || json.Unmarshal(raw, &result) != nil {
return ""
}
if sessionCapabilitySupported(result, "resume") ||
truthyNested(result, "agentCapabilities", "resumeSession") ||
truthyNested(result, "agentCapabilities", "resume") {
return acpMethodResume
}
if sessionCapabilitySupported(result, "load") ||
sessionCapabilitySupported(result, "loadSession") ||
truthyNested(result, "agentCapabilities", "loadSession") ||
truthyNested(result, "agentCapabilities", "load") {
return acpMethodLoadSession
}
return ""
}
func sessionCapabilitySupported(value map[string]any, fieldKey string) bool {
if nestedCapabilitySupported(value, "sessionCapabilities", fieldKey) {
return true
}
agentCapabilities, ok := value["agentCapabilities"].(map[string]any)
return ok && nestedCapabilitySupported(agentCapabilities, "sessionCapabilities", fieldKey)
}
func nestedCapabilitySupported(value map[string]any, objectKey, fieldKey string) bool {
nested, ok := value[objectKey].(map[string]any)
if !ok {
return false
}
field, exists := nested[fieldKey]
if !exists || field == nil {
return false
}
// ACP session capabilities use an object whose presence declares support;
// older agents may still publish the same capability as a boolean.
if _, ok := field.(map[string]any); ok {
return true
}
return truthyNested(value, objectKey, fieldKey)
}
func truthyNested(value map[string]any, objectKey, fieldKey string) bool {
nested, ok := value[objectKey].(map[string]any)
if !ok {
return false
}
switch field := nested[fieldKey].(type) {
case bool:
return field
case string:
return strings.EqualFold(strings.TrimSpace(field), "true")
default:
return false
}
}
func acpStopReason(raw json.RawMessage) string {
var result struct {
StopReason string `json:"stopReason"`
}
if err := json.Unmarshal(raw, &result); err != nil {
return ""
}
return canonicalACPStopReason(result.StopReason)
}
func canonicalACPStopReason(reason string) string {
trimmed := strings.TrimSpace(reason)
switch strings.ToLower(trimmed) {
case "cancelled":
return SessionStatusCanceled
default:
return trimmed
}
}
func acpPromptResultAssistantText(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var decoded any
if err := json.Unmarshal(raw, &decoded); err != nil {
return ""
}
return acpTextFromValue(decoded)
}
func acpTextFromValue(value any) string {
switch typed := value.(type) {
case string:
return typed
case []any:
parts := make([]string, 0, len(typed))
for _, item := range typed {
if text := acpTextFromValue(item); strings.TrimSpace(text) != "" {
parts = append(parts, text)
}
}
return strings.Join(parts, "")
case map[string]any:
if role := strings.TrimSpace(asString(typed["role"])); role != "" && role != "assistant" && role != "agent" {
return ""
}
if text, ok := typed["text"].(string); ok && strings.TrimSpace(text) != "" {
return text
}
for _, key := range []string{"content", "message", "output", "result"} {
if text := acpTextFromValue(typed[key]); strings.TrimSpace(text) != "" {
return text
}
}
if messages, ok := typed["messages"].([]any); ok {
for i := len(messages) - 1; i >= 0; i-- {
if text := acpTextFromValue(messages[i]); strings.TrimSpace(text) != "" {
return text
}
}
}
}
return ""
}
func acpSystemNoticeEvents(session Session, turnID string, update map[string]any, normalizer *acpTurnNormalizer, fallbackKind string, allowSyntheticNotice bool) ([]activityshared.Event, bool) {
event, ok := acpSystemNoticeEvent(session, turnID, update, fallbackKind, allowSyntheticNotice)
if !ok {
return nil, false
}
normalizer.MarkSystemNoticeOutput()
return []activityshared.Event{event}, true
}
func acpFailureMetadata(err error) map[string]any {
if err == nil {
return nil
}
payload := map[string]any{"error": sanitizeProviderFailureText(err.Error())}
var transportFailure RuntimeTransportFailure
if errors.As(err, &transportFailure) {
if code := boundedRuntimeTransportFailureCode(transportFailure.RuntimeTransportFailureCode()); code != "" {
payload["errorCode"] = code
payload["transportReason"] = code
}
var grpcFailure RuntimeTransportGRPCFailure
if errors.As(err, &grpcFailure) {
if code := strings.TrimSpace(grpcFailure.RuntimeTransportGRPCCode()); code != "" && len(code) <= 32 {
payload["transportGrpcCode"] = code
}
}
}
var callErr *acpCallError
if !errors.As(err, &callErr) {
return payload
}
for key, value := range failureFromACPCall(callErr).metadata() {
payload[key] = value
}
payload["acpErrorCode"] = callErr.Err.Code
if message := strings.TrimSpace(callErr.Err.Message); message != "" {
payload["acpErrorMessage"] = sanitizeProviderFailureText(message)
}
data := acpErrorDataPayload(callErr.Err.Data)
if rawData := sanitizeProviderFailureText(string(callErr.Err.Data)); rawData != "" && rawData != "null" {
payload["acpErrorData"] = rawData
}
if message := strings.TrimSpace(asString(data["message"])); message != "" {
payload["error"] = sanitizeProviderFailureText(message)
payload["errorMessage"] = sanitizeProviderFailureText(message)
}
if codexErrorInfo := clonePayloadValue(firstPresentAny(data["codex_error_info"], data["codexErrorInfo"])); codexErrorInfo != nil {
payload["codexErrorInfo"] = codexErrorInfo
}
return payload
}
func boundedRuntimeTransportFailureCode(value string) string {
value = strings.TrimSpace(value)
if value == "" || len(value) > 80 {
return ""
}
for _, character := range value {
if (character < 'a' || character > 'z') &&
(character < '0' || character > '9') && character != '_' {
return ""
}
}
return value
}
func acpErrorDataPayload(raw json.RawMessage) map[string]any {
if len(raw) == 0 {
return nil
}
var payload map[string]any
if err := json.Unmarshal(raw, &payload); err != nil {
return nil
}
return payload
}
func acpSystemNoticeEvent(session Session, turnID string, update map[string]any, fallbackKind string, allowSyntheticNotice bool) (activityshared.Event, bool) {
notice := acpSystemNoticePayload(update)
updateType := asString(update["sessionUpdate"])
if len(notice) == 0 {
if !allowSyntheticNotice {
return activityshared.Event{}, false
}
switch updateType {
case "stream_error", "warning", "system_notice":
notice = map[string]any{}
default:
if textNotice, ok := acpSystemNoticeFromAgentText(acpTextContent(update["content"])); ok {
notice = textNotice
} else {
return activityshared.Event{}, false
}
}
}
payload := map[string]any{
"kind": "agent_system_notice",
"acp": map[string]any{
"sessionUpdate": firstNonEmpty(updateType, fallbackKind),
},
}
copyStringPayload(payload, notice, "noticeKind")
copyStringPayload(payload, notice, "severity")
copyStringPayload(payload, notice, "source")
copyStringPayload(payload, notice, "title")
copyStringPayload(payload, notice, "detail")
copyStringPayload(payload, notice, "code")
copyStringPayload(payload, notice, "noticeCommand")
copyStringPayload(payload, notice, "noticeCommandStatus")
// A caller-provided messageId lets related notices (e.g. compaction
// started/completed) share one transcript row instead of stacking.
copyStringPayload(payload, notice, "messageId")
copyBoolPayload(payload, notice, "retryable")
if extra := clonePayloadValue(notice["extra"]); extra != nil {
payload["extra"] = extra
}
if codexErrorInfo := clonePayloadValue(firstPresentAny(notice["codexErrorInfo"], notice["codex_error_info"], update["codexErrorInfo"], update["codex_error_info"])); codexErrorInfo != nil {
payload["codexErrorInfo"] = codexErrorInfo
}
if additionalDetails := firstNonEmpty(asString(notice["additionalDetails"]), asString(notice["additional_details"]), asString(update["additionalDetails"]), asString(update["additional_details"])); additionalDetails != "" {
payload["additionalDetails"] = additionalDetails
}
noticeKind := firstNonEmpty(asString(payload["noticeKind"]), acpNoticeKindFromUpdate(firstNonEmpty(updateType, fallbackKind)))
severity := firstNonEmpty(asString(payload["severity"]), acpNoticeSeverity(noticeKind))
detail := firstNonEmpty(asString(payload["detail"]), acpTextContent(update["content"]), asString(update["text"]), asString(payload["additionalDetails"]), asString(update["message"]))
title := firstNonEmpty(asString(payload["title"]), acpNoticeTitle(noticeKind, detail))
payload["noticeKind"] = noticeKind
payload["severity"] = severity
payload["title"] = title
if detail != "" {
payload["detail"] = detail
}
payload["content"] = title
payload["text"] = title
return newTurnActivityEvent(session, EventMessage, turnID, messageStreamStateCompleted, RoleAssistant, title, payload), true
}
func acpSystemNoticePayload(update map[string]any) map[string]any {
if asString(update["kind"]) == "agent_system_notice" {
return update
}
meta, _ := update["_meta"].(map[string]any)
if meta == nil {
return nil
}
if tsh, ok := meta["tsh"].(map[string]any); ok && asString(tsh["kind"]) == "agent_system_notice" {
return tsh
}
if asString(meta["kind"]) == "agent_system_notice" {
return meta
}
return nil
}
func acpSystemNoticeFromAgentText(text string) (map[string]any, bool) {
text = strings.TrimSpace(text)
if text == "" {
return nil, false
}
normalized := strings.ToLower(text)
if acpAgentTextLooksLikeTransportRetry(normalized) {
return map[string]any{
"kind": "agent_system_notice",
"noticeKind": "transport_retry",
"severity": "warning",
"title": "Codex connection interrupted. Reconnecting...",
"detail": text,
"retryable": true,
"source": "agent_text",
}, true
}
if strings.Contains(normalized, "falling back from websockets to https transport") ||
strings.Contains(normalized, "switched to https transport") {
return map[string]any{
"kind": "agent_system_notice",
"noticeKind": "transport_fallback",
"severity": "warning",
"title": "Codex switched to HTTPS transport.",
"detail": text,
"source": "agent_message_chunk",
}, true
}
return nil, false
}
func acpAgentTextLooksLikeTransportRetry(normalized string) bool {
if !strings.Contains(normalized, "reconnecting") {
return false
}
if strings.Contains(normalized, "responsestreamdisconnected") ||
strings.Contains(normalized, "broken pipe") ||
strings.Contains(normalized, "handled error during turn") ||
strings.Contains(normalized, "response stream") ||
strings.Contains(normalized, "websocket") {
return true
}
return strings.Contains(normalized, "/5") || strings.Contains(normalized, "/ 5")
}
func copyStringPayload(payload map[string]any, source map[string]any, key string) {
if value := asString(source[key]); value != "" {
payload[key] = value
}
}
func copyBoolPayload(payload map[string]any, source map[string]any, key string) {
if value, ok := source[key].(bool); ok {
payload[key] = value
}
}
func firstPresentAny(values ...any) any {
for _, value := range values {
if value != nil {
return value
}
}
return nil
}
func acpNoticeKindFromUpdate(updateType string) string {
switch strings.TrimSpace(updateType) {
case "stream_error":
return "transport_retry"
case "warning":
return "warning"
default:
return "system_notice"
}
}
func acpNoticeSeverity(noticeKind string) string {
switch strings.TrimSpace(noticeKind) {
case "transport_retry", "transport_fallback", "warning":
return "warning"
default:
return "info"
}
}
func acpNoticeTitle(noticeKind string, detail string) string {
switch strings.TrimSpace(noticeKind) {
case "transport_retry":
return "Codex connection interrupted. Reconnecting..."
case "transport_fallback":
return "Codex switched to HTTPS transport."
case "warning":
return firstNonEmpty(detail, "Codex warning")
default:
return firstNonEmpty(detail, "Agent notice")
}
}
func acpCurrentModeUpdatedEvent(session Session, modeID string) (activityshared.Event, bool) {
ctx, ok := activityEventContext(session, newID(), "")
if !ok {
return activityshared.Event{}, false
}
modeID = strings.TrimSpace(modeID)
if modeID == "" {
return activityshared.Event{}, false
}
event := activityshared.NewSessionUpdated(ctx, "")
event.Payload.Metadata = map[string]any{
"sessionUpdateKind": "current_mode_update",
"acpModeId": modeID,
}
return event, true
}
func hasACPCurrentModeUpdatedEvent(events []activityshared.Event) bool {
for _, event := range events {
if event.Type != activityshared.EventSessionUpdated {
continue
}
if normalizedSessionUpdateKind(event.Payload.Metadata) != "current_mode_update" {
continue
}
return true
}
return false
}
func newSessionTitleActivityEvent(session Session, title string) activityshared.Event {
ctx, ok := activityEventContext(session, newID(), "")
if !ok {
return activityshared.Event{}
}
ctx.Title = strings.TrimSpace(title)
return activityshared.NewSessionTitleUpdated(ctx)
}