Entropic 2.9.4
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 extract(root, "explicit_completion", fm.explicit_completion);
289 extract(root, "relay_single_delegate", fm.relay_single_delegate);
290 // E6 (2.0.6-rc18): per-identity loop + tool-call caps
291 extract(root, "max_iterations", fm.max_iterations);
292 extract(root, "max_tool_calls_per_turn", fm.max_tool_calls_per_turn);
293 extract_string_list(root, "validation_rules", fm.validation_rules);
294}
295
302 ryml::ConstNodeRef root, IdentityFrontmatter& fm)
303{
304 fm.type = PromptType::IDENTITY;
305 extract(root, "version", fm.version);
306 extract(root, "name", fm.name);
307 extract_string_list(root, "focus", fm.focus);
308 extract_string_list(root, "examples", fm.examples);
309
310 std::string grammar_str;
311 if (extract(root, "grammar", grammar_str)) {
312 fm.grammar = grammar_str;
313 }
314 std::string auto_chain_str;
315 if (extract(root, "auto_chain", auto_chain_str)) {
316 fm.auto_chain = auto_chain_str;
317 }
318
319 extract_string_list_opt(root, "allowed_tools", fm.allowed_tools);
320 extract_string_list_opt(root, "bash_commands", fm.bash_commands);
321
322 extract_identity_flags(root, fm);
323
324 extract_phases(root, fm);
325 extract_benchmark(root, fm);
326}
327
336std::string load_identity(
337 const std::filesystem::path& path,
338 ParsedIdentity& identity)
339{
340 std::string err;
341
342 auto content = read_file(path);
343 if (content.empty()) {
344 err = "cannot read identity file: " + path.string();
345 }
346
347 ryml::Tree tree;
348 if (err.empty()) {
349 err = parse_frontmatter(content, path, tree, identity.body);
350 }
351
352 ryml::ConstNodeRef root;
353 if (err.empty()) {
354 root = tree.rootref();
355
356 // Validate type field
357 std::string type_str;
358 if (!extract(root, "type", type_str) || type_str != "identity") {
359 err = "prompt file " + path.string()
360 + " is not an identity file (type='"
361 + type_str + "')";
362 }
363 }
364
365 if (err.empty()) {
366 extract_identity_fields(root, identity.frontmatter);
367
368 if (identity.frontmatter.focus.empty()) {
369 err = "identity " + path.string()
370 + ": focus must have at least one entry";
371 } else if (identity.frontmatter.name.empty()) {
372 err = "identity " + path.string()
373 + ": name must not be empty";
374 }
375 }
376
377 return err;
378}
379
391 const std::optional<std::filesystem::path>& constitution_path,
392 bool disabled,
393 const std::filesystem::path& data_dir,
394 std::string& body)
395{
396 std::string err;
397
398 if (disabled) {
399 s_log->info("Constitution disabled by config");
400 body.clear();
401 } else {
402 std::filesystem::path path = constitution_path.has_value()
403 ? *constitution_path
404 : data_dir / "prompts" / "constitution.md";
405
406 if (!std::filesystem::exists(path)) {
407 err = "constitution file not found: " + path.string();
408 }
409
410 ParsedPrompt result;
411 if (err.empty()) {
412 err = parse_prompt_file(path, PromptType::CONSTITUTION, result);
413 }
414
415 if (err.empty()) {
416 body = std::move(result.body);
417 s_log->info("Constitution loaded from {}", path.string());
418 }
419 }
420
421 return err;
422}
423
435 const std::optional<std::filesystem::path>& app_context_path,
436 bool disabled,
437 const std::filesystem::path& data_dir,
438 std::string& body)
439{
440 std::string err;
441
442 if (disabled || !app_context_path.has_value()) {
443 s_log->info("App context disabled (not configured)");
444 body.clear();
445 } else {
446 auto path = *app_context_path;
447
448 // Bare filename resolves as bundled prompt
449 if (!path.has_parent_path() || path.parent_path().empty()) {
450 path = data_dir / "prompts" / path;
451 }
452
453 if (!std::filesystem::exists(path)) {
454 err = "app_context file not found: " + path.string();
455 }
456
457 ParsedPrompt result;
458 if (err.empty()) {
459 err = parse_prompt_file(path, PromptType::APP_CONTEXT, result);
460 }
461
462 if (err.empty()) {
463 body = std::move(result.body);
464 s_log->info("App context loaded from {}", path.string());
465 }
466 }
467
468 return err;
469}
470
487 const entropic::TierConfig& tier_config,
488 const std::string& tier_name,
489 const std::filesystem::path& data_dir)
490{
491 std::filesystem::path id_path;
492 if (tier_config.identity.has_value()) {
493 id_path = tier_config.identity.value();
494 } else if (!tier_config.identity_disabled) {
495 id_path = data_dir / "prompts"
496 / ("identity_" + tier_name + ".md");
497 }
499 if (id_path.empty() || !std::filesystem::exists(id_path)) {
500 return id;
501 }
502 auto err = load_identity(id_path, id);
503 if (!err.empty()) {
504 s_log->warn("identity load failed for tier '{}': {}",
505 tier_name, err);
506 return ParsedIdentity{};
507 }
508 s_log->info("identity loaded for tier '{}' from {}",
509 tier_name, id_path.string());
510 return id;
511}
512
523 const entropic::TierConfig& tier_config,
524 const std::string& tier_name,
525 const std::filesystem::path& data_dir)
526{
528 tier_config, tier_name, data_dir).body;
529}
530
543std::string assemble(
544 const entropic::ParsedConfig& config,
545 const std::filesystem::path& data_dir) {
546 std::string constitution, app_ctx;
547
549 data_dir, constitution);
551 data_dir, app_ctx);
552
553 std::string identity_body;
554 auto tier_it = config.models.tiers.find(config.models.default_tier);
555 if (tier_it != config.models.tiers.end()) {
556 identity_body = resolve_tier_identity(
557 tier_it->second, config.models.default_tier, data_dir);
558 }
559
560 std::string prompt;
561 if (!constitution.empty()) { prompt += constitution + "\n\n"; }
562 if (!app_ctx.empty()) { prompt += app_ctx + "\n\n"; }
563 if (!identity_body.empty()) { prompt += identity_body; }
564
565 s_log->info("system prompt assembled: {} chars "
566 "(constitution={}, app_context={}, identity={})",
567 prompt.size(), !constitution.empty(),
568 !app_ctx.empty(), !identity_body.empty());
569 return prompt;
570}
571
572} // 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 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:301
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 std::string load_app_context(const std::optional< std::filesystem::path > &app_context_path, bool disabled, const std::filesystem::path &data_dir, std::string &body)
Load app_context prompt with tri-state resolution.
Definition manager.cpp:434
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:486
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:390
ENTROPIC_EXPORT std::string load_identity(const std::filesystem::path &path, ParsedIdentity &identity)
Load an identity file: parse frontmatter + body.
Definition manager.cpp:336
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:522
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:543
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:543
std::string default_tier
Default tier name.
Definition config.h:545
Full parsed configuration.
Definition config.h:929
std::optional< std::filesystem::path > app_context
App context: nullopt = disabled by default.
Definition config.h:946
ModelsConfig models
Tiers + router.
Definition config.h:930
bool app_context_disabled
true if app_context explicitly disabled
Definition config.h:947
std::optional< std::filesystem::path > constitution
Constitution: nullopt = bundled default, disabled = explicit false.
Definition config.h:942
bool constitution_disabled
true if constitution explicitly disabled
Definition config.h:943
Inference parameters for a single identity phase.
Definition config.h:997
int max_output_tokens
Max tokens per generation.
Definition config.h:999
float repeat_penalty
Repetition penalty.
Definition config.h:1001
bool enable_thinking
Enable think-block output.
Definition config.h:1000
std::optional< std::vector< std::string > > bash_commands
Phase-specific bash commands.
Definition config.h:1002
float temperature
Sampling temperature.
Definition config.h:998
Tier-specific model configuration.
Definition config.h:425
std::optional< std::filesystem::path > identity
Identity prompt path (nullopt = bundled)
Definition config.h:426
bool identity_disabled
true if identity explicitly disabled
Definition config.h:427
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
bool explicit_completion
Requires explicit completion.
Definition manager.h:87
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
std::optional< std::unordered_map< std::string, PhaseConfig > > phases
Named phases.
Definition manager.h:92
std::vector< std::string > examples
Few-shot examples.
Definition manager.h:71
std::optional< BenchmarkSpec > benchmark
Benchmark definition.
Definition manager.h:93
std::optional< std::string > grammar
Grammar file reference.
Definition manager.h:72
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:110
IdentityFrontmatter frontmatter
Full identity metadata.
Definition manager.h:111
std::string body
Markdown system prompt body.
Definition manager.h:112
Parsed prompt file result: type + version + body.
Definition manager.h:100
PromptType type
Prompt type.
Definition manager.h:101
std::string body
Markdown body after frontmatter.
Definition manager.h:103
int version
Schema version.
Definition manager.h:102
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.