源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
package agentruntime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
||||
)
|
||||
|
||||
type codexAppServerReducer struct {
|
||||
adapter *CodexAppServerAdapter
|
||||
}
|
||||
|
||||
type codexAppServerReduction struct {
|
||||
Events []activityshared.Event
|
||||
}
|
||||
|
||||
func newCodexAppServerReducer(adapter *CodexAppServerAdapter) codexAppServerReducer {
|
||||
return codexAppServerReducer{adapter: adapter}
|
||||
}
|
||||
|
||||
func (r codexAppServerReducer) ReduceNotification(
|
||||
client *codexAppServerClient,
|
||||
session Session,
|
||||
turnID string,
|
||||
message acpMessage,
|
||||
normalizer *acpTurnNormalizer,
|
||||
emitCommands CommandSnapshotSink,
|
||||
) codexAppServerReduction {
|
||||
return r.reduceNotification(client, session, turnID, message, normalizer, emitCommands, false)
|
||||
}
|
||||
|
||||
func (r codexAppServerReducer) reduceNotification(
|
||||
client *codexAppServerClient, session Session, turnID string, message acpMessage,
|
||||
normalizer *acpTurnNormalizer, emitCommands CommandSnapshotSink, replayingBuffered bool,
|
||||
) codexAppServerReduction {
|
||||
a := r.adapter
|
||||
if a == nil {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
rootAgentSessionID := session.AgentSessionID
|
||||
params := map[string]any{}
|
||||
if len(message.Params) > 0 {
|
||||
_ = json.Unmarshal(message.Params, ¶ms)
|
||||
}
|
||||
providerTurnID := appServerNotificationProviderTurnID(params)
|
||||
if !replayingBuffered && message.Method != appServerNotifyTurnStarted &&
|
||||
message.Method != appServerNotifyThreadGoalUpdated &&
|
||||
a.bufferPendingGoalTurnNotification(session.AgentSessionID, providerTurnID, message) {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
diagnosticTurn := a.activeTurnForNormalizer(rootAgentSessionID, normalizer)
|
||||
route := a.appServerNotificationRoute(session, turnID, message.Method, params)
|
||||
if route.drop {
|
||||
return codexAppServerReduction{Events: route.events}
|
||||
}
|
||||
if route.session.AgentSessionID != "" {
|
||||
session = route.session
|
||||
}
|
||||
if route.normalizer != nil {
|
||||
normalizer = route.normalizer
|
||||
}
|
||||
turnID = firstNonEmpty(route.turnID, turnID)
|
||||
prefixEvents := route.events
|
||||
if _, canceled := a.rootTurnCanceled(rootAgentSessionID); canceled && route.child == nil &&
|
||||
message.Method != appServerNotifyTurnStarted && message.Method != appServerNotifyTurnCompleted {
|
||||
// The route still runs first so a late spawn edge can interrupt its native
|
||||
// child threads. No transcript/progress from the canceled execution is
|
||||
// projected after the durable cancel boundary.
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
if diagnosticTurn != nil {
|
||||
diagnosticTurn.diagnostics.ObserveNotification(message.Method, params)
|
||||
}
|
||||
emit := func(events []activityshared.Event) codexAppServerReduction {
|
||||
events = appServerEventsForChild(events, route.child)
|
||||
combined := make([]activityshared.Event, 0, len(prefixEvents)+len(events))
|
||||
combined = append(combined, prefixEvents...)
|
||||
combined = append(combined, events...)
|
||||
prefixEvents = nil
|
||||
return codexAppServerReduction{Events: combined}
|
||||
}
|
||||
switch message.Method {
|
||||
case appServerNotifyTurnStarted:
|
||||
// Record the provider turn id (needed for turn/interrupt and
|
||||
// turn/steer) only while a turn context is registered, so stray
|
||||
// turns (for example compaction) cannot block future prompts.
|
||||
// First-wins: once the live turn's id is recorded, a stray
|
||||
// server-initiated turn/started on the same thread must not overwrite
|
||||
// it — the strict completion match would then drop the real
|
||||
// turn/completed and awaitTurnCompletion would never settle. After
|
||||
// completion clears the id, the next turn/started (for example a goal
|
||||
// continuation) records normally.
|
||||
providerTurnID = strings.TrimSpace(asString(payloadObject(params["turn"])["id"]))
|
||||
if activeTurn := a.sessionActiveTurn(session.AgentSessionID); activeTurn != nil {
|
||||
if turn := payloadObject(params["turn"]); turn != nil {
|
||||
providerTurnID = strings.TrimSpace(asString(turn["id"]))
|
||||
if recorded := a.sessionActiveTurnID(session.AgentSessionID); recorded != "" && recorded != providerTurnID {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
if a.setSessionActiveTurnID(session.AgentSessionID, activeTurn, providerTurnID) {
|
||||
a.interruptActiveTurnAsync(&codexAppServerSession{
|
||||
client: client,
|
||||
threadID: firstNonEmpty(asString(params["threadId"]), session.ProviderSessionID),
|
||||
}, session, activeTurn, providerTurnID, "queued cancel")
|
||||
}
|
||||
a.confirmSessionActiveTurnStarted(session.AgentSessionID, providerTurnID)
|
||||
}
|
||||
} else if turn := payloadObject(params["turn"]); turn != nil {
|
||||
// Server-initiated turn with no registered turn context: codex
|
||||
// drives goal continuation on its own, so adopt the turn while the
|
||||
// goal is active — otherwise its output would be dropped and the
|
||||
// GUI would freeze while codex keeps working invisibly.
|
||||
providerTurnID = strings.TrimSpace(asString(turn["id"]))
|
||||
goal := a.sessionGoal(session.AgentSessionID)
|
||||
goalStatus := strings.TrimSpace(asString(goal["status"]))
|
||||
canceledRootTurnID, rootTurnCanceled := a.rootTurnCanceled(session.AgentSessionID)
|
||||
switch {
|
||||
case rootTurnCanceled:
|
||||
slog.Warn("agent session app-server interrupting unowned turn after root cancellation",
|
||||
"event", "agent_session.app_server.turn.unowned_after_cancel",
|
||||
"agent_session_id", session.AgentSessionID,
|
||||
"root_turn_id", canceledRootTurnID,
|
||||
"provider_turn_id", providerTurnID,
|
||||
)
|
||||
// Async: a synchronous RPC on the read loop would block its own
|
||||
// response from being dispatched.
|
||||
go a.sendThreadInterrupt(client, session,
|
||||
firstNonEmpty(asString(params["threadId"]), session.ProviderSessionID),
|
||||
providerTurnID, "root turn canceled")
|
||||
return emit(nil)
|
||||
case goalStatus == "paused":
|
||||
// The user explicitly stopped the goal (Stop pauses it); codex
|
||||
// must not keep running turns for it.
|
||||
slog.Warn("agent session app-server interrupting unowned turn for paused goal",
|
||||
"event", "agent_session.app_server.turn.unowned_interrupted",
|
||||
"agent_session_id", session.AgentSessionID,
|
||||
"provider_turn_id", providerTurnID,
|
||||
"goal_status", goalStatus,
|
||||
)
|
||||
// Async: a synchronous RPC on the read loop would block its own
|
||||
// response from being dispatched.
|
||||
go a.sendThreadInterrupt(client, session,
|
||||
firstNonEmpty(asString(params["threadId"]), session.ProviderSessionID),
|
||||
providerTurnID, "goal paused")
|
||||
case len(goal) > 0:
|
||||
// A goal exists (in whatever state — codex may legitimately run
|
||||
// a wrap-up turn while flipping the goal to complete). Presence of
|
||||
// that mutable snapshot is not provenance: wait for the provider's
|
||||
// turn-scoped goal generation evidence before adopting.
|
||||
a.queueGoalTurnForProvenance(session, providerTurnID)
|
||||
default:
|
||||
slog.Info("agent session app-server unowned turn ignored",
|
||||
"event", "agent_session.app_server.turn.unowned",
|
||||
"agent_session_id", session.AgentSessionID,
|
||||
"provider_turn_id", providerTurnID,
|
||||
"goal_status", goalStatus,
|
||||
)
|
||||
}
|
||||
}
|
||||
if providerTurnID == "" {
|
||||
return emit(nil)
|
||||
}
|
||||
if ctx, ok := activityEventContext(session, "root-provider-turn-started:"+providerTurnID, turnID); ok {
|
||||
started := activityshared.NewRootProviderTurnStarted(
|
||||
ctx,
|
||||
turnID,
|
||||
providerTurnID,
|
||||
)
|
||||
if binding, err := a.WriteProviderTurnBinding(
|
||||
ProviderTurnBindingWriteInput{
|
||||
Kind: ProviderTurnBindingWriteStarted,
|
||||
ProviderTurnID: providerTurnID,
|
||||
},
|
||||
); err == nil {
|
||||
started.Payload.ProviderTurnBindingJSON = binding
|
||||
}
|
||||
return emit([]activityshared.Event{started})
|
||||
}
|
||||
return emit(nil)
|
||||
case appServerNotifyTurnCompleted:
|
||||
// Deliver the final turn payload to the goroutine waiting in Exec.
|
||||
a.completeActiveTurn(session.AgentSessionID, payloadObject(params["turn"]))
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyAgentMessageDelta:
|
||||
if normalizer == nil {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
return emit(normalizer.AppendAssistantChunk(session, turnID, asStringRaw(params["delta"])))
|
||||
case appServerNotifyCommandOutputDelta:
|
||||
if normalizer == nil {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
return emit(normalizer.AppendToolOutputDelta(
|
||||
session,
|
||||
turnID,
|
||||
asString(params["itemId"]),
|
||||
asStringRaw(params["delta"]),
|
||||
))
|
||||
case appServerNotifyReasoningSummaryPart, appServerNotifyThreadSettingsUpdated:
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyReasoningDelta, appServerNotifyReasoningSummary:
|
||||
if normalizer == nil {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
return emit(normalizer.AppendThinkingChunk(session, turnID, appServerReasoningDeltaText(params)))
|
||||
case appServerNotifyItemStarted:
|
||||
item := payloadObject(params["item"])
|
||||
if asString(item["type"]) == "subAgentActivity" && route.registeredChildCount == 0 {
|
||||
return emit(nil)
|
||||
}
|
||||
return emit(a.appServerItemEvents(session, turnID, item, false, normalizer))
|
||||
case appServerNotifyItemCompleted:
|
||||
item := payloadObject(params["item"])
|
||||
if asString(item["type"]) == "subAgentActivity" && route.registeredChildCount == 0 {
|
||||
return emit(nil)
|
||||
}
|
||||
return emit(a.appServerItemEvents(session, turnID, item, true, normalizer))
|
||||
case appServerNotifyFileChangePatchUpdated:
|
||||
if normalizer == nil {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
rawChanges := params["changes"]
|
||||
changes := payloadArray(rawChanges)
|
||||
if len(changes) == 0 {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
update, ok := appServerItemToolCallUpdate(map[string]any{
|
||||
"id": asString(params["itemId"]),
|
||||
"type": "fileChange",
|
||||
"status": "inProgress",
|
||||
"changes": rawChanges,
|
||||
}, false)
|
||||
if !ok {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
events, _ := normalizer.ToolCallEvents(session, turnID, update)
|
||||
events = append(events, normalizer.recordFileChangesEvents(session, turnID, changes)...)
|
||||
return emit(events)
|
||||
case appServerNotifyPlanUpdated:
|
||||
if normalizer == nil {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
update := appServerPlanUpdate(turnID, params)
|
||||
if update == nil {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
events, _ := normalizer.ToolCallEvents(session, turnID, update)
|
||||
return emit(events)
|
||||
case appServerNotifyTokenUsage:
|
||||
a.applyTokenUsage(session.AgentSessionID, params)
|
||||
if event, ok := normalizedUsageUpdatedEvent(session); ok {
|
||||
return emit([]activityshared.Event{event})
|
||||
}
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyRateLimitsUpdated:
|
||||
a.applyRateLimits(session.AgentSessionID, payloadObject(params["rateLimits"]))
|
||||
if event, ok := normalizedUsageUpdatedEvent(session); ok {
|
||||
return emit([]activityshared.Event{event})
|
||||
}
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyAccountUpdated:
|
||||
a.applyAccountUpdate(session.AgentSessionID, params)
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyThreadNameUpdated:
|
||||
threadName := asString(params["threadName"])
|
||||
if isInternalMentionRoutingTitle(threadName) {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
if event, ok := normalizedSessionTitleEvent(session, map[string]any{
|
||||
"title": threadName,
|
||||
}); ok {
|
||||
return emit([]activityshared.Event{event})
|
||||
}
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyError:
|
||||
if willRetry, _ := params["willRetry"].(bool); willRetry {
|
||||
turnError := payloadObject(params["error"])
|
||||
detail := asString(turnError["message"])
|
||||
return emit([]activityshared.Event{appServerSystemNoticeEvent(session, turnID, "transport_retry", "", detail)})
|
||||
}
|
||||
a.failActiveTurnFromAppServerError(session.AgentSessionID, params)
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyWarning:
|
||||
message := asStringRaw(params["message"])
|
||||
// The app-server currently sends this untyped advisory after a successful
|
||||
// compaction. Suppress only the exact duplicate while this turn owns a
|
||||
// compaction notice; keep every other warning visible.
|
||||
if normalizer.HasCompactionNotice() && message == appServerCompactionAdvisoryMessage {
|
||||
return emit(nil)
|
||||
}
|
||||
return emit([]activityshared.Event{appServerSystemNoticeEvent(session, turnID, "warning", "", message)})
|
||||
case appServerNotifyDeprecation:
|
||||
return emit([]activityshared.Event{appServerSystemNoticeEvent(session, turnID, "warning",
|
||||
asString(params["summary"]), asString(params["details"]))})
|
||||
case appServerNotifyModelRerouted:
|
||||
title := fmt.Sprintf("Codex rerouted the model from %s to %s.",
|
||||
asString(params["fromModel"]), asString(params["toModel"]))
|
||||
return emit([]activityshared.Event{appServerSystemNoticeEvent(session, turnID, "system_notice", title, asString(params["reason"]))})
|
||||
case appServerNotifyThreadCompacted:
|
||||
if normalizer == nil {
|
||||
return emit([]activityshared.Event{appServerSystemNoticeEvent(session, turnID, "system_notice", appServerContextCompactedTitle, "")})
|
||||
}
|
||||
messageID, shouldEmit := normalizer.CompleteCompactionNotice("compaction:" + turnID)
|
||||
if !shouldEmit {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
return emit([]activityshared.Event{appServerCompactionNoticeEvent(session, turnID, messageID, "completed")})
|
||||
case appServerNotifyServerRequestResolved:
|
||||
a.resolvePendingRequestFromProvider(session.AgentSessionID, params)
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyThreadGoalUpdated:
|
||||
// Goal updates are session-scoped metadata: emit through the session
|
||||
// sink so the GUI banner refreshes even while no turn context exists
|
||||
// (returned reduction events are dropped without an active turn).
|
||||
goal := payloadObject(params["goal"])
|
||||
a.observeGoalTurnGeneration(session, strings.TrimSpace(asString(params["turnId"])), goal)
|
||||
if a.providerGoalUpdateSuperseded(session.AgentSessionID, goal) {
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
_, newStatus, statusChanged := a.applyGoalUpdate(session.AgentSessionID, goal)
|
||||
a.scheduleProviderGoalAdoption(session, goal)
|
||||
goalEvents := []activityshared.Event{}
|
||||
if event, ok := normalizedGoalUpdatedEvent(session, "thread_goal_update"); ok {
|
||||
goalEvents = append(goalEvents, event)
|
||||
}
|
||||
if statusChanged {
|
||||
if noticeTurnID := firstNonEmpty(turnID, a.sessionMarkerTurnID(session.AgentSessionID)); noticeTurnID != "" {
|
||||
if notice := appServerGoalStatusNoticeEvent(session, noticeTurnID, newStatus); notice != nil {
|
||||
goalEvents = append(goalEvents, *notice)
|
||||
}
|
||||
}
|
||||
}
|
||||
a.emitSessionEvents(session.AgentSessionID, goalEvents)
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyThreadGoalCleared:
|
||||
a.applyGoalClear(session.AgentSessionID)
|
||||
if event, ok := normalizedGoalUpdatedEvent(session, "thread_goal_cleared"); ok {
|
||||
a.emitSessionEvents(session.AgentSessionID, []activityshared.Event{event})
|
||||
}
|
||||
return codexAppServerReduction{}
|
||||
case appServerNotifyMCPServerStartupStatusUpdated:
|
||||
status := appServerMCPServerStartupStatusPayload(params)
|
||||
if len(status) == 0 {
|
||||
return emit(nil)
|
||||
}
|
||||
slog.Info("agent session Codex MCP server startup status updated",
|
||||
"event", "agent_session.codex.mcp.startup_status",
|
||||
"agent_session_id", session.AgentSessionID,
|
||||
"server_name", asString(status["name"]),
|
||||
"status", asString(status["status"]),
|
||||
"failure_reason", asString(status["failureReason"]),
|
||||
)
|
||||
events := []activityshared.Event{
|
||||
newSessionActivityEvent(session, EventSessionUpdated, SessionStatusReady, map[string]any{
|
||||
"mcpServerStartupStatus": status,
|
||||
}),
|
||||
}
|
||||
if strings.EqualFold(asString(status["status"]), "failed") {
|
||||
client.observeMCPStartupStatus(status)
|
||||
events = append(events, codexMCPServerStartupWarningEvent(client, session, turnID, status))
|
||||
}
|
||||
a.emitSessionEvents(session.AgentSessionID, events)
|
||||
return emit(nil)
|
||||
case appServerNotifyMCPToolCallProgress:
|
||||
slog.Debug("agent session Codex MCP tool call progress",
|
||||
"event", "agent_session.codex.mcp.tool_progress",
|
||||
"agent_session_id", session.AgentSessionID,
|
||||
"item_id", asString(params["itemId"]),
|
||||
"message", asString(params["message"]),
|
||||
)
|
||||
return emit(nil)
|
||||
case appServerNotifyMCPOAuthLoginCompleted:
|
||||
a.emitSessionEvents(session.AgentSessionID, []activityshared.Event{
|
||||
newSessionActivityEvent(session, EventSessionUpdated, SessionStatusReady, map[string]any{
|
||||
"mcpServerOAuthLogin": clonePayload(params),
|
||||
}),
|
||||
})
|
||||
return emit(nil)
|
||||
case appServerNotifyThreadStarted:
|
||||
client.completeThreadLifecycleFromNotification(payloadObject(params["thread"]))
|
||||
return codexAppServerReduction{}
|
||||
default:
|
||||
_ = emitCommands
|
||||
return codexAppServerReduction{}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user