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

672 lines
21 KiB
TypeScript

import type {
AgentActivityDisplayStatus,
AgentActivityMessage,
AgentActivitySubmitDiagnostics,
AgentActivityTurn,
CanonicalAgentSession
} from "@tutti-os/agent-activity-core";
import { toast } from "@tutti-os/ui-system";
import { type AgentGUIRuntime } from "../../../agentActivityRuntime";
import type { AgentHostToastApi } from "../../../host/agentHostApi";
import type { AgentConversationVM } from "../../../shared/agentConversation/contracts/agentConversationVM";
import { createAgentClientId } from "../../../shared/agentConversation/agentClientIdentity";
import type { AgentSessionState } from "../../../shared/agentSessionTypes";
import type { AgentPromptContentBlock } from "../../../shared/contracts/dto";
import { agentPromptContentHasImage } from "../model/agentComposerDraft";
import {
type AgentGUIApprovalRequest,
type AgentGUIConversationSummary,
type AgentGUIInteractivePrompt
} from "../model/agentGuiConversationModel";
import {
agentGUIActivityTurnDiagnosticDetails,
agentGUIConversationDiagnosticDetails,
agentGUIRuntimeSessionDiagnosticDetails,
agentGUISessionStateDiagnosticDetails,
promptRequestId
} from "./agentGuiController.diagnostics";
import {
getAgentGUIErrorCode,
normalizeAgentGUIDiagnosticError
} from "./agentGuiController.errors";
import type { AgentConversationBatchDeletionCapability } from "./agentConversationBatchDeletionCapability";
export {
normalizePermissionModeSemantic,
permissionConfigFromComposerOptions,
permissionModeDescription,
permissionModeLabel,
permissionModeOptions
} from "./agentGuiController.composerHelpers";
export * from "./agentGuiController.conversationHelpers";
export {
agentGUIConversationDiagnosticDetails,
agentGUIRuntimeSessionDiagnosticDetails,
agentGUISessionStateDiagnosticDetails,
agentGUIToolCallStatusIsWaiting,
promptRequestId
} from "./agentGuiController.diagnostics";
export * from "./agentGuiController.draftMessageHelpers";
export * from "./agentGuiController.errors";
export {
createAgentGUIConversationId,
normalizeOptionalPrompt,
normalizeOptionalText,
projectAgentGUIMessagesToTimelineItems,
recordValue,
stringPayloadValue
} from "./agentGuiController.promptHelpers";
export * from "./agentGuiController.providerHelpers";
export {
messageFromMessageUpdate,
normalizeTimelineStatus,
normalizedPositiveNumber,
timelineItemTime
} from "./agentGuiController.sessionHelpers";
export * from "./agentGuiController.stableHelpers";
export {
filterMessagesForDetailWindowOverlay,
maxFiniteMessageVersion,
minFiniteMessageVersion,
sessionHasRenderableMessages,
windowHasTurnMissingUserPrompt
} from "./useAgentConversationMessagePaging";
export type AgentGUIRuntimeErrorPhase =
| "create_conversation"
| "interrupt_current_turn"
| "load_session_messages"
| "load_session_state"
| "synchronize_session"
| "retry_activation"
| "send_prompt"
| "submit_interactive"
| "toggle_conversation_pinned"
| "rename_conversation"
| "fork_conversation_through_turn"
| "open_fork_source_conversation"
| "delete_conversation"
| "update_session_settings"
| "warmup_openclaw_gateway";
export type AgentSubmitTraceState = {
agentSessionId: string;
blockCount: number;
clientSubmitId: string;
hasImage: boolean;
promptLength: number;
queued: boolean;
startedAtUnixMs: number;
turnId: string | null;
};
export function reportAgentGUIRuntimeError(input: {
agentSessionId?: string | null;
context?: Record<string, unknown>;
error: unknown;
phase: AgentGUIRuntimeErrorPhase;
provider?: string | null;
requestId?: number | string | null;
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
const details: Record<string, unknown> = {
error: normalizeAgentGUIDiagnosticError(input.error),
errorCode: getAgentGUIErrorCode(input.error),
phase: input.phase,
...(input.agentSessionId ? { agentSessionId: input.agentSessionId } : {}),
...(input.provider ? { provider: input.provider } : {}),
...(input.requestId !== undefined && input.requestId !== null
? { requestId: input.requestId }
: {}),
...(input.context ?? {})
};
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details,
event: "agent.gui.caught_error",
level: "error",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect the Agent GUI recovery path.
console.error("[agent-gui] reportAgentGUIRuntimeError failed", reportError);
}
}
export function showAgentGUIControllerErrorToast(
hostToast: AgentHostToastApi | null | undefined,
message: string
): void {
if (hostToast?.error) {
hostToast.error(message);
return;
}
toast.error(message);
}
export function reportAgentGUIConversationFilterTargetUnresolved(input: {
provider: string;
agentTargetId: string | null;
providerTargetCount: number;
reason: "disabled" | "unresolved";
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
provider: input.provider,
providerTargetCount: input.providerTargetCount,
agentTargetId: input.agentTargetId,
reason: input.reason
},
event: "agent.gui.conversation_filter.target_unresolved",
level: "warn",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect conversation filter selection.
console.error(
"[agent-gui] reportAgentGUIConversationFilterTargetUnresolved failed",
reportError
);
}
}
export function reportAgentGUIConversationBatchDeletionCapabilityIncomplete(input: {
missingMethods: AgentConversationBatchDeletionCapability["missingMethods"];
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
const reportFailure = (error: unknown) => {
console.error(
"[agent-gui-conversation-batch-delete-capability]",
JSON.stringify({
error: normalizeAgentGUIDiagnosticError(error),
workspaceId: input.workspaceId
})
);
};
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: { missingMethods: input.missingMethods },
event: "agent.gui.conversation_batch_delete.capability_incomplete",
level: "warn",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(reportFailure);
} catch (error) {
reportFailure(error);
}
}
export function reportAgentGUIMessagePageDiagnostic(input: {
agentSessionId: string;
details?: Record<string, unknown>;
event: string;
level?: "debug" | "info" | "warn";
messages?: readonly AgentActivityMessage[];
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
const versions = (input.messages ?? [])
.map((message) => message.version)
.filter((version) => Number.isFinite(version));
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
agentSessionId: input.agentSessionId,
...(input.messages
? {
firstVersion: versions.length ? Math.min(...versions) : null,
lastVersion: versions.length ? Math.max(...versions) : null,
messageCount: input.messages.length
}
: {}),
...(input.details ?? {})
},
event: input.event,
level: input.level ?? "info",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect message loading.
console.error(
"[agent-gui] reportAgentGUIMessagePageDiagnostic failed",
reportError
);
}
}
export function reportAgentGUIRenderStateDiagnostic(input: {
activeActivityDisplayStatus: AgentActivityDisplayStatus | null;
activeConversation: AgentGUIConversationSummary | null;
activeConversationBusy: boolean;
activeConversationId: string | null;
activeEngineActiveTurn: AgentActivityTurn | null;
activeEngineAvailability: "available" | "blocked" | "missing";
activeEngineLatestTurn: AgentActivityTurn | null;
activeEngineRuntimeActivity: "idle" | "running";
isAwaitingTurnStart: boolean;
activeLiveState: "inactive" | "activating" | "active" | "failed";
activeRuntimeSession: CanonicalAgentSession | null;
activeSessionState: AgentSessionState | null;
activeSubmitBlocked: boolean;
canQueueWhileBusy: boolean;
canSubmit: boolean;
conversation: AgentConversationVM | null;
isCreatingConversation: boolean;
isLoadingMessages: boolean;
isSubmitting: boolean;
pendingApproval: AgentGUIApprovalRequest | null;
pendingInteractivePrompt: AgentGUIInteractivePrompt | null;
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
activeActivityDisplayStatus: input.activeActivityDisplayStatus,
activeConversationBusy: input.activeConversationBusy,
activeConversationId: input.activeConversationId,
activeConversationStatus: input.activeConversation?.status ?? null,
activeEngineActiveTurn: agentGUIActivityTurnDiagnosticDetails(
input.activeEngineActiveTurn
),
activeEngineAvailability: input.activeEngineAvailability,
activeEngineLatestTurn: agentGUIActivityTurnDiagnosticDetails(
input.activeEngineLatestTurn
),
activeEngineRuntimeActivity: input.activeEngineRuntimeActivity,
isAwaitingTurnStart: input.isAwaitingTurnStart,
activeLiveState: input.activeLiveState,
activeSubmitBlocked: input.activeSubmitBlocked,
canQueueWhileBusy: input.canQueueWhileBusy,
canSubmit: input.canSubmit,
conversation: agentGUIConversationDiagnosticDetails(
input.conversation
),
isCreatingConversation: input.isCreatingConversation,
isLoadingMessages: input.isLoadingMessages,
isSubmitting: input.isSubmitting,
pendingApprovalRequestId: input.pendingApproval?.requestId ?? null,
pendingInteractivePromptKind:
input.pendingInteractivePrompt?.kind ?? null,
pendingInteractivePromptRequestId: promptRequestId(
input.pendingInteractivePrompt
),
runtimeSession: agentGUIRuntimeSessionDiagnosticDetails(
input.activeRuntimeSession
),
sessionState: agentGUISessionStateDiagnosticDetails(
input.activeSessionState
)
},
event: "agent.gui.node.render_state_changed",
level: "info",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect Agent GUI rendering.
console.error(
"[agent-gui] reportAgentGUIRenderStateDiagnostic failed",
reportError
);
}
}
export function reportAgentGUIActiveConversationCleared(input: {
details?: Record<string, unknown>;
previousAgentSessionId: string | null;
reason: string;
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
previousAgentSessionId: input.previousAgentSessionId,
reason: input.reason,
...(input.details ?? {})
},
event: "agent.gui.active_conversation.cleared",
level: "info",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect active conversation routing.
console.error(
"[agent-gui] reportAgentGUIActiveConversationCleared failed",
reportError
);
}
}
export function reportAgentGUIAttentionReadDecision(input: {
agentSessionId: string;
completionKey: string;
decision: "preserve_unread" | "read";
isSurfaceActive: boolean;
isSurfaceDocumentExposed: boolean;
isSurfaceVisible: boolean;
markedUnreadByUser: boolean;
nodeId?: string;
previousActiveConversationId: string | null;
reason:
| "active_selection"
| "manual_unread_current_selection"
| "reselected"
| "surface_hidden"
| "surface_inactive"
| "document_not_exposed";
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
agentSessionId: input.agentSessionId,
completionKey: input.completionKey,
decision: input.decision,
isSurfaceActive: input.isSurfaceActive,
isSurfaceDocumentExposed: input.isSurfaceDocumentExposed,
isSurfaceVisible: input.isSurfaceVisible,
markedUnreadByUser: input.markedUnreadByUser,
nodeId: input.nodeId ?? null,
previousActiveConversationId: input.previousActiveConversationId,
reason: input.reason
},
event: "agent.gui.attention_read.decision",
level: "info",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect attention/read decisions.
console.error(
"[agent-gui] reportAgentGUIAttentionReadDecision failed",
reportError
);
}
}
export function reportAgentGUIConversationListProjectionSkipped(input: {
activeConversationId: string | null;
currentUserIdPresent: boolean;
dataLastActiveAgentSessionId: string | null;
isComposerHome: boolean;
provider: string | null;
reason: string;
runtime: AgentGUIRuntime;
workspaceId: string;
workspaceIdPresent: boolean;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
activeConversationId: input.activeConversationId,
currentUserIdPresent: input.currentUserIdPresent,
dataLastActiveAgentSessionId: input.dataLastActiveAgentSessionId,
isComposerHome: input.isComposerHome,
provider: input.provider,
reason: input.reason,
workspaceIdPresent: input.workspaceIdPresent
},
event: "agent.gui.conversation_list_projection.skipped",
level: "info",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect active conversation routing.
console.error(
"[agent-gui] reportAgentGUIConversationListProjectionSkipped failed",
reportError
);
}
}
export function reportAgentGUISubmitWithoutActiveConversation(input: {
blockCount: number;
conversationCount: number;
conversationListQueryReady: boolean;
dataLastActiveAgentSessionId: string | null;
isComposerHome: boolean;
promptLength: number;
provider: string | null;
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
blockCount: input.blockCount,
conversationCount: input.conversationCount,
conversationListQueryReady: input.conversationListQueryReady,
dataLastActiveAgentSessionId: input.dataLastActiveAgentSessionId,
isComposerHome: input.isComposerHome,
promptLength: input.promptLength,
provider: input.provider
},
event: "agent.gui.submit.without_active_conversation",
level: "warn",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect active conversation routing.
console.error(
"[agent-gui] reportAgentGUISubmitWithoutActiveConversation failed",
reportError
);
}
}
export function reportAgentGUISubmitRecoveredActiveConversation(input: {
blockCount: number;
conversationCount: number;
conversationListQueryReady: boolean;
promptLength: number;
provider: string | null;
recoveredAgentSessionId: string;
runtime: AgentGUIRuntime;
workspaceId: string;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
blockCount: input.blockCount,
conversationCount: input.conversationCount,
conversationListQueryReady: input.conversationListQueryReady,
promptLength: input.promptLength,
provider: input.provider,
recoveredAgentSessionId: input.recoveredAgentSessionId
},
event: "agent.gui.submit.recovered_active_conversation",
level: "info",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect active conversation routing.
console.error(
"[agent-gui] reportAgentGUISubmitRecoveredActiveConversation failed",
reportError
);
}
}
export function reportAgentSubmitTraceDiagnostic(input: {
event: string;
runtime: AgentGUIRuntime;
trace: AgentSubmitTraceState;
workspaceId: string;
fields?: Record<string, unknown>;
}): void {
const reportDiagnostic = input.runtime.reportDiagnostic;
if (!reportDiagnostic) {
return;
}
const nowUnixMs = Date.now();
try {
void Promise.resolve(
reportDiagnostic.call(input.runtime, {
details: {
agentSessionId: input.trace.agentSessionId,
blockCount: input.trace.blockCount,
clientSubmitId: input.trace.clientSubmitId,
elapsedMs: Math.max(0, nowUnixMs - input.trace.startedAtUnixMs),
hasImage: input.trace.hasImage,
promptLength: input.trace.promptLength,
queued: input.trace.queued,
startedAtUnixMs: input.trace.startedAtUnixMs,
traceEvent: input.event,
turnId: input.trace.turnId,
...(input.fields ?? {})
},
event: "agent.submit.trace",
level: "info",
source: "agent-gui",
workspaceId: input.workspaceId
})
).catch(() => {});
} catch (reportError) {
// Diagnostic logging must never affect the Agent GUI submit path.
console.error(
"[agent-gui] reportAgentSubmitTraceDiagnostic failed",
reportError
);
}
}
export function scheduleAgentSubmitTracePaint(input: {
event?: string;
runtime: AgentGUIRuntime;
trace: AgentSubmitTraceState;
workspaceId: string;
}): void {
const logPaint = () =>
reportAgentSubmitTraceDiagnostic({
event: input.event ?? "optimistic_user_message_painted",
runtime: input.runtime,
trace: input.trace,
workspaceId: input.workspaceId
});
const requestFrame = globalThis.requestAnimationFrame;
if (typeof requestFrame !== "function") {
// timing: requestAnimationFrame fallback for non-browser runtimes; defer to next tick
setTimeout(logPaint, 0);
return;
}
requestFrame(() => requestFrame(logPaint));
}
export function createAgentSubmitTraceState(input: {
agentSessionId: string;
content: readonly AgentPromptContentBlock[];
prompt: string;
queued: boolean;
startedAtUnixMs: number;
}): AgentSubmitTraceState {
return {
agentSessionId: input.agentSessionId,
blockCount: input.content.length,
clientSubmitId: createAgentSubmitTraceId(),
hasImage: agentPromptContentHasImage(input.content),
promptLength: input.prompt.length,
queued: input.queued,
startedAtUnixMs: input.startedAtUnixMs,
turnId: null
};
}
export function agentSubmitTraceDiagnostics(
trace: AgentSubmitTraceState
): AgentActivitySubmitDiagnostics {
return {
submittedAtUnixMs: trace.startedAtUnixMs,
blockCount: trace.blockCount,
hasImage: trace.hasImage,
promptLength: trace.promptLength,
queued: trace.queued,
source: "agent-gui"
};
}
export function createAgentSubmitTraceId(): string {
return createAgentClientId();
}
/**
* Rewrites content for handoff to the runtime: pasted-text file blocks become a
* codex-style "read this file" instruction (path embedded). Applied at every
* runtime send boundary (new-session createSession + existing-session sendInput)
* so no `file` block reaches the desktop tuttid pipeline, while the draft/queue
* keep the structured block for the chip and edit-restore. The translated copy
* is resolved here (controller layer), never in the model.
*/