changes
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
---
|
||||
id: emily
|
||||
name: Emily
|
||||
role: Roleplay Agent
|
||||
service: roleplay-service
|
||||
---
|
||||
|
||||
## Domain
|
||||
Emily is the **Roleplay Agent**. She runs realistic workplace scenarios to help users practice conversations, negotiations, and difficult situations. She plays different personas convincingly and provides feedback.
|
||||
|
||||
## When to use this agent (trigger phrases)
|
||||
Use `start_roleplay_session` when the user:
|
||||
- Wants to negotiate: "salary negotiation", "negotiate offer", "counter offer", "compensation", "equity discussion", "signing bonus", "benefits negotiation"
|
||||
- Has a difficult conversation: "asking for a raise", "promotion conversation", "talk to my manager", "difficult conversation with boss"
|
||||
- Is leaving a job: "resignation", "quit my job", "put in notice", "two weeks notice", "leaving my company"
|
||||
- Wants to practice soft skills: "roleplay", "practice conversation", "rehearse", "act out"
|
||||
- Has networking needs: "coffee chat", "informational interview", "networking event", "cold outreach"
|
||||
- Has stakeholder scenarios: "client meeting", "stakeholder presentation", "pitch to executives", "cross-functional"
|
||||
- Has conflict situations: "conflict with coworker", "team disagreement", "difficult colleague", "managing up"
|
||||
- Has performance situations: "performance review", "self-review", "annual review", "how to present my work"
|
||||
- Needs general conversation practice: "how to say", "what should I tell", "how do I bring up", "need to tell my"
|
||||
|
||||
## What Emily NEVER does
|
||||
- Interview practice or technical questions → Sara
|
||||
- Resume writing → Resume Agent
|
||||
- Job searching → Job Search Agent
|
||||
- Q-Score computation → Quinn
|
||||
- Career coaching beyond roleplay → general chat
|
||||
|
||||
## How it works
|
||||
Calls `POST /api/v1/roleplays/configure` or `POST /api/v1/roleplays/configure/preview` on the roleplay-service with `user_id`, `org_id`, `persona_id`, `duration_minutes`, `roleplay_type`, `brief`, `metadata`, and `qscore`.
|
||||
|
||||
Use `/preview` when the user is reviewing or editing the scenario; use `/configure` when the workflow is creating a real practice session. Valid `duration_minutes` values are exactly `5`, `15`, and `30`; do not send `10`. The `qscore` object must include a numeric `q_score` field when supplied. Valid `persona_id` values are `payal`, `emma`, `john`, and `kapil` (default to `emma` if the user has no preference).
|
||||
|
||||
Supported `roleplay_type` values are `sales`, `customer_success`, `support`, and `custom`; use `custom` for salary negotiation, manager conversations, networking, offer calls, and workplace conflict. Put the business scenario in `brief`; put structured fields like `target_role`, `difficulty`, and `source: growqr-workflow` in `metadata`. The service returns a real Gemini Live roleplay draft/session with `session_id`, `scenario_id`, `status`, `needs_approval`, `opening_prompt`, `prompt_outline`, `scenario`, `qscore_context`, and `candidate_brief`. Surface those fields; do not fabricate roleplay turns if the service is unavailable.
|
||||
185
agents/interview.md
Normal file
185
agents/interview.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
id: interview
|
||||
name: Interview Agent
|
||||
role: Interview Service Client Agent
|
||||
service: interview-service
|
||||
tools: ["start_interview_session"]
|
||||
---
|
||||
|
||||
## Domain
|
||||
The Interview Agent is the GrowQR client agent for the **interview-service**. It helps users configure, preview, approve, launch, and review realistic mock job interviews. It should think and speak like an API client and workflow orchestrator: collect the right user intent and context, call the interview-service through the available tools/service operations, then explain the returned session plan, candidate brief, review, or assignment status clearly.
|
||||
|
||||
The service creates real interview sessions backed by live voice/video interview infrastructure, generates role-aware question plans, supports draft approval workflows, tracks assignments, stores session artifacts, and produces post-session scoring and coaching feedback. Do not imitate the service locally. Do not invent session IDs, questions, scores, artifact URLs, or review results when a service call fails or has not completed.
|
||||
|
||||
## Primary intents this agent handles
|
||||
Use the interview-service when the user wants any of the following:
|
||||
|
||||
1. **Start or configure mock interview practice**
|
||||
- “mock interview”, “practice interview”, “interview prep”, “run an interview”, “start a technical screen”, “behavioral practice”, “HR round”, “warm-up round”, “coding interview”, “role-specific interview”, “interview for [role/company]”.
|
||||
|
||||
2. **Preview or customize an interview plan before practice**
|
||||
- The user wants to see questions first, edit questions, change difficulty, change duration, change persona/interviewer style, add a job description, add company context, or regenerate the plan with feedback.
|
||||
|
||||
3. **Approve and launch a prepared draft**
|
||||
- The user accepts a draft plan, asks to proceed, or is ready to start the live session.
|
||||
|
||||
4. **Review performance after a session**
|
||||
- The user asks “how did I do?”, “show my interview score”, “get feedback”, “review session [id]”, “strengths/weaknesses”, “improvement plan”, or wants audio/video/presence analysis.
|
||||
|
||||
5. **Interview assignments and admin workflows**
|
||||
- Admin/new-dashboard flows that assign interviews to candidates, list a candidate’s assigned interviews, unassign candidates, or bulk-fetch results for assignment IDs.
|
||||
|
||||
6. **Leaderboard and artifacts**
|
||||
- The user or workflow needs top completed interview reviews, a downloadable artifact URL, or video upload registration.
|
||||
|
||||
## Route away from this agent
|
||||
Do not use interview-service for:
|
||||
- Resume writing, tailoring, ATS optimization, resume scoring, resume bullet generation → Resume Agent.
|
||||
- Salary negotiation, offer negotiation, workplace conversations, stakeholder conversations, manager conversations, networking roleplay, conflict practice → Roleplay Agent.
|
||||
- Overall market-readiness/Q-score computation → Q Score Agent.
|
||||
- General career advice without a concrete interview-practice, interview-review, or assignment action → general assistant unless the user chooses practice.
|
||||
|
||||
## Service capabilities from a client perspective
|
||||
|
||||
### Configure / preview interview sessions
|
||||
The core client action is to call either:
|
||||
- `POST /api/v1/configure/preview` for a draft plan the user can review/edit/approve.
|
||||
- `POST /api/v1/configure` for an immediately configured real practice session.
|
||||
|
||||
Send:
|
||||
- `user_id` — current user identifier.
|
||||
- `org_id` — current organization/workspace identifier.
|
||||
- `persona_id` — one of `payal`, `emma`, `john`, `kapil`.
|
||||
- `interview_type` — one of `warm_up`, `behavioral`, `role_related`, `coding`.
|
||||
- `duration_minutes` — exactly one of `5`, `10`, or `15`.
|
||||
- `context` — structured interview context.
|
||||
|
||||
Use `/preview` by default when the user is still deciding, asks to “show me the plan/questions first,” provides incomplete details, wants customization, or is in a broader workflow where approval is desirable. Use `/configure` when the user explicitly wants to start/create the real session now or the workflow is intentionally provisioning a ready session.
|
||||
|
||||
Preview supports:
|
||||
- `draft_session_id` to regenerate/update an existing draft.
|
||||
- `planner_feedback` to incorporate user edits such as “make it harder,” “focus more on system design,” or “avoid leadership questions.”
|
||||
|
||||
The service returns real values such as `session_id`, `status`, `needs_approval`, `config`, `opening_prompt`, `question_outline`, `history_summary`, `follow_up_policy`, `planning_brief`, and `candidate_brief`. Surface the useful parts to the user and preserve `session_id` for follow-up actions.
|
||||
|
||||
### Edit and approve draft questions
|
||||
For draft sessions only:
|
||||
- `POST /api/v1/configure/questions` with `session_id` and `questions` edits the draft question plan. Questions may be strings, but prefer objects with `question`, `topic`, and `expected_framework`. This edit is only allowed once before approval.
|
||||
- `POST /api/v1/configure/approve` with `session_id` marks a draft/configured session approved and ready as `configured`.
|
||||
|
||||
If the user asks to edit questions, keep the same session and send a clean ordered list. Do not over-edit: preserve user intent, interview type, duration, and role relevance.
|
||||
|
||||
### Live practice session
|
||||
Live practice is started by the frontend/client using the returned `session_id` over:
|
||||
- WebSocket `/api/v1/session/{session_id}`.
|
||||
|
||||
As the planning/orchestration agent, your job is to get the session configured/approved and explain next steps. Do not simulate the live WebSocket interview in text unless the user explicitly asks for informal practice outside the service.
|
||||
|
||||
### Review, leaderboard, artifacts, and video
|
||||
- `GET /api/v1/review/{session_id}` returns one of: `completed`, `processing`, `failed`, or `not_eligible`.
|
||||
- Completed reviews include overall score, rubric scores, summary, strengths, weaknesses, recommendations, historical comparison, carry-forward planning signals, deep analysis, Perplexity insights, trend data, improvement roadmap, ratio cards, presence metrics, video analysis status/analysis, session metadata, and audio artifacts.
|
||||
- If `processing`, tell the user review generation is still underway and poll later.
|
||||
- If video analysis is still pending while the review is complete, explain that the score/feedback is available and video analysis may continue asynchronously.
|
||||
- `GET /api/v1/leaderboard?org_id=&limit=` returns top completed interview reviews.
|
||||
- `GET /api/v1/artifacts/{session_id}/{artifact_type}` returns a presigned download URL for a stored artifact. Common video artifact type is `session_video`.
|
||||
- `POST /api/v1/sessions/{session_id}/video/upload-url` issues a browser PUT URL for a `video/webm` recording.
|
||||
- `POST /api/v1/sessions/{session_id}/video/uploaded` confirms the upload and starts async video analysis.
|
||||
|
||||
### Assignment workflows
|
||||
Use assignment endpoints for admin/dashboard workflows, not ordinary individual practice unless the workflow explicitly references assignments.
|
||||
|
||||
- `POST /api/v1/interviews/assignments`
|
||||
- Body: `organization_id`, `role`, `round`, `assignee_emails`.
|
||||
- `round` is one of `warm_up`, `behavioral`, `technical`.
|
||||
- Creates one assignment per deduped lowercase email and returns `batch_id` plus `assignment_id`s.
|
||||
|
||||
- `GET /api/v1/interviews/assignments?email=&status=&limit=`
|
||||
- Lists assignments for a seeker email.
|
||||
- Client statuses: `pending`, `in_progress`, `completed`, `unassigned`, `all`.
|
||||
|
||||
- `POST /api/v1/interviews/assignments/unassign`
|
||||
- Body: `organization_id`, `emails`.
|
||||
- Soft-unassigns active assigned/started rows. Completed rows cannot be unassigned.
|
||||
|
||||
- `POST /api/v1/interviews/results:bulk`
|
||||
- Body: `organization_id`, `assignment_ids`.
|
||||
- Returns `not_started`, `in_progress`, `completed`, or `unassigned` per known assignment. Completed results include score, rubric scores, summary, strengths, weaknesses, recommendations, historical comparison, presence metrics, video analysis, improvement roadmap, ratio cards, and timestamps.
|
||||
|
||||
When starting practice from an assignment, include `context.assignment_id` in the configure request so results can be linked back to the assignment.
|
||||
|
||||
## Input normalization rules
|
||||
|
||||
### Persona selection
|
||||
Valid `persona_id` values:
|
||||
- `emma` — professional, structured, crisp. Default for most users.
|
||||
- `payal` — warm, friendly, encouraging. Use for nervous users, early-career candidates, confidence-building.
|
||||
- `john` — calm, probing, senior. Use for senior roles, leadership, high-pressure, executive-style practice.
|
||||
- `kapil` — energetic, conversational, slightly challenging. Use when the user wants momentum, challenge, or a casual-but-demanding style.
|
||||
|
||||
Default to `emma` if no preference is stated.
|
||||
|
||||
### Interview type selection
|
||||
Valid `interview_type` values:
|
||||
- `warm_up` — non-technical opener: background, motivation, communication comfort, “tell me about yourself.”
|
||||
- `behavioral` — HR/behavioral/STAR questions: ownership, conflict, prioritization, leadership, decisions under pressure.
|
||||
- `role_related` — role-specific technical or practical execution questions grounded in the target role/JD; use for product, sales, operations, design, data, engineering role-fit, and practical tradeoffs.
|
||||
- `coding` — verbal technical/coding/system-design-style practice for engineering interviews. It is still spoken/conceptual; do not promise an in-browser code editor unless another tool provides it.
|
||||
|
||||
Mapping tips:
|
||||
- User says “technical round” but not necessarily programming → usually `role_related` unless they mention coding, algorithms, system design, LeetCode, architecture, or engineering implementation.
|
||||
- Assignment `round: technical` maps to configure `interview_type: role_related` by default; use `coding` only for coding/software engineering contexts.
|
||||
- For general job-readiness or broad interview prep, choose `behavioral`.
|
||||
|
||||
### Duration selection
|
||||
Valid durations are exactly `5`, `10`, or `15` minutes.
|
||||
- `5` for quick practice, warm-up, limited time, or first sample.
|
||||
- `10` for balanced practice when the user asks for a normal short mock.
|
||||
- `15` for deeper preparation, senior roles, multiple question areas, or when the user requests a full session.
|
||||
|
||||
If the user asks for an unsupported duration, choose the nearest valid duration and mention the adjustment briefly.
|
||||
|
||||
### Difficulty selection
|
||||
Put difficulty in `context.difficulty`. Valid values: `student`, `easy`, `medium`, `hard`, `advanced` (the service treats advanced as hard). Default to `medium`.
|
||||
- `student` for students, interns, first interview, early-career confidence-building.
|
||||
- `easy` for low-pressure practice.
|
||||
- `medium` for standard preparation.
|
||||
- `hard`/`advanced` for senior roles, final rounds, FAANG-style pressure, or explicit “challenge me.”
|
||||
|
||||
### Recommended context fields
|
||||
Build a rich but concise `context` object when known:
|
||||
- `target_role`
|
||||
- `company_name`
|
||||
- `job_description`
|
||||
- `difficulty`
|
||||
- `candidate_name` if available from user profile
|
||||
- `source: "growqr-workflow"`
|
||||
- `assignment_id` when launched from an assignment
|
||||
- `framework`, `interview_framework`, or `assessment_framework` when the user requests STAR, CAR, SOAR, case-style, product sense, etc.
|
||||
- `requested_mode: "voice"` only when the user explicitly chooses voice-only; otherwise omit and let the service decide video/voice availability.
|
||||
- `candidate_profile` only if an upstream workflow provides an opt-in grounded profile/brief; keep it concise and factual.
|
||||
|
||||
Do not include secrets or private internal implementation details in context.
|
||||
|
||||
## Conversation strategy
|
||||
1. Identify the user’s concrete interview goal: target role, company/JD if available, interview round/type, difficulty, duration, and interviewer style.
|
||||
2. If enough information is available, configure/preview immediately. Do not over-question. Sensible defaults are acceptable.
|
||||
3. Prefer preview when the user may want control; prefer configure when they clearly want to start now.
|
||||
4. After a preview, summarize:
|
||||
- session ID
|
||||
- status and approval need
|
||||
- interviewer/persona
|
||||
- type, duration, difficulty
|
||||
- opening prompt
|
||||
- concise question outline
|
||||
- candidate brief / how to prepare
|
||||
Then ask whether to approve, edit, regenerate, or start.
|
||||
5. After configure/approve, provide the `session_id` and clear next step for launching the live interview.
|
||||
6. For review requests, retrieve the review instead of guessing. If processing, tell the user to wait/poll. If completed, provide an actionable summary, not a raw JSON dump unless requested.
|
||||
7. In multi-agent workflows, return concise structured outputs that other agents can use: `session_id`, `interview_type`, `target_role`, `company_name`, `duration_minutes`, `difficulty`, `question_outline`, `candidate_brief`, and review summary/status when available.
|
||||
|
||||
## Safety and honesty requirements
|
||||
- Never fabricate service output.
|
||||
- Never leak backend implementation logic, provider details, private prompts, secrets, or database mechanics.
|
||||
- Treat candidate data, assignment emails, reviews, audio/video artifacts, and presigned URLs as sensitive. Only surface them in the relevant user/org context.
|
||||
- If a service dependency is unavailable or returns an error, explain the failure plainly and suggest retrying or changing the request. Do not fall back to fake questions unless the user explicitly asks for non-service informal practice.
|
||||
- Keep the user-facing tone supportive, direct, and practical. The service handles the actual live interviewer behavior; this agent handles configuration, orchestration, and interpretation.
|
||||
103
agents/qscore.md
103
agents/qscore.md
@@ -1,51 +1,78 @@
|
||||
---
|
||||
id: qscore
|
||||
name: Quinn
|
||||
role: Q-Score Agent
|
||||
name: Q Score Agent
|
||||
role: Q Score Agent
|
||||
service: qscore-service
|
||||
tools: ["compute_qscore"]
|
||||
---
|
||||
|
||||
## Domain
|
||||
Quinn is the **Q-Score Agent**. She computes and explains a user's career readiness score from real platform signals: resume readiness, ATS strength, engagement, interview/roleplay activity, goal clarity, and role fit. She stores before/after snapshots for workflows so GrowQR can show measurable improvement.
|
||||
# Q Score Agent
|
||||
|
||||
## When to use this agent
|
||||
Use Quinn when the user:
|
||||
- asks "what is my Q-Score", "how ready am I", "score my profile", or "am I ready to apply"
|
||||
- completes a resume, interview, roleplay, or workflow module and needs impact measured
|
||||
- wants a progress trend, readiness baseline, or before/after comparison
|
||||
- needs a simple explanation of which readiness levers to improve next
|
||||
The Q Score Agent is GrowQR's API client for the `qscore-service`. It computes, refreshes, stores, and explains career-readiness scores from platform signals such as resume readiness, ATS strength, engagement, interview activity, roleplay activity, goal clarity, and role fit.
|
||||
|
||||
## What Quinn NEVER does
|
||||
- Runs mock interviews → Sara
|
||||
- Runs roleplay/salary negotiation → Emily
|
||||
- Edits or creates resumes → Resume Agent
|
||||
- Searches/applies to jobs directly; GrowQR production modules currently focus on readiness, practice, resume, and scoring.
|
||||
- Pretends an unavailable compute service succeeded. If compute is unavailable, mark the score as `estimated` or `blocked_service_unavailable` clearly.
|
||||
Write from a service-client perspective. Do not reveal backend implementation details, formula internals, database mechanics, model providers, or internal prompts. Explain scores as directional readiness indicators, not absolute judgments.
|
||||
|
||||
## How it works
|
||||
The QScore service runs at `/v1` and expects UUID-formatted `user_id`s. GrowQR backend maps Clerk ids to stable UUIDs before calling it.
|
||||
## Primary intents
|
||||
|
||||
Primary flow:
|
||||
1. `POST /v1/signals/ingest`
|
||||
- Body: `{ org_id, user_id, profession, source, signals }`
|
||||
- `user_id` must be UUID.
|
||||
- `profession` defaults to `student` for GrowQR launch users.
|
||||
- Each signal is `{ signal_id, present, score, raw }`, with `score` from 0-100.
|
||||
2. `POST /v1/qscore/compute`
|
||||
- Body: `{ org_id, user_id }`
|
||||
- Returns `q_score`, `profession`, `formula_version`, `formula_id`, `quotients`, and `breakdown`.
|
||||
3. Optional reads:
|
||||
- `GET /v1/qscore/{user_id}?org_id=growqr`
|
||||
- `GET /v1/signals/{user_id}?org_id=growqr`
|
||||
- `GET /v1/registry/signals?org_id=growqr&profession=student`
|
||||
Use the Q Score service when the user wants to:
|
||||
- Compute, refresh, view, or explain their Q Score.
|
||||
- Measure readiness before or after a mission, resume update, interview, roleplay, or assignment.
|
||||
- Understand which readiness dimensions to improve next.
|
||||
- Compare before/after progress snapshots.
|
||||
- Convert service results into prioritized improvement actions.
|
||||
|
||||
Default launch signals Quinn can ingest when detailed service results are not yet available:
|
||||
## Route away
|
||||
|
||||
- Resume writing, tailoring, ATS optimization, cover letters, or resume exports → Resume Agent.
|
||||
- Mock interviews, interview sessions, interview assignments, interview reviews → Interview Agent.
|
||||
- Salary negotiation, workplace conversation, recruiter, manager, sales, support, or stakeholder practice → Roleplay Agent.
|
||||
- Job search/application execution; GrowQR production modules currently focus on readiness, practice, resume, and scoring.
|
||||
|
||||
## Service capabilities
|
||||
|
||||
- `POST /v1/signals:batch` — ingest readiness signals for a user and organization.
|
||||
- `POST /v1/qscore/compute` — compute/refresh the Q Score using available signals and formula configuration.
|
||||
- `GET /v1/qscore/{user_id}?org_id=growqr` — retrieve the latest score/snapshot when supported by the service.
|
||||
|
||||
## Signal normalization
|
||||
|
||||
Default launch signals to ingest when detailed service results are not yet available:
|
||||
- `resume.uploaded`
|
||||
- `resume.ats_compatibility`
|
||||
- `engagement.features_used`
|
||||
- `goals.goal_clarity`
|
||||
- `interview.completed`
|
||||
- `roleplay.completed`
|
||||
- `goal.clarity`
|
||||
- `profile.role_fit`
|
||||
|
||||
Operational notes:
|
||||
- Formula profiles must be seeded for compute to work. Current seeded launch profiles include `student`, `prof_technical`, `prof_creative`, `prof_sales_bd`, `prof_leadership`, `prof_marketing`, and `prof_entrepreneur` for version `v1`.
|
||||
- If no signals exist, ingest signals first; `/v1/qscore/compute` may return `404 No signals found for this user`.
|
||||
- If formula store or compute is unavailable, backend may return an honest estimated score from signal averages with `compute_fallback: true`; label it as an estimate, not a verified Q-Score.
|
||||
When richer product outputs are available, prefer real scores, completion states, assignment outcomes, review feedback, timestamps, and target-role context over generic defaults.
|
||||
|
||||
Typical signal fields:
|
||||
- `user_id`: stable user identifier expected by the Q Score service.
|
||||
- `org_id`: default `growqr` unless a current organization is provided.
|
||||
- `signal_id`: event/dimension identifier such as `resume.ats_compatibility`.
|
||||
- `value`: numeric, boolean, or categorical readiness value.
|
||||
- `occurred_at`: ISO timestamp.
|
||||
- `raw`: source metadata such as mission ID, target role, service result IDs, or confidence notes.
|
||||
|
||||
## Computation strategy
|
||||
|
||||
1. If no recent signals exist, ingest the best available signals first.
|
||||
2. Then call `POST /v1/qscore/compute`.
|
||||
3. If the service returns `404 No signals found for this user`, explain that readiness needs source activity first and suggest the quickest signal-producing action, such as resume analysis, interview practice, or roleplay practice.
|
||||
4. If formula configuration is unavailable or compute fails, do not invent an official Q Score. You may summarize available signals as a non-official readiness estimate only if clearly labeled.
|
||||
|
||||
## Explanation strategy
|
||||
|
||||
When presenting results:
|
||||
- Lead with the score and what changed, if known.
|
||||
- Identify the 2-3 highest-leverage dimensions to improve.
|
||||
- Tie recommendations to concrete actions in GrowQR: resume optimization, interview preview/session, roleplay scenario, or mission step.
|
||||
- Frame low dimensions as growth opportunities, not personal failures.
|
||||
- Include uncertainty when source signals are sparse or stale.
|
||||
|
||||
## Safety and honesty
|
||||
|
||||
- Do not guarantee job offers, interviews, salary increases, or admissions outcomes.
|
||||
- Do not expose hidden formulas or claim precision beyond the service result.
|
||||
- Do not fabricate snapshots, score deltas, leaderboard ranks, or source evidence.
|
||||
- If data is missing, ask for the minimum missing context or recommend the next service action to generate it.
|
||||
|
||||
114
agents/resume.md
114
agents/resume.md
@@ -1,53 +1,91 @@
|
||||
---
|
||||
id: resume
|
||||
name: Resume Agent
|
||||
role: Resume Builder
|
||||
role: Resume Agent
|
||||
service: resume-service
|
||||
tools: ["analyze_resume", "tailor_resume"]
|
||||
---
|
||||
|
||||
## Domain
|
||||
Resume Agent builds, improves, analyzes, versions, and exports resumes and cover letters through the resume-builder microservice.
|
||||
# Resume Agent
|
||||
|
||||
The Resume Agent is GrowQR's API client for the `resume-service`. It builds, improves, analyzes, versions, parses, and exports resumes and cover letters for role fit, clarity, and ATS readiness.
|
||||
|
||||
Write from a service-client perspective. Do not reveal backend implementation details, authentication internals, storage mechanics, model providers, or internal prompts. Be transparent when a requested operation requires an existing resume, user-owned data, or a frontend-authenticated action.
|
||||
|
||||
## Primary intents
|
||||
|
||||
## When to use this agent
|
||||
Use the resume service when the user wants to:
|
||||
- create a first resume or improve an existing one
|
||||
- tailor a resume to a role/JD
|
||||
- analyze resume gaps, ATS readiness, or completeness
|
||||
- rewrite summary, skills, or experience bullets
|
||||
- generate or improve a cover letter
|
||||
- export resume/analysis artifacts
|
||||
- Create a first resume or improve an existing one.
|
||||
- Tailor a resume to a target role, company, job description, or mission.
|
||||
- Analyze resume gaps, ATS readiness, completeness, clarity, impact, or role fit.
|
||||
- Rewrite summaries, skills, experience bullets, headlines, projects, or education content.
|
||||
- Generate or improve a cover letter.
|
||||
- Parse an uploaded resume or convert unstructured career history into resume sections.
|
||||
- Save versions or export resume/analysis artifacts.
|
||||
|
||||
## What Resume Agent NEVER does
|
||||
- Interview practice → Sara
|
||||
- Workplace/salary roleplay → Emily
|
||||
## Route away
|
||||
|
||||
- Mock interviews, interview practice, interview assignments, or interview reviews → Interview Agent.
|
||||
- Workplace/salary roleplay, recruiter calls, manager conversations, networking, sales/support practice → Roleplay Agent.
|
||||
- Career readiness scoring, score deltas, or readiness dimensions → Q Score Agent.
|
||||
- Job search/application execution; GrowQR production modules currently focus on readiness, practice, resume, and scoring.
|
||||
- Q Score computation → Quinn/QScore service
|
||||
|
||||
## How it works
|
||||
Health/readiness:
|
||||
- `GET /health` returns service health.
|
||||
- `GET /api/state/{user_id}` returns quick user resume state/completeness and is A2A-safe.
|
||||
- `GET /api/v1/templates` lists active public resume templates and does not require Clerk auth.
|
||||
## Service capabilities
|
||||
|
||||
Agent operations should use the A2A task endpoint, not Clerk-protected REST routes, when called from GrowQR backend actors/OpenCode:
|
||||
Health and discovery:
|
||||
- `GET /health` — service health.
|
||||
- `GET /api/state/{user_id}` — quick user resume state, count, completeness, and current-role summary when available.
|
||||
- `GET /api/v1/templates` — active public resume templates.
|
||||
|
||||
A2A task interface for backend/service-agent use:
|
||||
- `POST /a2a/tasks`
|
||||
- Auth: `Authorization: Bearer <A2A_ALLOWED_KEY>`
|
||||
- Body: `{ "user_id": "<clerk/user id>", "action": "<action>", "params": {...} }`
|
||||
- Body shape: `{ "user_id": "<user id>", "action": "<action>", "params": { ... } }`
|
||||
|
||||
Supported A2A actions include:
|
||||
- `create_resume` with `title`, `template_id`, `initial_content`
|
||||
- `update_resume_meta`
|
||||
- `save_version`
|
||||
- `ai_analyze`
|
||||
- `ai_copilot`
|
||||
- `ai_optimize_summary`
|
||||
- `ai_optimize_experience`
|
||||
- `ai_suggest_skills`
|
||||
- `ai_generate_summary`
|
||||
- `generate_cover_letter`
|
||||
- `cover_letter_copilot`
|
||||
- `parse_resume`
|
||||
- `export_pdf`
|
||||
- `export_analysis_pdf`
|
||||
Supported task actions include:
|
||||
- `create_resume` — create a resume with `title`, `template_id`, and `initial_content`.
|
||||
- `update_resume_meta` — update resume metadata.
|
||||
- `save_version` — save a version snapshot.
|
||||
- `ai_analyze` — analyze completeness, ATS readiness, gaps, and recommendations.
|
||||
- `ai_copilot` — perform targeted resume edits or content generation.
|
||||
- `ai_optimize_summary` — improve the professional summary.
|
||||
- `ai_optimize_experience` — improve experience bullets.
|
||||
- `ai_suggest_skills` — suggest role-aligned skills.
|
||||
- `ai_generate_summary` — generate a summary from user context.
|
||||
- `generate_cover_letter` — draft a cover letter.
|
||||
- `cover_letter_copilot` — improve a cover letter.
|
||||
- `parse_resume` — parse uploaded/unstructured resume content.
|
||||
- `export_pdf` — export a resume PDF when supported.
|
||||
- `export_analysis_pdf` — export an analysis PDF when supported.
|
||||
|
||||
Important auth note: `/api/v1/resumes`, `/api/v1/ai/*`, cover-letter CRUD, and export REST endpoints are Clerk-user routes. Backend service-token/actor calls must not call those with the A2A key directly; use `/a2a/tasks` or add internal A2A endpoints.
|
||||
Frontend/user-owned REST capabilities may include resume CRUD, AI endpoints, cover-letter CRUD, versions, and export preview. If those require a user-authenticated frontend session, do not pretend a backend service-token call can complete them directly; explain the next user action or use the A2A task path when available.
|
||||
|
||||
## Default workflow
|
||||
|
||||
1. If the user's current resume state is unknown, call/read `GET /api/state/{user_id}` first when possible.
|
||||
2. If the user has an existing resume and asks for improvement, tailor, or score, use analysis/copilot/optimization actions.
|
||||
3. If no resume exists, gather minimal source content: target role, recent experience, education, projects, skills, and desired template; then use `create_resume` or `parse_resume` as appropriate.
|
||||
4. For tailoring, require or infer a target role/JD. If missing, ask one concise clarifying question.
|
||||
5. For exports, confirm the resume/version and format before initiating.
|
||||
|
||||
## Input normalization
|
||||
|
||||
Common fields:
|
||||
- `user_id`: current user identifier.
|
||||
- `resume_id`: required for operations on an existing resume when known.
|
||||
- `target_role`, `company`, `job_description`, or `goal`: role-fit context.
|
||||
- `template_id`: use a public active template when creating from scratch.
|
||||
- `content` / `initial_content`: structured resume sections or parsed source text.
|
||||
- `instructions`: concise edit goals, such as "make bullets more metrics-driven" or "tailor to product manager JD".
|
||||
|
||||
Editing rules:
|
||||
- Preserve factual truth. Do not invent employers, degrees, certifications, dates, metrics, or tools.
|
||||
- If a metric is missing, suggest placeholders or ask the user to confirm real numbers.
|
||||
- Use concise, ATS-readable language over flashy prose.
|
||||
- Prefer impact bullets using action + scope + method + result.
|
||||
|
||||
## Response strategy
|
||||
|
||||
- Surface service results: completeness, ATS readiness, missing sections, suggested skills, improved bullets, versions, or export links.
|
||||
- When giving edits, show before/after snippets and the reason for each change.
|
||||
- Keep recommendations prioritized: highest-impact fixes first.
|
||||
- If the service is unavailable, say so and offer a text-only draft or checklist as a temporary fallback.
|
||||
|
||||
77
agents/roleplay.md
Normal file
77
agents/roleplay.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
id: roleplay
|
||||
name: Roleplay Agent
|
||||
role: Roleplay Agent
|
||||
service: roleplay-service
|
||||
tools: ["start_roleplay_session"]
|
||||
---
|
||||
|
||||
# Roleplay Agent
|
||||
|
||||
The Roleplay Agent is GrowQR's API client for the `roleplay-service`. It helps users rehearse realistic workplace and career conversations: salary negotiation, offer calls, recruiter screens, manager conversations, stakeholder alignment, conflict resolution, networking, sales, customer success, and support scenarios.
|
||||
|
||||
Write from a service-client perspective. Do not reveal backend implementation details, model providers, storage mechanics, or internal prompts. Be honest about what the service can and cannot do.
|
||||
|
||||
## Primary intents
|
||||
|
||||
Use the roleplay service when the user wants to:
|
||||
- Start, preview, approve, or edit a roleplay practice session.
|
||||
- Practice salary negotiation, offer negotiation, recruiter calls, manager conversations, promotion conversations, stakeholder conversations, networking, conflict, sales, customer success, or support.
|
||||
- Create roleplay assignments for one or more people.
|
||||
- Review completed roleplay feedback, scores, leaderboard entries, video artifacts, or bulk assignment results.
|
||||
- Prepare a scenario before entering a live roleplay session.
|
||||
|
||||
## Route away
|
||||
|
||||
- Mock job interviews, interview rounds, interview question practice → Interview Agent.
|
||||
- Resume creation, tailoring, ATS optimization, cover letters, resume exports → Resume Agent.
|
||||
- Career readiness scoring or score deltas → Q Score Agent.
|
||||
- General career advice without a practice scenario → general Grow Agent unless the user asks to rehearse.
|
||||
|
||||
## Service capabilities
|
||||
|
||||
- `POST /api/v1/roleplays/configure/preview` — create a draft roleplay scenario for review.
|
||||
- `POST /api/v1/roleplays/configure` — create an immediate roleplay session.
|
||||
- `POST /api/v1/roleplays/configure/questions` — edit draft prompts/questions before approval when supported.
|
||||
- `POST /api/v1/roleplays/configure/approve` — approve a draft and create/activate the session.
|
||||
- WebSocket `/api/v1/roleplays/session/{session_id}` or frontend session URL — live practice is frontend-driven.
|
||||
- `GET /api/v1/roleplays/review/{session_id}` — review status, scores, transcript-derived feedback, and recommendations.
|
||||
- `GET /api/v1/roleplays/leaderboard` — top completed roleplay reviews.
|
||||
- `GET /api/v1/artifacts/{session_id}/{artifact_type}` — presigned artifact download, e.g. `session_video`.
|
||||
- `POST /api/v1/sessions/{session_id}/video/upload-url` — browser upload URL for `video/webm`.
|
||||
- `POST /api/v1/sessions/{session_id}/video/uploaded` — confirm upload and trigger async analysis.
|
||||
- `POST /api/v1/roleplays/assignments` — create assignments.
|
||||
- `GET /api/v1/roleplays/assignments?email=&status=&limit=` — list assignments.
|
||||
- `POST /api/v1/roleplays/assignments/unassign` — soft-unassign users.
|
||||
- `POST /api/v1/roleplays/results:bulk` — bulk results by assignment IDs.
|
||||
|
||||
## Default workflow
|
||||
|
||||
Default to `POST /api/v1/roleplays/configure/preview` when the user is deciding, exploring, requesting a scenario, or wants to review/edit before starting. Use `POST /api/v1/roleplays/configure` only when the user explicitly says they want to start, launch, begin, or practice now.
|
||||
|
||||
## Input normalization
|
||||
|
||||
Required/typical fields:
|
||||
- `user_id`: current user identifier.
|
||||
- `org_id`: default to `growqr` unless a current organization is provided.
|
||||
- `persona_id`: one of `payal`, `emma`, `john`, `kapil`; default `emma`.
|
||||
- `duration_minutes`: exactly `5`, `15`, or `30`; never send `10`.
|
||||
- `roleplay_type`: one of `sales`, `customer_success`, `support`, `custom`.
|
||||
- `brief`: concise scenario brief, including user's goal, counterpart, stakes, likely objections, and desired outcome.
|
||||
- `metadata`: structured context such as `target_role`, `company`, `difficulty`, `conversation_type`, `source`, assignment IDs, or workflow IDs.
|
||||
- `qscore`: include only when available; if supplied, it must contain numeric `q_score`.
|
||||
|
||||
Mapping rules:
|
||||
- Salary negotiation, offer negotiation, recruiter calls, manager conversations, promotion talks, networking, and workplace conflict → `roleplay_type: custom`.
|
||||
- Sales discovery, demos, objection handling, or closing → `sales`.
|
||||
- Renewal, onboarding, expansion, or customer health conversations → `customer_success`.
|
||||
- Troubleshooting, escalation, or service recovery → `support`.
|
||||
- If duration is missing, choose `15` for most scenarios, `5` for quick drills, and `30` only when the user requests a deeper simulation.
|
||||
|
||||
## Conversation strategy
|
||||
|
||||
- Ask at most 1-2 clarifying questions if the counterpart, situation, or desired outcome is unclear.
|
||||
- If enough context exists, create a preview rather than stalling.
|
||||
- Summarize returned `session_id`, `scenario_id`, `status`, `needs_approval`, `opening_prompt`, `prompt_outline`, `scenario`, `qscore_context`, and `candidate_brief` when available.
|
||||
- Do not fabricate live roleplay turns, recordings, scores, or reviews. If the service is unavailable, say so and offer a text-only rehearsal as a fallback.
|
||||
- Keep feedback practical: better phrasing, likely pushback, confidence cues, and one next rehearsal step.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
id: sara
|
||||
name: Sara
|
||||
role: Interview Agent
|
||||
service: interview-service
|
||||
---
|
||||
|
||||
## Domain
|
||||
Sara is the **Interview Agent**. She only handles job interview preparation and practice. Her focus is behavioral interviews, technical interviews, mock sessions, and interview feedback.
|
||||
|
||||
## When to use this agent (trigger phrases)
|
||||
Use `start_interview_session` when the user:
|
||||
- Wants to practice interviews: "mock interview", "interview prep", "practice interview", "rehearse interview"
|
||||
- Has behavioral questions: "STAR method", "tell me about yourself", "behavioral questions", "common interview questions"
|
||||
- Has technical interview needs: "coding interview", "system design", "technical screen", "whiteboard"
|
||||
- Has an upcoming interview: "interview tomorrow", "interview next week", "upcoming interview", "phone screen", "onsite", "final round", "panel interview"
|
||||
- Wants interview feedback: "how did I do", "improve my answers", "interview confidence", "nervous about interview"
|
||||
- Asks about specific question types: "case interview", "product sense", "estimation questions", "leadership questions"
|
||||
- Mentions any FAANG/tech company in interview context: Google, Meta, Amazon, Apple, Netflix, Microsoft, Stripe, Airbnb, Uber, etc.
|
||||
|
||||
## What Sara NEVER does
|
||||
- Resume writing or optimization → Resume Agent
|
||||
- Roleplay scenarios, negotiation, salary talk → Emily
|
||||
- Job searching or matching → Job Search Agent
|
||||
- Q-Score analysis → Quinn
|
||||
- Career switching advice → general chat
|
||||
|
||||
## How it works
|
||||
Calls `POST /api/v1/configure` or `POST /api/v1/configure/preview` on the interview-service with `user_id`, `org_id`, `persona_id`, `interview_type`, `duration_minutes`, and `context`.
|
||||
|
||||
Use `/preview` when the user is still reviewing the plan; use `/configure` when the workflow is creating a real practice session. Valid `persona_id` values are `payal`, `emma`, `john`, and `kapil` (default to `emma` if the user has no preference). Valid interview types include `warm_up`, `behavioral`, `role_related`, and `coding`; use `behavioral` for general job-readiness workflows, `role_related` for JD-specific practice, and `coding` only when the user asks for coding/technical rounds.
|
||||
|
||||
Recommended payload context fields: `target_role`, `company_name`, `job_description`, `difficulty`, and `source: growqr-workflow`. The service returns a real Gemini Live interview draft/session with `session_id`, `status`, `needs_approval`, `opening_prompt`, `question_outline`, and `candidate_brief`. Surface the `session_id` and candidate brief to the user; do not invent local interview questions if the service is unavailable.
|
||||
49
drizzle/0006_conversations_active_missions.sql
Normal file
49
drizzle/0006_conversations_active_missions.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- Durable Talk to Me conversations + mission actor snapshots.
|
||||
-- Actors are runtime caches/orchestrators; Postgres is the source of truth.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS grow_conversations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL DEFAULT 'Talk to Me',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS grow_conversations_user_idx
|
||||
ON grow_conversations(user_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS grow_conversation_messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT NOT NULL REFERENCES grow_conversations(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
||||
sender TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS grow_conversation_messages_conversation_idx
|
||||
ON grow_conversation_messages(conversation_id, created_at ASC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS grow_active_missions (
|
||||
instance_id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
mission_id TEXT NOT NULL,
|
||||
workflow_id TEXT NOT NULL,
|
||||
actor_type TEXT,
|
||||
title TEXT NOT NULL,
|
||||
short_title TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
progress_percent INTEGER NOT NULL DEFAULT 0,
|
||||
current_stage_id TEXT,
|
||||
goal TEXT,
|
||||
mission JSONB NOT NULL,
|
||||
snapshot JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS grow_active_missions_user_idx
|
||||
ON grow_active_missions(user_id, updated_at DESC);
|
||||
@@ -43,6 +43,13 @@
|
||||
"when": 1780307000000,
|
||||
"tag": "0005_mission_registry",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1780481100000,
|
||||
"tag": "0006_conversations_active_missions",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -73,9 +73,9 @@ Assistant calls analyze_resume → "Here's your analysis: [results]. Your streng
|
||||
## Workflow Tags (put at the VERY END, on their own line)
|
||||
|
||||
- [WORKFLOW: interview-to-offer] — full interview prep pipeline
|
||||
- [WORKFLOW: interview-practice] — interview sessions with Sara
|
||||
- [WORKFLOW: interview-practice] — interview sessions with the Interview Agent
|
||||
- [WORKFLOW: resume-boost] — resume analysis and optimization
|
||||
- [WORKFLOW: roleplay-practice] — roleplay sessions with Emily
|
||||
- [WORKFLOW: roleplay-practice] — roleplay sessions with Roleplay Agent
|
||||
- [WORKFLOW: career-switch] — career change navigation
|
||||
- [WORKFLOW: job-preparation] — broad company preparation
|
||||
|
||||
|
||||
@@ -175,11 +175,11 @@ function buildUnifiedTools(): Array<{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "run_workflow_module",
|
||||
description: "Execute a specific production sub-agent module in the workflow (e.g., resume, sara, emily, qscore).",
|
||||
description: "Execute a specific production sub-agent module in the workflow (e.g., resume, interview, roleplay, qscore).",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
moduleId: { type: "string", description: "Module id: resume, sara, emily, qscore" },
|
||||
moduleId: { type: "string", description: "Module id: resume, interview, roleplay, qscore" },
|
||||
},
|
||||
required: ["moduleId"],
|
||||
},
|
||||
@@ -189,7 +189,7 @@ function buildUnifiedTools(): Array<{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "start_interview_session",
|
||||
description: "Create a real interview practice session via the Sara / interview-service microservice.",
|
||||
description: "Create a real interview practice session via the Interview Agent / interview-service microservice.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { goal: { type: "string" } },
|
||||
@@ -201,7 +201,7 @@ function buildUnifiedTools(): Array<{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "start_roleplay_session",
|
||||
description: "Create a real roleplay practice session via the Emily / roleplay-service microservice.",
|
||||
description: "Create a real roleplay practice session via the Roleplay Agent / roleplay-service microservice.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { goal: { type: "string" } },
|
||||
@@ -213,7 +213,7 @@ function buildUnifiedTools(): Array<{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "compute_qscore",
|
||||
description: "Compute or refresh the user's Q-Score via the Quinn / qscore-service microservice.",
|
||||
description: "Compute or refresh the user's Q-Score via the Q Score Agent / qscore-service microservice.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
@@ -253,7 +253,7 @@ function buildUnifiedTools(): Array<{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "start_interview_to_offer",
|
||||
description: "Start the Interview-to-Offer Accelerator workflow. This is a guided end-to-end pipeline: (1) Analyze & tailor resume for the role, (2) Create interview practice session with Sara, (3) Create roleplay session with Emily, (4) Compute Q-Score readiness. Use this when the user has a specific interview scheduled and wants comprehensive preparation.",
|
||||
description: "Start the Interview-to-Offer Accelerator workflow. This is a guided end-to-end pipeline: (1) Analyze & tailor resume for the role, (2) Create interview practice session with the Interview Agent, (3) Create roleplay session with Roleplay Agent, (4) Compute Q-Score readiness. Use this when the user has a specific interview scheduled and wants comprehensive preparation.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -786,33 +786,33 @@ async function dispatchUnifiedTool(
|
||||
|
||||
case "start_interview_session": {
|
||||
const goal = String(input.goal ?? "");
|
||||
const saraModule = getSubAgentModule("sara");
|
||||
if (!saraModule?.service) return { ok: false, error: "Sara module not available" };
|
||||
const interviewModule = getSubAgentModule("interview");
|
||||
if (!interviewModule?.service) return { ok: false, error: "Interview module not available" };
|
||||
const result = await runServiceAgentProbe(
|
||||
{ id: saraModule.id, name: saraModule.name, role: saraModule.role, kind: "microservice", description: saraModule.description, service: saraModule.service },
|
||||
{ id: interviewModule.id, name: interviewModule.name, role: interviewModule.role, kind: "microservice", description: interviewModule.description, service: interviewModule.service },
|
||||
{ userId, goal },
|
||||
);
|
||||
c.broadcast("service-result", { moduleId: "sara", result });
|
||||
c.broadcast("service-result", { moduleId: "interview", result });
|
||||
return result;
|
||||
}
|
||||
|
||||
case "start_roleplay_session": {
|
||||
const goal = String(input.goal ?? "");
|
||||
const emilyModule = getSubAgentModule("emily");
|
||||
if (!emilyModule?.service) return { ok: false, error: "Emily module not available" };
|
||||
const roleplayModule = getSubAgentModule("roleplay");
|
||||
if (!roleplayModule?.service) return { ok: false, error: "Roleplay Agent module not available" };
|
||||
const result = await runServiceAgentProbe(
|
||||
{ id: emilyModule.id, name: emilyModule.name, role: emilyModule.role, kind: "microservice", description: emilyModule.description, service: emilyModule.service },
|
||||
{ id: roleplayModule.id, name: roleplayModule.name, role: roleplayModule.role, kind: "microservice", description: roleplayModule.description, service: roleplayModule.service },
|
||||
{ userId, goal },
|
||||
);
|
||||
c.broadcast("service-result", { moduleId: "emily", result });
|
||||
c.broadcast("service-result", { moduleId: "roleplay", result });
|
||||
return result;
|
||||
}
|
||||
|
||||
case "compute_qscore": {
|
||||
const quinnModule = getSubAgentModule("qscore");
|
||||
if (!quinnModule?.service) return { ok: false, error: "Quinn module not available" };
|
||||
const qscoreModule = getSubAgentModule("qscore");
|
||||
if (!qscoreModule?.service) return { ok: false, error: "Q Score Agent module not available" };
|
||||
const result = await runServiceAgentProbe(
|
||||
{ id: quinnModule.id, name: quinnModule.name, role: quinnModule.role, kind: "score", description: quinnModule.description, service: quinnModule.service },
|
||||
{ id: qscoreModule.id, name: qscoreModule.name, role: qscoreModule.role, kind: "score", description: qscoreModule.description, service: qscoreModule.service },
|
||||
{ userId, goal: c.state.workflowGoal || "general assessment" },
|
||||
);
|
||||
c.broadcast("service-result", { moduleId: "qscore", result });
|
||||
@@ -881,73 +881,73 @@ async function dispatchUnifiedTool(
|
||||
|
||||
c.broadcast("workflow.updated", workflowSnapshot(c.state));
|
||||
|
||||
// Step 2: Sara — create interview session
|
||||
const saraModule = getSubAgentModule("sara");
|
||||
const saraMod = c.state.modules.find(m => m.id === "sara");
|
||||
if (saraMod && saraModule?.service) {
|
||||
saraMod.status = "running";
|
||||
appendTimelineEvent(c.state, saraMod, "module", "Sara creating interview practice session...");
|
||||
// Step 2: Interview Agent — create interview session
|
||||
const interviewModule = getSubAgentModule("interview");
|
||||
const interviewMod = c.state.modules.find(m => m.id === "interview");
|
||||
if (interviewMod && interviewModule?.service) {
|
||||
interviewMod.status = "running";
|
||||
appendTimelineEvent(c.state, interviewMod, "module", "Interview Agent creating interview practice session...");
|
||||
c.broadcast("workflow.updated", workflowSnapshot(c.state));
|
||||
|
||||
try {
|
||||
const saraResult = await runServiceAgentProbe(
|
||||
{ id: saraModule.id, name: saraModule.name, role: saraModule.role, kind: "microservice", description: saraModule.description, service: saraModule.service },
|
||||
const interviewResult = await runServiceAgentProbe(
|
||||
{ id: interviewModule.id, name: interviewModule.name, role: interviewModule.role, kind: "microservice", description: interviewModule.description, service: interviewModule.service },
|
||||
{ userId, goal: goal + (jobDesc ? `\nJob Description: ${jobDesc}` : "") },
|
||||
);
|
||||
saraMod.lastResult = saraResult;
|
||||
saraMod.status = saraResult.status === "unavailable" ? "blocked" : "done";
|
||||
appendTimelineEvent(c.state, saraMod, "module", saraResult.summary);
|
||||
interviewMod.lastResult = interviewResult;
|
||||
interviewMod.status = interviewResult.status === "unavailable" ? "blocked" : "done";
|
||||
appendTimelineEvent(c.state, interviewMod, "module", interviewResult.summary);
|
||||
} catch (err) {
|
||||
saraMod.status = "blocked";
|
||||
appendTimelineEvent(c.state, saraMod, "module", `Sara session failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
interviewMod.status = "blocked";
|
||||
appendTimelineEvent(c.state, interviewMod, "module", `Interview session failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
c.broadcast("workflow.updated", workflowSnapshot(c.state));
|
||||
|
||||
// Step 3: Emily — create roleplay session
|
||||
const emilyModule = getSubAgentModule("emily");
|
||||
const emilyMod = c.state.modules.find(m => m.id === "emily");
|
||||
if (emilyMod && emilyModule?.service) {
|
||||
emilyMod.status = "running";
|
||||
appendTimelineEvent(c.state, emilyMod, "module", "Emily creating roleplay scenario...");
|
||||
// Step 3: Roleplay Agent — create roleplay session
|
||||
const roleplayModule = getSubAgentModule("roleplay");
|
||||
const roleplayMod = c.state.modules.find(m => m.id === "roleplay");
|
||||
if (roleplayMod && roleplayModule?.service) {
|
||||
roleplayMod.status = "running";
|
||||
appendTimelineEvent(c.state, roleplayMod, "module", "Roleplay Agent creating roleplay scenario...");
|
||||
c.broadcast("workflow.updated", workflowSnapshot(c.state));
|
||||
|
||||
try {
|
||||
const emilyResult = await runServiceAgentProbe(
|
||||
{ id: emilyModule.id, name: emilyModule.name, role: emilyModule.role, kind: "microservice", description: emilyModule.description, service: emilyModule.service },
|
||||
const roleplayResult = await runServiceAgentProbe(
|
||||
{ id: roleplayModule.id, name: roleplayModule.name, role: roleplayModule.role, kind: "microservice", description: roleplayModule.description, service: roleplayModule.service },
|
||||
{ userId, goal: `Interview negotiation and communication practice for: ${goal}` },
|
||||
);
|
||||
emilyMod.lastResult = emilyResult;
|
||||
emilyMod.status = emilyResult.status === "unavailable" ? "blocked" : "done";
|
||||
appendTimelineEvent(c.state, emilyMod, "module", emilyResult.summary);
|
||||
roleplayMod.lastResult = roleplayResult;
|
||||
roleplayMod.status = roleplayResult.status === "unavailable" ? "blocked" : "done";
|
||||
appendTimelineEvent(c.state, roleplayMod, "module", roleplayResult.summary);
|
||||
} catch (err) {
|
||||
emilyMod.status = "blocked";
|
||||
appendTimelineEvent(c.state, emilyMod, "module", `Emily session failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
roleplayMod.status = "blocked";
|
||||
appendTimelineEvent(c.state, roleplayMod, "module", `Roleplay Agent session failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
c.broadcast("workflow.updated", workflowSnapshot(c.state));
|
||||
|
||||
// Step 4: Quinn — compute Q-Score
|
||||
const quinnModule = getSubAgentModule("qscore");
|
||||
const quinnMod = c.state.modules.find(m => m.id === "qscore");
|
||||
if (quinnMod && quinnModule?.service) {
|
||||
quinnMod.status = "running";
|
||||
appendTimelineEvent(c.state, quinnMod, "module", "Quinn computing your readiness Q-Score...");
|
||||
// Step 4: Q Score Agent — compute Q-Score
|
||||
const qscoreModule = getSubAgentModule("qscore");
|
||||
const qscoreMod = c.state.modules.find(m => m.id === "qscore");
|
||||
if (qscoreMod && qscoreModule?.service) {
|
||||
qscoreMod.status = "running";
|
||||
appendTimelineEvent(c.state, qscoreMod, "module", "Q Score Agent computing your readiness Q-Score...");
|
||||
c.broadcast("workflow.updated", workflowSnapshot(c.state));
|
||||
|
||||
try {
|
||||
const quinnResult = await runServiceAgentProbe(
|
||||
{ id: quinnModule.id, name: quinnModule.name, role: quinnModule.role, kind: "score", description: quinnModule.description, service: quinnModule.service },
|
||||
const qscoreResult = await runServiceAgentProbe(
|
||||
{ id: qscoreModule.id, name: qscoreModule.name, role: qscoreModule.role, kind: "score", description: qscoreModule.description, service: qscoreModule.service },
|
||||
{ userId, goal },
|
||||
);
|
||||
quinnMod.lastResult = quinnResult;
|
||||
quinnMod.status = quinnResult.status === "unavailable" ? "blocked" : "done";
|
||||
appendTimelineEvent(c.state, quinnMod, "module", quinnResult.summary);
|
||||
qscoreMod.lastResult = qscoreResult;
|
||||
qscoreMod.status = qscoreResult.status === "unavailable" ? "blocked" : "done";
|
||||
appendTimelineEvent(c.state, qscoreMod, "module", qscoreResult.summary);
|
||||
} catch (err) {
|
||||
quinnMod.status = "blocked";
|
||||
appendTimelineEvent(c.state, quinnMod, "module", `Q-Score computation failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
qscoreMod.status = "blocked";
|
||||
appendTimelineEvent(c.state, qscoreMod, "module", `Q-Score computation failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ export const config = {
|
||||
process.env.RESUME_PUBLIC_URL ?? process.env.RESUME_SERVICE_URL ?? "http://localhost:8002",
|
||||
matchmakingServiceUrl:
|
||||
process.env.MATCHMAKING_SERVICE_URL ?? "http://localhost:8006",
|
||||
socialBrandingServiceUrl:
|
||||
process.env.SOCIAL_BRANDING_SERVICE_URL ?? "http://localhost:8005",
|
||||
workflowsDashboardUrl:
|
||||
process.env.WORKFLOWS_DASHBOARD_URL ??
|
||||
process.env.FRONTEND_ORIGIN ??
|
||||
|
||||
@@ -272,5 +272,55 @@ export const workflowEvents = pgTable("workflow_events", {
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const growConversations = pgTable(
|
||||
"grow_conversations",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull().default("Talk to Me"),
|
||||
active: boolean("active").notNull().default(true),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({ userIdx: index("grow_conversations_user_idx").on(t.userId, t.updatedAt) }),
|
||||
);
|
||||
|
||||
export const growConversationMessages = pgTable(
|
||||
"grow_conversation_messages",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
conversationId: text("conversation_id").notNull().references(() => growConversations.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
role: text("role", { enum: ["user", "assistant"] }).notNull(),
|
||||
sender: text("sender").notNull(),
|
||||
content: text("content").notNull(),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({ conversationIdx: index("grow_conversation_messages_conversation_idx").on(t.conversationId, t.createdAt) }),
|
||||
);
|
||||
|
||||
export const growActiveMissions = pgTable(
|
||||
"grow_active_missions",
|
||||
{
|
||||
instanceId: text("instance_id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
missionId: text("mission_id").notNull(),
|
||||
workflowId: text("workflow_id").notNull(),
|
||||
actorType: text("actor_type"),
|
||||
title: text("title").notNull(),
|
||||
shortTitle: text("short_title").notNull(),
|
||||
status: text("status").notNull(),
|
||||
progressPercent: integer("progress_percent").notNull().default(0),
|
||||
currentStageId: text("current_stage_id"),
|
||||
goal: text("goal"),
|
||||
mission: jsonb("mission").$type<Record<string, unknown>>().notNull(),
|
||||
snapshot: jsonb("snapshot").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({ userIdx: index("grow_active_missions_user_idx").on(t.userId, t.updatedAt) }),
|
||||
);
|
||||
|
||||
export type OpencodeSessionRow = typeof opencodeSessions.$inferSelect;
|
||||
export type WorkflowRunRow = typeof workflowRuns.$inferSelect;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { config } from "../config.js";
|
||||
|
||||
export type GrowServiceId = "resume-service" | "interview-service" | "roleplay-service" | "qscore-service";
|
||||
export type GrowFeatureId = "resume-building" | "mock-interview" | "mock-roleplay" | "q-score";
|
||||
export type GrowServiceId = "resume-service" | "interview-service" | "roleplay-service" | "qscore-service" | "social-branding-service" | "matchmaking-service";
|
||||
export type GrowFeatureId = "resume-building" | "mock-interview" | "mock-roleplay" | "q-score" | "social-branding" | "matchmaking";
|
||||
|
||||
export type GrowFeatureDefinition = {
|
||||
id: GrowFeatureId;
|
||||
serviceId: GrowServiceId;
|
||||
title: string;
|
||||
skillLabel: string;
|
||||
label: string;
|
||||
description: string;
|
||||
promptModulePath: string;
|
||||
enabled: boolean;
|
||||
@@ -21,7 +21,7 @@ export const featureDefinitions: GrowFeatureDefinition[] = [
|
||||
id: "resume-building",
|
||||
serviceId: "resume-service",
|
||||
title: "Resume Building",
|
||||
skillLabel: "Resume Building",
|
||||
label: "Resume",
|
||||
description: "Build, tailor, analyze, and improve resumes for role fit and ATS readiness.",
|
||||
promptModulePath: "agents/resume.md",
|
||||
enabled: Boolean(config.resumeServiceUrl),
|
||||
@@ -33,9 +33,9 @@ export const featureDefinitions: GrowFeatureDefinition[] = [
|
||||
id: "mock-interview",
|
||||
serviceId: "interview-service",
|
||||
title: "Mock Interview",
|
||||
skillLabel: "Mock Interview",
|
||||
label: "Interview",
|
||||
description: "Configure, practice, review, and score interview sessions.",
|
||||
promptModulePath: "agents/sara.md",
|
||||
promptModulePath: "agents/interview.md",
|
||||
enabled: Boolean(config.interviewServiceUrl),
|
||||
internalUrl: config.interviewServiceUrl,
|
||||
publicUrl: config.interviewPublicUrl,
|
||||
@@ -45,9 +45,9 @@ export const featureDefinitions: GrowFeatureDefinition[] = [
|
||||
id: "mock-roleplay",
|
||||
serviceId: "roleplay-service",
|
||||
title: "Mock Roleplay",
|
||||
skillLabel: "Mock Roleplay",
|
||||
label: "Roleplay",
|
||||
description: "Practice negotiations, recruiter calls, manager conversations, and stakeholder roleplays.",
|
||||
promptModulePath: "agents/emily.md",
|
||||
promptModulePath: "agents/roleplay.md",
|
||||
enabled: Boolean(config.roleplayServiceUrl),
|
||||
internalUrl: config.roleplayServiceUrl,
|
||||
publicUrl: config.roleplayPublicUrl,
|
||||
@@ -57,20 +57,42 @@ export const featureDefinitions: GrowFeatureDefinition[] = [
|
||||
id: "q-score",
|
||||
serviceId: "qscore-service",
|
||||
title: "Q Score",
|
||||
skillLabel: "Q Score",
|
||||
label: "Q Score",
|
||||
description: "Analyze overall job-market readiness and convert signals into improvement priorities.",
|
||||
promptModulePath: "agents/qscore.md",
|
||||
enabled: Boolean(config.qscoreServiceUrl),
|
||||
internalUrl: config.qscoreServiceUrl,
|
||||
operations: ["qscore.ingest", "qscore.compute"],
|
||||
},
|
||||
{
|
||||
id: "social-branding",
|
||||
serviceId: "social-branding-service",
|
||||
title: "Social Branding",
|
||||
label: "Branding",
|
||||
description: "Build and optimize your professional profile, LinkedIn presence, and personal brand.",
|
||||
promptModulePath: "agents/social-branding.md",
|
||||
enabled: Boolean(config.socialBrandingServiceUrl),
|
||||
internalUrl: config.socialBrandingServiceUrl,
|
||||
operations: ["branding.profile", "branding.linkedin", "branding.content", "branding.analyze"],
|
||||
},
|
||||
{
|
||||
id: "matchmaking",
|
||||
serviceId: "matchmaking-service",
|
||||
title: "Matchmaking",
|
||||
label: "Matchmaking",
|
||||
description: "Connect with relevant professionals, mentors, and opportunities through curated matching.",
|
||||
promptModulePath: "agents/matchmaking.md",
|
||||
enabled: Boolean(config.matchmakingServiceUrl),
|
||||
internalUrl: config.matchmakingServiceUrl,
|
||||
operations: ["matchmaking.find", "matchmaking.connect", "matchmaking.schedule", "matchmaking.review"],
|
||||
},
|
||||
];
|
||||
|
||||
export const internalWorkflowSkills = [
|
||||
export const internalWorkflowModules = [
|
||||
{
|
||||
id: "mission-planning",
|
||||
title: "Mission Planning",
|
||||
skillLabel: "Mission Planning",
|
||||
label: "Mission Planning",
|
||||
description: "Internal planning and artifact drafting for a mission. This is not a user-facing feature service.",
|
||||
execution: "opencode" as const,
|
||||
},
|
||||
@@ -86,7 +108,7 @@ export function getFeatureByServiceId(serviceId: string) {
|
||||
|
||||
export function displayLabelForService(serviceId: string | undefined) {
|
||||
if (!serviceId) return undefined;
|
||||
return getFeatureByServiceId(serviceId)?.skillLabel;
|
||||
return getFeatureByServiceId(serviceId)?.label;
|
||||
}
|
||||
|
||||
export function displayLabelForExecution(execution: string) {
|
||||
|
||||
126
src/grow/persistence.ts
Normal file
126
src/grow/persistence.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { asc, desc, eq, and } from "drizzle-orm";
|
||||
import { db } from "../db/client.js";
|
||||
import { growActiveMissions, growConversationMessages, growConversations } from "../db/schema.js";
|
||||
import type { GrowActiveMission, MissionSnapshot } from "../actors/missions/types.js";
|
||||
import type { ConversationMessage } from "../actors/conversation/types.js";
|
||||
import type { GrowConversation } from "../actors/grow/types.js";
|
||||
|
||||
const buildId = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const dateFromMs = (ms?: number) => new Date(ms && Number.isFinite(ms) ? ms : Date.now());
|
||||
|
||||
function toConversation(row: typeof growConversations.$inferSelect): GrowConversation {
|
||||
return { id: row.id, title: row.title, createdAt: row.createdAt.getTime(), updatedAt: row.updatedAt.getTime() };
|
||||
}
|
||||
|
||||
function toMessage(row: typeof growConversationMessages.$inferSelect): ConversationMessage {
|
||||
return {
|
||||
id: row.id,
|
||||
conversationId: row.conversationId,
|
||||
role: row.role,
|
||||
sender: row.sender,
|
||||
content: row.content,
|
||||
createdAt: row.createdAt.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureConversation(userId: string, title = "Talk to Me") {
|
||||
const existing = await listConversationsPg(userId);
|
||||
if (existing[0]) return existing[0];
|
||||
return createConversationPg(userId, title);
|
||||
}
|
||||
|
||||
export async function listConversationsPg(userId: string): Promise<GrowConversation[]> {
|
||||
const rows = await db.select().from(growConversations).where(eq(growConversations.userId, userId)).orderBy(desc(growConversations.updatedAt));
|
||||
return rows.map(toConversation);
|
||||
}
|
||||
|
||||
export async function createConversationPg(userId: string, title = "Talk to Me"): Promise<GrowConversation> {
|
||||
const now = new Date();
|
||||
const [row] = await db.insert(growConversations).values({ id: buildId("conversation"), userId, title, createdAt: now, updatedAt: now }).returning();
|
||||
if (!row) throw new Error("Failed to create conversation");
|
||||
return toConversation(row);
|
||||
}
|
||||
|
||||
export async function getConversationPg(userId: string, conversationId: string): Promise<GrowConversation | null> {
|
||||
const [row] = await db.select().from(growConversations).where(and(eq(growConversations.userId, userId), eq(growConversations.id, conversationId))).limit(1);
|
||||
return row ? toConversation(row) : null;
|
||||
}
|
||||
|
||||
export async function touchConversationPg(userId: string, conversationId: string, title?: string) {
|
||||
const patch: Partial<typeof growConversations.$inferInsert> = { updatedAt: new Date() };
|
||||
if (title?.trim()) patch.title = title.trim();
|
||||
const [row] = await db.update(growConversations).set(patch).where(and(eq(growConversations.userId, userId), eq(growConversations.id, conversationId))).returning();
|
||||
return row ? toConversation(row) : null;
|
||||
}
|
||||
|
||||
export async function resetConversationPg(userId: string, conversationId: string) {
|
||||
await db.delete(growConversationMessages).where(and(eq(growConversationMessages.userId, userId), eq(growConversationMessages.conversationId, conversationId)));
|
||||
return touchConversationPg(userId, conversationId);
|
||||
}
|
||||
|
||||
export async function listMessagesPg(userId: string, conversationId: string): Promise<ConversationMessage[]> {
|
||||
const rows = await db.select().from(growConversationMessages).where(and(eq(growConversationMessages.userId, userId), eq(growConversationMessages.conversationId, conversationId))).orderBy(asc(growConversationMessages.createdAt));
|
||||
return rows.map(toMessage);
|
||||
}
|
||||
|
||||
export async function addMessagePg(userId: string, input: Omit<ConversationMessage, "createdAt"> & { createdAt?: number }) {
|
||||
const createdAt = dateFromMs(input.createdAt);
|
||||
const [row] = await db.insert(growConversationMessages).values({
|
||||
id: input.id,
|
||||
userId,
|
||||
conversationId: input.conversationId,
|
||||
role: input.role,
|
||||
sender: input.sender,
|
||||
content: input.content,
|
||||
createdAt,
|
||||
}).onConflictDoUpdate({
|
||||
target: growConversationMessages.id,
|
||||
set: { content: input.content, sender: input.sender },
|
||||
}).returning();
|
||||
await touchConversationPg(userId, input.conversationId);
|
||||
if (!row) throw new Error("Failed to persist message");
|
||||
return toMessage(row);
|
||||
}
|
||||
|
||||
export async function upsertActiveMissionPg(userId: string, mission: GrowActiveMission, snapshot?: MissionSnapshot) {
|
||||
const now = new Date();
|
||||
await db.insert(growActiveMissions).values({
|
||||
instanceId: mission.instanceId,
|
||||
userId,
|
||||
missionId: mission.missionId,
|
||||
workflowId: mission.workflowId,
|
||||
actorType: mission.actorType,
|
||||
title: mission.title,
|
||||
shortTitle: mission.shortTitle,
|
||||
status: mission.status,
|
||||
progressPercent: mission.progressPercent,
|
||||
currentStageId: mission.currentStageId,
|
||||
goal: mission.goal,
|
||||
mission: mission as unknown as Record<string, unknown>,
|
||||
snapshot: snapshot as unknown as Record<string, unknown> | undefined,
|
||||
createdAt: dateFromMs(mission.createdAt),
|
||||
updatedAt: dateFromMs(mission.updatedAt),
|
||||
}).onConflictDoUpdate({
|
||||
target: growActiveMissions.instanceId,
|
||||
set: {
|
||||
status: mission.status,
|
||||
progressPercent: mission.progressPercent,
|
||||
currentStageId: mission.currentStageId,
|
||||
goal: mission.goal,
|
||||
actorType: mission.actorType,
|
||||
mission: mission as unknown as Record<string, unknown>,
|
||||
snapshot: snapshot as unknown as Record<string, unknown> | undefined,
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function listActiveMissionsPg(userId: string) {
|
||||
const rows = await db.select().from(growActiveMissions).where(eq(growActiveMissions.userId, userId)).orderBy(desc(growActiveMissions.updatedAt));
|
||||
return rows.map((row) => ({ mission: row.mission as unknown as GrowActiveMission, snapshot: row.snapshot as unknown as MissionSnapshot | null }));
|
||||
}
|
||||
|
||||
export async function getActiveMissionPg(userId: string, instanceId: string) {
|
||||
const [row] = await db.select().from(growActiveMissions).where(and(eq(growActiveMissions.userId, userId), eq(growActiveMissions.instanceId, instanceId))).limit(1);
|
||||
return row ? { mission: row.mission as unknown as GrowActiveMission, snapshot: row.snapshot as unknown as MissionSnapshot | null } : null;
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export function getSubAgentModule(id: string): SubAgentModule | undefined {
|
||||
}
|
||||
|
||||
export function jobApplicationModuleIds(): string[] {
|
||||
return ["resume", "sara", "emily", "qscore"];
|
||||
return ["resume", "interview", "roleplay", "qscore"];
|
||||
}
|
||||
|
||||
// Load all prompt and agent files from disk.
|
||||
|
||||
@@ -52,7 +52,7 @@ function buildTools() {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "start_interview_session",
|
||||
description: "Create a real interview practice session via the Sara / interview-service microservice. Call this when the user asks to start or launch an interview.",
|
||||
description: "Create a real interview practice session via the Interview Agent / interview-service microservice. Call this when the user asks to start or launch an interview.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -66,7 +66,7 @@ function buildTools() {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "start_roleplay_session",
|
||||
description: "Create a real roleplay session via Emily / roleplay-service. Call when user asks for roleplay or negotiation practice.",
|
||||
description: "Create a real roleplay session via Roleplay Agent / roleplay-service. Call when user asks for roleplay or negotiation practice.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -94,7 +94,7 @@ function buildTools() {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "compute_qscore",
|
||||
description: "Compute user's readiness Q-Score via Quinn / qscore-service.",
|
||||
description: "Compute user's readiness Q-Score via Q Score Agent / qscore-service.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
@@ -174,14 +174,14 @@ export function chatRoutes() {
|
||||
switch (toolCall.name) {
|
||||
case "start_interview_session": {
|
||||
toolResult = await runServiceAgentProbe(
|
||||
{ id: "sara", name: "Sara", role: "Interview Agent", kind: "microservice", description: "Interview practice", service: "interview-service" },
|
||||
{ id: "interview", name: "Interview Agent", role: "Interview Agent", kind: "microservice", description: "Interview practice", service: "interview-service" },
|
||||
{ userId, goal: String(toolCall.arguments.target_role ?? "general preparation") },
|
||||
);
|
||||
if (toolResult.status === "ok" && toolResult.detail) {
|
||||
const detail = toolResult.detail as Record<string, unknown>;
|
||||
sessions.push({
|
||||
moduleId: "sara",
|
||||
moduleName: "Sara",
|
||||
moduleId: "interview",
|
||||
moduleName: "Interview Agent",
|
||||
status: "done",
|
||||
sessionId: detail.session_id as string,
|
||||
sessionUrl: typeof detail.ui_session_url === "string"
|
||||
@@ -194,14 +194,14 @@ export function chatRoutes() {
|
||||
}
|
||||
case "start_roleplay_session": {
|
||||
toolResult = await runServiceAgentProbe(
|
||||
{ id: "emily", name: "Emily", role: "Roleplay Agent", kind: "microservice", description: "Roleplay practice", service: "roleplay-service" },
|
||||
{ id: "roleplay", name: "Roleplay Agent", role: "Roleplay Agent", kind: "microservice", description: "Roleplay practice", service: "roleplay-service" },
|
||||
{ userId, goal: String(toolCall.arguments.goal ?? "general practice") },
|
||||
);
|
||||
if (toolResult.status === "ok" && toolResult.detail) {
|
||||
const detail = toolResult.detail as Record<string, unknown>;
|
||||
sessions.push({
|
||||
moduleId: "emily",
|
||||
moduleName: "Emily",
|
||||
moduleId: "roleplay",
|
||||
moduleName: "Roleplay Agent",
|
||||
status: "done",
|
||||
sessionId: detail.session_id as string,
|
||||
sessionUrl: typeof detail.ui_session_url === "string"
|
||||
@@ -214,7 +214,7 @@ export function chatRoutes() {
|
||||
}
|
||||
case "analyze_resume": {
|
||||
toolResult = await runServiceAgentProbe(
|
||||
{ id: "resume", name: "Resume Agent", role: "Resume Builder", kind: "microservice", description: "Resume analysis", service: "resume-service" },
|
||||
{ id: "resume", name: "Resume Agent", role: "Resume Agent", kind: "microservice", description: "Resume analysis", service: "resume-service" },
|
||||
{ userId, goal: String(toolCall.arguments.goal ?? "general") },
|
||||
);
|
||||
if (toolResult.status === "ok") {
|
||||
@@ -233,11 +233,11 @@ export function chatRoutes() {
|
||||
}
|
||||
case "compute_qscore": {
|
||||
toolResult = await runServiceAgentProbe(
|
||||
{ id: "qscore", name: "Quinn", role: "Q-Score Agent", kind: "score", description: "Readiness scoring", service: "qscore-service" },
|
||||
{ id: "qscore", name: "Q Score Agent", role: "Q Score Agent", kind: "score", description: "Readiness scoring", service: "qscore-service" },
|
||||
{ userId, goal: "general assessment" },
|
||||
);
|
||||
if (toolResult.status === "ok") {
|
||||
sessions.push({ moduleId: "qscore", moduleName: "Quinn", status: "done", summary: toolResult.summary });
|
||||
sessions.push({ moduleId: "qscore", moduleName: "Q Score Agent", status: "done", summary: toolResult.summary });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ import { getConversationModel } from "../actors/conversation/agent.js";
|
||||
import { getMissionDefinition, isActorBackedMission, listMissionDefinitions } from "../missions/registry.js";
|
||||
import type { GrowActiveMission, MissionActorType, MissionSnapshot } from "../actors/missions/types.js";
|
||||
import { getSubAgentModules } from "../lib/prompt-loader.js";
|
||||
import { addMessagePg, createConversationPg, ensureConversation, getConversationPg, listActiveMissionsPg, listConversationsPg, listMessagesPg, resetConversationPg, touchConversationPg, upsertActiveMissionPg } from "../grow/persistence.js";
|
||||
|
||||
let _client: Client<Registry> | null = null;
|
||||
function getClient(): Client<Registry> {
|
||||
return (_client ??= createClient<Registry>(config.rivetClientEndpoint));
|
||||
}
|
||||
|
||||
type ActorSchedulingFailure = Error & { actorId?: string; metadata?: { actorId?: string } };
|
||||
|
||||
function growFor(userId: string) {
|
||||
return getClient().growActor.getOrCreate([userId]);
|
||||
}
|
||||
@@ -23,6 +26,60 @@ function conversationFor(userId: string, conversationId: string) {
|
||||
return getClient().conversationActor.getOrCreate([userId, conversationId]);
|
||||
}
|
||||
|
||||
function isActorSchedulingFailure(error: unknown): error is ActorSchedulingFailure {
|
||||
if (!(error instanceof Error)) return false;
|
||||
const maybe = error as ActorSchedulingFailure;
|
||||
return maybe.name === "ActorSchedulingError" || /actor_ready_timeout|failed to start/i.test(error.message);
|
||||
}
|
||||
|
||||
function actorIdFromError(error: ActorSchedulingFailure) {
|
||||
return error.actorId ?? error.metadata?.actorId;
|
||||
}
|
||||
|
||||
function rivetAdminEndpoint() {
|
||||
const url = new URL(config.rivetEndpoint);
|
||||
const token = url.password || process.env.RIVET_TOKEN || process.env.RIVET_ADMIN_TOKEN || "dev-admin-token";
|
||||
url.username = "";
|
||||
url.password = "";
|
||||
const headers: Record<string, string> = token
|
||||
? { Authorization: `Bearer ${token}`, "x-rivet-token": token }
|
||||
: {};
|
||||
return {
|
||||
baseUrl: url.toString().replace(/\/$/, ""),
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteCrashedActor(actorId: string) {
|
||||
const { baseUrl, headers } = rivetAdminEndpoint();
|
||||
const url = new URL(`${baseUrl}/actors/${encodeURIComponent(actorId)}`);
|
||||
url.searchParams.set("namespace", process.env.RIVET_NAMESPACE ?? "default");
|
||||
const response = await fetch(url, { method: "DELETE", headers });
|
||||
if (!response.ok && response.status !== 404) {
|
||||
throw new Error(`Failed to delete crashed actor ${actorId}: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function withActorRecovery<T>(label: string, operation: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (!isActorSchedulingFailure(error)) throw error;
|
||||
const actorId = actorIdFromError(error);
|
||||
if (!actorId) throw error;
|
||||
|
||||
console.warn(`${label} actor crashed while starting; deleting ${actorId} and retrying once`);
|
||||
await deleteCrashedActor(actorId);
|
||||
return operation();
|
||||
}
|
||||
}
|
||||
|
||||
async function setupGrow(userId: string) {
|
||||
const grow = growFor(userId);
|
||||
await withActorRecovery("growActor", () => grow.setup({ userId }));
|
||||
return grow;
|
||||
}
|
||||
|
||||
function memoryFor(userId: string) {
|
||||
return getClient().memoryActor.getOrCreate([userId]);
|
||||
}
|
||||
@@ -97,61 +154,35 @@ function forcedToolForPrompt(text: string) {
|
||||
// see/discover/find workflows, always produce workflow cards instead of a
|
||||
// text-only answer.
|
||||
if (
|
||||
/\b(discover|find|show|list|recommend|suggest|compare|what|which)\b/.test(lower) &&
|
||||
/\b(workflow|workflows|plan|plans|program|programs)\b/.test(lower)
|
||||
/\b(discover|find|show|list|recommend|suggest|compare)\b/.test(lower) &&
|
||||
/\b(workflow|workflows|mission|missions|plan|plans|program|programs)\b/.test(lower)
|
||||
) {
|
||||
return "discoverWorkflows" as const;
|
||||
}
|
||||
|
||||
if (/\b(mission|missions|next actions|next steps|what should i do today)\b/.test(lower)) {
|
||||
return "showMissions" as const;
|
||||
}
|
||||
|
||||
if (/\b(agent registry|specialists|subagents|coaches|who can help)\b/.test(lower)) {
|
||||
return "listAgentRegistry" as const;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildSystemPrompt() {
|
||||
return `You are Grow, the warm, encouraging career companion inside GrowQR. You are part coach, part strategist, and part cheerleader.
|
||||
return `You are Grow — a friendly, normal career buddy inside GrowQR. Talk like a real person, not a coach or a robot.
|
||||
|
||||
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.
|
||||
- Be warm, natural, and genuinely helpful. Talk like a smart friend who happens to know careers.
|
||||
- Use "we" and "you" naturally. Be concise but never robotic.
|
||||
- When the user is stressed, be calm. When they're excited, match their energy.
|
||||
- Use markdown when it helps readability, but don't over-structure everything.
|
||||
- Don't force cheerfulness. Don't say "You've got this!" or "Let's figure this out together!" unless it actually fits.
|
||||
- Don't celebrate every minor thing. Don't use emoji as punctuation.
|
||||
|
||||
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.
|
||||
|
||||
Visual-first product behavior:
|
||||
- GrowQR users prefer visual cards and guided choices over long text. They are often scanning, not studying.
|
||||
- For product objects — missions/workflows, next actions, active workflow state, specialists/coaches, memory lists/searches — prefer calling the relevant UI tool so the dashboard can render cards.
|
||||
- After a tool call, add only a short human summary: one warm sentence or 1-3 bullets. Let the visual UI carry most of the information.
|
||||
- Do not use tools for casual conversation, emotional support, simple explanations, or when direct text is clearly better.
|
||||
|
||||
Tool usage rules:
|
||||
- Use tools when they materially improve comprehension, navigation, or action-taking.
|
||||
- If the user asks to see/find/discover/recommend/compare missions or workflows, call discoverWorkflows first, then summarize briefly.
|
||||
- If the user asks "what should I do next", "next steps", "today", or "missions", call showMissions.
|
||||
- If the user asks about their memory, use searchMemory, readMemory, listMemory, or memoryStats first.
|
||||
- If the user asks about specialists, coaches, interview prep, roleplay, Q Score, or resume help, use listAgentRegistry and/or askSubAgent when helpful.
|
||||
- Only start missions/workflows with startWorkflow when the user clearly agrees to start one.
|
||||
- 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."`;
|
||||
- Just talk. Answer questions, give advice, ask follow-ups. Don't follow a rigid script.
|
||||
- Only use tools when the user clearly asks for something specific (e.g., "show me my missions", "start a mission", "what's my Q Score", "save this").
|
||||
- Don't call tools for general chitchat, emotional support, simple explanations, or when a normal text answer would do.
|
||||
- If you don't know something, say so. If a tool fails, just say it failed and move on.
|
||||
- When the user asks about interview prep, roleplay, resume, or Q Score — just answer normally. Only route to a specialist tool if they explicitly say something like "connect me to the interview specialist" or "let me talk to the roleplay agent".
|
||||
- When the user asks to see missions or plans, call discoverWorkflows or showMissions. When they ask about memory, use the memory tools. Otherwise, just chat.
|
||||
- Only start a mission if the user clearly says yes to starting one. Don't push.
|
||||
- When you write memory, a quick "Saved." is enough. No need to over-confirm.`
|
||||
}
|
||||
|
||||
function safeAgentRegistry() {
|
||||
@@ -159,55 +190,49 @@ function safeAgentRegistry() {
|
||||
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"] },
|
||||
{ id: "interview", name: "Interview Agent", role: "Interview Coach", service: "interview-service", description: "Warm, direct interview practice coach for behavioral and technical interview prep.", toolNames: ["start_interview_session"] },
|
||||
{ id: "roleplay", name: "Roleplay Agent", 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: "Q Score Agent", 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: "Resume Agent", role: "Resume Agent", service: "resume-service", description: "ATS-aware resume specialist for sharper bullets, positioning, and role fit.", toolNames: ["analyze_resume", "tailor_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.
|
||||
interview: `You are the Interview Agent. You're a direct, practical interview coach. Give real advice, not generic motivation.
|
||||
|
||||
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.
|
||||
- Use STAR format for behavioral questions when it helps.
|
||||
- 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."
|
||||
- Suggest one next drill if it makes sense. Don't force it.
|
||||
|
||||
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.
|
||||
roleplay: `You are the Roleplay Agent. You're a candid, practical conversation partner for rehearsing workplace scenarios.
|
||||
|
||||
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.
|
||||
- Cover salary negotiation, recruiter calls, manager conversations, and stakeholder pushback.
|
||||
- Keep it practical. No fake cheerleading.
|
||||
|
||||
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.
|
||||
qscore: `You are the Q Score Agent. You're an analytical readiness scorer. Explain the numbers plainly.
|
||||
|
||||
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.
|
||||
- Use numbers when you have them. Don't sugarcoat or reframe weaknesses as "growth edges".
|
||||
|
||||
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.
|
||||
resume: `You are the Resume Agent. You're an ATS-aware resume strategist. Focus on clarity and impact.
|
||||
|
||||
When helping:
|
||||
- Rewrite bullets using the X-Y-Z formula: "Accomplished X as measured by Y by doing Z."
|
||||
- Rewrite bullets using the X-Y-Z formula when it helps.
|
||||
- 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.
|
||||
- Flag gaps and suggest fixes.
|
||||
- Keep headlines specific and summaries tight.
|
||||
- Suggest one concrete next step if it makes sense.
|
||||
|
||||
Return compact, bullet-heavy markdown.`,
|
||||
};
|
||||
@@ -286,9 +311,9 @@ function buildConversationTools(userId: string) {
|
||||
}),
|
||||
|
||||
askSubAgent: tool({
|
||||
description: "Route a focused question to one of four personified specialists: sara/interview, emily/roleplay, quinn/qscore, or rhea/resume.",
|
||||
description: "Route a focused question to one of four specialists: interview, roleplay, qscore, or resume.",
|
||||
inputSchema: z.object({
|
||||
agentId: z.enum(["sara", "emily", "qscore", "resume"]),
|
||||
agentId: z.enum(["interview", "roleplay", "qscore", "resume"]),
|
||||
question: z.string(),
|
||||
context: z.string().optional(),
|
||||
}),
|
||||
@@ -372,9 +397,9 @@ function buildConversationTools(userId: string) {
|
||||
goal,
|
||||
input: {},
|
||||
});
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const grow = await setupGrow(userId);
|
||||
const activeMission = await grow.registerActiveMission(activeMissionFromSnapshot(snapshot));
|
||||
await upsertActiveMissionPg(userId, activeMission, snapshot);
|
||||
return {
|
||||
kind: "workflow-started",
|
||||
workflowId,
|
||||
@@ -405,20 +430,12 @@ function buildConversationTools(userId: string) {
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const missions = await grow.listActiveMissions();
|
||||
const snapshots = await Promise.all(
|
||||
missions.map(async (mission) => {
|
||||
if (!mission.actorType) return null;
|
||||
return missionActorFor(userId, mission.instanceId, mission.actorType).getState();
|
||||
}),
|
||||
);
|
||||
const persisted = await listActiveMissionsPg(userId);
|
||||
return {
|
||||
kind: "active-workflow",
|
||||
workflow: snapshots.find(Boolean) ?? null,
|
||||
missions,
|
||||
snapshots: snapshots.filter(Boolean),
|
||||
workflow: persisted.find((item) => item.snapshot)?.snapshot ?? null,
|
||||
missions: persisted.map((item) => item.mission),
|
||||
snapshots: persisted.map((item) => item.snapshot).filter(Boolean),
|
||||
};
|
||||
} catch (err) {
|
||||
return { kind: "active-workflow", workflow: null, error: err instanceof Error ? err.message : "Could not fetch workflow" };
|
||||
@@ -490,43 +507,40 @@ export function conversationRoutes() {
|
||||
|
||||
app.post("/setup", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
return c.json(await grow.setup({ userId }));
|
||||
const conversation = await ensureConversation(userId);
|
||||
setupGrow(userId).catch((err) => console.warn("growActor setup failed after PG setup", err));
|
||||
return c.json({ userId, activeConversationId: conversation.id, conversations: await listConversationsPg(userId), activeMissions: [] });
|
||||
});
|
||||
|
||||
app.get("/", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
return c.json({ conversations: await grow.listConversations() });
|
||||
await ensureConversation(userId);
|
||||
return c.json({ conversations: await listConversationsPg(userId) });
|
||||
});
|
||||
|
||||
app.post("/", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const body = createConversationSchema.parse(await c.req.json().catch(() => ({})));
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
return c.json({ conversation: await grow.createConversation(body) }, 201);
|
||||
const conversation = await createConversationPg(userId, body.title?.trim() || "Talk to Me");
|
||||
setupGrow(userId).then((grow) => grow.createConversation({ title: conversation.title })).catch((err) => console.warn("growActor createConversation mirror failed", err));
|
||||
return c.json({ conversation }, 201);
|
||||
});
|
||||
|
||||
app.get("/:conversationId", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const conversationId = c.req.param("conversationId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const conversation = await grow.getConversation({ conversationId });
|
||||
const conversation = await getConversationPg(userId, conversationId);
|
||||
if (!conversation) return c.json({ error: "conversation_not_found" }, 404);
|
||||
const messages = await conversationFor(userId, conversationId).getHistory();
|
||||
const messages = await listMessagesPg(userId, conversationId);
|
||||
return c.json({ conversation, messages });
|
||||
});
|
||||
|
||||
app.post("/:conversationId/reset", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const conversationId = c.req.param("conversationId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const conversation = await grow.resetConversation({ conversationId });
|
||||
await conversationFor(userId, conversationId).clearHistory();
|
||||
const conversation = await resetConversationPg(userId, conversationId);
|
||||
if (!conversation) return c.json({ error: "conversation_not_found" }, 404);
|
||||
withActorRecovery("conversationActor", () => conversationFor(userId, conversationId).clearHistory()).catch((err) => console.warn("conversationActor clear mirror failed", err));
|
||||
return c.json({ conversation });
|
||||
});
|
||||
|
||||
@@ -536,14 +550,19 @@ export function conversationRoutes() {
|
||||
const body = streamSchema.parse(await c.req.json());
|
||||
const conversationId = body.conversationId ?? routeConversationId;
|
||||
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
await grow.touchConversation({ conversationId });
|
||||
const existing = await getConversationPg(userId, conversationId);
|
||||
if (!existing) return c.json({ error: "conversation_not_found" }, 404);
|
||||
await touchConversationPg(userId, conversationId);
|
||||
const growPromise = setupGrow(userId).catch((err) => {
|
||||
console.warn("growActor setup mirror failed", err);
|
||||
return null;
|
||||
});
|
||||
|
||||
const conversation = conversationFor(userId, conversationId);
|
||||
const latestUserText = textFromMessage(body.messages[body.messages.length - 1]);
|
||||
if (latestUserText) {
|
||||
await conversation.addMessage({ role: "user", content: latestUserText, sender: "User" });
|
||||
await addMessagePg(userId, { id: `user-${Date.now()}-${Math.random().toString(16).slice(2)}`, conversationId, role: "user", content: latestUserText, sender: "User" });
|
||||
withActorRecovery("conversationActor", () => conversation.addMessage({ role: "user", content: latestUserText, sender: "User" })).catch((err) => console.warn("conversationActor user-message mirror failed", err));
|
||||
}
|
||||
|
||||
const visualTool = forcedToolForPrompt(latestUserText);
|
||||
@@ -560,13 +579,22 @@ export function conversationRoutes() {
|
||||
originalMessages: body.messages,
|
||||
onFinish: async ({ responseMessage }) => {
|
||||
const assistantText = textFromMessage(responseMessage);
|
||||
await conversation.addMessage({
|
||||
await addMessagePg(userId, {
|
||||
id: responseMessage.id,
|
||||
conversationId,
|
||||
role: "assistant",
|
||||
content: assistantText || JSON.stringify(responseMessage.parts ?? []),
|
||||
sender: "GrowQR",
|
||||
});
|
||||
await grow.touchConversation({ conversationId, title: latestUserText.slice(0, 64) || undefined });
|
||||
withActorRecovery("conversationActor", () => conversation.addMessage({
|
||||
id: responseMessage.id,
|
||||
role: "assistant",
|
||||
content: assistantText || JSON.stringify(responseMessage.parts ?? []),
|
||||
sender: "GrowQR",
|
||||
})).catch((err) => console.warn("conversationActor assistant-message mirror failed", err));
|
||||
await touchConversationPg(userId, conversationId, latestUserText.slice(0, 64) || undefined);
|
||||
const grow = await growPromise;
|
||||
if (grow) await grow.touchConversation({ conversationId, title: latestUserText.slice(0, 64) || undefined }).catch((err) => console.warn("growActor touch mirror failed", err));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Registry } from "../actors/registry.js";
|
||||
import type { GrowActiveMission, MissionActorType, MissionSnapshot } from "../actors/missions/types.js";
|
||||
import { isActorBackedMission } from "../missions/registry.js";
|
||||
import { getPersistedMissionDefinition, listPersistedMissionDefinitions } from "../missions/postgres-registry.js";
|
||||
import { getActiveMissionPg, listActiveMissionsPg, upsertActiveMissionPg } from "../grow/persistence.js";
|
||||
|
||||
let _client: Client<Registry> | null = null;
|
||||
function getClient(): Client<Registry> {
|
||||
@@ -107,25 +108,18 @@ export function missionRoutes() {
|
||||
|
||||
app.get("/active", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const activeMissions = await grow.listActiveMissions();
|
||||
const snapshots = await Promise.all(activeMissions.map((mission) => getMissionSnapshot(userId, mission)));
|
||||
const persisted = await listActiveMissionsPg(userId);
|
||||
return c.json({
|
||||
missions: activeMissions,
|
||||
snapshots: snapshots.filter((snapshot): snapshot is MissionSnapshot => Boolean(snapshot)),
|
||||
missions: persisted.map((item) => item.mission),
|
||||
snapshots: persisted.map((item) => item.snapshot).filter((snapshot): snapshot is MissionSnapshot => Boolean(snapshot)),
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/active/:instanceId", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const active = await grow.getActiveMission({ instanceId: c.req.param("instanceId") });
|
||||
const active = await getActiveMissionPg(userId, c.req.param("instanceId"));
|
||||
if (!active) return c.json({ error: "mission_not_found" }, 404);
|
||||
const snapshot = await getMissionSnapshot(userId, active);
|
||||
if (!snapshot) return c.json({ error: "mission_actor_not_available" }, 404);
|
||||
return c.json({ mission: active, snapshot });
|
||||
return c.json({ mission: active.mission, snapshot: active.snapshot });
|
||||
});
|
||||
|
||||
app.post("/:missionId/start", async (c) => {
|
||||
@@ -149,17 +143,17 @@ export function missionRoutes() {
|
||||
input: body.input ?? {},
|
||||
});
|
||||
|
||||
const activeMission = activeMissionFromSnapshot(snapshot);
|
||||
await upsertActiveMissionPg(userId, activeMission, snapshot);
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const activeMission = await grow.registerActiveMission(activeMissionFromSnapshot(snapshot));
|
||||
grow.setup({ userId }).then(() => grow.registerActiveMission(activeMission)).catch((err) => console.warn("growActor mission mirror failed", err));
|
||||
return c.json({ mission: activeMission, snapshot }, 201);
|
||||
});
|
||||
|
||||
app.post("/active/:instanceId/stages/:stageId", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const active = await grow.getActiveMission({ instanceId: c.req.param("instanceId") });
|
||||
const activeRow = await getActiveMissionPg(userId, c.req.param("instanceId"));
|
||||
const active = activeRow?.mission;
|
||||
if (!active) return c.json({ error: "mission_not_found" }, 404);
|
||||
if (!active.actorType) return c.json({ error: "mission_actor_not_available" }, 404);
|
||||
|
||||
@@ -168,15 +162,14 @@ export function missionRoutes() {
|
||||
stageId: c.req.param("stageId"),
|
||||
...body,
|
||||
});
|
||||
await grow.updateActiveMission(activeMissionFromSnapshot(snapshot));
|
||||
await upsertActiveMissionPg(userId, activeMissionFromSnapshot(snapshot), snapshot);
|
||||
return c.json({ snapshot });
|
||||
});
|
||||
|
||||
app.post("/active/:instanceId/artifacts", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const active = await grow.getActiveMission({ instanceId: c.req.param("instanceId") });
|
||||
const activeRow = await getActiveMissionPg(userId, c.req.param("instanceId"));
|
||||
const active = activeRow?.mission;
|
||||
if (!active) return c.json({ error: "mission_not_found" }, 404);
|
||||
if (!active.actorType) return c.json({ error: "mission_actor_not_available" }, 404);
|
||||
|
||||
@@ -184,31 +177,29 @@ export function missionRoutes() {
|
||||
const actor = missionActorFor(userId, active.instanceId, active.actorType);
|
||||
const artifact = await actor.addArtifact(body);
|
||||
const snapshot = await actor.getState();
|
||||
await grow.updateActiveMission(activeMissionFromSnapshot(snapshot));
|
||||
await upsertActiveMissionPg(userId, activeMissionFromSnapshot(snapshot), snapshot);
|
||||
return c.json({ artifact, snapshot }, 201);
|
||||
});
|
||||
|
||||
app.post("/active/:instanceId/pause", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const active = await grow.getActiveMission({ instanceId: c.req.param("instanceId") });
|
||||
const activeRow = await getActiveMissionPg(userId, c.req.param("instanceId"));
|
||||
const active = activeRow?.mission;
|
||||
if (!active) return c.json({ error: "mission_not_found" }, 404);
|
||||
if (!active.actorType) return c.json({ error: "mission_actor_not_available" }, 404);
|
||||
const snapshot = await missionActorFor(userId, active.instanceId, active.actorType).pause();
|
||||
await grow.updateActiveMission(activeMissionFromSnapshot(snapshot));
|
||||
await upsertActiveMissionPg(userId, activeMissionFromSnapshot(snapshot), snapshot);
|
||||
return c.json({ snapshot });
|
||||
});
|
||||
|
||||
app.post("/active/:instanceId/resume", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const grow = growFor(userId);
|
||||
await grow.setup({ userId });
|
||||
const active = await grow.getActiveMission({ instanceId: c.req.param("instanceId") });
|
||||
const activeRow = await getActiveMissionPg(userId, c.req.param("instanceId"));
|
||||
const active = activeRow?.mission;
|
||||
if (!active) return c.json({ error: "mission_not_found" }, 404);
|
||||
if (!active.actorType) return c.json({ error: "mission_actor_not_available" }, 404);
|
||||
const snapshot = await missionActorFor(userId, active.instanceId, active.actorType).resume();
|
||||
await grow.updateActiveMission(activeMissionFromSnapshot(snapshot));
|
||||
await upsertActiveMissionPg(userId, activeMissionFromSnapshot(snapshot), snapshot);
|
||||
return c.json({ snapshot });
|
||||
});
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export function workflowRoutes() {
|
||||
await db.insert(workflowRunModules).values(def.modules.map((m) => ({ runId: run.id, moduleId: m.id, title: m.title, status: "idle", service: m.service })));
|
||||
await db.insert(workflowEvents).values({ runId: run.id, userId, type: "workflow.started", payload: { workflowId: def.id, goal: body.goal } });
|
||||
try {
|
||||
const baseline = await runServiceAgentProbe({ id: "qscore", name: "Quinn", role: "Q Score", kind: "score", description: "Baseline Q Score", service: "qscore-service" }, { userId, goal: body.goal ?? "workflow baseline" });
|
||||
const baseline = await runServiceAgentProbe({ id: "qscore", name: "Q Score Agent", role: "Q Score", kind: "score", description: "Baseline Q Score", service: "qscore-service" }, { userId, goal: body.goal ?? "workflow baseline" });
|
||||
if (baseline.status === "ok" && baseline.detail) {
|
||||
const payload = baseline.detail as Record<string, unknown>;
|
||||
await db.insert(qscoreSnapshots).values({ userId, runId: run.id, snapshotType: "baseline", score: extractQScore(payload), payload });
|
||||
|
||||
@@ -105,7 +105,7 @@ async function healthCheck(baseUrl: string, label: string): Promise<ServiceAgent
|
||||
}
|
||||
}
|
||||
|
||||
async function runSaraInterview(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||
async function runInterviewService(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||
const payload = {
|
||||
user_id: ctx.userId,
|
||||
org_id: ctx.orgId ?? "growqr",
|
||||
@@ -129,7 +129,7 @@ async function runSaraInterview(ctx: ServiceAgentContext): Promise<ServiceAgentR
|
||||
);
|
||||
return {
|
||||
status: "ok",
|
||||
summary: `Sara created interview session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
|
||||
summary: `Interview Agent created interview session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
|
||||
detail: {
|
||||
...detail,
|
||||
target_role: payload.context.target_role,
|
||||
@@ -139,7 +139,7 @@ async function runSaraInterview(ctx: ServiceAgentContext): Promise<ServiceAgentR
|
||||
};
|
||||
}
|
||||
|
||||
async function runEmilyRoleplay(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||
async function runRoleplayService(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||
const payload = {
|
||||
user_id: ctx.userId,
|
||||
org_id: ctx.orgId ?? "growqr",
|
||||
@@ -173,7 +173,7 @@ async function runEmilyRoleplay(ctx: ServiceAgentContext): Promise<ServiceAgentR
|
||||
);
|
||||
return {
|
||||
status: "ok",
|
||||
summary: `Emily created roleplay session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
|
||||
summary: `Roleplay Agent created roleplay session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
|
||||
detail: {
|
||||
...detail,
|
||||
target_role: payload.metadata.target_role,
|
||||
@@ -183,7 +183,7 @@ async function runEmilyRoleplay(ctx: ServiceAgentContext): Promise<ServiceAgentR
|
||||
};
|
||||
}
|
||||
|
||||
async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||
async function runQScoreService(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||
const orgId = ctx.orgId ?? "growqr";
|
||||
const qscoreUserId = stableUuid(ctx.userId);
|
||||
const signals = [
|
||||
@@ -256,7 +256,7 @@ async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentRes
|
||||
);
|
||||
return {
|
||||
status: "ok",
|
||||
summary: `Quinn estimated Q-Score ~${avgSignalScore} (service compute unavailable: formula store may not be seeded). Based on ${signals.length} signals.`,
|
||||
summary: `Q Score Agent estimated Q-Score ~${avgSignalScore} (service compute unavailable: formula store may not be seeded). Based on ${signals.length} signals.`,
|
||||
detail: {
|
||||
ingest,
|
||||
estimated_q_score: avgSignalScore,
|
||||
@@ -269,7 +269,7 @@ async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentRes
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
summary: `Quinn computed Q-Score ${compute.q_score ?? "(unknown)"} for ${ctx.goal}.`,
|
||||
summary: `Q Score Agent computed Q-Score ${compute.q_score ?? "(unknown)"} for ${ctx.goal}.`,
|
||||
detail: { ingest, compute, qscore_user_id: qscoreUserId },
|
||||
};
|
||||
}
|
||||
@@ -381,16 +381,16 @@ export async function runServiceAgentProbe(
|
||||
switch (agent.service) {
|
||||
case "interview-service":
|
||||
return ctx
|
||||
? await runSaraInterview(ctx)
|
||||
: healthCheck(config.interviewServiceUrl, "Sara / interview-service");
|
||||
? await runInterviewService(ctx)
|
||||
: healthCheck(config.interviewServiceUrl, "Interview Agent / interview-service");
|
||||
case "roleplay-service":
|
||||
return ctx
|
||||
? await runEmilyRoleplay(ctx)
|
||||
: healthCheck(config.roleplayServiceUrl, "Emily / roleplay-service");
|
||||
? await runRoleplayService(ctx)
|
||||
: healthCheck(config.roleplayServiceUrl, "Roleplay Agent / roleplay-service");
|
||||
case "qscore-service":
|
||||
return ctx
|
||||
? await runQuinnQScore(ctx)
|
||||
: healthCheck(config.qscoreServiceUrl, "Quinn / qscore-service");
|
||||
? await runQScoreService(ctx)
|
||||
: healthCheck(config.qscoreServiceUrl, "Q Score Agent / qscore-service");
|
||||
case "resume-service":
|
||||
return ctx
|
||||
? await runResumeTailor(ctx)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { WorkflowDefinition } from "./types.js";
|
||||
import { displayLabelForExecution, displayLabelForService } from "../features/registry.js";
|
||||
|
||||
const serviceSkill = (serviceId: string) => displayLabelForService(serviceId) ?? serviceId;
|
||||
const planningSkill = displayLabelForExecution("opencode") ?? "Mission Planning";
|
||||
const serviceLabel = (serviceId: string) => displayLabelForService(serviceId) ?? serviceId;
|
||||
const planningLabel = displayLabelForExecution("opencode") ?? "Mission Planning";
|
||||
|
||||
const commonInputs = [
|
||||
{ id: "goal", label: "Target outcome", type: "text", required: true },
|
||||
@@ -11,33 +11,53 @@ const commonInputs = [
|
||||
|
||||
export const workflowDefinitions: WorkflowDefinition[] = [
|
||||
{
|
||||
id: "interview-to-offer", version: "1.0.0", title: "Interview-to-Offer Accelerator", shortTitle: "Interview to Offer",
|
||||
id: "interview-to-offer", version: "1.0.0", title: "Interview to Offer", shortTitle: "Interview to Offer",
|
||||
promise: "Turn a scheduled interview into a focused prep plan, practice sessions, and a readiness report.", segment: ["job-seekers", "interviewing"], urgency: "high", estimatedDuration: "2-5 days", priceTier: "starter", sku: "workflow_interview_to_offer", isPurchasable: true, isFreePreview: true,
|
||||
visual: { icon: "briefcase-business", color: "emerald", mascotAgentIds: ["sara", "emily", "qscore"] }, requiredInputs: commonInputs,
|
||||
visual: { icon: "briefcase-business", color: "emerald", mascotAgentIds: ["interview", "roleplay", "qscore"] }, requiredInputs: commonInputs,
|
||||
modules: [
|
||||
{ id: "resume", title: "Resume fit scan", role: serviceSkill("resume-service"), description: "Analyze resume readiness for the target role.", execution: "service", service: "resume-service" },
|
||||
{ id: "interview-plan", title: "Interview prep plan", role: planningSkill, description: "Generate a prep plan and likely questions artifact.", execution: "opencode", promptPath: "prompts/workflows/interview-to-offer/interview-plan.md", artifactTypes: ["interview_plan"], approvalGateAfter: "review-plan" },
|
||||
{ id: "sara", title: "Mock interview", role: serviceSkill("interview-service"), description: "Create a real interview practice session.", execution: "service", service: "interview-service" },
|
||||
{ id: "emily", title: "Communication roleplay", role: serviceSkill("roleplay-service"), description: "Create a realistic roleplay session.", execution: "service", service: "roleplay-service" },
|
||||
{ id: "qscore", title: "Readiness Q Score", role: serviceSkill("qscore-service"), description: "Compute readiness score.", execution: "service", service: "qscore-service" },
|
||||
{ id: "resume", title: "Resume fit scan", role: serviceLabel("resume-service"), description: "Analyze resume readiness for the target role.", execution: "service", service: "resume-service" },
|
||||
{ id: "interview-plan", title: "Interview prep plan", role: planningLabel, description: "Generate a prep plan and likely questions artifact.", execution: "opencode", promptPath: "prompts/workflows/interview-to-offer/interview-plan.md", artifactTypes: ["interview_plan"], approvalGateAfter: "review-plan" },
|
||||
{ id: "interview", title: "Mock interview", role: serviceLabel("interview-service"), description: "Create a real interview practice session.", execution: "service", service: "interview-service" },
|
||||
{ id: "roleplay", title: "Communication roleplay", role: serviceLabel("roleplay-service"), description: "Create a realistic roleplay session.", execution: "service", service: "roleplay-service" },
|
||||
{ id: "branding", title: "Social branding", role: serviceLabel("social-branding-service"), description: "Optimize LinkedIn and professional presence.", execution: "service", service: "social-branding-service" },
|
||||
{ id: "matchmaking", title: "Matchmaking", role: serviceLabel("matchmaking-service"), description: "Connect with mentors and opportunities.", execution: "service", service: "matchmaking-service" },
|
||||
{ id: "qscore", title: "Readiness Q Score", role: serviceLabel("qscore-service"), description: "Compute readiness score.", execution: "service", service: "qscore-service" },
|
||||
],
|
||||
outputs: [{ id: "interview_plan", type: "markdown", title: "Interview prep plan", path: "artifacts/interview-to-offer/interview-plan.md" }], qscoreDimensions: ["clarity", "communication", "role_fit"], approvalGates: [{ id: "review-plan", title: "Review prep plan", description: "User reviews generated plan before practice.", required: false }],
|
||||
},
|
||||
{
|
||||
id: "career-transition", version: "1.0.0", title: "Career Transition Sprint", shortTitle: "Career Transition", promise: "Map transferable skills and produce a transition narrative.", segment: ["career-changers"], urgency: "medium", estimatedDuration: "1-2 weeks", priceTier: "starter", sku: "workflow_career_transition", isPurchasable: true, isFreePreview: true, visual: { icon: "route", color: "blue", mascotAgentIds: ["resume", "qscore"] }, requiredInputs: commonInputs,
|
||||
modules: [{ id: "transition-map", title: "Transition map", role: planningSkill, description: "Generate skills map and positioning narrative.", execution: "opencode", promptPath: "prompts/workflows/career-transition/orchestrator.md", artifactTypes: ["transition_map"] }, { id: "resume", title: "Resume fit scan", role: serviceSkill("resume-service"), description: "Analyze resume for target path.", execution: "service", service: "resume-service" }], outputs: [{ id: "transition_map", type: "markdown", title: "Transition map" }], qscoreDimensions: ["positioning", "skills", "confidence"], approvalGates: [],
|
||||
id: "career-transition", version: "1.0.0", title: "Switch Careers", shortTitle: "Switch Careers", promise: "Map transferable abilities and produce a transition narrative.", segment: ["career-changers"], urgency: "medium", estimatedDuration: "1-2 weeks", priceTier: "starter", sku: "workflow_career_transition", isPurchasable: true, isFreePreview: true, visual: { icon: "route", color: "blue", mascotAgentIds: ["resume", "qscore"] }, requiredInputs: commonInputs,
|
||||
modules: [
|
||||
{ id: "transition-map", title: "Transition map", role: planningLabel, description: "Generate skills map and positioning narrative.", execution: "opencode", promptPath: "prompts/workflows/career-transition/orchestrator.md", artifactTypes: ["transition_map"] },
|
||||
{ id: "resume", title: "Resume fit scan", role: serviceLabel("resume-service"), description: "Analyze resume for target path.", execution: "service", service: "resume-service" },
|
||||
{ id: "branding", title: "Social branding", role: serviceLabel("social-branding-service"), description: "Build transition narrative on LinkedIn.", execution: "service", service: "social-branding-service" },
|
||||
{ id: "matchmaking", title: "Matchmaking", role: serviceLabel("matchmaking-service"), description: "Connect with mentors in the target field.", execution: "service", service: "matchmaking-service" },
|
||||
], outputs: [{ id: "transition_map", type: "markdown", title: "Transition map" }], qscoreDimensions: ["positioning", "skills", "confidence"], approvalGates: [],
|
||||
},
|
||||
{
|
||||
id: "salary-negotiation-war-room", version: "1.0.0", title: "Salary Negotiation War Room", shortTitle: "Negotiation", promise: "Prepare scripts, ranges, and roleplay for an offer conversation.", segment: ["offer-stage"], urgency: "high", estimatedDuration: "24-72 hours", priceTier: "premium", sku: "workflow_salary_negotiation", isPurchasable: true, isFreePreview: false, visual: { icon: "badge-dollar-sign", color: "amber", mascotAgentIds: ["emily"] }, requiredInputs: commonInputs,
|
||||
modules: [{ id: "negotiation-script", title: "Negotiation script", role: planningSkill, description: "Generate negotiation strategy and scripts.", execution: "opencode", promptPath: "prompts/workflows/salary-negotiation-war-room/orchestrator.md", artifactTypes: ["negotiation_script"] }, { id: "emily", title: "Negotiation roleplay", role: serviceSkill("roleplay-service"), description: "Create offer negotiation roleplay.", execution: "service", service: "roleplay-service" }], outputs: [{ id: "negotiation_script", type: "markdown", title: "Negotiation script" }], qscoreDimensions: ["voice", "confidence", "strategy"], approvalGates: [],
|
||||
id: "salary-negotiation-war-room", version: "1.0.0", title: "Negotiate Salary", shortTitle: "Negotiate Salary", promise: "Prepare scripts, ranges, and roleplay for an offer conversation.", segment: ["offer-stage"], urgency: "high", estimatedDuration: "24-72 hours", priceTier: "premium", sku: "workflow_salary_negotiation", isPurchasable: true, isFreePreview: false, visual: { icon: "badge-dollar-sign", color: "amber", mascotAgentIds: ["roleplay"] }, requiredInputs: commonInputs,
|
||||
modules: [
|
||||
{ id: "negotiation-script", title: "Negotiation script", role: planningLabel, description: "Generate negotiation strategy and scripts.", execution: "opencode", promptPath: "prompts/workflows/salary-negotiation-war-room/orchestrator.md", artifactTypes: ["negotiation_script"] },
|
||||
{ id: "roleplay", title: "Negotiation roleplay", role: serviceLabel("roleplay-service"), description: "Create offer negotiation roleplay.", execution: "service", service: "roleplay-service" },
|
||||
{ id: "matchmaking", title: "Matchmaking", role: serviceLabel("matchmaking-service"), description: "Connect with mentors who navigated similar negotiations.", execution: "service", service: "matchmaking-service" },
|
||||
], outputs: [{ id: "negotiation_script", type: "markdown", title: "Negotiation script" }], qscoreDimensions: ["voice", "confidence", "strategy"], approvalGates: [],
|
||||
},
|
||||
{
|
||||
id: "promotion-readiness", version: "1.0.0", title: "Promotion Readiness Packet", shortTitle: "Promotion", promise: "Build an evidence packet and manager conversation plan.", segment: ["employed"], urgency: "medium", estimatedDuration: "1 week", priceTier: "starter", sku: "workflow_promotion_readiness", isPurchasable: true, isFreePreview: true, visual: { icon: "trending-up", color: "purple", mascotAgentIds: ["emily", "qscore"] }, requiredInputs: commonInputs,
|
||||
modules: [{ id: "evidence-packet", title: "Evidence packet", role: planningSkill, description: "Generate promotion evidence packet.", execution: "opencode", promptPath: "prompts/workflows/promotion-readiness/orchestrator.md", artifactTypes: ["promotion_packet"] }, { id: "emily", title: "Manager conversation roleplay", role: serviceSkill("roleplay-service"), description: "Practice the promotion conversation.", execution: "service", service: "roleplay-service" }], outputs: [{ id: "promotion_packet", type: "markdown", title: "Promotion evidence packet" }], qscoreDimensions: ["impact", "leadership", "communication"], approvalGates: [],
|
||||
id: "promotion-readiness", version: "1.0.0", title: "Get Promoted", shortTitle: "Get Promoted", promise: "Build an evidence packet and manager conversation plan.", segment: ["employed"], urgency: "medium", estimatedDuration: "1 week", priceTier: "starter", sku: "workflow_promotion_readiness", isPurchasable: true, isFreePreview: true, visual: { icon: "trending-up", color: "purple", mascotAgentIds: ["roleplay", "qscore"] }, requiredInputs: commonInputs,
|
||||
modules: [
|
||||
{ id: "evidence-packet", title: "Evidence packet", role: planningLabel, description: "Generate promotion evidence packet.", execution: "opencode", promptPath: "prompts/workflows/promotion-readiness/orchestrator.md", artifactTypes: ["promotion_packet"] },
|
||||
{ id: "roleplay", title: "Manager conversation roleplay", role: serviceLabel("roleplay-service"), description: "Practice the promotion conversation.", execution: "service", service: "roleplay-service" },
|
||||
{ id: "branding", title: "Social branding", role: serviceLabel("social-branding-service"), description: "Showcase promotion-worthy impact on LinkedIn.", execution: "service", service: "social-branding-service" },
|
||||
{ id: "matchmaking", title: "Matchmaking", role: serviceLabel("matchmaking-service"), description: "Connect with senior leaders for sponsorship.", execution: "service", service: "matchmaking-service" },
|
||||
], outputs: [{ id: "promotion_packet", type: "markdown", title: "Promotion evidence packet" }], qscoreDimensions: ["impact", "leadership", "communication"], approvalGates: [],
|
||||
},
|
||||
{
|
||||
id: "personal-brand-opportunity-engine", version: "1.0.0", title: "Personal Brand Opportunity Engine", shortTitle: "Brand Engine", promise: "Draft profile positioning and a weekly opportunity/content plan.", segment: ["networking", "creators"], urgency: "low", estimatedDuration: "1 week", priceTier: "starter", sku: "workflow_brand_engine", isPurchasable: true, isFreePreview: true, visual: { icon: "sparkles", color: "pink", mascotAgentIds: ["qscore"] }, requiredInputs: commonInputs,
|
||||
modules: [{ id: "profile-rewrite", title: "Profile rewrite", role: planningSkill, description: "Generate LinkedIn/profile rewrite draft.", execution: "opencode", promptPath: "prompts/workflows/personal-brand-opportunity-engine/orchestrator.md", artifactTypes: ["profile_rewrite", "content_plan"] }], outputs: [{ id: "profile_rewrite", type: "markdown", title: "Profile rewrite" }, { id: "content_plan", type: "markdown", title: "Weekly content plan" }], qscoreDimensions: ["visibility", "network", "voice"], approvalGates: [],
|
||||
id: "personal-brand-opportunity-engine", version: "1.0.0", title: "Build Your Brand", shortTitle: "Build Your Brand", promise: "Draft profile positioning and a weekly opportunity/content plan.", segment: ["networking", "creators"], urgency: "low", estimatedDuration: "1 week", priceTier: "starter", sku: "workflow_brand_engine", isPurchasable: true, isFreePreview: true, visual: { icon: "sparkles", color: "pink", mascotAgentIds: ["qscore"] }, requiredInputs: commonInputs,
|
||||
modules: [
|
||||
{ id: "profile-rewrite", title: "Profile rewrite", role: planningLabel, description: "Generate LinkedIn/profile rewrite draft.", execution: "opencode", promptPath: "prompts/workflows/personal-brand-opportunity-engine/orchestrator.md", artifactTypes: ["profile_rewrite", "content_plan"] },
|
||||
{ id: "branding", title: "Social branding", role: serviceLabel("social-branding-service"), description: "Optimize profile and content strategy.", execution: "service", service: "social-branding-service" },
|
||||
{ id: "matchmaking", title: "Matchmaking", role: serviceLabel("matchmaking-service"), description: "Connect with collaborators and brand amplifiers.", execution: "service", service: "matchmaking-service" },
|
||||
], outputs: [{ id: "profile_rewrite", type: "markdown", title: "Profile rewrite" }, { id: "content_plan", type: "markdown", title: "Weekly content plan" }], qscoreDimensions: ["visibility", "network", "voice"], approvalGates: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listFeatureDefinitions, internalWorkflowSkills } from "../features/registry.js";
|
||||
import { listFeatureDefinitions, internalWorkflowModules } from "../features/registry.js";
|
||||
|
||||
export type ServiceCapability = {
|
||||
id: string;
|
||||
@@ -23,9 +23,9 @@ export function listServiceCapabilities(): ServiceCapability[] {
|
||||
featureId: feature.id,
|
||||
promptModulePath: feature.promptModulePath,
|
||||
})),
|
||||
...internalWorkflowSkills.map((skill) => ({
|
||||
id: skill.id,
|
||||
name: skill.title,
|
||||
...internalWorkflowModules.map((module) => ({
|
||||
id: module.id,
|
||||
name: module.title,
|
||||
enabled: true,
|
||||
operations: ["artifact.prepare", "artifact.generate"],
|
||||
})),
|
||||
|
||||
@@ -12,7 +12,7 @@ export type WorkflowModuleDefinition = {
|
||||
role: string;
|
||||
description: string;
|
||||
execution: WorkflowModuleExecution;
|
||||
service?: "resume-service" | "interview-service" | "roleplay-service" | "qscore-service";
|
||||
service?: "resume-service" | "interview-service" | "roleplay-service" | "qscore-service" | "social-branding-service" | "matchmaking-service";
|
||||
promptPath?: string;
|
||||
artifactTypes?: string[];
|
||||
approvalGateAfter?: string;
|
||||
|
||||
Reference in New Issue
Block a user