644 lines
20 KiB
TypeScript
644 lines
20 KiB
TypeScript
import { fetchJson, fetchJsonWithRetry, type ApiRequestOptions } from "../client";
|
|
import { normalizeActivityEntry, type RawActivityEntry } from "./office-activity-normalize";
|
|
import type {
|
|
AgentProfile,
|
|
Project,
|
|
CostSummary,
|
|
Approval,
|
|
InboxItem,
|
|
OfficeMeta,
|
|
ProviderUsage,
|
|
LiveRun,
|
|
} from "@/lib/state/slices/office/types";
|
|
import type { CLIFlag, AgentRole, AgentStatus } from "@/lib/types/agent-profile";
|
|
import { agentProfileId, workspaceId } from "@/lib/types/ids";
|
|
import { normalizeProject } from "./office-project-normalize";
|
|
|
|
// Re-export extended API so existing imports continue to work.
|
|
export {
|
|
listChannels,
|
|
setupChannel,
|
|
deleteChannel,
|
|
exportConfig,
|
|
exportConfigManifest,
|
|
exportSelectedConfigZip,
|
|
exportConfigZipUrl,
|
|
previewImport,
|
|
applyImport,
|
|
getIncomingDiff,
|
|
getOutgoingDiff,
|
|
applyIncomingSync,
|
|
applyOutgoingSync,
|
|
listTasks,
|
|
getTask,
|
|
listComments,
|
|
createComment,
|
|
searchTasks,
|
|
updateTask,
|
|
addTaskBlocker,
|
|
removeTaskBlocker,
|
|
listTaskReviewers,
|
|
addTaskReviewer,
|
|
removeTaskReviewer,
|
|
listTaskApprovers,
|
|
addTaskApprover,
|
|
removeTaskApprover,
|
|
approveTask,
|
|
requestTaskChanges,
|
|
listTaskDecisions,
|
|
listInstructions,
|
|
getInstruction,
|
|
upsertInstruction,
|
|
deleteInstruction,
|
|
getOnboardingState,
|
|
completeOnboarding,
|
|
importFromFS,
|
|
getDashboard,
|
|
listRuns,
|
|
updateWorkspaceSettings,
|
|
getWorkspaceSettings,
|
|
getGitStatus,
|
|
gitClone,
|
|
gitPull,
|
|
gitPush,
|
|
} from "./office-extended-api";
|
|
export { listBudgets, createBudget, updateBudget, deleteBudget } from "./office-budget-api";
|
|
export {
|
|
listRoutines,
|
|
createRoutine,
|
|
getRoutine,
|
|
updateRoutine,
|
|
deleteRoutine,
|
|
runRoutine,
|
|
listRoutineTriggers,
|
|
createRoutineTrigger,
|
|
deleteRoutineTrigger,
|
|
listRoutineRuns,
|
|
listAllRoutineRuns,
|
|
} from "./office-routine-api";
|
|
export type {
|
|
ImportDiff,
|
|
ImportPreview,
|
|
ParseError,
|
|
SyncDiff,
|
|
TaskCommentResponse,
|
|
TimelineEvent,
|
|
UpdateTaskPayload,
|
|
TaskParticipant,
|
|
OnboardingStateData,
|
|
OnboardingCompletePayload,
|
|
OnboardingCompleteResult,
|
|
OnboardingFSWorkspace,
|
|
ImportFromFSResult,
|
|
GitStatusData,
|
|
WorkspaceSettingsData,
|
|
TaskDecisionDTO,
|
|
} from "./office-extended-api";
|
|
|
|
const BASE = "/api/v1/office";
|
|
|
|
export const getMeta = (options?: ApiRequestOptions) =>
|
|
fetchJsonWithRetry<OfficeMeta>(`${BASE}/meta`, options);
|
|
|
|
type AgentResponse = { agent: unknown };
|
|
type AgentListResponse = { agents: unknown[] };
|
|
|
|
export type WorkspaceDeletionSummary = {
|
|
workspace_name: string;
|
|
tasks: number;
|
|
agents: number;
|
|
skills: number;
|
|
config_path: string;
|
|
};
|
|
|
|
function parseJSONField<T>(value: unknown, fallback: T): T {
|
|
if (typeof value !== "string") return (value as T) ?? fallback;
|
|
if (!value) return fallback;
|
|
try {
|
|
return JSON.parse(value) as T;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function rawField(agent: Record<string, unknown>, camelKey: string, snakeKey: string) {
|
|
return agent[camelKey] ?? agent[snakeKey];
|
|
}
|
|
|
|
function stringField(agent: Record<string, unknown>, camelKey: string, snakeKey: string) {
|
|
const value = rawField(agent, camelKey, snakeKey);
|
|
return typeof value === "string" ? value : "";
|
|
}
|
|
|
|
function numberField(
|
|
agent: Record<string, unknown>,
|
|
camelKey: string,
|
|
snakeKey: string,
|
|
fallback: number,
|
|
) {
|
|
const value = rawField(agent, camelKey, snakeKey);
|
|
return typeof value === "number" ? value : fallback;
|
|
}
|
|
|
|
function boolField(agent: Record<string, unknown>, camelKey: string, snakeKey: string): boolean {
|
|
const value = rawField(agent, camelKey, snakeKey);
|
|
return typeof value === "boolean" ? value : false;
|
|
}
|
|
|
|
function normalizeAgent(raw: unknown): AgentProfile {
|
|
const agent = raw as Record<string, unknown>;
|
|
const id = agentProfileId(agent.id as string);
|
|
const rawAgentProfileId = stringField(agent, "agentProfileId", "agent_profile_id");
|
|
return {
|
|
id,
|
|
workspaceId: workspaceId(stringField(agent, "workspaceId", "workspace_id")),
|
|
name: agent.name as string,
|
|
agentProfileId: rawAgentProfileId ? agentProfileId(rawAgentProfileId) : id,
|
|
role: agent.role as AgentRole,
|
|
icon: agent.icon as string | undefined,
|
|
status: agent.status as AgentStatus,
|
|
reportsTo: stringField(agent, "reportsTo", "reports_to"),
|
|
permissions: parseJSONField(agent.permissions, {}),
|
|
budgetMonthlyCents: numberField(agent, "budgetMonthlyCents", "budget_monthly_cents", 0),
|
|
maxConcurrentSessions: numberField(
|
|
agent,
|
|
"maxConcurrentSessions",
|
|
"max_concurrent_sessions",
|
|
1,
|
|
),
|
|
desiredSkills: parseJSONField<string[]>(rawField(agent, "desiredSkills", "desired_skills"), []),
|
|
executorPreference: parseJSONField<Record<string, unknown>>(
|
|
rawField(agent, "executorPreference", "executor_preference"),
|
|
{},
|
|
),
|
|
pauseReason: stringField(agent, "pauseReason", "pause_reason"),
|
|
billingType: rawField(agent, "billingType", "billing_type") as AgentProfile["billingType"],
|
|
utilization: (agent.utilization ?? null) as AgentProfile["utilization"],
|
|
skillIds: parseJSONField<string[]>(rawField(agent, "skillIds", "skill_ids"), []),
|
|
// CLI subprocess fields. Office-served rows may omit these when the
|
|
// office agent is not yet wired to a CLI client; default to safe
|
|
// empty values so the canonical type stays satisfied.
|
|
agentId: stringField(agent, "agentId", "agent_id"),
|
|
agentDisplayName: stringField(agent, "agentDisplayName", "agent_display_name"),
|
|
model: stringField(agent, "model", "model"),
|
|
mode: (rawField(agent, "mode", "mode") as string | undefined) ?? undefined,
|
|
allowIndexing: boolField(agent, "allowIndexing", "allow_indexing"),
|
|
autoApprove: boolField(agent, "autoApprove", "auto_approve"),
|
|
cliFlags: parseJSONField<CLIFlag[]>(rawField(agent, "cliFlags", "cli_flags"), []),
|
|
cliPassthrough: boolField(agent, "cliPassthrough", "cli_passthrough"),
|
|
userModified: rawField(agent, "userModified", "user_modified") as boolean | undefined,
|
|
createdAt: stringField(agent, "createdAt", "created_at"),
|
|
updatedAt: stringField(agent, "updatedAt", "updated_at"),
|
|
};
|
|
}
|
|
|
|
function stringifyJSONField(value: unknown): string | undefined {
|
|
if (value === undefined) return undefined;
|
|
if (typeof value === "string") return value;
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function agentPayload(data: Partial<AgentProfile>): Record<string, unknown> {
|
|
const payload: Record<string, unknown> = {
|
|
name: data.name,
|
|
agent_profile_id: data.agentProfileId,
|
|
role: data.role,
|
|
icon: data.icon,
|
|
reports_to: data.reportsTo,
|
|
permissions: stringifyJSONField(data.permissions),
|
|
budget_monthly_cents: data.budgetMonthlyCents,
|
|
max_concurrent_sessions: data.maxConcurrentSessions,
|
|
desired_skills: stringifyJSONField(data.desiredSkills),
|
|
executor_preference: stringifyJSONField(data.executorPreference),
|
|
skill_ids: stringifyJSONField(data.skillIds),
|
|
};
|
|
if (data.autoApprove !== undefined) {
|
|
payload.auto_approve = data.autoApprove;
|
|
}
|
|
if (data.allowIndexing !== undefined) {
|
|
payload.allow_indexing = data.allowIndexing;
|
|
}
|
|
if (data.cliPassthrough !== undefined) {
|
|
payload.cli_passthrough = data.cliPassthrough;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
// --- Agent Instances ---
|
|
|
|
export function listAgentProfiles(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJsonWithRetry<AgentListResponse>(
|
|
`${BASE}/workspaces/${workspaceId}/agents`,
|
|
options,
|
|
).then((res) => ({ agents: (res.agents ?? []).map((agent) => normalizeAgent(agent)) }));
|
|
}
|
|
|
|
export function createAgentProfile(
|
|
workspaceId: string,
|
|
data: Partial<AgentProfile>,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<AgentResponse>(`${BASE}/workspaces/${workspaceId}/agents`, {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(agentPayload(data)), ...options?.init },
|
|
}).then((res) => normalizeAgent(res.agent));
|
|
}
|
|
|
|
export const getAgentProfile = (id: string, options?: ApiRequestOptions) =>
|
|
fetchJson<AgentResponse>(`${BASE}/agents/${id}`, options).then((res) =>
|
|
normalizeAgent(res.agent),
|
|
);
|
|
|
|
export const getAgentUtilization = (agentId: string, options?: ApiRequestOptions) =>
|
|
fetchJson<{ utilization: ProviderUsage | null }>(
|
|
`${BASE}/agents/${agentId}/utilization`,
|
|
options,
|
|
);
|
|
|
|
export function updateAgentProfile(
|
|
id: string,
|
|
data: Partial<AgentProfile>,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<AgentResponse>(`${BASE}/agents/${id}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(agentPayload(data)), ...options?.init },
|
|
}).then((res) => normalizeAgent(res.agent));
|
|
}
|
|
|
|
// Dedicated status transition endpoint: validates the transition table and
|
|
// clears pause_reason as a side effect. The general agent update endpoint
|
|
// above accepts a status field too but writes it unchecked, so callers that
|
|
// need transition validation (e.g. agent recovery) must use this instead.
|
|
// `expectedStatus` selects the guarded recovery form, which refuses to
|
|
// overwrite a status that is no longer paused or stopped.
|
|
export function updateAgentStatus(
|
|
id: string,
|
|
status: AgentStatus,
|
|
options?: ApiRequestOptions & { expectedStatus?: AgentStatus },
|
|
) {
|
|
const { expectedStatus, ...requestOptions } = options ?? {};
|
|
const body: { status: AgentStatus; expected_status?: AgentStatus } = { status };
|
|
if (expectedStatus) body.expected_status = expectedStatus;
|
|
return fetchJson<AgentResponse>(`${BASE}/agents/${id}/status`, {
|
|
...requestOptions,
|
|
init: { method: "PATCH", body: JSON.stringify(body), ...requestOptions.init },
|
|
}).then((res) => normalizeAgent(res.agent));
|
|
}
|
|
|
|
export function deleteAgentProfile(id: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`${BASE}/agents/${id}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...options?.init },
|
|
});
|
|
}
|
|
|
|
export function getWorkspaceDeletionSummary(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<WorkspaceDeletionSummary>(
|
|
`${BASE}/workspaces/${workspaceId}/deletion-summary`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function deleteWorkspace(
|
|
workspaceId: string,
|
|
confirmName: string,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<void>(`${BASE}/workspaces/${workspaceId}`, {
|
|
...options,
|
|
init: {
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ confirm_name: confirmName }),
|
|
...options?.init,
|
|
},
|
|
});
|
|
}
|
|
|
|
// --- Skills ---
|
|
// Moved to office-skills-api.ts so this module stays under the
|
|
// 600-line cap. The re-exports below keep every existing
|
|
// `from "@/lib/api/domains/office-api"` import working.
|
|
export {
|
|
listSkills,
|
|
createSkill,
|
|
getSkill,
|
|
updateSkill,
|
|
deleteSkill,
|
|
importSkill,
|
|
getSkillFile,
|
|
} from "./office-skills-api";
|
|
|
|
// --- Projects ---
|
|
|
|
export function listProjects(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJsonWithRetry<{ projects: unknown[] }>(
|
|
`${BASE}/workspaces/${workspaceId}/projects`,
|
|
options,
|
|
).then((res) => ({ projects: (res.projects ?? []).map((project) => normalizeProject(project)) }));
|
|
}
|
|
|
|
// Backend single-project endpoints wrap the row as `{ project: ... }`.
|
|
// The list endpoint wraps as `{ projects: [...] }`. These helpers
|
|
// unwrap once at the API boundary so callers always see a `Project`.
|
|
|
|
export async function createProject(
|
|
workspaceId: string,
|
|
data: Partial<Project>,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
const res = await fetchJson<{ project: unknown }>(`${BASE}/workspaces/${workspaceId}/projects`, {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(data), ...options?.init },
|
|
});
|
|
return normalizeProject(res.project);
|
|
}
|
|
|
|
export async function getProject(id: string, options?: ApiRequestOptions) {
|
|
const res = await fetchJson<{ project: unknown }>(`${BASE}/projects/${id}`, options);
|
|
return normalizeProject(res.project);
|
|
}
|
|
|
|
export async function updateProject(
|
|
id: string,
|
|
data: Partial<Project>,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
const res = await fetchJson<{ project: unknown }>(`${BASE}/projects/${id}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(data), ...options?.init },
|
|
});
|
|
return normalizeProject(res.project);
|
|
}
|
|
|
|
export function deleteProject(id: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`${BASE}/projects/${id}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...options?.init },
|
|
});
|
|
}
|
|
|
|
// --- Costs ---
|
|
|
|
export const getCosts = (workspaceId: string, options?: ApiRequestOptions) =>
|
|
fetchJson<CostSummary>(`${BASE}/workspaces/${workspaceId}/costs`, options);
|
|
|
|
export function getCostSummary(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ total_subcents: number }>(
|
|
`${BASE}/workspaces/${workspaceId}/costs/summary`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function getCostsByAgent(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<CostSummary["byAgent"]>(
|
|
`${BASE}/workspaces/${workspaceId}/costs/by-agent`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function getCostsByProject(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<CostSummary["byProject"]>(
|
|
`${BASE}/workspaces/${workspaceId}/costs/by-project`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function getCostsByModel(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<CostSummary["byModel"]>(
|
|
`${BASE}/workspaces/${workspaceId}/costs/by-model`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Composed costs view: total + by-agent / by-project / by-model / by-provider
|
|
* in a single round-trip (Stream D of office optimization). The five
|
|
* aggregations run concurrently on the backend with a snapshot-consistent
|
|
* read, so the page reflects one moment in time.
|
|
*/
|
|
export function getCostsBreakdown(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{
|
|
total_subcents: number;
|
|
by_agent: CostBreakdownItemRaw[];
|
|
by_project: CostBreakdownItemRaw[];
|
|
by_model: CostBreakdownItemRaw[];
|
|
by_provider: CostBreakdownItemRaw[];
|
|
}>(`${BASE}/workspaces/${workspaceId}/costs/breakdown`, options);
|
|
}
|
|
|
|
type CostBreakdownItemRaw = {
|
|
group_key: string;
|
|
group_label?: string;
|
|
count: number;
|
|
total_subcents: number;
|
|
};
|
|
|
|
// --- Built-in default spend ceiling (AC-OFFICE-BUDGET-003.5) ---
|
|
|
|
// DefaultCeilingRaw is the wire shape of GET/PUT .../budgets/default
|
|
// (internal/office/costs/dto.go's DefaultCeilingResponse /
|
|
// SetDefaultCeilingRequest, both `{"limit_subcents": ...}`). Named "Raw"
|
|
// and read as snake_case directly by the caller, matching this file's
|
|
// existing CostBreakdownItemRaw convention, rather than assuming a
|
|
// camelCase field the backend does not send.
|
|
type DefaultCeilingRaw = { limit_subcents: number };
|
|
|
|
export function getDefaultCeiling(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<DefaultCeilingRaw>(`${BASE}/workspaces/${workspaceId}/budgets/default`, options);
|
|
}
|
|
|
|
export function setDefaultCeiling(
|
|
workspaceId: string,
|
|
limitSubcents: number,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<DefaultCeilingRaw>(`${BASE}/workspaces/${workspaceId}/budgets/default`, {
|
|
...options,
|
|
init: {
|
|
method: "PUT",
|
|
body: JSON.stringify({ limit_subcents: limitSubcents }),
|
|
...options?.init,
|
|
},
|
|
});
|
|
}
|
|
|
|
// --- Approvals ---
|
|
|
|
export function listApprovals(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ approvals: Approval[] }>(
|
|
`${BASE}/workspaces/${workspaceId}/approvals`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function decideApproval(
|
|
id: string,
|
|
decision: { status: "approved" | "rejected"; note?: string },
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<Approval>(`${BASE}/approvals/${id}/decide`, {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(decision), ...options?.init },
|
|
});
|
|
}
|
|
|
|
// --- Activity ---
|
|
|
|
export function listActivity(
|
|
workspaceId: string,
|
|
filterType?: string,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
const query = filterType && filterType !== "all" ? `?type=${filterType}` : "";
|
|
return fetchJson<{ activity: RawActivityEntry[] }>(
|
|
`${BASE}/workspaces/${workspaceId}/activity${query}`,
|
|
options,
|
|
).then((res) => ({ activity: (res.activity ?? []).map(normalizeActivityEntry) }));
|
|
}
|
|
|
|
export function listActivityForTarget(
|
|
workspaceId: string,
|
|
targetId: string,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
const params = new URLSearchParams({ target_id: targetId });
|
|
return fetchJson<{ activity: RawActivityEntry[] }>(
|
|
`${BASE}/workspaces/${workspaceId}/activity?${params.toString()}`,
|
|
options,
|
|
).then((res) => ({ activity: (res.activity ?? []).map(normalizeActivityEntry) }));
|
|
}
|
|
|
|
// --- Inbox ---
|
|
|
|
export function getInbox(workspaceId: string, options?: ApiRequestOptions) {
|
|
// Returns items + total_count in a single call (Stream F of office
|
|
// optimization). The sidebar count subscribes to the store value the
|
|
// inbox page sets; no separate count fetch is required.
|
|
return fetchJsonWithRetry<{ items: InboxItem[]; total_count: number }>(
|
|
`${BASE}/workspaces/${workspaceId}/inbox`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use getInbox instead — it now returns total_count alongside
|
|
* items, removing the need for a second round-trip.
|
|
*/
|
|
export function getInboxCount(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ count: number }>(
|
|
`${BASE}/workspaces/${workspaceId}/inbox?count=true`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
// --- Agent Memory ---
|
|
|
|
export function getMemory(agentId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{
|
|
memory: Array<{
|
|
id: string;
|
|
layer: string;
|
|
key: string;
|
|
content: string;
|
|
metadata: string;
|
|
updated_at: string;
|
|
}>;
|
|
}>(`${BASE}/agents/${agentId}/memory`, options);
|
|
}
|
|
|
|
export function putMemory(
|
|
agentId: string,
|
|
data: { layer: string; key: string; content: string },
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<void>(`${BASE}/agents/${agentId}/memory`, {
|
|
...options,
|
|
init: { method: "PUT", body: JSON.stringify({ entries: [data] }), ...options?.init },
|
|
});
|
|
}
|
|
|
|
export function deleteMemory(agentId: string, entryId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`${BASE}/agents/${agentId}/memory/${entryId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...options?.init },
|
|
});
|
|
}
|
|
|
|
export function deleteAllMemory(agentId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`${BASE}/agents/${agentId}/memory/all`, {
|
|
...options,
|
|
init: { method: "DELETE", ...options?.init },
|
|
});
|
|
}
|
|
|
|
export function exportMemory(agentId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{
|
|
memory: Array<{
|
|
id: string;
|
|
layer: string;
|
|
key: string;
|
|
content: string;
|
|
metadata: string;
|
|
updated_at: string;
|
|
}>;
|
|
}>(`${BASE}/agents/${agentId}/memory/export`, options);
|
|
}
|
|
|
|
export function getMemorySummary(agentId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ count: number }>(`${BASE}/agents/${agentId}/memory/summary`, options);
|
|
}
|
|
|
|
// --- Live Runs ---
|
|
|
|
export function getLiveRuns(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ runs: LiveRun[] }>(`${BASE}/workspaces/${workspaceId}/live-runs`, options);
|
|
}
|
|
|
|
// --- Agent summaries ---
|
|
//
|
|
// Per-agent dashboard cards. One entry per workspace agent, with up to five
|
|
// most-recent sessions enriched with task identifier/title and command count.
|
|
// Backend: GET /api/v1/office/workspaces/:wsId/agent-summaries.
|
|
|
|
export type SessionSummary = {
|
|
session_id: string;
|
|
task_id: string;
|
|
task_identifier: string;
|
|
task_title: string;
|
|
state: string;
|
|
started_at: string;
|
|
completed_at?: string | null;
|
|
/**
|
|
* Backend-computed elapsed seconds. Stable across refetches: for
|
|
* non-RUNNING sessions it uses completed_at (or updated_at for office
|
|
* IDLE rows that never complete) so the value doesn't drift.
|
|
*/
|
|
duration_seconds: number;
|
|
command_count: number;
|
|
};
|
|
|
|
export type AgentSummary = {
|
|
agent_id: string;
|
|
agent_name: string;
|
|
agent_role: string;
|
|
status: "live" | "finished" | "never_run";
|
|
live_session?: SessionSummary | null;
|
|
last_session?: SessionSummary | null;
|
|
recent_sessions: SessionSummary[];
|
|
/** Most recent run status — "ok" | "failed". */
|
|
last_run_status?: "ok" | "failed";
|
|
/** Set when the agent is currently auto-paused after threshold. */
|
|
pause_reason?: string;
|
|
/** Counter for the auto-pause UX. */
|
|
consecutive_failures?: number;
|
|
};
|
|
|
|
export function getAgentSummaries(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ agents: AgentSummary[] }>(
|
|
`${BASE}/workspaces/${workspaceId}/agent-summaries`,
|
|
options,
|
|
);
|
|
}
|