Files
tutti/packages/agent/gui/agent-message-center/workspaceAgentMessageCenterEngineModel.ts
T
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

693 lines
21 KiB
TypeScript

import type {
AgentActivityInteraction,
AgentActivityMessage,
AgentActivityNeedsAttentionItem,
AgentSessionEngineState,
WorkspaceAgentConsumerSession
} from "@tutti-os/agent-activity-core";
import { normalizeAgentApprovalPurpose } from "../shared/agentConversation/agentApprovalPurpose";
import {
selectEngineInteractionResponse,
selectPendingPlanFeedbackSubmit,
selectPlanDecisionForTurn,
selectPlanTurnDismissed,
selectWorkspaceAgentRootConversationSessions
} from "@tutti-os/agent-activity-core";
import type { AgentConversationPromptVM } from "../shared/agentConversation/contracts/agentConversationVM";
import { normalizeAskUserQuestions } from "../shared/agentConversation/askUserQuestions";
import {
extractExitPlanKeepPlanningOptionId,
extractExitPlanModeOptions,
isExitPlanSwitchModeInput
} from "../shared/agentConversation/exitPlanOptions";
import {
consumerAwaitingPlanImplementation,
selectRootAgentSessionIdsAwaitingPlanImplementation
} from "../shared/agentConversation/planImplementationAwaiting";
import { planImplementationPromptFromPlanTurn } from "../shared/agentConversation/planImplementationPresentation";
import {
buildWorkspaceAgentMessageCenterItem,
buildWorkspaceAgentMessageCenterModelFromItems,
type BuildWorkspaceAgentMessageCenterOptions,
type WorkspaceAgentMessageCenterModel,
type WorkspaceAgentMessageCenterTurnOutcome
} from "./workspaceAgentMessageCenterModel";
import { workspaceAgentMessageCenterItemEqual } from "./workspaceAgentMessageCenterModelStability";
/**
* Canonical Message Center entrypoint. Session/turn/interaction truth comes
* directly from the engine. Durable messages are presentation input only:
* title and digest text may use them, but they cannot recreate lifecycle or
* pending-interaction state.
*/
export function buildWorkspaceAgentMessageCenterModelFromEngine(
presentation: WorkspaceAgentMessageCenterPresentation,
snapshot: WorkspaceAgentMessageCenterMessageSnapshot,
options: BuildWorkspaceAgentMessageCenterOptions = {}
): WorkspaceAgentMessageCenterModel {
const includeHiddenSessionIds = new Set(
(options.includeHiddenSessionIds ?? [])
.map((sessionId) => sessionId.trim())
.filter((sessionId) => sessionId.length > 0)
);
const items = presentation.consumers
.filter(
(consumer) =>
consumer.session.visible !== false ||
includeHiddenSessionIds.has(consumer.session.agentSessionId.trim())
)
.map((consumer) => {
const interaction = latestPendingInteraction(consumer);
const planTurn =
!interaction &&
consumer.latestTurn?.turnId ===
presentation.awaitingPlanImplementationTurnIdBySessionId?.[
consumer.session.agentSessionId
]
? consumer.latestTurn
: null;
const planPrompt = planTurn
? planImplementationPromptFromPlanTurn(
planTurn.turnId,
consumer.session.title
)
: null;
const needsAttention = interaction
? needsAttentionFromInteraction(consumer, interaction)
: planTurn && planPrompt
? needsAttentionFromPlan(consumer, planTurn, planPrompt.title)
: null;
return buildWorkspaceAgentMessageCenterItem({
session: consumer.session,
latestTurn: consumer.latestTurn,
messages: sessionMessages(snapshot.sessionMessagesById, consumer),
status: planTurn ? "waiting" : consumer.displayStatus,
needsAttention,
pendingInteractionTarget: interaction
? interactionTarget(interaction)
: null,
pendingPrompt: interaction
? promptFromInteraction(interaction)
: planPrompt,
latestTurnOutcome: planTurn ? null : turnOutcome(consumer),
options
});
});
return buildWorkspaceAgentMessageCenterModelFromItems(
items,
options.itemCutoffUnixMs
);
}
interface WorkspaceAgentMessageCenterMessageSnapshot {
readonly sessionMessagesById: Readonly<
Record<string, readonly AgentActivityMessage[]>
>;
readonly workspaceId: string;
}
export interface WorkspaceAgentMessageCenterPresentation {
consumers: readonly WorkspaceAgentConsumerSession[];
/**
* Derived from the canonical Engine message window. Optional for hosts that
* construct the older presentation shape; absence fails closed.
*/
awaitingPlanImplementationTurnIdBySessionId?: Readonly<
Record<string, string>
>;
dismissedPlanTurnKeys: Readonly<Record<string, true>>;
promptStatusByKey: Readonly<
Record<string, WorkspaceAgentMessageCenterPromptStatus>
>;
}
export function selectWorkspaceAgentMessageCenterPresentation(
state: AgentSessionEngineState
): WorkspaceAgentMessageCenterPresentation {
const consumers = selectWorkspaceAgentRootConversationSessions(state);
const promptStatusByKey: Record<
string,
WorkspaceAgentMessageCenterPromptStatus
> = {};
const dismissedPlanTurnKeys: Record<string, true> = {};
const awaitingPlanImplementationTurnIdBySessionId: Record<string, string> =
{};
const awaitingPlanImplementationSessionIds = new Set(
selectRootAgentSessionIdsAwaitingPlanImplementation(state)
);
for (const consumer of consumers) {
const sessionId = consumer.session.agentSessionId;
if (
awaitingPlanImplementationSessionIds.has(sessionId) &&
consumer.latestTurn
) {
awaitingPlanImplementationTurnIdBySessionId[sessionId] =
consumer.latestTurn.turnId;
}
for (const interaction of consumer.pendingInteractions) {
const response = selectEngineInteractionResponse(
state,
interaction.agentSessionId,
interaction.turnId,
interaction.requestId
);
if (response) {
promptStatusByKey[
promptStatusKey(
interaction.agentSessionId,
interaction.turnId,
interaction.requestId
)
] = response.status;
}
}
const turnId = consumer.latestTurn?.turnId ?? "";
if (!turnId) continue;
if (selectPlanTurnDismissed(state, sessionId, turnId)) {
dismissedPlanTurnKeys[promptStatusKey(sessionId, turnId, turnId)] = true;
}
const decision = selectPlanDecisionForTurn(state, sessionId, turnId);
if (decision) {
promptStatusByKey[promptStatusKey(sessionId, turnId, turnId)] =
decision.status === "requested" ? "responding" : decision.status;
continue;
}
const submit = selectPendingPlanFeedbackSubmit(
state,
sessionId,
turnId,
turnId
);
if (submit) {
promptStatusByKey[promptStatusKey(sessionId, turnId, turnId)] =
submit.status === "failed"
? "failed"
: submit.status === "uncertain"
? "unknown"
: "responding";
}
}
return {
consumers,
awaitingPlanImplementationTurnIdBySessionId,
dismissedPlanTurnKeys,
promptStatusByKey
};
}
export function workspaceAgentMessageCenterPresentationEqual(
left: WorkspaceAgentMessageCenterPresentation,
right: WorkspaceAgentMessageCenterPresentation
): boolean {
return (
stringMapsEqual(
left.awaitingPlanImplementationTurnIdBySessionId ?? {},
right.awaitingPlanImplementationTurnIdBySessionId ?? {}
) &&
booleanMapsEqual(left.dismissedPlanTurnKeys, right.dismissedPlanTurnKeys) &&
promptStatusMapsEqual(left.promptStatusByKey, right.promptStatusByKey) &&
left.consumers.length === right.consumers.length &&
left.consumers.every((item, index) => {
const candidate = right.consumers[index];
return (
candidate !== undefined &&
item.session === candidate.session &&
item.activeTurn === candidate.activeTurn &&
item.latestTurn === candidate.latestTurn &&
item.displayStatus === candidate.displayStatus &&
item.pendingInteractions.length ===
candidate.pendingInteractions.length &&
item.pendingInteractions.every(
(interaction, interactionIndex) =>
interaction === candidate.pendingInteractions[interactionIndex]
)
);
})
);
}
export type WorkspaceAgentAttentionTarget =
| {
kind: "interaction";
workspaceId: string;
agentSessionId: string;
turnId: string;
requestId: string;
}
| {
kind: "plan-implementation";
workspaceId: string;
agentSessionId: string;
turnId: string;
requestId: string;
};
export interface WorkspaceAgentAttentionItem {
item: import("./workspaceAgentMessageCenterModel").WorkspaceAgentMessageCenterItem;
status: WorkspaceAgentMessageCenterPromptStatus;
target: WorkspaceAgentAttentionTarget;
}
/**
* Projects every canonical actionable target instead of collapsing a root
* conversation to its latest interaction. The display item keeps the root
* conversation identity for navigation while target keeps the exact child
* session/turn/request identity used by the command.
*/
export function selectWorkspaceAgentAttentionItems(
presentation: WorkspaceAgentMessageCenterPresentation,
snapshot: WorkspaceAgentMessageCenterMessageSnapshot,
options: BuildWorkspaceAgentMessageCenterOptions = {}
): WorkspaceAgentAttentionItem[] {
const result: WorkspaceAgentAttentionItem[] = [];
for (const consumer of presentation.consumers) {
if (
consumer.session.visible === false ||
consumer.session.workspaceId !== snapshot.workspaceId
) {
continue;
}
const messages = sessionMessages(snapshot.sessionMessagesById, consumer);
for (const interaction of consumer.pendingInteractions) {
const prompt = promptFromInteraction(interaction);
if (!prompt) continue;
const target: WorkspaceAgentAttentionTarget = {
kind: "interaction",
workspaceId: consumer.session.workspaceId,
agentSessionId: interaction.agentSessionId,
turnId: interaction.turnId,
requestId: interaction.requestId
};
const item = buildWorkspaceAgentMessageCenterItem({
session: consumer.session,
latestTurn: consumer.latestTurn,
messages,
status: "waiting",
needsAttention: needsAttentionFromInteraction(consumer, interaction),
pendingInteractionTarget: interactionTarget(interaction),
pendingPrompt: prompt,
latestTurnOutcome: null,
options
});
item.id = workspaceAgentAttentionKey(target);
item.sortTimeUnixMs = interaction.createdAtUnixMs;
result.push({
item,
status:
presentation.promptStatusByKey[
promptStatusKey(
interaction.agentSessionId,
interaction.turnId,
interaction.requestId
)
] ?? "idle",
target
});
}
const latestTurn = consumer.latestTurn;
const statusKey = promptStatusKey(
consumer.session.agentSessionId,
latestTurn?.turnId ?? "",
latestTurn?.turnId ?? ""
);
if (!latestTurn) {
continue;
}
const presentationAwaitsPlan =
presentation.awaitingPlanImplementationTurnIdBySessionId?.[
consumer.session.agentSessionId
] === latestTurn.turnId;
if (
!presentationAwaitsPlan &&
!consumerAwaitingPlanImplementation({
capabilities: consumer.session.capabilities,
dismissed: presentation.dismissedPlanTurnKeys[statusKey] === true,
latestTurn,
messages
})
) {
continue;
}
const target: WorkspaceAgentAttentionTarget = {
kind: "plan-implementation",
workspaceId: consumer.session.workspaceId,
agentSessionId: consumer.session.agentSessionId,
turnId: latestTurn.turnId,
requestId: latestTurn.turnId
};
const prompt = planImplementationPromptFromPlanTurn(
latestTurn.turnId,
consumer.session.title
);
const needsAttention = needsAttentionFromPlan(
consumer,
latestTurn,
prompt.title
);
const item = buildWorkspaceAgentMessageCenterItem({
session: consumer.session,
latestTurn,
messages,
status: "waiting",
needsAttention,
pendingInteractionTarget: null,
pendingPrompt: prompt,
latestTurnOutcome: null,
options
});
item.id = workspaceAgentAttentionKey(target);
item.sortTimeUnixMs = needsAttention.occurredAtUnixMs;
result.push({
item,
status: presentation.promptStatusByKey[statusKey] ?? "idle",
target
});
}
return result;
}
export function workspaceAgentAttentionItemsEqual(
left: readonly WorkspaceAgentAttentionItem[],
right: readonly WorkspaceAgentAttentionItem[]
): boolean {
return (
left.length === right.length &&
left.every((entry, index) => {
const candidate = right[index];
return (
candidate !== undefined &&
entry.status === candidate.status &&
workspaceAgentAttentionTargetEqual(entry.target, candidate.target) &&
workspaceAgentMessageCenterItemEqual(entry.item, candidate.item)
);
})
);
}
function workspaceAgentAttentionKey(target: WorkspaceAgentAttentionTarget) {
return [
target.workspaceId,
target.agentSessionId,
target.turnId,
target.requestId
].join("\n");
}
function workspaceAgentAttentionTargetEqual(
left: WorkspaceAgentAttentionTarget,
right: WorkspaceAgentAttentionTarget
): boolean {
return (
left.kind === right.kind &&
left.workspaceId === right.workspaceId &&
left.agentSessionId === right.agentSessionId &&
left.turnId === right.turnId &&
left.requestId === right.requestId
);
}
export type WorkspaceAgentMessageCenterPromptStatus =
| "idle"
| "responding"
| "unknown"
| "failed";
export function workspaceAgentMessageCenterPromptStatus(
presentation: WorkspaceAgentMessageCenterPresentation,
item: Pick<
import("./workspaceAgentMessageCenterModel").WorkspaceAgentMessageCenterItem,
"agentSessionId" | "pendingInteractionTarget" | "pendingPrompt"
>
): WorkspaceAgentMessageCenterPromptStatus {
const prompt = item.pendingPrompt;
if (!prompt) return "idle";
const target = item.pendingInteractionTarget;
return (
presentation.promptStatusByKey[
target
? promptStatusKey(
target.agentSessionId,
target.turnId,
target.requestId
)
: promptStatusKey(
item.agentSessionId,
prompt.requestId,
prompt.requestId
)
] ?? "idle"
);
}
function promptStatusKey(
agentSessionId: string,
turnId: string,
requestId: string
): string {
return `${agentSessionId}\0${turnId}\0${requestId}`;
}
function promptStatusMapsEqual(
left: Readonly<Record<string, WorkspaceAgentMessageCenterPromptStatus>>,
right: Readonly<Record<string, WorkspaceAgentMessageCenterPromptStatus>>
): boolean {
const keys = Object.keys(left);
return (
keys.length === Object.keys(right).length &&
keys.every((key) => left[key] === right[key])
);
}
function booleanMapsEqual(
left: Readonly<Record<string, true>>,
right: Readonly<Record<string, true>>
): boolean {
const keys = Object.keys(left);
return (
keys.length === Object.keys(right).length &&
keys.every((key) => right[key] === true)
);
}
function stringMapsEqual(
left: Readonly<Record<string, string>>,
right: Readonly<Record<string, string>>
): boolean {
const keys = Object.keys(left);
return (
keys.length === Object.keys(right).length &&
keys.every((key) => left[key] === right[key])
);
}
function sessionMessages(
sessionMessagesById: Readonly<
Record<string, readonly AgentActivityMessage[]>
>,
consumer: WorkspaceAgentConsumerSession
): readonly AgentActivityMessage[] {
for (const id of [
consumer.session.agentSessionId,
consumer.session.providerSessionId
]) {
const normalized = id?.trim() ?? "";
if (normalized && sessionMessagesById[normalized]) {
return sessionMessagesById[normalized];
}
}
return [];
}
function latestPendingInteraction(
consumer: WorkspaceAgentConsumerSession
): AgentActivityInteraction | null {
return consumer.pendingInteractions.at(-1) ?? null;
}
function interactionTarget(
interaction: AgentActivityInteraction
): import("./workspaceAgentMessageCenterModel").WorkspaceAgentMessageCenterInteractionTarget {
return {
agentSessionId: interaction.agentSessionId,
requestId: interaction.requestId,
turnId: interaction.turnId
};
}
function needsAttentionFromInteraction(
consumer: WorkspaceAgentConsumerSession,
interaction: AgentActivityInteraction
): AgentActivityNeedsAttentionItem {
const summary = interactionSummary(interaction);
return {
id: `interaction:${interaction.requestId}`,
workspaceId: consumer.session.workspaceId,
agentSessionId: consumer.session.agentSessionId,
provider: consumer.session.provider,
title: summary,
cwd: consumer.session.cwd,
kind:
interaction.kind === "approval"
? "permission"
: interaction.kind === "question"
? "question"
: "constraint",
summary,
occurredAtUnixMs: interaction.createdAtUnixMs
};
}
function needsAttentionFromPlan(
consumer: WorkspaceAgentConsumerSession,
turn: NonNullable<WorkspaceAgentConsumerSession["latestTurn"]>,
title: string
): AgentActivityNeedsAttentionItem {
return {
id: `plan-implementation:${turn.turnId}`,
workspaceId: consumer.session.workspaceId,
agentSessionId: consumer.session.agentSessionId,
provider: consumer.session.provider,
title,
cwd: consumer.session.cwd,
kind: "constraint",
summary: title,
occurredAtUnixMs: turn.settledAtUnixMs ?? turn.updatedAtUnixMs
};
}
function promptFromInteraction(
interaction: AgentActivityInteraction
): AgentConversationPromptVM | null {
const input = interaction.input ?? {};
const normalizedToolName = (interaction.toolName ?? "")
.replace(/[_\s-]+/g, "")
.trim()
.toLowerCase();
if (
interaction.kind === "plan" ||
normalizedToolName === "exitplanmode" ||
isExitPlanSwitchModeInput(input)
) {
const keepPlanningOptionId = extractExitPlanKeepPlanningOptionId(input);
return {
kind: "exit-plan",
requestId: interaction.requestId,
title: interactionSummary(interaction),
options: extractExitPlanModeOptions(input),
...(keepPlanningOptionId ? { keepPlanningOptionId } : {})
};
}
if (interaction.kind === "question") {
const questions = normalizeAskUserQuestions(input.questions);
return {
agentSessionId: interaction.agentSessionId,
kind: "ask-user",
requestId: interaction.requestId,
title: interactionSummary(interaction),
turnId: interaction.turnId,
questions:
questions.length > 0
? questions
: [
{
id: "response",
header: "",
question: interactionSummary(interaction),
options: [],
multiSelect: false,
answer: null
}
]
};
}
if (interaction.kind !== "approval") {
return null;
}
const options = arrayValue(input.options).flatMap((value) => {
const option = recordValue(value);
const id = textValue(option.optionId) ?? textValue(option.id);
return id
? [
{
id,
label: textValue(option.label) ?? textValue(option.name) ?? id,
kind: textValue(option.kind) ?? id,
...(textValue(option.description)
? { description: textValue(option.description) as string }
: {})
}
]
: [];
});
const approvalPurpose = normalizeAgentApprovalPurpose(
interaction.metadata?.approvalPurpose
);
return {
kind: "approval",
id: `approval:${interaction.requestId}`,
turnId: interaction.turnId,
requestId: interaction.requestId,
callId: textValue(input.callId) ?? interaction.requestId,
...(approvalPurpose ? { approvalPurpose } : {}),
title: interactionSummary(interaction),
toolName: interaction.toolName ?? null,
status: interaction.status,
input,
options,
output: interaction.output ?? null,
occurredAtUnixMs: interaction.createdAtUnixMs
};
}
function turnOutcome(
consumer: WorkspaceAgentConsumerSession
): WorkspaceAgentMessageCenterTurnOutcome | null {
if (
consumer.displayStatus !== "completed" &&
consumer.displayStatus !== "failed"
) {
return null;
}
const turn = consumer.latestTurn;
if (!turn || turn.phase !== "settled") return null;
const status =
turn.outcome === "completed"
? "completed"
: turn.outcome === "failed"
? "failed"
: null;
return status
? {
notificationKey: `${consumer.session.agentSessionId}:turn:${turn.turnId}:${status}`,
status,
turnId: turn.turnId
}
: null;
}
function interactionSummary(interaction: AgentActivityInteraction): string {
const input = interaction.input ?? {};
return (
textValue(input.question) ??
textValue(input.title) ??
textValue(input.summary) ??
interaction.toolName?.trim() ??
interaction.kind
);
}
function textValue(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function recordValue(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}