Add daemon-wide system prompts (#1100)

* Add daemon-wide system prompt setting

* Align settings textarea rendering

* Polish daemon settings layout

* Remove orchestrator prompt injection
This commit is contained in:
Mohamed Boudra
2026-05-19 14:24:56 +00:00
committed by GitHub
parent da0e94fd18
commit 306601a0c3
21 changed files with 449 additions and 124 deletions
+1
View File
@@ -139,6 +139,7 @@ Single file, validated with `PersistedConfigSchema`.
listen: "127.0.0.1:6767",
hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load
mcp: { enabled: boolean, injectIntoAgents: boolean },
appendSystemPrompt: string, // appended to supported provider system/developer prompts
cors: { allowedOrigins: string[] },
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean },
auth: { password: string } // bcrypt hash, optional
@@ -0,0 +1,58 @@
import type { StyleProp, TextStyle } from "react-native";
import { useMemo } from "react";
import { TextInput, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings";
interface SettingsTextAreaProps {
accessibilityLabel: string;
value: string;
onChangeText: (text: string) => void;
placeholder?: string;
testID?: string;
style?: StyleProp<TextStyle>;
}
export function SettingsTextArea({
accessibilityLabel,
value,
onChangeText,
placeholder,
testID,
style,
}: SettingsTextAreaProps) {
const { theme } = useUnistyles();
const inputStyle = useMemo(() => [styles.input, style], [style]);
return (
<TextInput
testID={testID}
accessibilityLabel={accessibilityLabel}
multiline
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor={theme.colors.foregroundMuted}
style={inputStyle}
/>
);
}
export function SettingsTextAreaCard(props: SettingsTextAreaProps) {
return (
<View style={settingsStyles.card}>
<SettingsTextArea {...props} />
</View>
);
}
const styles = StyleSheet.create((theme) => ({
input: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
minHeight: 96,
textAlignVertical: "top",
},
}));
@@ -23,6 +23,7 @@ import { ExternalLink } from "@/components/ui/external-link";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Switch } from "@/components/ui/switch";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { SettingsTextAreaCard } from "@/components/settings-textarea";
import { SettingsGroup } from "@/screens/settings/settings-group";
import { SettingsSection } from "@/screens/settings/settings-section";
import { settingsStyles } from "@/styles/settings";
@@ -621,18 +622,13 @@ function ProjectConfigForm({
testID="worktree-group"
>
<SettingsSection title="Setup" testID="worktree-setup-section" trailing={setupDocsLink}>
<View style={settingsStyles.card}>
<TextInput
testID="worktree-setup-input"
accessibilityLabel="Worktree setup commands"
multiline
value={draft.setupText}
onChangeText={handleSetupChange}
placeholder="npm install"
placeholderTextColor={styles.placeholderColor.color}
style={styles.lifecycleInput}
/>
</View>
<SettingsTextAreaCard
testID="worktree-setup-input"
accessibilityLabel="Worktree setup commands"
value={draft.setupText}
onChangeText={handleSetupChange}
placeholder="npm install"
/>
</SettingsSection>
<SettingsSection
@@ -641,18 +637,13 @@ function ProjectConfigForm({
trailing={teardownDocsLink}
flush
>
<View style={settingsStyles.card}>
<TextInput
testID="worktree-teardown-input"
accessibilityLabel="Worktree teardown commands"
multiline
value={draft.teardownText}
onChangeText={handleTeardownChange}
placeholder="docker compose down"
placeholderTextColor={styles.placeholderColor.color}
style={styles.lifecycleInput}
/>
</View>
<SettingsTextAreaCard
testID="worktree-teardown-input"
accessibilityLabel="Worktree teardown commands"
value={draft.teardownText}
onChangeText={handleTeardownChange}
placeholder="docker compose down"
/>
</SettingsSection>
</SettingsGroup>
@@ -1008,18 +999,13 @@ function MetadataPromptSection({ promptKey, value, onChange, flush }: MetadataPr
);
return (
<SettingsSection title={meta.title} testID={meta.sectionTestID} flush={flush}>
<View style={settingsStyles.card}>
<TextInput
testID={meta.inputTestID}
accessibilityLabel={meta.title}
multiline
value={value}
onChangeText={handleChange}
placeholder={meta.placeholder}
placeholderTextColor={styles.placeholderColor.color}
style={styles.lifecycleInput}
/>
</View>
<SettingsTextAreaCard
testID={meta.inputTestID}
accessibilityLabel={meta.title}
value={value}
onChangeText={handleChange}
placeholder={meta.placeholder}
/>
</SettingsSection>
);
}
@@ -1344,14 +1330,6 @@ const styles = StyleSheet.create((theme) => ({
errorBlock: {
marginTop: theme.spacing[2],
},
lifecycleInput: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
minHeight: 96,
textAlignVertical: "top",
},
emptyScripts: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
+117 -4
View File
@@ -4,6 +4,7 @@ import { Alert, Pressable, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { AdaptiveRenameModal } from "@/components/rename-modal";
import { SettingsTextAreaCard } from "@/components/settings-textarea";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
@@ -379,9 +380,9 @@ function ConnectionRow({
function DaemonSection({ host, isLocalDaemon }: { host: HostProfile; isLocalDaemon: boolean }) {
return (
<>
<SettingsSection title="Operations">
<RestartDaemonCard host={host} />
<SettingsSection title="Daemon settings">
<InjectPaseoToolsCard serverId={host.serverId} />
<AppendSystemPromptCard serverId={host.serverId} />
</SettingsSection>
{isLocalDaemon ? (
<SettingsSection title="Pair devices">
@@ -544,9 +545,9 @@ function InjectPaseoToolsCard({ serverId }: { serverId: string }) {
<View style={settingsStyles.card} testID="host-page-inject-mcp-card">
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Inject Paseo tools</Text>
<Text style={settingsStyles.rowTitle}>Enable Paseo tools</Text>
<Text style={settingsStyles.rowHint}>
Automatically inject Paseo MCP tools into new agents
Agents will be able to manage worktrees, agents and schedules
</Text>
</View>
<Switch
@@ -559,6 +560,111 @@ function InjectPaseoToolsCard({ serverId }: { serverId: string }) {
);
}
function AppendSystemPromptCard({ serverId }: { serverId: string }) {
const isConnected = useHostRuntimeIsConnected(serverId);
const { config, patchConfig } = useDaemonConfig(serverId);
const persistedPrompt = config?.appendSystemPrompt ?? "";
const [draft, setDraft] = useState(persistedPrompt);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const header = useMemo<SheetHeader>(() => ({ title: "Append system prompt" }), []);
useEffect(() => {
setDraft(persistedPrompt);
}, [persistedPrompt]);
const hasChanges = draft !== persistedPrompt;
const handleOpen = useCallback(() => {
setDraft(persistedPrompt);
setIsEditing(true);
}, [persistedPrompt]);
const handleClose = useCallback(() => {
if (isSaving) return;
setDraft(persistedPrompt);
setIsEditing(false);
}, [isSaving, persistedPrompt]);
const handleSave = useCallback(() => {
setIsSaving(true);
void patchConfig({ appendSystemPrompt: draft })
.then(() => {
setIsEditing(false);
return;
})
.catch((error) => {
console.error("[HostPage] Failed to save append system prompt", error);
})
.finally(() => setIsSaving(false));
}, [draft, patchConfig]);
const handleReset = useCallback(() => {
setDraft(persistedPrompt);
}, [persistedPrompt]);
if (!isConnected) return null;
return (
<>
<View style={settingsStyles.card} testID="host-page-append-system-prompt-card">
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>System prompt</Text>
<Text style={settingsStyles.rowHint}>Added a system prompt to all agents</Text>
</View>
<Button
variant="outline"
size="sm"
onPress={handleOpen}
testID="host-page-append-system-prompt-edit"
>
Edit
</Button>
</View>
</View>
{isEditing ? (
<AdaptiveModalSheet
header={header}
visible
onClose={handleClose}
testID="host-page-append-system-prompt-sheet"
desktopMaxWidth={560}
>
<SettingsTextAreaCard
testID="host-page-append-system-prompt-input"
accessibilityLabel="Append system prompt"
value={draft}
onChangeText={setDraft}
placeholder="Always keep replies concise."
/>
<View style={styles.appendPromptActions}>
<Button
variant="ghost"
size="sm"
onPress={handleReset}
disabled={!hasChanges || isSaving}
testID="host-page-append-system-prompt-reset"
>
Reset
</Button>
<Button
variant="default"
size="sm"
onPress={handleSave}
disabled={!hasChanges || isSaving}
testID="host-page-append-system-prompt-save"
>
{isSaving ? "Saving..." : "Save"}
</Button>
</View>
</AdaptiveModalSheet>
) : null}
</>
);
}
function PairDeviceRow() {
const { theme } = useUnistyles();
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -631,6 +737,8 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
return (
<SettingsSection title="Danger zone" testID="host-page-remove-host-card">
<RestartDaemonCard host={host} />
<View style={settingsStyles.card}>
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
@@ -755,6 +863,11 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2],
marginTop: theme.spacing[4],
},
appendPromptActions: {
flexDirection: "row",
justifyContent: "flex-end",
gap: theme.spacing[2],
},
emptyCard: {
padding: theme.spacing[4],
alignItems: "center",
@@ -202,7 +202,12 @@ const disabledCodexEntry: ProviderSnapshotEntry = {
};
function makeConfig(providers: MutableDaemonConfig["providers"] = {}): MutableDaemonConfig {
return { mcp: { injectIntoAgents: false }, providers, autoArchiveAfterMerge: false };
return {
mcp: { injectIntoAgents: false },
providers,
autoArchiveAfterMerge: false,
appendSystemPrompt: "",
};
}
function descendants(el: HTMLElement): HTMLElement[] {
@@ -100,12 +100,15 @@ function expectArchivedAgentRecord(
class TestAgentClient implements AgentClient {
readonly provider = "codex" as const;
readonly capabilities = TEST_CAPABILITIES;
readonly createdConfigs: AgentSessionConfig[] = [];
readonly resumeOverrides: Array<Partial<AgentSessionConfig> | undefined> = [];
async isAvailable(): Promise<boolean> {
return true;
}
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
this.createdConfigs.push(config);
return new TestAgentSession(config);
}
@@ -135,9 +138,11 @@ class TestAgentClient implements AgentClient {
config?: Partial<AgentSessionConfig>,
_launchContext?: AgentLaunchContext,
): Promise<AgentSession> {
this.resumeOverrides.push(config);
return new TestAgentSession({
provider: "codex",
cwd: config?.cwd ?? process.cwd(),
daemonAppendSystemPrompt: config?.daemonAppendSystemPrompt,
});
}
}
@@ -391,6 +396,62 @@ test("normalizeConfig strips legacy 'default' model id", async () => {
expect(snapshot.config.modeId).toBe("auto");
});
test("createAgent injects daemon append system prompt at runtime only", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const client = new TestAgentClient();
const manager = new AgentManager({
clients: {
codex: client,
},
registry: storage,
logger,
appendSystemPrompt: " Daemon instructions. ",
idFactory: () => "00000000-0000-4000-8000-000000000103",
});
const snapshot = await manager.createAgent({
provider: "codex",
cwd: workdir,
systemPrompt: "Agent instructions.",
});
const record = await storage.get(snapshot.id);
expect(client.createdConfigs[0]?.systemPrompt).toBe("Agent instructions.");
expect(client.createdConfigs[0]?.daemonAppendSystemPrompt).toBe("Daemon instructions.");
expect(snapshot.config.daemonAppendSystemPrompt).toBe("Daemon instructions.");
expect(record?.config?.systemPrompt).toBe("Agent instructions.");
expect(record?.config).not.toHaveProperty("daemonAppendSystemPrompt");
});
test("daemon append system prompt is not injected into Pi", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const client = new TestAgentClient();
const manager = new AgentManager({
clients: {
pi: client as unknown as AgentClient,
},
providerDefinitions: {
pi: { enabled: true },
},
registry: storage,
logger,
appendSystemPrompt: "Daemon instructions.",
idFactory: () => "00000000-0000-4000-8000-000000000104",
});
await manager.createAgent({
provider: "pi",
cwd: workdir,
systemPrompt: "Agent instructions.",
});
expect(client.createdConfigs[0]?.daemonAppendSystemPrompt).toBeUndefined();
});
test("setAgentMode persists the selected mode across session reload", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
@@ -173,6 +173,7 @@ export interface AgentManagerOptions {
durableTimelineStore?: AgentTimelineStore;
terminalManager?: TerminalManager | null;
mcpBaseUrl?: string;
appendSystemPrompt?: string;
agentStreamCoalesceWindowMs?: number;
rescueTimeouts?: AgentManagerRescueTimeouts;
logger: Logger;
@@ -438,6 +439,7 @@ export class AgentManager {
private readonly backgroundTasks = new Set<Promise<void>>();
private readonly agentStreamCoalescer: AgentStreamCoalescer;
private mcpBaseUrl: string | null;
private appendSystemPrompt: string;
private onAgentAttention?: AgentAttentionCallback;
private logger: Logger;
private readonly rescueTimeouts: Required<AgentManagerRescueTimeouts>;
@@ -448,6 +450,7 @@ export class AgentManager {
this.durableTimelineStore = options?.durableTimelineStore;
this.onAgentAttention = options?.onAgentAttention;
this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
this.rescueTimeouts = {
reloadSessionCloseMs:
@@ -502,6 +505,10 @@ export class AgentManager {
this.mcpBaseUrl = url;
}
setAppendSystemPrompt(prompt: string | null | undefined): void {
this.appendSystemPrompt = prompt ?? "";
}
public getMetricsSnapshot(): AgentMetricsSnapshot {
const byLifecycle: Record<string, number> = {};
let withActiveForegroundTurn = 0;
@@ -816,7 +823,9 @@ export class AgentManager {
},
};
this.requireEnabledProvider(injectedConfig.provider);
const normalizedConfig = await this.normalizeConfig(injectedConfig);
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
await this.normalizeConfig(injectedConfig),
);
const launchContext = this.buildLaunchContext(resolvedAgentId);
const client = await this.requireAvailableClient({
provider: normalizedConfig.provider,
@@ -860,7 +869,9 @@ export class AgentManager {
...overrides,
provider: handle.provider,
} as AgentSessionConfig;
const normalizedConfig = await this.normalizeConfig(mergedConfig);
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
await this.normalizeConfig(mergedConfig),
);
const resumeOverrides: Partial<AgentSessionConfig> = { ...overrides };
let hasResumeOverrides = overrides !== undefined;
@@ -874,6 +885,11 @@ export class AgentManager {
hasResumeOverrides = true;
}
if (metadata.daemonAppendSystemPrompt !== normalizedConfig.daemonAppendSystemPrompt) {
resumeOverrides.daemonAppendSystemPrompt = normalizedConfig.daemonAppendSystemPrompt;
hasResumeOverrides = true;
}
const launchContext = this.buildLaunchContext(resolvedAgentId);
const client = this.requireClient(handle.provider);
const available = await client.isAvailable();
@@ -919,7 +935,9 @@ export class AgentManager {
...overrides,
provider,
} as AgentSessionConfig;
const normalizedConfig = await this.normalizeConfig(refreshConfig);
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
await this.normalizeConfig(refreshConfig),
);
const launchContext = this.buildLaunchContext(agentId);
const session = handle
@@ -3437,6 +3455,26 @@ export class AgentManager {
return normalized;
}
private applyDaemonAppendSystemPrompt(config: AgentSessionConfig): AgentSessionConfig {
if (config.provider === "pi") {
const next = { ...config };
delete next.daemonAppendSystemPrompt;
return next;
}
const trimmed = this.appendSystemPrompt.trim();
if (!trimmed) {
const next = { ...config };
delete next.daemonAppendSystemPrompt;
return next;
}
return {
...config,
daemonAppendSystemPrompt: trimmed,
};
}
private buildLaunchContext(agentId: string): AgentLaunchContext {
return {
agentId,
@@ -473,6 +473,11 @@ export interface AgentSessionConfig {
* Mapped by each provider to its native instruction field.
*/
systemPrompt?: string;
/**
* Daemon-level instructions appended at runtime. This is deliberately not
* persisted into agent config so daemon setting changes apply cleanly.
*/
daemonAppendSystemPrompt?: string;
modeId?: string;
model?: string;
thinkingOptionId?: string;
@@ -1,50 +0,0 @@
/**
* Returns orchestrator mode instructions to append to the system prompt.
* These instructions are from CLAUDE.md and guide agents on how to work
* effectively in this repository.
*/
export function getOrchestratorModeInstructions(): string {
return `
<orchestrator-mode>
Activation:
- Only activate if the user explicitly says "go into orchestrator mode" (or similar).
- Otherwise, do work directly yourself; do not spawn agents.
Core rules:
- In orchestrator mode, you accomplish tasks only by managing agents; do not perform the work yourself.
- Always prefix agent titles (e.g., "🎭 Feature Implementation", "🎭 Design Discussion").
- Set cwd to the repository root and choose the most permissive mode available.
- If an agent control tool call fails, list agents before launching another; it may just be a wait timeout.
Context management:
- Reuse an existing agent when the next step needs the same context (same files/module/folder or immediate follow-up like investigate → fix in the same area).
- Start a new agent when switching to a different area/module, or when an agent has run long and its context feels stale.
- Prefer sending follow-up prompts to an existing agent to avoid reloading context.
- Use multiple agents when roles diverge (e.g., one for refactor, one for external validation), but default to reuse when context overlaps.
Conversation with agents:
- Engage actively: ask pointed questions, probe risks, and request clarifications before accepting proposals.
- Encourage agents to validate assumptions, consider edge cases, and describe how they will test/verify.
Agent selection guidance:
- Codex: methodical and slower; great for deep debugging, tracing code paths, refactoring, complex features, and design discussions.
- Claude: fast; strong at tool use, agentic control, and managing other agents; may jump to conclusions—ask it to verify.
Clarifying ambiguous requests:
- Research first to understand the current state.
- Ask clarifying questions about what the user wants.
- Present options with trade-offs.
- Get explicit confirmation; never assume.
Investigation vs Implementation:
- Investigate only unless explicitly asked to implement.
- Report findings clearly.
- After investigation, ask for direction before implementing.
Tool usage discipline:
- Do not ask users to run commands—run them yourself.
- Do not repeat the user’s instructions verbatim—summarize them in your own words.
- Be explicit about results—tell the user what happened after every command.
</orchestrator-mode>
`;
}
@@ -81,7 +81,7 @@ import {
import { findExecutable, isCommandAvailable } from "../../../../utils/executable.js";
import { withTimeout } from "../../../../utils/promise-timeout.js";
import { execCommand } from "../../../../utils/spawn.js";
import { getOrchestratorModeInstructions } from "../../orchestrator-instructions.js";
import { composeSystemPromptParts } from "../../system-prompt.js";
const fsPromises = promises;
const CLAUDE_SETTING_SOURCES: NonNullable<ClaudeOptions["settingSources"]> = [
@@ -2314,9 +2314,9 @@ class ClaudeAgentSession implements AgentSession {
}
private buildAppendedSystemPrompt(): string {
return [getOrchestratorModeInstructions(), this.config.systemPrompt?.trim()]
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
.join("\n\n");
return (
composeSystemPromptParts(this.config.systemPrompt, this.config.daemonAppendSystemPrompt) ?? ""
);
}
private buildSdkEnv(extraClaudeOptions: Partial<ClaudeOptions> | undefined): NodeJS.ProcessEnv {
@@ -2365,7 +2365,7 @@ class ClaudeAgentSession implements AgentSession {
canUseTool: this.handlePermissionRequest,
pathToClaudeCodeExecutable: claudeBinary,
// Use Claude Code preset system prompt and load CLAUDE.md files
// Append provider-agnostic system prompt and orchestrator instructions for agents.
// Append provider-agnostic system prompts for agents.
systemPrompt: {
type: "preset",
preset: "claude_code",
@@ -41,6 +41,7 @@ import os from "node:os";
import path from "node:path";
import { z } from "zod";
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
import { composeSystemPromptParts } from "../system-prompt.js";
import { curateAgentActivity } from "../activity-curator.js";
import {
mapCodexRolloutToolCall,
@@ -3026,12 +3027,11 @@ class CodexAppServerAgentSession implements AgentSession {
const settings: Record<string, unknown> = {};
if (match.model) settings.model = match.model;
if (match.reasoning_effort) settings.reasoning_effort = match.reasoning_effort;
const developerInstructions = [
match.developer_instructions?.trim(),
this.config.systemPrompt?.trim(),
]
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
.join("\n\n");
const developerInstructions = composeSystemPromptParts(
match.developer_instructions,
this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
if (developerInstructions) settings.developer_instructions = developerInstructions;
if (this.config.model) settings.model = this.config.model;
const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
@@ -3161,8 +3161,12 @@ class CodexAppServerAgentSession implements AgentSession {
return;
}
const params: Record<string, unknown> = { threadId: this.currentThreadId };
if (this.config.systemPrompt?.trim()) {
params.developerInstructions = this.config.systemPrompt.trim();
const developerInstructions = composeSystemPromptParts(
this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
if (developerInstructions) {
params.developerInstructions = developerInstructions;
}
const codexConfig = this.buildCodexInnerConfig();
if (codexConfig) {
@@ -3257,6 +3261,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy: string;
sandboxPolicyType: string;
hasOutputSchema: boolean;
hasDeveloperInstructions: boolean;
hasCodexConfig: boolean;
}> {
const input = await this.buildUserInput(prompt);
@@ -3299,8 +3304,12 @@ class CodexAppServerAgentSession implements AgentSession {
if (options?.outputSchema) {
params.outputSchema = normalizeCodexOutputSchema(options.outputSchema);
}
if (this.config.systemPrompt?.trim()) {
params.developerInstructions = this.config.systemPrompt.trim();
const developerInstructions = composeSystemPromptParts(
this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
if (developerInstructions) {
params.developerInstructions = developerInstructions;
}
const codexConfig = this.buildCodexInnerConfig();
if (codexConfig) {
@@ -3313,6 +3322,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy,
sandboxPolicyType,
hasOutputSchema: Boolean(options?.outputSchema),
hasDeveloperInstructions: Boolean(developerInstructions),
hasCodexConfig: Boolean(codexConfig),
};
}
@@ -3323,6 +3333,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy,
sandboxPolicyType,
hasOutputSchema,
hasDeveloperInstructions,
hasCodexConfig,
}: {
turnId: string;
@@ -3330,6 +3341,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy: string;
sandboxPolicyType: string;
hasOutputSchema: boolean;
hasDeveloperInstructions: boolean;
hasCodexConfig: boolean;
}): void {
this.logger.info(
@@ -3345,7 +3357,7 @@ class CodexAppServerAgentSession implements AgentSession {
sandboxPolicyType,
hasCollaborationMode: Boolean(this.resolvedCollaborationMode),
hasOutputSchema,
hasDeveloperInstructions: Boolean(this.config.systemPrompt?.trim()),
hasDeveloperInstructions,
hasCodexConfig,
},
"Starting Codex app-server turn",
@@ -3407,6 +3419,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy: turnStart.approvalPolicy,
sandboxPolicyType: turnStart.sandboxPolicyType,
hasOutputSchema: turnStart.hasOutputSchema,
hasDeveloperInstructions: turnStart.hasDeveloperInstructions,
hasCodexConfig: turnStart.hasCodexConfig,
});
await this.client.request("turn/start", turnStart.params, TURN_START_TIMEOUT_MS);
@@ -3952,14 +3965,16 @@ class CodexAppServerAgentSession implements AgentSession {
const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
const sandbox = this.config.sandboxMode ?? preset.sandbox;
const innerConfig = this.buildCodexInnerConfig();
const developerInstructions = composeSystemPromptParts(
this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
const params: Record<string, unknown> = {
model,
cwd: this.config.cwd ?? null,
approvalPolicy,
sandbox,
...(this.config.systemPrompt?.trim()
? { developerInstructions: this.config.systemPrompt.trim() }
: {}),
...(developerInstructions ? { developerInstructions } : {}),
...(innerConfig ? { config: innerConfig } : {}),
...(this.ephemeral ? { ephemeral: true } : {}),
};
@@ -60,6 +60,7 @@ import {
} from "./diagnostic-utils.js";
import { runProviderTurn } from "./provider-runner.js";
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
import { composeSystemPromptParts } from "../system-prompt.js";
import {
createSdkOpenCodeClient,
type OpenCodeRuntime,
@@ -2589,6 +2590,10 @@ class OpenCodeAgentSession implements AgentSession {
partTypes: parts.map((p) => p.type),
});
try {
const systemPrompt = composeSystemPromptParts(
this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
const promptResponse = await this.client.session.promptAsync({
sessionID: this.sessionId,
directory: this.config.cwd,
@@ -2601,7 +2606,7 @@ class OpenCodeAgentSession implements AgentSession {
},
}
: {}),
...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}),
...(systemPrompt ? { system: systemPrompt } : {}),
...(model ? { model } : {}),
...(effectiveMode ? { agent: effectiveMode } : {}),
...(effectiveVariant ? { variant: effectiveVariant } : {}),
@@ -0,0 +1,10 @@
export function composeSystemPromptParts(
...parts: Array<string | null | undefined>
): string | undefined {
const prompt = parts
.map((part) => part?.trim())
.filter((part): part is string => typeof part === "string" && part.length > 0)
.join("\n\n");
return prompt.length > 0 ? prompt : undefined;
}
+6
View File
@@ -230,6 +230,7 @@ export interface PaseoDaemonConfig {
mcpEnabled?: boolean;
mcpInjectIntoAgents?: boolean;
autoArchiveAfterMerge?: boolean;
appendSystemPrompt?: string;
staticDir: string;
mcpDebug: boolean;
isDev?: boolean;
@@ -290,6 +291,7 @@ export async function createPaseoDaemon(
]),
),
autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
appendSystemPrompt: config.appendSystemPrompt ?? "",
},
logger,
);
@@ -516,6 +518,7 @@ export async function createPaseoDaemon(
},
providerDefinitions: providerRegistry,
registry: agentStorage,
appendSystemPrompt: config.appendSystemPrompt,
logger,
});
@@ -862,6 +865,9 @@ export async function createPaseoDaemon(
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
});
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
});
const relayEnabled = config.relayEnabled ?? true;
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
+14 -2
View File
@@ -256,6 +256,10 @@ function resolveAuthConfig(
: undefined;
}
function resolveAppendSystemPrompt(persisted: ReturnType<typeof loadPersistedConfig>): string {
return persisted.daemon?.appendSystemPrompt ?? "";
}
function resolveStaticLoadConfigSettings(
env: NodeJS.ProcessEnv,
cli: CliConfigOverrides | undefined,
@@ -266,6 +270,7 @@ function resolveStaticLoadConfigSettings(
mcpInjectIntoAgents:
cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false,
autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
appendSystemPrompt: resolveAppendSystemPrompt(persisted),
hostnames: mergeHostnames([
persisted.daemon?.hostnames,
parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
@@ -286,8 +291,14 @@ export function loadConfig(
const persisted = loadPersistedConfig(paseoHome);
const listen = resolveListenAddress(env, options?.cli, persisted);
const { mcpEnabled, mcpInjectIntoAgents, autoArchiveAfterMerge, hostnames, appBaseUrl } =
resolveStaticLoadConfigSettings(env, options?.cli, persisted);
const {
mcpEnabled,
mcpInjectIntoAgents,
autoArchiveAfterMerge,
appendSystemPrompt,
hostnames,
appBaseUrl,
} = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
const relay = resolveRelayConfig({
env,
@@ -315,6 +326,7 @@ export function loadConfig(
mcpEnabled,
mcpInjectIntoAgents,
autoArchiveAfterMerge,
appendSystemPrompt,
mcpDebug: env.MCP_DEBUG === "1",
isDev: resolvePaseoNodeEnv(env) === "development",
agentStoragePath: path.join(paseoHome, "agents"),
@@ -107,6 +107,28 @@ describe("DaemonConfigStore", () => {
});
});
test("patch persists append system prompt into config.json", () => {
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
tempDirs.push(paseoHome);
const store = new DaemonConfigStore(
paseoHome,
{
mcp: { injectIntoAgents: false },
providers: {},
appendSystemPrompt: "",
},
undefined,
);
store.patch({
appendSystemPrompt: "Prefer terse replies.",
});
const persisted = loadPersistedConfig(paseoHome);
expect(persisted.daemon?.appendSystemPrompt).toBe("Prefer terse replies.");
});
test("patch persists provider additional models into config.json", () => {
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
tempDirs.push(paseoHome);
@@ -186,6 +186,7 @@ function mergeMutableConfigIntoPersistedConfig(params: {
injectIntoAgents: mutable.mcp.injectIntoAgents,
},
autoArchiveAfterMerge: mutable.autoArchiveAfterMerge,
appendSystemPrompt: mutable.appendSystemPrompt,
},
agents:
providerOverrides && Object.keys(providerOverrides).length > 0
@@ -34,6 +34,18 @@ describe("PersistedConfigSchema daemon auth config", () => {
});
});
describe("PersistedConfigSchema daemon append system prompt config", () => {
test("accepts optional append system prompt", () => {
const parsed = PersistedConfigSchema.parse({
daemon: {
appendSystemPrompt: "Prefer terse replies.",
},
});
expect(parsed.daemon?.appendSystemPrompt).toBe("Prefer terse replies.");
});
});
describe("PersistedConfigSchema daemon relay config", () => {
test("accepts optional relay TLS setting", () => {
const parsed = PersistedConfigSchema.parse({
@@ -251,6 +251,7 @@ export const PersistedConfigSchema = z
.passthrough()
.optional(),
autoArchiveAfterMerge: z.boolean().optional(),
appendSystemPrompt: z.string().optional(),
cors: z
.object({
allowedOrigins: z.array(z.string()).optional(),
+2
View File
@@ -114,6 +114,7 @@ export const MutableDaemonConfigSchema = z
.passthrough(),
providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}),
autoArchiveAfterMerge: z.boolean().default(false),
appendSystemPrompt: z.string().default(""),
})
.passthrough();
@@ -124,6 +125,7 @@ export const MutableDaemonConfigPatchSchema = z
.record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough())
.optional(),
autoArchiveAfterMerge: z.boolean().optional(),
appendSystemPrompt: z.string().optional(),
})
.partial()
.passthrough();
@@ -54,6 +54,12 @@
},
"additionalProperties": true
},
"autoArchiveAfterMerge": {
"type": "boolean"
},
"appendSystemPrompt": {
"type": "string"
},
"cors": {
"type": "object",
"properties": {
@@ -77,6 +83,22 @@
},
"publicEndpoint": {
"type": "string"
},
"useTls": {
"type": "boolean"
},
"publicUseTls": {
"type": "boolean"
}
},
"additionalProperties": false
},
"auth": {
"type": "object",
"properties": {
"password": {
"type": "string",
"pattern": "^\\$2[aby]\\$\\d{2}\\$[./A-Za-z0-9]{53}$"
}
},
"additionalProperties": false
@@ -248,6 +270,10 @@
"type": "string",
"minLength": 1
},
"language": {
"type": "string",
"minLength": 1
},
"confidenceThreshold": {
"type": "number"
}
@@ -285,6 +311,10 @@
"model": {
"type": "string",
"minLength": 1
},
"language": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false