refactor(backend): rename workflows to missions in agent-facing tool descriptions and prompts

This commit is contained in:
-Puter
2026-06-03 15:26:30 +05:30
parent 289f6f7844
commit a1654d23b4

View File

@@ -1,12 +1,13 @@
import { Hono } from "hono";
import { z } from "zod";
import { createClient, type Client } from "rivetkit/client";
import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from "ai";
import { convertToModelMessages, generateText, stepCountIs, streamText, tool, type UIMessage } from "ai";
import { config } from "../config.js";
import { requireUser, type AuthContext } from "../auth/clerk.js";
import type { Registry } from "../actors/registry.js";
import { getConversationModel } from "../actors/conversation/agent.js";
import { listWorkflowDefinitions } from "../workflows/registry.js";
import { getSubAgentModules } from "../lib/prompt-loader.js";
let _client: Client<Registry> | null = null;
function getClient(): Client<Registry> {
@@ -45,16 +46,99 @@ function textFromMessage(message: UIMessage | undefined) {
}
function buildSystemPrompt() {
return `You are GrowQR's Grow Agent conversation layer.
You help users discover career workflows, start and manage workflows, remember durable career context, and surface actionable missions.
Be concise, practical, and product-native. When useful, call tools so the dashboard can render generative UI cards.
Use discoverWorkflows for workflow discovery, showMissions for next actions, startWorkflow to begin a workflow, and memory tools for durable profile details.`;
return `You are Grow, the warm, encouraging career companion inside GrowQR. You are part coach, part strategist, and part cheerleader.
Personality & Tone:
- Be warm, empathetic, and genuinely helpful. Celebrate small wins. Acknowledge frustrations.
- Use a conversational tone — like a smart friend who happens to know a lot about careers.
- Be concise but never cold. Use "we" and "you" naturally. Add warmth: "That's a great goal!", "You've got this", "Let's figure this out together."
- When the user is stressed, be calming and reassuring. When they're excited, match their energy.
- Prefer markdown with short headings, bullets, and checklists for readable streamed answers.
Nomenclature (important):
- When talking to the user, ALWAYS say "missions" instead of "workflows". The backend calls them workflows; the user sees them as missions.
- When talking to the user, ALWAYS say "specialists" or "coaches" instead of "sub-agents".
- When talking to the user, say "features" for interview, roleplay, resume, and Q Score tools.
How to help:
1. DISCOVER — Understand what the user wants *before* recommending. Ask 1-2 clarifying questions if their intent is ambiguous.
2. REMEMBER — Use memory tools (readMemory, searchMemory, writeMemory) to store durable context: goals, deadlines, companies, interview dates, skills. Always confirm when you save something.
3. MISSIONS — Use discoverWorkflows or inspectWorkflowRegistry to show relevant missions. Only start one with startWorkflow when the user explicitly agrees.
4. MISSIONS — Use showMissions to surface the next 1-3 actionable steps based on their current context.
5. SPECIALISTS — Use listAgentRegistry to show available specialists, then askSubAgent to route focused questions to Sara (interview), Emily (roleplay), Quinn (Q Score), or Rhea (resume).
6. BE HONEST — If you don't know something, say so. If a tool fails, acknowledge it and try a different approach.
Tool usage rules:
- Only call tools when they genuinely help the user. Don't call tools for every response.
- If the user asks about their memory, use searchMemory or readMemory first.
- If the user asks about missions, use discoverWorkflows or inspectWorkflowRegistry.
- If the user wants interview prep, salary negotiation, or resume help, route to the right specialist via askSubAgent.
- When you write memory, confirm what you saved: "Got it — I've saved that you're targeting PM roles at Stripe and Notion."
- When you start a mission, celebrate: "🎉 Let's do this! Starting your Interview-to-Offer mission now."`;
}
function safeAgentRegistry() {
try {
return getSubAgentModules();
} catch {
return [
{ id: "sara", name: "Sara", role: "Interview Coach", service: "interview-service", description: "Warm, direct interview practice coach for behavioral and technical interview prep.", toolNames: ["start_interview_session"] },
{ id: "emily", name: "Emily", role: "Roleplay Coach", service: "roleplay-service", description: "High-empathy roleplay partner for negotiation, recruiter, manager, and stakeholder conversations.", toolNames: ["start_roleplay_session"] },
{ id: "qscore", name: "Quinn", role: "Q Score Analyst", service: "qscore-service", description: "Analytical readiness scorer that turns profile signals into concrete score-improvement moves.", toolNames: ["compute_qscore"] },
{ id: "resume", name: "Rhea", role: "Resume Strategist", service: "resume-service", description: "ATS-aware resume strategist for sharper bullets, positioning, and role fit.", toolNames: ["optimize_resume"] },
];
}
}
const SUB_AGENT_PROMPTS: Record<string, string> = {
sara: `You are Sara, GrowQR's interview coach. You're warm, direct, and relentlessly practical. You believe practice beats perfection.
When helping:
- Give one mock question at a time, then wait for the user's answer before giving feedback.
- Use STAR format (Situation, Task, Action, Result) for behavioral questions.
- For technical questions, explain the concept, then give a follow-up question.
- End every response with ONE specific next drill the user should do.
- Be encouraging: "That's a solid start — here's how to make it unforgettable."
Return compact, actionable markdown.`,
emily: `You are Emily, GrowQR's roleplay coach. You're empathetic, candid, and a little theatrical — you make rehearsing feel like a conversation, not a chore.
When helping:
- Script realistic dialogue with the user's actual words and objections.
- For salary negotiation: practice the ask, the counter, and the graceful close.
- For recruiter conversations: prep the "tell me about yourself" and the "why are you leaving" answers.
- For manager/stakeholder conversations: frame the user's goal, anticipate pushback, and script responses.
- End with one confidence-building rehearsal step.
Return compact, dialogue-heavy markdown.`,
qscore: `You are Quinn, GrowQR's Q Score analyst. You're analytical, diagnostic, and optimistic — you see scores as starting points, not verdicts.
When helping:
- Explain what the Q Score measures and why it matters.
- Identify the 2-3 dimensions with the biggest improvement potential.
- Give specific, time-bound actions for each dimension.
- Use numbers and percentages when possible.
- Frame weaknesses as "growth edges" — opportunities, not flaws.
- End with a prioritized improvement plan.
Return compact, data-informed markdown.`,
resume: `You are Rhea, GrowQR's resume strategist. You're ATS-aware, outcome-focused, and slightly obsessed with strong verbs and metrics.
When helping:
- Rewrite bullets using the X-Y-Z formula: "Accomplished X as measured by Y by doing Z."
- Align experience to target roles by emphasizing relevant skills and outcomes.
- Flag gaps (missing skills, weak bullets, unclear impact) and suggest fixes.
- For headlines: make them specific and role-aligned.
- For summaries: 2-3 lines that hook the reader.
- End with one concrete next step the user should take.
Return compact, bullet-heavy markdown.`,
};
function buildConversationTools(userId: string) {
return {
discoverWorkflows: tool({
description: "Return sellable GrowQR workflows as UI-ready discovery cards.",
description: "Return sellable GrowQR missions as UI-ready discovery cards.",
inputSchema: z.object({
segment: z.string().optional().describe("Optional user segment or goal to filter/rank by."),
}),
@@ -80,6 +164,72 @@ function buildConversationTools(userId: string) {
}),
}),
inspectWorkflowRegistry: tool({
description: "Inspect the GrowQR mission registry with module-level detail.",
inputSchema: z.object({
workflowId: z.string().optional(),
}),
execute: async ({ workflowId }) => {
const workflows = listWorkflowDefinitions()
.filter((workflow) => !workflowId || workflow.id === workflowId)
.map((workflow) => ({
id: workflow.id,
title: workflow.title,
shortTitle: workflow.shortTitle,
promise: workflow.promise,
priceTier: workflow.priceTier,
estimatedDuration: workflow.estimatedDuration,
moduleCount: workflow.modules.length,
modules: workflow.modules.map((module) => ({
id: module.id,
title: module.title,
role: module.role,
description: module.description,
service: module.service,
})),
}));
return { kind: "workflow-registry", workflowId, workflows };
},
}),
listAgentRegistry: tool({
description: "List available GrowQR specialist subagents and their tools.",
inputSchema: z.object({}),
execute: async () => ({
kind: "agent-registry",
agents: safeAgentRegistry().map((agent) => ({
id: agent.id,
name: agent.name,
role: agent.role,
description: agent.description,
service: agent.service,
tools: agent.toolNames,
})),
}),
}),
askSubAgent: tool({
description: "Route a focused question to one of four personified specialists: sara/interview, emily/roleplay, quinn/qscore, or rhea/resume.",
inputSchema: z.object({
agentId: z.enum(["sara", "emily", "qscore", "resume"]),
question: z.string(),
context: z.string().optional(),
}),
execute: async ({ agentId, question, context }) => {
const agent = safeAgentRegistry().find((item) => item.id === agentId) ?? { id: agentId, name: agentId, role: agentId, description: "GrowQR specialist", service: undefined, toolNames: [] };
const answer = await generateText({
model: getConversationModel(),
system: `${SUB_AGENT_PROMPTS[agentId]}\n\nReturn compact markdown. Include: 1) diagnosis, 2) recommended response or plan, 3) next action.`,
prompt: `User question:\n${question}\n\nContext:\n${context ?? "No extra context provided."}`,
});
return {
kind: "subagent-response",
agent: { id: agent.id, name: agent.name, role: agent.role, service: agent.service },
answerMd: answer.text,
};
},
}),
showMissions: tool({
description: "Return UI-ready career missions for the user's dashboard.",
inputSchema: z.object({
@@ -92,13 +242,13 @@ function buildConversationTools(userId: string) {
{
id: "mission-share-goal",
title: "Tell Grow what you are aiming for",
description: "Share target role, company, deadline, and biggest blocker so the agent can set up your first workflow.",
description: "Share target role, company, deadline, and biggest blocker so the agent can set up your first mission.",
status: "suggested",
rewardLabel: "+10 readiness",
},
{
id: "mission-pick-workflow",
title: "Pick a workflow to activate",
title: "Pick a mission to activate",
description: "Start Interview-to-Offer if you have an interview coming up, or Career Transition if you are exploring a pivot.",
status: "suggested",
rewardLabel: "Unlock plan",
@@ -115,36 +265,68 @@ function buildConversationTools(userId: string) {
}),
startWorkflow: tool({
description: "Start one of the GrowQR workflows for this user.",
description: "Start one of the GrowQR missions for this user. Only call this when the user explicitly agrees to start a mission.",
inputSchema: z.object({
workflowId: z.string().describe("Workflow id, e.g. interview-to-offer."),
goal: z.string().optional(),
goal: z.string().optional().describe("Optional goal text to personalize the mission."),
}),
execute: async ({ workflowId, goal }) => {
const handle = workflowFor(userId);
await handle.init({ userId });
const state = await handle.startWorkflow({ workflowId, goal });
return {
kind: "workflow-started",
workflowId,
goal,
status: state.workflowStatus,
runId: state.workflowRunId,
title: listWorkflowDefinitions().find((workflow) => workflow.id === workflowId)?.title ?? workflowId,
};
try {
const handle = workflowFor(userId);
await handle.init({ userId });
const state = await handle.startWorkflow({ workflowId, goal });
const workflow = listWorkflowDefinitions().find((w) => w.id === workflowId);
return {
kind: "workflow-started",
workflowId,
goal,
status: state.workflowStatus,
runId: state.workflowRunId,
title: workflow?.title ?? workflowId,
};
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to start mission";
return {
kind: "workflow-started",
workflowId,
goal,
status: "error",
error: errorMessage,
title: listWorkflowDefinitions().find((w) => w.id === workflowId)?.title ?? workflowId,
};
}
},
}),
getActiveWorkflow: tool({
description: "Return the active workflow snapshot for this user.",
description: "Return the active mission snapshot for this user. Use this when the user asks 'what am I working on' or 'what's my current mission'.",
inputSchema: z.object({}),
execute: async () => {
const handle = workflowFor(userId);
await handle.init({ userId });
return { kind: "active-workflow", workflow: await handle.getWorkflowStatus() };
try {
const handle = workflowFor(userId);
await handle.init({ userId });
return { kind: "active-workflow", workflow: await handle.getWorkflowStatus() };
} catch (err) {
return { kind: "active-workflow", workflow: null, error: err instanceof Error ? err.message : "Could not fetch workflow" };
}
},
}),
listMemory: tool({
description: "List memory files for this user by path prefix.",
inputSchema: z.object({ prefix: z.string().optional() }),
execute: async ({ prefix }) => ({
kind: "memory-list",
memories: await memoryFor(userId).list(prefix ?? "/"),
}),
}),
memoryStats: tool({
description: "Get lightweight stats about the user's memory store.",
inputSchema: z.object({}),
execute: async () => ({ kind: "memory-stats", stats: await memoryFor(userId).getStats() }),
}),
readMemory: tool({
description: "Read a markdown memory file for this user.",
inputSchema: z.object({ path: z.string() }),
@@ -172,6 +354,19 @@ function buildConversationTools(userId: string) {
return { kind: "memory-written", path, queued: result.queued };
},
}),
appendMemory: tool({
description: "Append markdown content to a durable memory file without overwriting it.",
inputSchema: z.object({
path: z.string(),
contentMd: z.string(),
tags: z.array(z.string()).optional(),
}),
execute: async ({ path, contentMd, tags }) => {
const result = await memoryFor(userId).append({ path, contentMd, tags });
return { kind: "memory-appended", path, queued: result.queued };
},
}),
};
}