Skip to content

Commit c939f36

Browse files
committed
Fully dynamic sub-agent prompts: buildSubagentPrompt() replaces static category constants
Removed all 7 static prompt constants (buildSystem, debugSystem, testSystem, reviewSystem, refactorSystem, configSystem, researchSystem) and classifyGoal(). New buildSubagentPrompt(goal, context) constructs a per-task prompt: - Embeds the actual goal text — every prompt is unique - Detects task intent from keywords (debug/test/review/refactor/config/research) - Builds persona, methodology, and focus strings dynamically - Includes context when provided - Keeps under ~120 tokens Three-tier resolution preserved: 1. system field in delegate_tasks (parent-crafted) 2. KODE_SYSTEM / config override (user-configured) 3. buildSubagentPrompt() (dynamic Go-level fallback) Updated 12 contract tests to verify: - goal text embedded in prompt - context included when provided - intent detection per category (debugger, test, review, etc.) - empty goal falls back to subagentSystem - different goals produce different prompts - max length 800 chars All 12 packages pass, race detector clean, E2E pass.
1 parent fab8349 commit c939f36

3 files changed

Lines changed: 149 additions & 140 deletions

File tree

cmd/kode/subagent.go

Lines changed: 66 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import (
1818

1919
// ── Sub-agent System Prompts ────────────────────────────────────────
2020
//
21-
// Sub-agents receive a system prompt matched to their task's category.
22-
// The parent agent can also provide a custom system prompt via the
23-
// `system` field in delegate_tasks. When neither is provided, use
24-
// classifyGoal() to pick the best category.
21+
// Sub-agents receive a system prompt tailored to their specific task.
22+
// The parent agent can provide a custom prompt via the `system` field
23+
// in delegate_tasks. When not provided, buildSubagentPrompt() constructs
24+
// one dynamically by analyzing the goal text — embedding the actual task
25+
// so every prompt is unique.
2526

2627
const subagentSystem = `You are kode working on a single focused sub-task.
2728
Complete the assigned goal and report what you did.
@@ -30,74 +31,70 @@ Use the shell tool when you need information or to make changes.
3031
Report: what you built, what files changed, any issues encountered.
3132
Be concise. Output your answer, then stop.`
3233

33-
// Category-specific system prompts.
34-
// Each is optimized for a different task type.
35-
36-
const buildSystem = `You are kode — an expert engineer building production code.
37-
Architect and implement with confidence. Consider edge cases, error handling,
38-
and maintainability from the start. Write clean, idiomatic code that another
39-
engineer can read and extend. Report what you built and what files changed.`
40-
41-
const debugSystem = `You are kode — an expert debugger.
42-
Find the root cause. Isolate the bug before you write any fix. Prove the fix
43-
works by reasoning through the normal and edge cases. Report what was broken,
44-
the root cause, and how you fixed it.`
45-
46-
const testSystem = `You are kode — an expert in testing and quality.
47-
Write thorough tests. Cover the happy path, edge cases, and failure modes.
48-
Use table-driven tests where appropriate. Tests should be readable and
49-
maintainable. Report what you tested and the coverage pattern.`
50-
51-
const reviewSystem = `You are kode — a senior engineer reviewing code.
52-
Read every line critically. Look for logic errors, security vulnerabilities,
53-
performance issues, and style problems. Be constructive — propose specific
54-
improvements. Report all findings with file:line references.`
55-
56-
const refactorSystem = `You are kode — an expert in code architecture.
57-
Preserve behavior. Change structure only. Clean up technical debt while
58-
ensuring nothing breaks. Report what you changed and why the new structure
59-
is better.`
60-
61-
const configSystem = `You are kode — a DevOps engineer configuring systems.
62-
Make every change reproducible and documented. Use minimal permissions.
63-
Test the configuration after changing it. Report what you set up and how
64-
to verify it works.`
65-
66-
const researchSystem = `You are kode — a technical researcher.
67-
Explore thoroughly. Read source code, docs, and examples before concluding.
68-
Cite your sources. Synthesize findings into a clear recommendation.
69-
Report what you found and your recommended action.`
70-
71-
// classifyGoal returns a system prompt matched to the task's category
72-
// by analyzing the goal text. Falls back to the default subagentSystem
73-
// when no strong signal is detected.
74-
func classifyGoal(goal string) string {
34+
// buildSubagentPrompt constructs a system prompt tailored to the
35+
// specific goal and context. Every call produces a unique prompt
36+
// because the goal text is embedded.
37+
//
38+
// The returned string is ~90-120 tokens. Falls back to subagentSystem
39+
// when the goal is empty.
40+
func buildSubagentPrompt(goal, context string) string {
41+
if goal == "" {
42+
return subagentSystem
43+
}
44+
45+
// Detect task type from goal keywords
7546
lower := strings.ToLower(goal)
47+
matches := func(kws ...string) bool {
48+
for _, kw := range kws {
49+
if strings.Contains(lower, kw) {
50+
return true
51+
}
52+
}
53+
return false
54+
}
55+
56+
// Pick persona and methodology based on detected intent
57+
persona := "an expert engineer"
58+
methodology := "Architect and implement with confidence."
59+
focus := "Write clean, well-structured code."
60+
7661
switch {
77-
case containsAny(lower, "fix", "bug", "error", "crash", "broken", "incorrect", "wrong", "fail"):
78-
return debugSystem
79-
case containsAny(lower, "test", "spec", "coverage", "assert", "unit test", "integration test"):
80-
return testSystem
81-
case containsAny(lower, "review", "audit", "check", "inspect", "verify", "validate"):
82-
return reviewSystem
83-
case containsAny(lower, "refactor", "clean up", "simplify", "rename", "extract", "restructure", "reorganize"):
84-
return refactorSystem
85-
case containsAny(lower, "setup", "config", "install", "docker", "ci", "deploy", "provision"):
86-
return configSystem
87-
case containsAny(lower, "research", "explain", "compare", "understand", "find", "investigate", "analyze"):
88-
return researchSystem
89-
default:
90-
return buildSystem // greenfield / build tasks
62+
case matches("fix", "bug", "error", "crash", "broken", "incorrect", "wrong", "fail"):
63+
persona = "an expert debugger"
64+
methodology = "Find the root cause before writing any fix."
65+
focus = "Isolate the bug, prove the fix, and verify edge cases."
66+
case matches("test", "spec", "coverage", "assert"):
67+
persona = "a testing engineer"
68+
methodology = "Write thorough tests. Cover happy path, edge cases, and failures."
69+
focus = "Use clear assertions and descriptive test names."
70+
case matches("review", "audit", "check", "inspect", "verify", "validate", "inspect"):
71+
persona = "a senior engineer reviewing code"
72+
methodology = "Read every line critically."
73+
focus = "Find logic errors, security holes, and style issues. Be constructive."
74+
case matches("refactor", "clean up", "simplify", "rename", "extract", "restructure"):
75+
persona = "an architecture expert"
76+
methodology = "Preserve behavior. Change only the structure."
77+
focus = "Eliminate technical debt without breaking anything."
78+
case matches("setup", "config", "install", "docker", "ci", "deploy", "provision"):
79+
persona = "a DevOps engineer"
80+
methodology = "Make every change reproducible and minimal."
81+
focus = "Test the configuration after changing it."
82+
case matches("research", "explain", "compare", "understand", "investigate", "analyze"):
83+
persona = "a technical researcher"
84+
methodology = "Explore thoroughly before concluding."
85+
focus = "Read source code and docs. Cite findings. Recommend action."
9186
}
92-
}
9387

94-
func containsAny(s string, substrs ...string) bool {
95-
for _, sub := range substrs {
96-
if strings.Contains(s, sub) {
97-
return true
98-
}
88+
// Build the prompt with the actual goal embedded
89+
prompt := fmt.Sprintf("You are kode — %s.\n%s\n%s\nGoal: %s.",
90+
persona, methodology, focus, goal)
91+
92+
if context != "" {
93+
prompt += fmt.Sprintf("\n\nContext:\n%s", context)
9994
}
100-
return false
95+
96+
prompt += "\n\nReport what you built and what files changed."
97+
return prompt
10198
}
10299

103100
// subagentResult is the JSON contract written to stdout.
@@ -271,8 +268,8 @@ func subagentCmd(args []string) error {
271268
resolved := config.LoadConfig(config.CLIFlags{})
272269

273270
// Resolve system prompt for this sub-agent.
274-
// Priority: 1) task file override 2) user config override 3) classifyGoal 4) default
275-
systemMsg := classifyGoal(cfg.goal)
271+
// Priority: 1) task file override 2) user config override 3) dynamic build
272+
systemMsg := buildSubagentPrompt(cfg.goal, cfg.context)
276273
switch {
277274
case taskSystem != "":
278275
systemMsg = taskSystem

cmd/kode/subagent_contract_test.go

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -591,95 +591,98 @@ func TestSubagentSystemPrompt_Minimal(t *testing.T) {
591591
}
592592
}
593593

594-
// ── 9. classifyGoal ─────────────────────────────────────────────────
594+
// ── 9. buildSubagentPrompt ──────────────────────────────────────────
595595

596-
func TestClassifyGoal_Build(t *testing.T) {
597-
got := classifyGoal("Create a user model with CRUD")
598-
if got != buildSystem {
599-
t.Errorf("classifyGoal('Create...') = %q..., want buildSystem", truncate(got, 40))
596+
func TestBuildSubagentPrompt_IncludesGoal(t *testing.T) {
597+
got := buildSubagentPrompt("Create a user model with CRUD", "")
598+
if !strings.Contains(got, "Create a user model with CRUD") {
599+
t.Errorf("prompt should contain the goal text, got:\n%s", got)
600600
}
601601
}
602602

603-
func TestClassifyGoal_Debug(t *testing.T) {
603+
func TestBuildSubagentPrompt_IncludesContext(t *testing.T) {
604+
got := buildSubagentPrompt("Build auth middleware", "Uses gin, models at internal/models/user.go")
605+
if !strings.Contains(got, "Uses gin") {
606+
t.Errorf("prompt should contain context, got:\n%s", got)
607+
}
608+
}
609+
610+
func TestBuildSubagentPrompt_EmptyGoal(t *testing.T) {
611+
got := buildSubagentPrompt("", "")
612+
if got != subagentSystem {
613+
t.Errorf("empty goal should return subagentSystem, got:\n%s", got)
614+
}
615+
}
616+
617+
func TestBuildSubagentPrompt_DebugDetection(t *testing.T) {
604618
for _, goal := range []string{"fix OOM bug in parser", "crash in websocket handler", "broken import path"} {
605-
got := classifyGoal(goal)
606-
if got != debugSystem {
607-
t.Errorf("classifyGoal(%q) = %q..., want debugSystem", goal, truncate(got, 40))
619+
got := buildSubagentPrompt(goal, "")
620+
if !strings.Contains(got, "debugger") && !strings.Contains(got, "root cause") {
621+
t.Errorf("goal %q should produce debug prompt, got:\n%s", goal, got)
608622
}
609623
}
610624
}
611625

612-
func TestClassifyGoal_Test(t *testing.T) {
626+
func TestBuildSubagentPrompt_TestDetection(t *testing.T) {
613627
for _, goal := range []string{"write unit tests for auth", "add coverage for models", "create integration test for API"} {
614-
got := classifyGoal(goal)
615-
if got != testSystem {
616-
t.Errorf("classifyGoal(%q) = %q..., want testSystem", goal, truncate(got, 40))
628+
got := buildSubagentPrompt(goal, "")
629+
if !strings.Contains(got, "test") && !strings.Contains(got, "assert") && !strings.Contains(got, "coverage") {
630+
t.Errorf("goal %q should produce test prompt, got:\n%s", goal, got)
617631
}
618632
}
619633
}
620634

621-
func TestClassifyGoal_Review(t *testing.T) {
622-
got := classifyGoal("review PR #42 for security issues")
623-
if got != reviewSystem {
624-
t.Errorf("classifyGoal('review...') = %q..., want reviewSystem", truncate(got, 40))
635+
func TestBuildSubagentPrompt_ReviewDetection(t *testing.T) {
636+
got := buildSubagentPrompt("review PR #42 for security issues", "")
637+
if !strings.Contains(got, "reviewing") && !strings.Contains(got, "critically") {
638+
t.Errorf("review goal should produce review prompt, got:\n%s", got)
625639
}
626640
}
627641

628-
func TestClassifyGoal_Refactor(t *testing.T) {
629-
got := classifyGoal("refactor the monolith into handlers")
630-
if got != refactorSystem {
631-
t.Errorf("classifyGoal('refactor...') = %q..., want refactorSystem", truncate(got, 40))
642+
func TestBuildSubagentPrompt_RefactorDetection(t *testing.T) {
643+
got := buildSubagentPrompt("refactor the monolith into handlers", "")
644+
if !strings.Contains(got, "architecture") && !strings.Contains(got, "Preserve behavior") {
645+
t.Errorf("refactor goal should produce architecture prompt, got:\n%s", got)
632646
}
633647
}
634648

635-
func TestClassifyGoal_Config(t *testing.T) {
636-
got := classifyGoal("setup Docker CI pipeline")
637-
if got != configSystem {
638-
t.Errorf("classifyGoal('setup...') = %q..., want configSystem", truncate(got, 40))
649+
func TestBuildSubagentPrompt_ConfigDetection(t *testing.T) {
650+
got := buildSubagentPrompt("setup Docker CI pipeline", "")
651+
if !strings.Contains(got, "DevOps") && !strings.Contains(got, "reproducible") {
652+
t.Errorf("config goal should produce DevOps prompt, got:\n%s", got)
639653
}
640654
}
641655

642-
func TestClassifyGoal_Research(t *testing.T) {
643-
got := classifyGoal("research Go HTTP router performance")
644-
if got != researchSystem {
645-
t.Errorf("classifyGoal('research...') = %q..., want researchSystem", truncate(got, 40))
656+
func TestBuildSubagentPrompt_ResearchDetection(t *testing.T) {
657+
got := buildSubagentPrompt("research Go HTTP router performance", "")
658+
if !strings.Contains(got, "researcher") && !strings.Contains(got, "Explore thoroughly") {
659+
t.Errorf("research goal should produce research prompt, got:\n%s", got)
646660
}
647661
}
648662

649-
func TestClassifyGoal_FallbackToBuild(t *testing.T) {
650-
got := classifyGoal("do something random")
651-
if got != buildSystem {
652-
t.Errorf("classifyGoal('do something random') = %q..., want buildSystem", truncate(got, 40))
663+
func TestBuildSubagentPrompt_FallbackToBuild(t *testing.T) {
664+
got := buildSubagentPrompt("do something random", "")
665+
if !strings.Contains(got, "expert engineer") {
666+
t.Errorf("generic goal should produce engineer prompt, got:\n%s", got)
653667
}
654668
}
655669

656-
// ── 10. Category System Prompts ──────────────────────────────────────
657-
658-
func TestCategoryPrompts_NotEmpty(t *testing.T) {
659-
prompts := []struct {
660-
name string
661-
p string
662-
}{
663-
{"buildSystem", buildSystem},
664-
{"debugSystem", debugSystem},
665-
{"testSystem", testSystem},
666-
{"reviewSystem", reviewSystem},
667-
{"refactorSystem", refactorSystem},
668-
{"configSystem", configSystem},
669-
{"researchSystem", researchSystem},
670-
{"subagentSystem", subagentSystem},
670+
func TestBuildSubagentPrompt_UniquePerGoal(t *testing.T) {
671+
p1 := buildSubagentPrompt("Build auth middleware", "")
672+
p2 := buildSubagentPrompt("Create user model", "")
673+
if p1 == p2 {
674+
t.Error("different goals should produce different prompts")
671675
}
672-
for _, sp := range prompts {
673-
if sp.p == "" {
674-
t.Errorf("%s must not be empty", sp.name)
675-
}
676-
if len(sp.p) > 800 {
677-
t.Errorf("%s too long: %d chars (max 800)", sp.name, len(sp.p))
678-
}
676+
}
677+
678+
func TestBuildSubagentPrompt_MaxLength(t *testing.T) {
679+
got := buildSubagentPrompt("Create a full CRUD REST API with JWT auth and PostgreSQL storage", "Uses gin, GORM, models at internal/models/")
680+
if len(got) > 800 {
681+
t.Errorf("prompt too long: %d chars (max 800)\n%s", len(got), got)
679682
}
680683
}
681684

682-
// ── 11. Integration ─────────────────────────────────────────────────
685+
// ── 10. Integration ─────────────────────────────────────────────────
683686

684687
func TestDelegateTasks_PipesStderr(t *testing.T) {
685688
tool := &delegateTasksTool{

docs/SUBAGENTS.md

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ Every sub-agent receives a system prompt **tailored to its task** — not a one-
219219
|----------|--------|------|
220220
| 1 (highest) | `system` field in `delegate_tasks` | Parent explicitly provides a custom prompt |
221221
| 2 | `KODE_SYSTEM` / config file `system` | User-configured global override |
222-
| 3 | `classifyGoal()` auto-detection | Fallback — analyses the goal text |
222+
| 3 | `buildSubagentPrompt()` dynamic generation | Go-level fallback — analyzes goal text and constructs a per-task prompt with the actual goal embedded |
223223

224224
### Parent-crafted prompts
225225

@@ -240,21 +240,30 @@ The parent agent (kode) is instructed to write system prompts for each sub-task.
240240
}
241241
```
242242

243-
### Auto-classified prompts
243+
### Dynamically generated prompts
244244

245-
When no `system` field is provided, `classifyGoal()` analyzes the goal text and picks a matching category:
245+
When no `system` field is provided, `buildSubagentPrompt()` constructs a prompt at runtime by analyzing the goal text. Every call produces a **unique prompt** because the actual goal is embedded:
246246

247-
| Category | Trigger keywords | Prompt persona |
248-
|----------|-----------------|----------------|
249-
| **build** (default) | *(no match)* | Expert engineer building production code |
250-
| **debug** | fix, bug, error, crash, broken, incorrect | Expert debugger — find root cause first |
251-
| **test** | test, spec, coverage, assert, unit test | Testing & quality expert |
252-
| **review** | review, audit, check, inspect, verify | Senior engineer reading every line critically |
253-
| **refactor** | refactor, clean up, simplify, rename, extract | Code architecture expert — preserve behavior |
254-
| **config** | setup, config, install, docker, ci, deploy | DevOps engineer — reproducible, minimal permissions |
255-
| **research** | research, explain, compare, understand, find | Technical researcher — explore thoroughly |
247+
```text
248+
You are kode — an expert debugger.
249+
Find the root cause before writing any fix.
250+
Isolate the bug, prove the fix, and verify edge cases.
251+
Goal: Fix OOM bug in parser.js.
256252
257-
Each category prompt is a focused ~80-100 tokens with a distinct persona and methodology.
253+
Report what you built and what files changed.
254+
```
255+
256+
The generator detects task type from keywords and builds the persona, methodology, and focus dynamically:
257+
258+
| Detected intent | Persona | Methodology |
259+
|----------------|---------|-------------|
260+
| fix, bug, error, crash, broken | Expert debugger | Find root cause before writing fix |
261+
| test, spec, coverage, assert | Testing engineer | Write thorough tests — happy, edge, failure |
262+
| review, audit, check, inspect | Senior engineer reviewing code | Read every line critically |
263+
| refactor, clean up, simplify, rename | Architecture expert | Preserve behavior, change only structure |
264+
| setup, config, docker, ci, deploy | DevOps engineer | Reproducible, minimal permissions |
265+
| research, explain, compare, analyze | Technical researcher | Explore thoroughly before concluding |
266+
| *(default / no match)* | Expert engineer | Architect and implement with confidence |
258267

259268
### Default fallback
260269

@@ -281,7 +290,7 @@ The temp file written by `delegate_tasks` carries the system prompt:
281290
}
282291
```
283292

284-
When invoked directly via `kode subagent --goal "..."`, the `--goal` path uses `classifyGoal()` (no manual override) while `--task <file>` reads the `system` field from the JSON file.
293+
When invoked directly via `kode subagent --goal "..."`, the `--goal` path uses `buildSubagentPrompt()` (no manual override) while `--task <file>` reads the `system` field from the JSON file.
285294

286295
## Configuration
287296

@@ -319,7 +328,7 @@ The sub-agent system has three test layers:
319328

320329
| Layer | Tests | Runner | What's verified |
321330
|-------|-------|--------|-----------------|
322-
| **Contract tests** | 48 | `go test ./cmd/kode/` | Flag parsing, JSON stdout protocol, exit codes, tool schema, config parsing, classifyGoal categories, system prompt length/empty checks |
331+
| **Contract tests** | ~50 | `go test ./cmd/kode/` | Flag parsing, JSON stdout protocol, exit codes, tool schema, config parsing, buildSubagentPrompt dynamic generation (goal embedded, context, intent detection, uniqueness, max length) |
323332
| **E2E tests** | 16 | `KODE_E2E=true go test ./cmd/kode/ -run "TestE2E_"` | Real subprocess spawning, tool → binary pipeline, stderr protocol, concurrency, timeouts, custom system prompt threading |
324333
| **Full suite** | All | `go test -race ./...` | 12 packages, race-detector clean |
325334

0 commit comments

Comments
 (0)