From db4b49a4d46ef8cb2ef89fe67ac3ab57f6840f79 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 14:51:39 +0000 Subject: [PATCH 1/2] =?UTF-8?q?refactor(ai):=20skills-only=20AI=20surface?= =?UTF-8?q?=20=E2=80=94=20real=20Actions=20instead=20of=2010=20fictional?= =?UTF-8?q?=20tools,=20retire=20the=20two=20agents=20(objectstack#3820,=20?= =?UTF-8?q?ADR-0109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HotCRM was the corpus that motivated objectstack#3820: 6 skills declaring 16 tool references, of which 10 named tools that exist nowhere. The copilot's instructions therefore claimed abilities it did not have — the runtime silently drops an unresolved tool, so the model improvised or failed when asked to use one. Every instance passed `objectstack validate` and `lint` cleanly. The fix is mostly deletion, because the references were a category error: "analyze the pipeline", "generate email copy", "score this lead" are REASONING, not tools. What actually needed a tool was the handful of steps that change state — and those already exist as HotCRM Actions, reachable through the `action_` tools the runtime materialises from them (ADR-0109's default path: no tool records to author). - case_triage: `triage_case` → the priority rubric moves into instructions; `action_escalate_case` / `action_close_case` do the work. Also repoints the hand-off that named `response_drafting`, a skill that never existed (the audit's "hand-off to a nonexistent skill"), at the real `email_drafting`. - email_drafting: 4 fictional copy tools → the model writes the copy; `action_send_email` sends it, so recipient resolution, permissions and audit match the UI path. - lead_qualification: `analyze_lead` / `suggest_next_action` → BANT reasoning in instructions over `get_record`; `action_convert_lead` / `action_schedule_followup` act, and conversion requires confirmation. - revenue_forecasting: 3 fictional analytics tools → `aggregate_data` / `query_records` / `visualize_data` with the actual forecasting method spelled out in instructions. - live_data, customer_360: already correct, untouched but for prose. The two agents (`sales_copilot`, `service_copilot`) are DELETED. ADR-0063 §2 closed `*.agent.ts` to third parties and cloud#904 made the runtime refuse non-platform agent records, so they were unreachable metadata — carrying them is the ADR-0078 lie. The capability lives in the skills, which attach to the platform assistant by surface affinity. Verified with the merged reference-integrity rule (objectstack#3885 `validate-ai-tool-references`): 22 tool references, 0 findings — was 16 references with 10 dead. typecheck, `objectstack validate`, and the 67-test suite pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BHjroNkLkajskKbJaidko4 --- objectstack.config.ts | 2 -- src/agents/index.ts | 24 ------------- src/agents/sales-copilot.agent.ts | 41 ---------------------- src/agents/service-copilot.agent.ts | 29 ---------------- src/docs/crm_admin.md | 15 ++++++--- src/objects/knowledge_article.object.ts | 2 +- src/skills/case-triage.skill.ts | 41 +++++++++++++++++----- src/skills/email-drafting.skill.ts | 36 +++++++++++++++----- src/skills/lead-qualification.skill.ts | 45 ++++++++++++++++++------- src/skills/live-data.skill.ts | 6 ++-- src/skills/revenue-forecasting.skill.ts | 39 ++++++++++++++++----- 11 files changed, 136 insertions(+), 144 deletions(-) delete mode 100644 src/agents/index.ts delete mode 100644 src/agents/sales-copilot.agent.ts delete mode 100644 src/agents/service-copilot.agent.ts diff --git a/objectstack.config.ts b/objectstack.config.ts index effcfafd..2d537526 100644 --- a/objectstack.config.ts +++ b/objectstack.config.ts @@ -9,7 +9,6 @@ import * as dashboards from './src/dashboards/index.js'; import * as datasets from './src/datasets/index.js'; import * as reports from './src/reports/index.js'; import { allFlows } from './src/flows/index.js'; -import { allAgents } from './src/agents/index.js'; import { allSkills } from './src/skills/index.js'; import * as profiles from './src/profiles/index.js'; import * as apps from './src/apps/index.js'; @@ -79,7 +78,6 @@ export default defineStack({ datasets: Object.values(datasets), reports: Object.values(reports), flows: allFlows, - agents: allAgents, skills: allSkills, permissions: Object.values(profiles), apps: Object.values(apps), diff --git a/src/agents/index.ts b/src/agents/index.ts deleted file mode 100644 index 27eb9c31..00000000 --- a/src/agents/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import type { Agent } from '@objectstack/spec/ai'; - -/** - * Agent Definitions Barrel - * - * After the move to the Universal Assistant pattern, the CRM ships - * two persona-level Agents (Sales Copilot, Service Copilot) that - * compose capabilities via Skills. The previous task-specific agents - * (email_campaign, lead_enrichment, revenue_intelligence, - * sales_assistant, service_agent) have been refactored into Skills - * under `../skills/`. - */ -export { SalesCopilotAgent } from './sales-copilot.agent'; -export { ServiceCopilotAgent } from './service-copilot.agent'; - -import { SalesCopilotAgent } from './sales-copilot.agent'; -import { ServiceCopilotAgent } from './service-copilot.agent'; - -export const allAgents: Agent[] = [ - SalesCopilotAgent, - ServiceCopilotAgent, -]; diff --git a/src/agents/sales-copilot.agent.ts b/src/agents/sales-copilot.agent.ts deleted file mode 100644 index f5c43a72..00000000 --- a/src/agents/sales-copilot.agent.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { defineAgent } from '@objectstack/spec'; - -/** - * Sales Copilot — the unified persona for everything a sales rep does. - */ -export const SalesCopilotAgent = defineAgent({ - name: 'sales_copilot', - label: 'Sales Copilot', - role: 'assistant', - active: true, - - instructions: `You are the Sales Copilot — a single AI surface that -helps sales reps work an account end-to-end. Use the Active Skills -block in your system context to pick capabilities; do NOT enumerate -skills back to the user. Answer concisely, lead with the -recommendation, and cite record IDs whenever you reference data. - -CRITICAL: HotCRM's schema is alive — admins add, modify and remove -fields at any time. Before answering any question about a record, -call \`describe_object\` (via the Live Data skill) to see the -CURRENT fields. Never assume the schema you saw on a previous -turn is still accurate. If you spot a field you don't recognise, -use it — that's almost certainly what the user wants you to surface.`, - - model: { provider: 'openai', model: 'gpt-4', temperature: 0.6, maxTokens: 2000 }, - - skills: [ - 'live_data', - 'lead_qualification', - 'email_drafting', - 'revenue_forecasting', - 'customer_360', - ], - - knowledge: { - topics: ['sales_playbook', 'product_catalog', 'lead_qualification'], - indexes: ['sales_knowledge'], - }, -}); diff --git a/src/agents/service-copilot.agent.ts b/src/agents/service-copilot.agent.ts deleted file mode 100644 index 32249a72..00000000 --- a/src/agents/service-copilot.agent.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { defineAgent } from '@objectstack/spec'; - -export const ServiceCopilotAgent = defineAgent({ - name: 'service_copilot', - label: 'Service Copilot', - role: 'assistant', - active: true, - - instructions: `You are the Service Copilot — a single AI surface -that helps support reps triage and resolve cases. Be empathetic and -solution-focused. Use the Active Skills block in system context to -pick capabilities; never list skills back to the user. Always cite -case IDs.`, - - model: { provider: 'openai', model: 'gpt-4', temperature: 0.5, maxTokens: 1500 }, - - skills: [ - 'case_triage', - 'customer_360', - 'email_drafting', - ], - - knowledge: { - topics: ['support_kb', 'sla_policies', 'case_resolution'], - indexes: ['support_knowledge'], - }, -}); diff --git a/src/docs/crm_admin.md b/src/docs/crm_admin.md index 229d7d59..6d6cf5ba 100644 --- a/src/docs/crm_admin.md +++ b/src/docs/crm_admin.md @@ -86,8 +86,13 @@ revisits: > on each record's detail page — they are intentionally **not** duplicated here. > This guide documents only what those screens can't tell you. -## AI copilots - -Two assistants ship with HotCRM: the **Sales Copilot** and the **Service -Copilot**. They operate over live CRM data and the actions exposed to them; the -end user simply asks the assistant — no agent selection required. +## AI skills + +HotCRM ships **skills**, not assistants of its own. The platform provides the +assistant (ObjectStack ADR-0063: the runtime owns exactly two agents and the +surface you are in binds one); an app extends it by authoring skills that +attach by surface affinity. HotCRM's six — live data, lead qualification, case +triage, email drafting, revenue forecasting, customer 360 — operate over live +CRM data and reach anything that *changes* state through the same Actions the +UI buttons use, so permissions and audit are identical. The end user simply +asks — no agent selection required. diff --git a/src/objects/knowledge_article.object.ts b/src/objects/knowledge_article.object.ts index 9adb0640..a536cea0 100644 --- a/src/objects/knowledge_article.object.ts +++ b/src/objects/knowledge_article.object.ts @@ -7,7 +7,7 @@ import { F, P, cel } from '@objectstack/spec'; * Knowledge Article Object * * Reusable, search-indexed answers that back the Support Knowledge Base - * and ground the Service Copilot's "Suggest a resolution" skill. + * and ground the assistant's case-resolution skills. * * Lifecycle: draft → in_review → published → archived. * Audience: public (customer portal visible) | internal (agent-only). diff --git a/src/skills/case-triage.skill.ts b/src/skills/case-triage.skill.ts index 8e45c605..b49dc8c4 100644 --- a/src/skills/case-triage.skill.ts +++ b/src/skills/case-triage.skill.ts @@ -2,20 +2,43 @@ import { defineSkill } from '@objectstack/spec'; +/** + * Case Triage — prioritise a support case and take the real action. + * + * ADR-0109: this skill declares NO tool records. Triage itself is + * reasoning, not a tool — the agent reads the case with the platform's + * data tools and applies the rubric below. The two steps that actually + * *change* something are HotCRM's own declarative Actions, reached + * through the tools the runtime materialises from them + * (`action_escalate_case` / `action_close_case`), so they run under the + * same permissions and audit trail as the buttons in the UI. + */ export const CaseTriageSkill = defineSkill({ name: 'case_triage', label: 'Case Triage', - description: 'Triages a support case, assigns priority, and recommends the next action.', + description: 'Triages a support case, assigns priority, and takes the escalate/close action.', - instructions: `When the user asks to triage, prioritise, or -classify a case: -1. Call triage_case with the current record. -2. If priority resolves to "critical", immediately recommend - escalation and draft an internal notification message. -3. Otherwise, propose a customer-facing response by handing off to - the response_drafting skill (do NOT inline-draft here).`, + instructions: `When the user asks to triage, prioritise, or classify a case: - tools: ['triage_case'], +1. Read the case first — call \`describe_object\` for \`crm_case\` (the + schema is alive; admins add fields), then \`get_record\` for the case + at hand. Never triage from memory of a previous turn. +2. Assign a priority yourself from what you read. There is no triage + tool and you do not need one — weigh, in order: customer tier and + contract value, whether the customer is blocked with no workaround, + how long the case has been open against its SLA, and the sentiment + of the latest customer message. +3. State the priority and the ONE reason that drove it. Cite the case + ID and the field values you used. +4. If the priority is critical, recommend escalation and call + \`action_escalate_case\` once the user confirms — it takes a + \`reason\`, so pass the justification from step 3. +5. If the case is already resolved in substance, call + \`action_close_case\` with a \`resolution\` summary instead. +6. For the customer-facing reply, hand off to the \`email_drafting\` + skill rather than drafting it here.`, + + tools: ['describe_object', 'get_record', 'action_escalate_case', 'action_close_case'], triggerPhrases: [ 'triage this case', diff --git a/src/skills/email-drafting.skill.ts b/src/skills/email-drafting.skill.ts index 5bd59f64..a614cdf4 100644 --- a/src/skills/email-drafting.skill.ts +++ b/src/skills/email-drafting.skill.ts @@ -2,21 +2,39 @@ import { defineSkill } from '@objectstack/spec'; +/** + * Email Drafting — write the copy, then send it through the real Action. + * + * ADR-0109: writing a subject line and personalising a body is what the + * model does; it is not a tool call. The one step that leaves the + * system — sending — is HotCRM's own `send_email` Action, reached via + * the materialised `action_send_email` tool, so recipient resolution, + * permissions and audit are identical to the UI path. + */ export const EmailDraftingSkill = defineSkill({ name: 'email_drafting', label: 'Email Drafting', - description: 'Drafts personalised outbound emails and optimises subject lines for open rate.', + description: 'Drafts personalised outbound emails and sends them through the contact email Action.', instructions: `When the user asks to draft, write, or optimise an email: -1. Use generate_email_copy with the recipient and a brief intent. -2. Always run optimize_subject_line on the proposed subject before - returning the draft. -3. Personalise the body using personalize_content when the recipient - has known firmographics or recent activity. -4. Return the draft as { subject, body, alternatives } so the UI can - present A/B variants.`, - tools: ['generate_email_copy', 'optimize_subject_line', 'personalize_content', 'generate_email'], +1. Ground it in real data before writing a word: \`get_record\` the + contact (and the related account or opportunity when the request + references one) so names, titles and recent activity are accurate. + Use \`query_records\` when you need the recent history. +2. Write the copy yourself — subject and body. You do not have, and do + not need, a copy-generation tool. Aim for: a subject under 60 + characters naming the concrete value, a first line referencing + something specific to THIS recipient rather than a generic + pleasantry, one clear ask, and no more than 150 words. +3. Offer two subject-line variants, say which you recommend and why, + so the user can A/B them. +4. Show the draft and WAIT. Never send unprompted. +5. When the user approves, call \`action_send_email\` with the contact + and the final \`subject\` / \`body\`. Report the outcome, citing the + contact ID.`, + + tools: ['get_record', 'query_records', 'action_send_email'], triggerPhrases: [ 'draft an email', diff --git a/src/skills/lead-qualification.skill.ts b/src/skills/lead-qualification.skill.ts index b0ef65d8..0ce585a1 100644 --- a/src/skills/lead-qualification.skill.ts +++ b/src/skills/lead-qualification.skill.ts @@ -3,27 +3,48 @@ import { defineSkill } from '@objectstack/spec'; /** - * Lead Qualification Skill — qualifies inbound leads using BANT - * (Budget, Authority, Need, Timeline) and emits a 0–100 score. + * Lead Qualification — score an inbound lead with BANT, then act on it. + * + * ADR-0109: scoring is judgement, not a tool call. The lead is read + * with the platform's data tools; the two outcomes that change state + * (`convert_lead`, `schedule_followup`) are HotCRM's own Actions, + * reached through their materialised `action_` tools. * * Auto-activates when the user is viewing a lead record so the LLM - * naturally surfaces the "Qualify Lead" capability without the user - * having to pick an agent. + * surfaces the capability without the user having to ask for it. */ export const LeadQualificationSkill = defineSkill({ name: 'lead_qualification', label: 'Lead Qualification', description: 'Qualifies inbound leads using BANT criteria and assigns a 0–100 score.', - instructions: `When the user asks to qualify, score, or analyze a lead: -1. Fetch the current lead with analyze_lead. -2. Apply BANT (Budget, Authority, Need, Timeline) reasoning. -3. Return a numeric score (0–100), a one-line justification per BANT - dimension, and the recommended next action. -4. If the score >= 70, propose calling suggest_next_action to draft - the follow-up.`, + instructions: `When the user asks to qualify, score, or analyse a lead: + +1. Read the lead — \`describe_object\` for \`crm_lead\` first (fields + change), then \`get_record\`. Use \`query_records\` to pull the + related activity history when the lead has any. +2. Score it yourself, 0–100, with BANT. There is no scoring tool: + Budget (is spend confirmed or inferable from company size?), + Authority (is the contact a decision maker?), Need (is there a + stated problem you solve?), Timeline (is there a date?). Weight + them evenly unless the user says otherwise. +3. Report the score, one line of justification per BANT dimension + naming the field or activity you read, and the single recommended + next step. Cite the lead ID. +4. Score >= 70 and the user agrees → call \`action_convert_lead\`. +5. Otherwise, or when the user wants to nurture, call + \`action_schedule_followup\` with the date you recommend. Say why + that date. +6. Never convert without explicit confirmation — conversion is not + reversible from this surface.`, - tools: ['analyze_lead', 'suggest_next_action'], + tools: [ + 'describe_object', + 'get_record', + 'query_records', + 'action_convert_lead', + 'action_schedule_followup', + ], triggerPhrases: [ 'qualify this lead', diff --git a/src/skills/live-data.skill.ts b/src/skills/live-data.skill.ts index f9a289f7..796f736c 100644 --- a/src/skills/live-data.skill.ts +++ b/src/skills/live-data.skill.ts @@ -6,9 +6,9 @@ import { defineSkill } from '@objectstack/spec'; * Live Data — the foundation of HotCRM's "Wow #1" demo. * * Wires the platform's built-in metadata + data tools into the - * Sales Copilot so it always sees the current schema before - * querying. This is what makes "add a field, AI uses it 30s later" - * actually work — `describe_object` re-reads metadata on every call. + * assistant so it always sees the current schema before querying. + * This is what makes "add a field, AI uses it 30s later" actually + * work — `describe_object` re-reads metadata on every call. */ export const LiveDataSkill = defineSkill({ name: 'live_data', diff --git a/src/skills/revenue-forecasting.skill.ts b/src/skills/revenue-forecasting.skill.ts index dbd54fb1..5e07d105 100644 --- a/src/skills/revenue-forecasting.skill.ts +++ b/src/skills/revenue-forecasting.skill.ts @@ -2,22 +2,43 @@ import { defineSkill } from '@objectstack/spec'; +/** + * Revenue Forecasting — analysis over live pipeline data. + * + * ADR-0109: forecasting is the whole point of giving a model + * aggregation over the user's own records; it is not a bespoke tool. + * This skill declares no tool records at all — it composes the + * platform's `aggregate_data` / `query_records` / `visualize_data` + * with instructions that carry the actual method. + */ export const RevenueForecastingSkill = defineSkill({ name: 'revenue_forecasting', label: 'Revenue Forecasting', - description: 'Analyses pipeline health, surfaces at-risk deals, and forecasts revenue with confidence bands.', + description: 'Analyses pipeline health, surfaces at-risk deals, and forecasts revenue from live opportunity data.', instructions: `When the user asks about pipeline health, forecast, risk, or deal slippage: -1. Start with analyze_pipeline to summarise stage distribution and - weighted value. -2. Call identify_at_risk to surface deals with deteriorating signals. -3. Use forecast_revenue for the period the user names (default: this - quarter), reporting both expected value and the 80% confidence - interval. -4. Be quantitative — every claim must cite numbers or a deal name.`, - tools: ['analyze_pipeline', 'identify_at_risk', 'forecast_revenue'], +1. Establish the shape first: \`describe_object\` for + \`crm_opportunity\` so you use the CURRENT stage, amount, + probability and close-date fields rather than assumed ones. +2. Summarise the pipeline with \`aggregate_data\` — count and summed + amount grouped by stage, scoped to the period the user names + (default: the current quarter). Weighted value is amount × + probability; compute it yourself from what you read. +3. Surface at-risk deals with \`query_records\`: close date already + past, or no activity in 30+ days, or the stage unchanged for longer + than its typical duration. Name each deal and the specific signal. +4. Forecast by summing weighted value for open deals plus closed-won + in the period. State the assumption behind the number in one line, + and give a range rather than false precision — the spread between + commit-only (high-probability stages) and full weighted pipeline. +5. Offer \`visualize_data\` when a chart would carry the answer better + than prose, e.g. stage distribution or month-over-month trend. +6. Be quantitative: every claim cites a number or a deal name. If the + data cannot support a claim, say so instead of estimating.`, + + tools: ['describe_object', 'aggregate_data', 'query_records', 'visualize_data'], triggerPhrases: [ 'forecast revenue', From a9ba2fccc03a8322e7de2d7cae10625770f1e37c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 15:11:31 +0000 Subject: [PATCH 2/2] fix(ai): only reference Actions that actually materialise as tools (ADR-0011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the skills rewrite in this PR. The first pass routed state changes through `action_escalate_case` / `action_close_case` / `action_send_email` — but the runtime materialises `action_` ONLY for an Action that opts in (`ai.exposed` + `ai.description`) AND has a headless path. All three are `type: 'modal'`: they collect a reason / resolution / final copy from a person, so they are UI-only and no tool is ever generated. Referencing them would have re-created the bug this PR fixes, one layer down. - `convert_lead` and `schedule_followup` are flow-typed with targets, so they only needed the opt-in: both now carry `ai.exposed` + an LLM-facing description. `convert_lead` keeps its `confirmText`, which the runtime reads as approval-required — an agent invocation lands in the HITL queue instead of converting a lead unattended, which is what we want for an irreversible outcome. - `case_triage` and `email_drafting` no longer claim to act. They read, reason, and hand a ready-to-paste `reason` / `resolution` / draft to the user, pointing at the Escalate / Close / Send Email button. The instructions say plainly that the assistant cannot press it — a modal Action staying human-driven is the right shape for these, not a gap. Re-verified with the corrected rule (objectstack#3894, which now models AI exposure): 19 tool references, 0 findings. Both directions hold — the two opted-in flow Actions resolve, the three modal ones would not. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BHjroNkLkajskKbJaidko4 --- src/actions/lead.actions.ts | 18 +++++++++++++++++ src/skills/case-triage.skill.ts | 31 +++++++++++++++++------------- src/skills/email-drafting.skill.ts | 21 ++++++++++---------- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/actions/lead.actions.ts b/src/actions/lead.actions.ts index 2a9011ec..b3aa4414 100644 --- a/src/actions/lead.actions.ts +++ b/src/actions/lead.actions.ts @@ -26,6 +26,17 @@ export const ConvertLeadAction: Action = { confirmText: 'Are you sure you want to convert this lead?', successMessage: 'Lead converted successfully!', refreshAfter: true, + // ADR-0011 — opt this Action in to AI, which materialises the + // `action_convert_lead` tool the `lead_qualification` skill references. + // Flow-typed with a target, so it has a headless path. `confirmText` makes + // the runtime treat it as approval-requiring, so an agent invocation lands + // in the HITL queue rather than converting a lead unattended — which is the + // behaviour we want for an irreversible outcome. + ai: { + exposed: true, + description: + 'Converts a qualified lead into an account, contact and opportunity. Irreversible — requires human approval before it runs.', + }, }; /** @@ -56,6 +67,13 @@ export const ScheduleFollowUpAction: Action = { visible: P`record.is_converted == false && record.status != "unqualified" && record.status != "converted"`, successMessage: 'Follow-up scheduled.', refreshAfter: true, + // ADR-0011 — materialises `action_schedule_followup` for the + // `lead_qualification` skill. Additive and reversible, so no approval gate. + ai: { + exposed: true, + description: + 'Schedules the next follow-up task on a lead, assigned to its owner, so the next touch lands on the rep list.', + }, }; /** diff --git a/src/skills/case-triage.skill.ts b/src/skills/case-triage.skill.ts index b49dc8c4..e656ceaf 100644 --- a/src/skills/case-triage.skill.ts +++ b/src/skills/case-triage.skill.ts @@ -3,20 +3,24 @@ import { defineSkill } from '@objectstack/spec'; /** - * Case Triage — prioritise a support case and take the real action. + * Case Triage — prioritise a support case and route it to the right action. * * ADR-0109: this skill declares NO tool records. Triage itself is * reasoning, not a tool — the agent reads the case with the platform's - * data tools and applies the rubric below. The two steps that actually - * *change* something are HotCRM's own declarative Actions, reached - * through the tools the runtime materialises from them - * (`action_escalate_case` / `action_close_case`), so they run under the - * same permissions and audit trail as the buttons in the UI. + * data tools and applies the rubric below. + * + * It deliberately calls NOTHING that mutates. Escalate and Close are + * `type: 'modal'` Actions: they collect a reason / resolution from a + * person, so they have no headless path and the runtime never + * materialises tools for them (ADR-0011). That is the right shape here — + * the agent supplies the judgement, the human supplies the words and the + * decision — so the skill recommends the button instead of pretending to + * press it. */ export const CaseTriageSkill = defineSkill({ name: 'case_triage', label: 'Case Triage', - description: 'Triages a support case, assigns priority, and takes the escalate/close action.', + description: 'Triages a support case, assigns a priority with its justification, and points at the escalate/close action.', instructions: `When the user asks to triage, prioritise, or classify a case: @@ -30,15 +34,16 @@ export const CaseTriageSkill = defineSkill({ of the latest customer message. 3. State the priority and the ONE reason that drove it. Cite the case ID and the field values you used. -4. If the priority is critical, recommend escalation and call - \`action_escalate_case\` once the user confirms — it takes a - \`reason\`, so pass the justification from step 3. -5. If the case is already resolved in substance, call - \`action_close_case\` with a \`resolution\` summary instead. +4. If the priority is critical, say so and point the user at + **Escalate Case** on the record header — offer a ready-to-paste + \`reason\` built from step 3. You cannot escalate yourself, and should + not imply otherwise. +5. If the case is resolved in substance, point at **Close Case** and + offer a \`resolution\` summary the user can paste. 6. For the customer-facing reply, hand off to the \`email_drafting\` skill rather than drafting it here.`, - tools: ['describe_object', 'get_record', 'action_escalate_case', 'action_close_case'], + tools: ['describe_object', 'get_record'], triggerPhrases: [ 'triage this case', diff --git a/src/skills/email-drafting.skill.ts b/src/skills/email-drafting.skill.ts index a614cdf4..4eda6b18 100644 --- a/src/skills/email-drafting.skill.ts +++ b/src/skills/email-drafting.skill.ts @@ -6,15 +6,16 @@ import { defineSkill } from '@objectstack/spec'; * Email Drafting — write the copy, then send it through the real Action. * * ADR-0109: writing a subject line and personalising a body is what the - * model does; it is not a tool call. The one step that leaves the - * system — sending — is HotCRM's own `send_email` Action, reached via - * the materialised `action_send_email` tool, so recipient resolution, - * permissions and audit are identical to the UI path. + * model does; it is not a tool call. Sending is HotCRM's `send_email` + * Action — `type: 'modal'`, so it collects the final copy from a person + * and has no headless path (ADR-0011). The agent therefore drafts and + * hands over; a human presses Send. For outbound email that review step + * is a feature, not a limitation. */ export const EmailDraftingSkill = defineSkill({ name: 'email_drafting', label: 'Email Drafting', - description: 'Drafts personalised outbound emails and sends them through the contact email Action.', + description: 'Drafts personalised outbound emails grounded in live contact data, ready for a human to review and send.', instructions: `When the user asks to draft, write, or optimise an email: @@ -29,12 +30,12 @@ export const EmailDraftingSkill = defineSkill({ pleasantry, one clear ask, and no more than 150 words. 3. Offer two subject-line variants, say which you recommend and why, so the user can A/B them. -4. Show the draft and WAIT. Never send unprompted. -5. When the user approves, call \`action_send_email\` with the contact - and the final \`subject\` / \`body\`. Report the outcome, citing the - contact ID.`, +4. Show the draft and stop. You cannot send — say so plainly rather + than implying a send is queued. +5. Point the user at **Send Email** on the contact record, where the + subject and body you drafted can be pasted, reviewed and sent.`, - tools: ['get_record', 'query_records', 'action_send_email'], + tools: ['get_record', 'query_records'], triggerPhrases: [ 'draft an email',