13#include <c4/std/string.hpp>
24namespace entropic::prompts {
33static std::string
trim(
const std::string&
s)
35 auto start =
s.find_first_not_of(
" \t\n\r");
36 if (start == std::string::npos) {
39 auto end =
s.find_last_not_of(
" \t\n\r");
40 return s.substr(start,
end - start + 1);
59 const std::string& content,
60 const std::filesystem::path& path,
64 if (content.substr(0, 3) !=
"---") {
65 return "prompt file " + path.string()
66 +
" missing YAML frontmatter";
71 return "prompt file " + path.string()
72 +
" has malformed frontmatter";
78 tree = ryml::parse_in_arena(
79 ryml::to_csubstr(path.string()),
94 static constexpr const char*
names[] = {
"constitution",
"app_context",
"identity"};
95 int idx =
static_cast<int>(type);
109 const std::string&
type_str,
const std::filesystem::path& path,
113 t = PromptType::CONSTITUTION;
114 }
else if (
type_str ==
"app_context") {
115 t = PromptType::APP_CONTEXT;
116 }
else if (
type_str ==
"identity") {
117 t = PromptType::IDENTITY;
119 err =
"prompt file " + path.string()
120 +
" has unknown type '" +
type_str +
"'";
135 const std::filesystem::path& path,
141 auto content = read_file(path);
142 if (content.empty()) {
143 err =
"cannot read prompt file: " + path.string();
151 ryml::ConstNodeRef root;
154 root =
tree.rootref();
155 if (!extract(root,
"type",
type_str)) {
156 err =
"prompt file " + path.string()
157 +
" missing 'type' field";
167 err =
"prompt file " + path.string() +
" has type '"
168 +
type_str +
"' but was loaded as '"
174 extract(root,
"version", result.
version);
190 if (!root.has_child(
"phases") || !root[
"phases"].is_map()) {
194 for (
auto child : root[
"phases"]) {
216 if (!root.has_child(
"benchmark") || !root[
"benchmark"].is_map()) {
219 fm.benchmark.emplace();
220 auto bench = root[
"benchmark"];
221 if (!
bench.has_child(
"prompts") || !
bench[
"prompts"].is_seq()) {
226 extract(
p_node,
"prompt",
bp.prompt);
227 if (
p_node.has_child(
"checks") &&
p_node[
"checks"].is_seq()) {
234 fm.benchmark->prompts.push_back(std::move(
bp));
261 if (extract(root,
"max_output_tokens",
ival)) {
fm.max_output_tokens =
ival; }
262 if (extract(root,
"top_k",
ival)) {
fm.top_k =
ival; }
264 if (extract(root,
"temperature",
fval)) {
fm.temperature =
fval; }
265 if (extract(root,
"top_p",
fval)) {
fm.top_p =
fval; }
266 if (extract(root,
"min_p",
fval)) {
fm.min_p =
fval; }
267 if (extract(root,
"presence_penalty",
fval)) {
fm.presence_penalty =
fval; }
268 if (extract(root,
"frequency_penalty",
fval)) {
fm.frequency_penalty =
fval; }
269 if (extract(root,
"repeat_penalty",
fval)) {
fm.repeat_penalty =
fval; }
273 if (extract(root,
"enable_thinking",
think)) {
fm.enable_thinking =
think; }
286 extract(root,
"interstitial",
fm.interstitial);
287 extract(root,
"routable",
fm.routable);
289 if (extract(root,
"explicit_completion",
ec_val)) {
292 extract(root,
"relay_single_delegate",
fm.relay_single_delegate);
294 extract(root,
"max_iterations",
fm.max_iterations);
295 extract(root,
"max_tool_calls_per_turn",
fm.max_tool_calls_per_turn);
296 extract(root,
"max_consecutive_empty_turns",
fm.max_consecutive_empty_turns);
297 extract_string_list(root,
"validation_rules",
fm.validation_rules);
308 fm.type = PromptType::IDENTITY;
309 extract(root,
"version",
fm.version);
310 extract(root,
"name",
fm.name);
311 extract_string_list(root,
"focus",
fm.focus);
312 extract_string_list(root,
"examples",
fm.examples);
323 extract_string_list_opt(root,
"allowed_tools",
fm.allowed_tools);
324 extract_string_list_opt(root,
"bash_commands",
fm.bash_commands);
341 const std::filesystem::path& path,
346 auto content = read_file(path);
347 if (content.empty()) {
348 err =
"cannot read identity file: " + path.string();
356 ryml::ConstNodeRef root;
358 root =
tree.rootref();
363 err =
"prompt file " + path.string()
364 +
" is not an identity file (type='"
373 err =
"identity " + path.string()
374 +
": focus must have at least one entry";
376 err =
"identity " + path.string()
377 +
": name must not be empty";
397 const std::filesystem::path&
data_dir,
403 s_log->info(
"Constitution disabled by config");
408 :
data_dir /
"prompts" /
"constitution.md";
410 if (!std::filesystem::exists(path)) {
411 err =
"constitution file not found: " + path.string();
420 body = std::move(result.
body);
421 s_log->info(
"Constitution loaded from {}", path.string());
427 s_log->warn(
"Constitution not loaded: {}",
err);
451 const std::filesystem::path&
data_dir,
457 if (!path.has_parent_path() || path.parent_path().empty()) {
462 if (!std::filesystem::exists(path)) {
463 err =
"app_context file not found: " + path.string();
472 body = std::move(result.
body);
473 s_log->info(
"App context loaded from {}", path.string());
490 const std::optional<std::string>& app_context_content,
497 state = AppContextState::DISABLED;
498 }
else if (app_context_content.has_value()) {
499 state = AppContextState::INLINE;
501 state = AppContextState::FROM_PATH;
518 const char*
msg =
"App context not configured (no `app_context` key)";
519 if (state == AppContextState::DISABLED) {
520 msg =
"App context explicitly disabled (`app_context: false`)";
521 }
else if (state == AppContextState::INLINE) {
522 msg =
"App context supplied inline (`app_context: {content: ...}`)";
523 }
else if (state == AppContextState::FROM_PATH) {
524 msg =
"App context configured but rejected";
548 const std::optional<std::string>& app_context_content,
550 const std::filesystem::path&
data_dir,
557 if (state == AppContextState::INLINE) {
558 body = *app_context_content;
559 s_log->info(
"{} — {} bytes",
561 }
else if (state == AppContextState::FROM_PATH) {
588 const std::filesystem::path&
data_dir,
589 std::string& constitution,
590 std::string& app_context)
617 const std::filesystem::path&
data_dir)
620 if (
config.constitution.has_value() && !
config.constitution_disabled) {
624 err =
"constitution: " +
err
625 +
". A constitution file is one of entropic's own prompt "
626 "documents: it needs YAML frontmatter with "
627 "`type: constitution` between `---` lines.";
647 const std::filesystem::path&
data_dir)
650 && !
config.app_context_disabled
651 && !
config.app_context_content.has_value();
658 err =
"app_context: " +
err
659 +
". app_context is read as one of entropic's own prompt "
660 "documents, so the file needs YAML frontmatter with "
661 "`type: app_context` between `---` lines. To supply a "
662 "document you do not own — a repository's CLAUDE.md, for "
663 "example — pass the text inline instead of a path: "
664 "`app_context: {content: \"...\"}` (gh#141).";
680 const std::filesystem::path&
data_dir)
706 const std::string& tier_name,
707 const std::filesystem::path&
data_dir)
714 / (
"identity_" + tier_name +
".md");
722 s_log->warn(
"identity load failed for tier '{}': {}",
726 s_log->info(
"identity loaded for tier '{}' from {}",
742 const std::string& tier_name,
743 const std::filesystem::path&
data_dir)
763 const std::filesystem::path&
data_dir) {
764 std::string constitution,
app_ctx;
776 if (!constitution.empty()) { prompt += constitution +
"\n\n"; }
780 s_log->info(
"system prompt assembled: {} chars "
781 "(constitution={}, app_context={}, identity={})",
782 prompt.size(), !constitution.empty(),
std::string err
size_exceeded, when set
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
static std::string trim(const std::string &s)
Trim leading and trailing whitespace from a string.
static std::string load_app_context_file(const std::filesystem::path &app_context_path, const std::filesystem::path &data_dir, std::string &body)
Read app_context from a file, resolving bare names as bundled.
static void extract_benchmark(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract benchmark config from identity frontmatter.
static void extract_phases(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract phase configs from identity frontmatter.
static void extract_identity_flags(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract the scalar loop/flag fields of an identity.
static void extract_identity_fields(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract all identity frontmatter fields into the struct.
static std::string check_app_context_source(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Check a configured app_context path loads.
static PromptType prompt_type_from_string(const std::string &type_str, const std::filesystem::path &path, std::string &err)
Map a frontmatter type string to a PromptType.
static std::string check_constitution_source(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Check a configured constitution path loads.
static void extract_sampler_knobs(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract identity-specific fields from a pre-parsed ryml tree.
static std::string parse_frontmatter(const std::string &content, const std::filesystem::path &path, ryml::Tree &tree, std::string &body)
Parse YAML frontmatter from file content into a ryml tree.
Prompt manager — frontmatter parsing, identity loading, assembly.
ENTROPIC_EXPORT AppContextState classify_app_context(const std::optional< std::filesystem::path > &app_context_path, const std::optional< std::string > &app_context_content, bool disabled)
Classify the app_context configuration.
ENTROPIC_EXPORT const char * prompt_type_to_string(PromptType type)
Convert PromptType to string.
AppContextState
Which app_context spelling a config is using (gh#163).
ENTROPIC_EXPORT ParsedIdentity resolve_tier_identity_full(const entropic::TierConfig &tier_config, const std::string &tier_name, const std::filesystem::path &data_dir)
Resolve full parsed identity (body + frontmatter) for a tier.
ENTROPIC_EXPORT const char * app_context_state_message(AppContextState state)
The log message describing one app_context state.
ENTROPIC_EXPORT std::string load_constitution(const std::optional< std::filesystem::path > &constitution_path, bool disabled, const std::filesystem::path &data_dir, std::string &body)
Load constitution prompt with tri-state resolution.
ENTROPIC_EXPORT std::string load_identity(const std::filesystem::path &path, ParsedIdentity &identity)
Load an identity file: parse frontmatter + body.
ENTROPIC_EXPORT std::string validate_configured_prompts(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Check that every CONFIGURED prompt source can actually be loaded.
ENTROPIC_EXPORT std::string resolve_tier_identity(const entropic::TierConfig &tier_config, const std::string &tier_name, const std::filesystem::path &data_dir)
Resolve the system prompt body for a named tier.
ENTROPIC_EXPORT std::string load_app_context(const std::optional< std::filesystem::path > &app_context_path, const std::optional< std::string > &app_context_content, bool disabled, const std::filesystem::path &data_dir, std::string &body)
Load app_context prompt with tri-state resolution.
PromptType
Prompt file type (frontmatter "type" field).
ENTROPIC_EXPORT std::string assemble(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Assemble the full system prompt from config.
ENTROPIC_EXPORT std::string parse_prompt_file(const std::filesystem::path &path, PromptType expected_type, ParsedPrompt &result)
Parse a prompt file: validate frontmatter, return body.
ENTROPIC_EXPORT void load_shared_prompt_sources(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir, std::string &constitution, std::string &app_context)
Load constitution + app_context, logging any failure.
@ disabled
Named in disabled_mcpjson_servers — never connect.
@ optional
optional: every escape asks the host's path approver
std::string config
–config FILE
Full parsed configuration.
Inference parameters for a single identity phase.
int max_output_tokens
Max tokens per generation.
float repeat_penalty
Repetition penalty.
bool enable_thinking
Enable think-block output.
std::optional< std::vector< std::string > > bash_commands
Phase-specific bash commands.
float temperature
Sampling temperature.
Tier-specific model configuration.
A single benchmark prompt with quality checks.
Identity frontmatter — full tier identity metadata.
std::vector< std::string > focus
Focus areas (min 1)
std::string name
Tier name (e.g., "lead")
Parsed identity file: frontmatter + body.
IdentityFrontmatter frontmatter
Full identity metadata.
std::string body
Markdown system prompt body.
Parsed prompt file result: type + version + body.
PromptType type
Prompt type.
std::string body
Markdown body after frontmatter.
int version
Schema version.
std::string read_file(const std::filesystem::path &path)
Read a file into a string.
bool extract_string_list(ryml::ConstNodeRef node, c4::csubstr key, std::vector< std::string > &out)
Extract a vector of strings from a YAML sequence node.
bool extract_string_list_opt(ryml::ConstNodeRef node, c4::csubstr key, std::optional< std::vector< std::string > > &out)
Extract an optional vector of strings.
std::string to_string(c4::csubstr s)
Convert ryml csubstr to std::string.
bool extract(ryml::ConstNodeRef node, c4::csubstr key, std::string &out)
Extract a string value from a YAML node.
ryml extraction helpers for config parsing.