Skip to content

Commit 1d9fc50

Browse files
committed
feat: Improve output replacement pattern
1 parent b280d82 commit 1d9fc50

File tree

7 files changed

+48
-19
lines changed

7 files changed

+48
-19
lines changed

.agents/AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ magec/
6666
│ │ ├── recorder.go # ConversationRecorder + ConversationRecorderSSE (dual-perspective)
6767
│ │ ├── flowfilter.go # Flow response filtering by responseAgent
6868
│ │ ├── sessionensure.go # Idempotent session creation (prevents overwriting ContextGuard state)
69-
│ │ └── sessionstate.go # Seeds outputKey values into session state on creation
69+
│ │ └── sessionstate.go # Seeds outputKey values into session state for {{agent.output:key}} resolution
7070
│ ├── clients/ # Client type registry + runtime
7171
│ │ ├── provider.go # Provider interface: Type(), DisplayName(), ConfigSchema()
7272
│ │ ├── registry.go # Register(), ValidateConfig() with oneOf support
@@ -234,7 +234,8 @@ log:
234234
- **A2A protocol**: Agents/flows with `A2A.Enabled` get JSON-RPC endpoints via `a2a-go` + ADK `adka2a`. Agent cards auto-generated with capabilities and skills. SSE streaming for responses
235235
- **Dual-perspective conversation recording**: Middleware chains recorder twice: "admin" perspective (all events, before FlowResponseFilter) and "user" perspective (filtered, after). Each conversation has a `ParentID` linking the pair
236236
- **Store dual-copy pattern**: Store maintains `rawData` (unexpanded, with `${VAR}` refs) and `data` (env-expanded). API responses use raw data, runtime uses expanded. Secret values injected as env vars before expansion
237-
- **Session middleware**: `SessionEnsure` prevents overwriting existing sessions (protects ContextGuard summaries). `SessionStateSeed` injects empty outputKey values so flow agents don't fail on template vars
237+
- **Session middleware**: `SessionEnsure` prevents overwriting existing sessions (protects ContextGuard summaries). `SessionStateSeed` injects empty outputKey values so `{{agent.output:key}}` references in flow agent prompts resolve correctly
238+
- **InstructionProvider pattern**: Agents use `InstructionProvider` instead of static `Instruction` strings to bypass ADK's built-in `{variable}` substitution (which conflicts with curly braces in prompts, JSON examples, scripts, etc). Magec resolves its own `{{agent.output:key}}` pattern from session state inside the provider. Plain `{text}` in prompts is never touched
238239
- **Flow wrapAgent pattern**: Same agent can appear in multiple flow steps — `wrapAgent()` creates uniquely-named delegate agents to satisfy ADK's single-parent constraint
239240

240241
### Design Philosophy

server/agent/agent.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"os"
2525
"os/exec"
2626
"path/filepath"
27+
"regexp"
2728
"strconv"
2829
"strings"
2930
"time"
@@ -170,12 +171,12 @@ func New(ctx context.Context, agents []store.AgentDefinition, backends []store.B
170171
instruction := buildInstruction(agentDef, mcpServerMap, skillMap, filepath.Join("data", "skills"), memorySvc)
171172

172173
agentCfg := llmagent.Config{
173-
Name: agentDef.ID,
174-
Model: llmModel,
175-
Description: agentDef.Name,
176-
Instruction: instruction,
177-
Toolsets: toolsets,
178-
OutputKey: agentDef.OutputKey,
174+
Name: agentDef.ID,
175+
Model: llmModel,
176+
Description: agentDef.Name,
177+
InstructionProvider: makeInstructionProvider(instruction),
178+
Toolsets: toolsets,
179+
OutputKey: agentDef.OutputKey,
179180
}
180181

181182
adkAgent, err := llmagent.New(agentCfg)
@@ -582,3 +583,26 @@ func buildInstruction(agentDef store.AgentDefinition, mcpServerMap map[string]st
582583

583584
return instruction
584585
}
586+
587+
var stateVarRegex = regexp.MustCompile(`\{\{agent\.output:([a-zA-Z_][a-zA-Z0-9_]*)\}\}`)
588+
589+
func makeInstructionProvider(template string) llmagent.InstructionProvider {
590+
if !stateVarRegex.MatchString(template) {
591+
return func(_ agent.ReadonlyContext) (string, error) {
592+
return template, nil
593+
}
594+
}
595+
return func(ctx agent.ReadonlyContext) (string, error) {
596+
return stateVarRegex.ReplaceAllStringFunc(template, func(match string) string {
597+
sub := stateVarRegex.FindStringSubmatch(match)
598+
if len(sub) < 2 {
599+
return match
600+
}
601+
val, err := ctx.ReadonlyState().Get(sub[1])
602+
if err != nil || val == nil {
603+
return ""
604+
}
605+
return fmt.Sprintf("%v", val)
606+
}), nil
607+
}
608+
}

server/middleware/sessionstate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
// SessionStateSeed intercepts session creation requests (POST to
1414
// /apps/{app}/users/{user}/sessions/{session}) and injects empty outputKey
1515
// values into the session state. This ensures that flow agents referencing
16-
// {outputKey} template variables in their system prompts don't fail when
16+
// {{agent.output:outputKey}} template variables in their system prompts don't fail when
1717
// sessions are created by the Voice UI or other API clients that don't
1818
// pre-seed the state themselves.
1919
func SessionStateSeed(next http.Handler, dataStore *store.Store) http.Handler {

website/content/docs/agents.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ If you leave the system prompt empty, Magec uses a default prompt that makes the
4848

4949
### Output Key
5050

51-
The **output key** is used when the agent participates in a [flow](/docs/flows/). It saves the agent's output under a named key in the flow's shared state. Other agents in the same flow can then reference that output using `{key_name}` in their own prompts.
51+
The **output key** is used when the agent participates in a [flow](/docs/flows/). It saves the agent's output under a named key in the flow's shared state. Other agents in the same flow can then reference that output using `{{agent.output:key_name}}` in their own prompts.
5252

53-
For example, if a "researcher" agent has `outputKey: research_results`, a later "writer" agent can include `{research_results}` in its prompt to access what the researcher found.
53+
For example, if a "researcher" agent has `outputKey: research_results`, a later "writer" agent can include `{{agent.output:research_results}}` in its prompt to access what the researcher found.
5454

5555
| Field | Description |
5656
|-------|-------------|
5757
| `systemPrompt` | The full instruction text. Supports multi-line, markdown, examples, and any formatting you want. |
58-
| `outputKey` | Named key for flow data passing. Other agents in the same flow can reference it with `{key_name}`. |
58+
| `outputKey` | Named key for flow data passing. Other agents in the same flow can reference it with `{{agent.output:key_name}}`. |
5959

6060
## LLM
6161

website/content/docs/flows.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ Each step receives the accumulated output of all previous steps as context. The
7171

7272
1. Give an agent an `outputKey` in its [configuration](/docs/agents/) (e.g., `research_results`)
7373
2. The agent's output is saved under that key in the flow's shared state
74-
3. Later agents can reference it with `{research_results}` in their system prompt
74+
3. Later agents can reference it with `{{agent.output:research_results}}` in their system prompt
7575
4. Magec replaces the placeholder with the actual output at runtime
7676

77+
> **Note:** Magec uses `{{agent.output:variable}}` instead of `{variable}` for state references. This avoids conflicts with curly braces in your prompts, JSON examples, or any other content that uses `{` and `}`.
78+
7779
This lets you build precise data pipelines. A "researcher" outputs structured findings, a "writer" references those findings in its prompt, a "reviewer" references both. Each agent sees exactly the context it needs.
7880

7981
In parallel steps, all branches receive the same input. Their outputs are concatenated and passed to whatever comes next.

website/public/docs/agents/index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,8 @@ <h3 id="output-key">
214214
Output Key
215215
<a class="heading-anchor" href="#output-key" aria-label="Link to this section">#</a>
216216
</h3>
217-
<p>The <strong>output key</strong> is used when the agent participates in a <a href="/docs/flows/">flow</a>. It saves the agent&rsquo;s output under a named key in the flow&rsquo;s shared state. Other agents in the same flow can then reference that output using <code>{key_name}</code> in their own prompts.</p>
218-
<p>For example, if a &ldquo;researcher&rdquo; agent has <code>outputKey: research_results</code>, a later &ldquo;writer&rdquo; agent can include <code>{research_results}</code> in its prompt to access what the researcher found.</p>
217+
<p>The <strong>output key</strong> is used when the agent participates in a <a href="/docs/flows/">flow</a>. It saves the agent&rsquo;s output under a named key in the flow&rsquo;s shared state. Other agents in the same flow can then reference that output using <code>{{agent.output:key_name}}</code> in their own prompts.</p>
218+
<p>For example, if a &ldquo;researcher&rdquo; agent has <code>outputKey: research_results</code>, a later &ldquo;writer&rdquo; agent can include <code>{{agent.output:research_results}}</code> in its prompt to access what the researcher found.</p>
219219
<table>
220220
<thead>
221221
<tr>
@@ -230,7 +230,7 @@ <h3 id="output-key">
230230
</tr>
231231
<tr>
232232
<td><code>outputKey</code></td>
233-
<td>Named key for flow data passing. Other agents in the same flow can reference it with <code>{key_name}</code>.</td>
233+
<td>Named key for flow data passing. Other agents in the same flow can reference it with <code>{{agent.output:key_name}}</code>.</td>
234234
</tr>
235235
</tbody>
236236
</table>
@@ -305,11 +305,11 @@ <h2 id="skills">
305305
<p>While the <strong>system prompt</strong> defines who the agent is, skills define <strong>what it knows</strong>. An agent with a &ldquo;Return Policy&rdquo; skill and a &ldquo;Product Catalog&rdquo; skill becomes a capable customer support representative without you having to cram everything into one massive prompt.</p>
306306
<p>Skills are especially powerful when shared across agents. Update a product catalog skill once, and every agent that uses it sees the change.</p>
307307
<p>For a deeper comparison of when to use skills vs. creating specialized agents, see <a href="/docs/skills-vs-agents/">Skills vs. Agents</a>.</p>
308-
<h2 id="context-guard-hahahugoshortcode9s4hbhb">
308+
<h2 id="context-guard-hahahugoshortcode66s4hbhb">
309309
Context Guard
310310
<span class="docs-badge">Experimental</span>
311311

312-
<a class="heading-anchor" href="#context-guard-hahahugoshortcode9s4hbhb" aria-label="Link to this section">#</a>
312+
<a class="heading-anchor" href="#context-guard-hahahugoshortcode66s4hbhb" aria-label="Link to this section">#</a>
313313
</h2>
314314
<p>Long conversations eventually hit the model&rsquo;s token limit and fail. Context Guard watches the conversation size and automatically summarizes older messages before that happens. Recent messages stay untouched.</p>
315315
<p>You set it up per-agent inside the <strong>LLM</strong> section. Two strategies:</p>

website/public/docs/flows/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,11 @@ <h3 id="how-data-flows-between-agents">
238238
<ol>
239239
<li>Give an agent an <code>outputKey</code> in its <a href="/docs/agents/">configuration</a> (e.g., <code>research_results</code>)</li>
240240
<li>The agent&rsquo;s output is saved under that key in the flow&rsquo;s shared state</li>
241-
<li>Later agents can reference it with <code>{research_results}</code> in their system prompt</li>
241+
<li>Later agents can reference it with <code>{{agent.output:research_results}}</code> in their system prompt</li>
242242
<li>Magec replaces the placeholder with the actual output at runtime</li>
243243
</ol>
244+
<blockquote>
245+
<p><strong>Note:</strong> Magec uses <code>{{agent.output:variable}}</code> instead of <code>{variable}</code> for state references. This avoids conflicts with curly braces in your prompts, JSON examples, or any other content that uses <code>{</code> and <code>}</code>.</p></blockquote>
244246
<p>This lets you build precise data pipelines. A &ldquo;researcher&rdquo; outputs structured findings, a &ldquo;writer&rdquo; references those findings in its prompt, a &ldquo;reviewer&rdquo; references both. Each agent sees exactly the context it needs.</p>
245247
<p>In parallel steps, all branches receive the same input. Their outputs are concatenated and passed to whatever comes next.</p>
246248
<h2 id="response-agents">

0 commit comments

Comments
 (0)