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

506 lines
16 KiB
TypeScript

import type {
WorkbenchHostExternalStateLookupInput,
WorkbenchHostExternalStateSource
} from "@tutti-os/workbench-surface";
import {
agentGuiWorkbenchProviders,
normalizeAgentGuiWorkbenchProvider
} from "./providerCatalog.ts";
import type {
AgentGuiWorkbenchComposerOverrides,
AgentGuiWorkbenchComposerOverridesByAgentTargetId,
AgentGuiWorkbenchComposerOverridesByProvider,
AgentGuiWorkbenchNodeState,
AgentGuiWorkbenchProvider,
AgentGuiWorkbenchState,
AgentGuiWorkbenchWorkspaceState
} from "./types.ts";
type AgentGuiWorkbenchStateLookupRequest = Pick<
WorkbenchHostExternalStateLookupInput,
"instanceId" | "typeId"
> &
Partial<Pick<WorkbenchHostExternalStateLookupInput, "nodeId">>;
export function createDefaultAgentGuiWorkbenchNodeState(
provider: AgentGuiWorkbenchProvider = "codex"
): AgentGuiWorkbenchNodeState {
return {
agentTargetId: null,
composerOverrides: null,
composerOverridesByAgentTargetId: null,
composerOverridesByProvider: null,
conversationCount: null,
conversationRailCollapsed: false,
conversationRailWidthPx: null,
lastActiveAgentSessionId: null,
lastActiveAgentSessionIdByAgentTargetId: null,
provider
};
}
export function normalizeAgentGuiWorkbenchState(
state: unknown
): AgentGuiWorkbenchState {
if (!isRecord(state)) {
return createDefaultAgentGuiWorkbenchState();
}
const agentTargetId = normalizeOptionalNonEmptyString(state.agentTargetId);
const lastActiveAgentSessionIdByAgentTargetId =
normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
state.lastActiveAgentSessionIdByAgentTargetId
);
return {
...(agentTargetId ? { agentTargetId } : {}),
conversationRailCollapsed: state.conversationRailCollapsed === true,
conversationRailWidthPx: normalizeOptionalPositiveNumber(
state.conversationRailWidthPx
),
lastActiveAgentSessionId:
typeof state.lastActiveAgentSessionId === "string"
? state.lastActiveAgentSessionId
: null,
...(lastActiveAgentSessionIdByAgentTargetId
? { lastActiveAgentSessionIdByAgentTargetId }
: {})
};
}
/**
* One-time persisted-state reader for snapshots written before agentTargetId
* became the canonical AgentGUI selection key. Current-state normalizers stay
* strict so live callers cannot continue using the removed alias.
*/
export function migrateLegacyAgentGuiWorkbenchState(state: unknown): unknown {
if (
!isRecord(state) ||
normalizeOptionalNonEmptyString(state.agentTargetId)
) {
return state;
}
const legacyAgentTargetId = normalizeOptionalNonEmptyString(
state.providerTargetId
);
return legacyAgentTargetId
? { ...state, agentTargetId: legacyAgentTargetId }
: state;
}
export function projectAgentGuiWorkbenchState(
state: AgentGuiWorkbenchNodeState
): AgentGuiWorkbenchState {
const agentTargetId = normalizeOptionalNonEmptyString(state.agentTargetId);
const lastActiveAgentSessionIdByAgentTargetId =
normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
state.lastActiveAgentSessionIdByAgentTargetId
);
return {
...(agentTargetId ? { agentTargetId } : {}),
conversationRailCollapsed: state.conversationRailCollapsed === true,
conversationRailWidthPx: normalizeOptionalPositiveNumber(
state.conversationRailWidthPx
),
lastActiveAgentSessionId: state.lastActiveAgentSessionId ?? null,
...(lastActiveAgentSessionIdByAgentTargetId
? { lastActiveAgentSessionIdByAgentTargetId }
: {})
};
}
export function areAgentGuiWorkbenchStatesEqual(
left: AgentGuiWorkbenchState,
right: AgentGuiWorkbenchState
): boolean {
return (
(left.agentTargetId ?? null) === (right.agentTargetId ?? null) &&
left.conversationRailCollapsed === right.conversationRailCollapsed &&
left.conversationRailWidthPx === right.conversationRailWidthPx &&
left.lastActiveAgentSessionId === right.lastActiveAgentSessionId &&
stringRecordsEqual(
left.lastActiveAgentSessionIdByAgentTargetId,
right.lastActiveAgentSessionIdByAgentTargetId
)
);
}
export function normalizeAgentGuiWorkbenchNodeState(
state: unknown,
fallbackProvider: AgentGuiWorkbenchProvider = "codex"
): AgentGuiWorkbenchNodeState {
const persistedState = isRecord(state) ? state : {};
const provider =
persistedState.provider === undefined || persistedState.provider === null
? fallbackProvider
: normalizeAgentGuiWorkbenchProvider(persistedState.provider);
return {
...createDefaultAgentGuiWorkbenchNodeState(provider),
agentTargetId: normalizeOptionalNonEmptyString(
persistedState.agentTargetId
),
composerOverrides: normalizeAgentGuiWorkbenchComposerOverrides(
persistedState.composerOverrides
),
composerOverridesByAgentTargetId:
normalizeAgentGuiWorkbenchComposerOverridesByAgentTargetId(
persistedState.composerOverridesByAgentTargetId
),
composerOverridesByProvider:
normalizeAgentGuiWorkbenchComposerOverridesByProvider(
persistedState.composerOverridesByProvider
),
conversationCount: normalizeOptionalNonNegativeNumber(
persistedState.conversationCount
),
conversationRailCollapsed:
persistedState.conversationRailCollapsed === true,
conversationRailWidthPx: normalizeOptionalPositiveNumber(
persistedState.conversationRailWidthPx
),
lastActiveAgentSessionId:
typeof persistedState.lastActiveAgentSessionId === "string"
? persistedState.lastActiveAgentSessionId
: null,
lastActiveAgentSessionIdByAgentTargetId:
normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
persistedState.lastActiveAgentSessionIdByAgentTargetId
),
provider
};
}
export function areAgentGuiWorkbenchNodeStatesEqual(
left: AgentGuiWorkbenchNodeState,
right: AgentGuiWorkbenchNodeState
): boolean {
return (
(left.agentTargetId ?? null) === (right.agentTargetId ?? null) &&
composerOverridesEqual(left.composerOverrides, right.composerOverrides) &&
composerOverridesByAgentTargetIdEqual(
left.composerOverridesByAgentTargetId,
right.composerOverridesByAgentTargetId
) &&
composerOverridesByProviderEqual(
left.composerOverridesByProvider,
right.composerOverridesByProvider
) &&
left.conversationCount === right.conversationCount &&
left.conversationRailCollapsed === right.conversationRailCollapsed &&
left.conversationRailWidthPx === right.conversationRailWidthPx &&
left.lastActiveAgentSessionId === right.lastActiveAgentSessionId &&
stringRecordsEqual(
left.lastActiveAgentSessionIdByAgentTargetId,
right.lastActiveAgentSessionIdByAgentTargetId
) &&
left.provider === right.provider &&
(left.agentTargetId ?? null) === (right.agentTargetId ?? null)
);
}
export function createAgentGuiWorkbenchNodeStateSource(input: {
typeId?: string;
workspaceId: string;
}): {
externalStateSource: WorkbenchHostExternalStateSource<
AgentGuiWorkbenchState | null,
AgentGuiWorkbenchWorkspaceState
>;
readNodeState: (
request: AgentGuiWorkbenchStateLookupRequest
) => AgentGuiWorkbenchState | null;
writeNodeState: (
request: AgentGuiWorkbenchStateLookupRequest & {
state: AgentGuiWorkbenchState;
}
) => void;
/**
* Returns the launch instanceId of an open node currently showing the given
* agent session, or null when none is found. Used to focus an existing
* conversation instead of launching a duplicate node.
*/
findInstanceIdByAgentSessionId: (agentSessionId: string) => string | null;
} {
const typeId = input.typeId ?? "agent-gui";
const nodeStateByKey = new Map<string, AgentGuiWorkbenchState>();
// Tracks the launch instanceId behind each state key so a node can be located
// by the session it is currently showing, even when its key is node-scoped.
const instanceIdByKey = new Map<string, string>();
const listeners = new Set<() => void>();
const workspaceState = { workspaceId: input.workspaceId };
const lookupState = (request: AgentGuiWorkbenchStateLookupRequest) => {
const nodeState = request.nodeId
? nodeStateByKey.get(agentGuiWorkbenchNodeStateKey(request))
: null;
const state =
nodeState ??
nodeStateByKey.get(agentGuiWorkbenchInstanceStateKey(request));
return state ?? null;
};
const notify = () => {
for (const listener of listeners) {
listener();
}
};
return {
externalStateSource: {
getNodeState(request) {
if (request.typeId !== typeId) {
return null;
}
return lookupState(request);
},
getSnapshotNodeState(request) {
if (request.typeId !== typeId) {
return null;
}
return lookupState(request);
},
getWorkspaceState() {
return workspaceState;
},
subscribeNodeState(request, listener) {
if (request.typeId !== typeId) {
return () => {};
}
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
},
readNodeState(request) {
if (request.typeId !== typeId) {
return null;
}
return lookupState(request);
},
findInstanceIdByAgentSessionId(agentSessionId) {
const target = agentSessionId.trim();
if (!target) {
return null;
}
for (const [key, state] of nodeStateByKey) {
if (state.lastActiveAgentSessionId?.trim() === target) {
return instanceIdByKey.get(key) ?? null;
}
}
return null;
},
writeNodeState(request) {
if (request.typeId !== typeId) {
return;
}
const key = agentGuiWorkbenchNodeStateKey(request);
instanceIdByKey.set(key, request.instanceId);
const previous = nodeStateByKey.get(key);
let clearedInstanceSeed = false;
if (request.nodeId) {
const instanceKey = agentGuiWorkbenchInstanceStateKey(request);
instanceIdByKey.delete(instanceKey);
clearedInstanceSeed = nodeStateByKey.delete(instanceKey);
}
const next = {
...normalizeAgentGuiWorkbenchState(request.state)
};
if (
!clearedInstanceSeed &&
previous &&
areAgentGuiWorkbenchMemoryStatesEqual(previous, next)
) {
return;
}
nodeStateByKey.set(key, next);
notify();
}
};
}
function areAgentGuiWorkbenchMemoryStatesEqual(
left: AgentGuiWorkbenchState,
right: AgentGuiWorkbenchState
): boolean {
return areAgentGuiWorkbenchStatesEqual(left, right);
}
function agentGuiWorkbenchNodeStateKey(
request: Pick<WorkbenchHostExternalStateLookupInput, "instanceId"> &
Partial<Pick<WorkbenchHostExternalStateLookupInput, "nodeId">>
): string {
return request.nodeId
? `node:${request.nodeId}`
: agentGuiWorkbenchInstanceStateKey(request);
}
function agentGuiWorkbenchInstanceStateKey(
request: Pick<WorkbenchHostExternalStateLookupInput, "instanceId">
): string {
return `instance:${request.instanceId}`;
}
function createDefaultAgentGuiWorkbenchState(): AgentGuiWorkbenchState {
return {
conversationRailCollapsed: false,
conversationRailWidthPx: null,
lastActiveAgentSessionId: null
};
}
function normalizeOptionalNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function normalizeAgentGuiWorkbenchComposerOverrides(
value: unknown
): AgentGuiWorkbenchComposerOverrides | null {
if (!isRecord(value)) {
return null;
}
const composerOverrides: AgentGuiWorkbenchComposerOverrides = {};
if (typeof value.model === "string" && value.model.trim()) {
composerOverrides.model = value.model.trim();
}
if (
typeof value.reasoningEffort === "string" &&
value.reasoningEffort.trim()
) {
composerOverrides.reasoningEffort = value.reasoningEffort.trim();
}
if (
typeof value.permissionModeId === "string" &&
value.permissionModeId.trim()
) {
composerOverrides.permissionModeId = value.permissionModeId.trim();
}
if (typeof value.planMode === "boolean") {
composerOverrides.planMode = value.planMode;
}
return Object.keys(composerOverrides).length > 0 ? composerOverrides : null;
}
function normalizeAgentGuiWorkbenchComposerOverridesByProvider(
value: unknown
): AgentGuiWorkbenchComposerOverridesByProvider | null {
if (!isRecord(value)) {
return null;
}
const result: AgentGuiWorkbenchComposerOverridesByProvider = {};
for (const provider of agentGuiWorkbenchProviders) {
const overrides = normalizeAgentGuiWorkbenchComposerOverrides(
value[provider]
);
if (overrides) {
result[provider] = overrides;
}
}
return Object.keys(result).length > 0 ? result : null;
}
function normalizeAgentGuiWorkbenchComposerOverridesByAgentTargetId(
value: unknown
): AgentGuiWorkbenchComposerOverridesByAgentTargetId | null {
if (!isRecord(value)) {
return null;
}
const result: AgentGuiWorkbenchComposerOverridesByAgentTargetId = {};
for (const [rawAgentTargetId, rawOverrides] of Object.entries(value)) {
const agentTargetId = normalizeOptionalNonEmptyString(rawAgentTargetId);
if (!agentTargetId) {
continue;
}
const overrides = normalizeAgentGuiWorkbenchComposerOverrides(rawOverrides);
if (overrides) {
result[agentTargetId] = overrides;
}
}
return Object.keys(result).length > 0 ? result : null;
}
function normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
value: unknown
): Record<string, string> | null {
if (!isRecord(value)) {
return null;
}
const result: Record<string, string> = {};
for (const [rawAgentTargetId, rawAgentSessionId] of Object.entries(value)) {
const agentTargetId = normalizeOptionalNonEmptyString(rawAgentTargetId);
const agentSessionId = normalizeOptionalNonEmptyString(rawAgentSessionId);
if (agentTargetId && agentSessionId) {
result[agentTargetId] = agentSessionId;
}
}
return Object.keys(result).length > 0 ? result : null;
}
function normalizeOptionalPositiveNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) && value > 0
? Math.round(value)
: null;
}
function normalizeOptionalNonNegativeNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) && value >= 0
? Math.round(value)
: null;
}
function composerOverridesEqual(
left: AgentGuiWorkbenchComposerOverrides | null | undefined,
right: AgentGuiWorkbenchComposerOverrides | null | undefined
): boolean {
return (
(left?.model ?? null) === (right?.model ?? null) &&
(left?.permissionModeId ?? null) === (right?.permissionModeId ?? null) &&
(left?.planMode ?? null) === (right?.planMode ?? null) &&
(left?.reasoningEffort ?? null) === (right?.reasoningEffort ?? null)
);
}
function composerOverridesByProviderEqual(
left: AgentGuiWorkbenchComposerOverridesByProvider | null | undefined,
right: AgentGuiWorkbenchComposerOverridesByProvider | null | undefined
): boolean {
for (const provider of agentGuiWorkbenchProviders) {
if (!composerOverridesEqual(left?.[provider], right?.[provider])) {
return false;
}
}
return true;
}
function composerOverridesByAgentTargetIdEqual(
left: AgentGuiWorkbenchComposerOverridesByAgentTargetId | null | undefined,
right: AgentGuiWorkbenchComposerOverridesByAgentTargetId | null | undefined
): boolean {
const leftKeys = Object.keys(left ?? {}).sort();
const rightKeys = Object.keys(right ?? {}).sort();
if (leftKeys.length !== rightKeys.length) {
return false;
}
return leftKeys.every(
(key, index) =>
key === rightKeys[index] &&
composerOverridesEqual(left?.[key], right?.[key])
);
}
function stringRecordsEqual(
left: Record<string, string> | null | undefined,
right: Record<string, string> | null | undefined
): boolean {
const leftKeys = Object.keys(left ?? {}).sort();
const rightKeys = Object.keys(right ?? {}).sort();
return (
leftKeys.length === rightKeys.length &&
leftKeys.every(
(key, index) => key === rightKeys[index] && left?.[key] === right?.[key]
)
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}