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

632 lines
20 KiB
TypeScript

// Agent GUI controller — composer settings, drafts, and permission labels.
import type {
AgentActivityComposerOptions,
AgentActivityGoalControlAction,
AgentActivitySlashCommandPolicy
} from "@tutti-os/agent-activity-core";
import { parseAgentActivityGoalControlText } from "@tutti-os/agent-activity-core";
import type { AgentPromptContentBlock } from "../../../shared/contracts/dto";
import type {
AgentSessionComposerSettings,
AgentSessionPermissionConfig,
AgentSessionPermissionModeOption,
AgentSessionReasoningEffort,
AgentSessionSpeed
} from "../../../shared/agentSessionTypes";
import type { AgentGUINodeData } from "../../../types";
import { translate } from "../../../i18n/index";
import type {
AgentGUIComposerSettingOption,
AgentGUIProviderSkillOption
} from "../model/agentGuiNodeTypes";
import type { ACPConfigOptionSelection } from "./agentGuiController.types";
import { normalizeOptionalText } from "./agentGuiController.promptHelpers";
export function normalizeConfigOptionValue(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
export function typedGoalControlFromComposer(
content: AgentPromptContentBlock[],
_displayPrompt: string | undefined,
goalControlSupported: boolean
): { action: AgentActivityGoalControlAction; objective?: string } | null {
if (
!goalControlSupported ||
content.length !== 1 ||
content[0]?.type !== "text"
) {
return null;
}
// Presentation-only displayPrompt must not hide or manufacture a control.
return parseAgentActivityGoalControlText(content[0].text ?? "");
}
export function composerSettingOptionsFromActivity(
options: readonly AgentActivityComposerOptions["models"][number][]
): AgentGUIComposerSettingOption[] {
return options.map((option) => ({ ...option }));
}
function reasoningOptionsFromRuntimeConfig(
runtimeContext: Record<string, unknown> | null | undefined
): {
currentValue: string | null;
options: AgentGUIComposerSettingOption[];
} | null {
const configOptions = runtimeContext?.configOptions;
if (!Array.isArray(configOptions)) {
return null;
}
for (const rawOption of configOptions) {
const option = recordValue(rawOption);
const id = normalizeConfigOptionValue(option?.id);
if (
!option ||
!id ||
!["reasoning_effort", "model_reasoning_effort", "effort"].includes(id)
) {
continue;
}
const rawEntries = option.options;
if (!Array.isArray(rawEntries)) {
return null;
}
const options: AgentGUIComposerSettingOption[] = [];
for (const rawEntry of rawEntries) {
const entry = recordValue(rawEntry);
const value = normalizeConfigOptionValue(entry?.value);
if (
!entry ||
!value ||
options.some((candidate) => candidate.value === value)
) {
continue;
}
const label =
normalizeConfigOptionValue(entry.name) ??
normalizeConfigOptionValue(entry.label) ??
value;
const description = normalizeConfigOptionValue(entry.description);
options.push({
value,
label,
...(description ? { description } : {})
});
}
return {
currentValue: normalizeConfigOptionValue(
option.currentValue ?? option.current_value
),
options
};
}
return null;
}
function recordValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function reasoningProfileForModel(
options: AgentActivityComposerOptions | null,
model: string | null
): {
defaultValue: string | null;
options: AgentGUIComposerSettingOption[];
} | null {
if (!options || !model) {
return null;
}
const profile = options.reasoningOptionsByModel?.[model];
if (!profile || !Array.isArray(profile.options)) {
return null;
}
return {
defaultValue: normalizeConfigOptionValue(profile.defaultValue),
options: profile.options.map((option) => ({ ...option }))
};
}
export function reasoningSelectionForModelFromComposerOptions(
options: AgentActivityComposerOptions | null,
currentValue: AgentSessionReasoningEffort | null,
selectedModel: string | null
): ACPConfigOptionSelection | null {
const modelProfile = reasoningProfileForModel(options, selectedModel);
if (!modelProfile) {
return null;
}
const supportedValues = new Set(
modelProfile.options.map((option) => option.value)
);
return {
options: modelProfile.options,
currentValue: ((currentValue && supportedValues.has(currentValue)
? currentValue
: null) ??
modelProfile.defaultValue ??
modelProfile.options[0]?.value ??
null) as AgentSessionReasoningEffort | null
};
}
export function modelSelectionFromComposerOptions(
options: AgentActivityComposerOptions | null,
currentValue: string | null
): ACPConfigOptionSelection | null {
if (!options) {
return null;
}
return {
options: composerSettingOptionsFromActivity(options.models),
currentValue
};
}
export function reasoningSelectionFromComposerOptions(
options: AgentActivityComposerOptions | null,
currentValue: AgentSessionReasoningEffort | null,
selectedModel: string | null = null,
sessionRuntimeContext: Record<string, unknown> | null = null
): ACPConfigOptionSelection | null {
const liveConfig = reasoningOptionsFromRuntimeConfig(sessionRuntimeContext);
const modelSelection = reasoningSelectionForModelFromComposerOptions(
options,
currentValue,
selectedModel
);
const sourceOptions = liveConfig
? liveConfig.options
: modelSelection
? modelSelection.options
: options
? composerSettingOptionsFromActivity(options.reasoningEfforts)
: [];
if (!options && !liveConfig && !modelSelection) {
return null;
}
const supportedValues = new Set(sourceOptions.map((option) => option.value));
const supportedValue = (
value: AgentSessionReasoningEffort | string | null | undefined
): AgentSessionReasoningEffort | null =>
value && supportedValues.has(value)
? (value as AgentSessionReasoningEffort)
: null;
const resolvedCurrentValue =
supportedValue(currentValue) ??
supportedValue(liveConfig?.currentValue) ??
supportedValue(modelSelection?.currentValue) ??
(sourceOptions[0]?.value as AgentSessionReasoningEffort | undefined) ??
null;
return {
options: sourceOptions,
currentValue: resolvedCurrentValue as AgentSessionReasoningEffort | null
};
}
export function speedSelectionFromComposerOptions(
options: AgentActivityComposerOptions | null,
currentValue: AgentSessionSpeed | null
): ACPConfigOptionSelection | null {
if (!options) {
return null;
}
return {
options: composerSettingOptionsFromActivity(options.speeds ?? []),
currentValue
};
}
export function providerSkillsFromComposerOptions(
options: AgentActivityComposerOptions | null
): AgentGUIProviderSkillOption[] {
if (!options) {
return [];
}
const invocationByTrigger = new Map(
(options.capabilityCatalog ?? []).flatMap((capability) =>
capability.trigger &&
capability.status === "available" &&
(capability.invocation === "promptItem" ||
capability.invocation === "textTrigger")
? [[capability.trigger, capability.invocation] as const]
: []
)
);
return dedupeProviderSkills([
...options.skills.map((skill) => ({
...skill,
...(invocationByTrigger.get(skill.trigger)
? { invocation: invocationByTrigger.get(skill.trigger) }
: {})
})),
...(options.capabilityCatalog ?? [])
.filter((capability) => {
if (capability.kind === "connector") {
return (
capability.invocation !== "none" && Boolean(capability.trigger)
);
}
return (
capability.kind === "skill" &&
capability.invocation === "promptItem" &&
capability.status === "available" &&
Boolean(capability.trigger) &&
Boolean(capability.path)
);
})
.map((capability): AgentGUIProviderSkillOption => {
const isConnector = capability.kind === "connector";
return {
name: isConnector ? capability.label : capability.name,
...(isConnector ? { connectorKey: capability.name } : {}),
trigger: capability.trigger!,
invocation:
capability.invocation === "textTrigger"
? "textTrigger"
: "promptItem",
sourceKind: isConnector ? "connector" : "plugin",
kind: isConnector ? "connector" : "skill",
...(isConnector ? { status: capability.status } : {}),
...(isConnector && capability.iconUrl
? { iconUrl: capability.iconUrl }
: {}),
...(isConnector && capability.installedAtUnixMs
? { installedAtUnixMs: capability.installedAtUnixMs }
: {}),
...(capability.description
? { description: capability.description }
: {}),
...(capability.pluginName
? { pluginName: capability.pluginName }
: {}),
...(capability.path ? { path: capability.path } : {})
};
})
]);
}
export function areProviderSkillOptionsEqual(
left: AgentGUIProviderSkillOption,
right: AgentGUIProviderSkillOption
): boolean {
return (
left.name === right.name &&
left.connectorKey === right.connectorKey &&
left.iconUrl === right.iconUrl &&
left.installedAtUnixMs === right.installedAtUnixMs &&
left.trigger === right.trigger &&
left.invocation === right.invocation &&
left.sourceKind === right.sourceKind &&
left.description === right.description &&
left.pluginName === right.pluginName &&
left.path === right.path &&
left.kind === right.kind &&
left.status === right.status
);
}
export function areProviderSkillOptionListsEqual(
left: readonly AgentGUIProviderSkillOption[],
right: readonly AgentGUIProviderSkillOption[]
): boolean {
return (
left.length === right.length &&
left.every((skill, index) =>
areProviderSkillOptionsEqual(skill, right[index]!)
)
);
}
export function slashCommandPoliciesEqual(
left: AgentActivitySlashCommandPolicy | null | undefined,
right: AgentActivitySlashCommandPolicy | null | undefined
): boolean {
if (!left || !right) {
return left === right;
}
return (
(left.commandCatalogAuthoritative ?? false) ===
(right.commandCatalogAuthoritative ?? false) &&
left.fallbackCommands.length === right.fallbackCommands.length &&
left.fallbackCommands.every(
(command, index) => command === right.fallbackCommands[index]
) &&
left.commandEffects.length === right.commandEffects.length &&
left.commandEffects.every((effect, index) => {
const other = right.commandEffects[index];
return (
other !== undefined &&
effect.command === other.command &&
effect.effect === other.effect
);
})
);
}
function dedupeProviderSkills(
skills: readonly AgentGUIProviderSkillOption[]
): AgentGUIProviderSkillOption[] {
const seen = new Set<string>();
const result: AgentGUIProviderSkillOption[] = [];
for (const skill of skills) {
const key = skill.trigger || `${skill.kind ?? "skill"}:${skill.name}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push(skill);
}
return result;
}
export function permissionConfigFromComposerOptions(
options: AgentActivityComposerOptions | null
): AgentSessionPermissionConfig | null {
const config = options?.permissionConfig;
if (!config) {
return null;
}
const defaultValue = normalizePermissionModeId(config.defaultValue);
return {
configurable: config.configurable,
...(defaultValue ? { defaultValue } : {}),
modes: config.modes.map((mode) => ({
id: mode.id,
label: mode.label,
description: mode.description,
semantic: normalizePermissionModeSemantic(mode.semantic)
}))
};
}
export function normalizePermissionModeSemantic(
value: string | undefined
): AgentSessionPermissionModeOption["semantic"] {
switch (value) {
case "ask-before-write":
case "accept-edits":
case "locked-down":
case "auto":
case "full-access":
case "unconfigurable":
return value;
default:
return (normalizeOptionalText(value) ??
"unconfigurable") as AgentSessionPermissionModeOption["semantic"];
}
}
export function resolveEffectiveComposerSettings(input: {
settings: AgentSessionComposerSettings;
}): AgentSessionComposerSettings {
return {
model: normalizeOptionalText(input.settings.model) ?? null,
reasoningEffort:
(normalizeOptionalText(
input.settings.reasoningEffort
) as AgentSessionReasoningEffort | null) ?? null,
speed:
(normalizeOptionalText(
input.settings.speed
) as AgentSessionSpeed | null) ?? null,
planMode: Boolean(input.settings.planMode),
// Browser/computer use default on; preserve explicit opt-outs.
browserUse: input.settings.browserUse ?? true,
computerUse: input.settings.computerUse ?? true,
permissionModeId: normalizePermissionModeId(input.settings.permissionModeId)
};
}
export function normalizePermissionModeId(
value: string | null | undefined
): string | null {
return normalizeOptionalText(value);
}
export function cloneComposerSettings(
settings: AgentSessionComposerSettings | null
): AgentSessionComposerSettings | null {
if (!settings) {
return null;
}
return { ...settings };
}
export function sameComposerSettings(
left: AgentSessionComposerSettings | null,
right: AgentSessionComposerSettings | null
): boolean {
return (
left?.codexSaverMode === right?.codexSaverMode &&
left?.rtkSaverMode === right?.rtkSaverMode &&
(left?.model ?? null) === (right?.model ?? null) &&
(left?.reasoningEffort ?? null) === (right?.reasoningEffort ?? null) &&
(left?.speed ?? null) === (right?.speed ?? null) &&
Boolean(left?.planMode) === Boolean(right?.planMode) &&
(left?.browserUse ?? true) === (right?.browserUse ?? true) &&
(left?.computerUse ?? true) === (right?.computerUse ?? true) &&
(left?.permissionModeId ?? null) === (right?.permissionModeId ?? null)
);
}
export function buildNodeDefaultComposerSettings(
data: AgentGUINodeData,
options?: {
defaultReasoningEffort?: AgentSessionReasoningEffort | null;
defaultSpeed?: AgentSessionSpeed | null;
}
): AgentSessionComposerSettings {
// Generic cleanup only — provider-level clamping is owned by the daemon
// (normalizeComposerSettingsForProvider and the session create path).
const composerOverrides = nodeComposerOverridesForProvider(data) ?? {};
return {
codexSaverMode: composerOverrides.codexSaverMode === true,
rtkSaverMode: composerOverrides.rtkSaverMode === true,
model: normalizeOptionalText(composerOverrides.model),
reasoningEffort:
(normalizeOptionalText(
composerOverrides.reasoningEffort
) as AgentSessionReasoningEffort | null) ??
options?.defaultReasoningEffort ??
null,
speed:
(normalizeOptionalText(
composerOverrides.speed
) as AgentSessionSpeed | null) ??
options?.defaultSpeed ??
null,
planMode: Boolean(composerOverrides.planMode),
browserUse: composerOverrides.browserUse ?? true,
computerUse: composerOverrides.computerUse ?? true,
permissionModeId: normalizePermissionModeId(
composerOverrides.permissionModeId
)
};
}
export function nodeComposerOverridesForProvider(
data: AgentGUINodeData
): AgentSessionComposerSettings | null {
const agentTargetId = normalizeOptionalText(data.agentTargetId);
if (agentTargetId) {
return data.composerOverridesByAgentTargetId?.[agentTargetId] ?? null;
}
return (
data.composerOverridesByProvider?.[data.provider] ??
data.composerOverrides ??
null
);
}
export function permissionModeOptions(
provider: AgentGUINodeData["provider"],
permissionConfig: AgentSessionPermissionConfig | null | undefined
): AgentGUIComposerSettingOption[] {
if (!permissionConfig?.configurable) {
return [];
}
return permissionConfig.modes.map((mode) => ({
value: mode.id,
label: permissionModeLabel(provider, mode),
description: permissionModeDescription(provider, mode)
}));
}
export function nodeDataFromComposerSettings(
current: AgentGUINodeData,
settings: AgentSessionComposerSettings
): AgentGUINodeData {
// Generic cleanup only — provider-level clamping is owned by the daemon.
const composerOverrides = {
codexSaverMode: settings.codexSaverMode === true,
rtkSaverMode: settings.rtkSaverMode === true,
model: normalizeOptionalText(settings.model),
reasoningEffort: normalizeOptionalText(settings.reasoningEffort),
speed: normalizeOptionalText(settings.speed),
planMode: Boolean(settings.planMode),
// Raw passthrough (no Boolean coercion): undefined means "default on", so
// only an explicit false persists as an opt-out.
browserUse: settings.browserUse,
computerUse: settings.computerUse,
permissionModeId: normalizePermissionModeId(settings.permissionModeId)
};
const agentTargetId = normalizeOptionalText(current.agentTargetId);
if (agentTargetId) {
return {
...current,
composerOverridesByAgentTargetId: {
...current.composerOverridesByAgentTargetId,
[agentTargetId]: composerOverrides
}
};
}
return {
...current,
composerOverrides,
composerOverridesByProvider: {
...current.composerOverridesByProvider,
[current.provider]: composerOverrides
}
};
}
export function permissionModeLabel(
provider: AgentGUINodeData["provider"],
option: AgentSessionPermissionModeOption
): string {
const providerKey = `agentHost.agentGui.permissionModes.${provider}.${option.id}.label`;
const providerLabel = translate(providerKey);
if (providerLabel !== providerKey) {
return providerLabel;
}
const semanticKey = `agentHost.agentGui.permissionSemantics.${option.semantic}.label`;
const semanticLabel = translate(semanticKey);
if (semanticLabel !== semanticKey) {
return semanticLabel;
}
const contractLabel = normalizeOptionalText(option.label);
if (contractLabel) {
return contractLabel;
}
return option.id;
}
export function permissionModeDescription(
provider: AgentGUINodeData["provider"],
option: AgentSessionPermissionModeOption
): string | undefined {
const providerKey = `agentHost.agentGui.permissionModes.${provider}.${option.id}.description`;
const providerLabel = translate(providerKey);
if (providerLabel !== providerKey) {
return providerLabel;
}
const semanticKey = `agentHost.agentGui.permissionSemantics.${option.semantic}.description`;
const semanticLabel = translate(semanticKey);
if (semanticLabel !== semanticKey) {
return semanticLabel;
}
const contractDescription = normalizeOptionalText(option.description);
if (contractDescription) {
return contractDescription;
}
return undefined;
}
export const NODE_DEFAULT_DRAFT_KEY = "__agent_gui_node_defaults__";
export function nodeDefaultDraftKey(
agentProvider: AgentGUINodeData["provider"],
agentTargetId?: string | null
): string {
const normalizedAgentTargetId = normalizeOptionalText(agentTargetId);
if (normalizedAgentTargetId) {
return `${NODE_DEFAULT_DRAFT_KEY}:target:${normalizedAgentTargetId}`;
}
return `${NODE_DEFAULT_DRAFT_KEY}:${agentProvider}`;
}
export function readNodeDefaultDraftSettings(input: {
data: AgentGUINodeData;
defaultReasoningEffort?: AgentSessionReasoningEffort | null;
defaultSpeed?: AgentSessionSpeed | null;
drafts: Record<string, AgentSessionComposerSettings>;
}): AgentSessionComposerSettings {
const agentTargetId = normalizeOptionalText(input.data.agentTargetId);
if (agentTargetId) {
return (
input.drafts[nodeDefaultDraftKey(input.data.provider, agentTargetId)] ??
{}
);
}
return (
input.drafts[nodeDefaultDraftKey(input.data.provider)] ??
input.drafts[NODE_DEFAULT_DRAFT_KEY] ??
{}
);
}