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
159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
import type { AgentActivitySessionDetailSnapshot } from "@tutti-os/agent-activity-core";
|
|
import type { WorkspaceAgentSessionDetailResponse } from "@tutti-os/client-tuttid-ts";
|
|
import {
|
|
agentActivityEditRetryAvailabilityFromTuttid,
|
|
agentActivitySessionFromTuttidSession,
|
|
agentActivityTurnFromTuttidTurn,
|
|
type AgentActivitySessionMappingOptions
|
|
} from "./mappers.ts";
|
|
|
|
/**
|
|
* Maps one authoritative tuttid detail response without performing transport
|
|
* or dispatch work. Hosts feed the returned aggregate to the engine through a
|
|
* single `session/detailSnapshotReceived` intent so root, child, and Turn
|
|
* state cannot become observably half-applied.
|
|
*/
|
|
export function agentActivitySessionDetailFromTuttid(
|
|
workspaceId: string,
|
|
expectedAgentSessionId: string,
|
|
detail: WorkspaceAgentSessionDetailResponse,
|
|
options: AgentActivitySessionMappingOptions
|
|
): AgentActivitySessionDetailSnapshot {
|
|
assertTuttidSessionDetailContract(expectedAgentSessionId, detail);
|
|
const projectionOptions = {
|
|
...options,
|
|
lifecycleCapabilitiesProjected: detail.lifecycleCapabilitiesProjected
|
|
};
|
|
return {
|
|
projection:
|
|
detail.projection === "full" ? "authoritative" : "message_hydration",
|
|
lifecycleCapabilitiesProjected: detail.lifecycleCapabilitiesProjected,
|
|
editRetry: agentActivityEditRetryAvailabilityFromTuttid(detail.editRetry),
|
|
session: agentActivitySessionFromTuttidSession(
|
|
workspaceId,
|
|
detail.session,
|
|
projectionOptions
|
|
),
|
|
childSessions: detail.childSessions.map((session) =>
|
|
agentActivitySessionFromTuttidSession(
|
|
workspaceId,
|
|
session,
|
|
projectionOptions
|
|
)
|
|
),
|
|
turns: detail.turns.map(agentActivityTurnFromTuttidTurn)
|
|
};
|
|
}
|
|
|
|
function assertTuttidSessionDetailContract(
|
|
expectedAgentSessionId: string,
|
|
detail: WorkspaceAgentSessionDetailResponse
|
|
): void {
|
|
if (
|
|
detail.projection !== "full" &&
|
|
detail.projection !== "messageHydration"
|
|
) {
|
|
throw detailContractError(
|
|
`projection ${JSON.stringify(detail.projection)} is invalid`
|
|
);
|
|
}
|
|
const capabilitiesShouldBeProjected = detail.projection === "full";
|
|
if (detail.lifecycleCapabilitiesProjected !== capabilitiesShouldBeProjected) {
|
|
throw detailContractError(
|
|
`lifecycle capability projection does not match detail projection ${JSON.stringify(detail.projection)}`
|
|
);
|
|
}
|
|
const expectedId = trimmedString(expectedAgentSessionId);
|
|
const rootId = trimmedString(detail.session?.id);
|
|
if (!expectedId || rootId !== expectedId) {
|
|
throw detailContractError(
|
|
`root Session id ${JSON.stringify(rootId)} does not match requested id ${JSON.stringify(expectedId)}`
|
|
);
|
|
}
|
|
if (!Array.isArray(detail.childSessions) || !Array.isArray(detail.turns)) {
|
|
throw detailContractError(
|
|
"childSessions and turns must be authoritative arrays"
|
|
);
|
|
}
|
|
const hierarchyRootId =
|
|
detail.session.kind === "root"
|
|
? rootId
|
|
: trimmedString(detail.session.rootAgentSessionId);
|
|
if (
|
|
(detail.session.kind !== "root" && detail.session.kind !== "child") ||
|
|
!hierarchyRootId
|
|
) {
|
|
throw detailContractError(
|
|
"requested Session hierarchy identity is invalid"
|
|
);
|
|
}
|
|
|
|
const childrenById = new Map<
|
|
string,
|
|
WorkspaceAgentSessionDetailResponse["childSessions"][number]
|
|
>();
|
|
for (const child of detail.childSessions) {
|
|
const childId = trimmedString(child.id);
|
|
if (
|
|
!childId ||
|
|
childId === rootId ||
|
|
child.kind !== "child" ||
|
|
trimmedString(child.rootAgentSessionId) !== hierarchyRootId ||
|
|
childrenById.has(childId)
|
|
) {
|
|
throw detailContractError(
|
|
`child Session identity ${JSON.stringify(childId)} is invalid`
|
|
);
|
|
}
|
|
childrenById.set(childId, child);
|
|
}
|
|
|
|
for (const child of detail.childSessions) {
|
|
assertDescendsFromRequestedSession(child, rootId, childrenById);
|
|
}
|
|
for (const turn of detail.turns) {
|
|
const turnId = trimmedString(turn.turnId);
|
|
const ownerId = trimmedString(turn.agentSessionId);
|
|
if (!turnId || ownerId !== rootId) {
|
|
throw detailContractError(
|
|
`Turn ${JSON.stringify(turnId)} must be owned by requested Session ${JSON.stringify(rootId)}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertDescendsFromRequestedSession(
|
|
child: WorkspaceAgentSessionDetailResponse["childSessions"][number],
|
|
requestedSessionId: string,
|
|
childrenById: ReadonlyMap<
|
|
string,
|
|
WorkspaceAgentSessionDetailResponse["childSessions"][number]
|
|
>
|
|
): void {
|
|
const childId = trimmedString(child.id);
|
|
const visited = new Set<string>([childId]);
|
|
let parentId = trimmedString(child.parentAgentSessionId);
|
|
while (parentId && parentId !== requestedSessionId) {
|
|
if (visited.has(parentId)) {
|
|
throw detailContractError(
|
|
`child Session ${JSON.stringify(childId)} contains a parent cycle`
|
|
);
|
|
}
|
|
visited.add(parentId);
|
|
parentId = trimmedString(childrenById.get(parentId)?.parentAgentSessionId);
|
|
}
|
|
if (parentId !== requestedSessionId) {
|
|
throw detailContractError(
|
|
`child Session ${JSON.stringify(childId)} is outside the requested Session hierarchy`
|
|
);
|
|
}
|
|
}
|
|
|
|
function detailContractError(reason: string): Error {
|
|
return new Error(`Protocol v2 contract error: ${reason}`);
|
|
}
|
|
|
|
function trimmedString(value: unknown): string {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|