update source code (src) (13 files)

This commit is contained in:
-Puter
2026-06-01 23:03:20 +05:30
parent 068b57c553
commit a937bcf09e
13 changed files with 317 additions and 24 deletions

View File

@@ -0,0 +1,70 @@
import { actor } from "rivetkit";
import { interviewService, resumeService, roleplayService, type JsonObject } from "../services/product-service-clients.js";
export const interviewServiceActor = actor({
state: { calls: 0, lastCallAt: null as string | null },
actions: {
health: async (c) => track(c, () => interviewService.health()),
configure: async (c, payload: JsonObject) => track(c, () => interviewService.configure(payload)),
preview: async (c, payload: JsonObject) => track(c, () => interviewService.preview(payload)),
editQuestions: async (c, sessionId: string, questions: Array<JsonObject | string>) => track(c, () => interviewService.editQuestions({ session_id: sessionId, questions })),
approve: async (c, sessionId: string) => track(c, () => interviewService.approve(sessionId)),
createAssignments: async (c, payload: { organization_id: string; role: string; round: "warm_up" | "behavioral" | "technical"; assignee_emails: string[] }) => track(c, () => interviewService.createAssignments(payload)),
listAssignments: async (c, email: string, status?: string, limit?: number) => track(c, () => interviewService.listAssignments(email, status, limit)),
unassign: async (c, payload: { organization_id: string; emails: string[] }) => track(c, () => interviewService.unassign(payload)),
resultsBulk: async (c, payload: JsonObject) => track(c, () => interviewService.resultsBulk(payload)),
review: async (c, sessionId: string) => track(c, () => interviewService.review(sessionId)),
leaderboard: async (c) => track(c, () => interviewService.leaderboard()),
artifact: async (c, sessionId: string, artifactType: string) => track(c, () => interviewService.artifact(sessionId, artifactType)),
createVideoUploadUrl: async (c, sessionId: string, payload: JsonObject) => track(c, () => interviewService.createVideoUploadUrl(sessionId, payload)),
markVideoUploaded: async (c, sessionId: string, payload: JsonObject) => track(c, () => interviewService.markVideoUploaded(sessionId, payload)),
},
});
export const roleplayServiceActor = actor({
state: { calls: 0, lastCallAt: null as string | null },
actions: {
health: async (c) => track(c, () => roleplayService.health()),
configure: async (c, payload: JsonObject) => track(c, () => roleplayService.configure(payload)),
preview: async (c, payload: JsonObject) => track(c, () => roleplayService.preview(payload)),
editQuestions: async (c, sessionId: string, questions: Array<JsonObject | string>) => track(c, () => roleplayService.editQuestions({ session_id: sessionId, questions })),
approve: async (c, sessionId: string) => track(c, () => roleplayService.approve(sessionId)),
createAssignments: async (c, payload: { organization_id: string; name: string; scenario: string; assignee_emails: string[] }) => track(c, () => roleplayService.createAssignments(payload)),
listAssignments: async (c, email: string, status?: string, limit?: number) => track(c, () => roleplayService.listAssignments(email, status, limit)),
unassign: async (c, payload: { organization_id: string; roleplay_id: string; emails: string[] }) => track(c, () => roleplayService.unassign(payload)),
resultsBulk: async (c, payload: JsonObject) => track(c, () => roleplayService.resultsBulk(payload)),
review: async (c, sessionId: string) => track(c, () => roleplayService.review(sessionId)),
leaderboard: async (c) => track(c, () => roleplayService.leaderboard()),
artifact: async (c, sessionId: string, artifactType: string) => track(c, () => roleplayService.artifact(sessionId, artifactType)),
createVideoUploadUrl: async (c, sessionId: string, payload: JsonObject) => track(c, () => roleplayService.createVideoUploadUrl(sessionId, payload)),
markVideoUploaded: async (c, sessionId: string, payload: JsonObject) => track(c, () => roleplayService.markVideoUploaded(sessionId, payload)),
},
});
export const resumeServiceActor = actor({
state: { calls: 0, lastCallAt: null as string | null },
actions: {
health: async (c) => track(c, () => resumeService.health()),
state: async (c, clerkId: string) => track(c, () => resumeService.state(clerkId)),
listResumes: async (c, clerkId: string) => track(c, () => resumeService.listResumes(clerkId)),
createResume: async (c, payload: JsonObject) => track(c, () => resumeService.createResume(payload)),
getResume: async (c, resumeId: string) => track(c, () => resumeService.getResume(resumeId)),
updateResume: async (c, resumeId: string, payload: JsonObject) => track(c, () => resumeService.updateResume(resumeId, payload)),
analyzeResume: async (c, resumeId: string, payload?: JsonObject) => track(c, () => resumeService.analyzeResume(resumeId, payload ?? {})),
suggestions: async (c, resumeId: string) => track(c, () => resumeService.suggestions(resumeId)),
copilot: async (c, payload: JsonObject) => track(c, () => resumeService.copilot(payload)),
optimizeSummary: async (c, payload: JsonObject) => track(c, () => resumeService.optimizeSummary(payload)),
optimizeExperience: async (c, payload: JsonObject) => track(c, () => resumeService.optimizeExperience(payload)),
suggestSkills: async (c, payload: JsonObject) => track(c, () => resumeService.suggestSkills(payload)),
generateSummary: async (c, payload: JsonObject) => track(c, () => resumeService.generateSummary(payload)),
listVersions: async (c, resumeId: string) => track(c, () => resumeService.listVersions(resumeId)),
createVersion: async (c, resumeId: string, payload: JsonObject) => track(c, () => resumeService.createVersion(resumeId, payload)),
preview: async (c, resumeId: string) => track(c, () => resumeService.preview(resumeId)),
},
});
async function track<T>(c: { state: { calls: number; lastCallAt: string | null } }, fn: () => Promise<T>): Promise<T> {
c.state.calls += 1;
c.state.lastCallAt = new Date().toISOString();
return fn();
}

View File

@@ -1,6 +1,7 @@
import { setup } from "rivetkit";
import { userActor } from "./user-actor.js";
import { workflowRunActor } from "./workflow-run-actor.js";
import { interviewServiceActor, resumeServiceActor, roleplayServiceActor } from "./product-service-actors.js";
// Per changes.md §5: ONE unified actor per user.
// No separate growAgent, subAgent, or workflowJob actors.
@@ -8,6 +9,9 @@ export const registry = setup({
use: {
userActor,
workflowRunActor,
interviewServiceActor,
roleplayServiceActor,
resumeServiceActor,
},
});

View File

@@ -175,11 +175,11 @@ function buildUnifiedTools(): Array<{
type: "function" as const,
function: {
name: "run_workflow_module",
description: "Execute a specific sub-agent module in the workflow (e.g., resume, job-search, job-apply, sara, emily, qscore).",
description: "Execute a specific production sub-agent module in the workflow (e.g., resume, sara, emily, qscore).",
parameters: {
type: "object",
properties: {
moduleId: { type: "string", description: "Module id: resume, job-search, job-apply, sara, emily, qscore" },
moduleId: { type: "string", description: "Module id: resume, sara, emily, qscore" },
},
required: ["moduleId"],
},

View File

@@ -92,7 +92,7 @@ export const config = {
// Version tracking for rollout (changes.md §9)
opencodeImageVersion: process.env.OPENCODE_IMAGE_VERSION ?? "dev",
migrationVersion: process.env.MIGRATION_VERSION ?? "1",
promptVersion: process.env.PROMPT_VERSION ?? "2",
promptVersion: process.env.PROMPT_VERSION ?? "4",
// Host that user containers expose ports on (the host running Docker).
userContainerHost: process.env.USER_CONTAINER_HOST ?? "127.0.0.1",

View File

@@ -152,8 +152,9 @@ async function cloneRepoIntoContainer(opts: {
let cmd: string[];
if (checkOutput.includes("exists")) {
// Pull latest changes.
cmd = ["sh", "-c", "cd /workspace && git pull origin main 2>&1 || echo 'pull failed, attempting fresh clone'"];
// Existing workspace template may have a local git repo without origin.
// Ensure origin points at the user's Gitea repo before pulling.
cmd = ["sh", "-c", `cd /workspace && (git remote get-url origin >/dev/null 2>&1 && git remote set-url origin "${authUrl}" || git remote add origin "${authUrl}") && git pull origin main --allow-unrelated-histories 2>&1 || true`];
} else {
// Clone into /workspace (remove any placeholder files first, then clone).
cmd = [
@@ -209,7 +210,7 @@ export async function syncWorkspaceToGit(userId: string, message?: string): Prom
// Set the remote URL with auth, add all, commit, push.
const cmd = [
"sh", "-c",
`git remote set-url origin "${authUrl}" 2>/dev/null; ` +
`(git remote get-url origin >/dev/null 2>&1 && git remote set-url origin "${authUrl}" || git remote add origin "${authUrl}"); ` +
`git config user.email "growqr@local" && git config user.name "GrowQR"; ` +
`git add -A && git commit -m "${commitMsg.replace(/"/g, '\\"')}" 2>/dev/null; ` +
`git push origin main 2>&1`,
@@ -337,7 +338,12 @@ export async function provisionUserStack(userId: string): Promise<UserStack> {
existing.imageVersion === config.opencodeImageVersion &&
existing.migrationVersion === config.migrationVersion &&
existing.promptVersion === config.promptVersion;
if (current) return existing;
if (current) {
const containerName = existing.opencodeContainerName ?? safeContainerName("growqr-opencode", userId);
const container = await findExistingContainer(containerName);
if (container) return existing;
log.warn({ userId, containerName }, "OpenCode stack marked running but container is missing; reprovisioning");
}
log.info(
{

View File

@@ -12,6 +12,7 @@ import { userRoutes } from "./routes/users.js";
import { agentRoutes } from "./routes/agents.js";
import { workflowRoutes, workflowRunRoutes } from "./routes/workflows.js";
import { chatRoutes } from "./routes/chat.js";
import { serviceRoutes } from "./routes/services.js";
import { db } from "./db/client.js";
import { hydratePortAllocator, reconcileOnBoot, ensureCentralGiteaReady } from "./docker/manager.js";
import { initCatalog } from "./agents/catalog.js";
@@ -78,6 +79,7 @@ async function main() {
app.route("/opencode", opencodeRoutes());
app.route("/git", gitRoutes());
app.route("/api/chat", chatRoutes());
app.route("/services", serviceRoutes());
if (process.env.RIVET_ENDPOINT) {
// Self-hosted: embedded engine runs at localhost:6420.

View File

@@ -89,7 +89,7 @@ export function getSubAgentModule(id: string): SubAgentModule | undefined {
}
export function jobApplicationModuleIds(): string[] {
return ["resume", "job-search", "job-apply", "sara", "emily", "qscore"];
return ["resume", "sara", "emily", "qscore"];
}
// Load all prompt and agent files from disk.

View File

@@ -206,7 +206,7 @@ export function chatRoutes() {
sessionId: detail.session_id as string,
sessionUrl: typeof detail.ui_session_url === "string"
? detail.ui_session_url
: buildServiceSessionUrl("roleplay-service", detail, String(toolCall.arguments.goal ?? "general practice",
: buildServiceSessionUrl("roleplay-service", detail, String(toolCall.arguments.goal ?? "general practice")),
summary: toolResult.summary,
});
}

76
src/routes/services.ts Normal file
View File

@@ -0,0 +1,76 @@
import { Hono } from "hono";
import { requireUser, type AuthContext } from "../auth/clerk.js";
import { listServiceCapabilities } from "../workflows/service-capabilities.js";
import { interviewService, resumeService, roleplayService, type JsonObject } from "../services/product-service-clients.js";
export function serviceRoutes() {
const app = new Hono<AuthContext>();
app.use("*", requireUser);
app.get("/catalog", (c) => c.json({ services: listServiceCapabilities() }));
app.get("/:service/health", async (c) => {
const service = c.req.param("service");
if (service === "interview") return c.json(await interviewService.health());
if (service === "roleplay") return c.json(await roleplayService.health());
if (service === "resume") return c.json(await resumeService.health());
return c.json({ error: "unknown_service" }, 404);
});
app.post("/interview/configure", async (c) => c.json(await interviewService.configure(await c.req.json<JsonObject>())));
app.post("/interview/preview", async (c) => c.json(await interviewService.preview(await c.req.json<JsonObject>())));
app.post("/interview/questions", async (c) => c.json(await interviewService.editQuestions(await c.req.json())));
app.post("/interview/approve", async (c) => {
const body = await c.req.json<{ session_id: string }>();
return c.json(await interviewService.approve(body.session_id));
});
app.post("/interview/assignments", async (c) => c.json(await interviewService.createAssignments(await c.req.json())));
app.get("/interview/assignments", async (c) => c.json(await interviewService.listAssignments(c.req.query("email") ?? "", c.req.query("status") ?? "pending", Number(c.req.query("limit") ?? 20))));
app.post("/interview/assignments/unassign", async (c) => c.json(await interviewService.unassign(await c.req.json())));
app.post("/interview/results:bulk", async (c) => c.json(await interviewService.resultsBulk(await c.req.json<JsonObject>())));
app.get("/interview/review/:sessionId", async (c) => c.json(await interviewService.review(c.req.param("sessionId"))));
app.get("/interview/leaderboard", async (c) => c.json(await interviewService.leaderboard()));
app.get("/interview/artifacts/:sessionId/:artifactType", async (c) => c.json(await interviewService.artifact(c.req.param("sessionId"), c.req.param("artifactType"))));
app.post("/interview/sessions/:sessionId/video/upload-url", async (c) => c.json(await interviewService.createVideoUploadUrl(c.req.param("sessionId"), await c.req.json<JsonObject>())));
app.post("/interview/sessions/:sessionId/video/uploaded", async (c) => c.json(await interviewService.markVideoUploaded(c.req.param("sessionId"), await c.req.json<JsonObject>())));
app.post("/roleplay/configure", async (c) => c.json(await roleplayService.configure(await c.req.json<JsonObject>())));
app.post("/roleplay/preview", async (c) => c.json(await roleplayService.preview(await c.req.json<JsonObject>())));
app.post("/roleplay/questions", async (c) => c.json(await roleplayService.editQuestions(await c.req.json())));
app.post("/roleplay/approve", async (c) => {
const body = await c.req.json<{ session_id: string }>();
return c.json(await roleplayService.approve(body.session_id));
});
app.post("/roleplay/assignments", async (c) => c.json(await roleplayService.createAssignments(await c.req.json())));
app.get("/roleplay/assignments", async (c) => c.json(await roleplayService.listAssignments(c.req.query("email") ?? "", c.req.query("status") ?? "pending", Number(c.req.query("limit") ?? 20))));
app.post("/roleplay/assignments/unassign", async (c) => c.json(await roleplayService.unassign(await c.req.json())));
app.post("/roleplay/results:bulk", async (c) => c.json(await roleplayService.resultsBulk(await c.req.json<JsonObject>())));
app.get("/roleplay/review/:sessionId", async (c) => c.json(await roleplayService.review(c.req.param("sessionId"))));
app.get("/roleplay/leaderboard", async (c) => c.json(await roleplayService.leaderboard()));
app.get("/roleplay/artifacts/:sessionId/:artifactType", async (c) => c.json(await roleplayService.artifact(c.req.param("sessionId"), c.req.param("artifactType"))));
app.post("/roleplay/sessions/:sessionId/video/upload-url", async (c) => c.json(await roleplayService.createVideoUploadUrl(c.req.param("sessionId"), await c.req.json<JsonObject>())));
app.post("/roleplay/sessions/:sessionId/video/uploaded", async (c) => c.json(await roleplayService.markVideoUploaded(c.req.param("sessionId"), await c.req.json<JsonObject>())));
app.get("/resume/state/:clerkId", async (c) => c.json(await resumeService.state(c.req.param("clerkId"))));
app.get("/resume/templates", async (c) => c.json(await resumeService.templates()));
app.post("/resume/tasks", async (c) => {
const body = await c.req.json<JsonObject>();
return c.json(await resumeService.task({ ...body, user_id: String(body.user_id ?? c.get("userId")) }));
});
app.get("/resume/resumes", async (c) => c.json(await resumeService.listResumes(c.req.query("clerk_id") ?? "")));
app.post("/resume/resumes", async (c) => c.json(await resumeService.createResume(await c.req.json<JsonObject>())));
app.get("/resume/resumes/:resumeId", async (c) => c.json(await resumeService.getResume(c.req.param("resumeId"))));
app.put("/resume/resumes/:resumeId", async (c) => c.json(await resumeService.updateResume(c.req.param("resumeId"), await c.req.json<JsonObject>())));
app.post("/resume/resumes/:resumeId/analyze", async (c) => c.json(await resumeService.analyzeResume(c.req.param("resumeId"), await c.req.json<JsonObject>().catch(() => ({})))));
app.get("/resume/resumes/:resumeId/suggestions", async (c) => c.json(await resumeService.suggestions(c.req.param("resumeId"))));
app.post("/resume/ai/copilot", async (c) => c.json(await resumeService.copilot(await c.req.json<JsonObject>())));
app.post("/resume/ai/optimize-summary", async (c) => c.json(await resumeService.optimizeSummary(await c.req.json<JsonObject>())));
app.post("/resume/ai/optimize-experience", async (c) => c.json(await resumeService.optimizeExperience(await c.req.json<JsonObject>())));
app.post("/resume/ai/suggest-skills", async (c) => c.json(await resumeService.suggestSkills(await c.req.json<JsonObject>())));
app.post("/resume/ai/generate-summary", async (c) => c.json(await resumeService.generateSummary(await c.req.json<JsonObject>())));
app.get("/resume/resumes/:resumeId/versions", async (c) => c.json(await resumeService.listVersions(c.req.param("resumeId"))));
app.post("/resume/resumes/:resumeId/versions", async (c) => c.json(await resumeService.createVersion(c.req.param("resumeId"), await c.req.json<JsonObject>())));
app.get("/resume/resumes/:resumeId/preview", async (c) => c.json(await resumeService.preview(c.req.param("resumeId"))));
return app;
}

View File

@@ -15,6 +15,7 @@ import { validateWorkflowDefinition } from "../workflows/validation.js";
import { listServiceCapabilities } from "../workflows/service-capabilities.js";
import { getUserStack, giteaClientFor } from "../docker/manager.js";
import { runServiceAgentProbe } from "../services/service-agents.js";
import type { WorkflowDefinition } from "../workflows/types.js";
let _client: Client<Registry> | null = null;
function getClient(): Client<Registry> { return (_client ??= createClient<Registry>(config.rivetEndpoint)); }
@@ -147,14 +148,7 @@ export function workflowRunRoutes() {
await db.insert(workflowEvents).values({ runId: run.id, userId: c.get("userId"), type: "workflow.run_all_queued", payload: { idempotencyKey } });
const def = getWorkflowDefinition(run.workflowId);
if (!def) return c.json({ error: "workflow_not_found" }, 404);
for (const mod of def.modules) {
const result = await executeWorkflowModule({ userId: c.get("userId"), runId: run.id, moduleId: mod.id });
if (mod.approvalGateAfter && result.status === "done") {
await db.insert(workflowApprovals).values({ runId: run.id, approvalId: mod.approvalGateAfter, status: "pending", payload: { afterModuleId: mod.id } }).onConflictDoNothing();
await db.insert(workflowEvents).values({ runId: run.id, userId: c.get("userId"), type: "approval.required", payload: { approvalId: mod.approvalGateAfter, afterModuleId: mod.id } });
break;
}
}
await runModulesUntilGate({ userId: c.get("userId"), runId: run.id, def, idempotencyKey });
return c.json({ queued: true, runId: run.id, idempotencyKey });
});
app.post("/:runId/modules/:moduleId/run", async (c) => {
@@ -201,9 +195,16 @@ export function workflowRunRoutes() {
await db.insert(workflowApprovals).values({ runId: run.id, approvalId: c.req.param("approvalId"), status, payload: body, resolvedAt: new Date() });
await db.insert(workflowEvents).values({ runId: run.id, userId: c.get("userId"), type: "approval.recorded", payload: { approvalId: c.req.param("approvalId"), status, body } });
if (status === "approved" && body?.continue !== false) {
const actor = workflowActorFor(c.get("userId"), run.id);
await actor.init({ userId: c.get("userId"), runId: run.id });
await actor.runAll({ userId: c.get("userId"), runId: run.id, idempotencyKey: `${run.id}:approval:${c.req.param("approvalId")}:${Date.now()}` });
const def = getWorkflowDefinition(run.workflowId);
if (!def) return c.json({ error: "workflow_not_found" }, 404);
await db.update(workflowRuns).set({ status: "running", updatedAt: new Date() }).where(eq(workflowRuns.id, run.id));
await runModulesUntilGate({
userId: c.get("userId"),
runId: run.id,
def,
idempotencyKey: `${run.id}:approval:${c.req.param("approvalId")}:${Date.now()}`,
skipApprovalId: c.req.param("approvalId"),
});
}
return c.json({ ok: true, status });
});
@@ -215,6 +216,38 @@ async function requireOwnedRun(userId: string, runId: string) {
return run ?? null;
}
async function runModulesUntilGate(input: {
userId: string;
runId: string;
def: WorkflowDefinition;
idempotencyKey: string;
skipApprovalId?: string;
}) {
const terminal = new Set(["done", "blocked", "manual_required", "coming_soon", "opencode_required"]);
for (const mod of input.def.modules) {
const [existing] = await db
.select()
.from(workflowRunModules)
.where(and(eq(workflowRunModules.runId, input.runId), eq(workflowRunModules.moduleId, mod.id)))
.limit(1);
if (existing && terminal.has(existing.status)) continue;
const result = await executeWorkflowModule({ userId: input.userId, runId: input.runId, moduleId: mod.id });
if (mod.approvalGateAfter && mod.approvalGateAfter !== input.skipApprovalId && result.status === "done") {
const [pending] = await db
.select()
.from(workflowApprovals)
.where(and(eq(workflowApprovals.runId, input.runId), eq(workflowApprovals.approvalId, mod.approvalGateAfter), eq(workflowApprovals.status, "pending")))
.limit(1);
if (!pending) {
await db.insert(workflowApprovals).values({ runId: input.runId, approvalId: mod.approvalGateAfter, status: "pending", payload: { afterModuleId: mod.id, idempotencyKey: input.idempotencyKey } });
await db.insert(workflowEvents).values({ runId: input.runId, userId: input.userId, type: "approval.required", payload: { approvalId: mod.approvalGateAfter, afterModuleId: mod.id } });
}
break;
}
}
}
function extractQScore(output: Record<string, unknown>): number | undefined {
const direct = output.q_score ?? output.estimated_q_score;
if (typeof direct === "number") return Math.round(direct);

View File

@@ -0,0 +1,102 @@
import { config } from "../config.js";
export type JsonObject = Record<string, unknown>;
export type ServiceCallOptions = {
method?: string;
body?: unknown;
headers?: Record<string, string>;
};
async function serviceJson<T = JsonObject>(
baseUrl: string,
path: string,
opts: ServiceCallOptions = {},
): Promise<T> {
const res = await fetch(`${baseUrl.replace(/\/$/, "")}${path}`, {
method: opts.method ?? (opts.body === undefined ? "GET" : "POST"),
headers: {
"content-type": "application/json",
...(config.a2aAllowedKey ? { authorization: `Bearer ${config.a2aAllowedKey}` } : {}),
...(opts.headers ?? {}),
},
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
});
const text = await res.text();
if (!res.ok) throw new Error(`${path} returned HTTP ${res.status}: ${text}`);
return (text ? JSON.parse(text) : {}) as T;
}
export const interviewService = {
health: () => serviceJson(config.interviewServiceUrl, "/health"),
configure: (payload: JsonObject) => serviceJson(config.interviewServiceUrl, "/api/v1/configure", { body: payload }),
preview: (payload: JsonObject) => serviceJson(config.interviewServiceUrl, "/api/v1/configure/preview", { body: payload }),
editQuestions: (payload: { session_id: string; questions: Array<JsonObject | string> }) =>
serviceJson(config.interviewServiceUrl, "/api/v1/configure/questions", { body: payload }),
approve: (sessionId: string) =>
serviceJson(config.interviewServiceUrl, "/api/v1/configure/approve", { body: { session_id: sessionId } }),
createAssignments: (payload: { organization_id: string; role: string; round: "warm_up" | "behavioral" | "technical"; assignee_emails: string[] }) =>
serviceJson(config.interviewServiceUrl, "/api/v1/interviews/assignments", { body: payload }),
listAssignments: (email: string, status = "pending", limit = 20) =>
serviceJson(config.interviewServiceUrl, `/api/v1/interviews/assignments?${new URLSearchParams({ email, status, limit: String(limit) })}`),
unassign: (payload: { organization_id: string; emails: string[] }) =>
serviceJson(config.interviewServiceUrl, "/api/v1/interviews/assignments/unassign", { body: payload }),
resultsBulk: (payload: JsonObject) =>
serviceJson(config.interviewServiceUrl, "/api/v1/interviews/results:bulk", { body: payload }),
review: (sessionId: string) => serviceJson(config.interviewServiceUrl, `/api/v1/review/${encodeURIComponent(sessionId)}`),
leaderboard: () => serviceJson(config.interviewServiceUrl, "/api/v1/leaderboard"),
artifact: (sessionId: string, artifactType: string) =>
serviceJson(config.interviewServiceUrl, `/api/v1/artifacts/${encodeURIComponent(sessionId)}/${encodeURIComponent(artifactType)}`),
createVideoUploadUrl: (sessionId: string, payload: JsonObject) =>
serviceJson(config.interviewServiceUrl, `/api/v1/sessions/${encodeURIComponent(sessionId)}/video/upload-url`, { body: payload }),
markVideoUploaded: (sessionId: string, payload: JsonObject) =>
serviceJson(config.interviewServiceUrl, `/api/v1/sessions/${encodeURIComponent(sessionId)}/video/uploaded`, { body: payload }),
};
export const roleplayService = {
health: () => serviceJson(config.roleplayServiceUrl, "/health"),
configure: (payload: JsonObject) => serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/configure", { body: payload }),
preview: (payload: JsonObject) => serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/configure/preview", { body: payload }),
editQuestions: (payload: { session_id: string; questions: Array<JsonObject | string> }) =>
serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/configure/questions", { body: payload }),
approve: (sessionId: string) =>
serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/configure/approve", { body: { session_id: sessionId } }),
createAssignments: (payload: { organization_id: string; name: string; scenario: string; assignee_emails: string[] }) =>
serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/assignments", { body: payload }),
listAssignments: (email: string, status = "pending", limit = 20) =>
serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/assignments?${new URLSearchParams({ email, status, limit: String(limit) })}`),
unassign: (payload: { organization_id: string; roleplay_id: string; emails: string[] }) =>
serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/assignments/unassign", { body: payload }),
resultsBulk: (payload: JsonObject) =>
serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/results:bulk", { body: payload }),
review: (sessionId: string) => serviceJson(config.roleplayServiceUrl, `/api/v1/roleplays/review/${encodeURIComponent(sessionId)}`),
leaderboard: () => serviceJson(config.roleplayServiceUrl, "/api/v1/roleplays/leaderboard"),
artifact: (sessionId: string, artifactType: string) =>
serviceJson(config.roleplayServiceUrl, `/api/v1/artifacts/${encodeURIComponent(sessionId)}/${encodeURIComponent(artifactType)}`),
createVideoUploadUrl: (sessionId: string, payload: JsonObject) =>
serviceJson(config.roleplayServiceUrl, `/api/v1/sessions/${encodeURIComponent(sessionId)}/video/upload-url`, { body: payload }),
markVideoUploaded: (sessionId: string, payload: JsonObject) =>
serviceJson(config.roleplayServiceUrl, `/api/v1/sessions/${encodeURIComponent(sessionId)}/video/uploaded`, { body: payload }),
};
export const resumeService = {
health: () => serviceJson(config.resumeServiceUrl, "/health"),
state: (clerkId: string) => serviceJson(config.resumeServiceUrl, `/api/state/${encodeURIComponent(clerkId)}`),
templates: () => serviceJson(config.resumeServiceUrl, "/api/v1/templates"),
task: (payload: { action?: string; params?: JsonObject; user_id: string; user_context?: JsonObject; session_start?: boolean }) =>
serviceJson(config.resumeServiceUrl, "/a2a/tasks", { body: payload }),
listResumes: (clerkId: string) => serviceJson(config.resumeServiceUrl, `/api/v1/resumes?${new URLSearchParams({ clerk_id: clerkId })}`),
createResume: (payload: JsonObject) => serviceJson(config.resumeServiceUrl, "/api/v1/resumes", { body: payload }),
getResume: (resumeId: string) => serviceJson(config.resumeServiceUrl, `/api/v1/resumes/${encodeURIComponent(resumeId)}`),
updateResume: (resumeId: string, payload: JsonObject) => serviceJson(config.resumeServiceUrl, `/api/v1/resumes/${encodeURIComponent(resumeId)}`, { method: "PUT", body: payload }),
analyzeResume: (resumeId: string, payload: JsonObject = {}) => serviceJson(config.resumeServiceUrl, `/api/v1/ai/analyze/${encodeURIComponent(resumeId)}`, { body: payload }),
suggestions: (resumeId: string) => serviceJson(config.resumeServiceUrl, `/api/v1/ai/suggestions/${encodeURIComponent(resumeId)}`),
copilot: (payload: JsonObject) => serviceJson(config.resumeServiceUrl, "/api/v1/ai/copilot", { body: payload }),
optimizeSummary: (payload: JsonObject) => serviceJson(config.resumeServiceUrl, "/api/v1/ai/optimize-summary", { body: payload }),
optimizeExperience: (payload: JsonObject) => serviceJson(config.resumeServiceUrl, "/api/v1/ai/optimize-experience", { body: payload }),
suggestSkills: (payload: JsonObject) => serviceJson(config.resumeServiceUrl, "/api/v1/ai/suggest-skills", { body: payload }),
generateSummary: (payload: JsonObject) => serviceJson(config.resumeServiceUrl, "/api/v1/ai/generate-summary", { body: payload }),
listVersions: (resumeId: string) => serviceJson(config.resumeServiceUrl, `/api/v1/resumes/${encodeURIComponent(resumeId)}/versions`),
createVersion: (resumeId: string, payload: JsonObject) => serviceJson(config.resumeServiceUrl, `/api/v1/resumes/${encodeURIComponent(resumeId)}/versions`, { body: payload }),
preview: (resumeId: string) => serviceJson(config.resumeServiceUrl, `/api/v1/export/resumes/${encodeURIComponent(resumeId)}/preview`),
};

View File

@@ -51,7 +51,7 @@ export async function prepareOpenCodeWorkflowModule(input: {
const promptText = await loadPrompt(input.module.promptPath);
const session = await withTimeout(client.createSession({ title: `${input.workflow.shortTitle}: ${input.module.title}` }), 8_000, "OpenCode session creation timed out");
await withTimeout(client.sendMessage({ sessionId: session.id, text: `${promptText}\n\nWorkflow: ${input.workflow.id}\nRun ID: ${input.runId}\nModule: ${input.module.id}\nGoal: ${input.goal ?? ""}\nWrite the final markdown artifact exactly to /workspace/${repoPath}.` }), 30_000, "OpenCode message timed out");
await withTimeout(client.sendMessage({ sessionId: session.id, text: `${promptText}\n\nWorkflow: ${input.workflow.id}\nRun ID: ${input.runId}\nModule: ${input.module.id}\nGoal: ${input.goal ?? ""}\nWrite the final markdown artifact exactly to /workspace/${repoPath}.` }), 90_000, "OpenCode message timed out");
const markdown = await readFile(abs, "utf8").catch(() => "");
const validation = validateArtifactMarkdown(markdown);

View File

@@ -11,9 +11,9 @@ export type ServiceCapability = {
export function listServiceCapabilities(): ServiceCapability[] {
return [
{ id: "resume-service", name: "Resume Agent", enabled: Boolean(config.resumeServiceUrl), internalUrl: config.resumeServiceUrl, publicUrl: config.resumePublicUrl, operations: ["resume.analyze", "resume.tailor"] },
{ id: "interview-service", name: "Sara Interview", enabled: Boolean(config.interviewServiceUrl), internalUrl: config.interviewServiceUrl, publicUrl: config.interviewPublicUrl, operations: ["interview.configure", "interview.practice"] },
{ id: "roleplay-service", name: "Emily Roleplay", enabled: Boolean(config.roleplayServiceUrl), internalUrl: config.roleplayServiceUrl, publicUrl: config.roleplayPublicUrl, operations: ["roleplay.configure", "roleplay.practice"] },
{ id: "resume-service", name: "Resume Agent", enabled: Boolean(config.resumeServiceUrl), internalUrl: config.resumeServiceUrl, publicUrl: config.resumePublicUrl, operations: ["resume.state", "resume.templates", "resume.a2aTask", "resume.create", "resume.update", "resume.analyze", "resume.suggestions", "resume.copilot", "resume.optimizeSummary", "resume.optimizeExperience", "resume.suggestSkills", "resume.generateSummary", "resume.versions", "resume.preview"] },
{ id: "interview-service", name: "Sara Interview", enabled: Boolean(config.interviewServiceUrl), internalUrl: config.interviewServiceUrl, publicUrl: config.interviewPublicUrl, operations: ["interview.configure", "interview.preview", "interview.questions", "interview.approve", "interview.assignments", "interview.unassign", "interview.resultsBulk", "interview.review", "interview.leaderboard", "interview.artifacts", "interview.videoUpload", "interview.practice"] },
{ id: "roleplay-service", name: "Emily Roleplay", enabled: Boolean(config.roleplayServiceUrl), internalUrl: config.roleplayServiceUrl, publicUrl: config.roleplayPublicUrl, operations: ["roleplay.configure", "roleplay.preview", "roleplay.questions", "roleplay.approve", "roleplay.assignments", "roleplay.unassign", "roleplay.resultsBulk", "roleplay.review", "roleplay.leaderboard", "roleplay.artifacts", "roleplay.videoUpload", "roleplay.practice"] },
{ id: "qscore-service", name: "Quinn Q Score", enabled: Boolean(config.qscoreServiceUrl), internalUrl: config.qscoreServiceUrl, operations: ["qscore.ingest", "qscore.compute"] },
{ id: "opencode", name: "OpenCode Artifact Executor", enabled: true, operations: ["artifact.prepare", "artifact.generate"] },
];