refactor(backend): rename workflows to missions in agent-facing tool descriptions and prompts
This commit is contained in:
@@ -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 };
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user