Entropic 2.3.8
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
252static void extract_identity_flags(ryml::ConstNodeRef root,
254 extract(root, "max_output_tokens", fm.max_output_tokens);
255 extract(root, "temperature", fm.temperature);
256 extract(root, "repeat_penalty", fm.repeat_penalty);
257 extract(root, "enable_thinking", fm.enable_thinking);
258 extract(root, "interstitial", fm.interstitial);
259 extract(root, "routable", fm.routable);
260 extract(root, "explicit_completion", fm.explicit_completion);
261 extract(root, "relay_single_delegate", fm.relay_single_delegate);
262 // E6 (2.0.6-rc18): per-identity loop + tool-call caps
263 extract(root, "max_iterations", fm.max_iterations);
264 extract(root, "max_tool_calls_per_turn", fm.max_tool_calls_per_turn);
265 extract_string_list(root, "validation_rules", fm.validation_rules);
266}
267
274 ryml::ConstNodeRef root, IdentityFrontmatter& fm)
275{
276 fm.type = PromptType::IDENTITY;
277 extract(root, "version", fm.version);
278 extract(root, "name", fm.name);
279 extract_string_list(root, "focus", fm.focus);
280 extract_string_list(root, "examples", fm.examples);
281
282 std::string grammar_str;
283 if (extract(root, "grammar", grammar_str)) {
284 fm.grammar = grammar_str;
285 }
286 std::string auto_chain_str;
287 if (extract(root, "auto_chain", auto_chain_str)) {
288 fm.auto_chain = auto_chain_str;
289 }
290
291 extract_string_list_opt(root, "allowed_tools", fm.allowed_tools);
292 extract_string_list_opt(root, "bash_commands", fm.bash_commands);
293
294 extract_identity_flags(root, fm);
295
296 extract_phases(root, fm);
297 extract_benchmark(root, fm);
298}
299
308std::string load_identity(
309 const std::filesystem::path& path,
310 ParsedIdentity& identity)
311{
312 std::string err;
313
314 auto content = read_file(path);
315 if (content.empty()) {
316 err = "cannot read identity file: " + path.string();
317 }
318
319 ryml::Tree tree;
320 if (err.empty()) {
321 err = parse_frontmatter(content, path, tree, identity.body);
322 }
323
324 ryml::ConstNodeRef root;
325 if (err.empty()) {
326 root = tree.rootref();
327
328 // Validate type field
329 std::string type_str;
330 if (!extract(root, "type", type_str) || type_str != "identity") {
331 err = "prompt file " + path.string()
332 + " is not an identity file (type='"
333 + type_str + "')";
334 }
335 }
336
337 if (err.empty()) {
338 extract_identity_fields(root, identity.frontmatter);
339
340 if (identity.frontmatter.focus.empty()) {
341 err = "identity " + path.string()
342 + ": focus must have at least one entry";
343 } else if (identity.frontmatter.name.empty()) {
344 err = "identity " + path.string()
345 + ": name must not be empty";
346 }
347 }
348
349 return err;
350}
351
363 const std::optional<std::filesystem::path>& constitution_path,
364 bool disabled,
365 const std::filesystem::path& data_dir,
366 std::string& body)
367{
368 std::string err;
369
370 if (disabled) {
371 s_log->info("Constitution disabled by config");
372 body.clear();
373 } else {
374 std::filesystem::path path = constitution_path.has_value()
375 ? *constitution_path
376 : data_dir / "prompts" / "constitution.md";
377
378 if (!std::filesystem::exists(path)) {
379 err = "constitution file not found: " + path.string();
380 }
381
382 ParsedPrompt result;
383 if (err.empty()) {
384 err = parse_prompt_file(path, PromptType::CONSTITUTION, result);
385 }
386
387 if (err.empty()) {
388 body = std::move(result.body);
389 s_log->info("Constitution loaded from {}", path.string());
390 }
391 }
392
393 return err;
394}
395
407 const std::optional<std::filesystem::path>& app_context_path,
408 bool disabled,
409 const std::filesystem::path& data_dir,
410 std::string& body)
411{
412 std::string err;
413
414 if (disabled || !app_context_path.has_value()) {
415 s_log->info("App context disabled (not configured)");
416 body.clear();
417 } else {
418 auto path = *app_context_path;
419
420 // Bare filename resolves as bundled prompt
421 if (!path.has_parent_path() || path.parent_path().empty()) {
422 path = data_dir / "prompts" / path;
423 }
424
425 if (!std::filesystem::exists(path)) {
426 err = "app_context file not found: " + path.string();
427 }
428
429 ParsedPrompt result;
430 if (err.empty()) {
431 err = parse_prompt_file(path, PromptType::APP_CONTEXT, result);
432 }
433
434 if (err.empty()) {
435 body = std::move(result.body);
436 s_log->info("App context loaded from {}", path.string());
437 }
438 }
439
440 return err;
441}
442
459 const entropic::TierConfig& tier_config,
460 const std::string& tier_name,
461 const std::filesystem::path& data_dir)
462{
463 std::filesystem::path id_path;
464 if (tier_config.identity.has_value()) {
465 id_path = tier_config.identity.value();
466 } else if (!tier_config.identity_disabled) {
467 id_path = data_dir / "prompts"
468 / ("identity_" + tier_name + ".md");
469 }
471 if (id_path.empty() || !std::filesystem::exists(id_path)) {
472 return id;
473 }
474 auto err = load_identity(id_path, id);
475 if (!err.empty()) {
476 s_log->warn("identity load failed for tier '{}': {}",
477 tier_name, err);
478 return ParsedIdentity{};
479 }
480 s_log->info("identity loaded for tier '{}' from {}",
481 tier_name, id_path.string());
482 return id;
483}
484
495 const entropic::TierConfig& tier_config,
496 const std::string& tier_name,
497 const std::filesystem::path& data_dir)
498{
500 tier_config, tier_name, data_dir).body;
501}
502
515std::string assemble(
516 const entropic::ParsedConfig& config,
517 const std::filesystem::path& data_dir) {
518 std::string constitution, app_ctx;
519
521 data_dir, constitution);
523 data_dir, app_ctx);
524
525 std::string identity_body;
526 auto tier_it = config.models.tiers.find(config.models.default_tier);
527 if (tier_it != config.models.tiers.end()) {
528 identity_body = resolve_tier_identity(
529 tier_it->second, config.models.default_tier, data_dir);
530 }
531
532 std::string prompt;
533 if (!constitution.empty()) { prompt += constitution + "\n\n"; }
534 if (!app_ctx.empty()) { prompt += app_ctx + "\n\n"; }
535 if (!identity_body.empty()) { prompt += identity_body; }
536
537 s_log->info("system prompt assembled: {} chars "
538 "(constitution={}, app_context={}, identity={})",
539 prompt.size(), !constitution.empty(),
540 !app_ctx.empty(), !identity_body.empty());
541 return prompt;
542}
543
544} // 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 identity-specific fields from a pre-parsed ryml tree.
Definition manager.cpp:252
static void extract_identity_fields(ryml::ConstNodeRef root, IdentityFrontmatter &fm)
Extract all identity frontmatter fields into the struct.
Definition manager.cpp:273
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 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:406
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:458
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:362
ENTROPIC_EXPORT std::string load_identity(const std::filesystem::path &path, ParsedIdentity &identity)
Load an identity file: parse frontmatter + body.
Definition manager.cpp:308
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:494
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:515
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:357
std::string default_tier
Default tier name.
Definition config.h:359
Full parsed configuration.
Definition config.h:714
std::optional< std::filesystem::path > app_context
App context: nullopt = disabled by default.
Definition config.h:731
ModelsConfig models
Tiers + router.
Definition config.h:715
bool app_context_disabled
true if app_context explicitly disabled
Definition config.h:732
std::optional< std::filesystem::path > constitution
Constitution: nullopt = bundled default, disabled = explicit false.
Definition config.h:727
bool constitution_disabled
true if constitution explicitly disabled
Definition config.h:728
Inference parameters for a single identity phase.
Definition config.h:772
int max_output_tokens
Max tokens per generation.
Definition config.h:774
float repeat_penalty
Repetition penalty.
Definition config.h:776
bool enable_thinking
Enable think-block output.
Definition config.h:775
std::optional< std::vector< std::string > > bash_commands
Phase-specific bash commands.
Definition config.h:777
float temperature
Sampling temperature.
Definition config.h:773
Tier-specific model configuration.
Definition config.h:286
std::optional< std::filesystem::path > identity
Identity prompt path (nullopt = bundled)
Definition config.h:287
bool identity_disabled
true if identity explicitly disabled
Definition config.h:288
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:83
std::optional< std::vector< std::string > > allowed_tools
Tool filter.
Definition manager.h:74
std::optional< std::string > auto_chain
Auto-chain target tier.
Definition manager.h:73
int max_output_tokens
Default max output tokens.
Definition manager.h:76
int max_iterations
Per-identity loop iteration cap; -1 = use global (E6)
Definition manager.h:85
std::vector< std::string > focus
Focus areas (min 1)
Definition manager.h:70
PromptType type
Always IDENTITY.
Definition manager.h:66
bool explicit_completion
Requires explicit completion.
Definition manager.h:82
std::string name
Tier name (e.g., "lead")
Definition manager.h:69
bool routable
Visible to router.
Definition manager.h:81
bool relay_single_delegate
Skip re-synthesis when single delegate returns (v2.0.11)
Definition manager.h:84
bool enable_thinking
Default thinking mode.
Definition manager.h:79
float repeat_penalty
Default repetition penalty.
Definition manager.h:78
std::optional< std::vector< std::string > > bash_commands
Allowed bash commands.
Definition manager.h:75
int max_tool_calls_per_turn
Per-identity tool call cap; -1 = use global (E6)
Definition manager.h:86
bool interstitial
Interstitial role.
Definition manager.h:80
float temperature
Default temperature.
Definition manager.h:77
std::optional< std::unordered_map< std::string, PhaseConfig > > phases
Named phases.
Definition manager.h:87
std::vector< std::string > examples
Few-shot examples.
Definition manager.h:71
std::optional< BenchmarkSpec > benchmark
Benchmark definition.
Definition manager.h:88
std::optional< std::string > grammar
Grammar file reference.
Definition manager.h:72
Parsed identity file: frontmatter + body.
Definition manager.h:105
IdentityFrontmatter frontmatter
Full identity metadata.
Definition manager.h:106
std::string body
Markdown system prompt body.
Definition manager.h:107
Parsed prompt file result: type + version + body.
Definition manager.h:95
PromptType type
Prompt type.
Definition manager.h:96
std::string body
Markdown body after frontmatter.
Definition manager.h:98
int version
Schema version.
Definition manager.h:97
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.