From d4c3a9d2de6efe7528b81e32208ed8c0b05cfe4f Mon Sep 17 00:00:00 2001 From: Kayos Date: Wed, 13 May 2026 11:27:40 -0700 Subject: [PATCH] RunRequest: add system_mode (append|replace) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why this matters: - Default 'append' (--append-system-prompt): caller's system text is ADDED to Claude's default base prompt ('I am Claude, an AI assistant...'). Right for tool-using assistants and coding agents where the helpful-honest defaults are useful substrate. Every existing caller (cauldron, petalparse, etc) ran in this mode implicitly. - New 'replace' (--system-prompt): caller's system text REPLACES the default base prompt entirely. Right for personas — fiction authors, chat bots, in-world characters — where Claude's defaults would bleed through as friction. The model BECOMES the persona rather than 'Claude playing the persona.' Trigger for adding this: skald v0.3 — authors as personas with souls. Orson Black writing a Chernobyl piece can't have Claude's default helpfulness softening the prose; the soul needs to be the prompt, not append to it. Wire shape: - server.py: Literal['append','replace']|None in RunRequest, default 'append' - runner.py: branch on system_mode to pick the right CLI flag - clients/rust: SystemMode enum, Option on RunRequest Backward compatible: clients that don't set system_mode get append semantics (the existing behavior). --- clawdforge/runner.py | 11 ++++++++++- clawdforge/server.py | 22 +++++++++++++++++++++- clients/rust/src/lib.rs | 2 +- clients/rust/src/types.rs | 35 ++++++++++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/clawdforge/runner.py b/clawdforge/runner.py index f5bf687..261cd71 100644 --- a/clawdforge/runner.py +++ b/clawdforge/runner.py @@ -33,6 +33,7 @@ class Runner: prompt: str, model: str | None = None, system: str | None = None, + system_mode: str | None = "append", files: list[str] | None = None, timeout_secs: int | None = None, ) -> RunResult: @@ -50,7 +51,15 @@ class Runner: "--model", model or self.default_model, ] if system: - cmd += ["--append-system-prompt", system] + # "append" = additive on top of claude's defaults; right + # for tool-using assistants. "replace" = REPLACES the + # default base prompt entirely; right for personas + # (fiction authors, chat bots, in-world characters) where + # claude's defaults would bleed through as friction. + if system_mode == "replace": + cmd += ["--system-prompt", system] + else: + cmd += ["--append-system-prompt", system] if files: for f in files: cmd += ["--files", f] diff --git a/clawdforge/server.py b/clawdforge/server.py index 4238866..5e940ff 100644 --- a/clawdforge/server.py +++ b/clawdforge/server.py @@ -19,7 +19,7 @@ import os import time from contextlib import asynccontextmanager from pathlib import Path -from typing import Annotated +from typing import Annotated, Literal from fastapi import FastAPI, Header, HTTPException, Request, UploadFile, File, Form from fastapi.responses import JSONResponse @@ -103,6 +103,25 @@ class RunRequest(BaseModel): prompt: str = Field(min_length=1) model: str | None = None system: str | None = None + # How `system` gets passed to the underlying claude CLI: + # + # "append" → `--append-system-prompt`. Claude's default base + # prompt stays + the caller's `system` is added. + # Right for assistants / tool-use / coding agents + # where Claude's helpful-honest defaults are + # desirable substrate. + # + # "replace" → `--system-prompt`. Claude's default base prompt + # is REPLACED entirely by the caller's `system`. + # Right for personas (fiction authors, chat bots, + # in-world characters) where Claude's defaults + # would bleed through as friction. The model + # BECOMES the persona instead of "Claude playing + # the persona." + # + # Default is "append" for back-compat — every caller before this + # field landed used append semantics implicitly. + system_mode: Literal["append", "replace"] | None = "append" files: list[str] | None = None timeout_secs: int | None = Field(default=None, ge=5, le=600) @@ -183,6 +202,7 @@ def run( prompt=body.prompt, model=body.model, system=body.system, + system_mode=body.system_mode, files=file_paths or None, timeout_secs=body.timeout_secs, ) diff --git a/clients/rust/src/lib.rs b/clients/rust/src/lib.rs index 59d8313..bf3c207 100644 --- a/clients/rust/src/lib.rs +++ b/clients/rust/src/lib.rs @@ -54,5 +54,5 @@ pub use error::Error; pub use session::{Session, SessionList, SessionOptions, SessionState, TurnEvent, TurnResult}; pub use types::{ AppToken, AppTokenInfo, FileToken, Healthz, RunFailure, RunRequest, RunResult, - TokenCreateRequest, TokenList, + SystemMode, TokenCreateRequest, TokenList, }; diff --git a/clients/rust/src/types.rs b/clients/rust/src/types.rs index 1774539..6f62b9d 100644 --- a/clients/rust/src/types.rs +++ b/clients/rust/src/types.rs @@ -9,6 +9,23 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use crate::error::Error; +/// How `system` is passed to `claude -p`. See [`RunRequest::system_mode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SystemMode { + /// `--append-system-prompt` — additive on top of Claude's defaults. + Append, + /// `--system-prompt` — REPLACES Claude's default base prompt + /// entirely. Right for personas. + Replace, +} + +impl Default for SystemMode { + fn default() -> Self { + Self::Append + } +} + /// `GET /healthz` response body. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Healthz { @@ -43,10 +60,26 @@ pub struct RunRequest { #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, - /// System prompt appended via `claude -p --append-system-prompt`. + /// System prompt. How it's passed to `claude -p` depends on + /// [`Self::system_mode`]. #[serde(skip_serializing_if = "Option::is_none")] pub system: Option, + /// How `system` is passed to `claude -p`: + /// + /// - [`SystemMode::Append`] (default) → `--append-system-prompt`. + /// Claude's default base prompt stays + `system` is added. + /// Right for tool-using assistants where Claude's helpful- + /// honest defaults are useful substrate. + /// - [`SystemMode::Replace`] → `--system-prompt`. Replaces the + /// base prompt entirely. Right for personas (fiction authors, + /// chat bots, in-world characters) where Claude's defaults + /// would bleed through as friction. + /// + /// `None` = server default (currently `append`). + #[serde(skip_serializing_if = "Option::is_none")] + pub system_mode: Option, + /// File tokens previously returned from [`Client::upload_file`]. /// /// [`Client::upload_file`]: crate::Client::upload_file