CR identifier. CR-2026-056
Version. 0.1
Date. 2026-05-05
Status. Approved for execution.
Author. Claude.ai (CR drafting layer). Operator: Marvin Percival.
Executing agent. Claude Code (CC) on DUNIN7-M4.
Depends on. Phase 41 complete (tag phase-41-companion-identity-and-personal-engagement at 74ec7e7). Phase 40 complete (Orchestration API, vocabulary wall, converse endpoint with create_project and finalize_project intents).
Informed by. Phase 42 scoping note v0.1 (seven decisions settled). Companion expertise note v0.1. Product identity standing note v0.1. Personal memory investigation v0.1 (accepted). Companion as agent investigation v0.1 (accepted). Operator Layer discovery v0.4.
Phase 42 gives the Companion a brain (intent classification) and a voice (system prompt). Today the converse endpoint handles two intents — create_project and finalize_project — via caller-declared intent_hint. Phase 42 replaces this with LLM-based classification: the Operator sends natural language, the Companion determines what they mean, routes to the appropriate engine operation, and responds in the Companion's voice using Operator vocabulary.
After Phase 42 the Companion can: create projects, add knowledge to a project, answer questions about project progress and past input, initiate downloads, and carry on general domain-informed conversation — all through natural language. Production-pipeline intents (draft, approve, revise, redirect) ship as voiced stubs.
Substrate-only phase. No frontend changes.
All seven decisions from the scoping note v0.1 are adopted without modification.
| Decision | Summary |
|----------|---------|
| S1 | Separate classification call before response generation. Three stages: classify → route → respond. intent_hint overrides classifier when provided. |
| S2 | Seven live intents, four stub intents. Stubs are voiced "not yet" responses, not 501s. |
| S3 | Component-based system prompt: persona (static, versioned) + engagement context (dynamic) + intent instruction (per classified intent). Companion name wired in. |
| S4 | Tiered engagement context loading: Tier 1 seed-only, Tier 2 seed+recent assertions, Tier 3 Manifestation-derived. Informal cache per engagement. ~2,000 token budget. |
| S5 | general_conversation is the default intent. Confidence threshold (0.7 initially) — below threshold defaults to general conversation. |
| S6 | Substrate-only phase. No frontend. |
| S7 | Same key resolution pattern (person-level key with system fallback). Two LLM calls per turn. |
phase-41-companion-identity-and-personal-engagement at 74ec7e7.Step 0 confirms:
POST /operator/converse in src/loomworks/orchestration/routers/. [CC verifies: exact file, function name, route path.]ConverseRequest and ConverseResponse schemas exist in src/loomworks/orchestration/schemas.py. The request has intent_hint, message, project_id fields. The response has companion_message, side_effects, suggested_actions, project_id, draft_specification, readiness fields. [CC verifies: exact field names and types.]ConverseSideEffect schema exists. [CC verifies: location and fields.]POST /engagements/{eid}/seed/converse. The converse endpoint currently wraps this for create_project intent. [CC verifies: function name and how the orchestration converse endpoint currently calls it.]person.companion_name column exists (Phase 41). [CC verifies: column type, default value.]person.personal_engagement_id column exists (Phase 41). [CC verifies: FK target.]actor_from_companion helper exists in orchestration module. [CC verifies: file, signature.]actor_from_person helper exists in orchestration module. [CC verifies: file, signature.]verify_project_membership helper exists. [CC verifies: file, signature, return type.]GET /me/dashboard/active, /me/dashboard/needs_you, /me/dashboard/recent. [CC verifies: router file, function names.][CC verifies: router file, function names or relevant query functions.][CC verifies: the function-level entry point for creating a text-mode assertion — not the HTTP endpoint, but the function the router can call directly. File, signature, required arguments.][CC verifies: function for querying assertions on an engagement with display numbers and content. File, signature.]GET /operator/library. [CC verifies: router file, function name, and the download function that can be called directly.][CC verifies: file, function name, return type.][CC verifies: file, function.][CC verifies: the function or helper that resolves person-level key → system key → 503. File, signature.]ActorRef(kind="companion") construction works. [CC verifies: enum value exists in actor_kind type.]src/loomworks/orchestration/ with schemas.py, translators.py, helpers.py, routers/ subdirectory. [CC verifies: directory listing.]conversation_turns table. [CC verifies: no model, no migration.][CC verifies: how to check assertion existence and Manifestation existence for an engagement.]Pre-flight divergence stops execution at Step 0 and drives a CR amendment.
Archive this CR to docs/phase-crs/phase-42-cr-intent-classification-and-reactive-companion-v0_1.md at Step 0 before Step 1 begins.
CREATE TABLE conversation_turns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
person_id UUID NOT NULL REFERENCES persons(id),
engagement_id UUID REFERENCES engagements(id), -- nullable: project-less conversation
role TEXT NOT NULL, -- 'operator' or 'companion'
content TEXT NOT NULL,
classified_intent TEXT, -- null for operator turns, intent label for companion turns
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_conversation_turns_person_engagement
ON conversation_turns(person_id, engagement_id, created_at DESC);
The role column uses plain TEXT (not an enum) to match the existing pattern for short fixed-vocabulary columns. The classified_intent column records the intent that was classified for this exchange — stored on the companion turn so each response is tagged with the intent that produced it. Operator turns have classified_intent = NULL.
Retention. All turns kept. No pruning in Phase 42. Context window management is handled by the prompt assembler — it includes the most recent N turns (initially 10) in classification and response calls.
Relationship to Phase 31 conversation history. Phase 31 stores seed conversation turns as events on the candidate engagement (event kinds operator_turn, companion_turn). Phase 42's conversation_turns table is a separate, general-purpose conversation log for the Companion. When the converse endpoint handles create_project, it continues to use the Phase 31 event-based conversation (the seed conversation has its own lifecycle). For all other intents, conversation turns are recorded in conversation_turns. This avoids mixing general Companion conversation with seed-specific conversation events.
Live intents:
| Intent | Engine operation | Router action |
|--------|-----------------|---------------|
| create_project | Phase 31 seed conversation | Delegate to existing Phase 40 create-project flow. No change to current behavior. |
| finalize_project | Engagement instantiation | Delegate to existing Phase 40 finalize-project flow. No change to current behavior. |
| add_knowledge | Assertion creation (text mode) | Call assertion creation function directly (function-level, not HTTP). Create a held assertion with ActorRef(kind="companion"). The classifier extracts the knowledge text from the Operator's message via extracted_parameters["knowledge_text"]. |
| ask_about_progress | Dashboard + activity query | Call Phase 39 dashboard functions and Phase 33 activity functions directly. Collect active jobs, items needing attention, recent artifacts. Pass results to responder. |
| ask_about_past_input | Assertion query | Call assertion listing function directly. Retrieve committed assertions with display numbers and content summaries. Pass results to responder. |
| request_download | Library download | Call Phase 40 library function directly. The classifier extracts extracted_parameters["artifact_description"] — a description of what the Operator wants to download. The router searches available artifacts by matching the description. If a unique match is found, return the download information. If ambiguous, return the list of candidates for the responder to present. |
| general_conversation | None | No engine operation. Pass the message, engagement context, and conversation history directly to the responder. |
Stub intents:
| Intent | Stub response guidance |
|--------|----------------------|
| request_draft | Acknowledge the Operator wants a draft produced. Explain the Companion can't initiate drafting through conversation yet. Direct to the Workshop if the Operator wants to trigger a shape manually. |
| approve_draft | Acknowledge the Operator wants to approve something. Explain approval through conversation isn't available yet. Direct to the Workshop. |
| request_revision | Acknowledge the Operator wants to revise something. Explain conversational revision isn't available yet. Direct to the Workshop. |
| request_redirect | Acknowledge the Operator wants to redirect knowledge across projects. Explain this isn't available through conversation yet. |
Stub responses go through the full response-generation pipeline (system prompt, Companion voice) — they are not hardcoded strings. The router returns a stub marker ({"stub": True, "intent": "request_draft", "guidance": "..."}) that the responder uses as its intent instruction.
The classifier prompt is a versioned text asset stored outside Python code. [CC determines: file location — a text file in the orchestration module, a config directory, or a templates directory. The requirement is that the prompt text is editable without changing Python source.]
The classifier prompt contains:
intent, confidence (0.0–1.0), and extracted_parameters (a dict of intent-specific extracted data).general_conversation with high confidence. Below 0.7 confidence on any specific intent, default to general_conversation."add_knowledge: extract knowledge_text — the substantive content the Operator wants remembered, stripped of conversational framing.request_download: extract artifact_description — what the Operator wants to download.create_project / finalize_project: no extraction needed (existing flow handles these).extracted_parameters.
The classifier receives: the Operator's message, the most recent N conversation turns (for conversational context), and a compact engagement summary (project name + brief state — e.g., "FarmGuard: 12 saved notes, 2 specifications, 1 artifact"). The engagement summary helps the classifier distinguish between intents (e.g., "show me the report" is request_download if an artifact exists, ask_about_progress if none exists).
The persona component is a versioned text asset stored outside Python code, in the same location pattern as the classifier prompt. [CC determines: storage location.]
The persona component text:
You are {companion_name}, a lead expert working alongside {operator_name} on their projects in Loomworks.
POSTURE
You bring domain expertise to every project you work on. {operator_name} brings parameters, judgment, and authority. You arrive already knowing the structural shape of the domain — the questions that matter, the patterns that recur, the places where projects commonly go wrong. Your job is to bring that expertise to bear on {operator_name}'s specific situation.
KNOWLEDGE SOURCES
Your primary source is what {operator_name} has told you — their notes, their decisions, their stated goals. This is authoritative. Your secondary source is your general domain knowledge. When {operator_name}'s notes overlap with what you know generally, anchor in what they said and extend with what you know. When they conflict, {operator_name}'s notes win — but surface the conflict rather than silently overriding.
VOICE
- Warm without sycophancy. You are genuinely interested in the work. Your interest shows in attention, not adjectives. Never say "Great question!" or "I love that idea!"
- Capable without performance. You do not announce your expertise. You demonstrate it by asking the right questions and noticing what matters.
- Direct without curtness. Get to the point. Do not pad with throat-clearing. The right length is the length the answer deserves.
- Honest about uncertainty. When you do not know, say so. "I'm not certain about that" is always acceptable.
- Authoritative about what the project holds. You speak with confidence about what {operator_name} has said, decided, and produced — because you have reliable access to their project.
NEVER
- Never say "as an AI" or mention model names, training, or the technology behind you. You are {companion_name}. That is your identity.
- Never use engine vocabulary. Never say: engagement, assertion, shape, render, manifestation, specialist, materializer, normative_force, held, committed, retracted. Use Operator vocabulary: project, note, specification, artifact, draft, saved, waiting.
- Never perform enthusiasm you do not have. Never pad responses with filler.
- Never claim to remember conversations you did not have. If you are unsure whether something was discussed, say so.
- Never invent provenance. If you say "{operator_name} mentioned earlier that..." you must be drawing on actual project content. If you cannot, do not make the claim.
REFINEMENT
When the Operator contributes knowledge, offer your interpretation. Let them confirm or refine. "So you're saying the irrigation schedule needs to account for soil type — is that the key variable, or are there others?" This is how contributions become precise.
Template variables:
{companion_name} — from person.companion_name.{operator_name} — from person.display_name or person.email (whichever is available). [CC determines: the person field that carries the display name.]
The engagement context loader is a function in src/loomworks/orchestration/prompt.py (or a submodule if CC judges the file too large).
Tier determination:
def determine_context_tier(engagement_id: UUID, db: Session) -> Literal["seed_only", "seed_recent", "manifestation"]:
# Check if a Manifestation exists for this engagement
has_manifestation = ... # [CC verifies: query pattern]
if has_manifestation:
return "manifestation"
# Check if committed assertions exist
has_assertions = ... # [CC verifies: query pattern]
if has_assertions:
return "seed_recent"
return "seed_only"
Tier 1 — Seed only. Load the engagement's seed content. Format as a compact summary: project name, the six seed fields (what the work is, who consumes it, voice, constraints, success conditions, additional assertions) — each truncated to ~200 characters if longer.
Tier 2 — Seed + recent assertions. Seed summary (as Tier 1) + the most recent 20 committed assertions. Each assertion formatted as: #{display_number}: {content_summary} where content_summary is the first ~150 characters. Ordered by display number descending (most recent first).
Tier 3 — Manifestation-derived. Seed summary (as Tier 1) + the Manifestation structure: each group label, the count of assertions in the group, and a 1–2 sentence summary of each group's content. The rationale field for each group is included if it fits within budget. Individual assertion content is omitted — the Manifestation summary is the Companion's organized view.
Token budget. The assembled context targets ~2,000 tokens. If the content exceeds this (large engagements with many groups), compress: group labels and counts only, no per-group summaries. The Companion can drill deeper by querying assertions directly when the Operator asks about specifics.
[CC determines: cache implementation. Options from the scoping note: a column on the engagement table, a separate cache table, or an in-memory cache with TTL. The CR does not prescribe the mechanism. Requirements:]
The cache is informal — it does not appear in the API contract, is not queryable, and has no lifecycle of its own.
Each classified intent carries an instruction block that is appended to the system prompt for the response-generation call. These are stored alongside the persona and classifier prompt as versioned text assets.
create_project instruction:
The Operator wants to start a new project. You are beginning the project creation conversation. Ask about what they're building, who will use it, and what constraints matter. This is the start of a structured conversation — guide it naturally.
finalize_project instruction:
The Operator wants to finalize their project setup. Review what you have and confirm they're ready to proceed.
add_knowledge instruction:
The Operator just told you something they want remembered in their project. You are noting it down. Acknowledge substantively — show you understood what they said, not just that you recorded it. If the knowledge is ambiguous or could be interpreted multiple ways, offer your interpretation and let them refine it.
ask_about_progress instruction:
The Operator wants to know how their project is going. You have the current state below. Summarize conversationally: what's active, what needs their attention, what recently finished. If nothing is happening, say so honestly.
PROJECT STATE:
{operation_result}
ask_about_past_input instruction:
The Operator wants to know what they've told you — their past notes and contributions. You have the relevant notes below. Present them conversationally, using display numbers so the Operator can reference specific items. If they asked about a specific topic, focus on relevant notes.
NOTES:
{operation_result}
request_download instruction:
The Operator wants to download an artifact from their project. You have the search results below. If a single match was found, confirm and provide the download. If multiple candidates were found, present the options and ask which one. If no match was found, let them know what artifacts are available.
SEARCH RESULTS:
{operation_result}
general_conversation instruction:
The Operator is talking with you about their project or domain. Bring your expertise. This is conversation, not a command — do not trigger any engine operations. Discuss, advise, question, suggest. If the conversation naturally leads toward something actionable (noting something down, starting a draft), let the Operator drive that decision.
Stub intent instruction (shared across all four stubs):
The Operator asked for something you can't do through conversation yet: {stub_intent_description}. Acknowledge what they want. Explain you can't do it through conversation yet — it's coming. If the operation is available in the Workshop (the existing project interface), mention that. Stay in your voice. Do not apologize excessively — one sentence of acknowledgment is enough.
WHAT THEY ASKED FOR: {stub_intent_description}
Stub intent descriptions:
request_draft: "They want you to produce a draft of a specification or artifact."approve_draft: "They want to approve or confirm a draft."request_revision: "They want to revise or update an existing specification or artifact."request_redirect: "They want to move or redirect knowledge across projects."
The ConverseResponse schema gains one new field:
class ConverseResponse(BaseModel):
companion_message: str
side_effects: list[ConverseSideEffect]
suggested_actions: list[str]
project_id: UUID | None = None
draft_specification: str | None = None
readiness: Literal["drafting", "ready_for_review"] | None = None
classified_intent: str | None = None # NEW — the intent label from classification
The classified_intent field is for debugging and logging. It is not part of the Operator-facing contract — the frontend does not display it. It allows API callers (and future telemetry) to see what the classifier decided.
Backward compatibility. All new fields are optional with None defaults. Existing callers that only check companion_message, side_effects, and suggested_actions are unaffected.
The existing converse endpoint is rewritten to use the classify → route → respond pipeline. The endpoint function becomes:
async def converse(request: ConverseRequest, person: Person, db: Session):
# 1. Load engagement context (if project_id provided)
engagement_context = None
engagement = None
if request.project_id:
engagement = verify_project_membership(person, request.project_id, db)
engagement_context = load_engagement_context(engagement, db)
# 2. Load conversation history
recent_turns = get_recent_turns(person.id, engagement_id_or_none, db, limit=10)
# 3. Classify intent (or use intent_hint)
if request.intent_hint:
classified = ClassifiedIntent(
intent=request.intent_hint,
confidence=1.0,
extracted_parameters={}
)
else:
classified = await classify_intent(
message=request.message,
conversation_history=recent_turns,
engagement_summary=engagement_context.summary if engagement_context else None,
llm_key=resolve_llm_key(person, db)
)
# Apply confidence threshold
if classified.confidence < CONFIDENCE_THRESHOLD and classified.intent != "general_conversation":
classified = ClassifiedIntent(
intent="general_conversation",
confidence=classified.confidence,
extracted_parameters={}
)
# 4. Route to engine operation
operation_result = await route_intent(
classified=classified,
person=person,
engagement=engagement,
db=db
)
# 5. Assemble system prompt
system_prompt = assemble_prompt(
companion_name=person.companion_name,
operator_name=person.display_name, # [CC verifies field name]
engagement_context=engagement_context,
classified_intent=classified,
operation_result=operation_result
)
# 6. Generate response
companion_message = await generate_response(
system_prompt=system_prompt,
conversation_history=recent_turns,
operator_message=request.message,
llm_key=resolve_llm_key(person, db)
)
# 7. Record conversation turns
record_turn(person.id, engagement_id_or_none, "operator", request.message, None, db)
record_turn(person.id, engagement_id_or_none, "companion", companion_message, classified.intent, db)
# 8. Build response
return ConverseResponse(
companion_message=companion_message,
side_effects=operation_result.side_effects if operation_result else [],
suggested_actions=operation_result.suggested_actions if operation_result else [],
project_id=request.project_id or operation_result.project_id if operation_result else None,
draft_specification=operation_result.draft_specification if operation_result else None,
readiness=operation_result.readiness if operation_result else None,
classified_intent=classified.intent
)
This is illustrative pseudocode. CC writes the actual implementation using verified function names and signatures from pre-flight.
Backward compatibility for create_project and finalize_project. When intent_hint="create_project" or intent_hint="finalize_project", the pipeline short-circuits classification and delegates to the existing Phase 40 flow. The existing behavior is preserved exactly — the create-project and finalize-project paths are not rewritten, only wrapped. The classifier can also independently classify messages as create_project or finalize_project (e.g., "I want to start a new project" without intent_hint), which routes to the same flow.
The router is a function (not a class) in src/loomworks/orchestration/router.py.
async def route_intent(
classified: ClassifiedIntent,
person: Person,
engagement: Engagement | None,
db: Session
) -> RouteResult | None:
RouteResult schema:
class RouteResult(BaseModel):
side_effects: list[ConverseSideEffect] = []
suggested_actions: list[str] = []
project_id: UUID | None = None
draft_specification: str | None = None
readiness: Literal["drafting", "ready_for_review"] | None = None
operation_data: dict = {} # Intent-specific data for the responder
is_stub: bool = False
stub_guidance: str | None = None
The operation_data dict carries intent-specific results that the responder uses to construct the Companion's message:
add_knowledge: {"assertion_display_number": 42, "knowledge_text": "..."} (after creating the assertion).ask_about_progress: {"active_jobs": [...], "needs_attention": [...], "recent_artifacts": [...]}.ask_about_past_input: {"assertions": [{"display_number": 1, "content": "..."}, ...]}.request_download: {"match": "unique" | "ambiguous" | "none", "artifacts": [...]}.operation_data is empty; is_stub=True and stub_guidance carries the guidance text.
add_knowledge routing detail. The router calls the assertion creation function with:
add_knowledge requires a project context).classified.extracted_parameters["knowledge_text"].ActorRef(kind="companion") — the Companion is the actor because it is creating the assertion on behalf of the Operator's instruction.
If no project_id is provided and the intent is add_knowledge, the router returns an error result and the responder asks the Operator which project they're talking about.
ask_about_progress routing detail. The router calls the Phase 39 dashboard functions (active, needs_you, recent) scoped to the current engagement (not cross-engagement — the Operator is asking about a specific project). [CC verifies: whether the dashboard functions accept an engagement filter, or whether the router needs to filter the results.]
ask_about_past_input routing detail. The router calls the assertion query function for the current engagement. Returns committed assertions with display numbers and content. If the Operator's message suggests a specific topic (from extracted_parameters), the router can filter — but Phase 42 ships without topic-based filtering (the responder summarizes all assertions and the LLM naturally focuses on what the Operator asked about).
request_download routing detail. The router calls the Phase 40 library function for the current engagement. If extracted_parameters["artifact_description"] is present, the router attempts to match it against available artifact names/types. Matching is simple string-based (substring or keyword match against artifact titles/types). If ambiguous (multiple matches or no matches), all available artifacts are returned for the responder to present.
The responder is a function in src/loomworks/orchestration/responder.py.
async def generate_response(
system_prompt: str,
conversation_history: list[ConverseTurn],
operator_message: str,
llm_key: str
) -> str:
The responder calls the LLM (same model and calling pattern as Phase 31's seed conversation) with:
assemble_prompt().The responder returns the Companion's response text. It does not parse structured output — the response is free-form natural language in the Companion's voice.
Error handling. If the LLM call fails (key invalid, rate limit, service unavailable), the responder returns a fallback message in the Companion's voice: "I'm having trouble thinking right now — can you try again in a moment?" The fallback is not an error code; it is a voiced response. The ConverseResponse still returns 200 with the fallback message and an empty side_effects list.
create_project and finalize_project conversation turn recording
Today the create_project flow uses Phase 31's event-based conversation history (events on the candidate engagement). Phase 42 does not change this. The create_project and finalize_project intents continue to use their existing conversation mechanisms.
However, Phase 42 also records these turns in the conversation_turns table so the general conversation history includes project-creation exchanges. The responder records all turns regardless of intent. This means create_project turns appear in both the Phase 31 event log (for seed conversation continuity) and the conversation_turns table (for general conversation context).
conversation_turns table
One migration. Creates the conversation_turns table per D1.
Columns: id (UUID PK), person_id (UUID FK NOT NULL), engagement_id (UUID FK nullable), role (TEXT NOT NULL), content (TEXT NOT NULL), classified_intent (TEXT nullable), created_at (TIMESTAMPTZ NOT NULL DEFAULT now()).
Index: (person_id, engagement_id, created_at DESC).
No data migration. The table starts empty.
Phase 42 adds four new files to src/loomworks/orchestration/:
| File | Purpose |
|------|---------|
| classifier.py | Intent classification module. Loads classifier prompt, calls LLM, parses structured response, returns ClassifiedIntent. |
| router.py | Deterministic intent dispatcher. Maps ClassifiedIntent to engine function calls. Returns RouteResult. |
| prompt.py | System prompt assembler. Loads persona component, engagement context, intent instructions. Assembles into a single system prompt string. Includes the engagement context loader and cache. |
| responder.py | Response generator. Calls LLM with assembled system prompt, conversation history, and operator message. Returns Companion response text. |
Additionally, versioned text assets for the persona component, classifier prompt, and intent instruction templates. [CC determines: storage location — a templates/ or prompts/ subdirectory within the orchestration module, or a config directory. The requirement is that these are editable text files, not embedded Python strings.]
The converse endpoint router file (in routers/) is modified to use the new pipeline.
The conversation_turns model is added to the appropriate models file. [CC determines: location — existing models file or new file.]
ClassifiedIntent
class ClassifiedIntent(BaseModel):
intent: Literal[
"create_project",
"finalize_project",
"add_knowledge",
"ask_about_progress",
"ask_about_past_input",
"request_download",
"request_draft",
"approve_draft",
"request_revision",
"request_redirect",
"general_conversation",
]
confidence: float # 0.0–1.0
extracted_parameters: dict # Intent-specific extracted data
ClassificationInput
class ClassificationInput(BaseModel):
message: str
conversation_history: list[dict] # Simplified turn dicts for the classifier
engagement_summary: str | None
project_id: UUID | None
RouteResultPer D10.
ConverseTurn (database model)
Per D1. The SQLAlchemy model for the conversation_turns table.
ConverseResponse extension
Per D8. One new optional field: classified_intent.
All new schemas in the orchestration module use Operator vocabulary. The existing vocabulary-wall enforcement test (Phase 40) must continue to pass after Phase 42.
Specific checks:
ClassifiedIntent.intent values use Operator vocabulary where applicable: create_project (not create_engagement), add_knowledge (not create_assertion), request_download (not get_render).RouteResult uses Operator vocabulary in field names and values.
The classified_intent field on ConverseResponse carries the intent label (e.g., "add_knowledge"), which uses Operator vocabulary. It does not expose engine terms.
| Constant | Initial value | Location |
|----------|---------------|----------|
| CONFIDENCE_THRESHOLD | 0.7 | classifier.py or orchestration config |
| MAX_CONVERSATION_TURNS_IN_CONTEXT | 10 | prompt.py or orchestration config |
| MAX_ASSERTIONS_IN_CONTEXT | 20 | prompt.py (Tier 2 loading) |
| ENGAGEMENT_CONTEXT_TOKEN_BUDGET | 2000 | prompt.py (approximate, not strict) |
These are module-level constants, not environment variables. They are tuned by experience and changed via code.
| Condition | Behavior |
|-----------|----------|
| LLM call fails (classification) | Default to general_conversation with confidence 0.0. Log the failure. Proceed to response generation. |
| LLM call fails (response generation) | Return voiced fallback: "I'm having trouble thinking right now — can you try again in a moment?" Return 200 with empty side_effects. Log the failure. |
| add_knowledge with no project_id | Responder asks the Operator which project they're referring to. No assertion created. |
| request_download with no matching artifact | Responder tells the Operator no matching artifact was found and lists what's available. |
| ask_about_progress with no project_id | Responder asks the Operator which project they want to know about. Alternatively, the router could call the cross-engagement dashboard — [CC determines: whether to scope progress queries to a single project or allow cross-engagement. Single-project is simpler and consistent with Phase 42's "one project at a time" scope.] |
| Classifier returns invalid JSON | Treat as classification failure. Default to general_conversation. Log. |
| Engagement not found or person not a member | Existing verify_project_membership returns 404. No change. |
request_draft, approve_draft, request_revision, request_redirect are voiced stubs.ask_about_past_input. The responder summarizes all assertions; the LLM naturally focuses on the Operator's question.test_classifier.py)test_classify_create_project — Message: "I want to start a new project." → intent create_project, confidence ≥ 0.7.test_classify_create_project_alt — Message: "Let's build something new for tracking expenses." → intent create_project.test_classify_add_knowledge — Message: "The irrigation system needs to handle three soil types: sandy, clay, and loam." → intent add_knowledge, extracted_parameters["knowledge_text"] contains the substantive content.test_classify_add_knowledge_alt — Message: "Remember that the deadline is March 15th." → intent add_knowledge.test_classify_ask_about_progress — Message: "How's the project going?" → intent ask_about_progress.test_classify_ask_about_progress_alt — Message: "What's been happening since last week?" → intent ask_about_progress.test_classify_ask_about_past_input — Message: "What have I told you about the pricing model?" → intent ask_about_past_input.test_classify_ask_about_past_input_alt — Message: "Can you show me my notes?" → intent ask_about_past_input.test_classify_request_download — Message: "I need to download the report." → intent request_download.test_classify_request_download_alt — Message: "Can I get the latest artifact?" → intent request_download.test_classify_finalize_project — Message: "I think the project setup looks good, let's finalize it." → intent finalize_project.test_classify_general_conversation — Message: "What do you think about adding a glossary section?" → intent general_conversation.test_classify_general_conversation_default — Message: "hmm" → intent general_conversation (ambiguous, defaults).test_classify_stub_request_draft — Message: "Can you draft the specification for me?" → intent request_draft.test_classify_stub_approve_draft — Message: "The draft looks good, approve it." → intent approve_draft.test_classify_stub_request_revision — Message: "The report needs updating." → intent request_revision.test_classify_stub_request_redirect — Message: "Move those notes to the other project." → intent request_redirect.test_classify_confidence_threshold — Ambiguous message → confidence below 0.7 → defaults to general_conversation.test_classify_intent_hint_overrides — When intent_hint is provided, classifier is not called. Verify by checking that no LLM call is made (mock the LLM, assert not called).
Note on LLM-dependent tests. Classification tests call the LLM. They require a valid key (system config or test fixture). Tests should use the system key fallback pattern. If no key is available in the test environment, these tests should be skipped (not failed) with a clear skip reason. [CC determines: the skip mechanism — pytest marker, environment variable check, or fixture-level skip.]
test_router.py)test_route_add_knowledge_creates_assertion — Route add_knowledge intent with engagement context → assertion created in held state with ActorRef(kind="companion"). Verify assertion exists, has correct content, is held.test_route_add_knowledge_no_project — Route add_knowledge intent with no project_id → error result (no assertion created).test_route_ask_about_progress — Route ask_about_progress → operation_data contains dashboard-derived content.test_route_ask_about_past_input — Route ask_about_past_input with assertions in engagement → operation_data["assertions"] is non-empty with display numbers.test_route_ask_about_past_input_empty — Route ask_about_past_input with no assertions → operation_data["assertions"] is empty list.test_route_request_download — Route request_download with a matching artifact → operation_data["match"] is "unique".test_route_request_download_no_match — Route request_download with no matching artifact → operation_data["match"] is "none".test_route_general_conversation — Route general_conversation → returns None (no engine operation).test_route_stub_intent — Route request_draft → RouteResult with is_stub=True and stub_guidance set.test_route_create_project_delegates — Route create_project → delegates to existing Phase 40 flow. [CC determines: how to verify delegation — mock the Phase 40 function and assert it was called.]test_route_finalize_project_delegates — Route finalize_project → delegates to existing Phase 40 flow.test_prompt.py)test_persona_loads — Persona component loads from file. Contains no engine vocabulary terms. Contains {companion_name} and {operator_name} template variables.test_persona_engine_vocabulary_absent — Scan persona text for forbidden terms (engagement, assertion, shape, render, manifestation, specialist, materializer, normative_force, held, committed, retracted). None found.test_engagement_context_tier1 — Engagement with no assertions, no Manifestation → Tier 1 context (seed content only).test_engagement_context_tier2 — Engagement with committed assertions, no Manifestation → Tier 2 context (seed + recent assertions with display numbers).test_engagement_context_tier3 — Engagement with Manifestation → Tier 3 context (seed + Manifestation structure with group labels).test_prompt_assembly_contains_companion_name — Assembled prompt contains the person's companion_name.test_prompt_assembly_contains_intent_instruction — Assembled prompt contains the intent-specific instruction for the classified intent.test_context_cache_hit — Load context, load again without changes → cache hit (no re-query).test_context_cache_invalidation — Load context, commit a new assertion, load again → cache miss (re-query). New context includes the new assertion.test_conversation_turns.py)test_record_and_retrieve_turns — Record an operator turn and a companion turn. Retrieve recent turns. Verify ordering, content, role.test_turns_scoped_to_person_and_engagement — Two persons each record turns on different engagements. Each person's query returns only their own turns on the correct engagement.test_turns_limit — Record 15 turns. Retrieve with limit=10 → most recent 10 returned.test_turns_null_engagement — Record turns with no engagement (project-less conversation). Retrieve by person + null engagement. Verify returned correctly.test_converse_integration.py)test_converse_add_knowledge_roundtrip — Call POST /operator/converse with message "Remember that the soil pH should be between 6.0 and 7.0." → classified intent add_knowledge → held assertion created → Companion message acknowledges the knowledge substantively. Verify classified_intent in response.test_converse_ask_about_progress_roundtrip — Call converse with "How's the project coming along?" → Companion message describes project state.test_converse_ask_about_past_input_roundtrip — Create some assertions on an engagement. Call converse with "What have I told you so far?" → Companion message references the assertions.test_converse_general_conversation_roundtrip — Call converse with "What do you think about including a troubleshooting section?" → general_conversation → Companion message is domain-informed conversation with no engine side effects. Verify no assertion created, no shape triggered.test_converse_stub_intent_roundtrip — Call converse with "Can you draft the report for me?" → request_draft stub → Companion message acknowledges the request and explains it's not available yet. Verify response is 200 (not 501).test_converse_intent_hint_backward_compat — Call converse with intent_hint="create_project" → existing Phase 40 flow runs. Verify response matches pre-Phase-42 behavior.test_converse_no_project_general — Call converse with no project_id and a general message → general_conversation → Companion responds without project context.test_converse_turns_recorded — Call converse. Query conversation_turns → operator turn and companion turn recorded.test_vocabulary_wall.py)test_vocabulary_wall_passes_with_phase42_schemas — The existing Phase 40 vocabulary-wall test continues to pass with any new schemas added in Phase 42. [CC determines: whether this is a new test or an extension of the existing test. If the existing test scans the orchestration module automatically, no new test is needed — just verify it passes.]Expected test count: ~35–45 new tests (consistent with scoping note estimate). The exact count depends on CC's judgment about test granularity and LLM-dependent test handling.
| Step | What | Auto/checkpoint |
|------|------|-----------------|
| 0 | Pre-flight: verify baseline (Section 3.2). Archive CR. Create branch phase-42-intent-classification-and-reactive-companion. | Auto |
| 1 | Migration 0059: conversation_turns table (Section 5.1). Verify migration applies cleanly and all existing tests pass. | Auto |
| 2 | Conversation turns model and CRUD: SQLAlchemy model, record_turn() and get_recent_turns() functions. Tests §12.4. | Auto |
| 3 | Persona component and intent instruction templates: create versioned text assets (D4, D7). Verify no engine vocabulary. Tests §12.3 items 31–32. | Auto |
| 4 | Engagement context loader: tiered loading function with cache (D5, D6). Tests §12.3 items 33–39. | Auto |
| 5 | Intent classifier: classification module with prompt, LLM call, structured output parsing (D3). Tests §12.1. | Auto |
| 6 | Intent router: deterministic dispatch to engine operations, stub markers (D10). Tests §12.2. | Auto |
| 7 | Response generator: LLM call with system prompt assembly (D11). Prompt assembly function (D4 template rendering + D5 context + D7 intent instruction). Tests §12.3 items 36–37. | Auto |
| 8 | Converse endpoint rewrite: wire classifier → router → responder pipeline. Preserve intent_hint backward compatibility (D9). Integration tests §12.5. Vocabulary-wall regression §12.6. | Auto |
| A | Checkpoint A — Operator confirms. Full test suite green. Tag substrate repo: phase-42-intent-classification-and-reactive-companion. | Checkpoint |
Auto-mode posture: Steps 0–8 auto-mode-proceed. Checkpoint A halts for Operator confirmation and tag.
conversation_turns).classifier.py, router.py, prompt.py, responder.py in src/loomworks/orchestration/.conversation_turns table.phase-42-intent-classification-and-reactive-companion on substrate repo.ask_about_past_input returns all assertions and lets the LLM focus. For engagements with many assertions, targeted filtering would reduce token cost.ask_about_progress to cross-engagement queries is future work.
Read the Change Request document at the path I supply below. This is
CR-2026-056 v0.1, the Phase 42 Change Request. You are the executing
agent named in the CR.
CR path: ~/Downloads/phase-42-cr-intent-classification-and-reactive-companion-v0_1.md
Phase 42 gives the Companion a brain (intent classification) and a
voice (system prompt). The converse endpoint is rewritten to use a
classify → route → respond pipeline.
Key points:
- Four new modules in src/loomworks/orchestration/: classifier.py,
router.py, prompt.py, responder.py.
- Versioned text assets for persona, classifier prompt, and intent
instruction templates — stored outside Python code.
- One migration: conversation_turns table (0059).
- Seven live intents, four voiced stubs. Stubs go through the full
response pipeline (Companion voice, not 501s).
- Engagement context loader with three tiers: seed-only,
seed+recent assertions, Manifestation-derived. Informal cache.
- intent_hint backward compatibility: if provided, skips classifier.
- Two LLM calls per turn: one for classification, one for response.
- Classification tests are LLM-dependent — skip if no key available.
- ~35–45 new substrate tests.
Code baseline: at HEAD after Phase 41 (tag at 74ec7e7). 1,519 tests,
2 skipped, Alembic 0058.
Run pre-flight (Step 0) per Section 3.2. The Step 0 checklist has
21 verification items. Pre-flight divergence stops execution and
drives a CR amendment.
Per Section 13, eight steps with one checkpoint. Auto-mode posture:
Steps 0–8 auto-mode-proceed; Checkpoint A halts for Operator
to confirm and tag.
DUNIN7 — Done In Seven LLC — Miami, Florida Phase 42 CR — Intent classification and reactive Companion — v0.1 — 2026-05-05