Entropic 2.11.1
Local-first agentic inference engine
Loading...
Searching...
No Matches
manager.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
10#include "yaml_util.h"
11
12#include <ryml.hpp>
13#include <c4/std/string.hpp>
14
15static auto s_log = entropic::log::get("prompts");
16
17// Pull yaml_util helpers into this TU to avoid qualifying every call.
23
24namespace entropic::prompts {
25
33static std::string trim(const std::string& s)
34{
35 auto start = s.find_first_not_of(" \t\n\r");
36 if (start == std::string::npos) {
37 return "";
38 }
39 auto end = s.find_last_not_of(" \t\n\r");
40 return s.substr(start, end - start + 1);
41}
42
58static std::string parse_frontmatter(
59 const std::string& content,
60 const std::filesystem::path& path,
61 ryml::Tree& tree,
62 std::string& body)
63{
64 if (content.substr(0, 3) != "---") {
65 return "prompt file " + path.string()
66 + " missing YAML frontmatter";
67 }
68
69 auto second_delim = content.find("---", 3);
70 if (second_delim == std::string::npos) {
71 return "prompt file " + path.string()
72 + " has malformed frontmatter";
73 }
74
75 std::string yaml_block = content.substr(3, second_delim - 3);
76 body = trim(content.substr(second_delim + 3));
77
78 tree = ryml::parse_in_arena(
79 ryml::to_csubstr(path.string()),
80 ryml::to_csubstr(yaml_block));
81
82 return "";
83}
84
93{
94 static constexpr const char* names[] = {"constitution", "app_context", "identity"};
95 int idx = static_cast<int>(type);
96 return (idx >= 0 && idx <= 2) ? names[idx] : "unknown";
97}
98
109 const std::string& type_str, const std::filesystem::path& path,
110 std::string& err) {
111 PromptType t{};
112 if (type_str == "constitution") {
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;
118 } else {
119 err = "prompt file " + path.string()
120 + " has unknown type '" + type_str + "'";
121 }
122 return t;
123}
124
135 const std::filesystem::path& path,
136 PromptType expected_type,
137 ParsedPrompt& result)
138{
139 std::string err;
140
141 auto content = read_file(path);
142 if (content.empty()) {
143 err = "cannot read prompt file: " + path.string();
144 }
145
146 ryml::Tree tree;
147 if (err.empty()) {
148 err = parse_frontmatter(content, path, tree, result.body);
149 }
150
151 ryml::ConstNodeRef root;
152 std::string type_str;
153 if (err.empty()) {
154 root = tree.rootref();
155 if (!extract(root, "type", type_str)) {
156 err = "prompt file " + path.string()
157 + " missing 'type' field";
158 }
159 }
160
161 PromptType actual_type{};
162 if (err.empty()) {
163 actual_type = prompt_type_from_string(type_str, path, err);
164 }
165
166 if (err.empty() && actual_type != expected_type) {
167 err = "prompt file " + path.string() + " has type '"
168 + type_str + "' but was loaded as '"
169 + prompt_type_to_string(expected_type) + "'";
170 }
171
172 if (err.empty()) {
173 result.type = actual_type;
174 extract(root, "version", result.version);
175 }
176
177 return err;
178}
179
187static void extract_phases(
188 ryml::ConstNodeRef root, IdentityFrontmatter& fm)
189{
190 if (!root.has_child("phases") || !root["phases"].is_map()) {
191 return;
192 }
193 fm.phases.emplace();
194 for (auto child : root["phases"]) {
195 PhaseConfig phase;
196 std::string phase_name = to_string(child.key());
197 extract(child, "temperature", phase.temperature);
198 extract(child, "max_output_tokens", phase.max_output_tokens);
199 extract(child, "enable_thinking", phase.enable_thinking);
200 extract(child, "repeat_penalty", phase.repeat_penalty);
201 extract_string_list_opt(child, "bash_commands", phase.bash_commands);
202 (*fm.phases)[phase_name] = std::move(phase);
203 }
204}
205
214 ryml::ConstNodeRef root, IdentityFrontmatter& fm)
215{
216 if (!root.has_child("benchmark") || !root["benchmark"].is_map()) {
217 return;
218 }
219 fm.benchmark.emplace();
220 auto bench = root["benchmark"];
221 if (!bench.has_child("prompts") || !bench["prompts"].is_seq()) {
222 return;
223 }
224 for (auto p_node : bench["prompts"]) {
226 extract(p_node, "prompt", bp.prompt);
227 if (p_node.has_child("checks") && p_node["checks"].is_seq()) {
228 for (auto c_node : p_node["checks"]) {
229 std::string check_yaml;
230 ryml::emitrs_yaml(c_node, &check_yaml);
231 bp.checks_yaml.push_back(std::move(check_yaml));
232 }
233 }
234 fm.benchmark->prompts.push_back(std::move(bp));
235 }
236}
237
258static void extract_sampler_knobs(ryml::ConstNodeRef root,
260 int ival = 0;
261 if (extract(root, "max_output_tokens", ival)) { fm.max_output_tokens = ival; }
262 if (extract(root, "top_k", ival)) { fm.top_k = ival; }
263 float fval = 0.0f;
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; }
270 // enable_thinking's GenerationParams default is true, so "not set"
271 // must stay distinct from an explicit false (gh#86).
272 bool think = false;
273 if (extract(root, "enable_thinking", think)) { fm.enable_thinking = think; }
274}
275
283static void extract_identity_flags(ryml::ConstNodeRef root,
285 extract_sampler_knobs(root, fm); // gh#82/gh#85/gh#86
286 extract(root, "interstitial", fm.interstitial);
287 extract(root, "routable", fm.routable);
288 bool ec_val{};
289 if (extract(root, "explicit_completion", ec_val)) {
290 fm.explicit_completion = ec_val;
291 }
292 extract(root, "relay_single_delegate", fm.relay_single_delegate);
293 // E6 (2.0.6-rc18): per-identity loop + tool-call caps
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);
298}
299
306 ryml::ConstNodeRef root, IdentityFrontmatter& fm)
307{
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);
313
314 std::string grammar_str;
315 if (extract(root, "grammar", grammar_str)) {
316 fm.grammar = grammar_str;
317 }
318 std::string auto_chain_str;
319 if (extract(root, "auto_chain", auto_chain_str)) {
320 fm.auto_chain = auto_chain_str;
321 }
322
323 extract_string_list_opt(root, "allowed_tools", fm.allowed_tools);
324 extract_string_list_opt(root, "bash_commands", fm.bash_commands);
325
326 extract_identity_flags(root, fm);
327
328 extract_phases(root, fm);
329 extract_benchmark(root, fm);
330}
331
340std::string load_identity(
341 const std::filesystem::path& path,
342 ParsedIdentity& identity)
343{
344 std::string err;
345
346 auto content = read_file(path);
347 if (content.empty()) {
348 err = "cannot read identity file: " + path.string();
349 }
350
351 ryml::Tree tree;
352 if (err.empty()) {
353 err = parse_frontmatter(content, path, tree, identity.body);
354 }
355
356 ryml::ConstNodeRef root;
357 if (err.empty()) {
358 root = tree.rootref();
359
360 // Validate type field
361 std::string type_str;
362 if (!extract(root, "type", type_str) || type_str != "identity") {
363 err = "prompt file " + path.string()
364 + " is not an identity file (type='"
365 + type_str + "')";
366 }
367 }
368
369 if (err.empty()) {
370 extract_identity_fields(root, identity.frontmatter);
371
372 if (identity.frontmatter.focus.empty()) {
373 err = "identity " + path.string()
374 + ": focus must have at least one entry";
375 } else if (identity.frontmatter.name.empty()) {
376 err = "identity " + path.string()
377 + ": name must not be empty";
378 }
379 }
380
381 return err;
382}
383
395 const std::optional<std::filesystem::path>& constitution_path,
396 bool disabled,
397 const std::filesystem::path& data_dir,
398 std::string& body)
399{
400 std::string err;
401
402 if (disabled) {
403 s_log->info("Constitution disabled by config");
404 body.clear();
405 } else {
406 std::filesystem::path path = constitution_path.has_value()
407 ? *constitution_path
408 : data_dir / "prompts" / "constitution.md";
409
410 if (!std::filesystem::exists(path)) {
411 err = "constitution file not found: " + path.string();
412 }
413
414 ParsedPrompt result;
415 if (err.empty()) {
416 err = parse_prompt_file(path, PromptType::CONSTITUTION, result);
417 }
418
419 if (err.empty()) {
420 body = std::move(result.body);
421 s_log->info("Constitution loaded from {}", path.string());
422 }
423 }
424
425 return err;
426}
427
443static std::string load_app_context_file(
444 const std::filesystem::path& app_context_path,
445 const std::filesystem::path& data_dir,
446 std::string& body)
447{
448 auto path = app_context_path;
449
450 // Bare filename resolves as bundled prompt
451 if (!path.has_parent_path() || path.parent_path().empty()) {
452 path = data_dir / "prompts" / path;
453 }
454
455 std::string err;
456 if (!std::filesystem::exists(path)) {
457 err = "app_context file not found: " + path.string();
458 }
459
460 ParsedPrompt result;
461 if (err.empty()) {
462 err = parse_prompt_file(path, PromptType::APP_CONTEXT, result);
463 }
464
465 if (err.empty()) {
466 body = std::move(result.body);
467 s_log->info("App context loaded from {}", path.string());
468 }
469
470 return err;
471}
472
490 const std::optional<std::filesystem::path>& app_context_path,
491 const std::optional<std::string>& app_context_content,
492 bool disabled,
493 const std::filesystem::path& data_dir,
494 std::string& body)
495{
496 std::string err;
497
498 // gh#141: inline content wins over a path, but NOT over an explicit
499 // opt-out — `app_context: false` still means off, whatever else is set.
500 // Checked before the path branch so the filesystem is never touched when
501 // the caller already holds the text; that is the whole point of the
502 // feature for a consumer that cannot write the file.
503 if (!disabled && app_context_content.has_value()) {
504 body = *app_context_content;
505 s_log->info("App context supplied inline ({} bytes)", body.size());
506 return err;
507 }
508
509 if (disabled || !app_context_path.has_value()) {
510 s_log->info("App context disabled (not configured)");
511 body.clear();
512 } else {
513 err = load_app_context_file(*app_context_path, data_dir, body);
514 }
515
516 return err;
517}
518
535 const entropic::TierConfig& tier_config,
536 const std::string& tier_name,
537 const std::filesystem::path& data_dir)
538{
539 std::filesystem::path id_path;
540 if (tier_config.identity.has_value()) {
541 id_path = tier_config.identity.value();
542 } else if (!tier_config.identity_disabled) {
543 id_path = data_dir / "prompts"
544 / ("identity_" + tier_name + ".md");
545 }
547 if (id_path.empty() || !std::filesystem::exists(id_path)) {
548 return id;
549 }
550 auto err = load_identity(id_path, id);
551 if (!err.empty()) {
552 s_log->warn("identity load failed for tier '{}': {}",
553 tier_name, err);
554 return ParsedIdentity{};
555 }
556 s_log->info("identity loaded for tier '{}' from {}",
557 tier_name, id_path.string());
558 return id;
559}
560
571 const entropic::TierConfig& tier_config,
572 const std::string& tier_name,
573 const std::filesystem::path& data_dir)
574{
576 tier_config, tier_name, data_dir).body;
577}
578
591std::string assemble(
592 const entropic::ParsedConfig& config,
593 const std::filesystem::path& data_dir) {
594 std::string constitution, app_ctx;
595
597 data_dir, constitution);
599 config.app_context_disabled, data_dir, app_ctx);
600
601 std::string identity_body;
602 auto tier_it = config.models.tiers.find(config.models.default_tier);
603 if (tier_it != config.models.tiers.end()) {
604 identity_body = resolve_tier_identity(
605 tier_it->second, config.models.default_tier, data_dir);
606 }
607
608 std::string prompt;
609 if (!constitution.empty()) { prompt += constitution + "\n\n"; }
610 if (!app_ctx.empty()) { prompt += app_ctx + "\n\n"; }
611 if (!identity_body.empty()) { prompt += identity_body; }
612
613 s_log->info("system prompt assembled: {} chars "
614 "(constitution={}, app_context={}, identity={})",
615 prompt.size(), !constitution.empty(),
616 !app_ctx.empty(), !identity_body.empty());
617 return prompt;
618}
619
620} // namespace entropic::prompts
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
static std::string trim(const std::string &s)
Trim leading and trailing whitespace from a string.
Definition manager.cpp:33
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.
Definition manager.cpp:443
static void extract_benchmark(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract benchmark config from identity frontmatter.
Definition manager.cpp:213
static void extract_phases(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract phase configs from identity frontmatter.
Definition manager.cpp:187
static void extract_identity_flags(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract the scalar loop/flag fields of an identity.
Definition manager.cpp:283
static void extract_identity_fields(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract all identity frontmatter fields into the struct.
Definition manager.cpp:305
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.
Definition manager.cpp:108
static void extract_sampler_knobs(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract identity-specific fields from a pre-parsed ryml tree.
Definition manager.cpp:258
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.
Definition manager.cpp:58
Prompt manager — frontmatter parsing, identity loading, assembly.
ENTROPIC_EXPORT const char * prompt_type_to_string(PromptType type)
Convert PromptType to string.
Definition manager.cpp:92
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.
Definition manager.cpp:534
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.
Definition manager.cpp:394
ENTROPIC_EXPORT std::string load_identity(const std::filesystem::path &path, ParsedIdentity &identity)
Load an identity file: parse frontmatter + body.
Definition manager.cpp:340
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.
Definition manager.cpp:570
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.
Definition manager.cpp:489
PromptType
Prompt file type (frontmatter "type" field).
Definition manager.h:33
ENTROPIC_EXPORT std::string assemble(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Assemble the full system prompt from config.
Definition manager.cpp:591
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.
Definition manager.cpp:134
std::unordered_map< std::string, TierConfig > tiers
Tier name → config.
Definition config.h:573
std::string default_tier
Default tier name.
Definition config.h:575
Full parsed configuration.
Definition config.h:985
std::optional< std::filesystem::path > app_context
App context: nullopt = disabled by default.
Definition config.h:1002
std::optional< std::string > app_context_content
Inline app_context text, supplied instead of a path (gh#141).
Definition config.h:1013
ModelsConfig models
Tiers + router.
Definition config.h:986
bool app_context_disabled
true if app_context explicitly disabled
Definition config.h:1003
std::optional< std::filesystem::path > constitution
Constitution: nullopt = bundled default, disabled = explicit false.
Definition config.h:998
bool constitution_disabled
true if constitution explicitly disabled
Definition config.h:999
Inference parameters for a single identity phase.
Definition config.h:1063
int max_output_tokens
Max tokens per generation.
Definition config.h:1065
float repeat_penalty
Repetition penalty.
Definition config.h:1067
bool enable_thinking
Enable think-block output.
Definition config.h:1066
std::optional< std::vector< std::string > > bash_commands
Phase-specific bash commands.
Definition config.h:1068
float temperature
Sampling temperature.
Definition config.h:1064
Tier-specific model configuration.
Definition config.h:442
std::optional< std::filesystem::path > identity
Identity prompt path (nullopt = bundled)
Definition config.h:443
bool identity_disabled
true if identity explicitly disabled
Definition config.h:444
A single benchmark prompt with quality checks.
Definition manager.h:43
std::vector< std::string > checks_yaml
Check defs as YAML strings.
Definition manager.h:45
std::string prompt
Prompt text.
Definition manager.h:44
Identity frontmatter — full tier identity metadata.
Definition manager.h:65
std::vector< std::string > validation_rules
Per-identity constitutional rules (v2.0.6)
Definition manager.h:88
std::optional< std::vector< std::string > > allowed_tools
Tool filter.
Definition manager.h:74
std::optional< float > top_p
Per-tier top_p (gh#85); nullopt = use param default.
Definition manager.h:78
std::optional< float > repeat_penalty
Per-tier repeat_penalty (gh#86); nullopt = use param default.
Definition manager.h:83
std::optional< std::string > auto_chain
Auto-chain target tier.
Definition manager.h:73
int max_iterations
Per-identity loop iteration cap; -1 = use global (E6)
Definition manager.h:90
std::optional< float > min_p
Per-tier min_p (gh#85); nullopt = use param default.
Definition manager.h:80
std::vector< std::string > focus
Focus areas (min 1)
Definition manager.h:70
std::optional< float > presence_penalty
Per-tier presence_penalty (gh#85); nullopt = use param default.
Definition manager.h:81
PromptType type
Always IDENTITY.
Definition manager.h:66
std::optional< bool > enable_thinking
Per-tier thinking mode (gh#86); nullopt = use param default.
Definition manager.h:84
std::optional< int > max_output_tokens
Per-tier max output tokens (gh#82); nullopt = use param default.
Definition manager.h:76
std::string name
Tier name (e.g., "lead")
Definition manager.h:69
bool routable
Visible to router.
Definition manager.h:86
std::optional< int > top_k
Per-tier top_k (gh#85); nullopt = use param default.
Definition manager.h:79
bool relay_single_delegate
Skip re-synthesis when single delegate returns (v2.0.11)
Definition manager.h:89
std::optional< std::vector< std::string > > bash_commands
Allowed bash commands.
Definition manager.h:75
std::optional< float > frequency_penalty
Per-tier frequency_penalty (gh#85); nullopt = use param default.
Definition manager.h:82
int max_tool_calls_per_turn
Per-identity tool call cap; -1 = use global (E6)
Definition manager.h:91
bool interstitial
Interstitial role.
Definition manager.h:85
int max_consecutive_empty_turns
Per-identity empty-turn allowance; -1 = use global default (gh#123)
Definition manager.h:92
std::optional< std::unordered_map< std::string, PhaseConfig > > phases
Named phases.
Definition manager.h:93
std::vector< std::string > examples
Few-shot examples.
Definition manager.h:71
std::optional< BenchmarkSpec > benchmark
Benchmark definition.
Definition manager.h:94
std::optional< std::string > grammar
Grammar file reference.
Definition manager.h:72
std::optional< bool > explicit_completion
Requires explicit completion; nullopt = derive from auto_chain (gh#117)
Definition manager.h:87
std::optional< float > temperature
Per-tier sampling temperature (gh#82); nullopt = use param default.
Definition manager.h:77
Parsed identity file: frontmatter + body.
Definition manager.h:111
IdentityFrontmatter frontmatter
Full identity metadata.
Definition manager.h:112
std::string body
Markdown system prompt body.
Definition manager.h:113
Parsed prompt file result: type + version + body.
Definition manager.h:101
PromptType type
Prompt type.
Definition manager.h:102
std::string body
Markdown body after frontmatter.
Definition manager.h:104
int version
Schema version.
Definition manager.h:103
std::string read_file(const std::filesystem::path &path)
Read a file into a string.
Definition yaml_util.cpp:39
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.
Definition yaml_util.cpp:27
bool extract(ryml::ConstNodeRef node, c4::csubstr key, std::string &out)
Extract a string value from a YAML node.
Definition yaml_util.cpp:85
ryml extraction helpers for config parsing.