Merge pull request #123 from bnema/feat/global-writing-guidance
feat(agent): add universal writing guidance
5 files · +106 -15
M packages/agent/cli.go +1 -0
| @@ -2414,6 +2414,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||
| 2414 | 2414 | Reasoning: r.Reasoning, | |
| 2415 | 2415 | OnReasoningChanged: runtime.SetReasoning, | |
| 2416 | 2416 | SystemPrompt: r.SystemPrompt, | |
| 2417 | + | WritingGuidance: WritingGuidance(), | |
| 2417 | 2418 | Tools: r.ToolRegistry, | |
| 2418 | 2419 | MaxSteps: r.MaxSteps, | |
| 2419 | 2420 | CWD: r.CWD, | |
| @@ -2414,6 +2414,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||||
| 2414 | Reasoning: r.Reasoning, | 2414 | Reasoning: r.Reasoning, | ||
| 2415 | OnReasoningChanged: runtime.SetReasoning, | 2415 | OnReasoningChanged: runtime.SetReasoning, | ||
| 2416 | SystemPrompt: r.SystemPrompt, | 2416 | SystemPrompt: r.SystemPrompt, | ||
| 2417 | + | WritingGuidance: WritingGuidance(), | |||
| 2417 | Tools: r.ToolRegistry, | 2418 | Tools: r.ToolRegistry, | ||
| 2418 | MaxSteps: r.MaxSteps, | 2419 | MaxSteps: r.MaxSteps, | ||
| 2419 | CWD: r.CWD, | 2420 | CWD: r.CWD, | ||
M packages/agent/modes/auto_subagents_test.go +28 -0
| @@ -15,6 +15,34 @@ import ( | |||
| 15 | 15 | ||
| 16 | 16 | const autoSubagentsTestTimeout = 10 * time.Second | |
| 17 | 17 | ||
| 18 | + | func TestAutoSubagentsSystemPromptKeepsWritingGuidanceLast(t *testing.T) { | |
| 19 | + | const guidance = "universal writing guidance" | |
| 20 | + | iv := &Interactive{ | |
| 21 | + | agent: &core.Agent{System: "base system\n\n" + guidance}, | |
| 22 | + | cfg: InteractiveConfig{ | |
| 23 | + | AutoSubagentsSystemAddendum: "auto-subagents guidance", | |
| 24 | + | WritingGuidance: guidance, | |
| 25 | + | Supervisor: &subagents.Supervisor{}, | |
| 26 | + | }, | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | iv.applyAutoSubagentsSystemPrompt(true) | |
| 30 | + | if count := strings.Count(iv.agent.System, guidance); count != 1 { | |
| 31 | + | t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System) | |
| 32 | + | } | |
| 33 | + | if !strings.HasSuffix(iv.agent.System, guidance) { | |
| 34 | + | t.Fatalf("writing guidance is not final after enabling: %q", iv.agent.System) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | iv.applyAutoSubagentsSystemPrompt(false) | |
| 38 | + | if count := strings.Count(iv.agent.System, guidance); count != 1 { | |
| 39 | + | t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System) | |
| 40 | + | } | |
| 41 | + | if !strings.HasSuffix(iv.agent.System, guidance) { | |
| 42 | + | t.Fatalf("writing guidance is not final after disabling: %q", iv.agent.System) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 18 | 46 | func TestAutoSubagentsSystemPromptKeepsProfileManifestWhileTogglingDelegationGuidance(t *testing.T) { | |
| 19 | 47 | const onDemand = "delegate only on an explicit user request" | |
| 20 | 48 | iv := &Interactive{ | |
| @@ -15,6 +15,34 @@ import ( | |||||
| 15 | 15 | ||||
| 16 | const autoSubagentsTestTimeout = 10 * time.Second | 16 | const autoSubagentsTestTimeout = 10 * time.Second | ||
| 17 | 17 | ||||
| 18 | + | func TestAutoSubagentsSystemPromptKeepsWritingGuidanceLast(t *testing.T) { | |||
| 19 | + | const guidance = "universal writing guidance" | |||
| 20 | + | iv := &Interactive{ | |||
| 21 | + | agent: &core.Agent{System: "base system\n\n" + guidance}, | |||
| 22 | + | cfg: InteractiveConfig{ | |||
| 23 | + | AutoSubagentsSystemAddendum: "auto-subagents guidance", | |||
| 24 | + | WritingGuidance: guidance, | |||
| 25 | + | Supervisor: &subagents.Supervisor{}, | |||
| 26 | + | }, | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | iv.applyAutoSubagentsSystemPrompt(true) | |||
| 30 | + | if count := strings.Count(iv.agent.System, guidance); count != 1 { | |||
| 31 | + | t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System) | |||
| 32 | + | } | |||
| 33 | + | if !strings.HasSuffix(iv.agent.System, guidance) { | |||
| 34 | + | t.Fatalf("writing guidance is not final after enabling: %q", iv.agent.System) | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | iv.applyAutoSubagentsSystemPrompt(false) | |||
| 38 | + | if count := strings.Count(iv.agent.System, guidance); count != 1 { | |||
| 39 | + | t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System) | |||
| 40 | + | } | |||
| 41 | + | if !strings.HasSuffix(iv.agent.System, guidance) { | |||
| 42 | + | t.Fatalf("writing guidance is not final after disabling: %q", iv.agent.System) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 18 | func TestAutoSubagentsSystemPromptKeepsProfileManifestWhileTogglingDelegationGuidance(t *testing.T) { | 46 | func TestAutoSubagentsSystemPromptKeepsProfileManifestWhileTogglingDelegationGuidance(t *testing.T) { | ||
| 19 | const onDemand = "delegate only on an explicit user request" | 47 | const onDemand = "delegate only on an explicit user request" | ||
| 20 | iv := &Interactive{ | 48 | iv := &Interactive{ | ||
M packages/agent/modes/interactive.go +25 -15
| @@ -32,17 +32,18 @@ import ( | |||
| 32 | 32 | ||
| 33 | 33 | // InteractiveConfig configures the interactive loop. | |
| 34 | 34 | type InteractiveConfig struct { | |
| 35 | - | Terminal tui.Terminal | |
| 36 | - | Theme tui.Theme | |
| 37 | - | Model string | |
| 38 | - | Provider string | |
| 39 | - | AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar | |
| 40 | - | BaseURL string | |
| 41 | - | Reasoning string | |
| 42 | - | SystemPrompt string | |
| 43 | - | Tools core.Registry | |
| 44 | - | MaxSteps int | |
| 45 | - | CWD string | |
| 35 | + | Terminal tui.Terminal | |
| 36 | + | Theme tui.Theme | |
| 37 | + | Model string | |
| 38 | + | Provider string | |
| 39 | + | AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar | |
| 40 | + | BaseURL string | |
| 41 | + | Reasoning string | |
| 42 | + | SystemPrompt string | |
| 43 | + | WritingGuidance string | |
| 44 | + | Tools core.Registry | |
| 45 | + | MaxSteps int | |
| 46 | + | CWD string | |
| 46 | 47 | ||
| 47 | 48 | // Startup resource fields are loaded inputs available to list before the | |
| 48 | 49 | // transcript. They are never sent to the provider or persisted by the view. | |
| @@ -9176,6 +9177,11 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { | |||
| 9176 | 9177 | ||
| 9177 | 9178 | sys, _ := i.agent.PromptConfig() | |
| 9178 | 9179 | changed := false | |
| 9180 | + | if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" { | |
| 9181 | + | var removed bool | |
| 9182 | + | sys, removed = removeLastAutoSubagentsAddendum(sys, guidance) | |
| 9183 | + | changed = changed || removed | |
| 9184 | + | } | |
| 9179 | 9185 | for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- { | |
| 9180 | 9186 | var removed bool | |
| 9181 | 9187 | sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx]) | |
| @@ -9184,7 +9190,7 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { | |||
| 9184 | 9190 | i.managedAutoSubagentsAddenda = nil | |
| 9185 | 9191 | ||
| 9186 | 9192 | addenda := autoSubagentsAddenda(i.cfg, orchestrating) | |
| 9187 | - | if changed && len(addenda) > 0 { | |
| 9193 | + | if changed { | |
| 9188 | 9194 | sys = strings.TrimRight(sys, "\n") | |
| 9189 | 9195 | } | |
| 9190 | 9196 | for _, addendum := range addenda { | |
| @@ -9195,12 +9201,16 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { | |||
| 9195 | 9201 | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | |
| 9196 | 9202 | changed = true | |
| 9197 | 9203 | } | |
| 9204 | + | if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" { | |
| 9205 | + | if sys != "" { | |
| 9206 | + | sys += "\n\n" | |
| 9207 | + | } | |
| 9208 | + | sys += guidance | |
| 9209 | + | changed = true | |
| 9210 | + | } | |
| 9198 | 9211 | if !changed { | |
| 9199 | 9212 | return | |
| 9200 | 9213 | } | |
| 9201 | - | if len(addenda) == 0 { | |
| 9202 | - | sys = strings.TrimRight(sys, "\n") + "\n" | |
| 9203 | - | } | |
| 9204 | 9214 | i.agent.SetSystemPrompt(sys) | |
| 9205 | 9215 | } | |
| 9206 | 9216 | ||
| @@ -32,17 +32,18 @@ import ( | |||||
| 32 | 32 | ||||
| 33 | // InteractiveConfig configures the interactive loop. | 33 | // InteractiveConfig configures the interactive loop. | ||
| 34 | type InteractiveConfig struct { | 34 | type InteractiveConfig struct { | ||
| 35 | - | Terminal tui.Terminal | 35 | + | Terminal tui.Terminal |
| 36 | - | Theme tui.Theme | 36 | + | Theme tui.Theme |
| 37 | - | Model string | 37 | + | Model string |
| 38 | - | Provider string | 38 | + | Provider string |
| 39 | - | AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar | 39 | + | AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar |
| 40 | - | BaseURL string | 40 | + | BaseURL string |
| 41 | - | Reasoning string | 41 | + | Reasoning string |
| 42 | - | SystemPrompt string | 42 | + | SystemPrompt string |
| 43 | - | Tools core.Registry | 43 | + | WritingGuidance string |
| 44 | - | MaxSteps int | 44 | + | Tools core.Registry |
| 45 | - | CWD string | 45 | + | MaxSteps int |
| 46 | + | CWD string | |||
| 46 | 47 | ||||
| 47 | // Startup resource fields are loaded inputs available to list before the | 48 | // Startup resource fields are loaded inputs available to list before the | ||
| 48 | // transcript. They are never sent to the provider or persisted by the view. | 49 | // transcript. They are never sent to the provider or persisted by the view. | ||
| @@ -9176,6 +9177,11 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { | |||||
| 9176 | 9177 | ||||
| 9177 | sys, _ := i.agent.PromptConfig() | 9178 | sys, _ := i.agent.PromptConfig() | ||
| 9178 | changed := false | 9179 | changed := false | ||
| 9180 | + | if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" { | |||
| 9181 | + | var removed bool | |||
| 9182 | + | sys, removed = removeLastAutoSubagentsAddendum(sys, guidance) | |||
| 9183 | + | changed = changed || removed | |||
| 9184 | + | } | |||
| 9179 | for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- { | 9185 | for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- { | ||
| 9180 | var removed bool | 9186 | var removed bool | ||
| 9181 | sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx]) | 9187 | sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx]) | ||
| @@ -9184,7 +9190,7 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { | |||||
| 9184 | i.managedAutoSubagentsAddenda = nil | 9190 | i.managedAutoSubagentsAddenda = nil | ||
| 9185 | 9191 | ||||
| 9186 | addenda := autoSubagentsAddenda(i.cfg, orchestrating) | 9192 | addenda := autoSubagentsAddenda(i.cfg, orchestrating) | ||
| 9187 | - | if changed && len(addenda) > 0 { | 9193 | + | if changed { |
| 9188 | sys = strings.TrimRight(sys, "\n") | 9194 | sys = strings.TrimRight(sys, "\n") | ||
| 9189 | } | 9195 | } | ||
| 9190 | for _, addendum := range addenda { | 9196 | for _, addendum := range addenda { | ||
| @@ -9195,12 +9201,16 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { | |||||
| 9195 | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | 9201 | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | ||
| 9196 | changed = true | 9202 | changed = true | ||
| 9197 | } | 9203 | } | ||
| 9204 | + | if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" { | |||
| 9205 | + | if sys != "" { | |||
| 9206 | + | sys += "\n\n" | |||
| 9207 | + | } | |||
| 9208 | + | sys += guidance | |||
| 9209 | + | changed = true | |||
| 9210 | + | } | |||
| 9198 | if !changed { | 9211 | if !changed { | ||
| 9199 | return | 9212 | return | ||
| 9200 | } | 9213 | } | ||
| 9201 | - | if len(addenda) == 0 { | |||
| 9202 | - | sys = strings.TrimRight(sys, "\n") + "\n" | |||
| 9203 | - | } | |||
| 9204 | i.agent.SetSystemPrompt(sys) | 9214 | i.agent.SetSystemPrompt(sys) | ||
| 9205 | } | 9215 | } | ||
| 9206 | 9216 | ||||
M packages/agent/systemprompt.go +15 -0
| @@ -35,6 +35,8 @@ type SystemPromptOpts struct { | |||
| 35 | 35 | // | |
| 36 | 36 | // - A one-paragraph identity (who zut is, what the name means, | |
| 37 | 37 | // what the TUI expects for output format). | |
| 38 | + | // - Compact handoff and writing-quality guidance that must survive | |
| 39 | + | // custom identities and appended context. | |
| 38 | 40 | // - The date + cwd footer so the model has current-context. | |
| 39 | 41 | // | |
| 40 | 42 | // Everything else (tool listing, operating guidelines, "don't run | |
| @@ -81,6 +83,11 @@ func BuildSystemPrompt(o SystemPromptOpts) string { | |||
| 81 | 83 | sb.WriteString(a) | |
| 82 | 84 | } | |
| 83 | 85 | ||
| 86 | + | // Keep the universal writing policy after optional context so narrower | |
| 87 | + | // addenda cannot accidentally disable it. | |
| 88 | + | sb.WriteString("\n\n") | |
| 89 | + | sb.WriteString(writingGuidance) | |
| 90 | + | ||
| 84 | 91 | fmt.Fprintf(&sb, "\n\nCurrent date: %s\nCurrent working directory: %s\n", date, cwd) | |
| 85 | 92 | return sb.String() | |
| 86 | 93 | } | |
| @@ -92,3 +99,11 @@ Your output renders in a TUI that understands markdown for prose and plain text | |||
| 92 | 99 | For focused changes to an existing file, inspect its current contents and use edit with verbatim oldText taken from that same file. Include only enough context to make each match unambiguous. Use write when creating a file or replacing it wholesale. Do not mutate files through bash redirections or commands such as cat, echo, sed, or tee, because those changes appear as opaque shell output instead of a readable edit diff.` | |
| 93 | 100 | ||
| 94 | 101 | const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (compacted)" message, treat it as a handoff from earlier work. Keep its active constraints and preferences in force, continue the most recent unresolved user request without waiting for the user to type "continue", and follow a newer user request when it supersedes the summary.` | |
| 102 | + | ||
| 103 | + | const writingGuidance = `Adapt every response and document to its medium, audience, and reader's immediate need. Lead with the answer or next useful action. Use plain, precise language, concrete details, and strong verbs. Prefer verified facts to vague emphasis; qualify uncertain claims. Preserve useful structure, descriptive links, and accessibility. Cut throat-clearing, ceremony, repetition, catalogue-like prose, and mechanical sentence patterns. Do not manufacture slang, errors, hesitation, or artificial variation to sound human. Revise silently unless asked to explain your writing choices.` | |
| 104 | + | ||
| 105 | + | // WritingGuidance returns the universal policy kept at the end of resolved | |
| 106 | + | // prompt context, including after interactive prompt updates. | |
| 107 | + | func WritingGuidance() string { | |
| 108 | + | return writingGuidance | |
| 109 | + | } | |
| @@ -35,6 +35,8 @@ type SystemPromptOpts struct { | |||||
| 35 | // | 35 | // | ||
| 36 | // - A one-paragraph identity (who zut is, what the name means, | 36 | // - A one-paragraph identity (who zut is, what the name means, | ||
| 37 | // what the TUI expects for output format). | 37 | // what the TUI expects for output format). | ||
| 38 | + | // - Compact handoff and writing-quality guidance that must survive | |||
| 39 | + | // custom identities and appended context. | |||
| 38 | // - The date + cwd footer so the model has current-context. | 40 | // - The date + cwd footer so the model has current-context. | ||
| 39 | // | 41 | // | ||
| 40 | // Everything else (tool listing, operating guidelines, "don't run | 42 | // Everything else (tool listing, operating guidelines, "don't run | ||
| @@ -81,6 +83,11 @@ func BuildSystemPrompt(o SystemPromptOpts) string { | |||||
| 81 | sb.WriteString(a) | 83 | sb.WriteString(a) | ||
| 82 | } | 84 | } | ||
| 83 | 85 | ||||
| 86 | + | // Keep the universal writing policy after optional context so narrower | |||
| 87 | + | // addenda cannot accidentally disable it. | |||
| 88 | + | sb.WriteString("\n\n") | |||
| 89 | + | sb.WriteString(writingGuidance) | |||
| 90 | + | ||||
| 84 | fmt.Fprintf(&sb, "\n\nCurrent date: %s\nCurrent working directory: %s\n", date, cwd) | 91 | fmt.Fprintf(&sb, "\n\nCurrent date: %s\nCurrent working directory: %s\n", date, cwd) | ||
| 85 | return sb.String() | 92 | return sb.String() | ||
| 86 | } | 93 | } | ||
| @@ -92,3 +99,11 @@ Your output renders in a TUI that understands markdown for prose and plain text | |||||
| 92 | For focused changes to an existing file, inspect its current contents and use edit with verbatim oldText taken from that same file. Include only enough context to make each match unambiguous. Use write when creating a file or replacing it wholesale. Do not mutate files through bash redirections or commands such as cat, echo, sed, or tee, because those changes appear as opaque shell output instead of a readable edit diff.` | 99 | For focused changes to an existing file, inspect its current contents and use edit with verbatim oldText taken from that same file. Include only enough context to make each match unambiguous. Use write when creating a file or replacing it wholesale. Do not mutate files through bash redirections or commands such as cat, echo, sed, or tee, because those changes appear as opaque shell output instead of a readable edit diff.` | ||
| 93 | 100 | ||||
| 94 | const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (compacted)" message, treat it as a handoff from earlier work. Keep its active constraints and preferences in force, continue the most recent unresolved user request without waiting for the user to type "continue", and follow a newer user request when it supersedes the summary.` | 101 | const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (compacted)" message, treat it as a handoff from earlier work. Keep its active constraints and preferences in force, continue the most recent unresolved user request without waiting for the user to type "continue", and follow a newer user request when it supersedes the summary.` | ||
| 102 | + | ||||
| 103 | + | const writingGuidance = `Adapt every response and document to its medium, audience, and reader's immediate need. Lead with the answer or next useful action. Use plain, precise language, concrete details, and strong verbs. Prefer verified facts to vague emphasis; qualify uncertain claims. Preserve useful structure, descriptive links, and accessibility. Cut throat-clearing, ceremony, repetition, catalogue-like prose, and mechanical sentence patterns. Do not manufacture slang, errors, hesitation, or artificial variation to sound human. Revise silently unless asked to explain your writing choices.` | |||
| 104 | + | ||||
| 105 | + | // WritingGuidance returns the universal policy kept at the end of resolved | |||
| 106 | + | // prompt context, including after interactive prompt updates. | |||
| 107 | + | func WritingGuidance() string { | |||
| 108 | + | return writingGuidance | |||
| 109 | + | } | |||
M packages/agent/systemprompt_test.go +37 -0
| @@ -6,6 +6,43 @@ import ( | |||
| 6 | 6 | "time" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | + | func TestBuildSystemPromptAlwaysIncludesFinalWritingGuidance(t *testing.T) { | |
| 10 | + | for _, tt := range []struct { | |
| 11 | + | name string | |
| 12 | + | custom string | |
| 13 | + | }{ | |
| 14 | + | {name: "default"}, | |
| 15 | + | {name: "custom", custom: "custom identity"}, | |
| 16 | + | } { | |
| 17 | + | t.Run(tt.name, func(t *testing.T) { | |
| 18 | + | const conflictingAddendum = "Always use ceremonial language." | |
| 19 | + | prompt := BuildSystemPrompt(SystemPromptOpts{ | |
| 20 | + | Custom: tt.custom, | |
| 21 | + | Append: []string{conflictingAddendum}, | |
| 22 | + | }) | |
| 23 | + | ||
| 24 | + | if count := strings.Count(prompt, writingGuidance); count != 1 { | |
| 25 | + | t.Fatalf("writing guidance count = %d, want 1:\n%s", count, prompt) | |
| 26 | + | } | |
| 27 | + | if strings.Index(prompt, writingGuidance) < strings.Index(prompt, conflictingAddendum) { | |
| 28 | + | t.Fatalf("writing guidance must follow appended context:\n%s", prompt) | |
| 29 | + | } | |
| 30 | + | for _, want := range []string{ | |
| 31 | + | "medium, audience, and reader's immediate need", | |
| 32 | + | "plain, precise language", | |
| 33 | + | "verified facts", | |
| 34 | + | "descriptive links, and accessibility", | |
| 35 | + | "Do not manufacture slang, errors, hesitation", | |
| 36 | + | "Revise silently", | |
| 37 | + | } { | |
| 38 | + | if !strings.Contains(prompt, want) { | |
| 39 | + | t.Errorf("writing guidance missing %q:\n%s", want, prompt) | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | }) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 9 | 46 | func TestBuildSystemPromptAddsCompactionHandoffToCustomPrompt(t *testing.T) { | |
| 10 | 47 | prompt := BuildSystemPrompt(SystemPromptOpts{Custom: "custom identity"}) | |
| 11 | 48 | if !strings.Contains(prompt, "custom identity") { | |
| @@ -6,6 +6,43 @@ import ( | |||||
| 6 | "time" | 6 | "time" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | + | func TestBuildSystemPromptAlwaysIncludesFinalWritingGuidance(t *testing.T) { | |||
| 10 | + | for _, tt := range []struct { | |||
| 11 | + | name string | |||
| 12 | + | custom string | |||
| 13 | + | }{ | |||
| 14 | + | {name: "default"}, | |||
| 15 | + | {name: "custom", custom: "custom identity"}, | |||
| 16 | + | } { | |||
| 17 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 18 | + | const conflictingAddendum = "Always use ceremonial language." | |||
| 19 | + | prompt := BuildSystemPrompt(SystemPromptOpts{ | |||
| 20 | + | Custom: tt.custom, | |||
| 21 | + | Append: []string{conflictingAddendum}, | |||
| 22 | + | }) | |||
| 23 | + | ||||
| 24 | + | if count := strings.Count(prompt, writingGuidance); count != 1 { | |||
| 25 | + | t.Fatalf("writing guidance count = %d, want 1:\n%s", count, prompt) | |||
| 26 | + | } | |||
| 27 | + | if strings.Index(prompt, writingGuidance) < strings.Index(prompt, conflictingAddendum) { | |||
| 28 | + | t.Fatalf("writing guidance must follow appended context:\n%s", prompt) | |||
| 29 | + | } | |||
| 30 | + | for _, want := range []string{ | |||
| 31 | + | "medium, audience, and reader's immediate need", | |||
| 32 | + | "plain, precise language", | |||
| 33 | + | "verified facts", | |||
| 34 | + | "descriptive links, and accessibility", | |||
| 35 | + | "Do not manufacture slang, errors, hesitation", | |||
| 36 | + | "Revise silently", | |||
| 37 | + | } { | |||
| 38 | + | if !strings.Contains(prompt, want) { | |||
| 39 | + | t.Errorf("writing guidance missing %q:\n%s", want, prompt) | |||
| 40 | + | } | |||
| 41 | + | } | |||
| 42 | + | }) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 9 | func TestBuildSystemPromptAddsCompactionHandoffToCustomPrompt(t *testing.T) { | 46 | func TestBuildSystemPromptAddsCompactionHandoffToCustomPrompt(t *testing.T) { | ||
| 10 | prompt := BuildSystemPrompt(SystemPromptOpts{Custom: "custom identity"}) | 47 | prompt := BuildSystemPrompt(SystemPromptOpts{Custom: "custom identity"}) | ||
| 11 | if !strings.Contains(prompt, "custom identity") { | 48 | if !strings.Contains(prompt, "custom identity") { | ||