Agent Script Skill What This Skill Is For This skill is for developing Agentforce agents, primarily with Agent Script, Salesforce's scripting language for AI agents. Org-backed workflows require an Agentforce license, API v66.0 or later, and an Einstein Agent User. Static authoring and review can proceed without org access. CRITICAL: Agent Script is NOT AppleScript, JavaScript, Python, or any other language. Do NOT confuse Agent Script syntax or semantics with any other language you have been trained on. Agent Script agents are defined by metadata: an file (agent behavior) plus a sibling file…

&& echo \"BLOCKED: see Issue 21\"\n```\n\n### Screen 2 — pick a non-NGA version\n\nFor the chosen agent, gather its versions and each version's planner type, then\npresent **only the non-NGA versions**, ordered by `VersionNumber` **ascending**.\n\n1. **Versions + Ids** (ascending) — the `0X9…` Id per version:\n\n ```bash\n sf data query \\\n --query \"SELECT Id, VersionNumber, Status FROM BotVersion WHERE BotDefinition.DeveloperName = '\u003cagent-name>' AND BotDefinition.Type IN ('ExternalCopilot','InternalCopilot') ORDER BY VersionNumber ASC\" \\\n --target-org \u003corg-alias> \\\n --json\n ```\n\n2. **Per-version planner link** — retrieve all versions' metadata at once and\n read `\u003cgenAiPlannerName>` from each `vN.botVersion-meta.xml`:\n\n ```bash\n sf project retrieve start --metadata \"BotVersion:\u003cagent-name>.*\" --target-org \u003corg-alias> --json\n # each: \u003cpackage-dir>/main/default/bots/\u003cagent-name>/vN.botVersion-meta.xml\n # → \u003cconversationDefinitionPlanners>\u003cgenAiPlannerName>…\u003c/genAiPlannerName>\n ```\n\n3. **Planner types** — resolve every planner's `PlannerType`:\n\n ```bash\n sf data query \\\n --query \"SELECT DeveloperName, PlannerType FROM GenAiPlannerDefinition\" \\\n --target-org \u003corg-alias> \\\n --json\n ```\n\nJoin version → `genAiPlannerName` → `PlannerType`, drop any version whose\n`PlannerType` is `Atlas__ConcurrentMultiAgentOrchestration` (NGA), and present\nthe rest ascending, e.g.:\n\n```text\nAgent: Search_Agent\n v1 Inactive (legacy: SentOS__SearchAgent)\n v6 Inactive (legacy: SentOS__SearchAgent)\n # v2–v5, v7 hidden — already NGA (Atlas)\n```\n\nThe `Id` (`0X9…`) of the version the user picks is the `botVersionId` you pass to\n`migrateAgentToNga` in Step 3.\n\n> **Checklist:** mark Step 2 done, Step 3 `[>]`, and re-render before continuing.\n\n## Step 3 — Migrate the legacy agent with the `migrateAgentToNga` Connect API\n\nSalesforce Core exposes a Connect REST endpoint that converts a classic\n`BotVersion` into an NGA Agent Script bundle:\n\n```text\nPOST /services/data/v\u003capiVersion>/connect/migrateAgentToNga # \u003capiVersion> from Step 1, v67.0+\n```\n\n**Warning: This is a mutating call.** It does not validate/preview — a successful call\n*creates* a new NGA bundle in the org (a real `AiAuthoringBundle`, key prefix\n`1bY`, with a bundle version, key prefix `1bZ`). Confirm the target org and the\n`botVersionId` before running it.\n\n### Preferred: call it through the `sf` CLI\n\nInvoke the Connect endpoint with `sf api request rest` — no manual token\nhandling. **`sf api request rest` has no `--json` flag** (passing it errors with\n`Nonexistent flag: --json`). It streams the raw response *body* to stdout — for\nthis endpoint that body is already JSON — and prints a beta-command warning to\nstderr. The body is ANSI-colorized even when piped, so strip the color codes\nbefore reading or feeding it to `jq`:\n\n```bash\nsf api request rest \"/services/data/v\u003capiVersion>/connect/migrateAgentToNga\" \\\n --method POST \\\n --body '{\"botVersionId\":\"\u003cbotVersionId>\"}' \\\n --target-org \u003corg-alias> \\\n 2>/dev/null | sed -E 's/\\x1b\\[[0-9;]*m//g'\n```\n\n- `botVersionId` — the classic `BotVersion` Id (`0X9…`) selected in Step 2.\n- The stripped stdout is the full JSON response. Pipe it to `jq` after the `sed`\n strip to pull individual fields.\n\n#### Warning: Do the call in a subagent — the response can be huge\n\nA newer version of this endpoint returns an **`agentScript`** field containing\nthe *entire* generated Agent Script inline. Reading the raw response into the\nmain context would bloat it badly. **Make the `migrateAgentToNga` call inside a\nsubagent** and have the subagent return only the small fields you need:\n`bundleVersionApiName`, `bundleId`, `bundleVersionId`, `agentVersionFromNumber`,\n`bundleVersionToNumber`, `conversionWarnings`, and `agentResponse`. Never return\n`agentScript` up to the caller; the full script comes down through the Step 4\nretrieve instead.\n\n#### Response fields\n\nThe response shape is expanding. Handle the new fields **if present**, and fall\nback to the older shape when they are absent:\n\n| Field | Use |\n|-------|-----|\n| `bundleVersionApiName` | **Preferred.** The metadata `fullName` of the new bundle version — pass **directly** to the Step 4 retrieve, skipping the `bundleVersionId → fullName` lookup. |\n| `bundleVersionId` (`1bZ…`) | Fallback join key when `bundleVersionApiName` is absent (see Step 4). |\n| `bundleId` (`1bY…`) | The bundle (not version) Id. |\n| `conversionWarnings` | List of warnings; review in Step 5. Often non-empty. |\n| `agentResponse` | Notes/fixes from an agent that reviewed the generated script — useful input for Step 5 optimization. |\n| `agentScript` | The full generated script inline — **do not** surface it; retrieve the bundle in Step 4 instead. |\n| `agentVersionFromNumber` (Integer) | The legacy agent version that was migrated **from**. Use in the success message below. |\n| `bundleVersionToNumber` (Integer) | The NGA bundle version that was created **to**. Use in the success message below. |\n\n#### Report the result — always present the builder link\n\n**On any successful migration, present the builder deep link so the user can\nopen the new agent immediately.** The link needs only `bundleId` and\n`bundleVersionId`, which are present in **both** the old and new response shapes\n— so the link is unconditional, never gated on the newer fields.\n\nConfirm in one line, then hand over the link:\n\n```text\nMigrated \u003cagent> to a new NGA agent. Open it in the builder: \u003cbuilderUrl>\n```\n\nWhen the newer fields (`agentVersionFromNumber`, `bundleVersionToNumber`) are\npresent, use the more specific wording; otherwise fall back to the generic line\nabove:\n\n```text\nLegacy \u003cagent> version \u003cagentVersionFromNumber> has been migrated to NGA version \u003cbundleVersionToNumber>.\nOpen in the builder: \u003cbuilderUrl>\n```\n\nThe builder URL is the org's Lightning host + the agent-authoring app, with the\n**bundle** as the `project` and the **bundle version** as the `projectVersion`\n(`project == bundle`):\n\n```text\nhttps://\u003clightning-host>/AgentAuthoring/agentAuthoringBuilder.app#/project?projectId=\u003cbundleId>&projectVersionId=\u003cbundleVersionId>\n```\n\n- `projectId` = `bundleId` (`1bY…`); `projectVersionId` = `bundleVersionId` (`1bZ…`) — both from the Step 3 response.\n- Derive `\u003clightning-host>` from the chosen org, don't hardcode it:\n\n ```bash\n sf org display --target-org \u003corg-alias> --json 2>/dev/null \\\n | sed -E 's/\\x1b\\[[0-9;]*m//g' | jq -r '.result.instanceUrl'\n ```\n\n Use that host as-is; if it's a `*.my.salesforce.com` domain it redirects to the\n matching `*.lightning.force.com` builder.\n\n> **Always call through `sf api request rest`.** It keeps authentication\n> internal to the CLI. **Do not** hand-roll a `curl` call that extracts the org\n> access token (e.g. `jq -r '.result.accessToken'`) — pulling a session token\n> into a shell variable is prohibited in agent-executed workflows because it can\n> surface in the transcript, context, or traces, even when it is never echoed.\n\n#### Response shapes\n\nA successful migration returns **HTTP 201**. The **current** shape (older\nendpoint) looks like:\n\n```json\n{\n \"bundleId\": \"1bYak000000LoRdEAK\",\n \"bundleVersionId\": \"1bZak000000dXCDEA2\",\n \"conversionWarnings\": [\n \"WARNING: Subagent TestSubAgent007 could not be resolved from standard subagents or has no actions. Is this expected?\"\n ]\n}\n```\n\nThe **newer** shape adds `bundleVersionApiName`, `agentScript`, `agentResponse`,\nand version-number fields:\n\n```json\n{\n \"bundleId\": \"1bYak000000LoRdEAK\",\n \"bundleVersionId\": \"1bZak000000dXCDEA2\",\n \"bundleVersionApiName\": \"Agentforce_Service_Agent_4\",\n \"conversionWarnings\": [\"WARNING: …\"],\n \"agentScript\": \"system:\\n instructions:| … (full script — do not surface)\",\n \"agentResponse\": \"Reviewed the generated script; adjusted …\",\n \"bundleVersionToNumber\": 4,\n \"agentVersionFromNumber\": 1\n}\n```\n\n`conversionWarnings` is often non-empty. Common entries flag subagents that\nresolved to no actions during conversion (e.g. `Escalation`, custom test\nsubagents) — review them in Step 5 when refining the migrated script; they do\n**not** fail the migration.\n\n> **Checklist:** mark Step 3 done, Step 4 `[>]`, and re-render before continuing.\n\n## Step 4 — Retrieve the new NGA bundle into the project\n\nPull the freshly-created `AiAuthoringBundle` into the DX project from Step 1 so\nit can be reviewed and refined locally. Retrieval is by metadata **fullName**.\n\n### Get the retrievable fullName\n\n**Preferred (newer endpoint):** Step 3's response already includes\n**`bundleVersionApiName`** — that *is* the metadata `fullName`. Use it directly\nand **skip the lookup below**.\n\n**Fallback (older endpoint, no `bundleVersionApiName`):** map the\n`bundleVersionId` to a `fullName`. `sf org list metadata` reports each\n`AiAuthoringBundle`'s `id`, and that `id` equals the **`bundleVersionId`**\n(`1bZ…`) from Step 3 — **not** the `bundleId` (`1bY…`). Match on it:\n\n```bash\nsf org list metadata --metadata-type AiAuthoringBundle --target-org \u003corg-alias> --json \\\n 2>/dev/null | sed -E 's/\\x1b\\[[0-9;]*m//g' \\\n | jq -r --arg id \"\u003cbundleVersionId>\" '.result[] | select(.id == $id) | .fullName'\n```\n\nEither path yields a versioned name like `Agentforce_Service_Agent_4`.\n\n### Retrieve into the project\n\nRun from **inside the Step 1 project directory** so the bundle lands under\n`\u003cpackage-dir>/main/default/aiAuthoringBundles/` (the `\u003cpackage-dir>` resolved in\nStep 1):\n\n```bash\ncd \u003cproject-path>\nsf project retrieve start --metadata \"AiAuthoringBundle:\u003cfullName>\" --target-org \u003corg-alias>\n```\n\n> **Note on the on-disk name.** You pass the *versioned* `fullName` (e.g.\n> `Agentforce_Service_Agent_4`), but the retrieve normalizes the local folder to\n> the bundle's **`developer_name`** (its base name, e.g.\n> `Agentforce_Service_Agent`). The result is:\n>\n> ```text\n> \u003cpackage-dir>/main/default/aiAuthoringBundles/\u003cdeveloper_name>/\n> \u003cdeveloper_name>.agent # the Agent Script (YAML)\n> \u003cdeveloper_name>.bundle-meta.xml # \u003cbundleType>AGENT\u003c/bundleType>\n> ```\n\nThe `.agent` file is the migrated Agent Script — `system:` instructions,\n`config:`, `variables:`, subagents, and actions — ready for review in Step 5.\n\n> **Checklist:** mark Step 4 done, Step 5 `[>]`, and re-render before continuing.\n\n## Step 5 — Analyze, optimize, and iterate on the migrated Agent Script\n\n`migrateAgentToNga` produces a **basic, mechanical transformation** of the\nlegacy agent metadata into Agent Script. It maps the old subagents/actions across\nbut does **not** leverage any of the primitives that make Agent Script powerful —\ndeterministic flow control, wired data flow, proper reference syntax, structured\nescalation, etc. The migrated `.agent` is a *starting point*, not a finished\nagent. This step hands off to the existing capabilities of the\n**`agentforce-generate`** skill to understand it, then improve it.\n\nWork against the retrieved bundle from Step 4\n(`\u003cpackage-dir>/main/default/aiAuthoringBundles/\u003cdeveloper_name>/\u003cdeveloper_name>.agent`).\nAvailable inputs to inform the work:\n\n- The retrieved `.agent` file (Step 4) — the migrated script itself.\n- Step 3 `conversionWarnings` — unresolved subagents / actions to address.\n- Step 3 `agentResponse` (newer endpoint only) — fixes/comments from an agent\n that already reviewed the generated script; a useful starting checklist.\n\n### 5a — Understand what was produced\n\nRoute into the skill's **\"Comprehend an Existing Agent\"** task domain (see\n[SKILL.md](../SKILL.md)). It reverse-engineers an Agent Spec, produces a Subagent\nMap diagram, and flags anti-patterns — establishing what the mechanical\nconversion actually built before you change it. Core references:\n[Core Language](agent-script-core-language.md),\n[Subagent Map Diagrams](agent-subagent-map-diagrams.md).\n\n### 5b — Optimize to leverage Agent Script primitives\n\nRoute into the skill's **\"Optimize an Agent\"** task domain (see\n[SKILL.md](../SKILL.md)) — this is the heart of turning a mechanical conversion\ninto a real Agent Script agent. It scans every subagent and applies:\n\n- **Pattern 1 — Wire action outputs to deterministic consumers**\n ([Data Flow](optimization-pattern-1-data-flow.md)).\n- **Pattern 2 — Extract requirement-backed deterministic logic**\n ([Deterministic Logic](optimization-pattern-2-deterministic-logic.md)) — the\n biggest lever on migrated agents, which encode all logic as model judgment.\n In particular, **any consequential or state-changing action** (order/status\n updates, writes, refunds, cancellations) must be **structurally gated** with\n `available when @variables.\u003ctrusted_state>` over verified state — do not leave\n the guard as a prose instruction like \"only update for a verified customer,\"\n which the model can ignore. For example, gate an `UpdateOrderStatus` alias with\n `available when @variables.verified` rather than trusting reasoning text.\n- **Pattern 3 — Fix variable/action reference syntax**\n ([Reference Syntax](optimization-pattern-3-reference-syntax.md)) —\n `{[email protected]}` / `{[email protected]}` instead of bare references.\n- **Pattern 4 — Repair promised human handoff**\n ([Escalation](optimization-pattern-4-escalation.md)) — resolves the\n unresolved-`Escalation`-subagent warnings commonly seen in `conversionWarnings`.\n- **Pattern 5 — Voice-readiness** ([Voice Modality](voice-modality-reference.md))\n if the agent has a `modality voice:` block.\n\nThe domain presents an `## Optimization Report`, **stops for explicit approval**,\nthen applies approved edits. For deeper judgment on what \"good\" looks like, see\n[Zen of AgentScript](zen-of-agentscript.md),\n[Posture & Determinism](posture-and-determinism.md), and the\n[Scoring Rubric](scoring-rubric.md).\n\n### 5c — Validate, preview, and iterate\n\nAfter edits, validate and confirm behavior — then loop 5b/5c until satisfied:\n\n```bash\nsf agent validate authoring-bundle --json --api-name \u003cdeveloper_name>\nsf agent preview start --json --use-live-actions --authoring-bundle \u003cdeveloper_name>\n```\n\nSee [Validation & Debugging](agent-validation-and-debugging.md) for the\npreview/trace loop and [Safety Review](safety-review-reference.md) for the\n7-category safety pass. When the agent is ready to release, continue into the\nskill's **\"Deploy, Publish, and Activate\"** task domain.\n\n### 5d — Regression-test the draft against the legacy agent\n\nFor higher assurance — especially before release, or when 5b changed routing or\nactions — run a **side-by-side regression eval** that previews the migrated NGA\ndraft and the legacy agent against a fixed utterance suite and compares them.\nThis confirms the draft routes to the same subagents, calls the same actions, and\nreturns equivalent (ideally improved) responses. Full mechanism, scripts, and the\nutterance-suite format are in\n[legacy-nga-regression-eval.md](legacy-nga-regression-eval.md).\n\n**When to run:** after the draft compiles and previews cleanly (5c), when you want\nobjective evidence that the migration preserved behavior — not on every tiny edit.\nIt requires the legacy agent to still have an **Active** `BotVersion` and `sf` CLI\n**≥ 2.149.9**.\n\nTwo bundled scripts do the mechanical work:\n\n- `scripts/upgrade-legacy-nga-run-trials.sh` — collects fresh-session preview\n trials for a subject (`legacy` | `draft`) × utterance (agent/org from\n `EVAL_AGENT` / `EVAL_ORG`).\n- `scripts/upgrade-legacy-nga-analyze.py` — reads the local plan traces and emits\n the comparison report (mechanical Rubric #1 + verbatim responses + an LLM-judge\n prompt for Rubric #2).\n\n> **Safety.** Live utterances that fire real side effects (escalation, customer\n> email, lead capture) must be tagged and default to **simulated** mode or\n> explicit per-utterance confirmation — never run repeatedly in live mode without\n> opt-in. Draft-only otherwise: **no publish/activate/deploy** without explicit\n> human approval.\n\n**Stop condition:** every utterance's oracle-triangle verdict is `PASS` or\n`PASS + LEGACY-DEFECT` (no open `DRAFT-REGRESSION`; any `SPEC-REVIEW` /\n`ADJUDICATE` resolved), drift is `none` against a deliberately-approved baseline,\nand the Rubric #2 judgment is `EQUIVALENT` / `IMPROVED` (no open `REGRESSION`). A\n`LEGACY-DEFECT` is a **pass** — don't chase legacy parity when legacy was wrong.\n\n> **Checklist:** mark Step 5 done — all five steps complete. Confirm the\n> migration is finished and point the user at the optimized bundle.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":26835,"content_sha256":"73c895730c8562127e8d8ad1199abeaaca7f24408354b5e29a0a0df6d1638494"},{"filename":"references/version-history.md","content":"# Version History\n\nSkill version changelog for agentforce-generate.\n\n---\n\n## Current Skill (agentforce-generate)\n\n| Version | Date | Changes |\n|---------|------|---------|\n| 0.6.1 | 2026-05-20 | **Structure + posture overhaul**: Simplified `SKILL.md` into a cleaner execution router with draft-first lifecycle and explicit release gates. Added `posture-and-determinism.md` (default agentic, deterministic controls with cause). Added `patterns-by-requirement.md` and shifted architecture guidance to scenario-based pattern selection. Renamed architecture language to **router-first** and removed implied mandatory return-to-router transitions. Consolidated minimal examples into `examples.md`. Added `reference-map.md` for primary/supplemental reference governance. Updated `README.md`, `deploy/discover/scaffold` guidance, and terminology from \"backing logic\" to \"action implementations\" across core references. |\n| 0.4.8 | 2026-03-17 | **Agent access guide + merge cleanup**: Adopted `agent-access-guide.md` from sf-permissions (import rules applied). Routed post-activation access step in Create and Deploy domains. Merged `preview-test-loop.md` content (jq trace diagnostics, fix strategies table, context variable limitations, utterance derivation) into `agent-validation-and-debugging.md`. Deleted orphaned `preview-test-loop.md`. Added custom object scanning guidance to Agent Spec creation. Refined post-action output field instructions. |\n| 0.4.7 | 2026-03-15 | **Post-action session state fix**: Added explicit post-action instructions to prevent session state corruption by specifying output fields the agent must reference. Driven by T02 run-2026-03-15-vc-02 finding. |\n| 0.4.6 | 2026-03-15 | **USER_MODE P0-1 fix**: Added static vs. dynamic SOQL guidelines to backing logic sections in `agent-design-and-spec-creation.md` and invocable Apex template. `AccessLevel.USER_MODE` required for dynamic SOQL. Added \"Rules That Always Apply\" cardinal rules block to SKILL.md (`--json` first, diagnose before fix, spec approval gate). |\n| 0.4.5 | 2026-03-15 | **Full editorial pass + spec approval gate**: Conciseness pass across all 9 SKILL.md domains. Added spec approval hard gate (user must approve Agent Spec before implementation). Added `filter_from_agent` output visibility to Agent Spec template. Restructured Agent Spec inputs/outputs sections. Step 8 redirect to Diagnose Behavioral Issues workflow. |\n| 0.4.4 | 2026-03-13 | **Staging + cleanup**: Moved unmerged reference files to `staging/` folder. Clarified live actions command syntax. Refined `agent-user-setup.md` license requirements and USER_MODE documentation. |\n| 0.4.3 | 2026-03-12 | **sf-skills merge: production-gotchas + new domain**: Added \"Diagnose Production Issues\" task domain to SKILL.md (9 domains total). Routed `production-gotchas.md` as primary reference. Added `production-gotchas.md` as secondary reference in Diagnose Compilation (reserved keywords trigger). Added `WITH USER_MODE` object permissions warning to `agent-user-setup.md` Section 6.2. Archived orphan `agent-user-setup-and-perms.md`. Fixed stale SKILL.md reference (Section 2 → Section 6.2). |\n| 0.4.2 | 2026-03-12 | **sf-skills merge: known-issues + one-at-a-time deploy**: Integrated `known-issues.md` into 6 of 8 SKILL.md domains with domain-specific loading triggers. Added one-at-a-time Apex stub deploy instruction to SKILL.md Create/Modify domains, `agent-design-and-spec-creation.md`, and `salesforce-cli-for-agents.md`. Moved Issue 16 (`connections:` → `connection messaging:`) to Resolved. |\n| 0.4.0 | 2026-03-11 | **T03 test run + type mapping restructure**: Restructured Section 5 type mapping in `agent-design-and-spec-creation.md` into Primitive + Complex tables keyed by `target` type. Added steps 10 (Activate) and 11 (Verify published agent) to Create, Modify, and Deploy domains. |\n| 0.3.2 | 2026-03-10 | **T02 post-fix run**: Platform-injected `show_command` tool diagnostic pattern added to `agent-validation-and-debugging.md`. Post-publish preview language strengthened in SKILL.md. |\n| 0.3.0 | 2026-03-10 | **T02 first run + test framework**: Created testing framework (README, run structure, scoring rubric). First T02 run: 13/13 SUCCESS. |\n| 0.2.0 | 2026-03-09 | **T01 first run**: Created T01 test scenario. First end-to-end test of skill. Identified `agent_type` inference gap. |\n| 0.1.0 | 2026-03-08 | **Initial skill**: SKILL.md router with 8 task domains. 7 reference files. Agent Spec template. Asset library. |\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4500,"content_sha256":"f437dcf75f71986eb5cdd50bbb2b88a35a9ce6c8e5e83f29428b3c7e0812759b"},{"filename":"references/voice-latency-heuristics.md","content":"# Voice Latency Heuristics\n\nVoice agents live and die on latency. Under ~2 seconds between \"caller stops\nspeaking\" and \"agent starts speaking\" feels natural; every additional second\nerodes the illusion of talking to a person. Text agents can hide a slow tool\nbehind a spinner — voice agents can't. This reference encodes the latency\nanti-patterns worth catching at **authoring** time (`/agentforce-generate`) and\ndiagnosing from **traces** (`/agentforce-observe`).\n\nEach pattern has three parts: **detection** (how to spot it in the `.agent`\nbundle or a trace), **impact** (typical cost), and **fix** (instructional patch,\naction/tool change, or flag-for-human).\n\n> **Fix classification.** Only *purely instructional* fixes (adding an ack\n> phrase, tightening turn length, adding a spoken-form rule) are safe to apply\n> automatically. Anything that changes an action's implementation, its\n> synchronicity, or an org/channel-level setting is **flag-only** — surface it\n> with the specific object/action named and let a human decide.\n\n---\n\n## 1. Synchronous writes on the live-call critical path\n\nThe single biggest latency killer in the field. Any write (DML, external\ncallout, trigger cascade) that runs *inside* the turn between caller-speaks and\nagent-speaks adds directly to perceived response time.\n\n**Detection**\n- An action's target performs a write and runs synchronously in the reasoning turn.\n- The action name/description implies mutation: `update`, `insert`, `log`, `commit`, `save`, `record`, `create`, `submit`.\n- A trigger fires on the target object (custom or managed-package).\n\n**Impact**\n- Simple insert with a normal trigger: ~300–800ms.\n- Managed-package trigger cascade on a heavy object: multiple seconds.\n- Chained callout inside the trigger: add ~1–3s per hop.\n\n**Fix**\n- **Preferred:** move the write to post-call via a Platform Event or async queue.\n- **Interim:** wrap the write in a Queueable and return immediately from the action.\n- **Instructional (auto):** add an ack phrase before the action (\"One moment while I get that on file.\").\n- **Flag for human:** if the write MUST happen mid-turn (authentication, payment authorization), don't rewrite — flag with the specific object and trigger names.\n\n## 2. Large result sets returned raw to the reasoning LLM\n\nRetrieval that returns the full body of every matching record is a hidden\nlatency *and* accuracy tax — the model has to filter needles from a hayfield\nevery turn, on a bloated context.\n\n**Detection**\n- Action description mentions `search`, `retrieve`, `knowledge`, `articles`, `documents`.\n- Action returns more than ~5 items or ~2KB of text per call by default.\n- No `limit` / `top_k` / `max_chars` parameter surfaced to the planner.\n- Return type is the raw object (article, record), not a summarized shape.\n\n**Impact**\n- Seconds per call, plus downstream reasoning slowdown on the bloated context.\n- Degraded answer accuracy.\n\n**Fix**\n- **Action change:** summarize first (title + 1–2 sentence excerpt), page the rest behind a follow-up action.\n- **Action change:** add a `limit` parameter with a low default (3–5); name the allowed range in its `description` (Agent Script has no `enum` input attribute — see `actions-reference.md` \"Voice-Safe Action Authoring\" rule \"Enumerate small value sets in the description\").\n- **Instructional:** if the action stays bulky, add an ack phrase (\"This can take a few seconds — hang with me.\").\n- **Flag for human:** if the customer intentionally wants wide-net RAG over their whole KB, surface the tradeoff instead of rewriting.\n\n## 3. Chained external callouts\n\nEvery external HTTP hop is a chance for tail latency. Two hops brings the p95 of\nboth to the caller.\n\n**Detection**\n- An action's implementation makes 2+ external HTTP calls per invocation.\n- A trigger on the target object hits a webhook or external system.\n\n**Impact**\n- Per hop: ~200–800ms typical, seconds at p95.\n- Two chained hops: p95 easily crosses several seconds.\n\n**Fix**\n- **Flag for human** with the specific hops named — the topology matters.\n- Where feasible, propose consolidating hops server-side or pre-fetching in parallel.\n\n## 4. Over-decomposed multi-subagent routing\n\nA monolithic agent is often faster *and* more accurate than a deep tree of\nsubagents fronted by a classifier. Each planner turn costs ~1–2s; each subagent\nhandoff can add another. This is the voice cost of the \"dead hub\" and\n\"over-routing\" anti-patterns already tracked in `known-issues.md`.\n\n**Detection**\n- The bundle has several subagents whose only job is to route to one other subagent.\n- A classifier/guardrail subagent intercepts every utterance before the primary agent.\n\n**Impact**\n- Extra planner round-trips per turn; unexpected orchestrator calls when subagents chain.\n\n**Fix**\n- **Flag for human:** collapsing multi-subagent → flatter routing is a design decision.\n- If genuine domain boundaries remain and the entry router only selects\n transitions, consider Einstein HyperClassifier. Do not use it when the router\n must answer, clarify, run other actions, inspect images, or use lifecycle\n hooks.\n- Where the structure must stay, ensure each hop has an ack phrase.\n- Consider whether a guardrail subagent can become instruction-level rules in `system:`.\n\n## 5. Long agent turns trip the silence/nudge timer\n\nOn long agent turns, the platform's \"are you still there?\" nudge (Speak-Up) can\nfire before the caller has had a chance to respond, producing an awkward overlap\nor dead air. This is the same root cause as the response-verbosity check in\n`/agentforce-observe`.\n\n**Detection**\n- Response templates or instructions that produce turns over ~30 words / ~10s of speech.\n- Welcome/greeting message longer than one or two sentences.\n\n**Impact**\n- Dead air, overlap, or a spurious \"still there?\" prompt on the first or a long turn.\n\n**Fix**\n- **Instructional (auto):** enforce ≤2-sentence turns everywhere (the \"Keep responses concise\" rule in `voice-modality-reference.md`); keep the greeting to one sentence.\n- **Flag for human:** if long turns are legally required (disclaimers), the silent/nudge timing is an **org-level** setting, not an agent-bundle change.\n\n## 6. TTS number / currency / ID garble\n\nTTS engines read `$172,576.81` or `+14155551212` as garble unless the model\npre-formats them into spoken form.\n\n**Detection**\n- Instructions or action outputs surface prices, phone numbers, IDs, dates, or account numbers.\n- No rule telling the model to render numbers in spoken form.\n\n**Impact**\n- Callers hear unintelligible strings; erodes trust and forces repeats.\n\n**Fix**\n- **Instructional (auto):** add the spoken-form number rule (the \"Render numbers, prices, and IDs in spoken form\" rule in `voice-modality-reference.md`).\n- **Flag for human:** if the deployment language has weak TTS number-normalization coverage, note the gap so the instruction rule carries the load.\n\n## 7. Premature end-of-turn under noise (STT endpointing)\n\nCallers in noisy environments (drive-throughs, call centers, home appliances)\nget cut off before they finish because the STT endpointer signals end-of-turn\ntoo aggressively.\n\n**Detection**\n- Deployment is telephony (PSTN/SIP) rather than a web client.\n- Customer domain suggests a noisy environment (auto, retail, home services).\n- Endpointing silence threshold left at the default.\n\n**Impact**\n- Truncated caller utterances, repeated turns, frustration.\n\n**Fix**\n- **Flag for human (org-level):** raise the endpointing silence threshold for noisy environments (typical safe range ~300–900ms; lean higher when noisy). This is a channel/STT setting, not an agent-bundle change.\n- **Instructional (auto):** add an ASR repair prompt (Rule for misheard input in `voice-modality-reference.md`) so a truncated utterance recovers gracefully.\n\n## 8. Telephony transport hops (SIP / multi-region)\n\nEvery trunk hop and region crossing on the SIP path adds latency. Largely\noutside the agent bundle's control, but worth flagging.\n\n**Detection**\n- Deployment metadata mentions multiple trunks, a non-local region, or a third-party telephony proxy.\n\n**Fix**\n- **Flag for human only.** Not an agent-bundle-level fix.\n\n---\n\n## Automatable checks\n\nGrep the `.agent` bundle (and, for `/agentforce-observe`, cross-reference the\ntrace) for these:\n\n| Pattern | Meaning | Severity | Fix class |\n|---|---|---|---|\n| Mutating action (name/description implies write) runs synchronously in the turn | Sync write on critical path | high | flag (+ ack phrase auto) |\n| Retrieval action returns > ~5 items / > ~2KB with no `limit` parameter | Bulky retrieval | high | flag |\n| Action implementation makes 2+ external HTTP calls | Chained callout | medium | flag |\n| Several subagents that only route to one other subagent | Over-routing / dead hub | medium | flag |\n| Response templates > ~30 words, or greeting > 1–2 sentences | Nudge/long-turn risk | medium | instructional (auto) |\n| Prices / phone / IDs referenced with no spoken-form rule | TTS garble | medium | instructional (auto) |\n| Slow action (SOQL, external HTTP, retrieval) with no ack phrase in instructions | Missing filler | high | instructional (auto) |\n\n## What NOT to change automatically\n\n- Synchronous writes required to happen mid-turn (payment auth, identity verification) — flag with the reason.\n- Retrieval where the customer explicitly wants wide-net RAG — flag with the tradeoff.\n- Endpointing / silence / nudge thresholds — these are **channel/org-level** settings, not agent-bundle.\n- SIP topology — a platform/telephony decision.\n\n## Related\n\n- `voice-modality-reference.md` — instruction rules (ack phrases, spoken-form numbers, repair prompts, turn length).\n- `actions-reference.md` — voice-safe action authoring (descriptions, parameter names, error shapes).\n- `known-issues.md` — dead-hub / over-routing anti-patterns.\n- `/agentforce-observe` — trace-driven latency diagnosis (Phase 1 voice checks).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9951,"content_sha256":"920373cc6ba0069c649974ecf3cfe647b55cb6b86935c0ce005c8d95bbb44f5b"},{"filename":"references/voice-modality-reference.md","content":"# Voice Modality Reference\n\n## Overview\n\nVoice agents use the `modality voice:` block to configure text-to-speech (TTS) and speech-to-text (STT) behavior. This block is optional — omit it for text-only agents.\n\nVoice agents also require:\n- The standard `agent_type` (e.g. `AgentforceServiceAgent`) — **do NOT** set `Atlas__VoiceAgent` in the bundle `config` block. `Atlas__VoiceAgent` is a runtime `planner_type` value applied by the platform, not an authored field in the `.agent` file.\n- A `VoiceCallId` linked variable bound to `@VoiceCall.Id` (the voice-channel session identifier — the voice analog of `@MessagingSession.Id`).\n- A `language:` block with the appropriate locale.\n- A voice-capable connection surface — `connection customer_web_client:` (ECv2). `connection messaging:` is **additive**, needed only for human escalation (see \"Connection Blocks\" below).\n\n### VoiceCallId variable\n\nAdd this to the `variables:` block whenever `modality voice:` is present:\n\n```agentscript\n VoiceCallId: linked string\n source: @VoiceCall.Id\n description: \"This variable may also be referred to as Voice Call Id\"\n```\n\n## Agent Script Syntax\n\n```agentscript\nmodality voice:\n voice_id: \"UgBBYS2sOqTuMpoF3BR0\"\n outbound_speed: 1\n outbound_stability: 0.65\n outbound_similarity: 0.75\n```\n\n## Default Voice — start here\n\nThere is **no reliable CLI/API way to enumerate available voice IDs** and their tuning values, so ADLC always authors the platform default voice and lets the user customize afterward in the UI. Do **not** ask the user to supply a `voice_id`.\n\n| Field | Default value |\n|-------|---------------|\n| `voice_id` | `UgBBYS2sOqTuMpoF3BR0` (\"Mark\") |\n| `outbound_speed` | `1` |\n| `outbound_stability` | `0.65` |\n| `outbound_similarity` | `0.75` |\n| locale | `en_US` |\n\nThese match the platform default (`Eleven_Flash_V2_5` model config `outboundVoice` parameter).\n\n**Tell the user how to customize:** after the agent is created, open it in **Agent Builder → Connections → Voice** and click **Continue** to pick a different voice and tune speed/stability/similarity. The picklist of voices (with names, gender, accent, and locale) is only exposed in that UI — not via the CLI.\n\nThe `modality voice:` block is a top-level optional block, placed after `language:` and before `start_agent`:\n\n```agentscript\nsystem:\nconfig:\nvariables:\nconnection:\nknowledge:\nlanguage:\nmodality voice:\nstart_agent:\nsubagent:\n```\n\n## Properties\n\n### Core Voice Properties\n\n| Property | Type | Range | Description |\n|----------|------|-------|-------------|\n| `voice_id` | string | — | The ID of the voice model to use for TTS |\n| `outbound_speed` | float | 0.5–2.0 | Speech rate (0.5 = slow, 1.0 = normal, 2.0 = fast) |\n| `outbound_stability` | float | 0.0–1.0 | Voice consistency (lower = more emotional range, higher = more stable) |\n| `outbound_similarity` | float | 0.0–1.0 | How closely the AI replicates the original voice's characteristics |\n| `outbound_style_exaggeration` | float | 0.0–1.0 | Emotional intensity (0.0 = neutral, 1.0 = expressive) |\n\n### Inbound (STT) Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `inbound_filler_words_detection` | boolean | Enable recognition of filler words (\"uh\", \"um\") |\n| `inbound_keywords` | list | Keywords to improve speech recognition accuracy |\n\n### Advanced Configuration\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `outbound_filler_sentences` | object | Filler sentences by context (e.g., \"waiting\") — spoken while processing |\n| `pronunciation_dict` | object | Custom pronunciations for domain-specific terms |\n| `additional_configs` | object | Advanced voice settings (speak-up, endpointing, beep-boop) |\n\n### Additional Configs Sub-Properties\n\n**speak_up_config** — prompts when user is silent:\n\n| Property | Type | Range | Description |\n|----------|------|-------|-------------|\n| `speak_up_first_wait_time_ms` | int | 10000–300000 | Wait before first speak-up prompt (10s–5min) |\n| `speak_up_follow_up_wait_time_ms` | int | 10000–300000 | Wait for follow-up speak-up prompts |\n| `speak_up_message` | string | — | Message to speak when user is silent |\n\n**endpointing_config** — speech boundary detection:\n\n| Property | Type | Range | Description |\n|----------|------|-------|-------------|\n| `max_wait_time_ms` | int | 500–60000 | Max wait for speech endpoint detection (0.5s–60s) |\n\n**beepboop_config** — beep-boop tone behavior:\n\n| Property | Type | Range | Description |\n|----------|------|-------|-------------|\n| `max_wait_time_ms` | int | 500–60000 | Max wait for beep-boop behavior (0.5s–60s) |\n\n## Pronunciation Dictionary\n\nFor domain-specific terms that TTS may mispronounce:\n\n```agentscript\nmodality voice:\n voice_id: \"UgBBYS2sOqTuMpoF3BR0\"\n outbound_speed: 1\n outbound_stability: 0.7\n outbound_similarity: 0.8\n pronunciation_dict:\n pronunciations:\n - grapheme: \"Xfinity\"\n phoneme: \"ɛks.ˈfɪn.ɪ.ti\"\n type: \"IPA\"\n - grapheme: \"SkyMiles\"\n phoneme: \"S K AY M AY L Z\"\n type: \"CMU\"\n```\n\nSupported pronunciation types: `IPA` (International Phonetic Alphabet), `CMU` (Carnegie Mellon University Pronouncing Dictionary).\n\n## Voice-Specific Authoring Guidance\n\n### Instructions for Voice Agents\n\nVoice interactions differ from text. When authoring instructions for voice agents:\n\n1. **Keep responses concise.** Users cannot scan/skim voice responses. Aim for 1-2 sentences per turn, not paragraphs. (Long turns also risk tripping the silence/nudge timer — see [voice-latency-heuristics.md](voice-latency-heuristics.md) §5.)\n2. **Avoid lists longer than 3 items — and batch long ones.** Users lose track of spoken lists. For a list that can be long, read at most **2 items per turn** and offer to continue (*\"Would you like the next two?\"*) rather than reading the whole set. **Never speak a raw total count** (\"I found forty-seven results\") — it's meaningless aloud; summarize or offer to narrow instead.\n3. **Use confirmation patterns — and split capture from action.** Repeat back key information (account numbers, dates, amounts) before taking action. For anything that triggers an irreversible action (sending an email/SMS, creating a record), make **capture and send two separate steps**: capture and store the value, read it back, get a yes, and only *then* call the action that acts on it. Don't let a single turn both collect a misheard value and act on it.\n4. **Design for barge-in.** Users may interrupt. Instructions should handle partial inputs gracefully. Add: *\"If the caller starts talking, stop speaking immediately, listen, and respond to what they said — don't finish your sentence.\"*\n5. **Avoid formatting references.** Do not reference links, bullet points, tables, or visual formatting in instructions — they don't render in voice.\n6. **Acknowledge slow actions — prefer the platform's progress indicator over LLM filler.** Any action over ~800ms (SOQL, external HTTP, retrieval) needs a \"still working\" signal. **Best practice:** set a `progress_indicator_message` on the action itself (*\"Just a moment while I look that up\"*) so the platform speaks the hold phrase and the agent's own turn begins **with the answer** — don't have the LLM vocalize \"one moment\" as well, or the caller hears it twice. Reserve LLM-spoken filler (*\"Let me pull that up\"*) for cases where you can't set a progress message. Mark truly instant actions out of the progress indicator so they don't announce a delay that isn't there. This is the instruction-level fix for the latency patterns in [voice-latency-heuristics.md](voice-latency-heuristics.md).\n7. **Render numbers, prices, and IDs in spoken form.** TTS reads `$19.99` and `+14155551212` as garble. Instruct: *\"When reading numbers, prices, phone numbers, IDs, or dates, use natural spoken form — never read punctuation, currency symbols, or raw digits.\"* Spell out numbers under 100 (\"twenty-five\"); prices as *\"nineteen dollars and ninety-nine cents\"*; phone numbers digit-by-digit grouped naturally; dates as *\"May tenth, twenty twenty-six\"*. Author this guidance **once** — do not stack extra rules or tag overrides trying to force phone-number grouping, country-code suppression, or a specific date format. Those are platform-controlled; see \"Entity Confirmation & Normalization\" below.\n8. **Add ASR repair prompts for misheard input.** Speech recognition isn't perfect. Instruct: *\"If the caller's response doesn't match an expected value, or you're unsure what you heard, repeat it back and ask them to confirm — e.g. 'I heard four four two, is that right?'\"*\n9. **Give empty results a caller-friendly fallback.** Any lookup that can return zero results needs a graceful recovery. Instruct: *\"If a lookup returns nothing, don't say 'no records found.' Say something like 'I couldn't find that account — could you spell your last name?' or offer a different search.\"* (Pair with voice-friendly action error shapes — see [actions-reference.md](actions-reference.md) \"Voice-Safe Action Authoring\".)\n10. **Never claim an action happened unless it did.** The most common voice failure mode is the agent saying *\"I've sent that to you\"* or *\"you'll get a text shortly\"* when no action actually fired. Instruct explicitly: *\"Never state or imply that an email, SMS, or record action has happened, is happening, or will happen unless you actually executed the corresponding action this turn.\"* Confirm delivery only *after* the action returns success.\n11. **Never expose internals to the caller.** Action names, variable names, JSON, tool inputs, and retriever/knowledge-source names must never be spoken aloud. Instruct: *\"When invoking an action, say only the configured progress message or the final customer-facing answer — never read out action names, field names, or raw data structures.\"*\n\n### Entity Confirmation & Normalization — what you CANNOT control from instructions\n\nHow spoken entities are **read back and confirmed** (phone numbers, currency, dates, numerals, IDs) is decided by the platform's voice normalization layer, **downstream of your agent instructions**. A few specific behaviors are **not** reliably controllable from the `.agent` file — and trying to control them makes things *worse*, not better:\n\n- **Country code on phone numbers** (e.g. a \"plus one\" prepended to a US number). Whether it appears is decided by the normalization layer, not your prompt.\n- **Digit grouping when reading numbers back** (e.g. `980 23 222 45` vs. an even digit-by-digit read). Grouping is applied at the language level and can vary run-to-run.\n- **Dropping the current year from a date** (e.g. reading a same-year date as month and day only).\n\n**Do NOT author custom instructions or tag overrides to fight these.** Adding rules like \"always emit the phone tag with an empty country code,\" \"read phone numbers as area-code / prefix / last-four,\" or \"always include the year\" **conflicts with the platform's built-in tagging and makes readback *less* consistent, not more.** In practice these overrides produce unreliable, ~50/50 results and can destabilize otherwise-correct normalization.\n\n**What to do instead:**\n- Write the spoken-form guidance in item 7 **once**, and stop there. Tag entities correctly (say the value is a phone number, a price, a date) and let the platform normalize it.\n- If a customer needs a specific readback format or wants to **suppress an out-of-the-box confirmation**, that is **platform/product configuration, not agent-script authoring**. Per-entity confirmation configurability is active platform work — surface the requirement to the voice product team rather than encoding a workaround in the `.agent` file.\n- Set the customer's expectations: these three behaviors may still occur regardless of instructions, and are being addressed at the platform layer.\n\n### Instruction Example — Voice vs Text\n\n**Text agent instruction:**\n```agentscript\n| Here are your options:\n 1. Check order status\n 2. Return an item\n 3. Speak with a representative\n Please enter the number of your choice.\n```\n\n**Voice agent instruction:**\n```agentscript\n| Ask the customer what they'd like help with. You can check order status, process a return, or connect them with a representative. If unclear, ask one clarifying question.\n```\n\n### Connection Blocks — how `modality` and `connection` relate\n\n`connection` blocks are separate from `modality voice:`. **`modality voice:` configures voice *behavior*** (TTS voice, speed, STT tuning); **`connection` blocks declare the *surface/channel*** the agent is wired to. A voice agent needs both: the modality block for how it speaks, and a voice-capable connection surface for where it runs.\n\nThere is **no `connection voice:` surface type** — do not invent one. In Agent Script, the voice-capable connection surface is **`connection customer_web_client:`**, which corresponds to **Enhanced Chat v2 (ECv2)** in Agent Builder (see Agent Builder → Connections). This is the surface that makes Agent Builder **Preview** and voice work:\n\n```agentscript\nconnection customer_web_client:\n adaptive_response_allowed: True\n```\n\n**Is `connection messaging:` also required?** No — it is **additive, not required for voice**. Add `connection messaging:` only if the agent escalates to a human (`@utils.escalate`); escalation is routed through it. If the agent has no human-escalation path, `customer_web_client` alone is sufficient. Most service voice agents *do* escalate, so both blocks commonly appear together (this is what the UI shows when both ECv2 and Messaging connections are enabled):\n\n```agentscript\nconnection messaging:\n escalation_message: \"Let me transfer you to a specialist who can help.\"\n\nconnection customer_web_client:\n adaptive_response_allowed: True\n```\n\n> **Choosing a surface — ECv2 (`customer_web_client`) vs Telephony.** Both ECv2 and Telephony (Service Cloud Voice) are voice-capable channels. In Agent Builder, adding *either* connection auto-enables Voice Settings. ADLC authors **`customer_web_client` (ECv2)** because it is the surface that is reliably created via the CLI/DSL today and is what Agent Builder Preview requires. If your deployment target is Service Cloud Voice telephony, author `customer_web_client` for authoring/preview, then complete the telephony channel wiring **headless via the CLI** — see [voice-telephony-cli.md](voice-telephony-cli.md) (`MessagingChannel` + routing flow + `Atlas__VoiceAgent` planner). It is *not* a UI-only step.\n>\n> **Do not** invent `connection voice:`, and do not remove an existing `connection messaging:` block when enabling voice — enabling voice **adds** the `modality voice:` block, the `VoiceCallId` variable, and `connection customer_web_client:`.\n\n> **Note on the `telephony` connection type.** `actions-reference.md` lists `telephony` as an escalation-routing channel. That is a *routing* surface for the `connection` escalation block; for voice *authoring + preview* the DSL surface ADLC emits is `customer_web_client` (ECv2). See known-issues.md Issue 18 for why `CustomerWebClient` must sometimes be patched into the compiled `GenAiPlannerBundle` after publish.\n\n## When to Add a Modality Block\n\n| Scenario | Modality Block? |\n|----------|----------------|\n| Text-only agent (messaging, web chat) | No |\n| Voice-only agent (telephony) | Yes — required |\n| Multi-channel agent (text + voice) | Yes — voice channel uses it |\n| Employee agent (internal, no customer channel) | No (employee agents are text-only) |\n\n## Validation\n\nThe `modality voice:` block is validated during `sf agent validate`. Common issues:\n\n- Invalid `voice_id` — must be a valid voice model ID from the org's voice provider\n- Out-of-range floats — `outbound_speed` must be 0.5–2.0, others must be 0.0–1.0\n- Timing values out of bounds — speak-up timers: 10s–5min, endpointing/beepboop: 0.5s–60s\n\n## Telephony Channel Setup Is Headless (CLI) — No UI Step Required\n\nYou can **author**, **validate**, **publish**, AND **wire to a telephony channel** entirely\nheadless (CLI/API). `sf agent validate/publish authoring-bundle` compile and deploy the agent\nmetadata (including the `modality voice:` block); the last-mile telephony/voice channel\nattachment — creating the `MessagingChannel`, wiring the routing flow, and going Live on a\nphone number — is done with `sf data create record`, `sf project deploy`, and `sf api request\nrest`. See **[voice-telephony-cli.md](voice-telephony-cli.md)** for the full procedure.\n\n> **Correction to prior guidance.** Earlier versions of this doc treated voice-channel deploy as\n> UI-only (open Agent Builder → Connections → Voice → Continue). That is **no longer accurate** —\n> the same channel wiring is fully scriptable via the CLI (proven by the Contact Center\n> `afv-pstn-setup-cli` skill: \"fully headless, no browser\"). Do **not** tell the user the channel\n> step requires the UI.\n\nThe **one** thing that still needs the UI is picking a **non-default voice ID / tuning** — the\nvoice picklist (names, gender, accent, locale) is not enumerable via CLI, so ADLC authors the\nplatform default voice and the user customizes later in Agent Builder → Connections → Voice\n(see \"Default Voice — start here\" above). This is a voice-*selection* limitation, not a\nchannel-*attachment* one.\n\nHeadless telephony wiring, in brief (full detail + gotchas in\n[voice-telephony-cli.md](voice-telephony-cli.md)):\n1. Planner: `plannerType=Atlas__VoiceAgent` + a `SurfaceAction__Telephony` plannerSurface, with ≥1 topic.\n2. Deploy escalation flow → voice queue (`RoutingModel=ExternalRouting`) → inbound routing flow (`routingType=Copilot`).\n3. `MessagingChannel` (`MessageType=PstnVoice`, `MessagingPlatformKey=\u003cphone>`), then PATCH `SessionHandlerId` (FlowDefinition `300…`) + `FallbackQueueId`.\n4. Poll the phone number to `CodeStatus=Live`; smoke-test with a call.\n\n## Steel Thread Alignment (Project Codey)\n\nVoice work in ADLC targets **Steel Thread 2 — \"Voice-Enabled Agent with Knowledge Grounding\"**: build voice agents with subagents, actions, and knowledge integration (ADL / Salesforce Knowledge), then deploy to the voice channel. Two implications for authoring:\n\n- **Pair voice with knowledge grounding.** Voice service agents are almost always FAQ/policy-backed, so `/agentforce-generate` proactively asks the Knowledge Grounding question when it detects a voice agent. The combined template is `assets/agents/voice-knowledge-grounded.agent`.\n- **Deploy is now fully headless.** Authoring, validation, publish, AND telephony-channel wiring are all CLI — see [voice-telephony-cli.md](voice-telephony-cli.md). (Formerly a Steel Thread 2 gap; the channel step no longer requires the UI. Only non-default voice-ID selection remains UI-only.)\n\n## Related References\n\n- [voice-telephony-cli.md](voice-telephony-cli.md) — headless PSTN/telephony channel setup via SF CLI (MessagingChannel, routing flows, `Atlas__VoiceAgent` planner, troubleshooting).\n- [voice-latency-heuristics.md](voice-latency-heuristics.md) — latency anti-patterns (sync writes, bulky retrieval, long turns) for authoring and trace diagnosis.\n- [actions-reference.md](actions-reference.md) \"Voice-Safe Action Authoring\" — voice-safe action descriptions, parameter names, enums, error shapes.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19456,"content_sha256":"454bc6e5d91ff6e81b82a969199a8c0031355e27852de2a49e3a0081c35b6cab"},{"filename":"references/voice-telephony-cli.md","content":"# Headless Telephony (PSTN) Channel Attach via SF CLI\n\n**Scope: the last step only.** This covers attaching an **already-authored, published, and\nactive** voice agent to a real phone number (PSTN) — the step this skill previously deferred to\nthe Agent Builder UI. Authoring the agent (the `.agent` file, `modality voice:` block,\nsubagents/topics/actions, publish, activate) is handled by the normal `agentforce-generate`\nflow — **do not re-do it here.** This doc starts *after* `sf agent publish` + `sf agent activate`.\n\nThe whole channel attach is headless — `sf` CLI, `sf project deploy`, and REST. **No Agent\nBuilder step.** This supersedes the older \"channel wiring is UI-only\" guidance.\n\n> **Ask before wiring — this step is opt-in.** Channel attach creates real routing\n> infrastructure (flows, a queue, an active `MessagingChannel`) and binds a provisioned phone\n> number. **Never run it automatically** as part of publish/activate. Confirm both first:\n> (1) the user has **explicitly asked** to wire telephony, and (2) a **provisioned phone number**\n> exists (do not assume/invent one). If either is missing, stop and tell the user what's needed.\n\n> The only thing that still needs the UI is picking a **non-default voice ID / tuning** (the\n> voice picklist isn't enumerable via CLI — see [voice-modality-reference.md](voice-modality-reference.md)).\n> Channel *attachment* is not UI-only.\n\nCommands below are adapted from the Agentforce Contact Center `afv-pstn-setup-cli` skill.\n\n---\n\n## Prerequisites (produced by the normal authoring flow)\n\n- A voice agent **already published + active** via `agentforce-generate` (authored with\n `modality voice:` and a voice-capable connection surface).\n- Org authenticated via `sf` CLI; you are inside the SFDX project dir (`sf agent\n activate/deactivate` fail outside one with `InvalidProjectWorkspaceError`).\n- A **provisioned phone number** supplied by the user (do not assume one).\n\n---\n\n## What \"attach to telephony\" adds on top of the published agent\n\n```text\nInbound call on +1XXXXXXXXXX\n ↓ MessagingChannel (PstnVoice) ← created here\n SessionHandlerId → FlowDefinition (300…)\n ↓ Voice Routing Flow (routingType=Copilot) ← created here\n copilotId → BotDefinition (the published agent)\n ↓\n Published voice agent (planner needs a Telephony surface — see Step 1)\n ↓ (on escalation)\n Voice Escalation Flow (routingType=QueueBased) → voice queue ← created here\n```\n\nDeploy order: **preflight number is Live (Step 0) → escalation flow → queue → (verify planner) → inbound routing flow → channel.**\n\n---\n\n## Step 0 — Preflight: the supplied number must already be provisioned + Live (do this FIRST)\n\n**Before any deploy or create step**, confirm the exact phone number the user supplied is\nalready provisioned and `CodeStatus=Live`. Nothing below (planner patch, escalation flow, queue,\ninbound flow, `MessagingChannel`) may run until this passes — otherwise a typo'd or unprovisioned\nnumber leaves real routing infrastructure created for a number that can never route, which defeats\nthe opt-in gate.\n\n```bash\nsf data query --target-org \u003calias> \\\n --query \"SELECT Id, CodeStatus FROM PhoneNumber WHERE PhoneNumber='\u003cphone_number>'\"\n```\nProceed **only** when the query returns exactly the supplied number with `CodeStatus=Live`. **Stop\nand make zero changes** if any of these hold:\n- no row is returned (number not provisioned on this org),\n- `CodeStatus` is anything other than `Live` (still provisioning / failed),\n- the query errors with `INVALID_TYPE` (Agentforce Voice numbers aren't provisioned on this org —\n verify in Setup → Service → Voice → Agentforce Voice Setup).\n\nTell the user exactly what's missing and do not attempt to create or invent a number.\n\n## Step 1 — Verify the published planner carries a Telephony surface\n\nAuthoring produces the planner; you only need to **confirm** (and patch only if missing) that it\nhas a `Telephony` surface — the DSL commonly emits `Messaging` + `CustomerWebClient` (ECv2 for\nPreview) but PSTN routing needs `SurfaceAction__Telephony`.\n\n```bash\nsf project retrieve start --metadata \"GenAiPlannerBundle:\u003cAgent_API_Name>\" --target-org \u003calias>\n```\nConfirm: `plannerType=Atlas__VoiceAgent`, a `SurfaceAction__Telephony` `plannerSurface`\n(with `outboundRouteConfigs` naming the escalation flow), and ≥1 `localTopics` (a topic-less\nplanner accepts then drops calls in ~10s). If the Telephony surface is absent, add it and\nredeploy with the agent deactivated:\n```bash\nsf agent deactivate --api-name \u003cAgent_API_Name> --target-org \u003calias>\nsf project deploy start --source-dir \u003cproject-source-dir>/genAiPlannerBundles/\u003cAgent_API_Name> --target-org \u003calias> --wait 10\nsf agent activate --api-name \u003cAgent_API_Name> --target-org \u003calias>\n```\n> `\u003cproject-source-dir>` is the source directory derived from `sfdx-project.json`: take the\n> `packageDirectories[]` entry with `\"default\": true` (else the first) and append `/main/default`\n> to its `path` → e.g. `force-app/main/default`. The `path` value alone (e.g. `force-app`) is the\n> package root, not the source dir — don't append the metadata folder to it directly.\n> Topics/actions themselves come from the authored `.agent` — don't hand-write them here; if\n> they're missing, fix the source agent and re-publish via `agentforce-generate`.\n\n## Step 2 — Voice queue (ExternalRouting)\n\nDeploy `QueueRoutingConfig` with **`RoutingModel=ExternalRouting`** (`LeastActive` breaks PSTN —\ncalls arrive, agent never answers), a `Group` (Queue) linked to it, a `QueueSObject` for\n`VoiceCall`, and add the running user as a member. Save the queue Id.\n\n## Step 3 — Escalation flow (before Step 4/5)\n\nDeploy a `RoutingFlow`: `routingType=QueueBased`, `serviceChannelDevName=sfdc_phone`, variable\n`recordId` (capital I), `apiVersion=66.0`. The planner references `\u003cAgent_API_Name>_Voice_Escalation`\nby name — a missing flow silently breaks escalation.\n\n## Step 4 — Inbound voice routing flow\n\nDeploy a `RoutingFlow` that routes inbound calls to the agent. Critical fields (from a working org):\n- `routingType` = **`Copilot`** (NOT `Bot` — silently fails)\n- `copilotId.setupReferenceType` = **`BotDefinition`** (NOT `Bot`)\n- `serviceChannelLabel` = **`Phone`** (NOT `\"Voice Call\"`); `serviceChannelDevName` = `sfdc_phone`\n- Variable name = **`recordid`** (lowercase — differs from the escalation flow's `recordId`)\n- Include **all** empty `\u003cinputParameters>` Flow Builder generates; `apiVersion=66.0`,\n `CanvasMode=AUTO_LAYOUT_CANVAS`, `areMetricsLoggedToDataCloud=false`\n\n## Step 5 — Create + wire the MessagingChannel (the former \"UI-only\" step)\n\nThis is the core of the last step — what Agent Builder → Connections → Voice used to own:\n\n```bash\n# 1. Create the channel\nCHANNEL_ID=$(sf data create record --sobject MessagingChannel \\\n --values \"MasterLabel='\u003cLabel>' DeveloperName='\u003cDev_Name>' MessageType='PstnVoice' MessagingPlatformKey='\u003cphone_number>' IsActive=true\" \\\n --json --target-org \u003calias> | python3 -c \"import json,sys;print(json.load(sys.stdin)['result']['id'])\")\n\n# 2. Get the inbound flow's FlowDefinition ID (300… prefix — NOT the 301… version ID)\nFLOW_DEF_ID=$(sf api request rest \\\n \"/services/data/v62.0/tooling/query?q=SELECT+Id+FROM+FlowDefinition+WHERE+DeveloperName='\u003cAgent_API_Name>_Voice_Omni_Flow'\" \\\n --target-org \u003calias> | python3 -c \"import json,sys;r=json.load(sys.stdin)['records'];print(r[0]['Id'] if r else '')\")\n\n# 3. Wire SessionHandlerId + FallbackQueueId (both mandatory)\nsf api request rest \"/services/data/v62.0/sobjects/MessagingChannel/$CHANNEL_ID\" \\\n --method PATCH \\\n --body \"{\\\"SessionHandlerId\\\":\\\"$FLOW_DEF_ID\\\",\\\"FallbackQueueId\\\":\\\"\u003cQueue_Id>\\\"}\" \\\n --target-org \u003calias>\n```\n\nChannel facts:\n- `MessageType` = **`PstnVoice`** (NOT `Phone` — only one `Phone` channel allowed per org).\n- `SessionHandlerId` must be a **FlowDefinition ID (`300…`)**, not a Flow *version* ID (`301…`).\n- `FallbackQueueId` is **mandatory** — without it calls may drop.\n\n## Step 6 — Smoke-test end-to-end\n\nThe number was already confirmed `Live` in Step 0, so this step just verifies the wiring holds\ntogether: agent `Status=Active`, channel `IsActive=true` with `SessionHandlerId` set, phone still\n`CodeStatus=Live`, queue has a `VoiceCall` SObject + ≥1 member.\n```bash\nsf data query --query \"SELECT Id, CodeStatus FROM PhoneNumber WHERE PhoneNumber='\u003cphone_number>'\" --target-org \u003calias>\n```\nThen place a test call and confirm the agent answers.\n\n---\n\n## Troubleshooting — call not reaching the agent\n\n```bash\nsf data query --query \"SELECT Id, DisconnectReason, ConversationId, CallSubtype, VendorType FROM VoiceCall ORDER BY CreatedDate DESC LIMIT 5\" --target-org \u003calias>\nsf data query --query \"SELECT Id, BotId, Status, ActiveTime, HandleTime, RoutingModel FROM AgentWork ORDER BY CreatedDate DESC LIMIT 5\" --target-org \u003calias>\n```\n\n| VoiceCall.ConversationId | AgentWork | ActiveTime | Root cause |\n|---|---|---|---|\n| null | — | — | PSTN / Amazon Connect not reaching Salesforce |\n| set | none | — | Routing flow not running, or `SessionHandlerId` is a `301…` version ID |\n| set | BotId null | — | Queue `RoutingModel=LeastActive` → change to `ExternalRouting` |\n| set | BotId set | 0 | Planner has **no topics** — fix source agent, re-publish |\n| set | BotId set | >0 | Bot ran — inspect `MessagingSession` + transcript / welcome message |\n\n## Key gotchas (channel-attach only)\n\n| Mistake | Symptom | Fix |\n|---|---|---|\n| `routingType=Bot` in inbound flow | Agent not invoked | `routingType=Copilot` |\n| `setupReferenceType=Bot` | Deploy error / silent fail | `BotDefinition` |\n| `serviceChannelLabel=\"Voice Call\"` | Flow can't resolve channel | `\"Phone\"` |\n| No `SurfaceAction__Telephony` surface on planner | Escalation never fires on voice | Add Telephony surface + redeploy (Step 1) |\n| `SessionHandlerId` = `301…` (version) | Channel doesn't route | Use FlowDefinition `300…` |\n| `MessageType=Phone` | \"Only one Phone channel allowed\" | `PstnVoice` |\n| `RoutingModel=LeastActive` | Calls arrive, agent never answers | `ExternalRouting` |\n| `sf agent activate/deactivate` outside project dir | `InvalidProjectWorkspaceError` | `cd` into the SFDX project first |\n\n---\n\n## Related References\n- [voice-modality-reference.md](voice-modality-reference.md) — `modality voice:` block, connection surfaces, authoring guidance.\n- [voice-latency-heuristics.md](voice-latency-heuristics.md) — live-call latency anti-patterns.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10555,"content_sha256":"8c094e200feb41e8247478948880f94efd26b4eb71684c38ba4dd732a71042fa"},{"filename":"references/zen-of-agentscript.md","content":"# The Zen of AgentScript\n\nThese are enforceable, unordered authoring rules shipped with the skill. Each rule includes a test\nthat can fail. No rule takes precedence over another; a valid design satisfies\nall applicable rules at the same time.\n\n## Give each reachable branch one next outcome\n\nFor every branch, choose exactly one primary outcome:\n\n```text\nanswer | ask | invoke an action | transition | refuse | escalate\n```\n\nThe model may explain an outcome, but it must not receive two incompatible\nduties.\n\nPass:\n\n- A router transitions without answering the underlying request.\n- A request handler answers or invokes its domain action.\n- A verification gate asks for missing proof or transitions after proof.\n\nFail:\n\n- Global instructions say “always answer,” while a router branch says “do not\n answer; transition.”\n- A branch both escalates and promises that the current agent will continue\n the task.\n\n## Do not let prompt layout impersonate control flow\n\nInside `|` text, indentation, numbered steps, and words such as `Show`, `Ask`,\n`Call`, `Set`, or `STOP` are instructions for the model. They do not create\nruntime scope, mutate variables, execute tools, or enforce order.\n\nActions belong to the reasoning scope. Gate them with `available when` when\nthey must exist only for a particular machine-known branch. If two operations\nmust occur in order, use runtime control flow or one purpose-built\nimplementation that owns the sequence rather than a prose sequence of\nmodel-selected tools.\n\nPass:\n\n- A failed-validation branch includes self-contained response guidance while\n its success-only action is unavailable.\n- A consequential action becomes available only after its typed prerequisite\n output is stored.\n\nFail:\n\n- “Step 4” assumes the model saw Steps 1–3.\n- An action visually indented beneath one prompt branch remains ungated for\n every other branch.\n- Prompt text says “set the value, then call the action” as though the setter\n guarantees the next action.\n\n## Declare no mutable variable without a named consumer\n\nBefore adding a variable, identify at least one concrete consumer:\n\n```text\nif | available when | transition | action input | later-turn exact output\n```\n\nCommon justified uses include:\n\n- trusted action output;\n- authorization, eligibility, or confirmation proof;\n- an exact value required by later deterministic logic; or\n- a value explicitly required beyond the conversation-history window; or\n- a product-owned state value whose producer, consumer, and lifetime are clear.\n\nIf no consumer requires stored state, prefer conversation history or a direct\naction binding.\n\nPass:\n\n```agentscript\nauthenticated: mutable boolean = False\n\nlookup_profile: @actions.get_profile\n available when @variables.authenticated == True\n```\n\nFail:\n\n```agentscript\nhas_greeted: mutable boolean = False\nquestion_asked: mutable boolean = False\nconversation_stage: mutable string = \"collecting\"\n```\n\n## Add deterministic control only for a named cause\n\nEvery `if`, `available when`, automatic `run`, or forced transition should\nprotect a named requirement or reproduced failure. Common causes include:\n\n```text\nregulation | authorization | product invariant | repeatability requirement |\nirreversible consequence | external ordering | observed trace or eval failure\n```\n\nIf the decision depends on unstructured current intent and none of those causes\napplies, leave the decision to model reasoning.\n\nPass:\n\n- Hide a refund action until the exact amount and explicit confirmation are\n recorded.\n- Prevent step 2 until step 1’s external action returns `success=True`.\n\nFail:\n\n- Add a `current_step` counter because a conversation happens to have several\n questions.\n- Force every follow-up back into an old subagent without a reproduced routing\n defect.\n\n## Create a subagent only when the boundary changes behavior\n\nA subagent boundary should change at least one of:\n\n```text\nobjective | instructions | available actions | authority | escalation behavior\n```\n\nThe difference should be material enough to justify the extra prompt boundary,\ntool scope, routing behavior, and lifecycle. A greeting, cancellation\nacknowledgment, completion message, ambiguity question, or ordinary dialogue\nstep normally remains a branch unless separating it improves a measured\nbehavior or enforces a real boundary.\n\nFor a focused single-domain agent, the concrete default is:\n\n```agentscript\nstart_agent event_search:\n reasoning:\n actions:\n search: @actions.search_events\n```\n\nThat means one execution block and zero `subagent` blocks—not an\n`agent_router` that only transitions to `event_search`.\n\nPass:\n\n- Separate public FAQ actions from authenticated account actions.\n- Separate turn-ending human escalation from a returning specialist\n consultation.\n\nFail:\n\n- Create `greeting`, `collect_name`, `collect_email`, and `present_results`\n subagents solely to represent dialogue stages.\n- Wrap one read-only event search in a router and a cancellation subagent when\n the single search scope can cancel without invoking its action.\n\n## Make model-visible instructions concrete and self-contained\n\nWrite what the model must do now. Do not tell it to inspect AgentScript\nconstructs such as the active subagent, `@variables`, lifecycle hooks, or “the\nreasoning instructions.”\n\nFor every branch, concatenate the effective system text and resolved reasoning\ntext. The result must still prescribe one compatible outcome from the\nbranch-outcome rule.\n\nPass:\n\n```text\nAsk for the minimum information needed to verify identity. Do not use\naccount-changing actions.\n```\n\nFail:\n\n```text\nInspect the current subagent and variables, then follow the response duty in\nthe reasoning instructions.\n```\n\n## Reveal every runtime value the model must read\n\nThe runtime can evaluate `@variables.X` in predicates, assignments, and action\nbindings. The model cannot inspect the variable store directly. Inside `|`\ntext, use `{[email protected]}` when that prompt must receive the current stored\nvalue. A value may also be visible when separately preserved in conversation\nor tool history, but a bare `@variables.X` token does not reveal it.\n\nDo not interpolate a name merely because it resembles a variable. A literal\ninstruction such as `Set next_destination to \"self_service\"` supplies a\nparameter value; it does not read the current variable.\n\nPass:\n\n```agentscript\nif @variables.case_summary != \"\":\n | Summarize this case for the customer: {[email protected]_summary}\n```\n\nFail:\n\n```agentscript\n| Read @variables.case_summary and explain it to the customer.\n```\n\n## Give exact facts to runtime and meaning to the model\n\nFor each condition, decide who can evaluate it correctly.\n\n- Use AgentScript control for an exact trusted fact whose consequence must not\n vary.\n- Use model reasoning for intent, ambiguity, meaning, tone, and other\n situation-aware interpretation.\n- In mixed cases, let the model interpret intent while runtime state gates any\n consequential action.\n\nA merge field gives the model a value. It does not make the model's comparison\ndeterministic.\n\nPass:\n\n```agentscript\nif @variables.account_status == \"blocked\":\n | Help the customer recover access to the blocked account.\n\n| If the user wants to speak with a human, use\n {[email protected]_to_support}.\n```\n\nFail:\n\n```agentscript\n| If {[email protected]_status} == \"blocked\", follow the blocked-account\n workflow.\n```\n\nDo not convert every sentence containing “if” into state and branches. Make\nthe ownership choice from the nature and consequence of the decision.\n\n## Match reasoning burden to the deployed model\n\nEach reasoning iteration should have one coherent objective and a number of\nclassifications, precedence rules, and tool choices that the target model has\ndemonstrated it can handle.\n\nPass:\n\n- Target-model evals cover the semantic boundaries in the resolved prompt.\n- A smaller model receives a narrower task instead of more overlapping rules.\n\nFail:\n\n- A stronger model is expected to repair contradictory instructions, missing\n values, or impossible sequencing.\n- The target model is unknown, but the design is declared appropriately\n agentic or scripted without evidence.\n\n## Use slot filling unless the value is controlled\n\nBind an action input with `...` when the model can safely extract it from the\ncurrent turn and surviving history.\n\nUse `@variables.x` only when the value is trusted, canonicalized, needed by\ndeterministic logic, or must be reused after its action-output scope ends. Use a\nliteral only for an actual constant.\n\nAction descriptions need:\n\n1. the action’s outcome;\n2. when to choose it over its closest alternative; and\n3. any material consequence.\n\nThey do not need to script the surrounding conversation.\n\nPass:\n\n```agentscript\nfind_events: @actions.search_events\n with interest=...\n```\n\nFail:\n\n- Add a separate `setVariables` call just to copy “jazz” from history before\n calling the search action.\n- Pin a user-correctable value from stale state when `...` would use the latest\n turn.\n\n## Bind consequential actions to machine-checkable preconditions\n\nFor an action that changes money, access, records, commitments, or external\nstate:\n\n1. bind the exact target and material parameters;\n2. make required authorization and confirmation machine-checkable;\n3. keep the action unavailable until those checks pass;\n4. record the action result or idempotency key when repeat execution would\n cause harm; and\n5. do not advance workflow state when the action fails.\n\nPass:\n\n```agentscript\nissue_refund: @actions.refund\n with [email protected]_order_id\n with [email protected]_amount\n available when @variables.customer_verified == True\n available when @variables.refund_confirmed == True\n available when @variables.refund_id == \"\"\n set @variables.refund_id = @outputs.refund_id\n```\n\nFail:\n\n- A prose instruction says “only refund after confirmation,” but the action is\n always available.\n- `current_step` advances after `@outputs.success == False`.\n\n## Treat action execution—not model text—as evidence\n\nThe agent should claim an external fact or completed action only when the\nobserved action result supports that exact claim. If the action result proves\nonly that work was requested or accepted, use language such as “submitted” or\n“initiated,” not “completed.”\n\nUse direct `@outputs` chaining inside the same action scope. Persist only the\nfields a later deterministic consumer needs. If later logic needs only\ncomplete-versus-incomplete, persist a trusted boolean outcome rather than a\ndisplay-only external identifier. Persist the identifier itself only when an\nexact-ID consumer exists, such as a later action input, idempotency guard, or\nrequired later-turn evidence check.\n\nPass:\n\n- The response names the returned status from the order lookup.\n- A later action receives the canonical ID returned by verification.\n- A final verification writes `verified=True` for repeat gating while its\n display-only receipt remains in the action result and conversation history.\n\nFail:\n\n- The model says “your refund was issued” because the action was visible or it\n intended to call it.\n- An empty or failed action result is presented as success.\n- A final receipt ID is copied into mutable state even though later logic tests\n only whether completion occurred.\n\n## Give every flag, cache, and latch a complete lifecycle\n\nFor each persistent control value, identify the lifecycle fields that can\naffect its intended use:\n\n```text\nowner | writer | reader | reset | expiry | correction behavior | cancel path\n```\n\nNot every short-lived value needs every field. Require reset, expiry,\ncorrection, or cancellation behavior only when that path can occur before the\nvalue stops mattering. Reject a value when a reachable use case leaves its\nmeaning stale or ambiguous.\n\nAdditional checks:\n\n- Keep one source of truth; do not store both `current_step` and equivalent\n completion flags.\n- A cache must define when external data is refreshed.\n- A focus latch must be justified by a reproduced trace and allow the next user\n turn to cancel or change intent.\n\nFail:\n\n- `open_gate` bypasses fresh routing and the locked subagent has no exit action.\n- `data_loaded=True` suppresses refresh for the rest of the conversation.\n\n## Merge only on conversation behavior, with syntax as a hard precondition\n\nEvery candidate should pass the applicable deterministic checks available for\nthe artifact:\n\n```text\nparse | lint | reference resolution | compile | emitted-artifact inspection\n```\n\nThen select the parent-versus-candidate scenarios material to the changed\nbehavior:\n\n- natural follow-up;\n- correction of an earlier value;\n- intent change during a workflow;\n- cancellation during verification or confirmation;\n- action success, empty result, and failure;\n- trusted authorization;\n- consequential confirmation;\n- exact later-turn action data flow; and\n- completion without repeated actions.\n\nMerge only when:\n\n1. the candidate has no new deterministic validation failure;\n2. safety, authorization, confirmation, and release boundaries do not regress;\n3. protected parent behaviors remain correct; and\n4. the candidate improves or ties conversation outcomes without adding\n unjustified state, turns, or tool calls.\n\nParser success alone is not evidence that the agent behaves well.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13399,"content_sha256":"51668208b10eaffc01d50d64d294d179c242a45de58862fd4df8522119d758d3"},{"filename":"scripts/__init__.py","content":"","content_type":"text/x-python; charset=utf-8","language":"python","size":0,"content_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"filename":"scripts/agentscript-sdk-loader.mjs","content":"import fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { fileURLToPath, pathToFileURL } from \"node:url\"\n\nexport const PUBLIC_PACKAGE = \"@sf-agentscript/agentforce\"\nexport const PUBLIC_VERSION = \"2.9.27\"\nexport const LEGACY_PACKAGE = \"@agentscript/agentforce\"\nexport const SOURCE_REPOSITORY = \"https://github.com/salesforce/agentscript.git\"\nexport const SOURCE_REF = \"main\"\n\nexport function sdkCacheDirectory(explicitDirectory) {\n if (explicitDirectory) return path.resolve(explicitDirectory)\n if (process.env.AGENTFORCE_GENERATE_CACHE) {\n return path.resolve(process.env.AGENTFORCE_GENERATE_CACHE)\n }\n\n const cacheRoot = process.env.XDG_CACHE_HOME\n ? path.resolve(process.env.XDG_CACHE_HOME)\n : path.join(os.homedir(), \".cache\")\n return path.join(cacheRoot, \"agentforce-generate\")\n}\n\nexport function sdkManifestPath(cacheDirectory) {\n return path.join(cacheDirectory, \"agentscript-sdk\", \"current.json\")\n}\n\nfunction resolvePackage(packageName, baseFile) {\n const basePath = baseFile.startsWith(\"file:\")\n ? fileURLToPath(baseFile)\n : baseFile\n let directory =\n fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()\n ? basePath\n : path.dirname(basePath)\n\n while (true) {\n const packageRoot = path.join(\n directory,\n \"node_modules\",\n ...packageName.split(\"/\"),\n )\n const packageFile = path.join(packageRoot, \"package.json\")\n if (fs.existsSync(packageFile)) {\n try {\n const manifest = JSON.parse(fs.readFileSync(packageFile, \"utf8\"))\n const relativeEntry = manifest.exports?.[\".\"]?.import || manifest.main\n if (relativeEntry) {\n return {\n entry: path.join(packageRoot, relativeEntry),\n version: manifest.version,\n }\n }\n } catch {\n return undefined\n }\n }\n\n const parent = path.dirname(directory)\n if (parent === directory) return undefined\n directory = parent\n }\n}\n\nfunction packageCandidates() {\n const cwdBase = path.join(process.cwd(), \"package.json\")\n return [\n [PUBLIC_PACKAGE, cwdBase],\n [LEGACY_PACKAGE, cwdBase],\n [PUBLIC_PACKAGE, import.meta.url],\n [LEGACY_PACKAGE, import.meta.url],\n ]\n}\n\nasync function loadEntry(entry, provenance) {\n if (!entry || !fs.existsSync(entry)) return undefined\n\n try {\n const sdk = await import(pathToFileURL(entry).href)\n if (typeof sdk.compileSource !== \"function\") return undefined\n return { sdk, entry, ...provenance }\n } catch {\n return undefined\n }\n}\n\nexport async function loadAgentScriptSdk(options = {}) {\n const cacheDirectory = sdkCacheDirectory(options.cacheDirectory)\n const explicitEntry = process.env.AGENTSCRIPT_SDK_ENTRY\n if (explicitEntry) {\n const loaded = await loadEntry(path.resolve(explicitEntry), {\n provider: \"explicit\",\n })\n if (loaded) return loaded\n throw new Error(\n `AGENTSCRIPT_SDK_ENTRY does not expose compileSource(): ${explicitEntry}`,\n )\n }\n\n const manifestPath = sdkManifestPath(cacheDirectory)\n if (fs.existsSync(manifestPath)) {\n try {\n const manifest = JSON.parse(fs.readFileSync(manifestPath, \"utf8\"))\n const loaded = await loadEntry(manifest.entry, {\n provider: manifest.provider,\n package: manifest.package,\n version: manifest.version,\n sourceRevision: manifest.sourceRevision,\n manifestPath,\n })\n if (loaded) return loaded\n } catch {\n // Continue to ambient package resolution and return one actionable error.\n }\n }\n\n for (const [packageName, baseFile] of packageCandidates()) {\n const resolved = resolvePackage(packageName, baseFile)\n const loaded = await loadEntry(resolved?.entry, {\n provider: \"ambient-npm\",\n package: packageName,\n version: resolved?.version,\n })\n if (loaded) return loaded\n }\n\n const setupScript = fileURLToPath(\n new URL(\"./setup-agentscript-sdk.mjs\", import.meta.url),\n )\n throw new Error(\n `AgentScript SDK not found. Run \\`node \"${setupScript}\" --npm --json\\`, then retry.`,\n )\n}\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":4061,"content_sha256":"b342bd34559e3fcb305a21e0aeb800a9fb6bd65a0cd0cc6f017b0afc97e17749"},{"filename":"scripts/discover.py","content":"#!/usr/bin/env python3\n\"\"\"Discover which .agent file targets (flows, apex classes, retrievers) exist in a Salesforce org.\n\nReads .agent files to extract target: values (flow://, apex://, retriever://),\nqueries the org via sf CLI, and reports found/missing targets with fuzzy suggestions.\n\nUsage:\n python3 scripts/discover.py --agent-file path/to/Agent.agent -o OrgAlias\n python3 scripts/discover.py --agent-dir force-app/main/default/aiAuthoringBundles -o OrgAlias\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport difflib\nimport json\nimport re\nimport subprocess\nimport sys\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\n\n@dataclass\nclass Suggestion:\n \"\"\"A similar resource found in the org.\"\"\"\n name: str\n similarity: float # 0.0–1.0\n\n\n@dataclass\nclass TargetStatus:\n \"\"\"Status of one .agent target.\"\"\"\n agent_file: str\n target: str # e.g. \"flow://Get_Order_Status\"\n target_type: str # \"flow\", \"apex\", \"retriever\"\n target_name: str # \"Get_Order_Status\"\n found: bool = False\n details: str = \"\"\n suggestions: list[Suggestion] = field(default_factory=list)\n\n\n@dataclass\nclass IoMismatch:\n \"\"\"A mismatch between expected and actual I/O parameters.\"\"\"\n target_name: str\n field_name: str\n direction: str # \"input\" or \"output\"\n expected_type: str\n actual_type: str\n issue: str # \"missing\", \"type_mismatch\", \"extra\"\n\n\n@dataclass\nclass DiscoveryReport:\n \"\"\"Collection of target statuses.\"\"\"\n targets: list[TargetStatus] = field(default_factory=list)\n io_mismatches: list[IoMismatch] = field(default_factory=list)\n\n @property\n def found(self) -> list[TargetStatus]:\n return [t for t in self.targets if t.found]\n\n @property\n def missing(self) -> list[TargetStatus]:\n return [t for t in self.targets if not t.found]\n\n @property\n def all_found(self) -> bool:\n return all(t.found for t in self.targets)\n\n\ndef extract_targets(agent_file: Path) -> list[tuple[str, str, str]]:\n \"\"\"Extract target: values from an .agent file.\n\n Returns list of (target_uri, target_type, target_name) tuples.\n e.g. (\"flow://Get_Order_Status\", \"flow\", \"Get_Order_Status\")\n \"\"\"\n content = agent_file.read_text(encoding=\"utf-8\")\n targets = []\n # Match target: \"flow://Name\" or target: \"apex://Name\" or target: \"retriever://Name\"\n for match in re.finditer(r'target:\\s*\"?(flow|apex|retriever)://([^\"\\s]+)\"?', content):\n target_type = match.group(1)\n target_name = match.group(2)\n target_uri = f\"{target_type}://{target_name}\"\n targets.append((target_uri, target_type, target_name))\n return targets\n\n\ndef extract_actions(agent_file: Path) -> list[dict]:\n \"\"\"Extract action definitions from an .agent file for scaffolding.\n\n Returns list of dicts with keys: name, target, target_type, target_name, inputs, outputs.\n \"\"\"\n content = agent_file.read_text(encoding=\"utf-8\")\n actions = []\n # Simple regex-based extraction of action blocks\n # Matches patterns like:\n # action_name:\n # ...\n # target: \"flow://Name\"\n current_action = None\n current_inputs = []\n current_outputs = []\n in_inputs = False\n in_outputs = False\n\n for line in content.splitlines():\n stripped = line.strip()\n\n # Detect target line\n target_match = re.match(r'target:\\s*\"?(flow|apex|retriever)://([^\"\\s]+)\"?', stripped)\n if target_match and current_action:\n current_action[\"target_type\"] = target_match.group(1)\n current_action[\"target_name\"] = target_match.group(2)\n current_action[\"target\"] = f\"{target_match.group(1)}://{target_match.group(2)}\"\n # Target terminates input/output collection\n in_inputs = False\n in_outputs = False\n continue\n\n # Detect description line (captures action description for classification)\n desc_match = re.match(r'description:\\s*\"(.+)\"', stripped)\n if desc_match and current_action and \"description\" not in current_action:\n current_action[\"description\"] = desc_match.group(1)\n continue\n\n # Detect inputs/outputs sections\n if stripped == \"inputs:\":\n in_inputs = True\n in_outputs = False\n continue\n elif stripped == \"outputs:\":\n in_outputs = True\n in_inputs = False\n continue\n\n # Detect action start (indented name followed by colon)\n action_match = re.match(r'^(\\t{2,3}|\\s{6,12})(\\w+):\\s*

Agent Script Skill What This Skill Is For This skill is for developing Agentforce agents, primarily with Agent Script, Salesforce's scripting language for AI agents. Org-backed workflows require an Agentforce license, API v66.0 or later, and an Einstein Agent User. Static authoring and review can proceed without org access. CRITICAL: Agent Script is NOT AppleScript, JavaScript, Python, or any other language. Do NOT confuse Agent Script syntax or semantics with any other language you have been trained on. Agent Script agents are defined by metadata: an file (agent behavior) plus a sibling file…

, line)\n if action_match:\n # Save previous action\n if current_action and current_action.get(\"target\"):\n current_action[\"inputs\"] = current_inputs\n current_action[\"outputs\"] = current_outputs\n actions.append(current_action)\n current_action = {\"name\": action_match.group(2)}\n current_inputs = []\n current_outputs = []\n in_inputs = False\n in_outputs = False\n continue\n\n # Collect complex_data_type_name for the last collected param\n cdt_match = re.match(r'complex_data_type_name:\\s*\"([^\"]+)\"', stripped)\n if cdt_match:\n last_list = current_inputs if in_inputs else (current_outputs if in_outputs else None)\n if last_list:\n last_list[-1][\"complex_data_type_name\"] = cdt_match.group(1)\n continue\n\n # Collect input/output parameters (match \"name: type\" on stripped text)\n param_match = re.match(r'^(\\w+):\\s*(string|number|boolean|date|datetime|id|object)\\b', stripped)\n if param_match:\n param = {\"name\": param_match.group(1), \"type\": param_match.group(2)}\n if in_inputs:\n current_inputs.append(param)\n elif in_outputs:\n current_outputs.append(param)\n\n # Don't forget the last action\n if current_action and current_action.get(\"target\"):\n current_action[\"inputs\"] = current_inputs\n current_action[\"outputs\"] = current_outputs\n actions.append(current_action)\n\n return actions\n\n\ndef _query_org(query: str, target_org: str) -> list[dict]:\n \"\"\"Run SOQL query via sf CLI.\"\"\"\n cmd = [\"sf\", \"data\", \"query\", \"--query\", query, \"-o\", target_org, \"--json\"]\n try:\n proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)\n if proc.returncode != 0:\n return []\n data = json.loads(proc.stdout)\n return data.get(\"result\", {}).get(\"records\", [])\n except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):\n return []\n\n\ndef _check_flows(names: list[str], target_org: str) -> dict[str, bool]:\n \"\"\"Check which flows exist in the org.\"\"\"\n records = _query_org(\n \"SELECT ApiName FROM FlowDefinitionView WHERE IsActive = true\",\n target_org,\n )\n org_flows = {r[\"ApiName\"] for r in records if \"ApiName\" in r}\n return {name: name in org_flows for name in names}\n\n\ndef _check_apex(names: list[str], target_org: str) -> dict[str, bool]:\n \"\"\"Check which Apex classes exist in the org.\"\"\"\n records = _query_org(\n \"SELECT Name FROM ApexClass WHERE Status = 'Active'\",\n target_org,\n )\n org_classes = {r[\"Name\"] for r in records if \"Name\" in r}\n return {name: name in org_classes for name in names}\n\n\ndef _check_retrievers(names: list[str], target_org: str) -> dict[str, bool]:\n \"\"\"Check which retrievers (DataKnowledgeSpace) exist in the org.\"\"\"\n records = _query_org(\n \"SELECT DeveloperName FROM DataKnowledgeSpace\",\n target_org,\n )\n org_retrievers = {r[\"DeveloperName\"] for r in records if \"DeveloperName\" in r}\n return {name: name in org_retrievers for name in names}\n\n\ndef _suggest_similar(name: str, available: list[str], threshold: float = 0.4) -> list[Suggestion]:\n \"\"\"Find similar names in the org using fuzzy matching.\"\"\"\n suggestions = []\n name_lower = name.lower()\n name_tokens = set(re.split(r\"[_\\s]|(?\u003c=[a-z])(?=[A-Z])\", name))\n\n for candidate in available:\n # Sequence matching\n seq_score = difflib.SequenceMatcher(None, name_lower, candidate.lower()).ratio()\n\n # Jaccard keyword overlap\n cand_tokens = set(re.split(r\"[_\\s]|(?\u003c=[a-z])(?=[A-Z])\", candidate))\n if name_tokens and cand_tokens:\n jaccard = len(name_tokens & cand_tokens) / len(name_tokens | cand_tokens)\n else:\n jaccard = 0.0\n\n score = max(seq_score, jaccard)\n if score >= threshold:\n suggestions.append(Suggestion(name=candidate, similarity=round(score, 2)))\n\n return sorted(suggestions, key=lambda s: s.similarity, reverse=True)[:3]\n\n\ndef _rest_api_get(path: str, target_org: str) -> dict | None:\n \"\"\"Call a Salesforce REST API endpoint via sf CLI.\"\"\"\n cmd = [\"sf\", \"org\", \"open\", \"-o\", target_org, \"--json\", \"--url-only\"]\n try:\n proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)\n if proc.returncode != 0:\n return None\n data = json.loads(proc.stdout)\n instance_url = data.get(\"result\", {}).get(\"url\", \"\")\n # Extract base URL (everything before /secur/ or similar)\n import urllib.parse\n parsed = urllib.parse.urlparse(instance_url)\n base = f\"{parsed.scheme}://{parsed.netloc}\"\n except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):\n return None\n\n # Use sf api to call REST endpoint\n cmd = [\"sf\", \"api\", \"request\", \"rest\", path, \"-o\", target_org, \"--json\"]\n try:\n proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)\n if proc.returncode != 0:\n return None\n return json.loads(proc.stdout)\n except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):\n return None\n\n\n# Flow type to .agent type mapping for validation\n_FLOW_TYPE_TO_AGENT = {\n \"STRING\": \"string\",\n \"NUMBER\": \"number\",\n \"CURRENCY\": \"number\",\n \"BOOLEAN\": \"boolean\",\n \"DATE\": \"date\",\n \"DATETIME\": \"datetime\",\n}\n\n\ndef validate_action_io(\n target_type: str,\n target_name: str,\n expected_inputs: list[dict],\n expected_outputs: list[dict],\n target_org: str,\n) -> list[IoMismatch]:\n \"\"\"Validate that an existing target's I/O matches what the .agent file declares.\n\n For flows, queries the Actions REST API to get actual parameter schema.\n For apex, queries the Tooling API for @InvocableVariable fields.\n\n Args:\n target_type: \"flow\" or \"apex\".\n target_name: The API name of the target.\n expected_inputs: Input params declared in the .agent file.\n expected_outputs: Output params declared in the .agent file.\n target_org: Salesforce org alias.\n\n Returns:\n List of IoMismatch entries (empty if everything matches).\n \"\"\"\n mismatches = []\n\n if target_type == \"flow\":\n mismatches = _validate_flow_io(target_name, expected_inputs, expected_outputs, target_org)\n elif target_type == \"apex\":\n mismatches = _validate_apex_io(target_name, expected_inputs, expected_outputs, target_org)\n\n return mismatches\n\n\ndef _validate_flow_io(\n flow_name: str,\n expected_inputs: list[dict],\n expected_outputs: list[dict],\n target_org: str,\n) -> list[IoMismatch]:\n \"\"\"Validate flow I/O via the custom/flow REST API.\"\"\"\n mismatches = []\n api_path = f\"/services/data/v{66}.0/actions/custom/flow/{flow_name}\"\n result = _rest_api_get(api_path, target_org)\n if not result:\n return mismatches\n\n # Parse actual inputs/outputs from the REST response\n actual_inputs = {}\n actual_outputs = {}\n for param in result.get(\"result\", result).get(\"inputs\", []):\n name = param.get(\"name\", \"\")\n ptype = param.get(\"type\", \"\").upper()\n actual_inputs[name] = _FLOW_TYPE_TO_AGENT.get(ptype, \"string\")\n\n for param in result.get(\"result\", result).get(\"outputs\", []):\n name = param.get(\"name\", \"\")\n ptype = param.get(\"type\", \"\").upper()\n actual_outputs[name] = _FLOW_TYPE_TO_AGENT.get(ptype, \"string\")\n\n # Check expected inputs exist\n for inp in expected_inputs:\n name = inp[\"name\"]\n if name not in actual_inputs:\n mismatches.append(IoMismatch(\n target_name=flow_name, field_name=name, direction=\"input\",\n expected_type=inp.get(\"type\", \"string\"), actual_type=\"\",\n issue=\"missing\",\n ))\n elif actual_inputs[name] != inp.get(\"type\", \"string\"):\n mismatches.append(IoMismatch(\n target_name=flow_name, field_name=name, direction=\"input\",\n expected_type=inp.get(\"type\", \"string\"), actual_type=actual_inputs[name],\n issue=\"type_mismatch\",\n ))\n\n # Check expected outputs exist\n for out in expected_outputs:\n name = out[\"name\"]\n if name not in actual_outputs:\n mismatches.append(IoMismatch(\n target_name=flow_name, field_name=name, direction=\"output\",\n expected_type=out.get(\"type\", \"string\"), actual_type=\"\",\n issue=\"missing\",\n ))\n\n return mismatches\n\n\ndef _validate_apex_io(\n class_name: str,\n expected_inputs: list[dict],\n expected_outputs: list[dict],\n target_org: str,\n) -> list[IoMismatch]:\n \"\"\"Validate Apex I/O by querying the class body for @InvocableVariable fields.\"\"\"\n mismatches = []\n records = _query_org(\n f\"SELECT Body FROM ApexClass WHERE Name = '{class_name}' LIMIT 1\",\n target_org,\n )\n if not records:\n return mismatches\n\n body = records[0].get(\"Body\", \"\")\n # Extract @InvocableVariable field names from Request/Response classes\n # Simple regex: find field declarations after @InvocableVariable\n field_pattern = re.compile(r'@InvocableVariable[^;]*\\n\\s*public\\s+\\w+\\s+(\\w+)\\s*;')\n actual_fields = set(field_pattern.findall(body))\n\n for inp in expected_inputs:\n if inp[\"name\"] not in actual_fields:\n mismatches.append(IoMismatch(\n target_name=class_name, field_name=inp[\"name\"], direction=\"input\",\n expected_type=inp.get(\"type\", \"string\"), actual_type=\"\",\n issue=\"missing\",\n ))\n\n for out in expected_outputs:\n if out[\"name\"] not in actual_fields:\n mismatches.append(IoMismatch(\n target_name=class_name, field_name=out[\"name\"], direction=\"output\",\n expected_type=out.get(\"type\", \"string\"), actual_type=\"\",\n issue=\"missing\",\n ))\n\n return mismatches\n\n\ndef discover(agent_file: Path, target_org: str, validate_io: bool = False) -> DiscoveryReport:\n \"\"\"Run discovery for a single .agent file.\n\n Args:\n agent_file: Path to the .agent file.\n target_org: Salesforce org alias.\n validate_io: If True, validate I/O parameters for found targets.\n \"\"\"\n report = DiscoveryReport()\n raw_targets = extract_targets(agent_file)\n\n if not raw_targets:\n return report\n\n # Deduplicate targets (same action can appear in multiple topics)\n seen_uris = set()\n unique_targets = []\n for uri, ttype, tname in raw_targets:\n if uri not in seen_uris:\n seen_uris.add(uri)\n unique_targets.append((uri, ttype, tname))\n raw_targets = unique_targets\n\n # Group by type\n by_type: dict[str, list[tuple[str, str]]] = {\"flow\": [], \"apex\": [], \"retriever\": []}\n for uri, ttype, tname in raw_targets:\n by_type.setdefault(ttype, []).append((uri, tname))\n\n # Check each type\n checkers = {\n \"flow\": (_check_flows, \"SELECT ApiName FROM FlowDefinitionView WHERE IsActive = true\", \"ApiName\"),\n \"apex\": (_check_apex, \"SELECT Name FROM ApexClass WHERE Status = 'Active'\", \"Name\"),\n \"retriever\": (_check_retrievers, \"SELECT DeveloperName FROM DataKnowledgeSpace\", \"DeveloperName\"),\n }\n\n for ttype, targets in by_type.items():\n if not targets:\n continue\n\n checker_fn, query, field_name = checkers[ttype]\n names = [t[1] for t in targets]\n found_map = checker_fn(names, target_org)\n\n # Get all available resources for fuzzy matching\n all_records = _query_org(query, target_org)\n available = [r[field_name] for r in all_records if field_name in r]\n\n for uri, name in targets:\n status = TargetStatus(\n agent_file=str(agent_file),\n target=uri,\n target_type=ttype,\n target_name=name,\n found=found_map.get(name, False),\n )\n if not status.found:\n status.suggestions = _suggest_similar(name, available)\n report.targets.append(status)\n\n # Validate I/O for found targets\n if validate_io:\n actions = {a[\"target_name\"]: a for a in extract_actions(agent_file) if a.get(\"target_name\")}\n for target in report.found:\n if target.target_type in (\"flow\", \"apex\") and target.target_name in actions:\n action_def = actions[target.target_name]\n mismatches = validate_action_io(\n target.target_type,\n target.target_name,\n action_def.get(\"inputs\", []),\n action_def.get(\"outputs\", []),\n target_org,\n )\n report.io_mismatches.extend(mismatches)\n\n return report\n\n\ndef discover_dir(agent_dir: Path, target_org: str, validate_io: bool = False) -> DiscoveryReport:\n \"\"\"Run discovery for all .agent files in a directory.\"\"\"\n combined = DiscoveryReport()\n for agent_file in sorted(agent_dir.rglob(\"*.agent\")):\n sub_report = discover(agent_file, target_org, validate_io=validate_io)\n combined.targets.extend(sub_report.targets)\n return combined\n\n\ndef print_report(report: DiscoveryReport) -> None:\n \"\"\"Print a human-readable discovery report.\"\"\"\n if not report.targets:\n print(\"No targets found in .agent file(s).\")\n return\n\n print(f\"\\n{'=' * 60}\")\n print(f\"Discovery Report: {len(report.targets)} target(s)\")\n print(f\"{'=' * 60}\")\n\n # Found targets\n if report.found:\n print(f\"\\n✅ Found ({len(report.found)}):\")\n for t in report.found:\n print(f\" {t.target}\")\n\n # Missing targets\n if report.missing:\n print(f\"\\n❌ Missing ({len(report.missing)}):\")\n for t in report.missing:\n print(f\" {t.target}\")\n for s in t.suggestions:\n print(f\" 💡 Did you mean: {s.name} ({s.similarity:.0%} match)?\")\n\n # I/O mismatches\n if report.io_mismatches:\n print(f\"\\n⚠️ I/O Mismatches ({len(report.io_mismatches)}):\")\n for m in report.io_mismatches:\n if m.issue == \"missing\":\n print(f\" {m.target_name}: {m.direction} '{m.field_name}' not found in org target\")\n elif m.issue == \"type_mismatch\":\n print(f\" {m.target_name}: {m.direction} '{m.field_name}' type mismatch — expected {m.expected_type}, got {m.actual_type}\")\n\n print(f\"\\n{'=' * 60}\")\n if report.all_found:\n print(\"All targets found in org.\")\n else:\n print(f\"{len(report.missing)} target(s) missing. Run /adlc-scaffold to generate stubs.\")\n print()\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Discover .agent file targets in a Salesforce org\")\n group = parser.add_mutually_exclusive_group(required=True)\n group.add_argument(\"--agent-file\", type=Path, help=\"Path to a single .agent file\")\n group.add_argument(\"--agent-dir\", type=Path, help=\"Directory containing .agent files\")\n parser.add_argument(\"-o\", \"--target-org\", required=True, help=\"Salesforce org alias\")\n parser.add_argument(\"--validate-io\", action=\"store_true\", help=\"Validate I/O parameters for found targets\")\n args = parser.parse_args()\n\n if args.agent_file:\n if not args.agent_file.exists():\n print(f\"Error: {args.agent_file} not found\", file=sys.stderr)\n sys.exit(1)\n report = discover(args.agent_file, args.target_org, validate_io=args.validate_io)\n else:\n if not args.agent_dir.exists():\n print(f\"Error: {args.agent_dir} not found\", file=sys.stderr)\n sys.exit(1)\n report = discover_dir(args.agent_dir, args.target_org, validate_io=args.validate_io)\n\n print_report(report)\n\n # Exit with non-zero if any targets are missing\n sys.exit(0 if report.all_found else 1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":20663,"content_sha256":"8100811e0c4a029a58e2eaa15227a4b4222453ed658c721194c25e681706f134"},{"filename":"scripts/generators/__init__.py","content":"","content_type":"text/x-python; charset=utf-8","language":"python","size":0,"content_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"filename":"scripts/generators/apex_stub.py","content":"\"\"\"Generate Apex @InvocableMethod stubs for .agent file action targets.\"\"\"\n\nfrom __future__ import annotations\n\n# Mapping from .agent types to Apex types\n_TYPE_MAP = {\n \"string\": \"String\",\n \"number\": \"Decimal\",\n \"boolean\": \"Boolean\",\n \"date\": \"Date\",\n \"datetime\": \"Datetime\",\n \"id\": \"Id\",\n \"object\": \"Map\u003cString, Object>\",\n}\n\n# Mapping from complex_data_type_name to Apex types (for action I/O)\n_COMPLEX_TYPE_MAP = {\n \"lightning__integerType\": \"Integer\",\n \"lightning__numberType\": \"Integer\",\n \"lightning__doubleType\": \"Double\",\n \"lightning__currencyType\": \"Decimal\",\n \"lightning__dateTimeStringType\": \"Datetime\",\n \"lightning__recordInfoType\": \"SObject\",\n \"lightning__objectType\": \"Map\u003cString, Object>\",\n \"lightning__listType\": \"List\u003cObject>\",\n \"lightning__textType\": \"String\",\n}\n\nAPI_VERSION = \"63.0\"\n\n\ndef generate_apex_class(\n class_name: str,\n inputs: list[dict] | None = None,\n outputs: list[dict] | None = None,\n description: str = \"\",\n) -> str:\n \"\"\"Generate an Apex class with @InvocableMethod stub.\n\n Produces a class with:\n - Inner Request class with @InvocableVariable fields for each input\n - Inner Response class with @InvocableVariable fields for each output\n - @InvocableMethod static method returning placeholder response\n\n Args:\n class_name: The Apex class name.\n inputs: Action input definitions (list of dicts with 'name', 'type' keys).\n outputs: Action output definitions (list of dicts with 'name', 'type' keys).\n description: Action description for the @InvocableMethod annotation.\n\n Returns:\n Apex class source code string.\n \"\"\"\n inputs = inputs or []\n outputs = outputs or []\n\n lines = [\n f'public with sharing class {class_name} {{',\n '',\n ]\n\n # Request inner class\n lines.append(' public class Request {')\n for inp in inputs:\n apex_type = _COMPLEX_TYPE_MAP.get(inp.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(inp.get(\"type\", \"string\"), \"String\"))\n desc = inp.get(\"description\", \"\")\n is_req = inp.get(\"required\", True)\n if desc:\n lines.append(f' @InvocableVariable(label=\\'{_escape_apex(desc)}\\' required={_apex_bool(is_req)})')\n else:\n lines.append(f' @InvocableVariable(required={_apex_bool(is_req)})')\n lines.append(f' public {apex_type} {inp[\"name\"]};')\n lines.append('')\n lines.append(' }')\n lines.append('')\n\n # Response inner class\n lines.append(' public class Response {')\n for out in outputs:\n apex_type = _COMPLEX_TYPE_MAP.get(out.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(out.get(\"type\", \"string\"), \"String\"))\n desc = out.get(\"description\", \"\")\n if desc:\n lines.append(f' @InvocableVariable(label=\\'{_escape_apex(desc)}\\')')\n else:\n lines.append(f' @InvocableVariable')\n lines.append(f' public {apex_type} {out[\"name\"]};')\n lines.append('')\n lines.append(' }')\n lines.append('')\n\n # @InvocableMethod\n method_label = _class_to_label(class_name)\n desc_str = _escape_apex(description) if description else \"TODO: Add description\"\n lines.extend([\n f' @InvocableMethod(label=\\'{method_label}\\' description=\\'{desc_str}\\')',\n ' public static List\u003cResponse> invoke(List\u003cRequest> requests) {',\n ' List\u003cResponse> responses = new List\u003cResponse>();',\n ' for (Request req : requests) {',\n ' Response res = new Response();',\n ' // TODO: Implement business logic',\n ])\n\n for out in outputs:\n default = _default_for_type(out.get(\"type\", \"string\"))\n lines.append(f' res.{out[\"name\"]} = {default};')\n\n lines.extend([\n ' responses.add(res);',\n ' }',\n ' return responses;',\n ' }',\n '}',\n ])\n\n return \"\\n\".join(lines) + \"\\n\"\n\n\ndef generate_apex_meta_xml(api_version: str = API_VERSION) -> str:\n \"\"\"Generate the .cls-meta.xml companion file.\"\"\"\n return (\n '\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n'\n '\u003cApexClass xmlns=\"http://soap.sforce.com/2006/04/metadata\">\\n'\n f' \u003capiVersion>{api_version}\u003c/apiVersion>\\n'\n ' \u003cstatus>Active\u003c/status>\\n'\n '\u003c/ApexClass>\\n'\n )\n\n\ndef _escape_apex(text: str) -> str:\n \"\"\"Escape special characters for Apex strings.\"\"\"\n return (text\n .replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"'\", \"\\\\'\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\"))\n\n\ndef _apex_bool(value: bool) -> str:\n \"\"\"Format boolean for Apex (lowercase).\"\"\"\n return \"true\" if value else \"false\"\n\n\ndef _class_to_label(class_name: str) -> str:\n \"\"\"Convert PascalCase class name to a human-readable label.\"\"\"\n result = []\n for i, ch in enumerate(class_name):\n if ch.isupper() and i > 0 and not class_name[i - 1].isupper():\n result.append(\" \")\n result.append(ch)\n return \"\".join(result)\n\n\ndef generate_soql_apex_class(\n class_name: str,\n inputs: list[dict] | None = None,\n outputs: list[dict] | None = None,\n description: str = \"\",\n sobject_name: str = \"\",\n) -> str:\n \"\"\"Generate an Apex class with SOQL query boilerplate.\n\n Like generate_apex_class but the invoke() method includes a SOQL query\n pattern based on input/output field names.\n\n Args:\n class_name: The Apex class name.\n inputs: Action input definitions.\n outputs: Action output definitions.\n description: Action description for the @InvocableMethod annotation.\n sobject_name: SObject to query (inferred from description if empty).\n\n Returns:\n Apex class source code string with SOQL logic.\n \"\"\"\n inputs = inputs or []\n outputs = outputs or []\n\n # Try to infer SObject name from description\n if not sobject_name:\n import re\n # Look for common SObject patterns in the description\n sobject_match = re.search(\n r'\\b(Account|Contact|Case|Opportunity|Lead|Order|Product2|Asset|'\n r'ServiceContract|WorkOrder|ServiceAppointment|InventoryItem|'\n r'MessagingSession|Campaign|Task|Event|'\n r'\\w+__c)\\b',\n description,\n )\n sobject_name = sobject_match.group(1) if sobject_match else \"SObject\"\n\n lines = [\n f'public with sharing class {class_name} {{',\n '',\n ]\n\n # Request inner class\n lines.append(' public class Request {')\n for inp in inputs:\n apex_type = _COMPLEX_TYPE_MAP.get(inp.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(inp.get(\"type\", \"string\"), \"String\"))\n desc = inp.get(\"description\", \"\")\n is_req = inp.get(\"required\", True)\n if desc:\n lines.append(f' @InvocableVariable(label=\\'{_escape_apex(desc)}\\' required={_apex_bool(is_req)})')\n else:\n lines.append(f' @InvocableVariable(required={_apex_bool(is_req)})')\n lines.append(f' public {apex_type} {inp[\"name\"]};')\n lines.append('')\n lines.append(' }')\n lines.append('')\n\n # Response inner class\n lines.append(' public class Response {')\n for out in outputs:\n apex_type = _COMPLEX_TYPE_MAP.get(out.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(out.get(\"type\", \"string\"), \"String\"))\n desc = out.get(\"description\", \"\")\n if desc:\n lines.append(f' @InvocableVariable(label=\\'{_escape_apex(desc)}\\')')\n else:\n lines.append(f' @InvocableVariable')\n lines.append(f' public {apex_type} {out[\"name\"]};')\n lines.append('')\n lines.append(' }')\n lines.append('')\n\n # @InvocableMethod with SOQL query\n method_label = _class_to_label(class_name)\n desc_str = _escape_apex(description) if description else \"TODO: Add description\"\n\n # Build WHERE hint from inputs (commented, since field names may not match SObject columns)\n where_hint_parts = [f'{inp[\"name\"]} = :req.{inp[\"name\"]}' for inp in inputs]\n where_hint = \" AND \".join(where_hint_parts) if where_hint_parts else \"\"\n\n lines.extend([\n f' @InvocableMethod(label=\\'{method_label}\\' description=\\'{desc_str}\\')',\n ' public static List\u003cResponse> invoke(List\u003cRequest> requests) {',\n ' List\u003cResponse> responses = new List\u003cResponse>();',\n ' for (Request req : requests) {',\n ' Response res = new Response();',\n '',\n f' // TODO: Adjust SOQL query for {sobject_name}',\n f' // TODO: Replace Id with actual fields and add WHERE clause',\n ])\n\n if where_hint:\n lines.append(f' // Suggested WHERE: {where_hint}')\n\n lines.extend([\n f' List\u003c{sobject_name}> records = [',\n ' SELECT Id',\n f' FROM {sobject_name}',\n ' WHERE Id != null',\n ' WITH USER_MODE',\n ' LIMIT 100',\n ' ];',\n '',\n ' // TODO: Map query results to output fields',\n ])\n\n for out in outputs:\n default = _default_for_type(out.get(\"type\", \"string\"))\n lines.append(f' res.{out[\"name\"]} = {default};')\n\n lines.extend([\n '',\n ' responses.add(res);',\n ' }',\n ' return responses;',\n ' }',\n '}',\n ])\n\n return \"\\n\".join(lines) + \"\\n\"\n\n\ndef generate_callout_apex_class(\n class_name: str,\n inputs: list[dict] | None = None,\n outputs: list[dict] | None = None,\n endpoint_url: str = \"https://example.com/api\",\n description: str = \"\",\n) -> str:\n \"\"\"Generate an Apex class with HTTP callout boilerplate.\n\n Like generate_apex_class but the invoke() method includes Http/HttpRequest/HttpResponse\n scaffolding instead of a plain placeholder.\n\n Args:\n class_name: The Apex class name.\n inputs: Action input definitions.\n outputs: Action output definitions.\n endpoint_url: Default endpoint URL placeholder.\n\n Returns:\n Apex class source code string with callout logic.\n \"\"\"\n inputs = inputs or []\n outputs = outputs or []\n\n lines = [\n f'public with sharing class {class_name} {{',\n '',\n ]\n\n # Request inner class\n lines.append(' public class Request {')\n for inp in inputs:\n apex_type = _COMPLEX_TYPE_MAP.get(inp.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(inp.get(\"type\", \"string\"), \"String\"))\n desc = inp.get(\"description\", \"\")\n is_req = inp.get(\"required\", True)\n if desc:\n lines.append(f' @InvocableVariable(label=\\'{_escape_apex(desc)}\\' required={_apex_bool(is_req)})')\n else:\n lines.append(f' @InvocableVariable(required={_apex_bool(is_req)})')\n lines.append(f' public {apex_type} {inp[\"name\"]};')\n lines.append('')\n lines.append(' }')\n lines.append('')\n\n # Response inner class\n lines.append(' public class Response {')\n for out in outputs:\n apex_type = _COMPLEX_TYPE_MAP.get(out.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(out.get(\"type\", \"string\"), \"String\"))\n desc = out.get(\"description\", \"\")\n if desc:\n lines.append(f' @InvocableVariable(label=\\'{_escape_apex(desc)}\\')')\n else:\n lines.append(f' @InvocableVariable')\n lines.append(f' public {apex_type} {out[\"name\"]};')\n lines.append('')\n lines.append(' }')\n lines.append('')\n\n # @InvocableMethod with HTTP callout\n method_label = _class_to_label(class_name)\n desc_str = _escape_apex(description) if description else \"TODO: Add description\"\n lines.extend([\n f' @InvocableMethod(label=\\'{method_label}\\' description=\\'{desc_str}\\')',\n ' public static List\u003cResponse> invoke(List\u003cRequest> requests) {',\n ' List\u003cResponse> responses = new List\u003cResponse>();',\n ' for (Request req : requests) {',\n ' Response res = new Response();',\n '',\n ' HttpRequest httpReq = new HttpRequest();',\n f' httpReq.setEndpoint(\\'{_escape_apex(endpoint_url)}\\');',\n ' httpReq.setMethod(\\'GET\\');',\n ' httpReq.setHeader(\\'Content-Type\\', \\'application/json\\');',\n '',\n ' Http http = new Http();',\n ' HttpResponse httpRes = http.send(httpReq);',\n '',\n ' if (httpRes.getStatusCode() == 200) {',\n ' Map\u003cString, Object> body = (Map\u003cString, Object>) JSON.deserializeUntyped(httpRes.getBody());',\n ' // TODO: Map response body to output fields',\n ])\n\n for out in outputs:\n default = _default_for_type(out.get(\"type\", \"string\"))\n lines.append(f' res.{out[\"name\"]} = {default};')\n\n lines.extend([\n ' }',\n '',\n ' responses.add(res);',\n ' }',\n ' return responses;',\n ' }',\n '}',\n ])\n\n return \"\\n\".join(lines) + \"\\n\"\n\n\ndef _default_for_type(type_name: str) -> str:\n \"\"\"Return a placeholder Apex default value literal.\"\"\"\n defaults = {\n \"string\": \"'TODO'\",\n \"number\": \"0\",\n \"boolean\": \"false\",\n \"date\": \"Date.today()\",\n \"datetime\": \"Datetime.now()\",\n \"id\": \"null\",\n }\n return defaults.get(type_name, \"null\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13676,"content_sha256":"a0e411452b456a335d865661df24d3bc030a72b0bcfb784ada4caaa27b00ed94"},{"filename":"scripts/generators/apex_test_stub.py","content":"\"\"\"Generate @isTest companion classes for scaffolded Apex InvocableMethod actions.\"\"\"\n\nfrom __future__ import annotations\n\nfrom scripts.generators.apex_stub import _TYPE_MAP, _COMPLEX_TYPE_MAP, generate_apex_meta_xml\n\n\n# Type-appropriate placeholder values for test data\n_TEST_PLACEHOLDERS = {\n \"String\": \"'test'\",\n \"Decimal\": \"1\",\n \"Integer\": \"1\",\n \"Double\": \"1.0\",\n \"Boolean\": \"true\",\n \"Date\": \"Date.today()\",\n \"Datetime\": \"Datetime.now()\",\n \"Id\": \"null\",\n \"Map\u003cString, Object>\": \"new Map\u003cString, Object>()\",\n \"List\u003cObject>\": \"new List\u003cObject>()\",\n \"SObject\": \"new Account()\",\n}\n\n\ndef _placeholder_for_type(apex_type: str) -> str:\n \"\"\"Return a type-appropriate placeholder value for test data.\"\"\"\n return _TEST_PLACEHOLDERS.get(apex_type, \"null\")\n\n\ndef _resolve_apex_type(param: dict) -> str:\n \"\"\"Resolve a parameter dict to its Apex type string.\"\"\"\n return _COMPLEX_TYPE_MAP.get(\n param.get(\"complex_data_type_name\", \"\"),\n _TYPE_MAP.get(param.get(\"type\", \"string\"), \"String\"),\n )\n\n\ndef generate_apex_test_class(\n class_name: str,\n inputs: list[dict] | None = None,\n outputs: list[dict] | None = None,\n is_callout: bool = False,\n) -> str:\n \"\"\"Generate an @isTest companion class for an Apex InvocableMethod stub.\n\n Args:\n class_name: The production Apex class name (test class will be {class_name}Test).\n inputs: Action input definitions (list of dicts with 'name', 'type' keys).\n outputs: Action output definitions (list of dicts with 'name', 'type' keys).\n is_callout: If True, include HttpCalloutMock inner class.\n\n Returns:\n Apex test class source code string.\n \"\"\"\n inputs = inputs or []\n outputs = outputs or []\n test_name = f\"{class_name}Test\"\n\n lines = [\n \"@isTest\",\n f\"private class {test_name} {{\",\n ]\n\n # HttpCalloutMock inner class for callout actions\n if is_callout:\n lines.extend([\n \"\",\n \" private class MockHttpResponse implements HttpCalloutMock {\",\n \" public HttpResponse respond(HttpRequest req) {\",\n \" HttpResponse res = new HttpResponse();\",\n \" res.setHeader('Content-Type', 'application/json');\",\n \" res.setBody('{\\\"status\\\": \\\"ok\\\"}');\",\n \" res.setStatusCode(200);\",\n \" return res;\",\n \" }\",\n \" }\",\n ])\n\n # Test method\n lines.extend([\n \"\",\n \" @isTest\",\n \" static void testInvoke() {\",\n ])\n\n if is_callout:\n lines.append(\" Test.setMock(HttpCalloutMock.class, new MockHttpResponse());\")\n lines.append(\"\")\n\n # Build request\n lines.append(f\" {class_name}.Request req = new {class_name}.Request();\")\n for inp in inputs:\n apex_type = _resolve_apex_type(inp)\n placeholder = _placeholder_for_type(apex_type)\n lines.append(f\" req.{inp['name']} = {placeholder};\")\n\n # Invoke\n lines.extend([\n \"\",\n f\" List\u003c{class_name}.Response> results = {class_name}.invoke(\",\n f\" new List\u003c{class_name}.Request>{{ req }}\",\n \" );\",\n \"\",\n \" System.assertNotEquals(null, results, 'Expected non-null response');\",\n \" System.assertEquals(1, results.size(), 'Expected one response');\",\n ])\n\n # Assert outputs are populated\n if outputs:\n lines.append(\"\")\n lines.append(f\" {class_name}.Response resp = results[0];\")\n for out in outputs:\n apex_type = _resolve_apex_type(out)\n if apex_type == \"String\":\n lines.append(f\" System.assertNotEquals(null, resp.{out['name']}, 'Expected {out['name']} to be set');\")\n\n lines.extend([\n \" }\",\n \"}\",\n ])\n\n return \"\\n\".join(lines) + \"\\n\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3922,"content_sha256":"1f32d0e06ecb86fdec949d7885a1b1be5235f82ccef50da55f064332cac780b2"},{"filename":"scripts/generators/flow_xml.py","content":"\"\"\"Generate stub Flow XML for .agent file action targets.\"\"\"\n\nfrom __future__ import annotations\n\n# Mapping from .agent types to Flow variable dataTypes\n_TYPE_MAP = {\n \"string\": \"String\",\n \"number\": \"Number\",\n \"boolean\": \"Boolean\",\n \"date\": \"Date\",\n \"datetime\": \"DateTime\",\n \"id\": \"String\",\n \"object\": \"Apex\",\n}\n\n# Mapping from complex_data_type_name to Flow variable dataTypes (for action I/O)\n_COMPLEX_TYPE_MAP = {\n \"lightning__integerType\": \"Number\",\n \"lightning__numberType\": \"Number\",\n \"lightning__doubleType\": \"Number\",\n \"lightning__currencyType\": \"Currency\",\n \"lightning__dateTimeStringType\": \"DateTime\",\n \"lightning__recordInfoType\": \"SObject\",\n \"lightning__objectType\": \"Apex\",\n \"lightning__listType\": \"Apex\",\n \"lightning__textType\": \"String\",\n}\n\nAPI_VERSION = \"63.0\"\n\n\ndef generate_flow_xml(\n api_name: str,\n inputs: list[dict] | None = None,\n outputs: list[dict] | None = None,\n process_type: str = \"AutoLaunchedFlow\",\n) -> str:\n \"\"\"Generate a stub Flow XML with matching input/output variables.\n\n Produces a minimal .flow-meta.xml that:\n - Declares input variables matching action inputs\n - Declares output variables matching action outputs\n - Merges bidirectional variables (isInput=true, isOutput=true)\n - Uses Active status so flows are immediately callable\n - Has a single Assignment element as placeholder logic\n\n Args:\n api_name: The flow API name.\n inputs: Action input definitions (list of dicts with 'name', 'type' keys).\n outputs: Action output definitions (list of dicts with 'name', 'type' keys).\n process_type: Flow process type (default: AutoLaunchedFlow).\n\n Returns:\n Flow XML string.\n \"\"\"\n inputs = inputs or []\n outputs = outputs or []\n\n input_names = {inp[\"name\"] for inp in inputs}\n bidirectional_names = input_names & {out[\"name\"] for out in outputs}\n\n lines = [\n '\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '\u003cFlow xmlns=\"http://soap.sforce.com/2006/04/metadata\">',\n f' \u003capiVersion>{API_VERSION}\u003c/apiVersion>',\n f' \u003clabel>{api_name.replace(\"_\", \" \")}\u003c/label>',\n f' \u003cprocessType>{process_type}\u003c/processType>',\n ' \u003cstatus>Active\u003c/status>',\n ' \u003cinterviewLabel>{!$Flow.CurrentDateTime}\u003c/interviewLabel>',\n ]\n\n # Input variables\n for inp in inputs:\n flow_type = _COMPLEX_TYPE_MAP.get(inp.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(inp.get(\"type\", \"string\"), \"String\"))\n is_output = inp[\"name\"] in bidirectional_names\n lines.extend([\n ' \u003cvariables>',\n f' \u003cname>{inp[\"name\"]}\u003c/name>',\n f' \u003cdataType>{flow_type}\u003c/dataType>',\n ])\n if flow_type == \"Number\":\n lines.append(f' \u003cscale>{_infer_scale(inp[\"name\"])}\u003c/scale>')\n lines.extend([\n ' \u003cisCollection>false\u003c/isCollection>',\n ' \u003cisInput>true\u003c/isInput>',\n f' \u003cisOutput>{\"true\" if is_output else \"false\"}\u003c/isOutput>',\n ])\n if inp.get(\"description\"):\n lines.append(f' \u003cdescription>{_escape_xml(inp[\"description\"])}\u003c/description>')\n lines.append(' \u003c/variables>')\n\n # Output-only variables\n for out in outputs:\n if out[\"name\"] in bidirectional_names:\n continue\n flow_type = _COMPLEX_TYPE_MAP.get(out.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(out.get(\"type\", \"string\"), \"String\"))\n lines.extend([\n ' \u003cvariables>',\n f' \u003cname>{out[\"name\"]}\u003c/name>',\n f' \u003cdataType>{flow_type}\u003c/dataType>',\n ])\n if flow_type == \"Number\":\n lines.append(f' \u003cscale>{_infer_scale(out[\"name\"])}\u003c/scale>')\n lines.extend([\n ' \u003cisCollection>false\u003c/isCollection>',\n ' \u003cisInput>false\u003c/isInput>',\n ' \u003cisOutput>true\u003c/isOutput>',\n ])\n if out.get(\"description\"):\n lines.append(f' \u003cdescription>{_escape_xml(out[\"description\"])}\u003c/description>')\n lines.append(' \u003c/variables>')\n\n # Placeholder variable if no outputs\n if not outputs:\n lines.extend([\n ' \u003cvariables>',\n ' \u003cname>placeholder_result\u003c/name>',\n ' \u003cdataType>String\u003c/dataType>',\n ' \u003cisCollection>false\u003c/isCollection>',\n ' \u003cisInput>false\u003c/isInput>',\n ' \u003cisOutput>true\u003c/isOutput>',\n ' \u003c/variables>',\n ])\n\n # Placeholder assignment\n lines.extend([\n ' \u003cassignments>',\n ' \u003cname>Placeholder_Assignment\u003c/name>',\n ' \u003clabel>Placeholder Assignment\u003c/label>',\n ' \u003clocationX>176\u003c/locationX>',\n ' \u003clocationY>158\u003c/locationY>',\n ])\n\n for out in outputs:\n flow_type = _COMPLEX_TYPE_MAP.get(out.get(\"complex_data_type_name\", \"\"), _TYPE_MAP.get(out.get(\"type\", \"string\"), \"String\"))\n lines.extend([\n ' \u003cassignmentItems>',\n f' \u003cassignToReference>{out[\"name\"]}\u003c/assignToReference>',\n ' \u003coperator>Assign\u003c/operator>',\n f' \u003cvalue>{_default_value_element_by_flow_type(flow_type)}\u003c/value>',\n ' \u003c/assignmentItems>',\n ])\n\n if not outputs:\n lines.extend([\n ' \u003cassignmentItems>',\n ' \u003cassignToReference>placeholder_result\u003c/assignToReference>',\n ' \u003coperator>Assign\u003c/operator>',\n ' \u003cvalue>\u003cstringValue>TODO\u003c/stringValue>\u003c/value>',\n ' \u003c/assignmentItems>',\n ])\n\n lines.extend([\n ' \u003c/assignments>',\n ' \u003cstart>',\n ' \u003clocationX>50\u003c/locationX>',\n ' \u003clocationY>0\u003c/locationY>',\n ' \u003cconnector>',\n ' \u003ctargetReference>Placeholder_Assignment\u003c/targetReference>',\n ' \u003c/connector>',\n ' \u003c/start>',\n '\u003c/Flow>',\n ])\n\n return \"\\n\".join(lines) + \"\\n\"\n\n\ndef _infer_scale(name: str) -> int:\n \"\"\"Infer decimal scale from variable name. Currency/amount/price → 2, else 0.\"\"\"\n currency_hints = {\"balance\", \"amount\", \"price\", \"cost\", \"total\", \"credit\", \"fee\", \"rate\", \"pct\", \"percent\", \"utilization\"}\n lower = name.lower()\n for hint in currency_hints:\n if hint in lower:\n return 2\n return 0\n\n\ndef _escape_xml(text: str) -> str:\n \"\"\"Escape XML special characters.\"\"\"\n return (\n text\n .replace(\"&\", \"&\")\n .replace(\"\u003c\", \"<\")\n .replace(\">\", \">\")\n .replace('\"', \""\")\n .replace(\"'\", \"'\")\n )\n\n\ndef _default_value_element(type_name: str) -> str:\n \"\"\"Return a type-appropriate XML value element for a placeholder assignment.\"\"\"\n if type_name == \"boolean\":\n return \"\u003cbooleanValue>false\u003c/booleanValue>\"\n if type_name == \"number\":\n return \"\u003cnumberValue>0\u003c/numberValue>\"\n if type_name == \"date\":\n return \"\u003cstringValue>2000-01-01\u003c/stringValue>\"\n if type_name == \"datetime\":\n return \"\u003cstringValue>2000-01-01T00:00:00Z\u003c/stringValue>\"\n return \"\u003cstringValue>TODO\u003c/stringValue>\"\n\n\ndef _default_value_element_by_flow_type(flow_type: str) -> str:\n \"\"\"Return a type-appropriate XML value element based on resolved Flow dataType.\"\"\"\n if flow_type == \"Boolean\":\n return \"\u003cbooleanValue>false\u003c/booleanValue>\"\n if flow_type in (\"Number\", \"Currency\"):\n return \"\u003cnumberValue>0\u003c/numberValue>\"\n if flow_type == \"Date\":\n return \"\u003cstringValue>2000-01-01\u003c/stringValue>\"\n if flow_type == \"DateTime\":\n return \"\u003cstringValue>2000-01-01T00:00:00Z\u003c/stringValue>\"\n return \"\u003cstringValue>TODO\u003c/stringValue>\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7822,"content_sha256":"d7dc486a4df8e7db88c972b19f48801736939bff327de1c6650cfd9b9a19930f"},{"filename":"scripts/generators/permission_set_xml.py","content":"\"\"\"Generate Permission Set XML granting classAccesses to Apex classes.\"\"\"\n\nfrom __future__ import annotations\n\n\ndef generate_permission_set_xml(\n perm_set_name: str,\n apex_class_names: list[str],\n) -> str:\n \"\"\"Generate a Permission Set XML with classAccesses entries.\n\n Args:\n perm_set_name: The permission set API name (e.g. Agent_Action_Access).\n apex_class_names: List of Apex class names to grant access to.\n\n Returns:\n Permission Set XML string.\n \"\"\"\n lines = [\n '\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '\u003cPermissionSet xmlns=\"http://soap.sforce.com/2006/04/metadata\">',\n f\" \u003clabel>{perm_set_name}\u003c/label>\",\n ]\n\n for class_name in apex_class_names:\n lines.extend([\n \" \u003cclassAccesses>\",\n f\" \u003capexClass>{class_name}\u003c/apexClass>\",\n \" \u003cenabled>true\u003c/enabled>\",\n \" \u003c/classAccesses>\",\n ])\n\n lines.append(\"\u003c/PermissionSet>\")\n\n return \"\\n\".join(lines) + \"\\n\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1014,"content_sha256":"254c46e63dc18d938adf03466fd55b4cf21691409e91e38046e85a43f564d618"},{"filename":"scripts/generators/remote_site_xml.py","content":"\"\"\"Generate Remote Site Settings XML for HTTP callout targets.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\n\n\ndef safe_domain_name(domain: str) -> str:\n \"\"\"Sanitize a domain for use as a Remote Site Setting name.\n\n Replaces dots and hyphens with underscores.\n e.g. 'api.github.com' -> 'api_github_com'\n \"\"\"\n return re.sub(r\"[.\\-]\", \"_\", domain)\n\n\ndef generate_remote_site_xml(domain: str, description: str = \"\") -> str:\n \"\"\"Generate a .remoteSite-meta.xml file for a Remote Site Setting.\n\n Args:\n domain: The domain to allow (e.g. 'api.github.com').\n description: Human-readable description.\n\n Returns:\n Remote Site Setting XML string.\n \"\"\"\n url = f\"https://{domain}\"\n if not description:\n description = f\"Remote site for {domain}\"\n\n return (\n '\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n'\n '\u003cRemotesiteSetting xmlns=\"http://soap.sforce.com/2006/04/metadata\">\\n'\n f' \u003cdisableProtocolSecurity>false\u003c/disableProtocolSecurity>\\n'\n f' \u003cdescription>{_escape_xml(description)}\u003c/description>\\n'\n f' \u003cisActive>true\u003c/isActive>\\n'\n f' \u003curl>{url}\u003c/url>\\n'\n '\u003c/RemotesiteSetting>\\n'\n )\n\n\ndef _escape_xml(text: str) -> str:\n \"\"\"Escape special characters for XML content.\"\"\"\n return (text\n .replace(\"&\", \"&\")\n .replace(\"\u003c\", \"<\")\n .replace(\">\", \">\")\n .replace('\"', \""\")\n .replace(\"'\", \"'\"))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1467,"content_sha256":"87ae9e64d57d931df1e8f56c88e5fd45ec5194fca9a54d203f971136aecce419"},{"filename":"scripts/index-agent.mjs","content":"#!/usr/bin/env node\n\nimport fs from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\nimport { loadAgentScriptSdk } from \"./agentscript-sdk-loader.mjs\"\n\nlet sdk\ntry {\n sdk = await loadAgentScriptSdk()\n} catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(2)\n}\nconst { compileSource } = sdk.sdk\n\nconst file = process.argv[2]\nif (!file || process.argv.length > 3) {\n console.error(\"Usage: node scripts/index-agent.mjs \u003cpath-to-agent-file>\")\n process.exit(2)\n}\n\nconst absoluteFile = path.resolve(file)\nconst source = fs.readFileSync(absoluteFile, \"utf8\")\nconst result = compileSource(source)\nconst version = result.output.agent_version\n\nfunction rangeOf(node) {\n const range = node?.__cst?.range\n if (!range) return undefined\n return {\n startLine: range.start.line + 1,\n endLine: range.end.line + 1,\n }\n}\n\nfunction namedRanges(collection) {\n if (!collection?.__children) return []\n return collection.__children.map((child) => ({\n name: child.name,\n kind: child.value?.__kind,\n ...rangeOf(child.value),\n }))\n}\n\nconst sourceNodes = new Map(\n [\n ...namedRanges(result.document.ast.start_agent),\n ...namedRanges(result.document.ast.subagent),\n ].map((node) => [node.name, node]),\n)\n\nconst lifecycleNames = [\n \"before_reasoning\",\n \"before_reasoning_iteration\",\n \"after_reasoning\",\n]\n\nconst nodes = (version?.nodes ?? []).map((node) => {\n const lifecycle = {}\n for (const name of lifecycleNames) {\n if (Array.isArray(node[name]) && node[name].length > 0) {\n lifecycle[name] = node[name].map((step) => ({\n type: step.type,\n target: step.target,\n enabled: step.enabled,\n }))\n }\n }\n\n return {\n name: node.developer_name,\n type: node.type,\n reasoningType: node.reasoning_type,\n source: sourceNodes.get(node.developer_name),\n tools: (node.tools ?? []).map((tool) => ({\n name: tool.name,\n target: tool.target,\n enabled: tool.enabled,\n llmInputs: tool.llm_inputs ?? [],\n stateUpdates: tool.state_updates ?? [],\n })),\n actionDefinitions: (node.action_definitions ?? []).map((action) => ({\n name: action.developer_name,\n targetType: action.invocation_target_type,\n targetName: action.invocation_target_name,\n inputs: (action.input_type ?? []).map((input) => input.developer_name),\n outputs: (action.output_type ?? []).map(\n (output) => output.developer_name,\n ),\n })),\n lifecycle,\n }\n})\n\nconst index = {\n compiler: {\n provider: sdk.provider,\n package: sdk.package,\n version: sdk.version,\n sourceRevision: sdk.sourceRevision,\n entry: sdk.entry,\n },\n file: absoluteFile,\n diagnostics: result.diagnostics.map((diagnostic) => ({\n severity: diagnostic.severity,\n code: diagnostic.code,\n message: diagnostic.message,\n line: diagnostic.range.start.line + 1,\n column: diagnostic.range.start.character + 1,\n })),\n blocks: Object.entries(result.document.ast)\n .filter(([name, value]) => !name.startsWith(\"__\") && value?.__cst)\n .map(([name, value]) => ({ name, ...rangeOf(value) })),\n variables: (version?.state_variables ?? []).map((variable) => ({\n name: variable.developer_name,\n type: variable.data_type,\n isList: variable.is_list,\n visibility: variable.visibility,\n default: variable.default,\n })),\n initialNode: version?.initial_node,\n nodes,\n transitions: nodes.flatMap((node) =>\n Object.entries(node.lifecycle).flatMap(([lifecycle, steps]) =>\n steps\n .filter((step) => step.type === \"handoff\")\n .map((step) => ({\n from: node.name,\n to: step.target,\n lifecycle,\n enabled: step.enabled,\n })),\n ),\n ),\n}\n\nconsole.log(JSON.stringify(index, null, 2))\nprocess.exit(\n result.diagnostics.some((diagnostic) => diagnostic.severity === 1) ? 1 : 0,\n)\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":3893,"content_sha256":"2498a6a555637ddfc0c44bdc1d55e62039f66f60b54309453be0767d32880e4c"},{"filename":"scripts/org_describe.py","content":"#!/usr/bin/env python3\n\"\"\"Query SObject field definitions and match them to action inputs/outputs for smart code generation.\n\nUsage:\n python3 scripts/org_describe.py --sobject Account -o OrgAlias\n python3 scripts/org_describe.py --sobject Case -o OrgAlias --json\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport difflib\nimport json\nimport re\nimport subprocess\nimport sys\nfrom dataclasses import dataclass, field\n\n\n@dataclass\nclass FieldInfo:\n \"\"\"SObject field metadata.\"\"\"\n name: str\n label: str\n data_type: str\n filterable: bool\n\n\n@dataclass\nclass FieldMapping:\n \"\"\"Mapping between action inputs/outputs and SObject fields.\"\"\"\n input_mappings: dict[str, str] = field(default_factory=dict) # input_name → field_name\n output_mappings: dict[str, str] = field(default_factory=dict) # output_name → field_name\n select_fields: list[str] = field(default_factory=list)\n where_fields: list[str] = field(default_factory=list)\n\n\ndef describe_sobject(object_name: str, target_org: str) -> list[FieldInfo]:\n \"\"\"Query FieldDefinition for an SObject's fields.\"\"\"\n query = (\n f\"SELECT QualifiedApiName, Label, DataType, IsCompactLayoutable \"\n f\"FROM FieldDefinition \"\n f\"WHERE EntityDefinition.QualifiedApiName = '{object_name}'\"\n )\n cmd = [\"sf\", \"data\", \"query\", \"--query\", query, \"-o\", target_org, \"--json\"]\n try:\n proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)\n if proc.returncode != 0:\n return []\n data = json.loads(proc.stdout)\n records = data.get(\"result\", {}).get(\"records\", [])\n return [\n FieldInfo(\n name=r.get(\"QualifiedApiName\", \"\"),\n label=r.get(\"Label\", \"\"),\n data_type=r.get(\"DataType\", \"\"),\n filterable=r.get(\"IsCompactLayoutable\", False),\n )\n for r in records\n if r.get(\"QualifiedApiName\")\n ]\n except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):\n return []\n\n\ndef match_fields(\n inputs: list[dict],\n outputs: list[dict],\n fields: list[FieldInfo],\n) -> FieldMapping:\n \"\"\"Match action inputs/outputs to SObject fields.\n\n Args:\n inputs: List of dicts with 'name' and 'type' keys.\n outputs: List of dicts with 'name' and 'type' keys.\n fields: SObject field definitions from describe_sobject().\n\n Returns:\n FieldMapping with matched input/output mappings.\n \"\"\"\n mapping = FieldMapping()\n\n # Build field lookup\n field_names = [f.name for f in fields]\n filterable_fields = [f.name for f in fields if f.filterable]\n\n # Match inputs to filterable fields (for WHERE clause)\n for inp in inputs:\n if _is_computed_output(inp[\"name\"]):\n continue\n best = _find_best_match(inp[\"name\"], filterable_fields)\n if best:\n mapping.input_mappings[inp[\"name\"]] = best\n mapping.where_fields.append(best)\n\n # Match outputs to any fields (for SELECT clause)\n for out in outputs:\n if _is_computed_output(out[\"name\"]):\n continue\n best = _find_best_match(out[\"name\"], field_names)\n if best:\n mapping.output_mappings[out[\"name\"]] = best\n mapping.select_fields.append(best)\n\n return mapping\n\n\ndef _find_best_match(name: str, candidates: list[str], threshold: float = 0.5) -> str | None:\n \"\"\"Find the best matching field name using fuzzy matching.\"\"\"\n normalized = _normalize(name)\n\n # Exact normalized match\n for cand in candidates:\n if _normalize(cand) == normalized:\n return cand\n\n # Fuzzy sequence matching\n best_score = 0.0\n best_match = None\n for cand in candidates:\n score = difflib.SequenceMatcher(None, normalized, _normalize(cand)).ratio()\n # Bonus for word containment\n if normalized in _normalize(cand) or _normalize(cand) in normalized:\n score += 0.2\n if score > best_score and score >= threshold:\n best_score = score\n best_match = cand\n\n return best_match\n\n\ndef _normalize(name: str) -> str:\n \"\"\"Normalize a field name for comparison.\"\"\"\n # Remove __c suffix\n name = re.sub(r\"__c$\", \"\", name)\n # Split camelCase\n name = re.sub(r\"(?\u003c=[a-z])(?=[A-Z])\", \" \", name)\n # Replace underscores with spaces\n name = name.replace(\"_\", \" \")\n return name.lower().strip()\n\n\ndef _is_computed_output(name: str) -> bool:\n \"\"\"Detect computed output names that shouldn't be mapped to fields.\"\"\"\n computed_patterns = [\n \"total_count\", \"result_json\", \"error_message\", \"status_code\",\n \"success\", \"is_valid\", \"record_count\",\n ]\n return name.lower() in computed_patterns or name.endswith(\"_count\") or name.endswith(\"_json\")\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Describe SObject fields\")\n parser.add_argument(\"--sobject\", required=True, help=\"SObject API name (e.g. Account, Case)\")\n parser.add_argument(\"-o\", \"--target-org\", required=True, help=\"Salesforce org alias\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n args = parser.parse_args()\n\n fields = describe_sobject(args.sobject, args.target_org)\n if not fields:\n print(f\"No fields found for {args.sobject}\", file=sys.stderr)\n sys.exit(1)\n\n if args.json:\n print(json.dumps([{\"name\": f.name, \"label\": f.label, \"type\": f.data_type, \"filterable\": f.filterable} for f in fields], indent=2))\n else:\n print(f\"\\n{args.sobject} — {len(fields)} fields\\n\")\n print(f\"{'Field':\u003c40} {'Label':\u003c30} {'Type':\u003c15} {'Filter'}\")\n print(f\"{'-'*40} {'-'*30} {'-'*15} {'-'*6}\")\n for f in fields:\n filt = \"✓\" if f.filterable else \"\"\n print(f\"{f.name:\u003c40} {f.label:\u003c30} {f.data_type:\u003c15} {filt}\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5928,"content_sha256":"d9ce5762325bde7102e4d2c4f1b3da8395fc6e7922321473c3eedeacea8e5761"},{"filename":"scripts/scaffold.py","content":"#!/usr/bin/env python3\n\"\"\"Generate metadata stubs (Flow XML, Apex classes) for missing .agent file targets.\n\nReads discovery report or .agent file, generates stubs for targets not found in the org.\n\nUsage:\n python3 scripts/scaffold.py --agent-file path/to/Agent.agent -o OrgAlias --output-dir force-app/main/default\n python3 scripts/scaffold.py --agent-file path/to/Agent.agent --all --output-dir force-app/main/default\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport re\nimport sys\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\n# Ensure the parent directory is in sys.path for package imports\n_repo_root = Path(__file__).resolve().parent.parent\nif str(_repo_root) not in sys.path:\n sys.path.insert(0, str(_repo_root))\n\nfrom scripts.discover import DiscoveryReport, TargetStatus, discover, extract_actions\nfrom scripts.generators.flow_xml import generate_flow_xml\nfrom scripts.generators.apex_stub import generate_apex_class, generate_apex_meta_xml, generate_callout_apex_class, generate_soql_apex_class\nfrom scripts.generators.apex_test_stub import generate_apex_test_class\nfrom scripts.generators.permission_set_xml import generate_permission_set_xml\nfrom scripts.generators.remote_site_xml import generate_remote_site_xml, safe_domain_name\nfrom scripts.org_describe import describe_sobject, match_fields\n\n\n# --- Action Classification ---\n\n# Patterns that signal HTTP callout actions\n_CALLOUT_PATTERNS = re.compile(\n r'\\b(api|http|https|rest|soap|external|callout|webhook|endpoint)\\b'\n r'|https?://[^\\s\"\\']+',\n re.IGNORECASE,\n)\n\n# Patterns that signal SOQL/record-based actions\n_SOQL_PATTERNS = re.compile(\n r'\\b(soql|query|record|sobject|object|lookup|search|find|get\\s+record)\\b'\n r'|__c\\b',\n re.IGNORECASE,\n)\n\n# Patterns that signal auth/credential needs\n_AUTH_PATTERNS = re.compile(\n r'\\b(api\\s*key|bearer|token|auth|credential|secret|oauth)\\b',\n re.IGNORECASE,\n)\n\n# Pattern to extract domains from descriptions\n_DOMAIN_PATTERN = re.compile(\n r'https?://([a-zA-Z0-9][-a-zA-Z0-9]*(?:\\.[a-zA-Z0-9][-a-zA-Z0-9]*)+)',\n)\n\n\ndef _sanitize_apex_class_name(target_name: str) -> str:\n \"\"\"Convert an apex target name to a valid Salesforce Apex class name.\n\n Apex class names cannot contain dots. For targets like\n 'TargetService.searchProducts', convert to 'TargetServiceSearchProducts'.\n For 'utils.setVariables', convert to 'UtilsSetVariables'.\n \"\"\"\n if \".\" not in target_name:\n return target_name\n parts = target_name.split(\".\")\n # PascalCase each part and join\n return \"\".join(p[0].upper() + p[1:] if p else \"\" for p in parts)\n\n\ndef classify_action(action_def: dict) -> str:\n \"\"\"Classify an action to determine scaffold strategy.\n\n Examines the action description and name for signals:\n - 'callout': HTTP/REST/external API patterns detected\n - 'soql': SObject/query patterns detected\n - 'basic': No special signals\n\n Args:\n action_def: Action definition dict with 'name', 'description', etc.\n\n Returns:\n Classification string: 'callout', 'soql', or 'basic'.\n \"\"\"\n text = \" \".join([\n action_def.get(\"description\", \"\"),\n action_def.get(\"name\", \"\"),\n ])\n\n if _CALLOUT_PATTERNS.search(text):\n return \"callout\"\n if _SOQL_PATTERNS.search(text):\n return \"soql\"\n return \"basic\"\n\n\ndef _extract_domains(text: str) -> list[str]:\n \"\"\"Extract domain names from a text string.\"\"\"\n return list(set(_DOMAIN_PATTERN.findall(text)))\n\n\ndef _needs_auth_metadata(text: str) -> bool:\n \"\"\"Check if the action description suggests auth/credential needs.\"\"\"\n return bool(_AUTH_PATTERNS.search(text))\n\n\n@dataclass\nclass ScaffoldResult:\n \"\"\"Result of scaffolding operations.\"\"\"\n files_created: list[Path] = field(default_factory=list)\n warnings: list[str] = field(default_factory=list)\n\n\ndef scaffold(\n report: DiscoveryReport,\n output_dir: Path,\n agent_file: Path | None = None,\n target_org: str | None = None,\n) -> ScaffoldResult:\n \"\"\"Generate stubs for all missing targets in a discovery report.\n\n Args:\n report: Discovery report with found/missing targets.\n output_dir: Base output directory (e.g. force-app/main/default).\n agent_file: Path to .agent file (for extracting action I/O definitions).\n target_org: Org alias (for smart scaffold with SObject field mapping).\n\n Returns:\n ScaffoldResult with created files and warnings.\n \"\"\"\n result = ScaffoldResult()\n\n # Load action definitions from .agent file for input/output info\n actions = {}\n if agent_file and agent_file.exists():\n for action in extract_actions(agent_file):\n if action.get(\"target_name\"):\n actions[action[\"target_name\"]] = action\n\n # Derive agent name for permission set naming\n agent_name = \"\"\n if agent_file:\n agent_name = agent_file.stem # e.g. \"OrderService\"\n\n apex_classes = []\n seen_targets = set()\n seen_retriever_warnings = set()\n\n for target in report.missing:\n # Deduplicate: same target can appear in multiple topics\n if target.target in seen_targets:\n continue\n seen_targets.add(target.target)\n\n if target.target_type == \"flow\":\n _scaffold_flow(target, output_dir, actions, target_org, result)\n elif target.target_type == \"apex\":\n _scaffold_apex(target, output_dir, actions, target_org, result)\n apex_classes.append(_sanitize_apex_class_name(target.target_name))\n elif target.target_type == \"retriever\":\n if target.target_name not in seen_retriever_warnings:\n seen_retriever_warnings.add(target.target_name)\n result.warnings.append(\n f\"Retriever '{target.target_name}' must be created manually in \"\n f\"Setup → Data Cloud → Data Spaces → Knowledge\"\n )\n\n # Generate permission set if any Apex classes were scaffolded\n if apex_classes:\n perm_set_name = f\"{agent_name}_Action_Access\" if agent_name else \"Agent_Action_Access\"\n _scaffold_permission_set(apex_classes, output_dir, result, perm_set_name)\n\n return result\n\n\ndef scaffold_all(\n agent_file: Path,\n output_dir: Path,\n target_org: str | None = None,\n) -> ScaffoldResult:\n \"\"\"Scaffold all targets without org check (generate stubs for everything).\"\"\"\n from scripts.discover import extract_targets\n\n report = DiscoveryReport()\n for uri, ttype, tname in extract_targets(agent_file):\n report.targets.append(TargetStatus(\n agent_file=str(agent_file),\n target=uri,\n target_type=ttype,\n target_name=tname,\n found=False, # Treat all as missing\n ))\n\n return scaffold(report, output_dir, agent_file, target_org)\n\n\ndef _scaffold_flow(\n target: TargetStatus,\n output_dir: Path,\n actions: dict,\n target_org: str | None,\n result: ScaffoldResult,\n) -> None:\n \"\"\"Generate Flow XML stub.\"\"\"\n flow_dir = output_dir / \"flows\"\n flow_dir.mkdir(parents=True, exist_ok=True)\n\n action_def = actions.get(target.target_name, {})\n inputs = action_def.get(\"inputs\", [])\n outputs = action_def.get(\"outputs\", [])\n\n xml = generate_flow_xml(target.target_name, inputs, outputs)\n\n flow_path = flow_dir / f\"{target.target_name}.flow-meta.xml\"\n flow_path.write_text(xml, encoding=\"utf-8\")\n result.files_created.append(flow_path)\n\n\ndef _scaffold_apex(\n target: TargetStatus,\n output_dir: Path,\n actions: dict,\n target_org: str | None,\n result: ScaffoldResult,\n) -> None:\n \"\"\"Generate Apex class + test class + meta XMLs, with classification-aware output.\"\"\"\n classes_dir = output_dir / \"classes\"\n classes_dir.mkdir(parents=True, exist_ok=True)\n\n # Sanitize class name — dots are invalid in Apex class names\n cls_name = _sanitize_apex_class_name(target.target_name)\n\n action_def = actions.get(target.target_name, {})\n inputs = action_def.get(\"inputs\", [])\n outputs = action_def.get(\"outputs\", [])\n classification = classify_action(action_def)\n is_callout = classification == \"callout\"\n\n description_text = action_def.get(\"description\", \"\")\n\n # Main class — use classification-appropriate generator\n if is_callout:\n domains = _extract_domains(description_text)\n endpoint = f\"https://{domains[0]}\" if domains else \"https://example.com/api\"\n cls_code = generate_callout_apex_class(cls_name, inputs, outputs, endpoint, description=description_text)\n elif classification == \"soql\":\n cls_code = generate_soql_apex_class(cls_name, inputs, outputs, description=description_text)\n else:\n cls_code = generate_apex_class(cls_name, inputs, outputs, description=description_text)\n\n cls_path = classes_dir / f\"{cls_name}.cls\"\n cls_path.write_text(cls_code, encoding=\"utf-8\")\n result.files_created.append(cls_path)\n\n # Meta XML\n meta_xml = generate_apex_meta_xml()\n meta_path = classes_dir / f\"{cls_name}.cls-meta.xml\"\n meta_path.write_text(meta_xml, encoding=\"utf-8\")\n result.files_created.append(meta_path)\n\n # Test class — use the dedicated generator\n test_name = f\"{cls_name}Test\"\n test_code = generate_apex_test_class(cls_name, inputs, outputs, is_callout=is_callout)\n test_path = classes_dir / f\"{test_name}.cls\"\n test_path.write_text(test_code, encoding=\"utf-8\")\n result.files_created.append(test_path)\n\n # Test meta XML\n test_meta_path = classes_dir / f\"{test_name}.cls-meta.xml\"\n test_meta_path.write_text(meta_xml, encoding=\"utf-8\")\n result.files_created.append(test_meta_path)\n\n # Callout-specific artifacts\n if is_callout:\n # Remote Site Settings for discovered domains\n domains = _extract_domains(description_text)\n for domain in domains:\n _scaffold_remote_site(domain, description_text, output_dir, result)\n\n # Custom Metadata for auth if needed\n if _needs_auth_metadata(description_text):\n _scaffold_custom_metadata(cls_name, output_dir, result)\n\n\ndef _scaffold_remote_site(\n domain: str,\n description: str,\n output_dir: Path,\n result: ScaffoldResult,\n) -> None:\n \"\"\"Generate a Remote Site Setting for a callout domain.\"\"\"\n remote_dir = output_dir / \"remoteSiteSettings\"\n remote_dir.mkdir(parents=True, exist_ok=True)\n\n site_name = safe_domain_name(domain)\n xml = generate_remote_site_xml(domain, f\"Remote site for callout to {domain}\")\n site_path = remote_dir / f\"{site_name}.remoteSite-meta.xml\"\n site_path.write_text(xml, encoding=\"utf-8\")\n result.files_created.append(site_path)\n\n\ndef _scaffold_custom_metadata(\n action_name: str,\n output_dir: Path,\n result: ScaffoldResult,\n) -> None:\n \"\"\"Generate Custom Metadata Type + record for API key storage.\"\"\"\n cmd_dir = output_dir / \"customMetadata\"\n cmd_dir.mkdir(parents=True, exist_ok=True)\n\n type_name = f\"{action_name}_Config\"\n record_xml = (\n '\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n'\n '\u003cCustomMetadata xmlns=\"http://soap.sforce.com/2006/04/metadata\">\\n'\n f' \u003clabel>{action_name} Config Default\u003c/label>\\n'\n ' \u003cprotected>false\u003c/protected>\\n'\n ' \u003cvalues>\\n'\n ' \u003cfield>apikey__c\u003c/field>\\n'\n ' \u003cvalue xsi:type=\"xsd:string\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">REPLACE_WITH_ACTUAL_KEY\u003c/value>\\n'\n ' \u003c/values>\\n'\n '\u003c/CustomMetadata>\\n'\n )\n\n record_path = cmd_dir / f\"{type_name}.Default.md-meta.xml\"\n record_path.write_text(record_xml, encoding=\"utf-8\")\n result.files_created.append(record_path)\n\n # Also generate the Custom Metadata Type definition\n type_dir = output_dir / \"objects\" / f\"{type_name}__mdt\"\n type_dir.mkdir(parents=True, exist_ok=True)\n\n type_xml = (\n '\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n'\n '\u003cCustomObject xmlns=\"http://soap.sforce.com/2006/04/metadata\">\\n'\n f' \u003clabel>{action_name} Config\u003c/label>\\n'\n f' \u003cpluralLabel>{action_name} Configs\u003c/pluralLabel>\\n'\n ' \u003cvisibility>Public\u003c/visibility>\\n'\n '\u003c/CustomObject>\\n'\n )\n type_path = type_dir / f\"{type_name}__mdt.object-meta.xml\"\n type_path.write_text(type_xml, encoding=\"utf-8\")\n result.files_created.append(type_path)\n\n # Field definition for apikey__c\n fields_dir = type_dir / \"fields\"\n fields_dir.mkdir(parents=True, exist_ok=True)\n field_xml = (\n '\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n'\n '\u003cCustomField xmlns=\"http://soap.sforce.com/2006/04/metadata\">\\n'\n ' \u003cfullName>apikey__c\u003c/fullName>\\n'\n ' \u003cfieldManageability>SubscriberControlled\u003c/fieldManageability>\\n'\n ' \u003clabel>API Key\u003c/label>\\n'\n ' \u003clength>255\u003c/length>\\n'\n ' \u003ctype>Text\u003c/type>\\n'\n ' \u003cunique>false\u003c/unique>\\n'\n '\u003c/CustomField>\\n'\n )\n field_path = fields_dir / \"apikey__c.field-meta.xml\"\n field_path.write_text(field_xml, encoding=\"utf-8\")\n result.files_created.append(field_path)\n\n\ndef _scaffold_permission_set(\n apex_classes: list[str],\n output_dir: Path,\n result: ScaffoldResult,\n perm_set_name: str = \"Agent_Action_Access\",\n) -> None:\n \"\"\"Generate permission set granting access to scaffolded Apex classes.\"\"\"\n perm_dir = output_dir / \"permissionsets\"\n perm_dir.mkdir(parents=True, exist_ok=True)\n\n # Include both classes and their test classes\n all_classes = []\n for cls in apex_classes:\n all_classes.append(cls)\n all_classes.append(f\"{cls}Test\")\n\n xml = generate_permission_set_xml(perm_set_name, all_classes)\n perm_path = perm_dir / f\"{perm_set_name}.permissionset-meta.xml\"\n perm_path.write_text(xml, encoding=\"utf-8\")\n result.files_created.append(perm_path)\n\n\ndef print_result(result: ScaffoldResult) -> None:\n \"\"\"Print scaffold results.\"\"\"\n if result.files_created:\n print(f\"\\n✅ Created {len(result.files_created)} file(s):\")\n for f in result.files_created:\n print(f\" {f}\")\n\n if result.warnings:\n print(f\"\\n⚠️ Warnings:\")\n for w in result.warnings:\n print(f\" {w}\")\n\n print()\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Scaffold metadata stubs for missing .agent targets\")\n parser.add_argument(\"--agent-file\", type=Path, required=True, help=\"Path to .agent file\")\n parser.add_argument(\"-o\", \"--target-org\", help=\"Salesforce org alias (for discovery)\")\n parser.add_argument(\"--output-dir\", type=Path, default=Path(\"force-app/main/default\"), help=\"Output directory\")\n parser.add_argument(\"--all\", action=\"store_true\", help=\"Scaffold all targets (skip org check)\")\n args = parser.parse_args()\n\n if not args.agent_file.exists():\n print(f\"Error: {args.agent_file} not found\", file=sys.stderr)\n sys.exit(1)\n\n if args.all:\n result = scaffold_all(args.agent_file, args.output_dir, args.target_org)\n else:\n if not args.target_org:\n print(\"Error: --target-org required (or use --all to skip org check)\", file=sys.stderr)\n sys.exit(1)\n report = discover(args.agent_file, args.target_org)\n result = scaffold(report, args.output_dir, args.agent_file, args.target_org)\n\n print_result(result)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15432,"content_sha256":"92f99ed9bc93a940a1585c4d40144b800a9fbd172fdaa8692032ea1af7deacd4"},{"filename":"scripts/setup-agentscript-sdk.mjs","content":"#!/usr/bin/env node\n\nimport fs from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\nimport { spawnSync } from \"node:child_process\"\nimport { pathToFileURL } from \"node:url\"\nimport {\n PUBLIC_PACKAGE,\n PUBLIC_VERSION,\n SOURCE_REF,\n SOURCE_REPOSITORY,\n sdkCacheDirectory,\n sdkManifestPath,\n} from \"./agentscript-sdk-loader.mjs\"\n\nfunction usage() {\n console.error(\n \"Usage: node scripts/setup-agentscript-sdk.mjs [--npm | --source] [--version \u003cversion>] [--source-ref \u003cref>] [--cache-dir \u003cpath>] [--force] [--json]\",\n )\n}\n\nfunction parseArguments(argv) {\n const options = {\n provider: \"npm\",\n version: PUBLIC_VERSION,\n sourceRef: SOURCE_REF,\n force: false,\n json: false,\n }\n\n for (let index = 0; index \u003c argv.length; index += 1) {\n const argument = argv[index]\n if (argument === \"--npm\") options.provider = \"npm\"\n else if (argument === \"--source\") options.provider = \"source\"\n else if (argument === \"--force\") options.force = true\n else if (argument === \"--json\") options.json = true\n else if (argument === \"--version\") options.version = argv[++index]\n else if (argument === \"--source-ref\") options.sourceRef = argv[++index]\n else if (argument === \"--cache-dir\") options.cacheDirectory = argv[++index]\n else throw new Error(`Unknown argument: ${argument}`)\n }\n\n if (!options.version || !options.sourceRef) {\n throw new Error(\"--version and --source-ref require non-empty values\")\n }\n if (\n options.version !== \"latest\" &&\n !/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/.test(options.version)\n ) {\n throw new Error(\"--version must be an exact semantic version or latest\")\n }\n if (\n !/^[0-9A-Za-z][0-9A-Za-z._/-]{0,200}$/.test(options.sourceRef) ||\n options.sourceRef.includes(\"..\")\n ) {\n throw new Error(\"--source-ref must be a branch, tag, or commit name\")\n }\n return options\n}\n\nfunction run(command, args, options = {}) {\n const result = spawnSync(command, args, {\n cwd: options.cwd,\n encoding: \"utf8\",\n env: process.env,\n stdio: options.progress\n ? [\"ignore\", process.stderr, process.stderr]\n : \"pipe\",\n })\n if (result.error) throw result.error\n if (result.status !== 0) {\n const detail = (result.stderr || result.stdout || \"\").trim()\n throw new Error(`${command} failed (${result.status}): ${detail}`)\n }\n return (result.stdout || \"\").trim()\n}\n\nfunction assertManagedPath(target, cacheDirectory) {\n const relative = path.relative(cacheDirectory, target)\n if (!relative || relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n throw new Error(`Refusing to modify path outside SDK cache: ${target}`)\n }\n}\n\nfunction replaceDirectory(target, replacement, cacheDirectory) {\n assertManagedPath(target, cacheDirectory)\n if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true })\n fs.mkdirSync(path.dirname(target), { recursive: true })\n fs.renameSync(replacement, target)\n}\n\nasync function verifyCompiler(entry) {\n const sdk = await import(`${pathToFileURL(entry).href}?smoke=${Date.now()}`)\n if (typeof sdk.compileSource !== \"function\") {\n throw new Error(`SDK entry does not export compileSource(): ${entry}`)\n }\n\n const result = sdk.compileSource(`\nsystem:\n instructions: \"Answer questions about store hours.\"\n\nconfig:\n developer_name: \"Compiler_Smoke_Test\"\n agent_label: \"Compiler Smoke Test\"\n description: \"Validates the installed AgentScript SDK.\"\n\nstart_agent store_hours:\n label: \"Store Hours\"\n description: \"Answers store-hours questions.\"\n reasoning:\n instructions: ->\n | Answer the latest store-hours question.\n`)\n const errors = (result.diagnostics || []).filter((item) => item.severity \u003c= 2)\n if (errors.length > 0 || !result.output?.global_configuration) {\n throw new Error(\"Installed SDK failed its AgentScript compiler smoke test\")\n }\n}\n\nfunction writeManifest(cacheDirectory, manifest) {\n const manifestPath = sdkManifestPath(cacheDirectory)\n fs.mkdirSync(path.dirname(manifestPath), { recursive: true })\n const temporary = `${manifestPath}.${process.pid}.tmp`\n fs.writeFileSync(temporary, `${JSON.stringify(manifest, null, 2)}\\n`)\n fs.renameSync(temporary, manifestPath)\n return manifestPath\n}\n\nasync function installFromNpm(options, cacheDirectory) {\n const installRoot = path.join(\n cacheDirectory,\n \"agentscript-sdk\",\n \"npm\",\n `${PUBLIC_PACKAGE.replaceAll(\"/\", \"-\").replaceAll(\"@\", \"\")}-${options.version}`,\n )\n const packageFile = path.join(installRoot, \"package.json\")\n\n if (options.force && fs.existsSync(installRoot)) {\n assertManagedPath(installRoot, cacheDirectory)\n fs.rmSync(installRoot, { recursive: true, force: true })\n }\n fs.mkdirSync(installRoot, { recursive: true })\n if (!fs.existsSync(packageFile)) {\n fs.writeFileSync(\n packageFile,\n `${JSON.stringify({ private: true, type: \"module\" }, null, 2)}\\n`,\n )\n }\n\n const packageSpec = `${PUBLIC_PACKAGE}@${options.version}`\n if (options.force || !fs.existsSync(path.join(installRoot, \"node_modules\"))) {\n console.error(`Installing ${packageSpec} in the AgentScript SDK cache...`)\n run(\n \"npm\",\n [\n \"install\",\n \"--ignore-scripts\",\n \"--no-audit\",\n \"--no-fund\",\n \"--save-exact\",\n packageSpec,\n ],\n { cwd: installRoot, progress: true },\n )\n }\n\n const installedRoot = path.join(\n installRoot,\n \"node_modules\",\n \"@sf-agentscript\",\n \"agentforce\",\n )\n const installedPackage = JSON.parse(\n fs.readFileSync(path.join(installedRoot, \"package.json\"), \"utf8\"),\n )\n const entry = path.join(installedRoot, installedPackage.exports[\".\"].import)\n await verifyCompiler(entry)\n return {\n provider: \"npm\",\n package: PUBLIC_PACKAGE,\n requestedVersion: options.version,\n version: installedPackage.version,\n entry,\n }\n}\n\nfunction pnpmCommand() {\n const direct = spawnSync(\"pnpm\", [\"--version\"], { encoding: \"utf8\" })\n if (direct.status === 0) return [\"pnpm\", []]\n\n const corepack = spawnSync(\"corepack\", [\"pnpm\", \"--version\"], {\n encoding: \"utf8\",\n })\n if (corepack.status === 0) return [\"corepack\", [\"pnpm\"]]\n throw new Error(\"Source builds require pnpm or Corepack with pnpm enabled\")\n}\n\nasync function installFromSource(options, cacheDirectory) {\n const sourceRoot = path.join(\n cacheDirectory,\n \"agentscript-sdk\",\n \"source\",\n \"checkout\",\n )\n const temporary = path.join(\n cacheDirectory,\n \"agentscript-sdk\",\n \"source\",\n `checkout-${process.pid}.tmp`,\n )\n assertManagedPath(temporary, cacheDirectory)\n\n if (fs.existsSync(sourceRoot) && !options.force) {\n const entry = path.join(\n sourceRoot,\n \"packages\",\n \"agentforce\",\n \"dist\",\n \"index.js\",\n )\n if (!fs.existsSync(entry)) {\n throw new Error(\n \"Cached source checkout is incomplete; rerun with --source --force\",\n )\n }\n await verifyCompiler(entry)\n return {\n provider: \"source\",\n repository: SOURCE_REPOSITORY,\n requestedRef: options.sourceRef,\n sourceRevision: run(\"git\", [\"rev-parse\", \"HEAD\"], { cwd: sourceRoot }),\n entry,\n }\n }\n\n if (fs.existsSync(temporary))\n fs.rmSync(temporary, { recursive: true, force: true })\n fs.mkdirSync(temporary, { recursive: true })\n try {\n console.error(`Fetching ${SOURCE_REPOSITORY} at ${options.sourceRef}...`)\n run(\"git\", [\"init\", \"--quiet\"], { cwd: temporary })\n run(\"git\", [\"remote\", \"add\", \"origin\", SOURCE_REPOSITORY], {\n cwd: temporary,\n })\n run(\n \"git\",\n [\"fetch\", \"--quiet\", \"--depth\", \"1\", \"origin\", options.sourceRef],\n {\n cwd: temporary,\n progress: true,\n },\n )\n run(\"git\", [\"checkout\", \"--quiet\", \"--detach\", \"FETCH_HEAD\"], {\n cwd: temporary,\n })\n\n const [pnpm, prefix] = pnpmCommand()\n console.error(\n \"Installing the public source checkout's pinned dependencies...\",\n )\n run(pnpm, [...prefix, \"install\", \"--frozen-lockfile\"], {\n cwd: temporary,\n progress: true,\n })\n console.error(\n \"Building the AgentScript Agentforce SDK and its dependencies...\",\n )\n run(pnpm, [...prefix, \"--filter\", \"@agentscript/agentforce...\", \"build\"], {\n cwd: temporary,\n progress: true,\n })\n\n const temporaryEntry = path.join(\n temporary,\n \"packages\",\n \"agentforce\",\n \"dist\",\n \"index.js\",\n )\n await verifyCompiler(temporaryEntry)\n const sourceRevision = run(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd: temporary,\n })\n replaceDirectory(sourceRoot, temporary, cacheDirectory)\n return {\n provider: \"source\",\n repository: SOURCE_REPOSITORY,\n requestedRef: options.sourceRef,\n sourceRevision,\n entry: path.join(\n sourceRoot,\n \"packages\",\n \"agentforce\",\n \"dist\",\n \"index.js\",\n ),\n }\n } catch (error) {\n if (fs.existsSync(temporary))\n fs.rmSync(temporary, { recursive: true, force: true })\n throw error\n }\n}\n\nasync function main() {\n let options\n try {\n options = parseArguments(process.argv.slice(2))\n } catch (error) {\n usage()\n throw error\n }\n\n const cacheDirectory = sdkCacheDirectory(options.cacheDirectory)\n const installed =\n options.provider === \"source\"\n ? await installFromSource(options, cacheDirectory)\n : await installFromNpm(options, cacheDirectory)\n const manifest = {\n ...installed,\n installedAt: new Date().toISOString(),\n }\n const manifestPath = writeManifest(cacheDirectory, manifest)\n const result = { status: \"ok\", cacheDirectory, manifestPath, ...manifest }\n\n if (options.json) console.log(JSON.stringify(result, null, 2))\n else\n console.log(\n `AgentScript SDK ready: ${installed.provider} (${installed.entry})`,\n )\n}\n\nmain().catch((error) => {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n})\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":9908,"content_sha256":"0c799c75fb61d8a9d4046b51a01ceb02a6a34608e5e765df258ca4c1c3acf68d"},{"filename":"scripts/upgrade-legacy-nga-analyze.py","content":"#!/usr/bin/env python3\n\"\"\"Analyze collected legacy/draft preview trials and emit the comparison report.\n\nDIVISION OF LABOR:\n * Rubric #1 (trace / action equality) is MECHANICAL — it compares structured\n identifiers pulled from the local plan traces (subagent topic, ordered\n action names). This is objective equality, so the script does it.\n * Rubric #2 (response discrepancy) is a SEMANTIC judgment — data fidelity,\n formatting, verbosity, tone, spec compliance. This CANNOT be done by regex\n or heuristics. The script does NOT score it. It emits the verbatim responses\n plus an analysis prompt for an LLM judge (or human) to complete.\n\nORACLE MODEL (Rubric #1): legacy is a BASELINE, not the oracle. Three references:\n spec = expected_subagent/expected_actions (authored from intent) — ORACLE\n legacy = what the old agent actually did — descriptive baseline, can be WRONG\n draft = the NGA under test\n We classify the spec-legacy-draft triangle instead of gating on draft==legacy:\n legacy==spec & draft==spec -> PASS\n legacy==spec & draft!=spec -> DRAFT-REGRESSION (fix draft)\n legacy!=spec & draft==spec -> PASS + LEGACY-DEFECT (draft fixed it)\n legacy!=spec & draft!=spec & l==d -> SPEC-REVIEW (spec likely stale)\n legacy!=spec & draft!=spec & l!=d -> ADJUDICATE (LLM judge decides)\n If no spec is supplied the utterance is SPEC-UNVERIFIED: fall back to the\n legacy-vs-draft baseline diff + judge.\n\nDRIFT (across iterations): an accepted BASELINE file records the last\n spec-approved (subagent, actions) per utterance. Each run diffs the DRAFT\n signature against it — a change flags DRIFT even if it still matches the spec,\n catching unintended agentscript drift. Re-baseline deliberately (REBASELINE=1)\n when instructions intentionally change, so intended changes update truth\n instead of firing false regressions.\n\nNothing here is agent- or utterance-specific: no field labels, no domain\nvocabulary, no expected values baked in (expected_*/baseline are per run/repo).\n\nRubric #1 source — the LOCAL plan trace `sf agent preview end` writes to\n .sfdx/agents/\u003cagentId>/sessions/\u003csid>/traces/\u003cplanId>.json\nBoth sides populate it on CLI>=2.149.9 (@salesforce/agents>=2.0.4). We read\n`topic`/`intent` (subagent) and ordered FunctionStep entries (action name/input/\nstatus), for legacy (ReAct: UpdateTopicStep+topic) and draft (ScriptAgent:\nTransitionStep). Names match by substring containment (name_match), since legacy\nnames append a definition-id suffix of arbitrary format.\n\nUsage: analyze.py \u003crun_dir> \u003cutt_id> [expected_subagent] [expected_actions_csv]\n expected_* are OPTIONAL. When omitted, only observed traces + the\n legacy-vs-draft cross-check + drift are reported (no vs-spec triangle verdict).\n Traces live under `.sfdx/agents`; the tool auto-locates it by walking up from\n cwd (then from \u003crun_dir>), so it runs from any directory inside the repo. Only\n if no `.sfdx/agents` exists on either path does a subject report a (correct)\n UNVERIFIED-TRACE. \u003crun_dir> may be relative (e.g. agent-eval/runs/\u003cid>).\nEnv (optional):\n EVAL_ORG label only.\n DEMO_CONTACT if set, the first draft action input is checked for this contactId.\n EVAL_BASELINE path to the accepted-baseline JSON (default agent-eval/baseline.json).\n REBASELINE if \"1\", write the current draft signature into the baseline for\n this utterance (deliberate, human-approved re-baseline) and exit.\nWrites \u003crun_dir>/report.md and prints it.\n\"\"\"\nimport json, sys, re, glob, os, datetime\n\nRUN = sys.argv[1]\nUTT = sys.argv[2] if len(sys.argv) > 2 else \"utterance\"\nEXP_SUB = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None\nEXP_ACTS = [a for a in (sys.argv[4].split(\",\") if len(sys.argv) > 4 else []) if a]\nORG = os.environ.get(\"EVAL_ORG\", \"target-org\")\nDEMO_CONTACT = os.environ.get(\"DEMO_CONTACT\") # None => no contact-binding assertion\nBASELINE_FILE = os.environ.get(\"EVAL_BASELINE\", os.path.join(\"agent-eval\", \"baseline.json\"))\nREBASELINE = os.environ.get(\"REBASELINE\") == \"1\"\n\n# Locate `.sfdx/agents` by walking UP from cwd (then from the run dir) so the\n# tool works no matter which directory it's invoked from — otherwise a wrong cwd\n# silently yields a false UNVERIFIED-TRACE for every subject.\ndef _find_sfdx_agents():\n for start in (os.getcwd(), os.path.abspath(RUN)):\n d = start\n while True:\n cand = os.path.join(d, \".sfdx\", \"agents\")\n if os.path.isdir(cand):\n return cand\n parent = os.path.dirname(d)\n if parent == d:\n break\n d = parent\n # Fall back to the cwd-relative path; parse_trace() then reports \"not found\".\n return os.path.join(os.getcwd(), \".sfdx\", \"agents\")\n\n\nSFDX_AGENTS = _find_sfdx_agents()\n\n\ndef load(subj):\n out = []\n for f in sorted(glob.glob(f\"{RUN}/{subj}/{UTT}/trial-*/trial.json\")):\n with open(f) as fh:\n out.append(json.load(fh))\n return out\n\n\n# ---- Trace parsing (mechanical rubric #1) -----------------------------------\ndef find_trace(plan_id):\n if not plan_id:\n return None\n hits = glob.glob(f\"{SFDX_AGENTS}/**/traces/{plan_id}.json\", recursive=True)\n return hits[0] if hits else None\n\n\n# Legacy topic/action names carry a definition-id suffix appended to the base\n# name (e.g. Get_All_Loan_Application_Details_179KX000000YehM), while the NGA\n# draft uses the clean base name. The suffix format is NOT fixed (length/charset\n# vary), so a regex strip is unreliable. Instead treat two names as the same\n# entity when either is a substring of the other (order-independent, since the\n# suffix may land on either side). Compared case-insensitively.\ndef name_match(a, b):\n if a is None or b is None:\n return a == b\n a, b = a.lower(), b.lower()\n return a in b or b in a\n\n\ndef names_match(xs, ys):\n return len(xs) == len(ys) and all(name_match(x, y) for x, y in zip(xs, ys))\n\n\ndef signature(topic, action_names):\n \"\"\"Route+action signature = [subagent] + ordered actions. Compared with\n names_match so legacy definition-id suffixes are tolerated.\"\"\"\n return [topic] + list(action_names or [])\n\n\ndef triangle_verdict(leg_sig, draft_sig, spec_sig):\n \"\"\"Classify the spec-legacy-draft triangle. Returns (verdict, note).\n spec_sig is None when no expected_* was supplied (SPEC-UNVERIFIED).\"\"\"\n if not spec_sig or spec_sig == [None]:\n same = names_match(leg_sig, draft_sig)\n return (\"SPEC-UNVERIFIED\",\n \"no spec supplied — \"\n + (\"draft matches legacy baseline\" if same\n else \"draft diverges from legacy baseline; judge must decide\"))\n ls = names_match(leg_sig, spec_sig)\n ds = names_match(draft_sig, spec_sig)\n if ls and ds:\n return (\"PASS\", \"legacy, draft, and spec all agree\")\n if ls and not ds:\n return (\"DRAFT-REGRESSION\", \"legacy matched the spec but the draft does not — fix the draft\")\n if ds and not ls:\n return (\"PASS + LEGACY-DEFECT\",\n \"draft matches the spec; legacy diverged — legacy was wrong, the migration fixed it\")\n # neither matches the spec\n if names_match(leg_sig, draft_sig):\n return (\"SPEC-REVIEW\", \"legacy and draft agree with each other but not the spec — the spec is likely stale/wrong\")\n return (\"ADJUDICATE\", \"three-way disagreement (spec vs legacy vs draft) — LLM judge must adjudicate with traces + responses\")\n\n\ndef load_baseline():\n try:\n with open(BASELINE_FILE) as fh:\n return json.load(fh)\n except Exception:\n return {}\n\n\ndef save_baseline(bl):\n os.makedirs(os.path.dirname(BASELINE_FILE) or \".\", exist_ok=True)\n with open(BASELINE_FILE, \"w\") as fh:\n json.dump(bl, fh, indent=2, sort_keys=True)\n\n\ndef parse_trace(plan_id):\n \"\"\"Return normalized trace evidence, or a reason it's unavailable.\n\n Handles BOTH local trace shapes:\n - draft (ScriptAgent): routing via TransitionStep{from_agent,to_agent}\n - legacy (ProductionAgent, CLI>=2.149.9 / @salesforce/agents>=2.0.4):\n routing via UpdateTopicStep + `topic`; names carry a definitionId suffix.\n Legacy populates only when the SDK fetches the v1.1 preview plans endpoint;\n older SDKs (\u003c=2.0.1) stub getTrace()->undefined and write `{}` (empty plan).\n Names are kept RAW here; equality is decided by name_match() (substring),\n not by stripping the suffix.\n \"\"\"\n tf = find_trace(plan_id)\n if not tf:\n return {\"available\": False, \"reason\": \"trace file not found\"}\n try:\n with open(tf) as fh:\n d = json.load(fh)\n except Exception as e:\n return {\"available\": False, \"reason\": f\"unreadable: {e}\"}\n if not isinstance(d, dict) or not d.get(\"plan\"):\n return {\"available\": False,\n \"reason\": \"empty plan ({}) — SDK too old? need CLI>=2.149.9 (@salesforce/agents>=2.0.4)\"}\n transitions, actions = [], []\n for s in d[\"plan\"]:\n if s.get(\"type\") == \"TransitionStep\": # draft shape\n dd = s.get(\"data\", {})\n transitions.append((dd.get(\"from_agent\"), dd.get(\"to_agent\")))\n if s.get(\"type\") == \"FunctionStep\": # both shapes\n fn = s.get(\"function\", {})\n out = fn.get(\"output\", {})\n actions.append({\n \"name\": fn.get(\"name\"),\n \"input\": fn.get(\"input\"),\n \"status\": out.get(\"__action_execution_status__\") if isinstance(out, dict) else None,\n })\n topic = d.get(\"topic\")\n # Legacy routes via UpdateTopicStep rather than TransitionStep; synthesize a\n # transition to the topic so the report row is uniform across both shapes.\n if not transitions and topic:\n transitions = [(\"Agent Router\", topic)]\n return {\n \"available\": True,\n \"topic\": topic,\n \"intent\": d.get(\"intent\"),\n \"transitions\": transitions,\n \"actions\": actions,\n \"action_names\": [a[\"name\"] for a in actions],\n }\n\n\nleg, dr = load(\"legacy\"), load(\"draft\")\nif not leg or not dr:\n sys.exit(f\"missing trials: legacy={len(leg)} draft={len(dr)}\")\nwc = lambda t: len((t or \"\").split())\n\n# Trace evidence per subject (use trial 1's planId as representative).\nleg_tr = parse_trace(leg[0].get(\"planId\"))\ndr_tr = parse_trace(dr[0].get(\"planId\"))\n\n# Signatures (route + ordered actions) for the triangle + drift comparisons.\nleg_sig = signature(leg_tr.get(\"topic\"), leg_tr.get(\"action_names\")) if leg_tr[\"available\"] else None\ndraft_sig = signature(dr_tr.get(\"topic\"), dr_tr.get(\"action_names\")) if dr_tr[\"available\"] else None\nspec_sig = signature(EXP_SUB, EXP_ACTS) if (EXP_SUB or EXP_ACTS) else None\n\n# --- Deliberate re-baseline: record the current DRAFT signature and exit. -----\n# This is the human-approved \"the draft is now correct; make it the baseline\"\n# action. Use after an intentional instruction change (or first approval).\nif REBASELINE:\n if not dr_tr[\"available\"]:\n sys.exit(\"REBASELINE refused: draft trace unavailable (cannot capture a signature).\")\n bl = load_baseline()\n bl[UTT] = {\n \"subagent\": dr_tr[\"topic\"],\n \"actions\": dr_tr[\"action_names\"],\n \"approved_at\": datetime.datetime.now().isoformat(timespec=\"seconds\"),\n \"approved_from_run\": RUN,\n }\n save_baseline(bl)\n print(f\"Re-baselined `{UTT}` in {BASELINE_FILE}: subagent={dr_tr['topic']} actions={dr_tr['action_names']}\")\n sys.exit(0)\n\n# Triangle verdict (spec vs legacy vs draft) + drift vs accepted baseline.\ntri_verdict, tri_note = (triangle_verdict(leg_sig, draft_sig, spec_sig)\n if (leg_tr[\"available\"] and dr_tr[\"available\"])\n else (\"UNVERIFIED-TRACE\", \"one or both traces unavailable\"))\nbaseline = load_baseline()\nbase_entry = baseline.get(UTT)\nif base_entry and draft_sig:\n base_sig = signature(base_entry.get(\"subagent\"), base_entry.get(\"actions\"))\n drift = not names_match(base_sig, draft_sig)\nelse:\n base_sig, drift = None, None # None => no baseline recorded yet\n\nL = []\np = L.append\np(f\"# Legacy vs NGA Regression Report — utterance `{UTT}`\\n\")\np(f\"- Run: `{RUN}` | Org: `{ORG}` | Trials: {len(leg)} each\")\np(f\"- Legacy latency (ms): {[r['latency_ms'] for r in leg]} | Draft: {[r['latency_ms'] for r in dr]}\")\np(f\"- content-safe (all): legacy {all(r['isContentSafe'] for r in leg)} | draft {all(r['isContentSafe'] for r in dr)}\")\np(f\"- word count (per trial): legacy {[wc(r['response']) for r in leg]} | draft {[wc(r['response']) for r in dr]}\")\np(f\"- Legacy planIds: {[r['planId'] for r in leg]}\")\np(f\"- Draft planIds: {[r['planId'] for r in dr]}\\n\")\n\n# ---- Rubric #1 — mechanical -------------------------------------------------\np(\"## Rubric #1 — Trace / action equality (MECHANICAL, from local planId trace)\\n\")\nif EXP_SUB or EXP_ACTS:\n p(f\"Expected (from suite): subagent `{EXP_SUB}`, actions `{EXP_ACTS}`.\\n\")\nelse:\n p(\"_No expected subagent/actions supplied — reporting observed traces and the \"\n \"legacy-vs-draft cross-check only._\\n\")\np(\"| subject | trace | subagent (topic) | router transition | ordered actions | verdict |\")\np(\"|---|---|---|---|---|---|\")\n\n\ndef trace_row(name, tr):\n if not tr[\"available\"]:\n return f\"| {name} | ❌ {tr['reason']} | — | — | — | **UNVERIFIED-TRACE** |\"\n trans = \" → \".join(str(x) for x in tr[\"transitions\"][-1]) if tr[\"transitions\"] else \"(none)\"\n acts = \", \".join(a[\"name\"] for a in tr[\"actions\"]) or \"(none)\"\n if EXP_SUB or EXP_ACTS:\n sub_ok = name_match(tr[\"topic\"], EXP_SUB) if EXP_SUB else True\n act_ok = names_match(tr[\"action_names\"], EXP_ACTS) if EXP_ACTS else True\n verdict = \"TRACE-VERIFIED ✅\" if (sub_ok and act_ok) else \"TRACE-DIVERGENT ⚠️\"\n else:\n verdict = \"(observed)\"\n return f\"| {name} | ✅ local | {tr['topic']} | {trans} | {acts} | **{verdict}** |\"\n\n\np(trace_row(\"legacy\", leg_tr))\np(trace_row(\"draft\", dr_tr))\nif leg_tr[\"available\"] and dr_tr[\"available\"]:\n same_sub = name_match(leg_tr[\"topic\"], dr_tr[\"topic\"])\n same_acts = names_match(leg_tr[\"action_names\"], dr_tr[\"action_names\"])\n p(f\"\\n**Legacy vs draft:** subagent {'✅ match' if same_sub else '❌ differ'} \"\n f\"(legacy `{leg_tr['topic']}` vs draft `{dr_tr['topic']}`) · \"\n f\"actions {'✅ match' if same_acts else '❌ differ'} \"\n f\"(legacy `{leg_tr['action_names']}` vs draft `{dr_tr['action_names']}`) → \"\n f\"**{'ROUTE+ACTION EQUIVALENT' if (same_sub and same_acts) else 'TRACE-DIVERGENT'}**\")\n# ---- Triangle verdict (spec vs legacy vs draft) + drift ---------------------\np(\"\\n### Oracle triangle (spec vs legacy vs draft)\\n\")\np(\"| reference | subagent | actions |\")\np(\"|---|---|---|\")\np(f\"| spec (oracle) | {EXP_SUB or '_none supplied_'} | {EXP_ACTS or '_none supplied_'} |\")\np(f\"| legacy (baseline) | {leg_tr.get('topic') if leg_tr['available'] else '—'} | \"\n f\"{leg_tr.get('action_names') if leg_tr['available'] else '—'} |\")\np(f\"| draft (under test) | {dr_tr.get('topic') if dr_tr['available'] else '—'} | \"\n f\"{dr_tr.get('action_names') if dr_tr['available'] else '—'} |\")\np(f\"\\n**Triangle verdict: `{tri_verdict}`** — {tri_note}\\n\")\np(\"> Legacy is a descriptive baseline, NOT the oracle: it can be wrong or miss \"\n \"an action. The spec (expected_*) is the oracle. `PASS + LEGACY-DEFECT` means \"\n \"the draft matches the spec while legacy diverged — the migration fixed a legacy \"\n \"defect. `DRAFT-REGRESSION` means legacy was right and the draft broke it. \"\n \"`SPEC-REVIEW`/`ADJUDICATE` escalate to the LLM judge.\\n\")\n\n# Drift vs the accepted baseline (unintended-change detector).\nif drift is None:\n p(f\"**Drift:** no accepted baseline recorded for `{UTT}` yet \"\n f\"(set it deliberately with `REBASELINE=1`). File: `{BASELINE_FILE}`.\\n\")\nelif drift:\n p(f\"**Drift: ⚠️ DETECTED** — the draft signature changed vs the accepted \"\n f\"baseline (`{base_entry.get('approved_at')}`).\\n\"\n f\"- baseline: subagent `{base_entry.get('subagent')}` actions `{base_entry.get('actions')}`\\n\"\n f\"- current draft: subagent `{dr_tr.get('topic')}` actions `{dr_tr.get('action_names')}`\\n\"\n f\"\\n> If this change was intended, re-approve with `REBASELINE=1`. Otherwise the \"\n f\"draft drifted from its last spec-approved behavior — investigate.\\n\")\nelse:\n p(f\"**Drift:** ✅ none — draft matches the accepted baseline \"\n f\"(`{base_entry.get('approved_at')}`).\\n\")\n\np(\"\\n> **Local, symmetric trace.** Both sides write a full local plan trace at \"\n \"`.sfdx/agents/\u003cagentId>/sessions/\u003csid>/traces/\u003cplanId>.json` \"\n \"(`topic`=subagent + ordered `FunctionStep` action name/input/status, \"\n \"type-agnostic across Apex/Flow/Prompt). Requires **CLI >=2.149.9 \"\n \"(@salesforce/agents>=2.0.4)**; older SDKs stubbed `ProductionAgent.getTrace()` \"\n \"and wrote `{}`. Legacy names carry a definition-id suffix of arbitrary format; \"\n \"names match by substring containment (either side), not suffix-stripping.\\n\")\n\nif dr_tr[\"available\"] and dr_tr[\"actions\"]:\n ci = dr_tr[\"actions\"][0].get(\"input\")\n if DEMO_CONTACT:\n ok = isinstance(ci, dict) and ci.get(\"contactId\") == DEMO_CONTACT\n p(f\"Draft first action input: `{json.dumps(ci)}` — \"\n f\"{'✅ bound to DEMO_CONTACT' if ok else '⚠️ not bound to DEMO_CONTACT'}\\n\")\n else:\n p(f\"Draft first action input: `{json.dumps(ci)}` (set DEMO_CONTACT env to assert binding)\\n\")\n\n# ---- Responses (verbatim, all trials) ---------------------------------------\np(\"## Responses (verbatim)\\n\")\nfor subj, rows in ((\"Legacy\", leg), (\"Draft NGA\", dr)):\n p(f\"### {subj}\\n\")\n for r in rows:\n p(f\"**Trial {r['trial']}** (planId `{r['planId']}`):\\n```\\n{r['response']}\\n```\\n\")\n\n# ---- Rubric #2 — LLM analysis prompt (NOT scored by this script) ------------\np(\"## Rubric #2 — Response analysis (LLM judge — complete this)\\n\")\np(\"> The script does not score responses. An LLM (or human) must read the \"\n \"verbatim responses above and answer the rubric below. Base every claim on the \"\n \"actual text; do not assume domain-specific fields.\\n\")\ndrift_ctx = (\"no baseline recorded yet\" if drift is None\n else \"DRIFTED from accepted baseline\" if drift\n else \"matches accepted baseline\")\np(f\"> **Mechanical context for the judge (Rubric #1):** triangle verdict \"\n f\"`{tri_verdict}` — {tri_note}; drift status: {drift_ctx}. Treat the triangle \"\n f\"verdict as the routing/action ground truth: legacy is a baseline, not the \"\n f\"oracle. If the verdict is `PASS + LEGACY-DEFECT`, do not penalize the draft \"\n f\"for differing from legacy — legacy was wrong. If `DRAFT-REGRESSION`, the draft \"\n f\"broke a behavior legacy got right. If `SPEC-REVIEW`/`ADJUDICATE`, you are the \"\n f\"tie-breaker — decide which behavior is actually correct from the responses.\\n\")\np(\"\"\"You are comparing a **legacy** agent against its **migrated NGA draft** for\nthe same utterance. Using ONLY the response texts above plus the Rubric #1 trace\nfacts, produce:\n\n1. **Intra-trial consistency** — for each subject, are the N trials materially\n equivalent (same facts, acceptable wording variance) or do they diverge?\n Pick a canonical trial for each side.\n2. **Data fidelity** — do legacy and draft agree on every concrete fact the\n responses assert (numbers, amounts, dates, statuses, named entities, IDs)?\n List any value the draft changed, dropped, or added. A factual mismatch is a\n **REGRESSION**, not a style difference.\n3. **Formatting** — structural differences: bullets/headers/lists, number and\n money formatting, and any leaked internal field/action API names.\n4. **Verbosity & tone** — length, clarity, empathy, and whether each ends with\n an appropriate follow-up/closing offer.\n5. **Spec compliance** — does each side satisfy the utterance's intent? Note if\n the Rubric #1 action set differs (e.g. one side grounded via a retrieval\n action and the other answered from model knowledge) and whether that changes\n answer quality or trustworthiness.\n6. **Classification** — one of `EQUIVALENT` | `IMPROVED` | `ACCEPTABLE-DIFF` |\n `REGRESSION`, with a one-line justification. If not `EQUIVALENT`/`IMPROVED`,\n give a concrete recommended `.agent` change.\n\"\"\")\n\nreport = \"\\n\".join(L)\nwith open(f\"{RUN}/report.md\", \"w\") as fh:\n fh.write(report)\nprint(report)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":20443,"content_sha256":"e2cdcddd8dff36538873192aea13c9981a325924eaef0c23b39a8b0a0f3a387e"},{"filename":"scripts/upgrade-legacy-nga-run-trials.sh","content":"#!/usr/bin/env bash\n# Collect N preview trials for one subject × one utterance.\n# Usage: upgrade-legacy-nga-run-trials.sh \u003csubject:legacy|draft> \u003cutt_id> \u003ctrials> \u003coutdir> \"\u003cutterance>\" [mode:simulate|live]\n# mode defaults to `simulate` (safe: no real side effects). Pass `live` ONLY for\n# utterances the caller has opted in per the side-effect safety gate. mode gates\n# the DRAFT only; legacy (--api-name) has no mode flag and always runs live (CLI).\n# Reliable path: start -> send -> end, fresh session per trial,\n# stdout->file (never 2>&1 into a parser).\n# Agent api-name/bundle and org are config, supplied via env (no baked-in agent):\n# EVAL_AGENT legacy Bot api-name AND draft bundle name (they collide by design)\n# EVAL_ORG target-org alias\nset -euo pipefail\n# Force plain output: some shells/CI still get ANSI color codes on `sf --json`,\n# which corrupt the JSON once stdout is redirected to a file (the parser then\n# fails on the escape bytes). NO_COLOR + FORCE_COLOR=0 make the CLI emit clean JSON.\nexport NO_COLOR=1 FORCE_COLOR=0\nSUBJECT=\"$1\"; UTT=\"$2\"; N=\"$3\"; OUT=\"$4\"; TEXT=\"$5\"; MODE=\"${6:-simulate}\"\ncase \"$MODE\" in simulate|live) ;; *) echo \"mode must be 'simulate' or 'live' (got '$MODE')\" >&2; exit 2 ;; esac\nORG=\"${EVAL_ORG:?set EVAL_ORG to the target-org alias}\"\nAGENT=\"${EVAL_AGENT:?set EVAL_AGENT to the agent api-name/bundle}\"\n# NOTE: --use-live-actions / --simulate-actions are START-ONLY flags.\n# send/end accept only the subject selector (--api-name | --authoring-bundle) + --session-id.\nif [ \"$SUBJECT\" = \"legacy\" ]; then\n SEL=(--api-name \"$AGENT\") # session selector (start/send/end)\n START_SEL=(\"${SEL[@]}\") # legacy (--api-name) has no mode flag; always live (CLI)\n if [ \"$MODE\" = \"simulate\" ]; then\n echo \" NOTE: legacy (--api-name) has no simulate mode; it runs live. Gate side-effecting utterances upstream.\" >&2\n fi\nelse\n SEL=(--authoring-bundle \"$AGENT\") # session selector (start/send/end)\n if [ \"$MODE\" = \"live\" ]; then\n START_SEL=(--use-live-actions \"${SEL[@]}\") # mode flag only on start; explicit opt-in\n else\n START_SEL=(--simulate-actions \"${SEL[@]}\") # default: safe, no real side effects\n fi\nfi\nmkdir -p \"$OUT/$SUBJECT/$UTT\"\nfor i in $(seq 1 \"$N\"); do\n d=\"$OUT/$SUBJECT/$UTT/trial-$i\"; mkdir -p \"$d\"\n sf agent preview start --json \"${START_SEL[@]}\" -o \"$ORG\" > \"$d/start.json\" 2>\"$d/start.err\"\n SID=$(python3 -c \"import json;print(json.load(open('$d/start.json'))['result']['sessionId'])\")\n t0=$(python3 -c \"import time;print(int(time.time()*1000))\")\n sf agent preview send --json \"${SEL[@]}\" --session-id \"$SID\" -u \"$TEXT\" -o \"$ORG\" > \"$d/send.json\" 2>\"$d/send.err\"\n t1=$(python3 -c \"import time;print(int(time.time()*1000))\")\n sf agent preview end --json \"${SEL[@]}\" --session-id \"$SID\" -o \"$ORG\" > \"$d/end.json\" 2>\"$d/end.err\" || true\n python3 - \"$d\" \"$SUBJECT\" \"$UTT\" \"$i\" \"$SID\" \"$((t1-t0))\" \u003c\u003c'PY'\nimport json,sys\nd,subject,utt,i,sid,lat=sys.argv[1:7]\ns=json.load(open(f\"{d}/send.json\"))[\"result\"]\nm=s[\"messages\"][0]\nrec={\"subject\":subject,\"utterance_id\":utt,\"trial\":int(i),\"sessionId\":sid,\n \"planId\":m.get(\"planId\"),\"isContentSafe\":m.get(\"isContentSafe\"),\n \"response\":m.get(\"message\"),\"result\":m.get(\"result\"),\n \"citedReferences\":m.get(\"citedReferences\"),\"latency_ms\":int(lat)}\njson.dump(rec,open(f\"{d}/trial.json\",\"w\"),indent=1)\nprint(f\" {subject} {utt} trial {i}: {int(lat)}ms safe={m.get('isContentSafe')} planId={m.get('planId')}\")\nPY\ndone\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3485,"content_sha256":"61c44518b3ca1a1302897970d32f1663d1ca191afbe0ec549f17fe857738993e"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Agent Script Skill","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"What This Skill Is For","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill is for developing Agentforce agents, primarily with Agent Script, Salesforce's scripting language for AI agents.","type":"text"}]},{"type":"paragraph","content":[{"text":"Org-backed workflows require an Agentforce license, API v66.0 or later, and an Einstein Agent User. Static authoring and review can proceed without org access.","type":"text"}]},{"type":"paragraph","content":[{"text":"CRITICAL:","type":"text","marks":[{"type":"strong"}]},{"text":" Agent Script is NOT AppleScript, JavaScript, Python, or any other language. Do NOT confuse Agent Script syntax or semantics with any other language you have been trained on.","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent Script agents are defined by ","type":"text"},{"text":"AiAuthoringBundle","type":"text","marks":[{"type":"code_inline"}]},{"text":" metadata: an ","type":"text"},{"text":"\u003cApiName>.agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" file (agent behavior) plus a sibling ","type":"text"},{"text":"\u003cApiName>.bundle-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" file (bundle metadata). The directory and both filenames must use the same case-sensitive API name; a literal ","type":"text"},{"text":"bundle-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" filename is not deployable. Actions can be implemented with invocable Apex, autolaunched Flows, Prompt Templates, and other supported types.","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill covers the full Agent Script lifecycle: designing agents, writing Agent Script code, validating and debugging, deploying and publishing, and testing.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use This Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"This file maps user intent to task domains and relevant reference files in ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":". Treat this file as the execution router for end-to-end agent development, and use references for deep detail.","type":"text"}]},{"type":"paragraph","content":[{"text":"Identify user intent from task descriptions. Read only the reference explicitly required by the active step or needed for the current decision. Every ","type":"text"},{"text":"Reference Files","type":"text","marks":[{"type":"strong"}]},{"text":" section is a lookup index, not a preload list; do not load files for later or inapplicable steps.","type":"text"}]},{"type":"paragraph","content":[{"text":"For a comprehensive health check, common-pitfall audit, or audit-fix-evaluate loop over an existing agent, use the ","type":"text"},{"text":"Audit and Repair an Existing Agent","type":"text","marks":[{"type":"strong"}]},{"text":" task domain below as part of the same authoring lifecycle.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rules That Always Apply","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always ","type":"text","marks":[{"type":"strong"}]},{"text":"--json","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]},{"text":" ALWAYS include ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" on EVERY ","type":"text"},{"text":"sf","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI command. Do NOT pipe CLI output through ","type":"text"},{"text":"jq","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"2>/dev/null","type":"text","marks":[{"type":"code_inline"}]},{"text":". Read the full JSON response directly — LLMs parse JSON natively.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify target org.","type":"text","marks":[{"type":"strong"}]},{"text":" Before any org interaction, run ","type":"text"},{"text":"sf config get target-org --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" to confirm a target org is set. If none configured, ask the user to set one with ","type":"text"},{"text":"sf config set target-org \u003calias>","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Diagnose in proportion to the change.","type":"text","marks":[{"type":"strong"}]},{"text":" For syntax or local static defects, run the supported local parser/compiler first, then add target-org validation when available. For behavioral defects, preserve a baseline and use preview plus traces. For a Surface repair, freeze the exact accepted edit list, then inspect the final diff and revert every other hunk, including block-scalar or metadata normalization. In a smallest-change repair, keep optional cosmetic findings advisory unless the user explicitly includes cleanup in scope; valid syntax with no diagnostic or use-case consequence is not an extra repair. Simulation can establish routing and action selection; use ","type":"text"},{"text":"--use-live-actions","type":"text","marks":[{"type":"code_inline"}]},{"text":" only with explicit approval, a verified non-production environment, and safe test data. Do not claim an external effect from simulation or response text. See ","type":"text"},{"text":"Validation & Debugging","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-validation-and-debugging.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use a proportionate spec gate.","type":"text","marks":[{"type":"strong"}]},{"text":" Obtain explicit Agent Spec approval for greenfield agents and Structural or Rewrite changes. A user-authorized, well-specified local repair does not require recreating or reapproving the entire spec; record the affected use case and preserve the existing design. When the user supplies a sufficiently detailed design and explicitly says it is already approved, treat that as the approved spec: do not recreate it or stop for another approval unless requirements are missing or materially change.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't stall.","type":"text","marks":[{"type":"strong"}]},{"text":" After a step completes successfully, announce the next step and start it. Do not wait for the user to say \"what's next\" or \"ok, continue.\" Checkpoints that require explicit user approval include: (a) Agent Spec approval when required by Rule 4, (b) the pre-Publish CHECKPOINT, (c) destructive or consequential external operations, and (d) any A/B branch the skill explicitly surfaces (e.g., Data Cloud not provisioned during ADL setup). Long-running async work like ADL indexing should run in the background while the skill continues with work that doesn't depend on the result.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Draft-first lifecycle.","type":"text","marks":[{"type":"strong"}]},{"text":" During normal authoring, stay in draft iteration: edit ","type":"text"},{"text":".agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" + action implementations, validate, deploy, and preview as many times as needed. Do NOT publish/activate by default. Publish + activate are explicit release actions that require the user to confirm they are ready to commit the current draft to metadata and expose it to end users.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Start with one execution block and no mutable state.","type":"text","marks":[{"type":"strong"}]},{"text":" A focused agent puts reasoning and actions directly in ","type":"text"},{"text":"start_agent","type":"text","marks":[{"type":"code_inline"}]},{"text":". Add a subagent only for a real objective, instruction, action, authority, or escalation boundary. Add persistent state only for a named deterministic consumer and give it a complete lifecycle. Ordinary continuity stays in surviving history. Apply the concrete checks in ","type":"text"},{"text":"The Zen of AgentScript","type":"text","marks":[{"type":"link","attrs":{"href":"references/zen-of-agentscript.md","title":null}}]},{"text":" and ","type":"text"},{"text":"Posture & Determinism","type":"text","marks":[{"type":"link","attrs":{"href":"references/posture-and-determinism.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use supported control flow.","type":"text","marks":[{"type":"strong"}]},{"text":" Use the canonical conditional forms and never generate a nested ","type":"text"},{"text":"if","type":"text","marks":[{"type":"code_inline"}]},{"text":", which Agentforce lint rejects. See ","type":"text"},{"text":"Conditional Control Flow Syntax","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md#conditional-control-flow-syntax","title":null}}]},{"text":", then run full bundle validation.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action implementation is a user decision.","type":"text","marks":[{"type":"strong"}]},{"text":" During planning/spec work, default new actions to ","type":"text"},{"text":"NEEDS STUB","type":"text","marks":[{"type":"code_inline"}]},{"text":" placeholders. Always ask the user whether they want to scan org/project for existing implementations and/or generate new Apex/Flow/Prompt implementations before taking either path.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Give each reachable branch one next outcome.","type":"text","marks":[{"type":"strong"}]},{"text":" Choose exactly one primary outcome: answer, ask, invoke an action, transition, refuse, or escalate. The compiler selects a subagent ","type":"text"},{"text":"system.instructions","type":"text","marks":[{"type":"code_inline"}]},{"text":" override instead of the global value, and the current runtime assembles effective system and resolved reasoning text for the model. Keep authoring constructs out of model-facing text. See ","type":"text"},{"text":"Instruction Resolution","type":"text","marks":[{"type":"link","attrs":{"href":"references/instruction-resolution.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use portable structural indentation.","type":"text","marks":[{"type":"strong"}]},{"text":" Generate new ","type":"text"},{"text":".agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" files with 4 spaces per level. Preserve a consistently indented legacy file during a surgical edit, or normalize the whole file as a separate validated change.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not let prompt formatting impersonate control flow.","type":"text","marks":[{"type":"strong"}]},{"text":" Indentation, numbered steps, and words such as ","type":"text"},{"text":"Show","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Ask","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Call","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Set","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"STOP","type":"text","marks":[{"type":"code_inline"}]},{"text":" inside ","type":"text"},{"text":"|","type":"text","marks":[{"type":"code_inline"}]},{"text":" text are model instructions, not executable scope. Gate actions independently. Use one ","type":"text"},{"text":"|","type":"text","marks":[{"type":"code_inline"}]},{"text":" per contiguous prompt block; repeated adjacent markers do not create stages or priority. Do not use ","type":"text"},{"text":"@utils.setVariables","type":"text","marks":[{"type":"code_inline"}]},{"text":" to force a turn boundary or another reasoning iteration. Apply the checklist in ","type":"text"},{"text":"Common Control-Flow Pitfalls","type":"text","marks":[{"type":"link","attrs":{"href":"references/common-control-flow-pitfalls.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Choose who owns each decision.","type":"text","marks":[{"type":"strong"}]},{"text":" Use runtime predicates when an exact machine-known fact has a consequence that must remain stable. Use model instructions when semantic intent, ambiguity, recovery, or situation-aware judgment makes flexibility more valuable. A model cannot read stored variable values unless prompt text injects them with ","type":"text"},{"text":"{[email protected]}","type":"text","marks":[{"type":"code_inline"}]},{"text":"; interpolation reveals a value but does not make the model's comparison deterministic. Apply the tradeoff test in ","type":"text"},{"text":"Posture & Determinism","type":"text","marks":[{"type":"link","attrs":{"href":"references/posture-and-determinism.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Compile AgentScript locally first, cheaply, and visibly.","type":"text","marks":[{"type":"strong"}]},{"text":" For every authoring, repair, or audit task with an existing ","type":"text"},{"text":".agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" file, attempt the bundled local index/compiler before org-side validation or a completion report. Run ","type":"text"},{"text":"node \u003cskill-directory>/scripts/index-agent.mjs \u003cagent-file>","type":"text","marks":[{"type":"code_inline"}]},{"text":". If the SDK cannot load, follow ","type":"text"},{"text":"AgentScript Compiler Setup","type":"text","marks":[{"type":"link","attrs":{"href":"references/agentscript-toolchain.md","title":null}}]},{"text":", retry, and use its bounded npm/source fallback. Fix every severity-1 diagnostic and rerun until clean. Report the provider and exact version or commit. Org access does not replace this cheap local pass. If both local setup paths fail, continue with target-org validation or a bounded static review and state ","type":"text"},{"text":"compiler not used","type":"text","marks":[{"type":"strong"}]},{"text":" with the cause; do not stall the task or imply that a suggested future command was validation. ","type":"text"},{"text":"Offline","type":"text","marks":[{"type":"strong"}]},{"text":" or ","type":"text"},{"text":"non-interactive","type":"text","marks":[{"type":"strong"}]},{"text":" mode does not waive this step: it prohibits network and org operations, not the bundled local compiler.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keep the authoring-bundle shape deployable.","type":"text","marks":[{"type":"strong"}]},{"text":" Under ","type":"text"},{"text":"aiAuthoringBundles/\u003cApiName>/","type":"text","marks":[{"type":"code_inline"}]},{"text":", require exactly the matching pair ","type":"text"},{"text":"\u003cApiName>.agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"\u003cApiName>.bundle-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":". Do not shorten the metadata filename to ","type":"text"},{"text":"bundle-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":". Preserve scaffolded or retrieved metadata rather than rewriting its schema. A new CLI-scaffolded bundle normally uses ","type":"text"},{"text":"\u003cbundleType>AGENT\u003c/bundleType>","type":"text","marks":[{"type":"code_inline"}]},{"text":"; an existing descriptor can instead use the established ","type":"text"},{"text":"fullName","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]},{"text":" shape, with optional ","type":"text"},{"text":"label","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":". Do not create a partial hybrid or invent fields. Local compilation of the ","type":"text"},{"text":".agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" file does not verify the metadata filename or XML, so check both before reporting validation success.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Task Domains","type":"text"}]},{"type":"paragraph","content":[{"text":"Choose the domain that matches the user's current objective. Read its named references before acting; the links are load instructions, not an optional bibliography. Follow only the applicable workflow and preserve any satisfied prerequisites. The normal lifecycle is design -> draft -> validate/preview -> explicitly approved release.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Create an Agent","type":"text"}]},{"type":"paragraph","content":[{"text":"Use for a new agent or authoring bundle.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Design & Agent Spec","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-design-and-spec-creation.md","title":null}}]},{"text":", then use an already-approved, sufficiently detailed supplied design as the build contract without regenerating or reapproving it. Otherwise capture the requirements in a saved Agent Spec and obtain explicit approval. Keep new action implementations as ","type":"text"},{"text":"NEEDS STUB","type":"text","marks":[{"type":"code_inline"}]},{"text":" until the user chooses whether to reuse implementations, generate them, or leave placeholders.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read the applicable sections of ","type":"text"},{"text":"CLI for Agents","type":"text","marks":[{"type":"link","attrs":{"href":"references/salesforce-cli-for-agents.md","title":null}}]},{"text":" and validate the target-org prerequisites before org work. For document grounding, read ","type":"text"},{"text":"Data Library","type":"text","marks":[{"type":"link","attrs":{"href":"references/data-library-reference.md","title":null}}]},{"text":". For a voice agent, read ","type":"text"},{"text":"Voice Modality","type":"text","marks":[{"type":"link","attrs":{"href":"references/voice-modality-reference.md","title":null}}]},{"text":" and ","type":"text"},{"text":"Voice Latency","type":"text","marks":[{"type":"link","attrs":{"href":"references/voice-latency-heuristics.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate the authoring bundle with Salesforce CLI. Edit the scaffolded ","type":"text"},{"text":"\u003cApiName>.agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" and preserve the matching ","type":"text"},{"text":"\u003cApiName>.bundle-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":". Read ","type":"text"},{"text":"Core Language","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md","title":null}}]},{"text":", ","type":"text"},{"text":"Instruction Resolution","type":"text","marks":[{"type":"link","attrs":{"href":"references/instruction-resolution.md","title":null}}]},{"text":", and the applicable templates before writing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run the local compiler required by Rule 14. When an authenticated target org is available, also validate the authoring bundle in the org. Fix blocking diagnostics before implementing or deploying action dependencies.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate action implementations only when the user selected that path. Validate and deploy one dependency at a time.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Preview the draft and inspect traces using ","type":"text"},{"text":"Validation & Debugging","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-validation-and-debugging.md","title":null}}]},{"text":". Cover realistic happy, adjacent, recovery, and cancellation paths.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stay in the draft loop. Publish and activate only after the release gates in ","type":"text"},{"text":"Deploy, Publish, and Activate","type":"text","marks":[{"type":"strong"}]},{"text":" pass and the user explicitly approves.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Comprehend an Existing Agent","type":"text"}]},{"type":"paragraph","content":[{"text":"Use when the user wants to understand an existing bundle.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Locate the package and matching authoring-bundle files.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Core Language","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md","title":null}}]},{"text":", then map the subagent graph, deterministic blocks, model instructions, actions, variables, and action implementations.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Design & Agent Spec","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-design-and-spec-creation.md","title":null}}]},{"text":" and reverse-engineer a saved Agent Spec. Use ","type":"text"},{"text":"Subagent Map Diagrams","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-subagent-map-diagrams.md","title":null}}]},{"text":" for the graph. Annotate source only when the user requests it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Flag supported anti-patterns, distinguishing observed behavior from static inference. Load ","type":"text"},{"text":"Known Issues","type":"text","marks":[{"type":"link","attrs":{"href":"references/known-issues.md","title":null}}]},{"text":" only for an otherwise unexplained workaround.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit and Repair an Existing Agent","type":"text"}]},{"type":"paragraph","content":[{"text":"Use for “fix my AgentScript,” health checks, common-pitfall reviews, and baseline-versus-candidate repair loops.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Audit and Repair","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-and-repair.md","title":null}}]},{"text":", then follow its linked scope/path-review and repair/report workflow in order.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the ","type":"text"},{"text":"Diagnostic Catalog","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-diagnostic-catalog.md","title":null}}]},{"text":" and its focused diagnostic references only for categories present in the artifact. Use ","type":"text"},{"text":"Common Control-Flow Pitfalls","type":"text","marks":[{"type":"link","attrs":{"href":"references/common-control-flow-pitfalls.md","title":null}}]},{"text":" and its focused references for suspected prompt/control-flow defects.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Freeze accepted Surface edits before changing the artifact. For Structural or Rewrite work, obtain the approval required by Rule 4.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Compile locally, compare the unchanged baseline and candidate against the same use cases, and follow ","type":"text"},{"text":"Audit Evaluation Loop","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-evaluation-loop.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report Surface, Structural, and Rewrite assessments separately. Stay draft-only unless the user separately requests a release operation.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Audit Reference Files","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audit Scope and Path Review","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-scope-path-review.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audit Repair and Report","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-repair-report.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audit Candidate Verification","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-candidate-verification.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Instruction and Routing Diagnostics","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-diagnostics-instructions-routing.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action and State Diagnostics","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-diagnostics-actions-state.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Architecture and Evaluation Diagnostics","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-audit-diagnostics-architecture-evaluation.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action and Sequencing Pitfalls","type":"text","marks":[{"type":"link","attrs":{"href":"references/control-flow-actions-sequencing.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Lifecycle and Side-Effect Pitfalls","type":"text","marks":[{"type":"link","attrs":{"href":"references/control-flow-lifecycle-side-effects.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AgentScript Compiler Setup","type":"text","marks":[{"type":"link","attrs":{"href":"references/agentscript-toolchain.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Modify an Existing Agent","type":"text"}]},{"type":"paragraph","content":[{"text":"Use for an approved change to an existing response, route, action, subagent, state flow, grounding source, or modality.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Comprehend the affected paths first. For a material design change, update the Agent Spec and obtain approval; for a narrow specified repair, record the affected use case without forcing a full spec rewrite.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Core Language","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md","title":null}}]},{"text":" and only the feature references needed for the change. Preserve unrelated metadata, contracts, formatting, and behavior.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Edit the existing bundle in place. Generate action implementations only when explicitly requested.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Compile locally and, when available, validate against the target org. Preview every changed and adjacent path and inspect traces. Iterate in draft.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the release workflow only if the user separately requests release.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Diagnose Compilation Errors","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Capture the exact reported errors and run the Rule 14 local compiler.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When an authenticated target org is available, run org validation; use live preview only when compilation succeeds but runtime preparation still fails.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Classify and repair each concrete error using ","type":"text"},{"text":"Validation & Debugging","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-validation-and-debugging.md","title":null}}]},{"text":" and ","type":"text"},{"text":"Core Language","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rerun the surfaces that exposed the error. Report exact executed checks, remaining limitations, and no unexecuted command as validation evidence.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Diagnose Behavioral or Production Issues","type":"text"}]},{"type":"paragraph","content":[{"text":"For a local behavioral problem, preserve a baseline, preview with realistic utterances, and inspect traces using ","type":"text"},{"text":"Validation & Debugging","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-validation-and-debugging.md","title":null}}]},{"text":". Confirm which subagent, action calls, action results, state changes, and final response actually occurred before editing.","type":"text"}]},{"type":"paragraph","content":[{"text":"For a production session or trace ID, use ","type":"text"},{"text":"agentforce-observe","type":"text","marks":[{"type":"strong"}]},{"text":" for retrieval and reconstruction. Return here only when evidence identifies an AgentScript change. Never invent unavailable action inputs, outputs, or model reasoning.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Deploy, Publish, and Activate","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"CLI for Agents","type":"text","marks":[{"type":"link","attrs":{"href":"references/salesforce-cli-for-agents.md","title":null}}]},{"text":", ","type":"text"},{"text":"Metadata & Lifecycle","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-metadata-and-lifecycle.md","title":null}}]},{"text":", and ","type":"text"},{"text":"Deploy","type":"text","marks":[{"type":"link","attrs":{"href":"references/deploy-reference.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Compile locally and validate against the target org. Deploy the bundle and dependencies, then run live preview with realistic coverage and inspect traces. Do not proceed through a blocking result.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Present the exact target org and version state. Obtain explicit user approval before publishing or activating.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Publish, activate, and verify the user-facing agent only after approval.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Voice agents — wiring a telephony channel is a separate, opt-in step; never auto-wire it.","type":"text","marks":[{"type":"strong"}]},{"text":" Attaching a phone number creates real routing infrastructure (flows, a queue, an active ","type":"text"},{"text":"MessagingChannel","type":"text","marks":[{"type":"code_inline"}]},{"text":") and consumes a provisioned number, so do it only when the user ","type":"text"},{"text":"explicitly asks","type":"text","marks":[{"type":"strong"}]},{"text":" and has supplied a ","type":"text"},{"text":"provisioned phone number","type":"text","marks":[{"type":"strong"}]},{"text":" (confirm it exists — do not assume or invent one). If either is missing, stop and say what's needed. When both hold, attach it headless via the CLI — do not send the user to Agent Builder. Follow ","type":"text"},{"text":"Headless Telephony CLI","type":"text","marks":[{"type":"link","attrs":{"href":"references/voice-telephony-cli.md","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Delete or Rename an Agent","type":"text"}]},{"type":"paragraph","content":[{"text":"Read the delete/rename sections of ","type":"text"},{"text":"CLI for Agents","type":"text","marks":[{"type":"link","attrs":{"href":"references/salesforce-cli-for-agents.md","title":null}}]},{"text":" and ","type":"text"},{"text":"Metadata & Lifecycle","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-metadata-and-lifecycle.md","title":null}}]},{"text":". Enumerate references and dependencies, show the exact affected bundle, and obtain explicit confirmation before deletion. For rename, create and validate the replacement before deleting the original; verify orphaned metadata afterward.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Test an Agent","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"agentforce-test","type":"text","marks":[{"type":"strong"}]},{"text":" for test-spec design, security coverage, metadata creation, execution, and result analysis. First map the Agent Spec and all reachable routes/actions into coverage targets. Confirm before adding security tests or running tests that can invoke live actions.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Migrate a Legacy Agent to Agent Script","type":"text"}]},{"type":"paragraph","content":[{"text":"User wants to upgrade a legacy ","type":"text"},{"text":"GenAiPlannerBundle","type":"text","marks":[{"type":"code_inline"}]},{"text":" agent into an NGA ","type":"text"},{"text":"AiAuthoringBundle","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Agent Script) agent. May say \"migrate my legacy agent\", \"upgrade to the new builder\", \"convert agent\", \"start migration\", or mention ","type":"text"},{"text":"genAiPlannerBundle","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"aiAuthoringBundle","type":"text","marks":[{"type":"code_inline"}]},{"text":" migration. (Distinct from Einstein ","type":"text"},{"text":"Bot","type":"text","marks":[{"type":"strong"}]},{"text":" upgrades — this covers existing Agentforce ","type":"text"},{"text":"agents","type":"text","marks":[{"type":"strong"}]},{"text":". To convert an Einstein ","type":"text"},{"text":"Bot","type":"text","marks":[{"type":"strong"}]},{"text":" into a new agent, use the ","type":"text"},{"text":"agentforce-bot-upgrade","type":"text","marks":[{"type":"strong"}]},{"text":" skill instead.)","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Required Steps","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Upgrade a Legacy Agent to Agent Script","type":"text","marks":[{"type":"link","attrs":{"href":"references/upgrade-legacy-agent-to-agentscript.md","title":null}}]},{"text":" for the full workflow, exact commands, and the NGA-vs-legacy discriminator. In brief:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set up the project","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"Confirm the target org via a selectable menu — never assume the default org","type":"text","marks":[{"type":"strong"}]},{"text":" (show ","type":"text"},{"text":"sf org list","type":"text","marks":[{"type":"code_inline"}]},{"text":", mark the default, require an explicit pick). Confirm a DX project (create one if needed); run all retrieves from inside it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pick the legacy agent version","type":"text","marks":[{"type":"strong"}]},{"text":" — Two-screen selection: choose the agent, then a ","type":"text"},{"text":"non-NGA","type":"text","marks":[{"type":"strong"}]},{"text":" version. Exclude versions whose planner ","type":"text"},{"text":"PlannerType","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"Atlas__ConcurrentMultiAgentOrchestration","type":"text","marks":[{"type":"code_inline"}]},{"text":" (already NGA).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Migrate via the Connect API","type":"text","marks":[{"type":"strong"}]},{"text":" — POST the chosen ","type":"text"},{"text":"botVersionId","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"migrateAgentToNga","type":"text","marks":[{"type":"code_inline"}]},{"text":". Mutating call; confirm org + id first. Run it in a ","type":"text"},{"text":"subagent","type":"text","marks":[{"type":"strong"}]},{"text":" so the large response (inline ","type":"text"},{"text":"agentScript","type":"text","marks":[{"type":"code_inline"}]},{"text":") does not bloat context — return only ","type":"text"},{"text":"bundleVersionApiName","type":"text","marks":[{"type":"code_inline"}]},{"text":" (+ ","type":"text"},{"text":"conversionWarnings","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"agentResponse","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Retrieve the new bundle","type":"text","marks":[{"type":"strong"}]},{"text":" — Pull the ","type":"text"},{"text":"AiAuthoringBundle","type":"text","marks":[{"type":"code_inline"}]},{"text":" into the project (by ","type":"text"},{"text":"bundleVersionApiName","type":"text","marks":[{"type":"code_inline"}]},{"text":", or map ","type":"text"},{"text":"bundleVersionId → fullName","type":"text","marks":[{"type":"code_inline"}]},{"text":" as fallback).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Analyze, optimize, iterate","type":"text","marks":[{"type":"strong"}]},{"text":" — The migration is a mechanical transform that ignores Agent Script primitives. Hand off to ","type":"text"},{"text":"Comprehend an Existing Agent","type":"text","marks":[{"type":"strong"}]},{"text":", then ","type":"text"},{"text":"Optimize an Agent","type":"text","marks":[{"type":"strong"}]},{"text":", then validate/preview.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Reference Files","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Upgrade a Legacy Agent to Agent Script","type":"text","marks":[{"type":"link","attrs":{"href":"references/upgrade-legacy-agent-to-agentscript.md","title":null}}]},{"text":" — full step-by-step workflow, commands, and discriminator logic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Core Language","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md","title":null}}]},{"text":" — read the migrated ","type":"text"},{"text":".agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" structure","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Optimize an Agent","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Core Language","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md","title":null}}]},{"text":" and scan every reachable path using ","type":"text"},{"text":"Common Control-Flow Pitfalls","type":"text","marks":[{"type":"link","attrs":{"href":"references/common-control-flow-pitfalls.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Load only applicable optimization references: data flow, deterministic logic, reference syntax, human handoff, and voice readiness.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report evidence-backed improvements and obtain approval before editing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Apply only approved changes, compile locally, validate against the org when available, and report the resulting evidence.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Manage MCP Servers","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"MCP Server Management","type":"text","marks":[{"type":"link","attrs":{"href":"references/mcp-management-reference.md","title":null}}]},{"text":" before any MCP operation. Verify the target org, use ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":", keep secrets off command lines, review tools before allowlisting, and require confirmation for destructive or consequential changes.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"The Agent Spec","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent Spec","type":"text","marks":[{"type":"strong"}]},{"text":" is the central artifact this skill produces and consumes. A structured design document representing agent purpose, user outcomes, subagent graph, actions and implementations, variables, subagent posture, deterministic controls (when needed), and behavioral intent.","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent Specs evolve with the agent. Sparse during agent creation (purpose, use cases, planned placeholders). Fleshed out during agent build (flowchart, action implementations mapped, posture choices documented, deterministic controls added only where justified). Reverse-engineered when comprehending existing agents. Critical for advanced troubleshooting, providing reference to compare expected vs. actual behavior. During testing, test coverage maps against it.","type":"text"}]},{"type":"paragraph","content":[{"text":"Produce or update an Agent Spec for greenfield work, material design changes, or analysis whose result changes the documented contract. For a narrow, already-specified repair, record the affected use case and evidence without forcing a full spec rewrite.","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"Design & Agent Spec","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-design-and-spec-creation.md","title":null}}]},{"text":" for Agent Spec structure and production methodology.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Assets","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"assets/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory contains templates and examples. Read when you need a starting point or a concrete reference for artifacts and source files.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/agent-spec-template.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Agent Spec template with all sections and placeholder content. Copy to ","type":"text"},{"text":"\u003cAgentName>-AgentSpec.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" in project directory, then fill in during design. Save Agent Spec as file — significant design artifact that benefits from proper rendering, especially Mermaid Subagent Map diagram.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/agents/local-info-agent-annotated.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Complete annotated example based on Local Info Agent, showing all major Agent Script constructs in context with inline comments explaining why each construct is used. Read when you need concrete reference for how concepts compose into working agent, or as fallback when focused examples in reference files aren't sufficient.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/agents/template-single-subagent.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Compatibility-named focused starter with one ","type":"text"},{"text":"start_agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" execution block and no router or subagent blocks.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/agents/template-multi-subagent.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Minimal agent with multiple subagents and transitions. Copy and modify for complex agents.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/agents/router-first.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Transition-only router example with HyperClassifier and concise router instructions.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/agents/verification-gate.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Identity/authorization gate with protected action availability.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/agents/simple-qa.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", ","type":"text"},{"text":"production-faq.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", and ","type":"text"},{"text":"order-service.agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Complete examples at increasing behavioral and action complexity.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/patterns/README.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Route to focused complete patterns for callbacks, input binding, lifecycle, delegation, and multi-step workflows. Use a pattern only when its stated use-case preconditions apply.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/invocable-apex-template.cls","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — Reference for invocable Apex classes. Copy and modify when complex Apex action implementations are desired.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Important Constraints","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use supported tooling for the evidence needed.","type":"text","marks":[{"type":"strong"}]},{"text":" Use Salesforce CLI and the target org for org-backed validation and release operations. Use the published AgentScript SDK for local parse/compile checks, and invoke related skills only within their documented boundaries.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Only certain implementation types are valid for actions.","type":"text","marks":[{"type":"strong"}]},{"text":" For example, only invocable Apex (not arbitrary Apex classes) can back an action. Similar constraints may apply to Flows and Prompt Templates. When wiring actions to implementations, consult Design & Agent Spec reference file for valid types and stubbing methodology.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sf agent generate test-spec","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is not for agentic use.","type":"text","marks":[{"type":"strong"}]},{"text":" It is interactive, REPL-style command designed for humans. When creating test specs, start from boilerplate template in assets instead.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Issues Quick Reference","type":"text"}]},{"type":"paragraph","content":[{"text":"Internal Error, try again later","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" during publish:","type":"text","marks":[{"type":"strong"}]},{"text":" Server-side compile failure. The 500 doesn't tell you which check failed — walk all four causes in order before asking the user what's wrong. Do NOT stop at cause 1.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent type mismatch on ","type":"text","marks":[{"type":"strong"}]},{"text":"access.default_agent_user","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]},{"text":" Employee agents normally omit ","type":"text"},{"text":"access.default_agent_user","type":"text","marks":[{"type":"code_inline"}]},{"text":"; service agents MUST have it (and the user must hold an Einstein Agent license). See ","type":"text"},{"text":"Design & Agent Spec","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-design-and-spec-creation.md","title":null}}]},{"text":", Section 3. Re-run the query — do not invent the username.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action definition missing ","type":"text","marks":[{"type":"strong"}]},{"text":"outputs:","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" block.","type":"text","marks":[{"type":"strong"}]},{"text":" If any action has ","type":"text"},{"text":"target:","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"inputs:","type":"text","marks":[{"type":"code_inline"}]},{"text":" but no ","type":"text"},{"text":"outputs:","type":"text","marks":[{"type":"code_inline"}]},{"text":", the server-side compiler can't generate return bindings. CLI ","type":"text"},{"text":"validate","type":"text","marks":[{"type":"code_inline"}]},{"text":" and LSP both PASS — only publish fails. See ","type":"text"},{"text":"Known Issues","type":"text","marks":[{"type":"link","attrs":{"href":"references/known-issues.md","title":null}}]},{"text":", Issue 15.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Other structural drift in the ","type":"text","marks":[{"type":"strong"}]},{"text":".agent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" file.","type":"text","marks":[{"type":"strong"}]},{"text":" Diff against a known-good bundle in the same org: ","type":"text"},{"text":"sf project retrieve start --metadata \"AiAuthoringBundle:\u003cknown-working-agent>\" --output-dir /tmp/diff-bundle --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" Compare keyword-by-keyword. Look for missing required-but-undocumented fields, block-ordering drift, or DSL keywords your bundle uses that aren't in the working one.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Genuine transient backend error.","type":"text","marks":[{"type":"strong"}]},{"text":" If 1–3 are clean and the response ","type":"text"},{"text":"requestId","type":"text","marks":[{"type":"code_inline"}]},{"text":" differs across retries, wait 60 s and retry once.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Unable to access Salesforce Agent APIs...","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" during preview:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"default_agent_user","type":"text","marks":[{"type":"code_inline"}]},{"text":" lacks permissions. See ","type":"text"},{"text":"Agent User Setup & Permissions","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-user-setup.md","title":null}}]},{"text":". Do NOT publish as fix — ","type":"text"},{"text":"--use-live-actions","type":"text","marks":[{"type":"code_inline"}]},{"text":" does not require published agent.","type":"text"}]},{"type":"paragraph","content":[{"text":"Permission error referencing different username than configured:","type":"text","marks":[{"type":"strong"}]},{"text":" Same fix as above — error references org's default running user, but root cause is Einstein Agent User permissions.","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent fails with permission error even though current subagent's actions work:","type":"text","marks":[{"type":"strong"}]},{"text":" Planner validates ALL actions across ALL subagents at startup. One missing permission fails entire agent.","type":"text"}]},{"type":"paragraph","content":[{"text":"Apex action returns empty results in live preview but works in simulated:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"WITH USER_MODE","type":"text","marks":[{"type":"code_inline"}]},{"text":" + missing object permissions = silent failure (0 rows, no error). See ","type":"text"},{"text":"Agent User Setup & Permissions","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-user-setup.md","title":null}}]},{"text":", Section 6.2.","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent published, ADL indexed (","type":"text","marks":[{"type":"strong"}]},{"text":"retrieverId","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" populated), but every grounded question returns empty ","type":"text","marks":[{"type":"strong"}]},{"text":"knowledgeSummary","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" / \"I don't have that information\":","type":"text","marks":[{"type":"strong"}]},{"text":" The Einstein Agent User lacks Data Cloud access. Two things to check, in order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Permset/PSL not assigned.","type":"text","marks":[{"type":"strong"}]},{"text":" Run the verification queries from ","type":"text"},{"text":"Agent User Setup, Step 3b.3","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-user-setup.md","title":null}}]},{"text":". If no Data Cloud permset/PSL appears, run the discovery-then-assign procedure (priority: ","type":"text"},{"text":"GenieDataPlatformStarterPsl","type":"text","marks":[{"type":"code_inline"}]},{"text":" PSL → ","type":"text"},{"text":"GenieUserEnhancedSecurity","type":"text","marks":[{"type":"code_inline"}]},{"text":" PS → ","type":"text"},{"text":"DataCloudUser","type":"text","marks":[{"type":"code_inline"}]},{"text":" PS → ","type":"text"},{"text":"DataCloudArchitect","type":"text","marks":[{"type":"code_inline"}]},{"text":" PS).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data Space scope not granted on the permset.","type":"text","marks":[{"type":"strong"}]},{"text":" Currently no API. Setup → Permission Sets → click the assigned permset → \"Data Cloud Data Space Management\" under Apps → Edit → add the ADL's data space (usually ","type":"text"},{"text":"default","type":"text","marks":[{"type":"code_inline"}]},{"text":") → Save. See ","type":"text"},{"text":"Agent User Setup, Step 3b.4","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-user-setup.md","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Links (Deep Detail Lives in References)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Syntax and execution model: ","type":"text"},{"text":"Core Language","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-script-core-language.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent design/spec process: ","type":"text"},{"text":"Design & Agent Spec","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-design-and-spec-creation.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Posture dial (agentic vs deterministic): ","type":"text"},{"text":"Posture & Determinism","type":"text","marks":[{"type":"link","attrs":{"href":"references/posture-and-determinism.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Concrete authoring invariants: ","type":"text"},{"text":"The Zen of AgentScript","type":"text","marks":[{"type":"link","attrs":{"href":"references/zen-of-agentscript.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pattern selection by scenario: ","type":"text"},{"text":"Patterns by Requirement","type":"text","marks":[{"type":"link","attrs":{"href":"references/patterns-by-requirement.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Architecture mechanics, HyperClassifier routing, and migration: ","type":"text"},{"text":"Architecture Patterns","type":"text","marks":[{"type":"link","attrs":{"href":"references/architecture-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validation, preview, and traces: ","type":"text"},{"text":"Validation & Debugging","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-validation-and-debugging.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deploy/publish/activate lifecycle: ","type":"text"},{"text":"Deploy Reference","type":"text","marks":[{"type":"link","attrs":{"href":"references/deploy-reference.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Metadata lifecycle and publish troubleshooting: ","type":"text"},{"text":"Metadata & Lifecycle","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-metadata-and-lifecycle.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ADL provisioning and wiring: ","type":"text"},{"text":"Data Library Reference","type":"text","marks":[{"type":"link","attrs":{"href":"references/data-library-reference.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent access and permissions: ","type":"text"},{"text":"Agent Access Guide","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-access-guide.md","title":null}}]},{"text":", ","type":"text"},{"text":"Agent User Setup","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-user-setup.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Voice modality and telephony agents: ","type":"text"},{"text":"Voice Modality Reference","type":"text","marks":[{"type":"link","attrs":{"href":"references/voice-modality-reference.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Safety review framework: ","type":"text"},{"text":"Safety Review","type":"text","marks":[{"type":"link","attrs":{"href":"references/safety-review-reference.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rubric and review scoring: ","type":"text"},{"text":"Scoring Rubric","type":"text","marks":[{"type":"link","attrs":{"href":"references/scoring-rubric.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Optimization patterns: ","type":"text"},{"text":"Pattern 1 — Data Flow","type":"text","marks":[{"type":"link","attrs":{"href":"references/optimization-pattern-1-data-flow.md","title":null}}]},{"text":", ","type":"text"},{"text":"Pattern 2 — Deterministic Logic","type":"text","marks":[{"type":"link","attrs":{"href":"references/optimization-pattern-2-deterministic-logic.md","title":null}}]},{"text":", ","type":"text"},{"text":"Pattern 3 — Reference Syntax","type":"text","marks":[{"type":"link","attrs":{"href":"references/optimization-pattern-3-reference-syntax.md","title":null}}]},{"text":", ","type":"text"},{"text":"Pattern 4 — Escalation","type":"text","marks":[{"type":"link","attrs":{"href":"references/optimization-pattern-4-escalation.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MCP server registration and tool whitelisting: ","type":"text"},{"text":"MCP Server Management","type":"text","marks":[{"type":"link","attrs":{"href":"references/mcp-management-reference.md","title":null}}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"agentforce-generate","author":"@skillopedia","source":{"stars":1139,"repo_name":"sf-skills","origin_url":"https://github.com/forcedotcom/sf-skills/blob/HEAD/skills/agentforce-generate/SKILL.md","repo_owner":"forcedotcom","body_sha256":"80b21f8540c53f85506e249bdcbb8eeaefafab9b3c08d14ee8a7fc643528f1ba","cluster_key":"4c5080ff1677e78156c615841d2470311eb9b80224d36d61a8a04dbd772d0f36","clean_bundle":{"format":"clean-skill-bundle-v1","source":"forcedotcom/sf-skills/skills/agentforce-generate/SKILL.md","text_bytes":890241,"source_commit":"a8a18ce1e4ba12e69b4073eab98ce31bfbd8271e","skills_sh_rank":3843,"attachment_count":73,"text_attachments":73,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/agentforce-generate/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"ai-agent-development","category_label":"AI"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"ai-agent-development","metadata":{"domains":["Agentforce"],"version":"0.11","cliTools":[{"tool":["corepack"],"semver":">=0.25.0"},{"tool":["curl"],"semver":">=7.0.0"},{"tool":["git"],"semver":">=2.0.0"},{"tool":["jq"],"semver":">=1.6.0"},{"tool":["node"],"semver":">=20.0.0"},{"tool":["npm"],"semver":">=9.0.0"},{"tool":["pnpm"],"semver":">=8.0.0"},{"tool":["python3"],"semver":">=3.10.0"},{"tool":["sf"],"semver":">=2.139.6"}],"minApiVersion":"66.0","relatedSkills":["agentforce-bot-upgrade","agentforce-observe","agentforce-test","automation-flow-generate","integration-connectivity-generate","platform-apex-generate","platform-metadata-deploy"]},"import_tag":"skills-sh-top20k-2026-11","description":"Build, modify, audit, repair, optimize, debug, and deploy agents with Agentforce Agent Script. TRIGGER when: user creates, reviews, or changes .agent files or aiAuthoringBundle metadata; asks to fix AgentScript, audit an existing agent, run an AgentScript health check, common-pitfall review, or baseline-versus-candidate repair loop; changes a response, action, subagent, route, state flow, or Agent Spec; migrates or upgrades a legacy GenAiPlannerBundle Agentforce agent (NOT an Einstein Bot) to an aiAuthoringBundle (Agent Script) agent; previews, debugs, deploys, publishes, or tests agents; uses sf agent generate/preview/publish/test; or manages Agentforce MCP servers, tools, assets, or authentication. DO NOT TRIGGER when: Apex, Flow, Prompt Template, Experience Cloud, or general Salesforce CLI work is unrelated to Agent Script; the source is an Einstein Bot (BotDefinition/BotVersion) — use agentforce-bot-upgrade; or the primary input is a production session or trace ID rather than an agent artifact."}},"renderedAt":1793762220946}

Agent Script Skill What This Skill Is For This skill is for developing Agentforce agents, primarily with Agent Script, Salesforce's scripting language for AI agents. Org-backed workflows require an Agentforce license, API v66.0 or later, and an Einstein Agent User. Static authoring and review can proceed without org access. CRITICAL: Agent Script is NOT AppleScript, JavaScript, Python, or any other language. Do NOT confuse Agent Script syntax or semantics with any other language you have been trained on. Agent Script agents are defined by metadata: an file (agent behavior) plus a sibling file…