Entropic 2.14.3
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,
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
162 if (err.empty()) {
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 '"
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
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()
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 } else {
423 // gh#163: report the failure where the state is known. Every
424 // caller used to drop this string, so a rejected constitution
425 // was indistinguishable from one that was never configured.
426 body.clear();
427 s_log->warn("Constitution not loaded: {}", err);
428 }
429 }
430
431 return err;
432}
433
449static std::string load_app_context_file(
450 const std::filesystem::path& app_context_path,
451 const std::filesystem::path& data_dir,
452 std::string& body)
453{
454 auto path = app_context_path;
455
456 // Bare filename resolves as bundled prompt
457 if (!path.has_parent_path() || path.parent_path().empty()) {
458 path = data_dir / "prompts" / path;
459 }
460
461 std::string err;
462 if (!std::filesystem::exists(path)) {
463 err = "app_context file not found: " + path.string();
464 }
465
466 ParsedPrompt result;
467 if (err.empty()) {
468 err = parse_prompt_file(path, PromptType::APP_CONTEXT, result);
469 }
470
471 if (err.empty()) {
472 body = std::move(result.body);
473 s_log->info("App context loaded from {}", path.string());
474 }
475
476 return err;
477}
478
489 const std::optional<std::filesystem::path>& app_context_path,
490 const std::optional<std::string>& app_context_content,
491 bool disabled)
492{
493 // gh#141: inline content wins over a path, but NOT over an explicit
494 // opt-out — `app_context: false` still means off, whatever else is set.
495 AppContextState state = AppContextState::NOT_CONFIGURED;
496 if (disabled) {
497 state = AppContextState::DISABLED;
498 } else if (app_context_content.has_value()) {
499 state = AppContextState::INLINE;
500 } else if (app_context_path.has_value()) {
501 state = AppContextState::FROM_PATH;
502 }
503 return state;
504}
505
514{
515 // gh#163: three of these four states used to share one line,
516 // "App context disabled (not configured)" — which told a consumer
517 // whose path had been REJECTED that they had never configured one.
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";
525 }
526 return msg;
527}
528
547 const std::optional<std::filesystem::path>& app_context_path,
548 const std::optional<std::string>& app_context_content,
549 bool disabled,
550 const std::filesystem::path& data_dir,
551 std::string& body)
552{
553 std::string err;
554 const auto state = classify_app_context(
555 app_context_path, app_context_content, disabled);
556
557 if (state == AppContextState::INLINE) {
558 body = *app_context_content;
559 s_log->info("{} — {} bytes",
560 app_context_state_message(state), body.size());
561 } else if (state == AppContextState::FROM_PATH) {
563 if (!err.empty()) {
564 // The path was named and could not be used. Say so — this is
565 // the state the reporter was in, described as "not configured".
566 body.clear();
567 s_log->warn("{}: {}", app_context_state_message(state), err);
568 }
569 } else {
570 body.clear();
571 s_log->info("{}", app_context_state_message(state));
572 }
573
574 return err;
575}
576
587 const entropic::ParsedConfig& config,
588 const std::filesystem::path& data_dir,
589 std::string& constitution,
590 std::string& app_context)
591{
592 // gh#156: these two return values were dropped at all three call
593 // sites. A working load announced itself ("App context loaded
594 // from ..."), a failing one said nothing — the asymmetry WAS the
595 // bug, and it cost a consumer twenty minutes.
596 //
597 // gh#163: the report now lives inside each loader, which is the only
598 // layer that knows WHICH state produced no body. Collapsing the two
599 // calls here still matters — it is one call site to keep honest
600 // instead of three.
601 load_constitution(config.constitution, config.constitution_disabled,
602 data_dir, constitution);
603 load_app_context(config.app_context, config.app_context_content,
604 config.app_context_disabled, data_dir, app_context);
605}
606
615static std::string check_constitution_source(
616 const entropic::ParsedConfig& config,
617 const std::filesystem::path& data_dir)
618{
619 std::string err;
620 if (config.constitution.has_value() && !config.constitution_disabled) {
621 std::string body;
622 err = load_constitution(config.constitution, false, data_dir, body);
623 if (!err.empty()) {
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.";
628 }
629 }
630 return err;
631}
632
645static std::string check_app_context_source(
646 const entropic::ParsedConfig& config,
647 const std::filesystem::path& data_dir)
648{
649 const bool configured_as_path = config.app_context.has_value()
650 && !config.app_context_disabled
651 && !config.app_context_content.has_value();
652 std::string err;
653 if (configured_as_path) {
654 std::string body;
655 err = load_app_context(config.app_context, std::nullopt, false,
656 data_dir, body);
657 if (!err.empty()) {
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).";
665 }
666 }
667 return err;
668}
669
679 const entropic::ParsedConfig& config,
680 const std::filesystem::path& data_dir)
681{
683 if (err.empty()) {
685 }
686 return err;
687}
688
706 const std::string& tier_name,
707 const std::filesystem::path& data_dir)
708{
709 std::filesystem::path id_path;
710 if (tier_config.identity.has_value()) {
711 id_path = tier_config.identity.value();
712 } else if (!tier_config.identity_disabled) {
713 id_path = data_dir / "prompts"
714 / ("identity_" + tier_name + ".md");
715 }
717 if (id_path.empty() || !std::filesystem::exists(id_path)) {
718 return id;
719 }
720 auto err = load_identity(id_path, id);
721 if (!err.empty()) {
722 s_log->warn("identity load failed for tier '{}': {}",
723 tier_name, err);
724 return ParsedIdentity{};
725 }
726 s_log->info("identity loaded for tier '{}' from {}",
727 tier_name, id_path.string());
728 return id;
729}
730
742 const std::string& tier_name,
743 const std::filesystem::path& data_dir)
744{
746 tier_config, tier_name, data_dir).body;
747}
748
761std::string assemble(
762 const entropic::ParsedConfig& config,
763 const std::filesystem::path& data_dir) {
764 std::string constitution, app_ctx;
765
767
768 std::string identity_body;
769 auto tier_it = config.models.tiers.find(config.models.default_tier);
770 if (tier_it != config.models.tiers.end()) {
772 tier_it->second, config.models.default_tier, data_dir);
773 }
774
775 std::string prompt;
776 if (!constitution.empty()) { prompt += constitution + "\n\n"; }
777 if (!app_ctx.empty()) { prompt += app_ctx + "\n\n"; }
778 if (!identity_body.empty()) { prompt += identity_body; }
779
780 s_log->info("system prompt assembled: {} chars "
781 "(constitution={}, app_context={}, identity={})",
782 prompt.size(), !constitution.empty(),
783 !app_ctx.empty(), !identity_body.empty());
784 return prompt;
785}
786
787} // namespace entropic::prompts
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.
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:449
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 std::string check_app_context_source(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Check a configured app_context path loads.
Definition manager.cpp:645
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 std::string check_constitution_source(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Check a configured constitution path loads.
Definition manager.cpp:615
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 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.
Definition manager.cpp:488
ENTROPIC_EXPORT const char * prompt_type_to_string(PromptType type)
Convert PromptType to string.
Definition manager.cpp:92
AppContextState
Which app_context spelling a config is using (gh#163).
Definition manager.h:167
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:704
ENTROPIC_EXPORT const char * app_context_state_message(AppContextState state)
The log message describing one app_context state.
Definition manager.cpp:513
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 validate_configured_prompts(const entropic::ParsedConfig &config, const std::filesystem::path &data_dir)
Check that every CONFIGURED prompt source can actually be loaded.
Definition manager.cpp:678
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:740
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:546
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:761
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
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.
Definition manager.cpp:586
@ disabled
Named in disabled_mcpjson_servers — never connect.
@ optional
optional: every escape asks the host's path approver
std::string config
–config FILE
Definition serve.cpp:38
Full parsed configuration.
Definition config.h:1338
Inference parameters for a single identity phase.
Definition config.h:1456
int max_output_tokens
Max tokens per generation.
Definition config.h:1458
float repeat_penalty
Repetition penalty.
Definition config.h:1460
bool enable_thinking
Enable think-block output.
Definition config.h:1459
std::optional< std::vector< std::string > > bash_commands
Phase-specific bash commands.
Definition config.h:1461
float temperature
Sampling temperature.
Definition config.h:1457
Tier-specific model configuration.
Definition config.h:547
A single benchmark prompt with quality checks.
Definition manager.h:43
Identity frontmatter — full tier identity metadata.
Definition manager.h:65
std::vector< std::string > focus
Focus areas (min 1)
Definition manager.h:70
std::string name
Tier name (e.g., "lead")
Definition manager.h:69
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.