1034 lines
46 KiB
TypeScript
1034 lines
46 KiB
TypeScript
import { config } from "../config.js";
|
|
import type { CuratorServiceId, CuratorTask } from "../v1/curator/curator-types.js";
|
|
|
|
export type QueryValue = string | number | boolean | undefined | null;
|
|
export type QueryState = Record<string, QueryValue>;
|
|
|
|
export type ServiceId =
|
|
| "interview-service"
|
|
| "roleplay-service"
|
|
| "courses-service"
|
|
| "assessment-service"
|
|
| "matchmaking-service"
|
|
| "resume-service"
|
|
| "cover-letter-service"
|
|
| "qscore-service"
|
|
| "social-branding-service"
|
|
| "expert-service"
|
|
| "events-service";
|
|
|
|
export type ServiceCategory = "practice" | "learning" | "opportunity" | "document" | "measurement" | "profile";
|
|
|
|
export type ServiceEndpoint = {
|
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
path: string;
|
|
contract: string;
|
|
usage: string;
|
|
};
|
|
|
|
export type ServiceFrontendPage = {
|
|
path: string;
|
|
aliases?: string[];
|
|
queryParams: string[];
|
|
usage: string;
|
|
};
|
|
|
|
export type ServiceRecord = {
|
|
id: ServiceId;
|
|
label: string;
|
|
description: string;
|
|
category: ServiceCategory;
|
|
enabled: boolean;
|
|
featureId: string;
|
|
promptModulePath: string;
|
|
aliases?: string[];
|
|
backend: {
|
|
baseUrl?: string;
|
|
publicUrl?: string;
|
|
healthPath: string;
|
|
endpoints: Record<string, ServiceEndpoint>;
|
|
usage: string;
|
|
};
|
|
frontend: {
|
|
baseUrl: string;
|
|
pages: Record<string, ServiceFrontendPage>;
|
|
usage: string;
|
|
};
|
|
curator: {
|
|
defaultPage: string;
|
|
defaultActionLabel: string;
|
|
defaultQueryState?: QueryState;
|
|
actionLabels?: Record<string, string>;
|
|
toolName: string;
|
|
completionEvents: string[];
|
|
qscoreSignals?: string[];
|
|
usage: string;
|
|
};
|
|
eventContract: {
|
|
canonicalServiceId: string;
|
|
openEvents: string[];
|
|
startedEvents: string[];
|
|
configuredEvents: string[];
|
|
completionEvents: string[];
|
|
abandonmentEvents: string[];
|
|
qscoreSignalIds: readonly string[];
|
|
analyticsDimensions: readonly string[];
|
|
requiredCorrelationFields: readonly string[];
|
|
handoffParams: readonly string[];
|
|
actionKind: "active_user_action" | "passive_background_action" | "measurement_review";
|
|
};
|
|
usageDocs: string[];
|
|
};
|
|
|
|
type MissionServiceId = Extract<CuratorServiceId, "interview-service" | "roleplay-service" | "resume-service">;
|
|
|
|
type MissionRouteInput = {
|
|
serviceId: MissionServiceId;
|
|
missionInstanceId: string;
|
|
missionId: string;
|
|
stageId?: string;
|
|
goal?: string;
|
|
};
|
|
|
|
type CuratorRouteInput = {
|
|
serviceId?: CuratorServiceId;
|
|
missionInstanceId?: string;
|
|
missionId?: string;
|
|
stageId?: string;
|
|
taskId?: string;
|
|
taskTitle?: string;
|
|
taskSubtitle?: string;
|
|
taskType?: string;
|
|
targetRole?: string;
|
|
durationMinutes?: number;
|
|
difficulty?: string;
|
|
personaId?: string;
|
|
requestedMode?: string;
|
|
roleplayBrief?: string;
|
|
};
|
|
|
|
function endpoint(
|
|
method: ServiceEndpoint["method"],
|
|
path: string,
|
|
contract: string,
|
|
usage: string,
|
|
): ServiceEndpoint {
|
|
return { method, path, contract, usage };
|
|
}
|
|
|
|
function appendQuery(pathname: string, params: QueryState = {}) {
|
|
const search = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value === undefined || value === null || value === "") continue;
|
|
search.set(key, String(value));
|
|
}
|
|
const query = search.toString();
|
|
return query ? `${pathname}?${query}` : pathname;
|
|
}
|
|
|
|
function getString(value: unknown) {
|
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
}
|
|
|
|
function getSessionId(detail?: Record<string, unknown>) {
|
|
return getString(detail?.session_id ?? detail?.sessionId ?? detail?.id);
|
|
}
|
|
|
|
const frontendBaseUrl = config.workflowsDashboardUrl.replace(/\/$/, "");
|
|
|
|
const allowedServiceIds = new Set<ServiceId>([
|
|
"interview-service",
|
|
"matchmaking-service",
|
|
"roleplay-service",
|
|
"courses-service",
|
|
"resume-service",
|
|
"qscore-service",
|
|
"social-branding-service",
|
|
]);
|
|
|
|
const serviceRegistry: ServiceRecord[] = [
|
|
{
|
|
id: "interview-service",
|
|
label: "Interview",
|
|
description: "Configure, practice, review, and score mock interview sessions.",
|
|
category: "practice",
|
|
enabled: Boolean(config.interviewServiceUrl),
|
|
featureId: "mock-interview",
|
|
promptModulePath: "agents/interview.md",
|
|
backend: {
|
|
baseUrl: config.interviewServiceUrl,
|
|
publicUrl: config.interviewPublicUrl,
|
|
healthPath: "/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability before a handoff."),
|
|
pageState: endpoint("GET", "/api/v1/interviews/page-state?user_id=:userId", "Returns usage and personalization state.", "Hydrate interview landing/setup pages."),
|
|
configure: endpoint("POST", "/api/v1/configure", "Creates an interview plan from user, org, persona, type, duration, and context.", "Use for committed setup requests."),
|
|
preview: endpoint("POST", "/api/v1/configure/preview", "Creates a preview plan without starting live practice.", "Use for curator/dashboard previews."),
|
|
questions: endpoint("POST", "/api/v1/configure/questions", "Edits generated questions for a session.", "Use after preview edits."),
|
|
approve: endpoint("POST", "/api/v1/configure/approve", "Approves a generated session by session_id.", "Use when a user accepts a preview."),
|
|
assignments: endpoint("GET", "/api/v1/interviews/assignments", "Lists interview assignments by email/status/limit.", "Render assigned practice work."),
|
|
createAssignments: endpoint("POST", "/api/v1/interviews/assignments", "Creates interview assignments for assignee emails.", "Admin or organization handoffs."),
|
|
unassign: endpoint("POST", "/api/v1/interviews/assignments/unassign", "Removes interview assignments.", "Admin cleanup."),
|
|
resultsBulk: endpoint("POST", "/api/v1/interviews/results:bulk", "Fetches result summaries for multiple sessions.", "Dashboard history and summaries."),
|
|
review: endpoint("GET", "/api/v1/review/:sessionId", "Returns review/status for a session.", "Poll or open feedback."),
|
|
leaderboard: endpoint("GET", "/api/v1/leaderboard", "Returns interview leaderboard.", "Leaderboard widgets."),
|
|
artifact: endpoint("GET", "/api/v1/artifacts/:sessionId/:artifactType", "Returns session artifacts.", "Fetch transcript, report, or media artifacts."),
|
|
videoUploadUrl: endpoint("POST", "/api/v1/sessions/:sessionId/video/upload-url", "Returns signed upload instructions.", "Browser upload setup."),
|
|
markVideoUploaded: endpoint("POST", "/api/v1/sessions/:sessionId/video/uploaded", "Marks uploaded video as available.", "Complete upload flow."),
|
|
},
|
|
usage: "Backend callers should use the gateway /services/interview/* routes when user auth, mission correlation, and event recording are required.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
service: {
|
|
path: "/agents/interview",
|
|
aliases: ["discovery", "setup", "preview", "feedback", "session"],
|
|
queryParams: ["view", "phase", "sessionId", "role", "type", "persona", "duration", "difficulty", "media", "vip", "personalize", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "taskTitle", "taskSubtitle", "taskType"],
|
|
usage: "Single nuqs-driven interview surface for configuration, live practice, plan review, and feedback.",
|
|
},
|
|
},
|
|
usage: "All interview UI states use /agents/interview with typed query state; no child UI routes exist.",
|
|
},
|
|
curator: {
|
|
defaultPage: "service",
|
|
defaultActionLabel: "Open interview",
|
|
actionLabels: {
|
|
start: "Start mock",
|
|
review: "Review interview",
|
|
},
|
|
defaultQueryState: {
|
|
type: "behavioral",
|
|
persona: "payal",
|
|
duration: 5,
|
|
difficulty: "medium",
|
|
media: "video",
|
|
},
|
|
toolName: "prepare_interview_preview",
|
|
completionEvents: ["interview.session.completed", "interview.feedback.generated"],
|
|
qscoreSignals: ["communication.interview", "proof.story_bank", "readiness.practice"],
|
|
usage: "Include missionInstanceId, missionId, stageId, curatorTaskId, role, and media when building stateful handoffs.",
|
|
},
|
|
usageDocs: [
|
|
"Call buildServiceLink('interview-service', 'service', state) for curator handoffs.",
|
|
"Call getServiceEndpoint('interview-service', 'configure') for backend contract metadata.",
|
|
],
|
|
eventContract: {
|
|
canonicalServiceId: "interview",
|
|
openEvents: ["task.opened", "interview.preview.opened"],
|
|
startedEvents: ["service.started", "interview.session.started"],
|
|
configuredEvents: ["service.configured", "interview.session.configured"],
|
|
completionEvents: ["service.completed", "interview.session.completed", "interview.feedback.generated"],
|
|
abandonmentEvents: ["service.abandoned", "interview.session.abandoned"],
|
|
qscoreSignalIds: ["interview.communication_score", "interview.confidence_score", "readiness.practice"],
|
|
analyticsDimensions: ["interview_type", "target_role", "difficulty", "duration_minutes", "overall_score"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "active_user_action",
|
|
},
|
|
},
|
|
{
|
|
id: "roleplay-service",
|
|
label: "Roleplay",
|
|
description: "Practice negotiations, recruiter calls, stakeholder conversations, and workplace scenarios.",
|
|
category: "practice",
|
|
enabled: Boolean(config.roleplayServiceUrl),
|
|
featureId: "mock-roleplay",
|
|
promptModulePath: "agents/roleplay.md",
|
|
backend: {
|
|
baseUrl: config.roleplayServiceUrl,
|
|
publicUrl: config.roleplayPublicUrl,
|
|
healthPath: "/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability before a handoff."),
|
|
pageState: endpoint("GET", "/api/v1/roleplays/page-state?user_id=:userId", "Returns usage and personalization state.", "Hydrate roleplay landing/setup pages."),
|
|
configure: endpoint("POST", "/api/v1/roleplays/configure", "Creates a roleplay scenario from user, org, persona, duration, brief, metadata, qscore, and user_context.", "Use for committed scenario generation."),
|
|
preview: endpoint("POST", "/api/v1/roleplays/configure/preview", "Creates a roleplay preview.", "Use for curator/dashboard previews."),
|
|
questions: endpoint("POST", "/api/v1/roleplays/configure/questions", "Edits generated roleplay questions or beats.", "Use after preview edits."),
|
|
approve: endpoint("POST", "/api/v1/roleplays/configure/approve", "Approves a generated roleplay by session_id.", "Use when a user accepts a preview."),
|
|
assignments: endpoint("GET", "/api/v1/roleplays/assignments", "Lists roleplay assignments by email/status/limit.", "Render assigned drills."),
|
|
createAssignments: endpoint("POST", "/api/v1/roleplays/assignments", "Creates roleplay assignments.", "Admin or organization handoffs."),
|
|
unassign: endpoint("POST", "/api/v1/roleplays/assignments/unassign", "Removes roleplay assignments.", "Admin cleanup."),
|
|
resultsBulk: endpoint("POST", "/api/v1/roleplays/results:bulk", "Fetches result summaries for multiple sessions.", "Dashboard history and summaries."),
|
|
review: endpoint("GET", "/api/v1/roleplays/review/:sessionId", "Returns review/status for a roleplay session.", "Poll or open feedback."),
|
|
leaderboard: endpoint("GET", "/api/v1/roleplays/leaderboard", "Returns roleplay leaderboard.", "Leaderboard widgets."),
|
|
artifact: endpoint("GET", "/api/v1/artifacts/:sessionId/:artifactType", "Returns session artifacts.", "Fetch transcript, report, or media artifacts."),
|
|
videoUploadUrl: endpoint("POST", "/api/v1/sessions/:sessionId/video/upload-url", "Returns signed upload instructions.", "Browser upload setup."),
|
|
markVideoUploaded: endpoint("POST", "/api/v1/sessions/:sessionId/video/uploaded", "Marks uploaded video as available.", "Complete upload flow."),
|
|
},
|
|
usage: "Backend callers should use the gateway /services/roleplay/* routes when user auth, mission correlation, and event recording are required.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
service: {
|
|
path: "/agents/roleplay",
|
|
aliases: ["discovery", "setup", "builder", "feedback", "session"],
|
|
queryParams: ["view", "phase", "sessionId", "scenario", "scenarioName", "brief", "role", "persona", "duration", "difficulty", "mode", "vip", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "taskTitle", "taskSubtitle", "taskType"],
|
|
usage: "Single nuqs-driven roleplay surface for configuration, scenario review, live practice, and feedback.",
|
|
},
|
|
},
|
|
usage: "All roleplay UI states use /agents/roleplay with typed query state; no child UI routes exist.",
|
|
},
|
|
curator: {
|
|
defaultPage: "service",
|
|
defaultActionLabel: "Open roleplay",
|
|
actionLabels: {
|
|
start: "Run drill",
|
|
review: "Review roleplay",
|
|
},
|
|
defaultQueryState: {
|
|
role: "Professional",
|
|
persona: "emma",
|
|
duration: 5,
|
|
mode: "video",
|
|
},
|
|
toolName: "prepare_roleplay_preview",
|
|
completionEvents: ["roleplay.scenario.configured", "roleplay.scenario.completed", "roleplay.feedback.generated"],
|
|
qscoreSignals: ["communication.roleplay", "networking.conversation", "readiness.practice"],
|
|
usage: "Include role, brief, mission state, and curatorTaskId when building stateful handoffs.",
|
|
},
|
|
usageDocs: [
|
|
"Call buildServiceLink('roleplay-service', 'service', state) for curator handoffs.",
|
|
"Call getServiceEndpoint('roleplay-service', 'configure') for backend contract metadata.",
|
|
],
|
|
eventContract: {
|
|
canonicalServiceId: "roleplay",
|
|
openEvents: ["task.opened", "roleplay.preview.opened"],
|
|
startedEvents: ["service.started", "roleplay.scenario.started"],
|
|
configuredEvents: ["service.configured", "roleplay.scenario.configured"],
|
|
completionEvents: ["service.completed", "roleplay.scenario.completed", "roleplay.feedback.generated"],
|
|
abandonmentEvents: ["service.abandoned", "roleplay.scenario.abandoned"],
|
|
qscoreSignalIds: ["roleplay.negotiation_confidence", "communication.roleplay", "readiness.practice"],
|
|
analyticsDimensions: ["roleplay_type", "target_role", "difficulty", "duration_minutes", "overall_score"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "active_user_action",
|
|
},
|
|
},
|
|
{
|
|
id: "resume-service",
|
|
label: "Resume",
|
|
description: "Build, tailor, analyze, version, and preview resumes.",
|
|
category: "document",
|
|
enabled: Boolean(config.resumeServiceUrl),
|
|
featureId: "resume-building",
|
|
promptModulePath: "agents/resume.md",
|
|
backend: {
|
|
baseUrl: config.resumeServiceUrl,
|
|
publicUrl: config.resumePublicUrl,
|
|
healthPath: "/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability before a handoff."),
|
|
state: endpoint("GET", "/api/state/:clerkId", "Returns user resume-builder state.", "Hydrate profile and personalization context."),
|
|
templates: endpoint("GET", "/api/v1/templates", "Lists resume templates.", "Render template gallery."),
|
|
a2aTask: endpoint("POST", "/a2a/tasks", "Runs resume-builder agent actions for a user_id.", "Agent/curator orchestrated work."),
|
|
listResumes: endpoint("GET", "/api/v1/resumes?clerk_id=:clerkId", "Lists resumes for a Clerk user.", "Resume hub."),
|
|
createResume: endpoint("POST", "/api/v1/resumes", "Creates a resume.", "Resume creation."),
|
|
getResume: endpoint("GET", "/api/v1/resumes/:resumeId", "Reads a resume.", "Resume editor."),
|
|
updateResume: endpoint("PUT", "/api/v1/resumes/:resumeId", "Updates a resume.", "Resume editor saves."),
|
|
analyzeResume: endpoint("POST", "/api/v1/ai/analyze/:resumeId", "Runs AI analysis for a resume.", "Resume score and improvement plan."),
|
|
suggestions: endpoint("GET", "/api/v1/ai/suggestions/:resumeId", "Returns AI suggestions.", "Editor improvement rail."),
|
|
copilot: endpoint("POST", "/api/v1/ai/copilot", "Runs resume copilot.", "Inline editing assistant."),
|
|
versions: endpoint("GET", "/api/v1/resumes/:resumeId/versions", "Lists resume versions.", "Version history."),
|
|
preview: endpoint("GET", "/api/v1/export/resumes/:resumeId/preview", "Returns resume preview.", "PDF/preview surface."),
|
|
},
|
|
usage: "Use gateway /services/resume/* for browser-authenticated requests so Clerk bearer tokens are preserved.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
workspace: {
|
|
path: "/agents/resume",
|
|
queryParams: ["view", "phase", "tab", "section", "role", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "taskTitle", "taskSubtitle", "taskType"],
|
|
usage: "Resume hub. Use tab=resumes by default and section to deep-link editor panels.",
|
|
},
|
|
editor: {
|
|
path: "/agents/resume/:resumeId",
|
|
queryParams: ["section"],
|
|
usage: "Resume editor for a known resume.",
|
|
},
|
|
templates: {
|
|
path: "/agents/resume/templates",
|
|
queryParams: [],
|
|
usage: "Template gallery.",
|
|
},
|
|
session: {
|
|
path: "/agents/resume",
|
|
queryParams: ["goal", "role"],
|
|
usage: "Legacy service-session launcher used by service agent results.",
|
|
},
|
|
},
|
|
usage: "Curator links should open the workspace unless a concrete resumeId is known.",
|
|
},
|
|
curator: {
|
|
defaultPage: "workspace",
|
|
defaultActionLabel: "Open resume workspace",
|
|
actionLabels: {
|
|
start: "Open resume",
|
|
review: "Review resume",
|
|
},
|
|
defaultQueryState: {
|
|
tab: "resumes",
|
|
},
|
|
toolName: "prepare_resume_upload",
|
|
completionEvents: ["resume.analysis.completed", "resume.parsed", "resume.updated", "resume.exported"],
|
|
qscoreSignals: ["proof.resume", "readiness.ats", "profile.skills"],
|
|
usage: "Include mission state and optional section when linking into resume work.",
|
|
},
|
|
usageDocs: [
|
|
"Call buildServiceLink('resume-service', 'workspace', { tab: 'resumes' }) for curator handoffs.",
|
|
"Use the resume gateway proxy for browser calls that need Clerk auth.",
|
|
],
|
|
eventContract: {
|
|
canonicalServiceId: "resume",
|
|
openEvents: ["task.opened"],
|
|
startedEvents: ["service.started", "resume.uploaded"],
|
|
configuredEvents: ["service.configured", "resume.parsed"],
|
|
completionEvents: ["service.completed", "resume.analysis.completed", "resume.updated", "resume.exported"],
|
|
abandonmentEvents: ["service.abandoned"],
|
|
qscoreSignalIds: ["resume.ats_score", "resume.profile_completeness", "proof.resume"],
|
|
analyticsDimensions: ["resume_id", "version_id", "ats_score", "role_fit_score", "updated_sections"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "active_user_action",
|
|
},
|
|
},
|
|
{
|
|
id: "cover-letter-service",
|
|
label: "Cover Letter",
|
|
description: "Generate, tailor, analyze, version, and preview cover letters.",
|
|
category: "document",
|
|
enabled: Boolean(config.resumeServiceUrl),
|
|
featureId: "cover-letter",
|
|
promptModulePath: "agents/cover-letter.md",
|
|
aliases: ["coverletter-service"],
|
|
backend: {
|
|
baseUrl: config.resumeServiceUrl,
|
|
publicUrl: config.resumePublicUrl,
|
|
healthPath: "/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/health", "Readiness probe inherited from resume-builder.", "Check resume-builder availability."),
|
|
listCoverLetters: endpoint("GET", "/api/v1/cover-letters", "Lists cover letters.", "Cover-letter hub."),
|
|
createCoverLetter: endpoint("POST", "/api/v1/cover-letters", "Creates a cover letter.", "Manual creation."),
|
|
getCoverLetter: endpoint("GET", "/api/v1/cover-letters/:coverLetterId", "Reads a cover letter.", "Cover-letter editor."),
|
|
updateCoverLetter: endpoint("PUT", "/api/v1/cover-letters/:coverLetterId", "Updates a cover letter.", "Editor saves."),
|
|
generate: endpoint("POST", "/api/v1/cover-letters/generate", "Generates a tailored cover letter.", "Job application handoff."),
|
|
tailor: endpoint("POST", "/api/v1/cover-letters/:coverLetterId/tailor", "Tailors an existing cover letter.", "Application-specific rewrite."),
|
|
analyze: endpoint("POST", "/api/v1/cover-letters/:coverLetterId/analyze", "Analyzes a cover letter.", "Strength and fit scoring."),
|
|
copilot: endpoint("POST", "/api/v1/cover-letters/copilot", "Runs cover-letter copilot.", "Inline editing assistant."),
|
|
preview: endpoint("GET", "/api/v1/export/cover-letters/:coverLetterId/preview", "Returns cover-letter preview.", "Preview/PDF surface."),
|
|
},
|
|
usage: "Cover letters currently live behind resume-builder and the /services/resume/* proxy.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
workspace: {
|
|
path: "/agents/resume",
|
|
queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
usage: "Open with tab=cover-letters to land in the cover-letter list.",
|
|
},
|
|
generator: {
|
|
path: "/agents/resume",
|
|
queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
usage: "Cover-letter generation currently lands in the shared resume workspace with tab=cover-letters.",
|
|
},
|
|
editor: {
|
|
path: "/agents/resume/cover-letters/:coverLetterId",
|
|
queryParams: [],
|
|
usage: "Cover-letter editor for a known coverLetterId.",
|
|
},
|
|
},
|
|
usage: "Use workspace with tab=cover-letters for general handoffs.",
|
|
},
|
|
curator: {
|
|
defaultPage: "workspace",
|
|
defaultActionLabel: "Open cover letters",
|
|
actionLabels: {
|
|
start: "Write cover letter",
|
|
review: "Review cover letter",
|
|
},
|
|
defaultQueryState: {
|
|
tab: "cover-letters",
|
|
},
|
|
toolName: "prepare_cover_letter_handoff",
|
|
completionEvents: ["cover_letter.generated", "cover_letter.edited", "cover_letter.exported"],
|
|
qscoreSignals: ["proof.cover_letter", "readiness.application"],
|
|
usage: "Use for application-specific artifact tasks; share mission/job context in query or payload.",
|
|
},
|
|
usageDocs: [
|
|
"Call buildServiceLink('cover-letter-service', 'workspace', { tab: 'cover-letters' }) for curator handoffs.",
|
|
"Backend endpoint metadata maps to resume-builder cover-letter APIs.",
|
|
],
|
|
eventContract: {
|
|
canonicalServiceId: "cover_letter",
|
|
openEvents: ["task.opened"],
|
|
startedEvents: ["service.started"],
|
|
configuredEvents: ["service.configured"],
|
|
completionEvents: ["service.completed", "cover_letter.generated", "cover_letter.edited", "cover_letter.exported"],
|
|
abandonmentEvents: ["service.abandoned"],
|
|
qscoreSignalIds: ["proof.cover_letter", "readiness.application"],
|
|
analyticsDimensions: ["cover_letter_id", "role_fit_score", "export_format"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "active_user_action",
|
|
},
|
|
},
|
|
{
|
|
id: "courses-service",
|
|
label: "Courses",
|
|
description: "Create, list, and open learning courses.",
|
|
category: "learning",
|
|
enabled: Boolean(config.coursesServiceUrl),
|
|
featureId: "courses",
|
|
promptModulePath: "agents/courses.md",
|
|
aliases: ["course-service"],
|
|
backend: {
|
|
baseUrl: config.coursesServiceUrl,
|
|
publicUrl: config.coursesPublicUrl,
|
|
healthPath: "/api/v1/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
|
createCourse: endpoint("POST", "/api/v1/courses", "Creates a course.", "Admin or generated course creation."),
|
|
listCourses: endpoint("GET", "/api/v1/courses", "Lists courses with pagination/query filters.", "Course catalog."),
|
|
getCourse: endpoint("GET", "/api/v1/courses/:courseId", "Reads course details.", "Course detail page."),
|
|
},
|
|
usage: "Use for learning plan/course catalog handoffs; course generation stays in the service.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
catalog: {
|
|
path: "/agents/courses",
|
|
queryParams: ["view", "phase", "role", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "taskTitle", "taskSubtitle", "taskType"],
|
|
usage: "Course catalog and learning entry point.",
|
|
},
|
|
assigned: {
|
|
path: "/agents/courses/assigned",
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
usage: "Assigned course rail.",
|
|
},
|
|
lesson: {
|
|
path: "/agents/courses/assigned/:assignmentCode/lesson/:lessonId",
|
|
queryParams: [],
|
|
usage: "Assigned course lesson player.",
|
|
},
|
|
},
|
|
usage: "Open catalog for general learning handoffs.",
|
|
},
|
|
curator: {
|
|
defaultPage: "catalog",
|
|
defaultActionLabel: "Open courses",
|
|
actionLabels: {
|
|
start: "Start course",
|
|
},
|
|
toolName: "prepare_course_handoff",
|
|
completionEvents: ["course.generated", "course.started", "course.progress_recorded", "course.lesson.completed", "course.completed", "course.weakness.applied"],
|
|
qscoreSignals: ["courses.started", "courses.completed", "courses.completion_rate"],
|
|
usage: "Use for skill-gap tasks that should become learning work.",
|
|
},
|
|
usageDocs: ["Call buildServiceLink('courses-service', 'catalog', state) for course handoffs."],
|
|
eventContract: {
|
|
canonicalServiceId: "course",
|
|
openEvents: ["task.opened"],
|
|
startedEvents: ["service.started", "course.started"],
|
|
configuredEvents: ["course.generated"],
|
|
completionEvents: ["service.completed", "course.progress_recorded", "course.lesson.completed", "course.completed", "course.weakness.applied"],
|
|
abandonmentEvents: ["service.abandoned"],
|
|
qscoreSignalIds: ["courses.started", "courses.completed", "courses.completion_rate", "courses.difficulty", "courses.pathway_relevance"],
|
|
analyticsDimensions: ["course_id", "lesson_id", "progress_percentage", "weakness_tags"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "active_user_action",
|
|
},
|
|
},
|
|
{
|
|
id: "assessment-service",
|
|
label: "Assessment",
|
|
description: "Create, list, read, and submit assessments.",
|
|
category: "measurement",
|
|
enabled: Boolean(config.assessmentServiceUrl),
|
|
featureId: "assessment",
|
|
promptModulePath: "agents/assessment.md",
|
|
backend: {
|
|
baseUrl: config.assessmentServiceUrl,
|
|
publicUrl: config.assessmentPublicUrl,
|
|
healthPath: "/api/v1/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
|
createAssessment: endpoint("POST", "/api/v1/assessments", "Creates an assessment.", "Admin or generated assessment creation."),
|
|
listAssessments: endpoint("GET", "/api/v1/assessments", "Lists assessments with pagination/query filters.", "Assessment catalog."),
|
|
getAssessment: endpoint("GET", "/api/v1/assessments/:assessmentId", "Reads assessment details.", "Assessment page."),
|
|
submitAssessment: endpoint("POST", "/api/v1/assessments/:assessmentId/submit", "Submits answers and returns assessment state.", "Completion flow."),
|
|
},
|
|
usage: "Use for measurement tasks and proof of skill checks.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
assessment: {
|
|
path: "/agents/assessments",
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
usage: "Assessment landing and active assessment surface.",
|
|
},
|
|
},
|
|
usage: "Open assessment for skill/readiness measurement handoffs.",
|
|
},
|
|
curator: {
|
|
defaultPage: "assessment",
|
|
defaultActionLabel: "Open assessment",
|
|
actionLabels: {
|
|
start: "Start assessment",
|
|
review: "Review assessment",
|
|
},
|
|
toolName: "prepare_assessment_handoff",
|
|
completionEvents: ["assessment.started", "assessment.completed"],
|
|
qscoreSignals: ["skills.assessment", "readiness.measurement"],
|
|
usage: "Use when a task needs a scoreable assessment rather than practice.",
|
|
},
|
|
usageDocs: ["Call buildServiceLink('assessment-service', 'assessment', state) for assessment handoffs."],
|
|
eventContract: {
|
|
canonicalServiceId: "assessment",
|
|
openEvents: ["task.opened"],
|
|
startedEvents: ["service.started", "assessment.started"],
|
|
configuredEvents: [],
|
|
completionEvents: ["service.completed", "assessment.completed"],
|
|
abandonmentEvents: ["service.abandoned"],
|
|
qscoreSignalIds: ["skills.assessment", "readiness.measurement"],
|
|
analyticsDimensions: ["assessment_id", "skill_area", "score", "weakness_tags"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "measurement_review",
|
|
},
|
|
},
|
|
{
|
|
id: "matchmaking-service",
|
|
label: "Matchmaking",
|
|
description: "Match users to opportunities, employers, mentors, and networking targets.",
|
|
category: "opportunity",
|
|
enabled: Boolean(config.matchmakingServiceUrl),
|
|
featureId: "matchmaking",
|
|
promptModulePath: "agents/matchmaking.md",
|
|
aliases: ["jobs-service"],
|
|
backend: {
|
|
baseUrl: config.matchmakingServiceUrl,
|
|
publicUrl: config.matchmakingPublicUrl,
|
|
healthPath: "/api/v1/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
|
preferences: endpoint("GET", "/api/v1/preferences/:userId", "Reads matching preferences.", "Hydrate feed filters and personalization."),
|
|
writePreferences: endpoint("POST", "/api/v1/preferences", "Writes matching preferences.", "Preference onboarding."),
|
|
feed: endpoint("GET", "/api/v1/feed", "Returns matched opportunity feed.", "Job/opportunity feed."),
|
|
feedAction: endpoint("POST", "/api/v1/feed/actions", "Records feed actions.", "Save/apply/dismiss tracking."),
|
|
opportunity: endpoint("GET", "/api/v1/opportunities/:opportunityId", "Reads opportunity details.", "Opportunity detail panel."),
|
|
a2aTask: endpoint("POST", "/a2a/tasks", "Runs matching agent tasks.", "Agent/curator orchestrated work."),
|
|
},
|
|
usage: "Use for opportunity matching and feed intelligence; keep user-specific actions through authenticated gateway routes when added.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
jobs: {
|
|
path: "/agents/matchmaking",
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "query", "role", "location"],
|
|
usage: "Job/opportunity matching dashboard.",
|
|
},
|
|
},
|
|
usage: "Use matchmaking for immediate opportunity matching and Scout-style recommendations.",
|
|
},
|
|
curator: {
|
|
defaultPage: "jobs",
|
|
defaultActionLabel: "Open matches",
|
|
actionLabels: {
|
|
start: "View matches",
|
|
review: "Review matches",
|
|
},
|
|
toolName: "prepare_matchmaking_handoff",
|
|
completionEvents: [
|
|
"matchmaking.matches.generated",
|
|
"matchmaking.feed.viewed",
|
|
"matchmaking.match.viewed",
|
|
"matchmaking.match.saved",
|
|
"matchmaking.match.dismissed",
|
|
"matchmaking.matches.reviewed",
|
|
"matchmaking.match.applied",
|
|
"matchmaking.application.started",
|
|
"matchmaking.application.completed",
|
|
],
|
|
qscoreSignals: ["matching.jobs_viewed", "matching.applications_submitted", "matching.match_rate"],
|
|
usage: "Use for immediate opportunity matching or mentor/network suggestions.",
|
|
},
|
|
usageDocs: [
|
|
"Call buildServiceLink('matchmaking-service', 'jobs', state) for job matching.",
|
|
"Use the canonical /agents/matchmaking page for Scout-style opportunity review.",
|
|
],
|
|
eventContract: {
|
|
canonicalServiceId: "matchmaking",
|
|
openEvents: ["task.opened", "matchmaking.feed.viewed"],
|
|
startedEvents: ["service.started", "matchmaking.application.started"],
|
|
configuredEvents: [],
|
|
completionEvents: [
|
|
"service.completed",
|
|
"matchmaking.matches.generated",
|
|
"matchmaking.match.viewed",
|
|
"matchmaking.match.saved",
|
|
"matchmaking.match.dismissed",
|
|
"matchmaking.matches.reviewed",
|
|
"matchmaking.match.applied",
|
|
"matchmaking.application.completed",
|
|
],
|
|
abandonmentEvents: ["service.abandoned"],
|
|
qscoreSignalIds: ["matching.jobs_viewed", "matching.applications_submitted", "matching.application_quality", "matching.match_rate"],
|
|
analyticsDimensions: ["match_count", "role_ids", "role_fit", "saved_count", "dismissed_reason"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "active_user_action",
|
|
},
|
|
},
|
|
{
|
|
id: "qscore-service",
|
|
label: "QScore",
|
|
description: "Analyze readiness signals and expose score projections.",
|
|
category: "measurement",
|
|
enabled: Boolean(config.qscoreServiceUrl),
|
|
featureId: "q-score",
|
|
promptModulePath: "agents/qscore.md",
|
|
aliases: ["q-score-service"],
|
|
backend: {
|
|
baseUrl: config.qscoreServiceUrl,
|
|
publicUrl: config.qscorePublicUrl,
|
|
healthPath: "/health",
|
|
endpoints: {
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability."),
|
|
currentGateway: endpoint("GET", "/services/qscore/current", "Backend-projected current score and latest signals.", "Dashboard QScore panel."),
|
|
ingest: endpoint("POST", "/api/v1/signals", "Ingests score signals when available.", "Service-to-service signal updates."),
|
|
compute: endpoint("POST", "/api/v1/score/compute", "Computes or refreshes score when available.", "Score recalculation."),
|
|
},
|
|
usage: "Use backend gateway /services/qscore/current for dashboard-safe reads; direct service APIs vary by QScore deployment.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
dashboard: {
|
|
path: "/agents/qscore",
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
usage: "Dedicated Q Score service page with live readiness signals.",
|
|
},
|
|
},
|
|
usage: "Open the dedicated Q Score service page for score review.",
|
|
},
|
|
curator: {
|
|
defaultPage: "dashboard",
|
|
defaultActionLabel: "Review QScore",
|
|
actionLabels: {
|
|
review: "Review QScore",
|
|
},
|
|
toolName: "prepare_qscore_review",
|
|
completionEvents: ["qscore.baseline.created", "qscore.review.opened", "qscore.weak_driver.reviewed", "qscore.signal.projected", "qscore.updated"],
|
|
qscoreSignals: ["qscore.updated", "qscore.signal.projected"],
|
|
usage: "Use for measurement and projected readiness review tasks.",
|
|
},
|
|
usageDocs: ["Call buildServiceLink('qscore-service', 'dashboard', state) for QScore handoffs."],
|
|
eventContract: {
|
|
canonicalServiceId: "qscore",
|
|
openEvents: ["qscore.review.opened"],
|
|
startedEvents: ["service.started"],
|
|
configuredEvents: ["qscore.baseline.created"],
|
|
completionEvents: ["qscore.signal.projected", "qscore.updated", "qscore.weak_driver.reviewed"],
|
|
abandonmentEvents: [],
|
|
qscoreSignalIds: ["qscore.updated", "qscore.signal.projected"],
|
|
analyticsDimensions: ["score_before", "score_after", "signal_id", "delta", "trend_label"],
|
|
requiredCorrelationFields: [],
|
|
handoffParams: [],
|
|
actionKind: "measurement_review",
|
|
},
|
|
},
|
|
{
|
|
id: "social-branding-service",
|
|
label: "Social Branding",
|
|
description: "Build and optimize professional profile, LinkedIn, content, and brand signals.",
|
|
category: "profile",
|
|
enabled: Boolean(config.socialBrandingServiceUrl),
|
|
featureId: "social-branding",
|
|
promptModulePath: "agents/social-branding.md",
|
|
aliases: ["social-service"],
|
|
backend: {
|
|
baseUrl: config.socialBrandingServiceUrl,
|
|
publicUrl: config.socialBrandingPublicUrl,
|
|
healthPath: "/healthz",
|
|
endpoints: {
|
|
health: endpoint("GET", "/healthz", "Readiness probe.", "Check service availability."),
|
|
state: endpoint("GET", "/api/state/:clerkId", "Reads social/profile state.", "Hydrate personalization context."),
|
|
profile: endpoint("GET", "/api/v1/profile", "Reads profile data when available.", "Social profile page."),
|
|
linkedin: endpoint("POST", "/api/v1/linkedin", "Connects or imports LinkedIn data when available.", "LinkedIn onboarding."),
|
|
analyze: endpoint("POST", "/api/v1/analyze", "Analyzes profile/social brand when available.", "Brand improvement tasks."),
|
|
},
|
|
usage: "Use /services/social/* gateway proxy for browser-authenticated profile calls.",
|
|
},
|
|
frontend: {
|
|
baseUrl: frontendBaseUrl,
|
|
pages: {
|
|
profile: {
|
|
path: "/agents/social-branding",
|
|
queryParams: ["view", "phase", "role", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "taskTitle", "taskSubtitle", "taskType"],
|
|
usage: "Social/profile improvement page.",
|
|
},
|
|
},
|
|
usage: "Open profile for branding, LinkedIn, and social proof handoffs.",
|
|
},
|
|
curator: {
|
|
defaultPage: "profile",
|
|
defaultActionLabel: "Open social profile flow",
|
|
actionLabels: {
|
|
start: "Improve profile",
|
|
review: "Review profile",
|
|
},
|
|
toolName: "prepare_social_branding_handoff",
|
|
completionEvents: ["brand.post.drafted", "brand.post.edited", "brand.post.exported", "brand.profile.updated", "brand.content.published"],
|
|
qscoreSignals: ["profile.linkedin", "proof.visibility", "networking.brand"],
|
|
usage: "Use for profile visibility, LinkedIn cleanup, and social proof tasks.",
|
|
},
|
|
usageDocs: ["Call buildServiceLink('social-branding-service', 'profile', state) for social branding handoffs."],
|
|
eventContract: {
|
|
canonicalServiceId: "brand",
|
|
openEvents: ["task.opened"],
|
|
startedEvents: ["service.started"],
|
|
configuredEvents: ["brand.post.drafted"],
|
|
completionEvents: ["service.completed", "brand.post.edited", "brand.post.exported", "brand.profile.updated", "brand.content.published"],
|
|
abandonmentEvents: ["service.abandoned"],
|
|
qscoreSignalIds: ["brand.visibility_signal", "profile.linkedin", "proof.visibility"],
|
|
analyticsDimensions: ["content_type", "channel", "draft_id", "topic", "profile_field_updated"],
|
|
requiredCorrelationFields: ["taskId"],
|
|
handoffParams: ["curatorTaskId", "missionInstanceId", "missionId", "stageId"],
|
|
actionKind: "active_user_action",
|
|
},
|
|
},
|
|
];
|
|
|
|
const serviceAliases = new Map<string, ServiceId>();
|
|
for (const service of serviceRegistry) {
|
|
serviceAliases.set(service.id, service.id);
|
|
for (const alias of service.aliases ?? []) serviceAliases.set(alias, service.id);
|
|
}
|
|
|
|
export function normalizeServiceId(serviceId?: string | null): ServiceId | undefined {
|
|
if (!serviceId) return undefined;
|
|
return serviceAliases.get(serviceId);
|
|
}
|
|
|
|
export function listServices() {
|
|
return serviceRegistry.filter((service) => allowedServiceIds.has(service.id));
|
|
}
|
|
|
|
function getServiceRecord(serviceId?: string | null) {
|
|
const normalized = normalizeServiceId(serviceId);
|
|
return normalized ? serviceRegistry.find((service) => service.id === normalized) : undefined;
|
|
}
|
|
|
|
export function getService(serviceId?: string | null) {
|
|
const normalized = normalizeServiceId(serviceId);
|
|
return normalized ? listServices().find((service) => service.id === normalized) : undefined;
|
|
}
|
|
|
|
export function getServiceBackend(serviceId?: string | null) {
|
|
return getService(serviceId)?.backend;
|
|
}
|
|
|
|
export function getServiceFrontend(serviceId?: string | null) {
|
|
return getService(serviceId)?.frontend;
|
|
}
|
|
|
|
export function getServiceEndpoint(serviceId: string | undefined, endpointId: string) {
|
|
return getService(serviceId)?.backend.endpoints[endpointId];
|
|
}
|
|
|
|
export function getServiceUsageDocs(serviceId?: string | null) {
|
|
return getService(serviceId)?.usageDocs ?? [];
|
|
}
|
|
|
|
function resolvePage(service: ServiceRecord, pageId?: string) {
|
|
const selectedPageId = pageId || service.curator.defaultPage;
|
|
const direct = service.frontend.pages[selectedPageId];
|
|
if (direct) return direct;
|
|
return Object.values(service.frontend.pages).find((page) => page.aliases?.includes(selectedPageId));
|
|
}
|
|
|
|
export function buildServiceLink(serviceId: string | undefined, pageId?: string, state: QueryState = {}) {
|
|
const service = getService(serviceId);
|
|
if (!service) return undefined;
|
|
const page = resolvePage(service, pageId);
|
|
if (!page) return undefined;
|
|
const includeDefaultState = !pageId || pageId === service.curator.defaultPage;
|
|
return appendQuery(page.path, {
|
|
...(includeDefaultState ? service.curator.defaultQueryState : {}),
|
|
...state,
|
|
});
|
|
}
|
|
|
|
export function listServicesForCatalog() {
|
|
return listServices().map((service) => ({
|
|
id: service.id,
|
|
label: service.label,
|
|
description: service.description,
|
|
category: service.category,
|
|
enabled: service.enabled,
|
|
featureId: service.featureId,
|
|
backend: {
|
|
publicUrl: service.backend.publicUrl,
|
|
healthPath: service.backend.healthPath,
|
|
endpoints: service.backend.endpoints,
|
|
usage: service.backend.usage,
|
|
},
|
|
frontend: service.frontend,
|
|
curator: service.curator,
|
|
eventContract: service.eventContract,
|
|
usageDocs: service.usageDocs,
|
|
}));
|
|
}
|
|
|
|
export function buildServiceSessionPath(
|
|
serviceId: MissionServiceId,
|
|
detail?: Record<string, unknown>,
|
|
goal?: string,
|
|
) {
|
|
const sessionId = getSessionId(detail);
|
|
|
|
if (serviceId === "interview-service") {
|
|
if (!sessionId) return undefined;
|
|
return buildServiceLink(serviceId, "session", {
|
|
session_id: sessionId,
|
|
goal,
|
|
role: getString(detail?.target_role) ?? goal ?? "Interview practice",
|
|
type: getString(detail?.interview_type) ?? "behavioral",
|
|
});
|
|
}
|
|
|
|
if (serviceId === "roleplay-service") {
|
|
if (!sessionId) return undefined;
|
|
return buildServiceLink(serviceId, "session", {
|
|
session_id: sessionId,
|
|
goal,
|
|
role: getString(detail?.target_role) ?? goal ?? "Roleplay practice",
|
|
type: getString(detail?.roleplay_type) ?? "custom",
|
|
});
|
|
}
|
|
|
|
return buildServiceLink(serviceId, "session", {
|
|
goal,
|
|
role: goal,
|
|
});
|
|
}
|
|
|
|
export function buildMissionServiceRoute(input: MissionRouteInput) {
|
|
const pageId = input.serviceId === "resume-service" ? "workspace" : "setup";
|
|
return buildServiceLink(input.serviceId, pageId, {
|
|
source: "mission",
|
|
missionInstanceId: input.missionInstanceId,
|
|
missionId: input.missionId,
|
|
stageId: input.stageId,
|
|
goal: input.goal,
|
|
role: input.goal,
|
|
type: input.serviceId === "interview-service" ? "behavioral" : undefined,
|
|
}) ?? appendQuery("/missions/active", { missionInstanceId: input.missionInstanceId });
|
|
}
|
|
|
|
function curatorBaseParams(input: CuratorRouteInput) {
|
|
return {
|
|
source: "curator-v1",
|
|
missionInstanceId: input.missionInstanceId,
|
|
missionId: input.missionId,
|
|
stageId: input.stageId,
|
|
curatorTaskId: input.taskId,
|
|
};
|
|
}
|
|
|
|
function unavailableServiceRoute(input: CuratorRouteInput) {
|
|
return appendQuery("/agents/service-unavailable", {
|
|
...curatorBaseParams(input),
|
|
serviceId: input.serviceId,
|
|
});
|
|
}
|
|
|
|
export function buildCuratorServiceRoute(input: CuratorRouteInput) {
|
|
const service = getService(input.serviceId);
|
|
if (!service) return unavailableServiceRoute(input);
|
|
|
|
const state: QueryState = {
|
|
...curatorBaseParams(input),
|
|
};
|
|
if (input.taskTitle) state.taskTitle = input.taskTitle;
|
|
if (input.taskSubtitle) state.taskSubtitle = input.taskSubtitle;
|
|
if (input.taskType) state.taskType = input.taskType;
|
|
|
|
if (service.id === "interview-service") {
|
|
state.role = input.targetRole?.trim() || "Product Manager";
|
|
state.type = "behavioral";
|
|
state.persona = input.personaId ?? "payal";
|
|
state.duration = input.durationMinutes ?? 5;
|
|
state.difficulty = input.difficulty ?? "medium";
|
|
state.media = input.requestedMode ?? "video";
|
|
}
|
|
|
|
if (service.id === "roleplay-service") {
|
|
state.role = input.targetRole?.trim() || "Professional";
|
|
state.persona = input.personaId ?? "emma";
|
|
state.duration = input.durationMinutes ?? 5;
|
|
state.mode = input.requestedMode ?? "video";
|
|
state.brief = input.roleplayBrief;
|
|
}
|
|
|
|
if (service.id === "social-branding-service") {
|
|
state.agent = "nova";
|
|
}
|
|
|
|
if (
|
|
service.id === "resume-service" ||
|
|
service.id === "courses-service" ||
|
|
service.id === "social-branding-service"
|
|
) {
|
|
state.role = input.targetRole?.trim();
|
|
}
|
|
|
|
return buildServiceLink(service.id, service.curator.defaultPage, state)
|
|
?? appendQuery("/missions/active", { missionInstanceId: input.missionInstanceId });
|
|
}
|
|
|
|
const unavailableServiceLabels: Record<string, string> = {
|
|
"social-branding-service": "Social Branding",
|
|
"qscore-service": "Q Score",
|
|
"assessment-service": "Assessment",
|
|
"expert-service": "Expert",
|
|
"events-service": "Events",
|
|
};
|
|
|
|
export function getServiceDisplayName(serviceId?: string, fallback = "Mission planner") {
|
|
return getService(serviceId)?.label ?? (serviceId ? unavailableServiceLabels[serviceId] : undefined) ?? fallback;
|
|
}
|
|
|
|
export function getServiceToolName(serviceId?: string) {
|
|
return getService(serviceId)?.curator.toolName ?? "prepare_mission_step";
|
|
}
|
|
|
|
export function getCompletionEvents(serviceId?: string) {
|
|
return getServiceRecord(serviceId)?.curator.completionEvents ?? ["curator.task.completed"];
|
|
}
|
|
|
|
export const getServiceCompletionEvents = getCompletionEvents;
|
|
|
|
export function getServiceActionLabel(serviceId?: string, actionId?: string): string;
|
|
export function getServiceActionLabel(task: Pick<CuratorTask, "serviceId" | "cta">): string;
|
|
export function getServiceActionLabel(
|
|
input?: string | Pick<CuratorTask, "serviceId" | "cta">,
|
|
actionId?: string,
|
|
) {
|
|
if (typeof input === "object") {
|
|
const service = getService(input.serviceId);
|
|
if (actionId && service?.curator.actionLabels?.[actionId]) return service.curator.actionLabels[actionId];
|
|
return service?.curator.defaultActionLabel ?? input.cta ?? "Open";
|
|
}
|
|
const service = getService(input);
|
|
if (actionId && service?.curator.actionLabels?.[actionId]) return service.curator.actionLabels[actionId];
|
|
return service?.curator.defaultActionLabel ?? "Open";
|
|
}
|