Files
stack/packages/mosaic/src/tui/gateway-api.ts
jason.woltje 0b621660c8
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was canceled
feat(tess): wire durable interaction surfaces (#732)
2026-07-13 10:05:29 +00:00

640 lines
18 KiB
TypeScript

/**
* Minimal gateway REST API client for the TUI and CLI commands.
*/
export interface ModelInfo {
id: string;
provider: string;
name: string;
}
export interface ProviderInfo {
id: string;
name: string;
available: boolean;
models: ModelInfo[];
}
export interface SessionInfo {
id: string;
provider: string;
modelId: string;
createdAt: string;
promptCount: number;
channels: string[];
durationMs: number;
}
export interface SessionListResult {
sessions: SessionInfo[];
total: number;
}
// ── Agent Config types ──
export interface AgentConfigInfo {
id: string;
name: string;
provider: string;
model: string;
status: string;
projectId: string | null;
ownerId: string | null;
systemPrompt: string | null;
allowedTools: string[] | null;
skills: string[] | null;
isSystem: boolean;
config: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
}
// ── Project types ──
export interface ProjectInfo {
id: string;
name: string;
description: string | null;
status: string;
ownerId: string | null;
createdAt: string;
updatedAt: string;
}
// ── Mission types ──
export interface MissionInfo {
id: string;
name: string;
description: string | null;
status: string;
projectId: string | null;
userId: string | null;
phase: string | null;
milestones: Record<string, unknown>[] | null;
config: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
}
// ── Mission Task types ──
export interface MissionTaskInfo {
id: string;
missionId: string;
taskId: string | null;
userId: string;
status: string;
description: string | null;
notes: string | null;
pr: string | null;
createdAt: string;
updatedAt: string;
}
// ── Helpers ──
function headers(sessionCookie: string, gatewayUrl: string) {
return { Cookie: sessionCookie, Origin: gatewayUrl };
}
function jsonHeaders(sessionCookie: string, gatewayUrl: string) {
return { ...headers(sessionCookie, gatewayUrl), 'Content-Type': 'application/json' };
}
async function handleResponse<T>(res: Response, errorPrefix: string): Promise<T> {
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`${errorPrefix} (${res.status}): ${body}`);
}
return (await res.json()) as T;
}
// ── Conversation types ──
export interface ConversationInfo {
id: string;
title: string | null;
archived: boolean;
createdAt: string;
updatedAt: string;
}
// ── Conversation endpoints ──
export async function createConversation(
gatewayUrl: string,
sessionCookie: string,
data: { title?: string; projectId?: string } = {},
): Promise<ConversationInfo> {
const res = await fetch(`${gatewayUrl}/api/conversations`, {
method: 'POST',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<ConversationInfo>(res, 'Failed to create conversation');
}
// ── Provider / Model endpoints ──
export async function fetchAvailableModels(
gatewayUrl: string,
sessionCookie?: string,
): Promise<ModelInfo[]> {
try {
const res = await fetch(`${gatewayUrl}/api/providers/models`, {
headers: {
...(sessionCookie ? { Cookie: sessionCookie } : {}),
Origin: gatewayUrl,
},
});
if (!res.ok) return [];
const data = (await res.json()) as ModelInfo[];
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
export async function fetchProviders(
gatewayUrl: string,
sessionCookie?: string,
): Promise<ProviderInfo[]> {
try {
const res = await fetch(`${gatewayUrl}/api/providers`, {
headers: {
...(sessionCookie ? { Cookie: sessionCookie } : {}),
Origin: gatewayUrl,
},
});
if (!res.ok) return [];
const data = (await res.json()) as ProviderInfo[];
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
// ── Session endpoints ──
export async function fetchSessions(
gatewayUrl: string,
sessionCookie: string,
): Promise<SessionListResult> {
const res = await fetch(`${gatewayUrl}/api/sessions`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<SessionListResult>(res, 'Failed to list sessions');
}
export async function deleteSession(
gatewayUrl: string,
sessionCookie: string,
sessionId: string,
): Promise<void> {
const res = await fetch(`${gatewayUrl}/api/sessions/${encodeURIComponent(sessionId)}`, {
method: 'DELETE',
headers: headers(sessionCookie, gatewayUrl),
});
if (!res.ok && res.status !== 204) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to destroy session (${res.status}): ${body}`);
}
}
// ── Agent Config endpoints ──
export async function fetchAgentConfigs(
gatewayUrl: string,
sessionCookie: string,
): Promise<AgentConfigInfo[]> {
const res = await fetch(`${gatewayUrl}/api/agents`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<AgentConfigInfo[]>(res, 'Failed to list agents');
}
export async function fetchAgentConfig(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<AgentConfigInfo> {
const res = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(id)}`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<AgentConfigInfo>(res, 'Failed to get agent');
}
export async function createAgentConfig(
gatewayUrl: string,
sessionCookie: string,
data: {
name: string;
provider: string;
model: string;
projectId?: string;
systemPrompt?: string;
allowedTools?: string[];
skills?: string[];
config?: Record<string, unknown>;
},
): Promise<AgentConfigInfo> {
const res = await fetch(`${gatewayUrl}/api/agents`, {
method: 'POST',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<AgentConfigInfo>(res, 'Failed to create agent');
}
export async function updateAgentConfig(
gatewayUrl: string,
sessionCookie: string,
id: string,
data: Record<string, unknown>,
): Promise<AgentConfigInfo> {
const res = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<AgentConfigInfo>(res, 'Failed to update agent');
}
export async function deleteAgentConfig(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<void> {
const res = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: headers(sessionCookie, gatewayUrl),
});
if (!res.ok && res.status !== 204) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to delete agent (${res.status}): ${body}`);
}
}
// ── Project endpoints ──
export async function fetchProjects(
gatewayUrl: string,
sessionCookie: string,
): Promise<ProjectInfo[]> {
const res = await fetch(`${gatewayUrl}/api/projects`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<ProjectInfo[]>(res, 'Failed to list projects');
}
// ── Mission endpoints ──
export async function fetchMissions(
gatewayUrl: string,
sessionCookie: string,
): Promise<MissionInfo[]> {
const res = await fetch(`${gatewayUrl}/api/missions`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<MissionInfo[]>(res, 'Failed to list missions');
}
export async function fetchMission(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<MissionInfo> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(id)}`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<MissionInfo>(res, 'Failed to get mission');
}
export async function createMission(
gatewayUrl: string,
sessionCookie: string,
data: {
name: string;
description?: string;
projectId?: string;
status?: string;
phase?: string;
milestones?: Record<string, unknown>[];
config?: Record<string, unknown>;
},
): Promise<MissionInfo> {
const res = await fetch(`${gatewayUrl}/api/missions`, {
method: 'POST',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<MissionInfo>(res, 'Failed to create mission');
}
export async function updateMission(
gatewayUrl: string,
sessionCookie: string,
id: string,
data: Record<string, unknown>,
): Promise<MissionInfo> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<MissionInfo>(res, 'Failed to update mission');
}
export async function deleteMission(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<void> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: headers(sessionCookie, gatewayUrl),
});
if (!res.ok && res.status !== 204) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to delete mission (${res.status}): ${body}`);
}
}
// ── Authenticated interaction runtime endpoints ──
export interface InteractionRequest {
agentName: string;
correlationId?: string;
}
function interactionHeaders(sessionCookie: string, gatewayUrl: string, correlationId?: string) {
return {
...jsonHeaders(sessionCookie, gatewayUrl),
...(correlationId ? { 'X-Correlation-Id': correlationId } : {}),
};
}
function interactionPath(agentName: string, suffix: string): string {
return `/api/interaction/${encodeURIComponent(agentName)}${suffix}`;
}
export async function fetchInteractionSessions(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & { providerId: string },
): Promise<unknown[]> {
const params = new URLSearchParams({ provider: request.providerId });
const res = await fetch(
`${gatewayUrl}${interactionPath(request.agentName, `/sessions?${params}`)}`,
{
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
},
);
return handleResponse<unknown[]>(res, 'Failed to list interaction sessions');
}
export async function fetchInteractionTree(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & { providerId: string },
): Promise<unknown[]> {
const params = new URLSearchParams({ provider: request.providerId });
const res = await fetch(`${gatewayUrl}${interactionPath(request.agentName, `/tree?${params}`)}`, {
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
});
return handleResponse<unknown[]>(res, 'Failed to get interaction session tree');
}
export async function enrollInteractionSession(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & {
sessionId: string;
providerId: string;
runtimeSessionId: string;
},
): Promise<{ status: string; sessionId: string }> {
const res = await fetch(
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/enroll`)}`,
{
method: 'POST',
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
body: JSON.stringify({
providerId: request.providerId,
runtimeSessionId: request.runtimeSessionId,
}),
},
);
return handleResponse<{ status: string; sessionId: string }>(
res,
'Failed to enroll interaction session',
);
}
export async function attachInteractionSession(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & { sessionId: string; mode?: 'read' | 'control' },
): Promise<unknown> {
const res = await fetch(
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/attach`)}`,
{
method: 'POST',
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
body: JSON.stringify({ mode: request.mode ?? 'read' }),
},
);
return handleResponse<unknown>(res, 'Failed to attach interaction session');
}
export async function* streamInteractionSession(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & { sessionId: string; cursor?: string },
): AsyncIterable<unknown> {
const params = request.cursor?.trim()
? `?${new URLSearchParams({ cursor: request.cursor.trim() }).toString()}`
: '';
const res = await fetch(
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/stream${params}`)}`,
{ headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId) },
);
if (!res.ok || !res.body) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to stream interaction session (${res.status}): ${body}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let pending = '';
try {
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
pending += decoder.decode(chunk.value, { stream: true });
for (;;) {
const separator = pending.indexOf('\n\n');
if (separator < 0) break;
const frame = pending.slice(0, separator);
pending = pending.slice(separator + 2);
const data = frame
.split('\n')
.find((line: string): boolean => line.startsWith('data:'))
?.slice('data:'.length)
.trim();
if (data) yield JSON.parse(data) as unknown;
}
}
} finally {
reader.releaseLock();
}
}
export async function sendInteractionMessage(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & { sessionId: string; content: string; idempotencyKey: string },
): Promise<{ status: string; sessionId: string }> {
const res = await fetch(
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/send`)}`,
{
method: 'POST',
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
body: JSON.stringify({ content: request.content, idempotencyKey: request.idempotencyKey }),
},
);
return handleResponse<{ status: string; sessionId: string }>(
res,
'Failed to send interaction message',
);
}
export async function stopInteractionSession(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & { sessionId: string; approvalRef: string },
): Promise<{ status: string; sessionId: string }> {
const res = await fetch(
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/stop`)}`,
{
method: 'POST',
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
body: JSON.stringify({ approvalRef: request.approvalRef }),
},
);
return handleResponse<{ status: string; sessionId: string }>(
res,
'Failed to stop interaction session',
);
}
export async function recoverInteractionSession(
gatewayUrl: string,
sessionCookie: string,
request: InteractionRequest & { sessionId: string },
): Promise<{ status: string; sessionId: string }> {
const res = await fetch(
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/recover`)}`,
{
method: 'POST',
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
},
);
return handleResponse<{ status: string; sessionId: string }>(
res,
'Failed to recover interaction session',
);
}
export async function fetchInteractionStatus(
gatewayUrl: string,
sessionCookie: string,
): Promise<unknown> {
const res = await fetch(`${gatewayUrl}/api/providers/status`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<unknown>(res, 'Failed to get effective interaction policy');
}
export async function fetchInteractionHealth(gatewayUrl: string): Promise<unknown> {
const res = await fetch(`${gatewayUrl}/health/ready`);
return handleResponse<unknown>(res, 'Failed to get interaction readiness');
}
// ── Conversation Message types ──
export interface ConversationMessage {
id: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
createdAt: string;
}
// ── Conversation Message endpoints ──
export async function fetchConversationMessages(
gatewayUrl: string,
sessionCookie: string,
conversationId: string,
): Promise<ConversationMessage[]> {
const res = await fetch(
`${gatewayUrl}/api/conversations/${encodeURIComponent(conversationId)}/messages`,
{
headers: headers(sessionCookie, gatewayUrl),
},
);
return handleResponse<ConversationMessage[]>(res, 'Failed to fetch conversation messages');
}
// ── Mission Task endpoints ──
export async function fetchMissionTasks(
gatewayUrl: string,
sessionCookie: string,
missionId: string,
): Promise<MissionTaskInfo[]> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(missionId)}/tasks`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<MissionTaskInfo[]>(res, 'Failed to list mission tasks');
}
export async function createMissionTask(
gatewayUrl: string,
sessionCookie: string,
missionId: string,
data: {
description?: string;
status?: string;
notes?: string;
pr?: string;
taskId?: string;
},
): Promise<MissionTaskInfo> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(missionId)}/tasks`, {
method: 'POST',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<MissionTaskInfo>(res, 'Failed to create mission task');
}
export async function updateMissionTask(
gatewayUrl: string,
sessionCookie: string,
missionId: string,
taskId: string,
data: Record<string, unknown>,
): Promise<MissionTaskInfo> {
const res = await fetch(
`${gatewayUrl}/api/missions/${encodeURIComponent(missionId)}/tasks/${encodeURIComponent(taskId)}`,
{
method: 'PATCH',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
},
);
return handleResponse<MissionTaskInfo>(res, 'Failed to update mission task');
}