Skip to content

Commit bc8d0c9

Browse files
committed
feat(llm): split system/user prompts to enable provider-side caching
DeterministicChain now accepts WithSystemPrompt() to send a stable System message separate from the variable User template. Providers cache byte-identical system messages across calls (Anthropic 90% discount, OpenAI 50%, Google 75%). Split clarify, planning, decompose, and expand templates into stable system prompts and per-call user templates. Bootstrap agent templates left as-is (single-use, caching has no benefit).
1 parent d6d1d06 commit bc8d0c9

3 files changed

Lines changed: 83 additions & 49 deletions

File tree

internal/agents/core/eino.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,36 @@ type DeterministicChain[T any] struct {
3838
name string
3939
}
4040

41+
// ChainOption configures optional DeterministicChain behavior.
42+
type ChainOption func(*chainConfig)
43+
44+
type chainConfig struct {
45+
systemPrompt string
46+
}
47+
48+
// WithSystemPrompt sets a stable system message prepended before the user template.
49+
// This enables prompt caching: providers cache the system message across calls
50+
// when it's byte-identical (Anthropic 90% discount, OpenAI 50%, Google 75%).
51+
func WithSystemPrompt(prompt string) ChainOption {
52+
return func(c *chainConfig) {
53+
c.systemPrompt = prompt
54+
}
55+
}
56+
4157
// NewDeterministicChain creates a standardized Eino chain for deterministic tasks.
4258
func NewDeterministicChain[T any](
4359
ctx context.Context,
4460
name string,
4561
chatModel model.BaseChatModel,
4662
templateStr string,
63+
opts ...ChainOption,
4764
) (*DeterministicChain[T], error) {
4865

66+
var cfg chainConfig
67+
for _, o := range opts {
68+
o(&cfg)
69+
}
70+
4971
// 1. Template Node (Custom Lambda)
5072
tmpl, err := template.New(name).Parse(templateStr)
5173
if err != nil {
@@ -57,9 +79,13 @@ func NewDeterministicChain[T any](
5779
if err := tmpl.Execute(&buf, input); err != nil {
5880
return nil, fmt.Errorf("execute template: %w", err)
5981
}
60-
return []*schema.Message{
61-
{Role: schema.User, Content: buf.String()},
62-
}, nil
82+
msgs := make([]*schema.Message, 0, 2)
83+
// System message goes first (stable prefix, cached by providers).
84+
if cfg.systemPrompt != "" {
85+
msgs = append(msgs, schema.SystemMessage(cfg.systemPrompt))
86+
}
87+
msgs = append(msgs, &schema.Message{Role: schema.User, Content: buf.String()})
88+
return msgs, nil
6389
}
6490

6591
// 2. Model Node (Lambda Adapter)

internal/agents/impl/planning_agents.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ func (a *ClarifyingAgent) Run(ctx context.Context, input core.Input) (core.Outpu
5858
ctx,
5959
a.Name(),
6060
chatModel.BaseChatModel,
61-
config.SystemPromptClarifyingAgent,
61+
config.ClarifyingAgentUserTemplate,
62+
core.WithSystemPrompt(config.ClarifyingAgentSystemPrompt),
6263
)
6364
if err != nil {
6465
return core.Output{}, fmt.Errorf("create chain: %w", err)
@@ -228,7 +229,8 @@ func (a *PlanningAgent) Run(ctx context.Context, input core.Input) (core.Output,
228229
ctx,
229230
a.Name(),
230231
chatModel.BaseChatModel,
231-
config.SystemPromptPlanningAgent,
232+
config.PlanningAgentUserTemplate,
233+
core.WithSystemPrompt(config.PlanningAgentSystemPrompt),
232234
)
233235
if err != nil {
234236
return core.Output{}, fmt.Errorf("create chain: %w", err)
@@ -328,7 +330,8 @@ func (a *DecompositionAgent) Run(ctx context.Context, input core.Input) (core.Ou
328330
ctx,
329331
a.Name(),
330332
chatModel.BaseChatModel,
331-
config.SystemPromptDecompositionAgent,
333+
config.DecompositionAgentUserTemplate,
334+
core.WithSystemPrompt(config.DecompositionAgentSystemPrompt),
332335
)
333336
if err != nil {
334337
return core.Output{}, fmt.Errorf("create chain: %w", err)
@@ -419,7 +422,8 @@ func (a *ExpandAgent) Run(ctx context.Context, input core.Input) (core.Output, e
419422
ctx,
420423
a.Name(),
421424
chatModel.BaseChatModel,
422-
config.SystemPromptExpandAgent,
425+
config.ExpandAgentUserTemplate,
426+
core.WithSystemPrompt(config.ExpandAgentSystemPrompt),
423427
)
424428
if err != nil {
425429
return core.Output{}, fmt.Errorf("create chain: %w", err)

internal/config/prompts.go

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,9 @@ CLASSIFICATION RULES:
490490
491491
JSON ONLY, no explanation:`
492492

493-
// SystemPromptClarifyingAgent is the system prompt for the Clarifying Agent.
494-
// Use with Eino ChatTemplate.
495-
const SystemPromptClarifyingAgent = `You are a Senior Technical Architect helping a user refine their software engineering goal.
493+
// ClarifyingAgentSystemPrompt is the stable system message for the Clarifying Agent.
494+
// Sent as a System message to enable provider-side prompt caching.
495+
const ClarifyingAgentSystemPrompt = `You are a Senior Technical Architect helping a user refine their software engineering goal.
496496
Your job is to ask clarifying questions to turn a vague request into a concrete specification.
497497
498498
**Guidelines:**
@@ -512,26 +512,26 @@ Every question MUST include concrete options so the user can pick, modify, or ex
512512
Format: "[Topic]: [Option A] vs [Option B]. [Brief tradeoff]."
513513
This lets the user reply "first one" or "B but also add X" instead of writing paragraphs.
514514
515-
**Input Context:**
516-
Goal: {{.Goal}}
517-
{{if .Context}}
518-
Architectural Knowledge:
519-
{{.Context}}
520-
{{end}}
521-
{{if .History}}Previous Clarifications:
522-
{{.History}}{{end}}
523-
524515
**Output Format (JSON):**
525516
{
526517
"questions": ["Topic: Option A vs Option B. Tradeoff note."],
527518
"goal_summary": "Concise one-line summary for UI display (max 80 chars)",
528519
"enriched_goal": "A detailed technical specification using facts from context...",
529520
"is_ready_to_plan": boolean // true if sufficient info gathered
530-
}
531-
`
521+
}`
532522

533-
// SystemPromptPlanningAgent is the system prompt for the Planning Agent.
534-
const SystemPromptPlanningAgent = `You are an Engineering Lead creating a development plan.
523+
// ClarifyingAgentUserTemplate is the per-call user message template (variable content).
524+
const ClarifyingAgentUserTemplate = `Goal: {{.Goal}}
525+
{{if .Context}}
526+
Architectural Knowledge:
527+
{{.Context}}
528+
{{end}}
529+
{{if .History}}Previous Clarifications:
530+
{{.History}}{{end}}`
531+
532+
533+
// PlanningAgentSystemPrompt is the stable system message for the Planning Agent.
534+
const PlanningAgentSystemPrompt = `You are an Engineering Lead creating a development plan.
535535
Your input is an "Enriched Goal" and relevant context from the project knowledge graph.
536536
Your job is to decompose this goal into a sequential list of actionable execution tasks.
537537
@@ -547,10 +547,6 @@ Use the minimum number of tasks needed. Do NOT over-decompose.
547547
4. **Verification**: Each task needs acceptance criteria and a validation command.
548548
5. **No Overlap**: Do NOT split implementation and testing of the same feature into separate tasks. When explicit tasks are provided, use them directly.
549549
550-
**Input Context:**
551-
- Enriched Goal: {{.Goal}}
552-
- Knowledge Graph: {{.Context}}
553-
554550
**Output Format (JSON):**
555551
{
556552
"tasks": [
@@ -566,12 +562,17 @@ Use the minimum number of tasks needed. Do NOT over-decompose.
566562
}
567563
],
568564
"rationale": "Why this approach and how it respects architectural constraints..."
569-
}
570-
`
565+
}`
566+
567+
// PlanningAgentUserTemplate is the per-call user message template.
568+
const PlanningAgentUserTemplate = `Enriched Goal: {{.Goal}}
569+
570+
Knowledge Graph:
571+
{{.Context}}`
571572

572-
// SystemPromptDecompositionAgent is the system prompt for the Decomposition Agent.
573-
// Breaks enriched goals into 3-5 high-level phases for interactive planning.
574-
const SystemPromptDecompositionAgent = `You are an Engineering Lead decomposing a development goal into high-level phases.
573+
574+
// DecompositionAgentSystemPrompt is the stable system message for the Decomposition Agent.
575+
const DecompositionAgentSystemPrompt = `You are an Engineering Lead decomposing a development goal into high-level phases.
575576
Break the goal into 3-5 logical phases that deliver incremental value.
576577
577578
**Guidelines:**
@@ -580,10 +581,6 @@ Break the goal into 3-5 logical phases that deliver incremental value.
580581
3. Each phase should expand into 2-4 tasks. No overlap between phases.
581582
4. Use the Knowledge Graph Context to align with existing patterns and constraints.
582583
583-
**Input Context:**
584-
- Enriched Goal: {{.EnrichedGoal}}
585-
- Knowledge Graph: {{.Context}}
586-
587584
**Output Format (JSON):**
588585
{
589586
"phases": [
@@ -596,12 +593,17 @@ Break the goal into 3-5 logical phases that deliver incremental value.
596593
}
597594
],
598595
"rationale": "Overall reasoning for this phase breakdown and sequencing..."
599-
}
600-
`
596+
}`
597+
598+
// DecompositionAgentUserTemplate is the per-call user message template.
599+
const DecompositionAgentUserTemplate = `Enriched Goal: {{.EnrichedGoal}}
601600
602-
// SystemPromptExpandAgent is the system prompt for the Expand Agent.
603-
// Generates detailed tasks for a single phase during interactive planning.
604-
const SystemPromptExpandAgent = `You are an Engineering Lead expanding a development phase into detailed tasks.
601+
Knowledge Graph:
602+
{{.Context}}`
603+
604+
605+
// ExpandAgentSystemPrompt is the stable system message for the Expand Agent.
606+
const ExpandAgentSystemPrompt = `You are an Engineering Lead expanding a development phase into detailed tasks.
605607
Generate 2-4 self-contained tasks that fully accomplish this phase.
606608
607609
**Guidelines:**
@@ -611,12 +613,6 @@ Generate 2-4 self-contained tasks that fully accomplish this phase.
611613
4. Use the Knowledge Graph Context to respect existing patterns and constraints.
612614
5. Each task needs acceptance criteria and validation steps.
613615
614-
**Input Context:**
615-
- Phase Title: {{.PhaseTitle}}
616-
- Phase Description: {{.PhaseDescription}}
617-
- Overall Goal: {{.EnrichedGoal}}
618-
- Knowledge Graph: {{.Context}}
619-
620616
**CRITICAL - Constraint Compliance:**
621617
If the context contains architectural CONSTRAINTS (marked as CRITICAL, MUST, mandatory), ALL tasks must comply with them.
622618
@@ -636,8 +632,16 @@ If the context contains architectural CONSTRAINTS (marked as CRITICAL, MUST, man
636632
}
637633
],
638634
"rationale": "Why these tasks accomplish the phase and in this order..."
639-
}
640-
`
635+
}`
636+
637+
// ExpandAgentUserTemplate is the per-call user message template.
638+
const ExpandAgentUserTemplate = `Phase Title: {{.PhaseTitle}}
639+
Phase Description: {{.PhaseDescription}}
640+
Overall Goal: {{.EnrichedGoal}}
641+
642+
Knowledge Graph:
643+
{{.Context}}`
644+
641645

642646
// SystemPromptSimplifyAgent is the system prompt for the Simplify Agent.
643647
// Reduces code complexity and line count while preserving behavior.

0 commit comments

Comments
 (0)