
Resume
- 5 installs
- Updated April 17, 2026
- aas-ee/resume-skills
resume is a Claude Code skill that runs a template-first resume-building workflow with structured intake and markdown/html/css rendering.
About
resume is a Claude Code skill that runs a template-first resume workflow across agent environments. It chooses a template, then collects content through direct entry, material parsing, or structured follow-up questions before drafting and rendering. A developer uses it to improve a resume or turn raw notes into resume content. It ships Python CLIs for the template catalog, intake sessions, and markdown/html/css rendering.
- Template-first resume intake and drafting workflow
- Agent-neutral CLIs for catalog, intake, and rendering
- Renders markdown/html/css resume output bundles
Resume by the numbers
- 5 all-time installs (skills.sh)
- Ranked #528 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
resume capabilities & compatibility
- Capabilities
- resume building · template intake · document rendering
- Use cases
- resume · documentation
- Pricing
- Free
What resume says it does
Use when improving a resume, extracting resume material, choosing a resume template, or running a template-first intake workflow in any agent environment.
The current built-in templates are `typora-classic` and `markdown-basic`
npx skills add https://github.com/aas-ee/resume-skills --skill resumeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| Last updated | April 17, 2026 |
| Repository | aas-ee/resume-skills ↗ |
What it does
Run a template-first resume workflow: pick a template, collect content via structured intake, draft, and render markdown/html/css output.
When should I use this skill?
When improving a resume, extracting resume material, choosing a resume template, or running structured resume intake.
What you get
- Resume draft
- Rendered markdown/html/css bundle
By the numbers
- 2 built-in templates (typora-classic, markdown-basic)
- 5 public CLI entrypoints
Files
Shared Resume Workflow
This is the shared, agent-neutral definition of the resume workflow.
Use it when you need to:
- improve an existing resume draft;
- turn project notes or raw material into resume content;
- select a template before intake;
- run structured guided intake for missing fields;
- keep the workflow reusable across Claude and non-Claude agents.
Core rule
This installed skill bundle is self-contained.
When the skill is installed correctly, this directory should contain:
template_catalog_cli.pytemplate_store_cli.pyagent_intake_cli.pyhost_cli.pyrender_cli.pyresume_runtime/resume_core/examples/
Treat the bundled resume_runtime/ under this skill directory as the primary shared interface.
Use these public entrypoints:
template_catalog_cli.py— list built-in and stored templates and derivetemplate_contexttemplate_store_cli.py— save personal templates and promote candidate templatesagent_intake_cli.py— host-facing outer intake entrypointhost_cli.py— lower-level structured session controlrender_cli.py— render markdown/html/css output bundles from a manifest and profile
Equivalent bundled module paths also exist under:
resume_runtime/template_catalog_cli.pyresume_runtime/template_store_cli.pyresume_runtime/agent_intake_cli.pyresume_runtime/host_cli.pyresume_runtime/render_cli.py
Claude-specific wrappers under .claude/skills/resume/ are compatibility adapters, not the primary shared contract.
Recommended workflow
1. Choose the template first
Before parsing materials or asking for missing information, choose a template.
- In the installed skill bundle, built-in template manifests live under
resume_core/examples/templates/ - Built-in template assets, including CSS, live under
resume_core/examples/template-assets/ - The current built-in templates are
typora-classicandmarkdown-basic - load built-in and stored templates through
template_catalog_cli.py - read
asset_pathsfrom the catalog CLI response when you need the concrete markdown/html/css file locations for a template - show template cards with template id, style, use cases, and required-content summary
- let the user choose a built-in template, a stored template, upload a new template, or ask for a derivative template
- once a template is chosen, use the returned
manifest+ derivedchecklistas thetemplate_contextforagent_intake_cli.py
If you know the absolute path of this installed skill directory, enumerate built-in templates with:
python3 /path/to/installed/skill/template_catalog_cli.pyIf you are already in the installed skill directory, you can also run:
python3 template_catalog_cli.pyIf you are developing inside this repository root, the bundled entrypoint is:
python3 skills/resume/template_catalog_cli.pyYou can still override the defaults when needed:
python3 /path/to/installed/skill/template_catalog_cli.py \
--examples-root /path/to/installed/skill/resume_core/examples \
--generated-at 2026-04-16T12:00:00ZIf you need to inspect a built-in template directly, start from:
resume_core/examples/templates/typora-classic.v1.jsonresume_core/examples/templates/markdown-basic.v1.jsonresume_core/examples/template-assets/typora-classic/style.cssresume_core/examples/template-assets/markdown-basic/style.css
Do not assume the template files live beside skills/resume/SKILL.md. The skill file defines the workflow; the built-in template manifests and assets live in the repository paths above.
2. Choose the content path after template selection
Use one of these paths:
- direct content entry
- parsing existing materials
- follow-up questioning only for missing template-required fields
If the work is straightforward rewrite/polish, stay in direct editing. If the work depends on structured intake state, missing-field projection, or follow-up rounds, route through the public intake/session CLIs.
3. Use the public intake flow for structured collection
For structured intake and follow-up:
- call
resume_runtime/agent_intake_cli.pyas the public outer entrypoint - provide
template_contextwhen a template has already been chosen - let the runtime decide whether the turn should stay freeform, ask for material, parse material, continue a structured session, or hand off to drafting
- only ask the current batch of questions materialized by the runtime
- at the recommended-only boundary, collect only a yes/no continuation decision
4. Draft against the selected template
When enough information is available:
- produce the resume draft that matches the selected template’s required structure
- localize section headings, field labels, and fixed template copy to match the user’s language; Chinese requests should render Chinese labels by default
- preserve evidence-based wording and truthful ownership
- list any remaining missing facts explicitly instead of guessing
5. Render final template artifacts through the public renderer
When a profile is ready:
- use
resume_runtime/render_cli.pyor the shared rendering helpers - generate markdown/html/css outputs from the selected manifest and profile
- treat these rendered assets as the portable output bundle for host integrations and Typora-first preview flows
Host-neutral request shape
When calling resume_runtime/agent_intake_cli.py, use the public request envelope documented in README.md and this shared skill.
At minimum, expect these concepts:
versionturn- optional
template_context - optional
materials - optional
drafting_started
Output expectations
A good run should usually produce:
- a selected template or template decision
- a filled or partially filled profile
- a clear missing-information list when facts are still absent
- a resume draft or rendered output bundle when enough information is present
Boundary rule
Shared workflow logic belongs in resume_runtime/ and shared docs like this file. Agent-specific prompt shaping belongs in that agent’s adapter layer.
"""Self-contained shared resume skill bundle."""
from __future__ import annotations
import sys
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parent
if str(SKILL_ROOT) not in sys.path:
sys.path.insert(0, str(SKILL_ROOT))
from resume_runtime.agent_intake_cli import main
if __name__ == "__main__":
raise SystemExit(main())
from __future__ import annotations
import sys
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parent
if str(SKILL_ROOT) not in sys.path:
sys.path.insert(0, str(SKILL_ROOT))
from resume_runtime.host_cli import main
if __name__ == "__main__":
raise SystemExit(main())
from __future__ import annotations
import sys
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parent
if str(SKILL_ROOT) not in sys.path:
sys.path.insert(0, str(SKILL_ROOT))
from resume_runtime.render_cli import main
if __name__ == "__main__":
raise SystemExit(main())
{
"projectionId": "follow-up-profile-projection-for-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"followUpResponseSetId": "follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"followUpQuestionSetId": "follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"reportId": "gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"templateId": "markdown-basic",
"templateVersion": "1.0.0",
"baseProfileId": "profile-from-guided-intake-response-set-markdown-basic",
"profile": {
"profileId": "profile-from-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"fieldValues": {
"basic.email": "alex@example.com",
"basic.name": "Alex Example",
"education[].school": "Example University",
"links.github": "https://github.com/alex-example",
"project[].bullets": [
"Implemented a Markdown-first resume template workflow for public examples.",
"Validated question-set and response-set contracts with unittest."
],
"project[].role": "Backend Engineer"
},
"provenance": {
"basic.email": ["guided-intake-response-set-markdown-basic"],
"basic.name": ["guided-intake-response-set-markdown-basic"],
"education[].school": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic"],
"links.github": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic"],
"project[].bullets": ["guided-intake-response-set-markdown-basic"],
"project[].role": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic"]
},
"profileStatus": "partial",
"updatedAt": "2026-04-12T10:05:00Z"
}
}
{
"projectionId": "follow-up-profile-projection-for-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"followUpResponseSetId": "follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"followUpQuestionSetId": "follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"reportId": "gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"templateId": "typora-classic",
"templateVersion": "1.0.0",
"baseProfileId": "profile-from-guided-intake-response-set-typora-classic",
"profile": {
"profileId": "profile-from-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"fieldValues": {
"basic.name": "Alex Example",
"basic.nameEn": "Alex Example",
"basic.phone": "+1 555-0100",
"basic.email": "alex@example.com",
"required.role": "Software Engineer",
"links.github": "https://github.com/alex-example",
"summary.items": [
"Builds reusable workflow tooling.",
"Turns structured intake into production-ready artifacts."
],
"skills.items": [
"Engineering: Python / TypeScript / CLI tooling",
"Platforms: Resume workflows / template packaging / render pipelines"
],
"work[].date": "2024.01 - Present",
"work[].company": "Example Labs",
"work[].role": "Software Engineer",
"work[].bullets": [
"Built shared runtime flows for multiple hosts.",
"Improved validation coverage for synthetic public fixtures."
],
"project[].date": "2025.01 - Present",
"project[].name": "Example Stream",
"project[].role": "Open Source Maintainer",
"project[].techStack": ["TypeScript", "Node.js", "Express", "CLI", "Automation"],
"project[].bullets": [
"Published synthetic examples that explain the resume workflow end to end.",
"Turned structured intake data into reusable rendering and validation fixtures."
],
"education[].date": "2018.09 - 2022.06",
"education[].school": "Example University",
"education[].degree": "BSc",
"education[].major": "Computer Science"
},
"provenance": {
"basic.name": ["guided-intake-response-set-typora-classic"],
"basic.nameEn": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"basic.phone": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"basic.email": ["guided-intake-response-set-typora-classic"],
"required.role": ["guided-intake-response-set-typora-classic"],
"links.github": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"summary.items": ["guided-intake-response-set-typora-classic"],
"skills.items": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"work[].date": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"work[].company": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"work[].role": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"work[].bullets": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"project[].date": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"project[].name": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"project[].role": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"project[].techStack": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"project[].bullets": ["guided-intake-response-set-typora-classic"],
"education[].date": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"education[].school": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"education[].degree": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"],
"education[].major": ["follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic"]
},
"profileStatus": "partial",
"updatedAt": "2026-04-12T10:00:00Z"
}
}
{
"followUpQuestionSetId": "follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"reportId": "gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"templateId": "markdown-basic",
"profileId": "profile-from-guided-intake-response-set-markdown-basic",
"questions": [
{
"fieldId": "links.github",
"question": "Please provide Optional GitHub link below the title."
},
{
"fieldId": "summary.items",
"question": "Please provide Short summary bullets."
},
{
"fieldId": "education[].school",
"question": "Please provide School name."
},
{
"fieldId": "education[].major",
"question": "Please provide Major name."
},
{
"fieldId": "education[].degree",
"question": "Please provide Degree label."
},
{
"fieldId": "project[].name",
"question": "Please provide Project title."
},
{
"fieldId": "project[].role",
"question": "Please provide Project role label."
}
],
"generatedAt": "2026-04-11T10:25:00Z"
}
{
"followUpQuestionSetId": "follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"reportId": "gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"templateId": "typora-classic",
"profileId": "profile-from-guided-intake-response-set-typora-classic",
"questions": [
{
"fieldId": "basic.nameEn",
"question": "Please provide Optional second-language name shown beside the main name."
},
{
"fieldId": "basic.phone",
"question": "Please provide Phone number shown in the contact rows."
},
{
"fieldId": "links.github",
"question": "Please provide GitHub or portfolio link if it strengthens the resume."
},
{
"fieldId": "skills.items",
"question": "Please provide Repeatable skill lines that stay profession-neutral."
},
{
"fieldId": "work[].date",
"question": "Please provide Date range for each work experience row."
},
{
"fieldId": "work[].company",
"question": "Please provide Company or organization name for each work row."
},
{
"fieldId": "work[].role",
"question": "Please provide Role title for each work row."
},
{
"fieldId": "work[].bullets",
"question": "Please provide Bullets describing responsibilities or impact for each work entry."
},
{
"fieldId": "project[].date",
"question": "Please provide Date range shown in the project table."
},
{
"fieldId": "project[].name",
"question": "Please provide Project or initiative name."
},
{
"fieldId": "project[].role",
"question": "Please provide Role label shown on the project row."
},
{
"fieldId": "project[].techStack",
"question": "Please provide Short stack or descriptor line shown in the meta row."
},
{
"fieldId": "education[].date",
"question": "Please provide Date range for each education entry."
},
{
"fieldId": "education[].school",
"question": "Please provide School name for each education entry."
},
{
"fieldId": "education[].degree",
"question": "Please provide Degree label for each education entry."
},
{
"fieldId": "education[].major",
"question": "Please provide Major for each education entry."
}
],
"generatedAt": "2026-04-11T10:20:00Z"
}
{
"followUpResponseSetId": "follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"followUpQuestionSetId": "follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"reportId": "gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"templateId": "markdown-basic",
"profileId": "profile-from-guided-intake-response-set-markdown-basic",
"responses": {
"links.github": "https://github.com/alex-example",
"education[].school": "Example University",
"project[].role": "Backend Engineer"
},
"updatedAt": "2026-04-12T10:05:00Z"
}
{
"followUpResponseSetId": "follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"followUpQuestionSetId": "follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"reportId": "gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"templateId": "typora-classic",
"profileId": "profile-from-guided-intake-response-set-typora-classic",
"responses": {
"basic.nameEn": "Alex Example",
"basic.phone": "+1 555-0100",
"links.github": "https://github.com/alex-example",
"skills.items": [
"Engineering: Python / TypeScript / CLI tooling",
"Platforms: Resume workflows / template packaging / render pipelines"
],
"work[].date": "2024.01 - Present",
"work[].company": "Example Labs",
"work[].role": "Software Engineer",
"work[].bullets": [
"Built shared runtime flows for multiple hosts.",
"Improved validation coverage for synthetic public fixtures."
],
"project[].date": "2025.01 - Present",
"project[].name": "Example Stream",
"project[].role": "Open Source Maintainer",
"project[].techStack": ["TypeScript", "Node.js", "Express", "CLI", "Automation"],
"education[].date": "2018.09 - 2022.06",
"education[].school": "Example University",
"education[].degree": "BSc",
"education[].major": "Computer Science"
},
"updatedAt": "2026-04-12T10:00:00Z"
}
{
"reportId": "gap-for-profile-from-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic-markdown-basic",
"templateId": "markdown-basic",
"profileId": "profile-from-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"missingRequired": [
"education[].major",
"education[].degree",
"project[].name"
],
"missingRecommended": [
"summary.items"
],
"conflicts": [],
"questions": [
{
"fieldId": "summary.items",
"question": "Please provide Short summary bullets."
},
{
"fieldId": "education[].major",
"question": "Please provide Major name."
},
{
"fieldId": "education[].degree",
"question": "Please provide Degree label."
},
{
"fieldId": "project[].name",
"question": "Please provide Project title."
}
],
"generatedAt": "2026-04-12T10:15:00Z"
}
{
"reportId": "gap-for-profile-from-guided-intake-response-set-markdown-basic-markdown-basic",
"templateId": "markdown-basic",
"profileId": "profile-from-guided-intake-response-set-markdown-basic",
"missingRequired": [
"education[].school",
"education[].major",
"education[].degree",
"project[].name",
"project[].role"
],
"missingRecommended": [
"links.github",
"summary.items"
],
"conflicts": [],
"questions": [
{
"fieldId": "links.github",
"question": "Please provide Optional GitHub link below the title."
},
{
"fieldId": "summary.items",
"question": "Please provide Short summary bullets."
},
{
"fieldId": "education[].school",
"question": "Please provide School name."
},
{
"fieldId": "education[].major",
"question": "Please provide Major name."
},
{
"fieldId": "education[].degree",
"question": "Please provide Degree label."
},
{
"fieldId": "project[].name",
"question": "Please provide Project title."
},
{
"fieldId": "project[].role",
"question": "Please provide Project role label."
}
],
"generatedAt": "2026-04-11T10:15:00Z"
}
{
"reportId": "gap-for-profile-from-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic-typora-classic",
"templateId": "typora-classic",
"profileId": "profile-from-follow-up-response-for-follow-up-for-gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"missingRequired": [],
"missingRecommended": [],
"conflicts": [],
"questions": [],
"generatedAt": "2026-04-12T10:10:00Z"
}
{
"reportId": "gap-for-profile-from-guided-intake-response-set-typora-classic-typora-classic",
"templateId": "typora-classic",
"profileId": "profile-from-guided-intake-response-set-typora-classic",
"missingRequired": [
"project[].date",
"project[].name",
"project[].role",
"education[].school",
"education[].degree",
"education[].major"
],
"missingRecommended": [
"basic.nameEn",
"basic.phone",
"links.github",
"skills.items",
"work[].date",
"work[].company",
"work[].role",
"work[].bullets",
"project[].techStack",
"education[].date"
],
"conflicts": [],
"questions": [
{
"fieldId": "basic.nameEn",
"question": "Please provide Optional second-language name shown beside the main name."
},
{
"fieldId": "basic.phone",
"question": "Please provide Phone number shown in the contact rows."
},
{
"fieldId": "links.github",
"question": "Please provide GitHub or portfolio link if it strengthens the resume."
},
{
"fieldId": "skills.items",
"question": "Please provide Repeatable skill lines that stay profession-neutral."
},
{
"fieldId": "work[].date",
"question": "Please provide Date range for each work experience row."
},
{
"fieldId": "work[].company",
"question": "Please provide Company or organization name for each work row."
},
{
"fieldId": "work[].role",
"question": "Please provide Role title for each work row."
},
{
"fieldId": "work[].bullets",
"question": "Please provide Bullets describing responsibilities or impact for each work entry."
},
{
"fieldId": "project[].date",
"question": "Please provide Date range shown in the project table."
},
{
"fieldId": "project[].name",
"question": "Please provide Project or initiative name."
},
{
"fieldId": "project[].role",
"question": "Please provide Role label shown on the project row."
},
{
"fieldId": "project[].techStack",
"question": "Please provide Short stack or descriptor line shown in the meta row."
},
{
"fieldId": "education[].date",
"question": "Please provide Date range for each education entry."
},
{
"fieldId": "education[].school",
"question": "Please provide School name for each education entry."
},
{
"fieldId": "education[].degree",
"question": "Please provide Degree label for each education entry."
},
{
"fieldId": "education[].major",
"question": "Please provide Major for each education entry."
}
],
"generatedAt": "2026-04-11T10:10:00Z"
}
{
"checklistId": "guided-intake-markdown-basic",
"templateId": "markdown-basic",
"templateVersion": "1.0.0",
"requiredFields": [
"basic.name",
"basic.email",
"education[].school",
"education[].major",
"education[].degree",
"project[].name",
"project[].role",
"project[].bullets"
],
"optionalFields": [
"links.github",
"summary.items"
],
"repeatableFields": [
"summary.items",
"education[].school",
"education[].major",
"education[].degree",
"project[].name",
"project[].role",
"project[].bullets"
],
"generatedAt": "2026-04-10T10:05:00Z"
}
{
"checklistId": "guided-intake-typora-classic",
"templateId": "typora-classic",
"templateVersion": "1.0.0",
"requiredFields": [
"basic.name",
"basic.email",
"required.role",
"project[].date",
"project[].name",
"project[].role",
"project[].bullets",
"education[].school",
"education[].degree",
"education[].major"
],
"optionalFields": [
"basic.nameEn",
"basic.phone",
"links.github",
"summary.items",
"skills.items",
"work[].date",
"work[].company",
"work[].role",
"work[].bullets",
"project[].techStack",
"education[].date"
],
"repeatableFields": [
"summary.items",
"skills.items",
"work[].date",
"work[].company",
"work[].role",
"work[].bullets",
"project[].date",
"project[].name",
"project[].role",
"project[].techStack",
"project[].bullets",
"education[].date",
"education[].school",
"education[].degree",
"education[].major"
],
"generatedAt": "2026-04-10T10:00:00Z"
}
{
"projectionId": "guided-intake-profile-projection-markdown-basic",
"responseSetId": "guided-intake-response-set-markdown-basic",
"questionSetId": "guided-intake-question-set-markdown-basic",
"templateId": "markdown-basic",
"templateVersion": "1.0.0",
"profile": {
"profileId": "profile-from-guided-intake-response-set-markdown-basic",
"fieldValues": {
"basic.name": "Alex Example",
"basic.email": "alex@example.com",
"project[].bullets": [
"Implemented a Markdown-first resume template workflow for public examples.",
"Validated question-set and response-set contracts with unittest."
]
},
"provenance": {
"basic.name": ["guided-intake-response-set-markdown-basic"],
"basic.email": ["guided-intake-response-set-markdown-basic"],
"project[].bullets": ["guided-intake-response-set-markdown-basic"]
},
"profileStatus": "partial",
"updatedAt": "2026-04-11T10:05:00Z"
}
}
{
"projectionId": "guided-intake-profile-projection-typora-classic",
"responseSetId": "guided-intake-response-set-typora-classic",
"questionSetId": "guided-intake-question-set-typora-classic",
"templateId": "typora-classic",
"templateVersion": "1.0.0",
"profile": {
"profileId": "profile-from-guided-intake-response-set-typora-classic",
"fieldValues": {
"basic.name": "Alex Example",
"basic.email": "alex@example.com",
"required.role": "Software Engineer",
"summary.items": [
"Builds reusable workflow tooling.",
"Turns structured intake into production-ready artifacts."
],
"project[].bullets": [
"Published synthetic examples that explain the resume workflow end to end.",
"Turned structured intake data into reusable rendering and validation fixtures."
]
},
"provenance": {
"basic.name": ["guided-intake-response-set-typora-classic"],
"basic.email": ["guided-intake-response-set-typora-classic"],
"required.role": ["guided-intake-response-set-typora-classic"],
"summary.items": ["guided-intake-response-set-typora-classic"],
"project[].bullets": ["guided-intake-response-set-typora-classic"]
},
"profileStatus": "partial",
"updatedAt": "2026-04-11T10:00:00Z"
}
}
{
"questionSetId": "guided-intake-question-set-markdown-basic",
"templateId": "markdown-basic",
"templateVersion": "1.0.0",
"questions": [
{
"fieldId": "basic.name",
"question": "Please provide Candidate name in the Markdown title."
},
{
"fieldId": "basic.email",
"question": "Please provide Primary email below the title."
},
{
"fieldId": "education[].school",
"question": "Please provide School name."
},
{
"fieldId": "education[].major",
"question": "Please provide Major name."
},
{
"fieldId": "education[].degree",
"question": "Please provide Degree label."
},
{
"fieldId": "project[].name",
"question": "Please provide Project title."
},
{
"fieldId": "project[].role",
"question": "Please provide Project role label."
},
{
"fieldId": "project[].bullets",
"question": "Please provide Project bullets."
},
{
"fieldId": "links.github",
"question": "Please provide Optional GitHub link below the title."
},
{
"fieldId": "summary.items",
"question": "Please provide Short summary bullets."
}
],
"generatedAt": "2026-04-10T11:05:00Z"
}
{
"questionSetId": "guided-intake-question-set-typora-classic",
"templateId": "typora-classic",
"templateVersion": "1.0.0",
"questions": [
{
"fieldId": "basic.name",
"question": "Please provide Name shown in the first header row."
},
{
"fieldId": "basic.email",
"question": "Please provide Primary email shown in the contact rows."
},
{
"fieldId": "required.role",
"question": "Please provide Target role or direction shown in the final header row."
},
{
"fieldId": "project[].date",
"question": "Please provide Date range shown in the project table."
},
{
"fieldId": "project[].name",
"question": "Please provide Project or initiative name."
},
{
"fieldId": "project[].role",
"question": "Please provide Role label shown on the project row."
},
{
"fieldId": "project[].bullets",
"question": "Please provide Bullets describing contribution and impact."
},
{
"fieldId": "education[].school",
"question": "Please provide School name for each education entry."
},
{
"fieldId": "education[].degree",
"question": "Please provide Degree label for each education entry."
},
{
"fieldId": "education[].major",
"question": "Please provide Major for each education entry."
},
{
"fieldId": "basic.nameEn",
"question": "Please provide Optional second-language name shown beside the main name."
},
{
"fieldId": "basic.phone",
"question": "Please provide Phone number shown in the contact rows."
},
{
"fieldId": "links.github",
"question": "Please provide GitHub or portfolio link if it strengthens the resume."
},
{
"fieldId": "summary.items",
"question": "Please provide Two or more short summary bullets."
},
{
"fieldId": "skills.items",
"question": "Please provide Repeatable skill lines that stay profession-neutral."
},
{
"fieldId": "work[].date",
"question": "Please provide Date range for each work experience row."
},
{
"fieldId": "work[].company",
"question": "Please provide Company or organization name for each work row."
},
{
"fieldId": "work[].role",
"question": "Please provide Role title for each work row."
},
{
"fieldId": "work[].bullets",
"question": "Please provide Bullets describing responsibilities or impact for each work entry."
},
{
"fieldId": "project[].techStack",
"question": "Please provide Short stack or descriptor line shown in the meta row."
},
{
"fieldId": "education[].date",
"question": "Please provide Date range for each education entry."
}
],
"generatedAt": "2026-04-10T11:00:00Z"
}
{
"responseSetId": "guided-intake-response-set-markdown-basic",
"templateId": "markdown-basic",
"templateVersion": "1.0.0",
"questionSetId": "guided-intake-question-set-markdown-basic",
"responses": {
"basic.name": "Alex Example",
"basic.email": "alex@example.com",
"project[].bullets": [
"Implemented a Markdown-first resume template workflow for public examples.",
"Validated question-set and response-set contracts with unittest."
]
},
"updatedAt": "2026-04-11T10:05:00Z"
}
{
"responseSetId": "guided-intake-response-set-typora-classic",
"templateId": "typora-classic",
"templateVersion": "1.0.0",
"questionSetId": "guided-intake-question-set-typora-classic",
"responses": {
"basic.name": "Alex Example",
"basic.email": "alex@example.com",
"required.role": "Software Engineer",
"summary.items": [
"Builds reusable workflow tooling.",
"Turns structured intake into production-ready artifacts."
],
"project[].bullets": [
"Published synthetic examples that explain the resume workflow end to end.",
"Turned structured intake data into reusable rendering and validation fixtures."
]
},
"updatedAt": "2026-04-11T10:00:00Z"
}
{
"sessionId": "intake-markdown-manual-override",
"templateId": "markdown-basic",
"templateVersion": "1.0.0",
"hasExistingMaterial": true,
"documentIds": ["source-existing-resume-md"],
"phase": "handed-off",
"route": "guided-intake",
"status": "completed",
"createdAt": "2026-04-10T09:20:00Z",
"updatedAt": "2026-04-10T09:23:00Z"
}
{
"sessionId": "intake-typora-guided-empty",
"templateId": "typora-classic",
"templateVersion": "1.0.0",
"hasExistingMaterial": false,
"documentIds": [],
"phase": "handed-off",
"route": "guided-intake",
"status": "completed",
"createdAt": "2026-04-10T09:10:00Z",
"updatedAt": "2026-04-10T09:12:00Z"
}
{
"sessionId": "intake-typora-import-existing",
"templateId": "typora-classic",
"templateVersion": "1.0.0",
"hasExistingMaterial": true,
"documentIds": ["source-existing-resume-md"],
"phase": "handed-off",
"route": "import-existing",
"status": "completed",
"createdAt": "2026-04-10T09:00:00Z",
"updatedAt": "2026-04-10T09:03:00Z"
}
Public Resume Examples
All files in this directory are synthetic fixtures for schema, validator, and runtime demonstrations. They are not redacted copies of a real candidate resume.
Baseline templates
The public baseline templates are:
typora-classicmarkdown-basic
Start with the shared template artifacts
Read these first:
shared-field-catalog.v1.jsontemplate-registry.v1.jsontemplates/typora-classic.v1.jsontemplates/markdown-basic.v1.json
Template asset directories
Each built-in template now points to concrete assets in template-assets/:
template.md— markdown-first editable templatetemplate.html— HTML rendering templatestyle.css— reusable style asset for the Typora-first template package and later host reuse
For typora-classic, the markdown asset is the primary authoring surface and closely follows a Typora-style table-based printable resume layout.
Built-in template gallery
Each built-in template manifest includes a previewCard block that hosts can surface as a template-selection card before intake starts.
Hosts can list those cards through the public catalog CLI:
python3 resume_runtime/template_catalog_cli.pyThe CLI response includes one card per template, derived directly from each manifest's previewCard, alongside the matching template_context payload for downstream intake and asset_paths for the resolved markdown/html/css files.
Raw material to extracted facts
This chain shows how one synthetic source document becomes extracted facts and then a reusable profile:
1. source-documents/existing-resume-markdown.v1.json 2. source-extractions/extract-basic-name.v1.json 3. source-extractions/extract-github-link.v1.json 4. source-extractions/extract-project-name.v1.json 5. source-extractions/extract-project-role.v1.json 6. source-extractions/extract-project-tech-stack.v1.json 7. resume-profiles/sample-ai-agent-profile.v1.json
Typora Classic reading path
The Typora-first package is the primary end-to-end example. It shows both import-existing and guided-intake entry points, then one guided round and one follow-up round:
intake-sessions/typora-import-existing.v1.jsonintake-sessions/typora-guided-empty.v1.jsonguided-intake-checklists/typora-classic.v1.jsonguided-intake-question-sets/typora-classic.v1.jsonguided-intake-response-sets/typora-classic.partial.v1.jsonguided-intake-profile-projections/typora-classic.partial.v1.jsongap-reports/typora-classic-gap.v1.jsonfollow-up-question-sets/typora-classic.v1.jsonfollow-up-response-sets/typora-classic.partial.v1.jsonfollow-up-profile-projections/typora-classic.partial.v1.jsongap-reports/typora-classic-follow-up-gap.v1.json
Markdown Basic reading path
The Markdown path shows the same contract flow with a different template manifest and field requirements:
intake-sessions/markdown-manual-override.v1.jsonguided-intake-checklists/markdown-basic.v1.jsonguided-intake-question-sets/markdown-basic.v1.jsonguided-intake-response-sets/markdown-basic.partial.v1.jsonguided-intake-profile-projections/markdown-basic.partial.v1.jsongap-reports/markdown-basic-gap.v1.jsonfollow-up-question-sets/markdown-basic.v1.jsonfollow-up-response-sets/markdown-basic.partial.v1.jsonfollow-up-profile-projections/markdown-basic.partial.v1.jsongap-reports/markdown-basic-follow-up-gap.v1.json
Ids to follow while reading
templateIdandtemplateVersionconnect template-scoped artifacts.documentIdandextractionIdconnect the raw-material example to extracted facts.questionSetId,responseSetId,followUpQuestionSetId, andfollowUpResponseSetIdconnect each intake round.profileIdandreportIdconnect projections and gap reports across the flow.
{
"profileId": "sample-ai-agent-profile",
"fieldValues": {
"basic.name": "Alex Example",
"basic.nameEn": "Alex Example",
"basic.phone": "+1 555-0100",
"basic.email": "alex@example.com",
"required.role": "Software Engineer",
"links.github": "https://github.com/alex-example",
"summary.items": [
"Builds reusable workflow tooling.",
"Turns structured intake into production-ready artifacts."
],
"skills.items": [
"Engineering: Python / TypeScript / CLI tooling",
"Platforms: Resume workflows / template packaging / render pipelines"
],
"work[].date": ["2024.01 - Present"],
"work[].company": ["Example Labs"],
"work[].role": ["Software Engineer"],
"work[].bullets": [[
"Built shared runtime flows for multiple hosts.",
"Improved validation coverage for synthetic public fixtures."
]],
"education[].date": ["2018.09 - 2022.06"],
"education[].school": ["Example University"],
"education[].degree": ["BSc"],
"education[].major": ["Computer Science"],
"project[].date": ["2025.01 - Present"],
"project[].name": ["Example Stream"],
"project[].role": ["Open Source Maintainer"],
"project[].techStack": [["TypeScript", "Node.js", "Express", "CLI", "Automation"]],
"project[].bullets": [[
"Published synthetic examples that explain the resume workflow end to end.",
"Turned structured intake data into reusable rendering and validation fixtures."
]]
},
"provenance": {
"basic.name": ["extract-basic-name"],
"links.github": ["extract-github-link"],
"project[].name": ["extract-project-name"],
"project[].role": ["extract-project-role"],
"project[].techStack": ["extract-project-tech-stack"]
},
"profileStatus": "partial",
"updatedAt": "2026-04-16T12:30:00Z"
}
{
"catalogId": "resume-core-fields",
"version": "1.0.0",
"fields": [
{
"fieldId": "basic.name",
"label": "Name",
"dataType": "string",
"cardinality": "one",
"path": "basic.name",
"description": "Candidate display name"
},
{
"fieldId": "basic.nameEn",
"label": "English name",
"dataType": "string",
"cardinality": "one",
"path": "basic.nameEn",
"description": "Optional second-language name shown in the header"
},
{
"fieldId": "basic.phone",
"label": "Phone",
"dataType": "string",
"cardinality": "one",
"path": "basic.phone",
"description": "Primary contact phone number"
},
{
"fieldId": "basic.email",
"label": "Email",
"dataType": "email",
"cardinality": "one",
"path": "basic.email",
"description": "Primary contact email"
},
{
"fieldId": "required.role",
"label": "Target role",
"dataType": "string",
"cardinality": "one",
"path": "required.role",
"description": "Target role or direction shown in the resume header"
},
{
"fieldId": "links.github",
"label": "GitHub",
"dataType": "url",
"cardinality": "one",
"path": "links.github",
"description": "Primary GitHub profile link",
"aliases": ["github", "GitHub"]
},
{
"fieldId": "summary.items",
"label": "Summary bullets",
"dataType": "richtext",
"cardinality": "many",
"path": "summary.items",
"description": "Short summary bullets shown near the top of the resume"
},
{
"fieldId": "skills.items",
"label": "Skills lines",
"dataType": "richtext",
"cardinality": "many",
"path": "skills.items",
"description": "Repeatable skill lines shown in the skills section"
},
{
"fieldId": "work[].date",
"label": "Work date",
"dataType": "string",
"cardinality": "many",
"path": "work[].date",
"description": "Date range for each work entry"
},
{
"fieldId": "work[].company",
"label": "Work company",
"dataType": "string",
"cardinality": "many",
"path": "work[].company",
"description": "Company or organization name for each work entry"
},
{
"fieldId": "work[].role",
"label": "Work role",
"dataType": "string",
"cardinality": "many",
"path": "work[].role",
"description": "Role title for each work entry"
},
{
"fieldId": "work[].bullets",
"label": "Work bullets",
"dataType": "richtext",
"cardinality": "many",
"path": "work[].bullets",
"description": "Bullet list describing responsibilities or impact for each work entry"
},
{
"fieldId": "education[].date",
"label": "Education date",
"dataType": "string",
"cardinality": "many",
"path": "education[].date",
"description": "Date range for each education entry"
},
{
"fieldId": "education[].school",
"label": "Education school",
"dataType": "string",
"cardinality": "many",
"path": "education[].school",
"description": "School name for each education entry"
},
{
"fieldId": "education[].major",
"label": "Education major",
"dataType": "string",
"cardinality": "many",
"path": "education[].major",
"description": "Major or field of study for each education entry"
},
{
"fieldId": "education[].degree",
"label": "Education degree",
"dataType": "string",
"cardinality": "many",
"path": "education[].degree",
"description": "Degree label for each education entry"
},
{
"fieldId": "project[].date",
"label": "Project date",
"dataType": "string",
"cardinality": "many",
"path": "project[].date",
"description": "Date range for each project entry"
},
{
"fieldId": "project[].name",
"label": "Project name",
"dataType": "string",
"cardinality": "many",
"path": "project[].name",
"description": "Project title for each project entry"
},
{
"fieldId": "project[].role",
"label": "Project role",
"dataType": "string",
"cardinality": "many",
"path": "project[].role",
"description": "Role or responsibility label for each project entry"
},
{
"fieldId": "project[].techStack",
"label": "Project tech stack",
"dataType": "list",
"cardinality": "many",
"path": "project[].techStack",
"description": "Key technologies associated with each project entry"
},
{
"fieldId": "project[].bullets",
"label": "Project bullets",
"dataType": "richtext",
"cardinality": "many",
"path": "project[].bullets",
"description": "Bullet list describing contribution and impact for each project"
}
]
}
{
"documentId": "source-existing-resume-md",
"type": "resume",
"format": "markdown",
"sourceRef": "public://examples/existing-resume.md",
"rawText": "# Alex Example\n\nEmail: alex@example.com\nGitHub: https://github.com/alex-example\n\n### Example Stream | Open Source Maintainer\nTech Stack: TypeScript / Node.js / Express / CLI / Automation\n",
"metadata": {
"title": "Synthetic existing resume draft",
"language": "en",
"importedAt": "2026-04-09T12:00:00Z"
}
}
{
"extractionId": "extract-basic-name",
"documentId": "source-existing-resume-md",
"fragmentText": "# Alex Example",
"sourceLocator": {
"kind": "line-range",
"value": "1-1"
},
"candidateFieldIds": ["basic.name"],
"confidence": 0.99
}
{
"extractionId": "extract-github-link",
"documentId": "source-existing-resume-md",
"fragmentText": "GitHub: https://github.com/alex-example",
"sourceLocator": {
"kind": "line-range",
"value": "4-4"
},
"candidateFieldIds": ["links.github"],
"confidence": 0.97
}
{
"extractionId": "extract-project-name",
"documentId": "source-existing-resume-md",
"fragmentText": "Example Stream",
"sourceLocator": {
"kind": "line-range",
"value": "6-6"
},
"candidateFieldIds": ["project[].name"],
"confidence": 0.98
}
{
"extractionId": "extract-project-role",
"documentId": "source-existing-resume-md",
"fragmentText": "Open Source Maintainer",
"sourceLocator": {
"kind": "line-range",
"value": "6-6"
},
"candidateFieldIds": ["project[].role"],
"confidence": 0.95
}
{
"extractionId": "extract-project-tech-stack",
"documentId": "source-existing-resume-md",
"fragmentText": "TypeScript / Node.js / Express / CLI / Automation",
"sourceLocator": {
"kind": "line-range",
"value": "7-7"
},
"candidateFieldIds": ["project[].techStack"],
"confidence": 0.96
}
.markdown-basic {
font-family: Georgia, serif;
color: #222;
max-width: 760px;
margin: 0 auto;
}
.markdown-basic h1,
.markdown-basic h2,
.markdown-basic h3 {
margin-bottom: 8px;
}
.markdown-basic section {
margin-bottom: 12px;
}
<div class="markdown-basic">
<h1>{{basic.name}}</h1>
<p>{{basic.email}}</p>
{{#links.github}}<p>{{links.github}}</p>{{/links.github}}
<h2>个人总结</h2>
<ul>{{#summary.items}}<li>{{.}}</li>{{/summary.items}}</ul>
<h2>教育经历</h2>
<ul>{{#education}}<li>{{school}} · {{degree}} · {{major}}</li>{{/education}}</ul>
<h2>项目经历</h2>
{{#project}}
<section>
<h3>{{name}}</h3>
<p>角色:{{role}}</p>
<ul>{{#bullets}}<li>{{.}}</li>{{/bullets}}</ul>
</section>
{{/project}}
</div>
{{basic.name}}
{{basic.email}} {{#links.github}}{{links.github}}{{/links.github}}
个人总结
{{#summary.items}}
- {{.}}
{{/summary.items}}
教育经历
{{#education}}
- {{school}} · {{degree}} · {{major}}
{{/education}}
项目经历
{{#project}}
{{name}}
角色:{{role}} {{#bullets}}
- {{.}}
{{/bullets}} {{/project}}
:root {
--accent: #4a89cf;
--text: #111111;
--muted: #4b4b4b;
--line: #2f2f2f;
--date-col: 170px;
--side-col: 140px;
--project-gap: 10px;
--label-gap: 14px;
}
html,
body {
background: #ffffff;
}
body {
max-width: 900px;
margin: 0 auto;
padding: 26px 34px 36px;
color: var(--text);
font-family: "Times New Roman", "Noto Serif SC", "Songti SC", "SimSun", serif;
font-size: 17px;
line-height: 1.5;
}
.resume-page {
width: 100%;
}
.resume-header,
.entry-table {
width: 100%;
border-collapse: collapse;
}
.resume-header {
margin-bottom: 14px;
}
.resume-header td {
padding: 2px 0;
vertical-align: top;
}
.resume-header .label {
width: 160px;
padding-right: var(--label-gap);
font-weight: 700;
white-space: nowrap;
}
.resume-header .value {
font-size: 16px;
}
.resume-header tr:first-child td {
color: var(--accent);
font-size: 20px;
font-weight: 700;
padding-bottom: 5px;
}
.resume-header tr:last-child td {
padding-bottom: 10px;
border-bottom: 4px solid var(--accent);
}
.resume-header .emphasis {
color: var(--accent);
font-weight: 700;
}
h2 {
display: flex;
align-items: flex-end;
gap: 10px;
margin: 12px 0 6px;
color: var(--accent);
font-size: 17px;
font-weight: 700;
}
h2::after {
content: "";
flex: 1;
border-bottom: 2px solid var(--line);
transform: translateY(-3px);
}
.entry-table {
margin-bottom: 4px;
}
.project-table {
margin-top: var(--project-gap);
}
.project-table:first-of-type {
margin-top: 4px;
}
.entry-table td {
padding: 1px 0;
vertical-align: top;
}
.entry-table .date {
width: var(--date-col);
white-space: nowrap;
}
.entry-table .main {
font-weight: 700;
text-align: center;
}
.entry-table .side {
width: var(--side-col);
font-weight: 700;
white-space: nowrap;
text-align: right;
}
.edu-table .main,
.edu-table .meta {
text-align: center;
}
.project-table .meta {
text-align: left;
}
.entry-table .meta {
color: var(--muted);
font-size: 14px;
line-height: 1.35;
}
.meta-row td {
padding-top: 1px;
}
p {
margin: 0 0 4px;
}
ul {
margin: 4px 0 10px 26px;
padding: 0;
}
li {
margin: 2px 0;
text-align: justify;
}
strong {
font-weight: 700;
}
em {
font-style: normal;
}
a {
color: var(--accent);
text-decoration: underline;
word-break: break-all;
}
@media print {
body {
max-width: none;
padding: 8mm 12mm 10mm;
font-size: 11pt;
line-height: 1.42;
}
.resume-header tr:first-child td {
font-size: 16pt;
}
.resume-header .value {
font-size: 11pt;
}
h2 {
font-size: 13pt;
}
.entry-table .meta {
font-size: 10pt;
}
li,
h2,
table {
page-break-inside: avoid;
}
}
<div class="resume-page">
<table class="resume-header">
<tr>
<td class="label name">{{basic.name}}</td>
<td class="value name">{{basic.nameEn}}</td>
</tr>
{{#basic.phone}}
<tr>
<td class="label">电话</td>
<td class="value">{{basic.phone}}</td>
</tr>
{{/basic.phone}}
<tr>
<td class="label">邮箱</td>
<td class="value">{{basic.email}}</td>
</tr>
{{#links.github}}
<tr>
<td class="label">链接</td>
<td class="value"><a href="{{links.github}}">{{links.github}}</a></td>
</tr>
{{/links.github}}
<tr>
<td class="label">求职方向</td>
<td class="value emphasis">{{required.role}}</td>
</tr>
</table>
<section>
<h2>个人总结</h2>
<ul>{{#summary.items}}<li>{{.}}</li>{{/summary.items}}</ul>
</section>
<section>
<h2>专业技能</h2>
<ul>{{#skills.items}}<li>{{.}}</li>{{/skills.items}}</ul>
</section>
{{#work}}
<section>
<h2>工作经历</h2>
<table class="entry-table work-table">
<tr>
<td class="date">{{date}}</td>
<td class="main">{{company}}</td>
<td class="side">{{role}}</td>
</tr>
</table>
<ul>{{#bullets}}<li>{{.}}</li>{{/bullets}}</ul>
</section>
{{/work}}
<section>
<h2>项目经历</h2>
{{#project}}
<table class="entry-table project-table">
<tr>
<td class="date">{{date}}</td>
<td class="main">{{name}}</td>
<td class="side">{{role}}</td>
</tr>
{{#techStack}}
<tr class="meta-row">
<td class="meta" colspan="3">技术栈:{{techStack}}</td>
</tr>
{{/techStack}}
</table>
<ul>{{#bullets}}<li>{{.}}</li>{{/bullets}}</ul>
{{/project}}
</section>
<section>
<h2>教育经历</h2>
{{#education}}
<table class="entry-table edu-table">
<tr>
<td class="date">{{date}}</td>
<td class="main">{{school}}</td>
<td class="side">{{degree}}</td>
</tr>
<tr class="meta-row">
<td class="date"></td>
<td class="meta">{{major}}</td>
<td class="side"></td>
</tr>
</table>
{{/education}}
</section>
</div>
<div class="resume-page">
<table class="resume-header"> <tr> <td class="label name">{{basic.name}}</td> <td class="value name">{{basic.nameEn}}</td> </tr>{{#basic.phone}} <tr> <td class="label">电话</td> <td class="value">{{basic.phone}}</td> </tr>{{/basic.phone}} <tr> <td class="label">邮箱</td> <td class="value">{{basic.email}}</td> </tr>{{#links.github}} <tr> <td class="label">链接</td> <td class="value"><a href="{{links.github}}">{{links.github}}</a></td> </tr>{{/links.github}} <tr> <td class="label">求职方向</td> <td class="value emphasis">{{required.role}}</td> </tr> </table>
个人总结
{{#summary.items}}
- {{.}}
{{/summary.items}}
专业技能
{{#skills.items}}
- {{.}}
{{/skills.items}}
{{#work}}
工作经历
<table class="entry-table work-table"> <tr> <td class="date">{{date}}</td> <td class="main">{{company}}</td> <td class="side">{{role}}</td> </tr> </table>
{{#bullets}}
- {{.}}
{{/bullets}} {{/work}}
项目经历
{{#project}} <table class="entry-table project-table"> <tr> <td class="date">{{date}}</td> <td class="main">{{name}}</td> <td class="side">{{role}}</td> </tr>{{#techStack}} <tr class="meta-row"> <td class="meta" colspan="3">技术栈:{{techStack}}</td> </tr>{{/techStack}} </table>
{{#bullets}}
- {{.}}
{{/bullets}} {{/project}}
教育经历
{{#education}} <table class="entry-table edu-table"> <tr> <td class="date">{{date}}</td> <td class="main">{{school}}</td> <td class="side">{{degree}}</td> </tr> <tr class="meta-row"> <td class="date"></td> <td class="meta">{{major}}</td> <td class="side"></td> </tr> </table> {{/education}}
</div>
{
"registryId": "resume-template-registry",
"entries": [
{
"templateId": "typora-classic",
"version": "1.0.0",
"source": "builtin",
"visibility": "official",
"status": "active"
},
{
"templateId": "markdown-basic",
"version": "1.0.0",
"source": "builtin",
"visibility": "official",
"status": "active"
}
],
"defaultTemplateId": "typora-classic"
}
{
"templateId": "markdown-basic",
"name": "Markdown Basic Resume",
"version": "1.0.0",
"description": "Plain Markdown resume template that can be rendered without Typora-specific HTML blocks.",
"target": "markdown",
"sectionOrder": ["header", "summary", "education", "projects"],
"fieldRequirements": [
{
"fieldId": "basic.name",
"required": true,
"repeatable": false,
"order": 10,
"promptHint": "Candidate name in the Markdown title"
},
{
"fieldId": "basic.email",
"required": true,
"repeatable": false,
"order": 20,
"promptHint": "Primary email below the title"
},
{
"fieldId": "links.github",
"required": false,
"repeatable": false,
"order": 30,
"promptHint": "Optional GitHub link below the title"
},
{
"fieldId": "summary.items",
"required": false,
"repeatable": true,
"order": 40,
"promptHint": "Short summary bullets"
},
{
"fieldId": "education[].school",
"required": true,
"repeatable": true,
"order": 50,
"promptHint": "School name"
},
{
"fieldId": "education[].major",
"required": true,
"repeatable": true,
"order": 60,
"promptHint": "Major name"
},
{
"fieldId": "education[].degree",
"required": true,
"repeatable": true,
"order": 70,
"promptHint": "Degree label"
},
{
"fieldId": "project[].name",
"required": true,
"repeatable": true,
"order": 80,
"promptHint": "Project title"
},
{
"fieldId": "project[].role",
"required": true,
"repeatable": true,
"order": 90,
"promptHint": "Project role label"
},
{
"fieldId": "project[].bullets",
"required": true,
"repeatable": true,
"order": 100,
"promptHint": "Project bullets"
}
],
"renderSpecRef": "renderers/markdown-basic@1.0.0",
"origin": "user-promoted",
"assetRefs": {
"markdown": "../template-assets/markdown-basic/template.md",
"html": "../template-assets/markdown-basic/template.html",
"css": "../template-assets/markdown-basic/style.css"
},
"previewCard": {
"title": "Markdown Basic Resume",
"styleLabel": "Plain markdown with light styling",
"useCases": ["通用简历", "跨编辑器复用", "轻量主题"],
"requiredContentSummary": ["姓名和邮箱", "教育经历", "项目标题和项目描述"]
},
"storageScope": "builtin"
}
{
"templateId": "typora-classic",
"name": "Typora Classic Resume",
"version": "1.0.0",
"description": "Typora-first professional resume template with markdown plus HTML tables and print-oriented CSS.",
"target": "typora",
"sectionOrder": ["header", "summary", "skills", "work", "projects", "education"],
"fieldRequirements": [
{"fieldId": "basic.name", "required": true, "repeatable": false, "order": 10, "promptHint": "Name shown in the first header row"},
{"fieldId": "basic.nameEn", "required": false, "repeatable": false, "order": 20, "promptHint": "Optional second-language name shown beside the main name"},
{"fieldId": "basic.phone", "required": false, "repeatable": false, "order": 30, "promptHint": "Phone number shown in the contact rows"},
{"fieldId": "basic.email", "required": true, "repeatable": false, "order": 40, "promptHint": "Primary email shown in the contact rows"},
{"fieldId": "links.github", "required": false, "repeatable": false, "order": 50, "promptHint": "GitHub or portfolio link if it strengthens the resume"},
{"fieldId": "required.role", "required": true, "repeatable": false, "order": 60, "promptHint": "Target role or direction shown in the final header row"},
{"fieldId": "summary.items", "required": false, "repeatable": true, "order": 70, "promptHint": "Two or more short summary bullets"},
{"fieldId": "skills.items", "required": false, "repeatable": true, "order": 80, "promptHint": "Repeatable skill lines that stay profession-neutral"},
{"fieldId": "work[].date", "required": false, "repeatable": true, "order": 90, "promptHint": "Date range for each work experience row"},
{"fieldId": "work[].company", "required": false, "repeatable": true, "order": 100, "promptHint": "Company or organization name for each work row"},
{"fieldId": "work[].role", "required": false, "repeatable": true, "order": 110, "promptHint": "Role title for each work row"},
{"fieldId": "work[].bullets", "required": false, "repeatable": true, "order": 120, "promptHint": "Bullets describing responsibilities or impact for each work entry"},
{"fieldId": "project[].date", "required": true, "repeatable": true, "order": 130, "promptHint": "Date range shown in the project table"},
{"fieldId": "project[].name", "required": true, "repeatable": true, "order": 140, "promptHint": "Project or initiative name"},
{"fieldId": "project[].role", "required": true, "repeatable": true, "order": 150, "promptHint": "Role label shown on the project row"},
{"fieldId": "project[].techStack", "required": false, "repeatable": true, "order": 160, "promptHint": "Short stack or descriptor line shown in the meta row"},
{"fieldId": "project[].bullets", "required": true, "repeatable": true, "order": 170, "promptHint": "Bullets describing contribution and impact"},
{"fieldId": "education[].date", "required": false, "repeatable": true, "order": 180, "promptHint": "Date range for each education entry"},
{"fieldId": "education[].school", "required": true, "repeatable": true, "order": 190, "promptHint": "School name for each education entry"},
{"fieldId": "education[].degree", "required": true, "repeatable": true, "order": 200, "promptHint": "Degree label for each education entry"},
{"fieldId": "education[].major", "required": true, "repeatable": true, "order": 210, "promptHint": "Major for each education entry"}
],
"renderSpecRef": "renderers/typora-classic@1.0.0",
"origin": "builtin",
"assetRefs": {
"markdown": "../template-assets/typora-classic/template.md",
"html": "../template-assets/typora-classic/template.html",
"css": "../template-assets/typora-classic/style.css"
},
"previewCard": {
"title": "Typora Classic Resume",
"styleLabel": "Typora-first classic print layout",
"useCases": ["通用简历", "Typora 预览", "项目导向简历"],
"requiredContentSummary": ["姓名和邮箱", "求职方向", "至少一个项目"]
},
"storageScope": "builtin"
}
"""Public runtime package for host-agnostic resume workflows."""
from resume_runtime.runtime import (
AgentIntakeCore,
AgentIntakeCoreError,
AgentIntakeCoreOutcome,
AskedQuestion,
BatchAnswerResult,
ConversationRoute,
EntrypointMode,
HostConversationAdapter,
HostConversationAdapterError,
HostConversationOutcome,
HostSessionAction,
HostSessionRunner,
HostSessionRunnerError,
HostSessionState,
HostSessionStore,
HostSessionStoreError,
MaterialIntakeResult,
PromptDirective,
ResumeMaterial,
SessionRunner,
SessionRunnerResult,
TemplateCatalogEntry,
TemplateCard,
TemplateStore,
default_host_session_store_path,
derive_guided_intake_checklist,
host_conversation_outcome_to_dict,
load_template_catalog,
render_template_bundle,
route_conversation_turn,
serialize_question_batch,
write_rendered_bundle,
)
__all__ = [
"AgentIntakeCore",
"AgentIntakeCoreError",
"AgentIntakeCoreOutcome",
"AskedQuestion",
"BatchAnswerResult",
"ConversationRoute",
"EntrypointMode",
"TemplateCatalogEntry",
"TemplateCard",
"TemplateStore",
"HostConversationAdapter",
"HostConversationAdapterError",
"HostConversationOutcome",
"HostSessionAction",
"HostSessionRunner",
"HostSessionRunnerError",
"HostSessionState",
"HostSessionStore",
"HostSessionStoreError",
"MaterialIntakeResult",
"PromptDirective",
"ResumeMaterial",
"SessionRunner",
"SessionRunnerResult",
"default_host_session_store_path",
"derive_guided_intake_checklist",
"host_conversation_outcome_to_dict",
"load_template_catalog",
"render_template_bundle",
"route_conversation_turn",
"serialize_question_batch",
"write_rendered_bundle",
]
from __future__ import annotations
import argparse
import importlib.util
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
def _bootstrap_package_for_direct_script() -> None:
package_root = Path(__file__).resolve().parent
package_init = package_root / "__init__.py"
if "resume_runtime" in sys.modules or not package_init.exists():
return
spec = importlib.util.spec_from_file_location(
"resume_runtime",
package_init,
submodule_search_locations=[str(package_root)],
)
if spec is None or spec.loader is None:
return
module = importlib.util.module_from_spec(spec)
sys.modules["resume_runtime"] = module
spec.loader.exec_module(module)
try:
from resume_runtime.runtime import (
AgentIntakeCore,
HostConversationAdapter,
HostSessionStore,
ResumeMaterial,
SessionRunner,
default_host_session_store_path,
host_conversation_outcome_to_dict,
)
from resume_runtime.runtime.agent_intake_core import AgentIntakeCoreError
except ModuleNotFoundError: # pragma: no cover - direct script fallback
_bootstrap_package_for_direct_script()
from resume_runtime.runtime import (
AgentIntakeCore,
HostConversationAdapter,
HostSessionStore,
ResumeMaterial,
SessionRunner,
default_host_session_store_path,
host_conversation_outcome_to_dict,
)
from resume_runtime.runtime.agent_intake_core import AgentIntakeCoreError
CLI_VERSION = "resume-agent-intake-cli/v1"
_DEFAULT_BATCH_SIZE_POLICY = 2
_CAMEL_BOUNDARY_RE = re.compile(r"(?<!^)(?=[A-Z])")
@dataclass(frozen=True)
class RequestEnvelope:
turn_kind: str
timestamp: str
user_message: str | None
manifest: dict[str, Any] | None
checklist: dict[str, Any] | None
materials: list[ResumeMaterial]
drafting_started: bool
class AgentIntakeCliRequestError(Exception):
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run the resume agent intake JSON CLI")
parser.add_argument(
"--session-store",
type=Path,
default=None,
help="Override the persisted host session directory",
)
parser.add_argument(
"--input-file",
type=Path,
default=None,
help="Read the JSON request payload from a file instead of stdin",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
raw_request = _read_request_text(args.input_file)
request = _parse_request(raw_request)
core = _build_core(
session_store_path=args.session_store,
timestamp=request.timestamp,
)
outcome = core.handle_turn(
turn_kind=request.turn_kind,
timestamp=request.timestamp,
user_message=request.user_message,
manifest=request.manifest,
checklist=request.checklist,
materials=request.materials,
drafting_started=request.drafting_started,
)
_write_json(_serialize_success_payload(outcome))
return 0
except AgentIntakeCliRequestError as exc:
_write_json(
{
"ok": False,
"version": CLI_VERSION,
"error": {
"code": exc.code,
"message": str(exc),
},
}
)
return 2
except AgentIntakeCoreError as exc:
_write_json(
{
"ok": False,
"version": CLI_VERSION,
"error": {
"code": "invalid_request_shape",
"message": str(exc),
},
}
)
return 2
def _build_core(*, session_store_path: Path | None, timestamp: str) -> AgentIntakeCore:
effective_store_path = session_store_path or default_host_session_store_path(
Path(__file__).resolve().parent
)
session_runner = SessionRunner(
guided_question_set_generated_at=timestamp,
guided_response_set_updated_at=timestamp,
follow_up_generated_at=timestamp,
follow_up_response_updated_at=timestamp,
batch_size_policy=_DEFAULT_BATCH_SIZE_POLICY,
)
store = HostSessionStore(effective_store_path)
adapter = HostConversationAdapter(store, session_runner)
return AgentIntakeCore(store, adapter)
def _read_request_text(input_file: Path | None) -> str:
if input_file is None:
return sys.stdin.read()
try:
return input_file.read_text(encoding="utf-8")
except OSError as exc:
raise AgentIntakeCliRequestError("invalid_request_io", str(exc)) from exc
def _parse_request(raw_request: str) -> RequestEnvelope:
try:
payload = json.loads(raw_request)
except json.JSONDecodeError as exc:
raise AgentIntakeCliRequestError(
"invalid_request_json",
"Request body must be valid JSON",
) from exc
if not isinstance(payload, dict):
raise AgentIntakeCliRequestError("invalid_request_shape", "Request root must be a JSON object")
version = payload.get("version")
if version != CLI_VERSION:
raise AgentIntakeCliRequestError(
"invalid_request_shape",
f"Request version must be {CLI_VERSION}",
)
turn = payload.get("turn")
if not isinstance(turn, dict):
raise AgentIntakeCliRequestError("invalid_request_shape", "turn must be an object")
turn_kind = turn.get("kind")
if turn_kind not in {"reply", "resume"}:
raise AgentIntakeCliRequestError(
"invalid_request_shape",
"turn.kind must be 'reply' or 'resume'",
)
timestamp = turn.get("timestamp")
if not isinstance(timestamp, str) or not timestamp:
raise AgentIntakeCliRequestError(
"invalid_request_shape",
"turn.timestamp must be a non-empty string",
)
user_message = turn.get("user_message")
if user_message is not None and not isinstance(user_message, str):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
"turn.user_message must be a string when provided",
)
template_context = payload.get("template_context")
manifest = checklist = None
if template_context is not None:
if not isinstance(template_context, dict):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
"template_context must be an object",
)
manifest = _optional_object(template_context, "manifest", "template_context")
checklist = _optional_object(template_context, "checklist", "template_context")
provided = [manifest is not None, checklist is not None]
if not all(provided):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
"template_context must include manifest and checklist together",
)
materials_payload = payload.get("materials", [])
if materials_payload is None:
materials_payload = []
if not isinstance(materials_payload, list):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
"materials must be a list when provided",
)
materials = [_parse_material(item, index) for index, item in enumerate(materials_payload)]
drafting_started = payload.get("drafting_started", False)
if not isinstance(drafting_started, bool):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
"drafting_started must be a boolean when provided",
)
return RequestEnvelope(
turn_kind=turn_kind,
timestamp=timestamp,
user_message=user_message,
manifest=manifest,
checklist=checklist,
materials=materials,
drafting_started=drafting_started,
)
def _optional_object(payload: dict[str, Any], key: str, path: str) -> dict[str, Any] | None:
value = payload.get(key)
if value is None:
return None
if not isinstance(value, dict):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
f"{path}.{key} must be an object",
)
return value
def _parse_material(payload: Any, index: int) -> ResumeMaterial:
path = f"materials[{index}]"
if not isinstance(payload, dict):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
f"{path} must be an object",
)
document_id = _required_string(payload, "document_id", path)
source_label = _required_string(payload, "source_label", path)
media_type = _required_string(payload, "media_type", path)
text = payload.get("text")
if text is not None and not isinstance(text, str):
raise AgentIntakeCliRequestError(
"invalid_request_shape",
f"{path}.text must be a string when provided",
)
return ResumeMaterial(
documentId=document_id,
sourceLabel=source_label,
mediaType=media_type,
text=text,
)
def _required_string(payload: dict[str, Any], key: str, path: str) -> str:
value = payload.get(key)
if not isinstance(value, str) or not value:
raise AgentIntakeCliRequestError(
"invalid_request_shape",
f"{path}.{key} must be a non-empty string",
)
return value
def _serialize_success_payload(outcome: Any) -> dict[str, Any]:
payload: dict[str, Any] = {
"ok": True,
"version": CLI_VERSION,
"outcome": _serialize_agent_outcome(outcome),
}
if outcome.structuredOutcome is not None:
payload["structured_outcome"] = _serialize_structured_outcome(outcome.structuredOutcome)
if outcome.materialResult is not None:
payload["material_result"] = _serialize_material_result(outcome.materialResult)
return payload
def _serialize_agent_outcome(outcome: Any) -> dict[str, Any]:
return {
"mode": outcome.mode,
"prompt_directive": outcome.promptDirective,
"prompt": outcome.prompt,
}
def _serialize_structured_outcome(outcome: Any) -> dict[str, Any]:
payload = _snake_case_keys(host_conversation_outcome_to_dict(outcome))
payload["mode"] = outcome.mode
return payload
def _serialize_material_result(material_result: Any) -> dict[str, Any]:
return _snake_case_keys(
{
"parseStatus": material_result.parseStatus,
"guidedAnswers": material_result.guidedAnswers,
"bootstrapChecklist": material_result.bootstrapChecklist,
"missingRequiredFields": material_result.missingRequiredFields,
"missingOptionalFields": material_result.missingOptionalFields,
"documentIds": material_result.documentIds,
}
)
def _snake_case_keys(value: Any) -> Any:
if isinstance(value, dict):
return {_to_snake_case(key): _snake_case_keys(nested) for key, nested in value.items()}
if isinstance(value, list):
return [_snake_case_keys(item) for item in value]
return value
def _to_snake_case(key: str) -> str:
return _CAMEL_BOUNDARY_RE.sub("_", key).lower()
def _write_json(payload: dict[str, Any]) -> None:
sys.stdout.write(json.dumps(payload, sort_keys=True))
sys.stdout.write("\n")
if __name__ == "__main__":
raise SystemExit(main())
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
if str(PACKAGE_ROOT) not in sys.path:
sys.path.insert(0, str(PACKAGE_ROOT))
from resume_runtime.runtime import (
HostConversationAdapter,
HostConversationAdapterError,
HostSessionStore,
SessionRunner,
default_host_session_store_path,
host_conversation_outcome_to_dict,
)
CLI_VERSION = "resume-host-cli/v1"
_DEFAULT_BATCH_SIZE_POLICY = 2
@dataclass(frozen=True)
class RequestEnvelope:
turn_kind: str
timestamp: str
user_reply: str | None
manifest: dict[str, Any] | None
checklist: dict[str, Any] | None
guided_answers: dict[str, Any] | None
intake_session: dict[str, Any] | None
class HostCliRequestError(Exception):
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run the resume host JSON CLI")
parser.add_argument(
"--session-store",
type=Path,
default=None,
help="Override the persisted host session directory",
)
parser.add_argument(
"--input-file",
type=Path,
default=None,
help="Read the JSON request payload from a file instead of stdin",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
raw_request = _read_request_text(args.input_file)
request = _parse_request(raw_request)
adapter = _build_adapter(
session_store_path=args.session_store,
timestamp=request.timestamp,
)
outcome = adapter.handle_turn(
turn_kind=request.turn_kind,
timestamp=request.timestamp,
user_reply=request.user_reply,
manifest=request.manifest,
checklist=request.checklist,
guided_answers=request.guided_answers,
intake_session=request.intake_session,
)
_write_json(
{
"ok": True,
"version": CLI_VERSION,
"mode": outcome.mode,
"outcome": host_conversation_outcome_to_dict(outcome),
}
)
return 0
except HostCliRequestError as exc:
_write_json(
{
"ok": False,
"version": CLI_VERSION,
"error": {
"code": exc.code,
"message": str(exc),
},
}
)
return 2
except HostConversationAdapterError as exc:
_write_json(
{
"ok": False,
"version": CLI_VERSION,
"error": {
"code": "host_conversation_error",
"message": str(exc),
},
}
)
return 1
def _build_adapter(*, session_store_path: Path | None, timestamp: str) -> HostConversationAdapter:
effective_store_path = session_store_path or default_host_session_store_path(
Path(__file__).resolve().parent
)
session_runner = SessionRunner(
guided_question_set_generated_at=timestamp,
guided_response_set_updated_at=timestamp,
follow_up_generated_at=timestamp,
follow_up_response_updated_at=timestamp,
batch_size_policy=_DEFAULT_BATCH_SIZE_POLICY,
)
return HostConversationAdapter(HostSessionStore(effective_store_path), session_runner)
def _read_request_text(input_file: Path | None) -> str:
if input_file is None:
return sys.stdin.read()
try:
return input_file.read_text(encoding="utf-8")
except OSError as exc:
raise HostCliRequestError("invalid_request_io", str(exc)) from exc
def _parse_request(raw_request: str) -> RequestEnvelope:
try:
payload = json.loads(raw_request)
except json.JSONDecodeError as exc:
raise HostCliRequestError("invalid_request_json", "Request body must be valid JSON") from exc
if not isinstance(payload, dict):
raise HostCliRequestError("invalid_request_shape", "Request root must be a JSON object")
version = payload.get("version")
if version != CLI_VERSION:
raise HostCliRequestError(
"invalid_request_shape",
f"Request version must be {CLI_VERSION}",
)
turn = payload.get("turn")
if not isinstance(turn, dict):
raise HostCliRequestError("invalid_request_shape", "turn must be an object")
turn_kind = turn.get("kind")
if turn_kind not in {"reply", "resume"}:
raise HostCliRequestError(
"invalid_request_shape",
"turn.kind must be 'reply' or 'resume'",
)
timestamp = turn.get("timestamp")
if not isinstance(timestamp, str) or not timestamp:
raise HostCliRequestError(
"invalid_request_shape",
"turn.timestamp must be a non-empty string",
)
user_reply = turn.get("user_reply")
if user_reply is not None and not isinstance(user_reply, str):
raise HostCliRequestError(
"invalid_request_shape",
"turn.user_reply must be a string when provided",
)
structured_start = payload.get("structured_start")
manifest = checklist = guided_answers = intake_session = None
if structured_start is not None:
if not isinstance(structured_start, dict):
raise HostCliRequestError(
"invalid_request_shape",
"structured_start must be an object",
)
manifest = _optional_object(structured_start, "manifest")
checklist = _optional_object(structured_start, "checklist")
guided_answers = _optional_object(structured_start, "guided_answers")
intake_session = _optional_object(structured_start, "intake_session")
provided = [manifest is not None, checklist is not None, guided_answers is not None]
if any(provided) and not all(provided):
raise HostCliRequestError(
"invalid_request_shape",
"structured_start must include manifest, checklist, and guided_answers together",
)
return RequestEnvelope(
turn_kind=turn_kind,
timestamp=timestamp,
user_reply=user_reply,
manifest=manifest,
checklist=checklist,
guided_answers=guided_answers,
intake_session=intake_session,
)
def _optional_object(payload: dict[str, Any], key: str) -> dict[str, Any] | None:
value = payload.get(key)
if value is None:
return None
if not isinstance(value, dict):
raise HostCliRequestError(
"invalid_request_shape",
f"structured_start.{key} must be an object",
)
return value
def _write_json(payload: dict[str, Any]) -> None:
sys.stdout.write(json.dumps(payload, sort_keys=True))
sys.stdout.write("\n")
if __name__ == "__main__":
raise SystemExit(main())
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from resume_runtime.runtime.template_renderer import render_template_bundle, write_rendered_bundle
CLI_VERSION = "resume-render-cli/v1"
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Render a resume template bundle")
parser.add_argument("--output-dir", type=Path, default=None)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
request = json.loads(sys.stdin.read())
if request.get("version") != CLI_VERSION:
raise SystemExit(2)
manifest_path = Path(request["manifest_path"])
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
bundle = render_template_bundle(
manifest=manifest,
manifest_path=manifest_path,
profile=request["profile"],
)
payload = {
"ok": True,
"version": CLI_VERSION,
"bundle": bundle,
}
if args.output_dir is not None:
payload["written"] = write_rendered_bundle(bundle, args.output_dir)
sys.stdout.write(json.dumps(payload, sort_keys=True))
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
"""Runtime helpers for the public resume runtime package."""
from resume_runtime.runtime.agent_intake_core import (
AgentIntakeCore,
AgentIntakeCoreError,
AgentIntakeCoreOutcome,
EntrypointMode,
PromptDirective,
)
from resume_runtime.runtime.artifact_builders import derive_guided_intake_checklist
from resume_runtime.runtime.conversation_router import ConversationRoute, route_conversation_turn
from resume_runtime.runtime.follow_up_agent_adapter import AskedQuestion, BatchAnswerResult
from resume_runtime.runtime.host_conversation_adapter import (
HostConversationAdapter,
HostConversationAdapterError,
HostConversationOutcome,
default_host_session_store_path,
)
from resume_runtime.runtime.host_session_runner import HostSessionAction, HostSessionRunner, HostSessionRunnerError
from resume_runtime.runtime.host_session_state import HostSessionState
from resume_runtime.runtime.host_session_store import HostSessionStore, HostSessionStoreError
from resume_runtime.runtime.material_intake_adapter import MaterialIntakeResult, ResumeMaterial
from resume_runtime.runtime.serialization import host_conversation_outcome_to_dict, serialize_question_batch
from resume_runtime.runtime.session_runner import SessionRunner, SessionRunnerResult
from resume_runtime.runtime.template_catalog import TemplateCatalogEntry, TemplateCard, load_template_catalog
from resume_runtime.runtime.template_renderer import render_template_bundle, write_rendered_bundle
from resume_runtime.runtime.template_store import TemplateStore
__all__ = [
"AgentIntakeCore",
"AgentIntakeCoreError",
"AgentIntakeCoreOutcome",
"AskedQuestion",
"BatchAnswerResult",
"ConversationRoute",
"EntrypointMode",
"TemplateCatalogEntry",
"TemplateCard",
"TemplateStore",
"HostConversationAdapter",
"default_host_session_store_path",
"HostConversationAdapterError",
"HostConversationOutcome",
"HostSessionAction",
"HostSessionRunner",
"HostSessionRunnerError",
"HostSessionState",
"HostSessionStore",
"HostSessionStoreError",
"MaterialIntakeResult",
"PromptDirective",
"ResumeMaterial",
"SessionRunner",
"SessionRunnerResult",
"derive_guided_intake_checklist",
"host_conversation_outcome_to_dict",
"load_template_catalog",
"render_template_bundle",
"serialize_question_batch",
"route_conversation_turn",
"write_rendered_bundle",
]
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Literal
from resume_runtime.runtime.conversation_router import route_conversation_turn
from resume_runtime.runtime.host_conversation_adapter import (
HostConversationAdapter,
HostConversationAdapterError,
HostConversationOutcome,
)
from resume_runtime.runtime.host_session_store import HostSessionStore, HostSessionStoreError
from resume_runtime.runtime.material_intake_adapter import (
MaterialIntakeResult,
ResumeMaterial,
build_material_intake_artifacts,
)
EntrypointMode = Literal["freeform_discovery", "material_parsing", "structured_intake", "drafting"]
PromptDirective = Literal[
"stay_freeform",
"ask_existing_material",
"parsing_failed",
"session_recovery_failed",
"ask_current_batch",
"ask_yes_no_only",
"handoff_to_drafting",
"start_drafting",
"continue_drafting",
]
class AgentIntakeCoreError(Exception):
"""Raised when public intake orchestration inputs are incomplete."""
@dataclass(frozen=True)
class AgentIntakeCoreOutcome:
mode: EntrypointMode
promptDirective: PromptDirective
prompt: str | None
structuredOutcome: HostConversationOutcome | None = None
materialResult: MaterialIntakeResult | None = None
class AgentIntakeCore:
def __init__(self, store: HostSessionStore, adapter: HostConversationAdapter) -> None:
self._store = store
self._adapter = adapter
def handle_turn(
self,
*,
turn_kind: Literal["reply", "resume"],
timestamp: str,
user_message: str | None = None,
manifest: dict[str, Any] | None = None,
checklist: dict[str, Any] | None = None,
materials: list[ResumeMaterial] | None = None,
drafting_started: bool = False,
) -> AgentIntakeCoreOutcome:
materials = materials or []
try:
has_active_session = self._store.find_active_session() is not None
except HostSessionStoreError:
return AgentIntakeCoreOutcome(
mode="freeform_discovery",
promptDirective="session_recovery_failed",
prompt=None,
)
route = route_conversation_turn(
user_message=user_message,
has_material=bool(materials),
has_active_session=has_active_session,
drafting_started=drafting_started,
)
if route.mode == "resume_active_session":
try:
structured_outcome = self._adapter.handle_turn(
turn_kind=turn_kind,
timestamp=timestamp,
user_reply=user_message,
)
except HostConversationAdapterError:
return AgentIntakeCoreOutcome(
mode="freeform_discovery",
promptDirective="session_recovery_failed",
prompt=None,
)
if structured_outcome.promptDirective == "handoff_to_drafting":
return AgentIntakeCoreOutcome(
mode="drafting",
promptDirective=structured_outcome.promptDirective,
prompt=None,
structuredOutcome=structured_outcome,
)
return AgentIntakeCoreOutcome(
mode="structured_intake",
promptDirective=structured_outcome.promptDirective,
prompt=None,
structuredOutcome=structured_outcome,
)
if route.mode == "parse_material":
if manifest is None or checklist is None:
raise AgentIntakeCoreError("manifest and checklist are required for material parsing")
material_result = build_material_intake_artifacts(
manifest=manifest,
checklist=checklist,
materials=materials,
)
if material_result.parseStatus == "needs_fallback":
return AgentIntakeCoreOutcome(
mode="freeform_discovery",
promptDirective="parsing_failed",
prompt=None,
materialResult=material_result,
)
if not material_result.missingRequiredFields:
return AgentIntakeCoreOutcome(
mode="drafting",
promptDirective="start_drafting",
prompt=None,
materialResult=material_result,
)
try:
structured_outcome = self._adapter.handle_turn(
turn_kind="reply",
timestamp=timestamp,
manifest=manifest,
checklist=material_result.bootstrapChecklist,
guided_answers=material_result.guidedAnswers,
intake_session={
"hasExistingMaterial": True,
"documentIds": material_result.documentIds,
"phase": "handed-off",
"route": "guided-intake",
"status": "active",
},
)
except HostConversationAdapterError:
return AgentIntakeCoreOutcome(
mode="freeform_discovery",
promptDirective="parsing_failed",
prompt=None,
materialResult=material_result,
)
return AgentIntakeCoreOutcome(
mode="structured_intake",
promptDirective=structured_outcome.promptDirective,
prompt=None,
structuredOutcome=structured_outcome,
materialResult=material_result,
)
if route.mode == "continue_drafting":
return AgentIntakeCoreOutcome(
mode="drafting",
promptDirective="continue_drafting",
prompt=None,
)
if route.mode == "ask_existing_material":
return AgentIntakeCoreOutcome(
mode="freeform_discovery",
promptDirective="ask_existing_material",
prompt=None,
)
return AgentIntakeCoreOutcome(
mode="freeform_discovery",
promptDirective="stay_freeform",
prompt=None,
)
__all__ = [
"AgentIntakeCore",
"AgentIntakeCoreError",
"AgentIntakeCoreOutcome",
"EntrypointMode",
"PromptDirective",
]
from __future__ import annotations
from copy import deepcopy
from typing import Any
def question_text(prompt_hint: str) -> str:
return f"Please provide {prompt_hint}."
def _requirements_by_field(manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {
requirement["fieldId"]: requirement
for requirement in manifest["fieldRequirements"]
}
def _question_field_ids(question_set: dict[str, Any]) -> set[str]:
return {item["fieldId"] for item in question_set["questions"]}
def _validate_response_fields(responses: dict[str, Any], allowed_field_ids: set[str]) -> None:
unknown_field_ids = sorted(set(responses.keys()) - allowed_field_ids)
if unknown_field_ids:
raise ValueError("unknown response field: " + ", ".join(unknown_field_ids))
def derive_guided_intake_checklist(
manifest: dict[str, Any],
*,
generated_at: str,
) -> dict[str, Any]:
requirements = sorted(
manifest["fieldRequirements"],
key=lambda requirement: requirement["order"],
)
return {
"checklistId": f"guided-intake-{manifest['templateId']}",
"templateId": manifest["templateId"],
"templateVersion": manifest["version"],
"requiredFields": [item["fieldId"] for item in requirements if item["required"]],
"optionalFields": [item["fieldId"] for item in requirements if not item["required"]],
"repeatableFields": [item["fieldId"] for item in requirements if item["repeatable"]],
"generatedAt": generated_at,
}
def derive_guided_intake_question_set(
manifest: dict[str, Any],
checklist: dict[str, Any],
*,
generated_at: str,
) -> dict[str, Any]:
requirements_by_field = _requirements_by_field(manifest)
ordered_field_ids = checklist["requiredFields"] + checklist["optionalFields"]
return {
"questionSetId": f"guided-intake-question-set-{manifest['templateId']}",
"templateId": manifest["templateId"],
"templateVersion": manifest["version"],
"questions": [
{
"fieldId": field_id,
"question": question_text(requirements_by_field[field_id]["promptHint"]),
}
for field_id in ordered_field_ids
],
"generatedAt": generated_at,
}
def assemble_guided_intake_response_set(
question_set: dict[str, Any],
responses: dict[str, Any],
*,
updated_at: str,
) -> dict[str, Any]:
allowed_field_ids = _question_field_ids(question_set)
_validate_response_fields(responses, allowed_field_ids)
return {
"responseSetId": f"guided-intake-response-set-{question_set['templateId']}",
"templateId": question_set["templateId"],
"templateVersion": question_set["templateVersion"],
"questionSetId": question_set["questionSetId"],
"responses": deepcopy(responses),
"updatedAt": updated_at,
}
def project_guided_intake_profile(response_set: dict[str, Any]) -> dict[str, Any]:
profile_id = f"profile-from-{response_set['responseSetId']}"
return {
"projectionId": f"guided-intake-profile-projection-{response_set['templateId']}",
"responseSetId": response_set["responseSetId"],
"questionSetId": response_set["questionSetId"],
"templateId": response_set["templateId"],
"templateVersion": response_set["templateVersion"],
"profile": {
"profileId": profile_id,
"fieldValues": deepcopy(response_set["responses"]),
"provenance": {
field_id: [response_set["responseSetId"]]
for field_id in response_set["responses"].keys()
},
"profileStatus": "partial",
"updatedAt": response_set["updatedAt"],
},
}
def derive_gap_report(
manifest: dict[str, Any],
projection: dict[str, Any],
*,
generated_at: str,
) -> dict[str, Any]:
profile = projection["profile"]
profile_field_values = profile["fieldValues"]
missing_required: list[str] = []
missing_recommended: list[str] = []
for requirement in manifest["fieldRequirements"]:
field_id = requirement["fieldId"]
if field_id in profile_field_values:
continue
if requirement["required"]:
missing_required.append(field_id)
else:
missing_recommended.append(field_id)
missing_field_ids = set(missing_required + missing_recommended)
questions = [
{
"fieldId": requirement["fieldId"],
"question": question_text(requirement["promptHint"]),
}
for requirement in manifest["fieldRequirements"]
if requirement["fieldId"] in missing_field_ids
]
return {
"reportId": f"gap-for-{profile['profileId']}-{manifest['templateId']}",
"templateId": manifest["templateId"],
"profileId": profile["profileId"],
"missingRequired": missing_required,
"missingRecommended": missing_recommended,
"conflicts": [],
"questions": questions,
"generatedAt": generated_at,
}
def derive_follow_up_question_set(
gap_report: dict[str, Any],
*,
generated_at: str,
) -> dict[str, Any]:
return {
"followUpQuestionSetId": f"follow-up-for-{gap_report['reportId']}",
"reportId": gap_report["reportId"],
"templateId": gap_report["templateId"],
"profileId": gap_report["profileId"],
"questions": deepcopy(gap_report["questions"]),
"generatedAt": generated_at,
}
def assemble_follow_up_response_set(
question_set: dict[str, Any],
responses: dict[str, Any],
current_profile: dict[str, Any],
*,
updated_at: str,
) -> dict[str, Any]:
allowed_field_ids = _question_field_ids(question_set)
_validate_response_fields(responses, allowed_field_ids)
if current_profile["profileId"] != question_set["profileId"]:
raise ValueError("follow-up response set profileId does not match follow-up question set")
return {
"followUpResponseSetId": (
f"follow-up-response-for-{question_set['followUpQuestionSetId']}"
),
"followUpQuestionSetId": question_set["followUpQuestionSetId"],
"reportId": question_set["reportId"],
"templateId": question_set["templateId"],
"profileId": question_set["profileId"],
"responses": deepcopy(responses),
"updatedAt": updated_at,
}
def project_follow_up_profile(
response_set: dict[str, Any],
base_projection: dict[str, Any],
) -> dict[str, Any]:
base_profile = base_projection["profile"]
if response_set["profileId"] != base_profile["profileId"]:
raise ValueError("follow-up response set profileId does not match base projection profileId")
if response_set["templateId"] != base_projection["templateId"]:
raise ValueError("follow-up response set templateId does not match base projection templateId")
field_values = deepcopy(base_profile["fieldValues"])
field_values.update(deepcopy(response_set["responses"]))
provenance = deepcopy(base_profile["provenance"])
provenance.update(
{
field_id: [response_set["followUpResponseSetId"]]
for field_id in response_set["responses"].keys()
}
)
return {
"projectionId": (
"follow-up-profile-projection-for-"
+ response_set["followUpResponseSetId"]
),
"followUpResponseSetId": response_set["followUpResponseSetId"],
"followUpQuestionSetId": response_set["followUpQuestionSetId"],
"reportId": response_set["reportId"],
"templateId": base_projection["templateId"],
"templateVersion": base_projection["templateVersion"],
"baseProfileId": base_profile["profileId"],
"profile": {
"profileId": f"profile-from-{response_set['followUpResponseSetId']}",
"fieldValues": field_values,
"provenance": provenance,
"profileStatus": "partial",
"updatedAt": response_set["updatedAt"],
},
}
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Literal
RouteMode = Literal[
"resume_active_session",
"parse_material",
"ask_existing_material",
"continue_drafting",
"stay_freeform",
]
@dataclass(frozen=True)
class ConversationRoute:
mode: RouteMode
reason: str
_CHINESE_RESUME_INTENT_TOKENS = (
"简历",
"改简历",
"优化简历",
"修改简历",
"润色简历",
"制作简历",
"写简历",
"完善简历",
"整理项目经历",
"把项目写进简历",
"生成一版社招后端简历",
)
_ENGLISH_RESUME_INTENT_PATTERNS = (
re.compile(r"\b(?:update|tailor)\s+my\s+(?:resume|cv)\b"),
re.compile(r"\b(?:resume|cv)\s+(?:for|to)\b"),
)
def looks_like_resume_intent(message: str | None) -> bool:
if not message:
return False
lowered = message.lower()
return any(token in message for token in _CHINESE_RESUME_INTENT_TOKENS) or any(
pattern.search(lowered) for pattern in _ENGLISH_RESUME_INTENT_PATTERNS
)
def route_conversation_turn(
*,
user_message: str | None,
has_material: bool,
has_active_session: bool,
drafting_started: bool,
) -> ConversationRoute:
if has_active_session:
return ConversationRoute(
mode="resume_active_session",
reason="active structured session wins",
)
if has_material:
return ConversationRoute(
mode="parse_material",
reason="new material provided",
)
if drafting_started:
return ConversationRoute(
mode="continue_drafting",
reason="drafting already started",
)
if looks_like_resume_intent(user_message):
return ConversationRoute(
mode="ask_existing_material",
reason="resume intent without material",
)
return ConversationRoute(
mode="stay_freeform",
reason="no resume intent detected",
)
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Protocol
@dataclass(frozen=True)
class AskedQuestion:
fieldId: str
question: str
@dataclass
class BatchAnswerResult:
answers: dict[str, Any]
userDeclined: bool = False
class FollowUpBatchAgent(Protocol):
def ask_question_batch(self, batch: list[AskedQuestion]) -> BatchAnswerResult:
...
def materialize_batch(
question_set: dict[str, Any],
field_ids: list[str],
) -> list[AskedQuestion]:
questions_by_field_id = {
item["fieldId"]: item["question"]
for item in question_set["questions"]
}
batch: list[AskedQuestion] = []
for field_id in field_ids:
if field_id not in questions_by_field_id:
raise ValueError(f"unknown fieldId in pending question batch: {field_id}")
batch.append(
AskedQuestion(
fieldId=field_id,
question=questions_by_field_id[field_id],
)
)
return batch
def validate_batch_result(
asked_field_ids: list[str],
batch_result: BatchAnswerResult,
) -> None:
unexpected_field_ids = sorted(
set(batch_result.answers.keys()) - set(asked_field_ids)
)
if unexpected_field_ids:
raise ValueError(
"batch answers contain unasked fields: "
+ ", ".join(unexpected_field_ids)
)
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable
from resume_runtime.runtime.follow_up_agent_adapter import (
AskedQuestion,
BatchAnswerResult,
materialize_batch,
validate_batch_result,
)
from resume_runtime.runtime.follow_up_policy import (
StopDecision,
decide_stop_or_continue,
select_question_batch,
should_close_round,
)
from resume_runtime.runtime.follow_up_state import (
FollowUpLoopState,
projection_ref_from_projection,
)
GapReportFn = Callable[[dict[str, Any]], dict[str, Any]]
QuestionSetFn = Callable[[dict[str, Any]], dict[str, Any]]
AssembleResponseSetFn = Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], dict[str, Any]]
ProjectProfileFn = Callable[[dict[str, Any], dict[str, Any]], dict[str, Any]]
@dataclass(frozen=True)
class FollowUpLoopDependencies:
derive_gap_report: GapReportFn
derive_follow_up_question_set: QuestionSetFn
assemble_follow_up_response_set: AssembleResponseSetFn
project_follow_up_profile: ProjectProfileFn
@dataclass
class FollowUpLoopResult:
state: FollowUpLoopState
gapReport: dict[str, Any] | None = None
questionSet: dict[str, Any] | None = None
nextBatch: list[AskedQuestion] | None = None
followUpResponseSet: dict[str, Any] | None = None
projection: dict[str, Any] | None = None
class FollowUpLoop:
def __init__(self, dependencies: FollowUpLoopDependencies):
self.dependencies = dependencies
def start_or_resume(
self,
state: FollowUpLoopState,
current_projection: dict[str, Any],
*,
question_set: dict[str, Any] | None = None,
) -> FollowUpLoopResult:
if state.loopPhase in {"completed", "awaiting_recommended_decision"}:
return FollowUpLoopResult(state=state)
if state.loopPhase == "asking_batch":
if question_set is None:
raise ValueError("question_set is required when resuming asking_batch")
return FollowUpLoopResult(
state=state,
questionSet=question_set,
nextBatch=materialize_batch(question_set, state.pendingQuestionBatch),
)
if state.loopPhase == "assembling_response_set":
if question_set is None:
raise ValueError(
"question_set is required when resuming assembly or projection"
)
return self._assemble_and_project(state, current_projection, question_set)
if state.loopPhase == "projecting_profile":
if question_set is None:
raise ValueError(
"question_set is required when resuming assembly or projection"
)
return self._project_from_existing_response_set(
state,
current_projection,
question_set,
)
return self._analyze_and_prepare_batch(state, current_projection)
def apply_recommended_decision(
self,
state: FollowUpLoopState,
gap_report: dict[str, Any],
continue_for_recommended: str,
) -> FollowUpLoopResult:
if state.loopPhase != "awaiting_recommended_decision":
raise ValueError(
"recommended decision can only be applied while awaiting_recommended_decision"
)
if continue_for_recommended not in {"yes", "no"}:
raise ValueError(
"continue_for_recommended must be 'yes' or 'no'"
)
state.continueForRecommended = continue_for_recommended
decision = decide_stop_or_continue(gap_report, state.continueForRecommended)
if decision.action == "complete":
state.pendingQuestionBatch = []
state.pendingRoundAnswers = {}
state.currentFollowUpQuestionSetId = None
state.loopPhase = "completed"
state.lastDecisionReason = decision.reason
return FollowUpLoopResult(state=state, gapReport=gap_report)
return self._prepare_question_set(state, gap_report, decision)
def submit_batch_result(
self,
state: FollowUpLoopState,
current_projection: dict[str, Any],
gap_report: dict[str, Any],
question_set: dict[str, Any],
batch_result: BatchAnswerResult,
) -> FollowUpLoopResult:
validate_batch_result(state.pendingQuestionBatch, batch_result)
state.pendingRoundAnswers.update(batch_result.answers)
if should_close_round(
gap_report,
state.pendingRoundAnswers,
user_declined=batch_result.userDeclined,
):
state.pendingQuestionBatch = []
state.loopPhase = "assembling_response_set"
state.lastDecisionReason = "closing current round"
return self._assemble_and_project(state, current_projection, question_set)
state.pendingQuestionBatch = select_question_batch(
question_set,
state.pendingRoundAnswers,
state.batchSizePolicy,
)
if not state.pendingQuestionBatch:
state.loopPhase = "assembling_response_set"
state.lastDecisionReason = "no unanswered questions remain in current round"
return self._assemble_and_project(state, current_projection, question_set)
state.loopPhase = "asking_batch"
state.lastDecisionReason = "continuing current round with another small batch"
return FollowUpLoopResult(
state=state,
gapReport=gap_report,
questionSet=question_set,
nextBatch=materialize_batch(question_set, state.pendingQuestionBatch),
)
def _analyze_and_prepare_batch(
self,
state: FollowUpLoopState,
current_projection: dict[str, Any],
) -> FollowUpLoopResult:
state.loopPhase = "analyzing_gaps"
gap_report = self.dependencies.derive_gap_report(current_projection)
state.currentGapReportId = gap_report["reportId"]
state.history.gapReportIds.append(gap_report["reportId"])
decision = decide_stop_or_continue(
gap_report,
state.continueForRecommended,
)
if decision.action == "complete":
state.pendingQuestionBatch = []
state.pendingRoundAnswers = {}
state.currentFollowUpQuestionSetId = None
state.loopPhase = "completed"
state.lastDecisionReason = decision.reason
return FollowUpLoopResult(state=state, gapReport=gap_report)
if decision.action == "await_recommended_decision":
state.pendingQuestionBatch = []
state.pendingRoundAnswers = {}
state.currentFollowUpQuestionSetId = None
state.loopPhase = "awaiting_recommended_decision"
state.lastDecisionReason = decision.reason
return FollowUpLoopResult(state=state, gapReport=gap_report)
return self._prepare_question_set(state, gap_report, decision)
def _prepare_question_set(
self,
state: FollowUpLoopState,
gap_report: dict[str, Any],
decision: StopDecision,
) -> FollowUpLoopResult:
question_set = self.dependencies.derive_follow_up_question_set(gap_report)
state.currentFollowUpQuestionSetId = question_set["followUpQuestionSetId"]
state.history.followUpQuestionSetIds.append(question_set["followUpQuestionSetId"])
state.pendingRoundAnswers = {}
state.pendingQuestionBatch = select_question_batch(
question_set,
state.pendingRoundAnswers,
state.batchSizePolicy,
)
if not state.pendingQuestionBatch:
raise ValueError("follow-up question set produced no pending question batch")
state.loopPhase = "asking_batch"
state.lastDecisionReason = decision.reason
return FollowUpLoopResult(
state=state,
gapReport=gap_report,
questionSet=question_set,
nextBatch=materialize_batch(question_set, state.pendingQuestionBatch),
)
def _assemble_and_project(
self,
state: FollowUpLoopState,
current_projection: dict[str, Any],
question_set: dict[str, Any],
) -> FollowUpLoopResult:
state.loopPhase = "assembling_response_set"
response_set = self.dependencies.assemble_follow_up_response_set(
question_set,
dict(state.pendingRoundAnswers),
current_projection["profile"],
)
state.history.followUpResponseSetIds.append(
response_set["followUpResponseSetId"]
)
return self._project_from_response_set(state, current_projection, response_set)
def _project_from_existing_response_set(
self,
state: FollowUpLoopState,
current_projection: dict[str, Any],
question_set: dict[str, Any],
) -> FollowUpLoopResult:
response_set = self.dependencies.assemble_follow_up_response_set(
question_set,
dict(state.pendingRoundAnswers),
current_projection["profile"],
)
return self._project_from_response_set(state, current_projection, response_set)
def _project_from_response_set(
self,
state: FollowUpLoopState,
current_projection: dict[str, Any],
response_set: dict[str, Any],
) -> FollowUpLoopResult:
state.loopPhase = "projecting_profile"
projection = self.dependencies.project_follow_up_profile(
response_set,
current_projection,
)
state.history.followUpProfileProjectionIds.append(projection["projectionId"])
state.currentProjectionRef = projection_ref_from_projection(
projection,
"follow-up",
)
state.pendingQuestionBatch = []
state.pendingRoundAnswers = {}
state.currentGapReportId = None
state.currentFollowUpQuestionSetId = None
state.lastDecisionReason = "projected follow-up round and re-entered gap analysis"
next_result = self._analyze_and_prepare_batch(state, projection)
next_result.followUpResponseSet = response_set
next_result.projection = projection
return next_result
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Literal
RecommendedDecision = Literal["unset", "yes", "no"]
DecisionAction = Literal[
"continue",
"await_recommended_decision",
"complete",
]
@dataclass(frozen=True)
class StopDecision:
action: DecisionAction
reason: str
def select_question_batch(
question_set: dict[str, Any],
pending_round_answers: dict[str, Any],
batch_size: int,
) -> list[str]:
normalized_batch_size = max(1, batch_size)
unanswered_field_ids = [
item["fieldId"]
for item in question_set["questions"]
if item["fieldId"] not in pending_round_answers
]
return unanswered_field_ids[:normalized_batch_size]
def should_close_round(
gap_report: dict[str, Any],
pending_round_answers: dict[str, Any],
*,
user_declined: bool = False,
) -> bool:
if user_declined:
return True
missing_required_fields = set(gap_report["missingRequired"])
answered_fields = set(pending_round_answers.keys())
return missing_required_fields.issubset(answered_fields)
def decide_stop_or_continue(
gap_report: dict[str, Any],
continue_for_recommended: RecommendedDecision,
) -> StopDecision:
if gap_report["missingRequired"]:
return StopDecision(
action="continue",
reason="required fields remain",
)
if not gap_report["missingRecommended"]:
return StopDecision(
action="complete",
reason="required and recommended fields are complete",
)
if continue_for_recommended == "yes":
return StopDecision(
action="continue",
reason="user chose to continue for recommended fields",
)
if continue_for_recommended == "no":
return StopDecision(
action="complete",
reason="user chose to stop after required fields",
)
return StopDecision(
action="await_recommended_decision",
reason="only recommended fields remain",
)
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
LoopPhase = Literal[
"analyzing_gaps",
"asking_batch",
"assembling_response_set",
"projecting_profile",
"awaiting_recommended_decision",
"completed",
]
ProjectionKind = Literal["guided-intake", "follow-up"]
RecommendedDecision = Literal["unset", "yes", "no"]
@dataclass(frozen=True)
class CurrentProjectionRef:
projectionKind: ProjectionKind
projectionId: str
profileId: str
@dataclass
class FollowUpLoopHistory:
gapReportIds: list[str] = field(default_factory=list)
followUpQuestionSetIds: list[str] = field(default_factory=list)
followUpResponseSetIds: list[str] = field(default_factory=list)
followUpProfileProjectionIds: list[str] = field(default_factory=list)
@dataclass
class FollowUpLoopState:
templateId: str
templateVersion: str
currentProjectionRef: CurrentProjectionRef
currentGapReportId: str | None = None
currentFollowUpQuestionSetId: str | None = None
pendingQuestionBatch: list[str] = field(default_factory=list)
pendingRoundAnswers: dict[str, Any] = field(default_factory=dict)
loopPhase: LoopPhase = "analyzing_gaps"
continueForRecommended: RecommendedDecision = "unset"
batchSizePolicy: int = 2
lastDecisionReason: str = "loop initialized"
history: FollowUpLoopHistory = field(default_factory=FollowUpLoopHistory)
def projection_ref_from_projection(
projection: dict[str, Any],
projection_kind: ProjectionKind,
) -> CurrentProjectionRef:
return CurrentProjectionRef(
projectionKind=projection_kind,
projectionId=projection["projectionId"],
profileId=projection["profile"]["profileId"],
)
def new_follow_up_loop_state(
*,
template_id: str,
template_version: str,
projection: dict[str, Any],
projection_kind: ProjectionKind,
batch_size_policy: int = 2,
) -> FollowUpLoopState:
return FollowUpLoopState(
templateId=template_id,
templateVersion=template_version,
currentProjectionRef=projection_ref_from_projection(
projection,
projection_kind,
),
batchSizePolicy=batch_size_policy,
)
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
from uuid import uuid4
from resume_runtime.runtime.follow_up_agent_adapter import AskedQuestion
from resume_runtime.runtime.host_session_runner import (
HostSessionAction,
HostSessionRunner,
HostSessionRunnerError,
)
from resume_runtime.runtime.host_session_state import HostSessionState
from resume_runtime.runtime.host_session_store import HostSessionStore, HostSessionStoreError
from resume_runtime.runtime.session_runner import SessionRunner
HostConversationTurnKind = Literal["resume", "reply"]
HostConversationMode = Literal["structured", "freeform"]
HostPromptDirective = Literal[
"ask_current_batch",
"ask_yes_no_only",
"handoff_to_drafting",
"stay_freeform",
]
_RUNTIME_TO_PROMPT_DIRECTIVE: dict[str, HostPromptDirective] = {
"ask_batch": "ask_current_batch",
"await_recommended_decision": "ask_yes_no_only",
"completed": "handoff_to_drafting",
}
class HostConversationAdapterError(Exception):
"""Raised when host conversation routing cannot safely use structured runtime."""
@dataclass(frozen=True)
class HostConversationOutcome:
mode: HostConversationMode
promptDirective: HostPromptDirective
sessionId: str | None = None
sessionState: HostSessionState | None = None
nextActionKind: str | None = None
currentProjection: dict[str, Any] | None = None
currentBatch: list[AskedQuestion] | None = None
class HostConversationAdapter:
def __init__(
self,
store: HostSessionStore,
session_runner: SessionRunner,
) -> None:
self._store = store
self._runner = HostSessionRunner(store, session_runner)
def handle_turn(
self,
*,
turn_kind: HostConversationTurnKind,
timestamp: str,
user_reply: str | None = None,
manifest: dict[str, Any] | None = None,
checklist: dict[str, Any] | None = None,
guided_answers: dict[str, Any] | None = None,
intake_session: dict[str, Any] | None = None,
) -> HostConversationOutcome:
try:
active_session = self._store.find_active_session()
if active_session is not None:
if turn_kind == "resume":
action = self._runner.resume_session(
active_session.sessionId,
timestamp=timestamp,
)
elif turn_kind == "reply":
action = self._runner.continue_session(
session_id=active_session.sessionId,
user_reply=user_reply,
timestamp=timestamp,
)
else:
raise HostConversationAdapterError(f"Unsupported turn kind: {turn_kind}")
return self._structured_outcome(action)
if self._has_structured_start_inputs(
manifest=manifest,
checklist=checklist,
guided_answers=guided_answers,
):
action = self._runner.start_structured_guided_intake_session(
session_id=self._new_session_id(),
manifest=manifest,
checklist=checklist,
guided_answers=guided_answers,
timestamp=timestamp,
intake_session=intake_session,
)
return self._structured_outcome(action)
return HostConversationOutcome(
mode="freeform",
promptDirective="stay_freeform",
)
except HostConversationAdapterError:
raise
except (HostSessionStoreError, HostSessionRunnerError, ValueError, KeyError, TypeError) as exc:
raise HostConversationAdapterError(str(exc)) from exc
@staticmethod
def _has_structured_start_inputs(
*,
manifest: dict[str, Any] | None,
checklist: dict[str, Any] | None,
guided_answers: dict[str, Any] | None,
) -> bool:
provided = [manifest is not None, checklist is not None, guided_answers is not None]
if any(provided) and not all(provided):
raise HostConversationAdapterError(
"Structured start requires manifest, checklist, and guided_answers"
)
return all(provided)
@staticmethod
def _new_session_id() -> str:
return f"host-session-{uuid4()}"
@staticmethod
def _structured_outcome(action: HostSessionAction) -> HostConversationOutcome:
next_action_kind = action.nextActionKind
try:
prompt_directive = _RUNTIME_TO_PROMPT_DIRECTIVE[next_action_kind]
except KeyError as exc:
raise HostConversationAdapterError(
f"Unsupported runtime next action kind: {next_action_kind}"
) from exc
return HostConversationOutcome(
mode="structured",
promptDirective=prompt_directive,
sessionId=action.sessionState.sessionId,
sessionState=action.sessionState,
nextActionKind=next_action_kind,
currentProjection=action.currentProjection,
currentBatch=action.currentBatch,
)
def default_host_session_store_path(skill_root: Path | None = None) -> Path:
base_root = skill_root or Path(__file__).resolve().parents[1]
return Path(base_root) / ".runtime" / "host_sessions"
__all__ = [
"HostConversationAdapter",
"HostConversationAdapterError",
"HostConversationMode",
"HostConversationOutcome",
"HostConversationTurnKind",
"HostPromptDirective",
"default_host_session_store_path",
]
from __future__ import annotations
import sys
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parent
if str(SKILL_ROOT) not in sys.path:
sys.path.insert(0, str(SKILL_ROOT))
from resume_runtime.template_catalog_cli import main
if __name__ == "__main__":
raise SystemExit(main())
from __future__ import annotations
import sys
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parent
if str(SKILL_ROOT) not in sys.path:
sys.path.insert(0, str(SKILL_ROOT))
from resume_runtime.template_store_cli import main
if __name__ == "__main__":
raise SystemExit(main())