afv-library/skills/agentforce-generate/references/instruction-resolution.md

546 lines
17 KiB
Markdown

<!-- Parent: adlc-author/SKILL.md -->
# Instruction Resolution
> How Agent Script instructions are processed at runtime: from static text to dynamic LLM prompts.
---
## 1. Three Phases of Instruction Resolution
Agent Script instructions go through three distinct phases at runtime. Understanding these phases is critical for writing effective instructions and debugging unexpected behavior.
```
Phase 1: Pre-LLM Setup
(deterministic -- runs before the LLM sees anything)
|
v
Phase 2: LLM Reasoning
(non-deterministic -- LLM processes the assembled prompt)
|
v
Phase 3: Post-Action Loop
(deterministic -- runs after an action completes, then loops back to Phase 1)
```
---
## 2. Phase 1: Pre-LLM Resolution
During Phase 1, the Agent Script runtime evaluates deterministic constructs in `instructions: ->` blocks. This happens BEFORE the LLM sees any text.
### What Happens in Phase 1
1. **`if`/`else` evaluation**: Conditions are evaluated against current variable values. Only the matching branch is included in the prompt.
2. **Variable injection**: `{!@variables.X}` tokens are replaced with current values.
3. **`run` execution**: Deterministic `run @actions.X` calls execute and their outputs are captured.
4. **`set` execution**: Variable assignments execute immediately.
5. **`transition to`**: If reached, the subagent switch happens immediately (LLM is never called).
### Phase 1 Example
Given this instruction block:
```
reasoning:
instructions: ->
# 1. Post-action check (from previous loop)
if @variables.refund_approved == True:
| Your refund has been processed. Reference: {!@variables.refund_id}
transition to @subagent.confirmation
# 2. Pre-LLM data loading
if @variables.data_loaded == False:
run @actions.load_customer_profile
with customer_id = @variables.customer_id
set @variables.risk_score = @outputs.risk_score
set @variables.tier = @outputs.tier
set @variables.data_loaded = True
# 3. Dynamic instructions
| Customer tier: {!@variables.tier}, Risk score: {!@variables.risk_score}
if @variables.risk_score >= 80:
| HIGH RISK -- Offer full cash refund to retain this customer.
| Do NOT offer store credit. Prioritize retention.
if @variables.risk_score < 80:
| STANDARD -- Offer $10 store credit as goodwill.
| Only escalate to cash refund if customer insists.
```
**First turn resolution** (variables at defaults):
- `refund_approved == True` -> False. Skip this block.
- `data_loaded == False` -> True. Execute `run @actions.load_customer_profile`. Variables now set.
- Set `data_loaded = True`.
- Inject `{!@variables.tier}` -> `"gold"`, `{!@variables.risk_score}` -> `85`.
- `risk_score >= 80` -> True. Include high-risk instructions.
- `risk_score < 80` -> False. Skip standard instructions.
**What the LLM actually sees**:
```
Customer tier: gold, Risk score: 85
HIGH RISK -- Offer full cash refund to retain this customer.
Do NOT offer store credit. Prioritize retention.
```
---
## 3. Phase 2: LLM Processing
In Phase 2, the LLM receives the assembled prompt and produces a response. The LLM sees:
### The 4-Message Prompt Structure
The Agent Script runtime assembles a 4-message prompt for the LLM:
| # | Message Role | Content Source | Purpose |
|---|---|---|---|
| 1 | **System** | `system: instructions:` + agent metadata | Global persona, safety rules, capabilities |
| 2 | **System** | `subagent: reasoning: instructions:` (resolved from Phase 1) | Subagent-specific operating instructions |
| 3 | **User/Assistant** | Conversation history (all turns) | Context for the current request |
| 4 | **System** | Available actions + their descriptions | Tool palette the LLM can choose from |
### What the LLM Decides
Based on the assembled prompt, the LLM:
1. **Selects an action** (if applicable) from the available actions list
2. **Fills slot parameters** (`...` values) from conversation context
3. **Generates a text response** to send to the user
4. **Decides whether to transition** (if a transition action is available and appropriate)
### What the LLM Does NOT See
- Raw `if`/`else` blocks (already resolved in Phase 1)
- `run` statements (already executed in Phase 1)
- `set` statements (already executed)
- `available when` conditions (already evaluated -- hidden actions are simply absent)
- `after_reasoning` blocks (run after the LLM, not shown to it)
---
## 4. Phase 3: Post-Action Loop
After the LLM selects and executes an action, the system loops back to Phase 1 for **re-resolution**. This is the post-action loop pattern described in the SKILL.md architecture section.
### Loop Sequence
```
1. Phase 1 resolves instructions (first time)
2. Phase 2: LLM reasons and selects an action
3. Action executes -> outputs captured in variables
4. Phase 1 re-resolves instructions (with updated variables)
- Post-action checks at TOP of instructions fire
- New data is injected into the prompt
5. Phase 2: LLM reasons again with updated context
6. Repeat until: transition, escalation, or no action selected
```
### Why Post-Action Checks Go at the TOP
Place post-action checks at the TOP of `instructions: ->` so they fire immediately on re-resolution:
```
reasoning:
instructions: ->
# POST-ACTION CHECK (at TOP -- fires on re-resolution)
if @variables.order_cancelled == True:
| Your order has been cancelled successfully.
transition to @subagent.confirmation
# These instructions are for the FIRST entry (before action runs)
| I can help you cancel your order.
| What is your order number?
```
If the check were at the BOTTOM, the LLM would see the "ask for order number" instructions again even after the cancellation succeeded, causing confusion.
---
## 5. Recommended Instruction Order
Within a `instructions: ->` block, follow this order for maximum clarity:
```
reasoning:
instructions: ->
# 1. POST-ACTION CHECKS (deterministic transitions)
if @variables.action_completed == True:
transition to @subagent.next_step
# 2. PRE-LLM DATA LOADING (deterministic actions)
if @variables.data_needed == True:
run @actions.load_data
with id = @variables.record_id
set @variables.loaded_data = @outputs.result
# 3. CONDITIONAL INSTRUCTIONS (based on state)
if @variables.is_verified == True:
| Full access granted. You can:
| - View account details
| - Make changes
| - Request refunds
if @variables.is_verified == False:
| Please verify your identity first.
| I need your email address and order number.
# 4. STATIC INSTRUCTIONS (always included)
| Be concise and professional.
| Always confirm before making changes.
```
---
## 6. Common Instruction Patterns
### Pattern 1: Security Gate
Prevent access to sensitive actions until identity is verified:
```
reasoning:
instructions: ->
if @variables.is_verified == False:
| You must verify your identity before I can help with account changes.
| Please provide your email address.
if @variables.is_verified == True:
| Identity verified. I can now help with account changes.
| What would you like to do?
actions:
update_account: @actions.update_account_info
description: "Update account information"
available when @variables.is_verified == True
with field = ...
with value = ...
```
The `available when` guard hides the action from the LLM until verification passes. The conditional instructions tell the user what to do.
### Pattern 2: Data-Dependent Instructions
Load data first, then tailor instructions based on the result:
```
reasoning:
instructions: ->
run @actions.get_account_status
with account_id = @variables.account_id
set @variables.account_status = @outputs.status
set @variables.balance = @outputs.balance
| Account status: {!@variables.account_status}
| Current balance: {!@variables.balance}
if @variables.account_status == "delinquent":
| IMPORTANT: This account is delinquent.
| Collect payment before processing any other requests.
| Offer payment plan options if customer cannot pay in full.
if @variables.account_status == "active":
| This account is in good standing.
| Process requests normally.
```
### Pattern 3: Action Chaining
Execute one action, then use its output to drive the next:
```
reasoning:
instructions: ->
# Post-action check: case was created in previous loop
if @variables.case_id != "":
| Case {!@variables.case_id} has been created.
run @actions.assign_case
with case_id = @variables.case_id
with priority = @variables.priority
transition to @subagent.case_confirmation
| I need to collect some information to create a support case.
| What is the issue you're experiencing?
```
### Pattern 4: Multi-Condition Routing
Route based on multiple variable values:
```
reasoning:
instructions: ->
if @variables.intent == "billing" and @variables.is_verified == True:
| I can help with your billing question.
transition to @subagent.billing_support
if @variables.intent == "billing" and @variables.is_verified == False:
| For billing questions, I need to verify your identity first.
transition to @subagent.identity_verification
if @variables.intent == "general":
| How can I help you today?
```
---
## 7. Anti-Patterns to Avoid
### Anti-Pattern 1: Nested If Blocks
```
# WRONG -- Agent Script does not support nested if or else if
if @variables.tier == "gold":
if @variables.is_verified == True:
| VIP treatment
else:
| Verify first
# CORRECT -- Use compound conditions
if @variables.tier == "gold" and @variables.is_verified == True:
| VIP treatment
if @variables.tier == "gold" and @variables.is_verified == False:
| Verify first
```
### Anti-Pattern 2: Post-Action Check at Bottom
```
# WRONG -- Check at bottom; LLM sees stale instructions on re-resolution
reasoning:
instructions: ->
| What is your order number?
if @variables.order_status != "":
transition to @subagent.show_status
# CORRECT -- Check at TOP
reasoning:
instructions: ->
if @variables.order_status != "":
transition to @subagent.show_status
| What is your order number?
```
### Anti-Pattern 3: Persona in Subagent Instructions
```
# WRONG -- Persona text duplicated in every subagent
reasoning:
instructions: |
You are a friendly, professional customer service agent.
Help the customer with their order.
# CORRECT -- Persona in system instructions, subagent has operational instructions only
system:
instructions: |
You are a friendly, professional customer service agent.
subagent order_support:
reasoning:
instructions: ->
| Help the customer check their order status.
| Ask for the order number if not provided.
```
### Anti-Pattern 4: Using `|` When `->` Is Needed
```
# WRONG -- Using literal mode when conditionals are needed
reasoning:
instructions: |
if @variables.is_verified == True:
Show account details.
# The above sends the literal text "if @variables.is_verified == True:" to the LLM!
# CORRECT -- Use procedural mode for conditionals
reasoning:
instructions: ->
if @variables.is_verified == True:
| Show account details.
```
### Anti-Pattern 5: Missing Variable Injection Syntax
```
# WRONG -- Variable name as literal text
reasoning:
instructions: ->
| Your order ID is @variables.order_id
# CORRECT -- Use injection syntax
reasoning:
instructions: ->
| Your order ID is {!@variables.order_id}
```
### Anti-Pattern 6: `run` Inside `after_reasoning`
While `run` compiles inside `after_reasoning:`, its runtime behavior is inconsistent across bundle types. Prefer using `run` in `reasoning: instructions: ->` or `reasoning: actions:` instead.
```
# RISKY -- run in after_reasoning has inconsistent behavior
after_reasoning:
run @actions.log_event
with event = "turn_completed"
# SAFER -- Use instructions: -> for deterministic runs
reasoning:
instructions: ->
# Post-action logging
if @variables.last_action != "":
run @actions.log_event
with event = @variables.last_action
```
---
## 8. Syntax Patterns Reference
### Literal Mode (`|`)
Static text passed directly to the LLM. No evaluation occurs:
```
instructions: |
Help the customer with their order.
Be professional and concise.
```
Or with the `|` prefix on each line (inside procedural mode):
```
instructions: ->
| Help the customer with their order.
| Be professional and concise.
```
### Procedural Mode (`->`)
Enables conditionals, variable injection, and deterministic actions:
```
instructions: ->
if @variables.condition == True:
| Text shown when condition is true.
else:
| Text shown when condition is false.
```
### Variable Injection
```
| Your order {!@variables.order_id} is {!@variables.status}.
```
### Deterministic Run
```
run @actions.load_data
with param = @variables.value
set @variables.result = @outputs.field
```
### Deterministic Set
```
set @variables.counter = @variables.counter + 1
```
### Deterministic Transition
```
transition to @subagent.next_subagent
```
### Conditional Transition
```
if @variables.all_collected == True:
transition to @subagent.confirmation
```
---
## 9. Programmatic Trace Access
To verify how instructions were resolved at runtime, use the trace files generated by `sf agent preview`.
### Trace File Location
```
.sfdx/agents/{BundleName}/sessions/{sessionId}/traces/{planId}.json
```
### Reading Instruction Resolution from Traces
```bash
# Extract the resolved instructions that the LLM received
jq -r '.planTrace.steps[] | select(.type == "LLM_STEP") | .input' \
~/.sf/sfdx/agents/MyAgent/sessions/*/traces/*.json
# Extract the LLM's response
jq -r '.planTrace.steps[] | select(.type == "LLM_STEP") | .output' \
~/.sf/sfdx/agents/MyAgent/sessions/*/traces/*.json
# Check which variables were set during resolution
jq -r '.planTrace.steps[] | select(.type == "ACTION_STEP") | {name: .name, pre: .preVars, post: .postVars}' \
~/.sf/sfdx/agents/MyAgent/sessions/*/traces/*.json
```
### Verifying Phase 1 Resolution
To confirm that `if`/`else` blocks resolved correctly, compare the trace's `LLM_STEP` input against your `instructions: ->` block. The LLM input should contain only the branches that matched, with all `{!@variables.X}` tokens replaced with actual values.
If the trace shows unexpected instruction text:
1. Check that you used `->` mode (not `|` mode) when conditionals are present
2. Verify variable values at the time of resolution (check `preVars` on preceding `ACTION_STEP`)
3. Confirm that `if` conditions use the correct comparison operators
### Using STDM for Production Trace Analysis
For production agents, use the Session Trace Data Model (STDM) in Data Cloud to access trace data programmatically. The STDM captures `LLM_STEP` records with `input` and `output` fields that contain the resolved prompt and LLM response. This is useful for auditing instruction resolution at scale across hundreds of live sessions.
---
## 10. Resolution Across Subagent Transitions
When a subagent transition occurs (via `@utils.transition to @subagent.X` or `transition to @subagent.X`), instruction resolution starts fresh in the new subagent:
1. The current subagent's remaining instructions are NOT processed
2. The new subagent's `before_reasoning:` runs (if present)
3. The new subagent's `reasoning: instructions:` resolves from Phase 1
4. The LLM receives the new subagent's assembled prompt
**Important**: Variables persist across transitions. A variable set in Subagent A is available in Subagent B. This is how you pass data between subagents:
```
# Subagent A: Collect data
subagent collect_info:
reasoning:
instructions: ->
| Please provide your order number.
actions:
capture_order: @actions.get_order_id
with input = ...
set @variables.order_id = @outputs.order_id
after_reasoning:
if @variables.order_id != "":
transition to @subagent.process_order
# Subagent B: Use the data
subagent process_order:
reasoning:
instructions: ->
# order_id is available from Subagent A
| Processing order {!@variables.order_id}...
run @actions.get_order_details
with order_id = @variables.order_id
set @variables.order_status = @outputs.status
```