Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/controller/agentGuiController.composerPresentation.ts
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

415 lines
13 KiB
TypeScript

// Agent GUI controller — pure composer target and settings presentation policy.
import type { AgentActivityComposerOptions } from "@tutti-os/agent-activity-core";
import type {
AgentSessionComposerSettings,
AgentSessionReasoningEffort,
AgentSessionSpeed
} from "../../../shared/agentSessionTypes";
import type { AgentGUINodeData, AgentGUIProvider } from "../../../types";
import type { ACPConfigOptionSelection } from "./agentGuiController.types";
import { reasoningSelectionForModelFromComposerOptions } from "./agentGuiController.composerHelpers";
import { normalizeOptionalText } from "./agentGuiController.promptHelpers";
export interface AgentGUIComposerTargetData {
agentTargetId: string | null;
data: AgentGUINodeData;
provider: AgentGUIProvider;
targetId: string;
}
export interface AgentGUIActiveSessionTarget {
agentTargetId: string | null;
agentSessionId: string;
provider: AgentGUIProvider;
}
export interface OptimisticComposerTarget {
agentSessionId: string;
target: AgentGUIComposerTargetData;
}
export function isConversationCreateActive(input: {
activeConversationId: string | null;
pendingConversationId: string | null;
}): boolean {
return (
input.pendingConversationId !== null &&
(input.activeConversationId === null ||
input.activeConversationId === input.pendingConversationId)
);
}
export function composerTargetDataFromNodeData(
data: AgentGUINodeData
): AgentGUIComposerTargetData {
const agentTargetId = normalizeOptionalText(data.agentTargetId);
return {
agentTargetId,
provider: data.provider,
targetId: agentTargetId ?? `local:${data.provider}`,
data
};
}
/**
* Resolve the device-local model-history owner without guessing an active
* Session's identity. Home may use its stable target fallback, while an active
* Session without a canonical Agent Target fails closed.
*/
export function composerModelChoiceHistoryTargetId(input: {
activeConversationId: string | null;
target: Pick<AgentGUIComposerTargetData, "agentTargetId" | "targetId">;
}): string | null {
const agentTargetId = normalizeOptionalText(input.target.agentTargetId);
if (agentTargetId) {
return agentTargetId;
}
return input.activeConversationId === null
? normalizeOptionalText(input.target.targetId)
: null;
}
export function composerTargetDataForConversation(input: {
activeConversationId: string | null;
activeSessionTarget: AgentGUIActiveSessionTarget | null;
data: AgentGUINodeData;
optimisticTarget: OptimisticComposerTarget | null;
selectedTarget: AgentGUIComposerTargetData;
}): AgentGUIComposerTargetData {
if (input.activeConversationId === null) {
return input.selectedTarget;
}
if (
input.optimisticTarget?.agentSessionId === input.activeConversationId &&
(!input.activeSessionTarget ||
input.activeSessionTarget.agentSessionId !== input.activeConversationId ||
!nodeDataMatchesComposerTarget(input.data, input.optimisticTarget.target))
) {
return input.optimisticTarget.target;
}
if (
!input.activeSessionTarget ||
input.activeSessionTarget.agentSessionId !== input.activeConversationId
) {
return {
agentTargetId: null,
provider: input.data.provider,
targetId: "__active_session_loading__",
data: {
...input.data,
agentTargetId: null
}
};
}
const activeAgentTargetId = normalizeOptionalText(
input.activeSessionTarget.agentTargetId
);
const dataMatchesActiveSessionTarget =
input.data.provider === input.activeSessionTarget.provider &&
normalizeOptionalText(input.data.agentTargetId) === activeAgentTargetId;
return {
agentTargetId: activeAgentTargetId,
provider: input.activeSessionTarget.provider,
targetId:
activeAgentTargetId ?? `local:${input.activeSessionTarget.provider}`,
data: dataMatchesActiveSessionTarget
? input.data
: {
...input.data,
agentTargetId: activeAgentTargetId,
provider: input.activeSessionTarget.provider
}
};
}
export function reconcileOptimisticComposerTarget(input: {
activeConversationId: string | null;
data: AgentGUINodeData;
optimisticTarget: OptimisticComposerTarget | null;
}): OptimisticComposerTarget | null {
const optimisticTarget = input.optimisticTarget;
if (
!optimisticTarget ||
input.activeConversationId !== optimisticTarget.agentSessionId ||
nodeDataMatchesComposerTarget(input.data, optimisticTarget.target)
) {
return null;
}
return optimisticTarget;
}
export function nodeDataMatchesComposerTarget(
data: AgentGUINodeData,
target: AgentGUIComposerTargetData
): boolean {
return (
data.provider === target.provider &&
normalizeOptionalText(data.agentTargetId) === target.agentTargetId
);
}
export function isForegroundModelOptionsLoading(input: {
modelOptionsLoading: boolean | undefined;
selection: ACPConfigOptionSelection | null;
supportsModel: boolean;
}): boolean {
return (
input.supportsModel &&
input.modelOptionsLoading === true &&
(input.selection === null || input.selection.options.length === 0)
);
}
export function effectiveComposerSettingsFromOptions(
options: AgentActivityComposerOptions | null
): AgentSessionComposerSettings | null {
const settings = options?.effectiveSettings;
if (!settings) {
return null;
}
return {
model: normalizeOptionalText(settings.model),
reasoningEffort: normalizeOptionalText(
settings.reasoningEffort
) as AgentSessionReasoningEffort | null,
speed: normalizeOptionalText(settings.speed) as AgentSessionSpeed | null,
planMode: settings.planMode ?? undefined,
permissionModeId: normalizeOptionalText(settings.permissionModeId)
};
}
function composerOptionValues(
options: readonly { value: string }[]
): ReadonlySet<string> {
return new Set(options.map((option) => option.value));
}
export function sanitizeComposerSettingsForOptions(
settings: AgentSessionComposerSettings,
options: AgentActivityComposerOptions | null
): AgentSessionComposerSettings {
if (!options) {
return settings;
}
const modelValues = composerOptionValues(options.models);
const reasoningValues = composerOptionValues(options.reasoningEfforts);
const speedValues = composerOptionValues(options.speeds ?? []);
const permissionValues = new Set(
options.permissionConfig?.modes.map((mode) => mode.id) ?? []
);
const model = normalizeOptionalText(settings.model);
const reasoningEffort = normalizeOptionalText(settings.reasoningEffort);
const speed = normalizeOptionalText(settings.speed);
const permissionModeId = normalizeOptionalText(settings.permissionModeId);
const modelReasoningSelection = reasoningSelectionForModelFromComposerOptions(
options,
reasoningEffort as AgentSessionReasoningEffort | null,
model
);
return {
...settings,
model:
options.behavior?.modelOptionsAuthoritative === true &&
model &&
modelValues.size > 0 &&
!modelValues.has(model)
? null
: model,
reasoningEffort:
modelReasoningSelection !== null
? modelReasoningSelection.currentValue
: options.reasoningConfigurable !== true
? null
: reasoningEffort &&
reasoningValues.size > 0 &&
!reasoningValues.has(reasoningEffort)
? null
: (reasoningEffort as AgentSessionReasoningEffort | null),
speed:
speed && speedValues.size > 0 && !speedValues.has(speed)
? null
: (speed as AgentSessionSpeed | null),
permissionModeId:
permissionModeId &&
permissionValues.size > 0 &&
!permissionValues.has(permissionModeId)
? null
: permissionModeId
};
}
export function sanitizeComposerSettingsForTarget(input: {
settings: AgentSessionComposerSettings;
target: AgentGUIComposerTargetData;
options: AgentActivityComposerOptions | null;
}): AgentSessionComposerSettings {
if (!input.target.agentTargetId) {
return input.settings;
}
return sanitizeComposerSettingsForOptions(input.settings, input.options);
}
export type ComposerNativeModelVerdict =
| "verified"
| "rejected"
| "unverifiable";
export interface ComposerNativeModelOptionsTestimony {
models: readonly {
value: string;
requested?: boolean;
}[];
modelOptionsLoading?: boolean;
effectiveSettings?: {
model?: string | null;
} | null;
}
/**
* Verdict of a bare model id against the provider-native options list. Only
* settled catalog entries are testimony:
* - options missing or the model catalog still loading have no opinion
* ("unverifiable");
* - requested-origin entries (daemon warm-catalog append of the requested
* model, selected-model bootstrap echo, GUI current-value append) mirror
* the request rather than the catalog and are excluded — a list with no
* catalog entries left proves nothing;
* - as a fallback for options produced before provenance marking, a
* single-entry list mirroring the effective selection is recognized as the
* daemon's selected-model bootstrap echo (composerSelectedModelOptions)
* and proves nothing either;
* - otherwise the catalog either contains the model ("verified") or
* positively rejects it ("rejected").
*/
export function verifyComposerModelAgainstNativeOptions(
model: string,
options: ComposerNativeModelOptionsTestimony | null
): ComposerNativeModelVerdict {
if (options === null || options.modelOptionsLoading === true) {
return "unverifiable";
}
const catalogEntries = options.models.filter(
(option) => option.requested !== true
);
if (catalogEntries.length === 0) {
return "unverifiable";
}
const isSelectedModelEcho =
catalogEntries.length === 1 &&
catalogEntries[0]!.value === model &&
normalizeOptionalText(options.effectiveSettings?.model) === model;
if (isSelectedModelEcho) {
return "unverifiable";
}
return catalogEntries.some((option) => option.value === model)
? "verified"
: "rejected";
}
/**
* Home-composer default policy: a pure provider target must never adopt a
* bare model that the settled provider-native list rejects — it falls back
* to the provider default instead of presenting (and later submitting) a
* model the provider cannot run. This only acts on a positive rejection:
* while options are missing or the catalog is loading the stored default is
* left alone so a transient load state cannot destroy a legitimate
* remembered model.
*/
export function enforceComposerModelBindingForHomeDefaults(
settings: AgentSessionComposerSettings,
options: AgentActivityComposerOptions | null
): AgentSessionComposerSettings {
const model = normalizeOptionalText(settings.model);
const modelPlanId = normalizeOptionalText(settings.modelPlanId);
if (!model || modelPlanId) {
return settings;
}
return verifyComposerModelAgainstNativeOptions(model, options) === "rejected"
? { ...settings, model: null, modelPlanId: null }
: settings;
}
export function resolvePresentedComposerSettings(input: {
homeSettings: AgentSessionComposerSettings;
optimisticSettings: AgentSessionComposerSettings | null;
preloadedSettings: AgentSessionComposerSettings | null;
sessionSettings: AgentSessionComposerSettings | null;
}): AgentSessionComposerSettings {
const layers = [
input.sessionSettings,
input.optimisticSettings,
input.preloadedSettings,
input.homeSettings
];
const firstText = (
field: "model" | "reasoningEffort" | "speed" | "permissionModeId"
): string | null => {
for (const layer of layers) {
const value = normalizeOptionalText(layer?.[field]);
if (value) {
return value;
}
}
return null;
};
const firstBoolean = (
field:
| "codexSaverMode"
| "rtkSaverMode"
| "planMode"
| "browserUse"
| "computerUse",
fallback: boolean
): boolean => {
for (const layer of layers) {
const value = layer?.[field];
if (typeof value === "boolean") {
return value;
}
}
return fallback;
};
return {
codexSaverMode: firstBoolean("codexSaverMode", false),
rtkSaverMode: firstBoolean("rtkSaverMode", false),
model: firstText("model"),
reasoningEffort: firstText(
"reasoningEffort"
) as AgentSessionReasoningEffort | null,
speed: firstText("speed") as AgentSessionSpeed | null,
planMode: firstBoolean("planMode", false),
browserUse: firstBoolean("browserUse", true),
computerUse: firstBoolean("computerUse", true),
permissionModeId: firstText("permissionModeId")
};
}
export function resolveComposerSettingsPresentation(input: {
active: boolean;
homeSettings: AgentSessionComposerSettings;
optimisticSettings?: AgentSessionComposerSettings | null;
options: AgentActivityComposerOptions | null;
permissionModeId?: string | null;
sessionSettings?: AgentSessionComposerSettings | null;
}): AgentSessionComposerSettings {
const sessionSettings =
input.active &&
(input.sessionSettings != null || Boolean(input.permissionModeId))
? {
...(input.sessionSettings ?? {}),
permissionModeId:
normalizeOptionalText(input.permissionModeId) ??
normalizeOptionalText(input.sessionSettings?.permissionModeId)
}
: null;
return resolvePresentedComposerSettings({
sessionSettings,
optimisticSettings: input.active
? (input.optimisticSettings ?? null)
: input.homeSettings,
preloadedSettings: effectiveComposerSettingsFromOptions(input.options),
homeSettings: input.homeSettings
});
}