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:
@@ -139,6 +139,7 @@ Single file, validated with `PersistedConfigSchema`.
|
|||||||
listen: "127.0.0.1:6767",
|
listen: "127.0.0.1:6767",
|
||||||
hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load
|
hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load
|
||||||
mcp: { enabled: boolean, injectIntoAgents: boolean },
|
mcp: { enabled: boolean, injectIntoAgents: boolean },
|
||||||
|
appendSystemPrompt: string, // appended to supported provider system/developer prompts
|
||||||
cors: { allowedOrigins: string[] },
|
cors: { allowedOrigins: string[] },
|
||||||
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean },
|
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean },
|
||||||
auth: { password: string } // bcrypt hash, optional
|
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 { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||||
|
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
||||||
import { SettingsGroup } from "@/screens/settings/settings-group";
|
import { SettingsGroup } from "@/screens/settings/settings-group";
|
||||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||||
import { settingsStyles } from "@/styles/settings";
|
import { settingsStyles } from "@/styles/settings";
|
||||||
@@ -621,18 +622,13 @@ function ProjectConfigForm({
|
|||||||
testID="worktree-group"
|
testID="worktree-group"
|
||||||
>
|
>
|
||||||
<SettingsSection title="Setup" testID="worktree-setup-section" trailing={setupDocsLink}>
|
<SettingsSection title="Setup" testID="worktree-setup-section" trailing={setupDocsLink}>
|
||||||
<View style={settingsStyles.card}>
|
<SettingsTextAreaCard
|
||||||
<TextInput
|
testID="worktree-setup-input"
|
||||||
testID="worktree-setup-input"
|
accessibilityLabel="Worktree setup commands"
|
||||||
accessibilityLabel="Worktree setup commands"
|
value={draft.setupText}
|
||||||
multiline
|
onChangeText={handleSetupChange}
|
||||||
value={draft.setupText}
|
placeholder="npm install"
|
||||||
onChangeText={handleSetupChange}
|
/>
|
||||||
placeholder="npm install"
|
|
||||||
placeholderTextColor={styles.placeholderColor.color}
|
|
||||||
style={styles.lifecycleInput}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
@@ -641,18 +637,13 @@ function ProjectConfigForm({
|
|||||||
trailing={teardownDocsLink}
|
trailing={teardownDocsLink}
|
||||||
flush
|
flush
|
||||||
>
|
>
|
||||||
<View style={settingsStyles.card}>
|
<SettingsTextAreaCard
|
||||||
<TextInput
|
testID="worktree-teardown-input"
|
||||||
testID="worktree-teardown-input"
|
accessibilityLabel="Worktree teardown commands"
|
||||||
accessibilityLabel="Worktree teardown commands"
|
value={draft.teardownText}
|
||||||
multiline
|
onChangeText={handleTeardownChange}
|
||||||
value={draft.teardownText}
|
placeholder="docker compose down"
|
||||||
onChangeText={handleTeardownChange}
|
/>
|
||||||
placeholder="docker compose down"
|
|
||||||
placeholderTextColor={styles.placeholderColor.color}
|
|
||||||
style={styles.lifecycleInput}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
|
|
||||||
@@ -1008,18 +999,13 @@ function MetadataPromptSection({ promptKey, value, onChange, flush }: MetadataPr
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={meta.title} testID={meta.sectionTestID} flush={flush}>
|
<SettingsSection title={meta.title} testID={meta.sectionTestID} flush={flush}>
|
||||||
<View style={settingsStyles.card}>
|
<SettingsTextAreaCard
|
||||||
<TextInput
|
testID={meta.inputTestID}
|
||||||
testID={meta.inputTestID}
|
accessibilityLabel={meta.title}
|
||||||
accessibilityLabel={meta.title}
|
value={value}
|
||||||
multiline
|
onChangeText={handleChange}
|
||||||
value={value}
|
placeholder={meta.placeholder}
|
||||||
onChangeText={handleChange}
|
/>
|
||||||
placeholder={meta.placeholder}
|
|
||||||
placeholderTextColor={styles.placeholderColor.color}
|
|
||||||
style={styles.lifecycleInput}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1344,14 +1330,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
errorBlock: {
|
errorBlock: {
|
||||||
marginTop: theme.spacing[2],
|
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: {
|
emptyScripts: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Alert, Pressable, Text, View } from "react-native";
|
|||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||||
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||||
|
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
|
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
|
||||||
@@ -379,9 +380,9 @@ function ConnectionRow({
|
|||||||
function DaemonSection({ host, isLocalDaemon }: { host: HostProfile; isLocalDaemon: boolean }) {
|
function DaemonSection({ host, isLocalDaemon }: { host: HostProfile; isLocalDaemon: boolean }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSection title="Operations">
|
<SettingsSection title="Daemon settings">
|
||||||
<RestartDaemonCard host={host} />
|
|
||||||
<InjectPaseoToolsCard serverId={host.serverId} />
|
<InjectPaseoToolsCard serverId={host.serverId} />
|
||||||
|
<AppendSystemPromptCard serverId={host.serverId} />
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
{isLocalDaemon ? (
|
{isLocalDaemon ? (
|
||||||
<SettingsSection title="Pair devices">
|
<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.card} testID="host-page-inject-mcp-card">
|
||||||
<View style={settingsStyles.row}>
|
<View style={settingsStyles.row}>
|
||||||
<View style={settingsStyles.rowContent}>
|
<View style={settingsStyles.rowContent}>
|
||||||
<Text style={settingsStyles.rowTitle}>Inject Paseo tools</Text>
|
<Text style={settingsStyles.rowTitle}>Enable Paseo tools</Text>
|
||||||
<Text style={settingsStyles.rowHint}>
|
<Text style={settingsStyles.rowHint}>
|
||||||
Automatically inject Paseo MCP tools into new agents
|
Agents will be able to manage worktrees, agents and schedules
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<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() {
|
function PairDeviceRow() {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@@ -631,6 +737,8 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection title="Danger zone" testID="host-page-remove-host-card">
|
<SettingsSection title="Danger zone" testID="host-page-remove-host-card">
|
||||||
|
<RestartDaemonCard host={host} />
|
||||||
|
|
||||||
<View style={settingsStyles.card}>
|
<View style={settingsStyles.card}>
|
||||||
<View style={settingsStyles.row}>
|
<View style={settingsStyles.row}>
|
||||||
<View style={settingsStyles.rowContent}>
|
<View style={settingsStyles.rowContent}>
|
||||||
@@ -755,6 +863,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
marginTop: theme.spacing[4],
|
marginTop: theme.spacing[4],
|
||||||
},
|
},
|
||||||
|
appendPromptActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
emptyCard: {
|
emptyCard: {
|
||||||
padding: theme.spacing[4],
|
padding: theme.spacing[4],
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -202,7 +202,12 @@ const disabledCodexEntry: ProviderSnapshotEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function makeConfig(providers: MutableDaemonConfig["providers"] = {}): MutableDaemonConfig {
|
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[] {
|
function descendants(el: HTMLElement): HTMLElement[] {
|
||||||
|
|||||||
@@ -100,12 +100,15 @@ function expectArchivedAgentRecord(
|
|||||||
class TestAgentClient implements AgentClient {
|
class TestAgentClient implements AgentClient {
|
||||||
readonly provider = "codex" as const;
|
readonly provider = "codex" as const;
|
||||||
readonly capabilities = TEST_CAPABILITIES;
|
readonly capabilities = TEST_CAPABILITIES;
|
||||||
|
readonly createdConfigs: AgentSessionConfig[] = [];
|
||||||
|
readonly resumeOverrides: Array<Partial<AgentSessionConfig> | undefined> = [];
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
|
this.createdConfigs.push(config);
|
||||||
return new TestAgentSession(config);
|
return new TestAgentSession(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,9 +138,11 @@ class TestAgentClient implements AgentClient {
|
|||||||
config?: Partial<AgentSessionConfig>,
|
config?: Partial<AgentSessionConfig>,
|
||||||
_launchContext?: AgentLaunchContext,
|
_launchContext?: AgentLaunchContext,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
|
this.resumeOverrides.push(config);
|
||||||
return new TestAgentSession({
|
return new TestAgentSession({
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd: config?.cwd ?? process.cwd(),
|
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");
|
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 () => {
|
test("setAgentMode persists the selected mode across session reload", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ export interface AgentManagerOptions {
|
|||||||
durableTimelineStore?: AgentTimelineStore;
|
durableTimelineStore?: AgentTimelineStore;
|
||||||
terminalManager?: TerminalManager | null;
|
terminalManager?: TerminalManager | null;
|
||||||
mcpBaseUrl?: string;
|
mcpBaseUrl?: string;
|
||||||
|
appendSystemPrompt?: string;
|
||||||
agentStreamCoalesceWindowMs?: number;
|
agentStreamCoalesceWindowMs?: number;
|
||||||
rescueTimeouts?: AgentManagerRescueTimeouts;
|
rescueTimeouts?: AgentManagerRescueTimeouts;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
@@ -438,6 +439,7 @@ export class AgentManager {
|
|||||||
private readonly backgroundTasks = new Set<Promise<void>>();
|
private readonly backgroundTasks = new Set<Promise<void>>();
|
||||||
private readonly agentStreamCoalescer: AgentStreamCoalescer;
|
private readonly agentStreamCoalescer: AgentStreamCoalescer;
|
||||||
private mcpBaseUrl: string | null;
|
private mcpBaseUrl: string | null;
|
||||||
|
private appendSystemPrompt: string;
|
||||||
private onAgentAttention?: AgentAttentionCallback;
|
private onAgentAttention?: AgentAttentionCallback;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private readonly rescueTimeouts: Required<AgentManagerRescueTimeouts>;
|
private readonly rescueTimeouts: Required<AgentManagerRescueTimeouts>;
|
||||||
@@ -448,6 +450,7 @@ export class AgentManager {
|
|||||||
this.durableTimelineStore = options?.durableTimelineStore;
|
this.durableTimelineStore = options?.durableTimelineStore;
|
||||||
this.onAgentAttention = options?.onAgentAttention;
|
this.onAgentAttention = options?.onAgentAttention;
|
||||||
this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
|
this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
|
||||||
|
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
||||||
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
||||||
this.rescueTimeouts = {
|
this.rescueTimeouts = {
|
||||||
reloadSessionCloseMs:
|
reloadSessionCloseMs:
|
||||||
@@ -502,6 +505,10 @@ export class AgentManager {
|
|||||||
this.mcpBaseUrl = url;
|
this.mcpBaseUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAppendSystemPrompt(prompt: string | null | undefined): void {
|
||||||
|
this.appendSystemPrompt = prompt ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
public getMetricsSnapshot(): AgentMetricsSnapshot {
|
public getMetricsSnapshot(): AgentMetricsSnapshot {
|
||||||
const byLifecycle: Record<string, number> = {};
|
const byLifecycle: Record<string, number> = {};
|
||||||
let withActiveForegroundTurn = 0;
|
let withActiveForegroundTurn = 0;
|
||||||
@@ -816,7 +823,9 @@ export class AgentManager {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.requireEnabledProvider(injectedConfig.provider);
|
this.requireEnabledProvider(injectedConfig.provider);
|
||||||
const normalizedConfig = await this.normalizeConfig(injectedConfig);
|
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
|
||||||
|
await this.normalizeConfig(injectedConfig),
|
||||||
|
);
|
||||||
const launchContext = this.buildLaunchContext(resolvedAgentId);
|
const launchContext = this.buildLaunchContext(resolvedAgentId);
|
||||||
const client = await this.requireAvailableClient({
|
const client = await this.requireAvailableClient({
|
||||||
provider: normalizedConfig.provider,
|
provider: normalizedConfig.provider,
|
||||||
@@ -860,7 +869,9 @@ export class AgentManager {
|
|||||||
...overrides,
|
...overrides,
|
||||||
provider: handle.provider,
|
provider: handle.provider,
|
||||||
} as AgentSessionConfig;
|
} as AgentSessionConfig;
|
||||||
const normalizedConfig = await this.normalizeConfig(mergedConfig);
|
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
|
||||||
|
await this.normalizeConfig(mergedConfig),
|
||||||
|
);
|
||||||
const resumeOverrides: Partial<AgentSessionConfig> = { ...overrides };
|
const resumeOverrides: Partial<AgentSessionConfig> = { ...overrides };
|
||||||
let hasResumeOverrides = overrides !== undefined;
|
let hasResumeOverrides = overrides !== undefined;
|
||||||
|
|
||||||
@@ -874,6 +885,11 @@ export class AgentManager {
|
|||||||
hasResumeOverrides = true;
|
hasResumeOverrides = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata.daemonAppendSystemPrompt !== normalizedConfig.daemonAppendSystemPrompt) {
|
||||||
|
resumeOverrides.daemonAppendSystemPrompt = normalizedConfig.daemonAppendSystemPrompt;
|
||||||
|
hasResumeOverrides = true;
|
||||||
|
}
|
||||||
|
|
||||||
const launchContext = this.buildLaunchContext(resolvedAgentId);
|
const launchContext = this.buildLaunchContext(resolvedAgentId);
|
||||||
const client = this.requireClient(handle.provider);
|
const client = this.requireClient(handle.provider);
|
||||||
const available = await client.isAvailable();
|
const available = await client.isAvailable();
|
||||||
@@ -919,7 +935,9 @@ export class AgentManager {
|
|||||||
...overrides,
|
...overrides,
|
||||||
provider,
|
provider,
|
||||||
} as AgentSessionConfig;
|
} as AgentSessionConfig;
|
||||||
const normalizedConfig = await this.normalizeConfig(refreshConfig);
|
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
|
||||||
|
await this.normalizeConfig(refreshConfig),
|
||||||
|
);
|
||||||
const launchContext = this.buildLaunchContext(agentId);
|
const launchContext = this.buildLaunchContext(agentId);
|
||||||
|
|
||||||
const session = handle
|
const session = handle
|
||||||
@@ -3437,6 +3455,26 @@ export class AgentManager {
|
|||||||
return normalized;
|
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 {
|
private buildLaunchContext(agentId: string): AgentLaunchContext {
|
||||||
return {
|
return {
|
||||||
agentId,
|
agentId,
|
||||||
|
|||||||
@@ -473,6 +473,11 @@ export interface AgentSessionConfig {
|
|||||||
* Mapped by each provider to its native instruction field.
|
* Mapped by each provider to its native instruction field.
|
||||||
*/
|
*/
|
||||||
systemPrompt?: string;
|
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;
|
modeId?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
thinkingOptionId?: 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 { findExecutable, isCommandAvailable } from "../../../../utils/executable.js";
|
||||||
import { withTimeout } from "../../../../utils/promise-timeout.js";
|
import { withTimeout } from "../../../../utils/promise-timeout.js";
|
||||||
import { execCommand } from "../../../../utils/spawn.js";
|
import { execCommand } from "../../../../utils/spawn.js";
|
||||||
import { getOrchestratorModeInstructions } from "../../orchestrator-instructions.js";
|
import { composeSystemPromptParts } from "../../system-prompt.js";
|
||||||
|
|
||||||
const fsPromises = promises;
|
const fsPromises = promises;
|
||||||
const CLAUDE_SETTING_SOURCES: NonNullable<ClaudeOptions["settingSources"]> = [
|
const CLAUDE_SETTING_SOURCES: NonNullable<ClaudeOptions["settingSources"]> = [
|
||||||
@@ -2314,9 +2314,9 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildAppendedSystemPrompt(): string {
|
private buildAppendedSystemPrompt(): string {
|
||||||
return [getOrchestratorModeInstructions(), this.config.systemPrompt?.trim()]
|
return (
|
||||||
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
|
composeSystemPromptParts(this.config.systemPrompt, this.config.daemonAppendSystemPrompt) ?? ""
|
||||||
.join("\n\n");
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildSdkEnv(extraClaudeOptions: Partial<ClaudeOptions> | undefined): NodeJS.ProcessEnv {
|
private buildSdkEnv(extraClaudeOptions: Partial<ClaudeOptions> | undefined): NodeJS.ProcessEnv {
|
||||||
@@ -2365,7 +2365,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
canUseTool: this.handlePermissionRequest,
|
canUseTool: this.handlePermissionRequest,
|
||||||
pathToClaudeCodeExecutable: claudeBinary,
|
pathToClaudeCodeExecutable: claudeBinary,
|
||||||
// Use Claude Code preset system prompt and load CLAUDE.md files
|
// 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: {
|
systemPrompt: {
|
||||||
type: "preset",
|
type: "preset",
|
||||||
preset: "claude_code",
|
preset: "claude_code",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
|
import { composeSystemPromptParts } from "../system-prompt.js";
|
||||||
import { curateAgentActivity } from "../activity-curator.js";
|
import { curateAgentActivity } from "../activity-curator.js";
|
||||||
import {
|
import {
|
||||||
mapCodexRolloutToolCall,
|
mapCodexRolloutToolCall,
|
||||||
@@ -3026,12 +3027,11 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
const settings: Record<string, unknown> = {};
|
const settings: Record<string, unknown> = {};
|
||||||
if (match.model) settings.model = match.model;
|
if (match.model) settings.model = match.model;
|
||||||
if (match.reasoning_effort) settings.reasoning_effort = match.reasoning_effort;
|
if (match.reasoning_effort) settings.reasoning_effort = match.reasoning_effort;
|
||||||
const developerInstructions = [
|
const developerInstructions = composeSystemPromptParts(
|
||||||
match.developer_instructions?.trim(),
|
match.developer_instructions,
|
||||||
this.config.systemPrompt?.trim(),
|
this.config.systemPrompt,
|
||||||
]
|
this.config.daemonAppendSystemPrompt,
|
||||||
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
|
);
|
||||||
.join("\n\n");
|
|
||||||
if (developerInstructions) settings.developer_instructions = developerInstructions;
|
if (developerInstructions) settings.developer_instructions = developerInstructions;
|
||||||
if (this.config.model) settings.model = this.config.model;
|
if (this.config.model) settings.model = this.config.model;
|
||||||
const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
||||||
@@ -3161,8 +3161,12 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const params: Record<string, unknown> = { threadId: this.currentThreadId };
|
const params: Record<string, unknown> = { threadId: this.currentThreadId };
|
||||||
if (this.config.systemPrompt?.trim()) {
|
const developerInstructions = composeSystemPromptParts(
|
||||||
params.developerInstructions = this.config.systemPrompt.trim();
|
this.config.systemPrompt,
|
||||||
|
this.config.daemonAppendSystemPrompt,
|
||||||
|
);
|
||||||
|
if (developerInstructions) {
|
||||||
|
params.developerInstructions = developerInstructions;
|
||||||
}
|
}
|
||||||
const codexConfig = this.buildCodexInnerConfig();
|
const codexConfig = this.buildCodexInnerConfig();
|
||||||
if (codexConfig) {
|
if (codexConfig) {
|
||||||
@@ -3257,6 +3261,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
approvalPolicy: string;
|
approvalPolicy: string;
|
||||||
sandboxPolicyType: string;
|
sandboxPolicyType: string;
|
||||||
hasOutputSchema: boolean;
|
hasOutputSchema: boolean;
|
||||||
|
hasDeveloperInstructions: boolean;
|
||||||
hasCodexConfig: boolean;
|
hasCodexConfig: boolean;
|
||||||
}> {
|
}> {
|
||||||
const input = await this.buildUserInput(prompt);
|
const input = await this.buildUserInput(prompt);
|
||||||
@@ -3299,8 +3304,12 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
if (options?.outputSchema) {
|
if (options?.outputSchema) {
|
||||||
params.outputSchema = normalizeCodexOutputSchema(options.outputSchema);
|
params.outputSchema = normalizeCodexOutputSchema(options.outputSchema);
|
||||||
}
|
}
|
||||||
if (this.config.systemPrompt?.trim()) {
|
const developerInstructions = composeSystemPromptParts(
|
||||||
params.developerInstructions = this.config.systemPrompt.trim();
|
this.config.systemPrompt,
|
||||||
|
this.config.daemonAppendSystemPrompt,
|
||||||
|
);
|
||||||
|
if (developerInstructions) {
|
||||||
|
params.developerInstructions = developerInstructions;
|
||||||
}
|
}
|
||||||
const codexConfig = this.buildCodexInnerConfig();
|
const codexConfig = this.buildCodexInnerConfig();
|
||||||
if (codexConfig) {
|
if (codexConfig) {
|
||||||
@@ -3313,6 +3322,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
approvalPolicy,
|
approvalPolicy,
|
||||||
sandboxPolicyType,
|
sandboxPolicyType,
|
||||||
hasOutputSchema: Boolean(options?.outputSchema),
|
hasOutputSchema: Boolean(options?.outputSchema),
|
||||||
|
hasDeveloperInstructions: Boolean(developerInstructions),
|
||||||
hasCodexConfig: Boolean(codexConfig),
|
hasCodexConfig: Boolean(codexConfig),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3323,6 +3333,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
approvalPolicy,
|
approvalPolicy,
|
||||||
sandboxPolicyType,
|
sandboxPolicyType,
|
||||||
hasOutputSchema,
|
hasOutputSchema,
|
||||||
|
hasDeveloperInstructions,
|
||||||
hasCodexConfig,
|
hasCodexConfig,
|
||||||
}: {
|
}: {
|
||||||
turnId: string;
|
turnId: string;
|
||||||
@@ -3330,6 +3341,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
approvalPolicy: string;
|
approvalPolicy: string;
|
||||||
sandboxPolicyType: string;
|
sandboxPolicyType: string;
|
||||||
hasOutputSchema: boolean;
|
hasOutputSchema: boolean;
|
||||||
|
hasDeveloperInstructions: boolean;
|
||||||
hasCodexConfig: boolean;
|
hasCodexConfig: boolean;
|
||||||
}): void {
|
}): void {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
@@ -3345,7 +3357,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
sandboxPolicyType,
|
sandboxPolicyType,
|
||||||
hasCollaborationMode: Boolean(this.resolvedCollaborationMode),
|
hasCollaborationMode: Boolean(this.resolvedCollaborationMode),
|
||||||
hasOutputSchema,
|
hasOutputSchema,
|
||||||
hasDeveloperInstructions: Boolean(this.config.systemPrompt?.trim()),
|
hasDeveloperInstructions,
|
||||||
hasCodexConfig,
|
hasCodexConfig,
|
||||||
},
|
},
|
||||||
"Starting Codex app-server turn",
|
"Starting Codex app-server turn",
|
||||||
@@ -3407,6 +3419,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
approvalPolicy: turnStart.approvalPolicy,
|
approvalPolicy: turnStart.approvalPolicy,
|
||||||
sandboxPolicyType: turnStart.sandboxPolicyType,
|
sandboxPolicyType: turnStart.sandboxPolicyType,
|
||||||
hasOutputSchema: turnStart.hasOutputSchema,
|
hasOutputSchema: turnStart.hasOutputSchema,
|
||||||
|
hasDeveloperInstructions: turnStart.hasDeveloperInstructions,
|
||||||
hasCodexConfig: turnStart.hasCodexConfig,
|
hasCodexConfig: turnStart.hasCodexConfig,
|
||||||
});
|
});
|
||||||
await this.client.request("turn/start", turnStart.params, TURN_START_TIMEOUT_MS);
|
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 approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
|
||||||
const sandbox = this.config.sandboxMode ?? preset.sandbox;
|
const sandbox = this.config.sandboxMode ?? preset.sandbox;
|
||||||
const innerConfig = this.buildCodexInnerConfig();
|
const innerConfig = this.buildCodexInnerConfig();
|
||||||
|
const developerInstructions = composeSystemPromptParts(
|
||||||
|
this.config.systemPrompt,
|
||||||
|
this.config.daemonAppendSystemPrompt,
|
||||||
|
);
|
||||||
const params: Record<string, unknown> = {
|
const params: Record<string, unknown> = {
|
||||||
model,
|
model,
|
||||||
cwd: this.config.cwd ?? null,
|
cwd: this.config.cwd ?? null,
|
||||||
approvalPolicy,
|
approvalPolicy,
|
||||||
sandbox,
|
sandbox,
|
||||||
...(this.config.systemPrompt?.trim()
|
...(developerInstructions ? { developerInstructions } : {}),
|
||||||
? { developerInstructions: this.config.systemPrompt.trim() }
|
|
||||||
: {}),
|
|
||||||
...(innerConfig ? { config: innerConfig } : {}),
|
...(innerConfig ? { config: innerConfig } : {}),
|
||||||
...(this.ephemeral ? { ephemeral: true } : {}),
|
...(this.ephemeral ? { ephemeral: true } : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import {
|
|||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
import { runProviderTurn } from "./provider-runner.js";
|
import { runProviderTurn } from "./provider-runner.js";
|
||||||
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
|
import { composeSystemPromptParts } from "../system-prompt.js";
|
||||||
import {
|
import {
|
||||||
createSdkOpenCodeClient,
|
createSdkOpenCodeClient,
|
||||||
type OpenCodeRuntime,
|
type OpenCodeRuntime,
|
||||||
@@ -2589,6 +2590,10 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
partTypes: parts.map((p) => p.type),
|
partTypes: parts.map((p) => p.type),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
const systemPrompt = composeSystemPromptParts(
|
||||||
|
this.config.systemPrompt,
|
||||||
|
this.config.daemonAppendSystemPrompt,
|
||||||
|
);
|
||||||
const promptResponse = await this.client.session.promptAsync({
|
const promptResponse = await this.client.session.promptAsync({
|
||||||
sessionID: this.sessionId,
|
sessionID: this.sessionId,
|
||||||
directory: this.config.cwd,
|
directory: this.config.cwd,
|
||||||
@@ -2601,7 +2606,7 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}),
|
...(systemPrompt ? { system: systemPrompt } : {}),
|
||||||
...(model ? { model } : {}),
|
...(model ? { model } : {}),
|
||||||
...(effectiveMode ? { agent: effectiveMode } : {}),
|
...(effectiveMode ? { agent: effectiveMode } : {}),
|
||||||
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
...(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;
|
||||||
|
}
|
||||||
@@ -230,6 +230,7 @@ export interface PaseoDaemonConfig {
|
|||||||
mcpEnabled?: boolean;
|
mcpEnabled?: boolean;
|
||||||
mcpInjectIntoAgents?: boolean;
|
mcpInjectIntoAgents?: boolean;
|
||||||
autoArchiveAfterMerge?: boolean;
|
autoArchiveAfterMerge?: boolean;
|
||||||
|
appendSystemPrompt?: string;
|
||||||
staticDir: string;
|
staticDir: string;
|
||||||
mcpDebug: boolean;
|
mcpDebug: boolean;
|
||||||
isDev?: boolean;
|
isDev?: boolean;
|
||||||
@@ -290,6 +291,7 @@ export async function createPaseoDaemon(
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
|
autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
|
||||||
|
appendSystemPrompt: config.appendSystemPrompt ?? "",
|
||||||
},
|
},
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
@@ -516,6 +518,7 @@ export async function createPaseoDaemon(
|
|||||||
},
|
},
|
||||||
providerDefinitions: providerRegistry,
|
providerDefinitions: providerRegistry,
|
||||||
registry: agentStorage,
|
registry: agentStorage,
|
||||||
|
appendSystemPrompt: config.appendSystemPrompt,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -862,6 +865,9 @@ export async function createPaseoDaemon(
|
|||||||
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
|
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
|
||||||
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
|
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
|
||||||
});
|
});
|
||||||
|
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
|
||||||
|
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
|
||||||
|
});
|
||||||
const relayEnabled = config.relayEnabled ?? true;
|
const relayEnabled = config.relayEnabled ?? true;
|
||||||
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
||||||
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
||||||
|
|||||||
@@ -256,6 +256,10 @@ function resolveAuthConfig(
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAppendSystemPrompt(persisted: ReturnType<typeof loadPersistedConfig>): string {
|
||||||
|
return persisted.daemon?.appendSystemPrompt ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
function resolveStaticLoadConfigSettings(
|
function resolveStaticLoadConfigSettings(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
cli: CliConfigOverrides | undefined,
|
cli: CliConfigOverrides | undefined,
|
||||||
@@ -266,6 +270,7 @@ function resolveStaticLoadConfigSettings(
|
|||||||
mcpInjectIntoAgents:
|
mcpInjectIntoAgents:
|
||||||
cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false,
|
cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false,
|
||||||
autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
|
autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
|
||||||
|
appendSystemPrompt: resolveAppendSystemPrompt(persisted),
|
||||||
hostnames: mergeHostnames([
|
hostnames: mergeHostnames([
|
||||||
persisted.daemon?.hostnames,
|
persisted.daemon?.hostnames,
|
||||||
parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
|
parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
|
||||||
@@ -286,8 +291,14 @@ export function loadConfig(
|
|||||||
const persisted = loadPersistedConfig(paseoHome);
|
const persisted = loadPersistedConfig(paseoHome);
|
||||||
|
|
||||||
const listen = resolveListenAddress(env, options?.cli, persisted);
|
const listen = resolveListenAddress(env, options?.cli, persisted);
|
||||||
const { mcpEnabled, mcpInjectIntoAgents, autoArchiveAfterMerge, hostnames, appBaseUrl } =
|
const {
|
||||||
resolveStaticLoadConfigSettings(env, options?.cli, persisted);
|
mcpEnabled,
|
||||||
|
mcpInjectIntoAgents,
|
||||||
|
autoArchiveAfterMerge,
|
||||||
|
appendSystemPrompt,
|
||||||
|
hostnames,
|
||||||
|
appBaseUrl,
|
||||||
|
} = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
|
||||||
|
|
||||||
const relay = resolveRelayConfig({
|
const relay = resolveRelayConfig({
|
||||||
env,
|
env,
|
||||||
@@ -315,6 +326,7 @@ export function loadConfig(
|
|||||||
mcpEnabled,
|
mcpEnabled,
|
||||||
mcpInjectIntoAgents,
|
mcpInjectIntoAgents,
|
||||||
autoArchiveAfterMerge,
|
autoArchiveAfterMerge,
|
||||||
|
appendSystemPrompt,
|
||||||
mcpDebug: env.MCP_DEBUG === "1",
|
mcpDebug: env.MCP_DEBUG === "1",
|
||||||
isDev: resolvePaseoNodeEnv(env) === "development",
|
isDev: resolvePaseoNodeEnv(env) === "development",
|
||||||
agentStoragePath: path.join(paseoHome, "agents"),
|
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", () => {
|
test("patch persists provider additional models into config.json", () => {
|
||||||
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
|
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
|
||||||
tempDirs.push(paseoHome);
|
tempDirs.push(paseoHome);
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ function mergeMutableConfigIntoPersistedConfig(params: {
|
|||||||
injectIntoAgents: mutable.mcp.injectIntoAgents,
|
injectIntoAgents: mutable.mcp.injectIntoAgents,
|
||||||
},
|
},
|
||||||
autoArchiveAfterMerge: mutable.autoArchiveAfterMerge,
|
autoArchiveAfterMerge: mutable.autoArchiveAfterMerge,
|
||||||
|
appendSystemPrompt: mutable.appendSystemPrompt,
|
||||||
},
|
},
|
||||||
agents:
|
agents:
|
||||||
providerOverrides && Object.keys(providerOverrides).length > 0
|
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", () => {
|
describe("PersistedConfigSchema daemon relay config", () => {
|
||||||
test("accepts optional relay TLS setting", () => {
|
test("accepts optional relay TLS setting", () => {
|
||||||
const parsed = PersistedConfigSchema.parse({
|
const parsed = PersistedConfigSchema.parse({
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const PersistedConfigSchema = z
|
|||||||
.passthrough()
|
.passthrough()
|
||||||
.optional(),
|
.optional(),
|
||||||
autoArchiveAfterMerge: z.boolean().optional(),
|
autoArchiveAfterMerge: z.boolean().optional(),
|
||||||
|
appendSystemPrompt: z.string().optional(),
|
||||||
cors: z
|
cors: z
|
||||||
.object({
|
.object({
|
||||||
allowedOrigins: z.array(z.string()).optional(),
|
allowedOrigins: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const MutableDaemonConfigSchema = z
|
|||||||
.passthrough(),
|
.passthrough(),
|
||||||
providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}),
|
providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}),
|
||||||
autoArchiveAfterMerge: z.boolean().default(false),
|
autoArchiveAfterMerge: z.boolean().default(false),
|
||||||
|
appendSystemPrompt: z.string().default(""),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ export const MutableDaemonConfigPatchSchema = z
|
|||||||
.record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough())
|
.record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough())
|
||||||
.optional(),
|
.optional(),
|
||||||
autoArchiveAfterMerge: z.boolean().optional(),
|
autoArchiveAfterMerge: z.boolean().optional(),
|
||||||
|
appendSystemPrompt: z.string().optional(),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|||||||
@@ -54,6 +54,12 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
|
"autoArchiveAfterMerge": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"appendSystemPrompt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"cors": {
|
"cors": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -77,6 +83,22 @@
|
|||||||
},
|
},
|
||||||
"publicEndpoint": {
|
"publicEndpoint": {
|
||||||
"type": "string"
|
"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
|
"additionalProperties": false
|
||||||
@@ -248,6 +270,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
"confidenceThreshold": {
|
"confidenceThreshold": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
@@ -285,6 +311,10 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
Reference in New Issue
Block a user