21#include <nlohmann/json.hpp>
45 const std::filesystem::path& repo_dir,
47 : run_child_fn_(run_child),
49 tier_res_(tier_resolution),
61 todo_callbacks_ = callbacks;
74 swap_dir_data_ = user_data;
100std::string DelegationManager::mint_delegation_id(
101 const std::string&
prefix) {
102 if (sandbox_mgr_ ==
nullptr) {
121std::filesystem::path DelegationManager::restore_root_for(
124 return std::filesystem::path(
parent_ctx.active_root);
143 delegation_cb_data_ = user_data;
164 const std::string& delegation_id,
165 const std::string& target_tier,
166 const std::string& task,
169 if (delegation_start_cb_ ==
nullptr) {
170 return ENT_DECISION_ACCEPT;
174 req.target_tier = target_tier.c_str();
175 req.task = task.c_str();
177 req.is_pipeline = is_pipeline ? 1 : 0;
184 return delegation_start_cb_(&
req, delegation_cb_data_);
186 logger->warn(
"delegation_start_cb threw for {}; treating as "
187 "REJECT (gh#29 exception shield)",
189 return ENT_DECISION_REJECT;
241void DelegationManager::deliver_sandbox_result(
244 const DelegationResult& result) {
246 if (delegation_complete_cb_ ==
nullptr) {
248 "no complete callback registered");
257 std::vector<const char*>
files_c;
266 invoke_complete_cb(
res,
sb_info.delegation_id);
267 if (
decision == ENT_DECISION_REJECT) {
269 "consumer REJECTED");
271 logger->info(
"Delegation {}: consumer ACCEPTED ({} files, "
293 return delegation_complete_cb_(&
res, delegation_cb_data_);
295 logger->warn(
"delegation_complete_cb threw for {}; treating as "
296 "REJECT (patch preserved to pending/)", delegation_id);
297 return ENT_DECISION_REJECT;
306void DelegationManager::persist_pending_patch(
309 const char* reason) {
313 logger->warn(
"Delegation {}: {}; patch saved to {} "
314 "({} files, {} bytes)",
315 sb_info.delegation_id, reason, path->string(),
348std::optional<DelegationResult>
349DelegationManager::check_delegation_preconditions(
350 const ChildContextInfo& info,
351 const std::string& target_tier,
352 const std::string& task,
353 const std::string&
del_id,
355 std::optional<SandboxInfo>&
sb_info) {
356 std::optional<DelegationResult>
early;
358 logger->error(
"Tier '{}' not found", target_tier);
359 early = DelegationResult{
360 "Unknown tier: " + target_tier,
false, target_tier, task};
361 }
else if (fire_start_cb(
del_id, target_tier, task, depth,
false)
362 == ENT_DECISION_REJECT) {
363 logger->info(
"Delegation {} ({}) rejected by start callback",
365 early = DelegationResult{
366 "Delegation rejected by consumer",
false, target_tier, task};
367 }
else if (sandbox_mgr_ !=
nullptr) {
379 "Delegation {} ({}): session sandbox unavailable",
381 early = DelegationResult{
382 "(DELEGATION FAILED: session sandbox unavailable)",
383 false, target_tier, task};
401 const std::string& target_tier,
402 const std::string& task,
403 std::optional<int> max_turns,
404 const std::vector<ContextRef>& context) {
406 logger->info(
"Delegation: target_tier='{}', task='{}', depth={}",
413 std::string
del_id = mint_delegation_id(
414 "d" + std::to_string(
parent_ctx.delegation_depth + 1));
415 std::optional<SandboxInfo>
sb_info;
416 if (
auto early = check_delegation_preconditions(
417 info, target_tier, task,
del_id,
427 const std::string
recorded = format_context_block(context) + task;
430 if (
sb_info && swap_dir_fn_ !=
nullptr) {
440 finalize_sandbox_for(
sb_info, result);
469LoopContext DelegationManager::build_resumed_child_context(
472 const std::string& target_tier,
473 const std::string& task,
479 child_ctx.delegation_ancestor_tiers.push_back(target_tier);
488 &&
child_ctx.messages.front().role ==
"system";
489 if (!
has_system && !info.system_prompt.empty()) {
492 sys.content = info.system_prompt;
499 if (!info.completion_instructions.empty()) {
500 user.content +=
"\n\n" + info.completion_instructions;
515 const std::string& target_tier,
516 const std::string& task,
518 std::optional<int> max_turns,
519 const std::vector<ContextRef>& context) {
521 logger->info(
"Resume delegation: target_tier='{}' task='{}' "
522 "history_messages={}",
528 std::string
del_id = mint_delegation_id(
529 "d" + std::to_string(
parent_ctx.delegation_depth + 1) +
"r");
530 std::optional<SandboxInfo>
sb_info;
531 if (
auto early = check_delegation_preconditions(
532 info, target_tier, task,
del_id,
537 const std::string
recorded = format_context_block(context) + task;
538 auto child_ctx = build_resumed_child_context(
543 if (
sb_info && swap_dir_fn_ !=
nullptr) {
552 finalize_sandbox_for(
sb_info, result);
581 const std::vector<std::string>& stages,
583 std::string
ctx =
"[PIPELINE CONTEXT]\n";
585 " of " + std::to_string(
total) +
"\n";
587 ctx +=
"Stay within your role. Do not perform work "
588 "outside your stage's responsibility.\n";
590 ctx +=
"\n[PRIOR STAGE OUTPUT]\n";
616 const std::vector<std::string>& stages,
617 const std::string& task,
618 std::vector<DelegationResult>&
stage_log,
619 const std::vector<ContextRef>& context) {
621 logger->info(
"Pipeline: {} stages, task='{}'", stages.size(), task);
624 auto first = stages.empty() ? std::string{} : stages.front();
625 if (fire_start_cb(
"pipeline",
first, task,
627 == ENT_DECISION_REJECT) {
628 logger->info(
"Pipeline rejected by start callback");
629 return {
"Pipeline rejected by consumer",
false,
first, task};
636 if (sandbox_mgr_ !=
nullptr) {
638 mint_delegation_id(
"pipeline"));
642 "Pipeline ({} stages): session sandbox unavailable",
644 return {
"(DELEGATION FAILED: session sandbox unavailable)",
650 last_result.
task = task;
652 for (
size_t i = 0;
i < stages.size(); ++
i) {
655 format_context_block(context) + task,
661 finalize_sandbox_for(
shared_sb, last_result);
684bool DelegationManager::run_pipeline_stage(
686 const std::vector<std::string>& stages,
688 const std::string& task,
689 const std::optional<SandboxInfo>&
shared_sb,
690 std::vector<DelegationResult>&
stage_log,
693 const auto& tier_name = stages[
stage_idx];
700 : ChildContextInfo{};
703 logger->error(
"Pipeline stage {}: tier '{}' not found",
706 last_result.
summary =
"Unknown tier: " + tier_name;
713 if (
shared_sb && swap_dir_fn_ !=
nullptr) {
715 ScopedSandbox
scope(swap_dir_fn_, swap_dir_data_,
726 logger->info(
"Pipeline stage {} ({}): {}",
stage_idx, tier_name,
727 last_result.
success ?
"complete" :
"failed");
742LoopContext DelegationManager::build_child_context(
744 const ChildContextInfo& info,
745 const std::string& task,
746 const std::vector<ContextRef>& context) {
765 child.delegation_ancestor_tiers =
parent_ctx.delegation_ancestor_tiers;
769 child.locked_tier = info.system_prompt.empty()
771 child.active_phase =
"default";
776 sys.content = info.system_prompt;
777 child.messages.push_back(std::move(
sys));
783 std::string
user_content = format_context_block(context) + task;
784 if (!info.completion_instructions.empty()) {
790 child.messages.push_back(std::move(
user));
802std::string DelegationManager::format_context_block(
803 const std::vector<ContextRef>& context) {
804 if (context.empty()) {
808 "[CONTEXT] The lead already located these files. Read them "
809 "directly instead of searching:\n";
810 for (
const auto&
ref : context) {
812 if (!
ref.lines.empty()) {
block +=
" (lines " +
ref.lines +
")"; }
813 if (!
ref.note.empty()) {
block +=
" — " +
ref.note; }
827std::string DelegationManager::extract_summary(
830 auto it =
child_ctx.metadata.find(
"explicit_completion_summary");
832 return mcp::sanitize_utf8(
it->second);
838 if (
rit->role ==
"assistant" && !
rit->content.empty()) {
839 return mcp::sanitize_utf8(
rit->content);
843 return "(No response from delegate)";
866std::string DelegationManager::create_storage_record(
867 LoopContext&
child_ctx,
const std::string& target_tier,
868 const std::string& task, std::optional<int> max_turns) {
874 ?
"root" :
child_ctx.locked_tier.c_str();
876 child_ctx.parent_conversation_id.c_str(),
877 src_tier, target_tier.c_str(), task.c_str(),
891void DelegationManager::complete_storage_record(
892 const std::string& delegation_id,
893 const DelegationResult& result) {
895 || delegation_id.empty()) {
898 const char* status = result.success ?
"completed" :
"failed";
900 delegation_id.c_str(), status,
901 result.summary.c_str(), storage_->
user_data);
917DelegationResult DelegationManager::run_child(
919 const std::string& target_tier,
920 const std::string& task,
921 std::optional<int> max_turns) {
925 if (todo_callbacks_.
save !=
nullptr) {
932 auto delegation_id = create_storage_record(
933 child_ctx, target_tier, task, max_turns);
942 if (max_turns.has_value() && *max_turns > 0) {
943 child_ctx.delegated_max_turns = *max_turns;
944 logger->info(
"Child loop bound requested by model: tier={} "
945 "max_turns={}", target_tier, *max_turns);
948 logger->info(
"Running child loop: tier={} depth={} msgs={} "
949 "system_hash={:016x}",
952 std::hash<std::string>{}(
957 if (run_child_fn_ !=
nullptr) {
958 run_child_fn_(
child_ctx, run_child_data_);
966 auto result = build_child_result(
968 complete_storage_record(delegation_id, result);
969 log_child_result(result);
994DelegationResult DelegationManager::build_child_result(
995 const std::string& target_tier,
996 const std::string& task,
998 DelegationResult result;
999 result.target_tier = target_tier;
1001 auto tr =
child_ctx.metadata.find(
"terminal_reason");
1003 result.terminal_reason =
tr->second;
1005 result.success = (
child_ctx.state == AgentState::COMPLETE
1006 && result.terminal_reason.empty());
1007 result.turns_used =
child_ctx.metrics.iterations;
1008 result.summary = extract_summary(
child_ctx);
1014 auto cg =
child_ctx.metadata.find(
"coverage_gap");
1015 if (
cg !=
child_ctx.metadata.end() &&
cg->second ==
"true") {
1016 result.coverage_gap =
true;
1017 auto gd =
child_ctx.metadata.find(
"gap_description");
1019 result.gap_description =
gd->second;
1021 auto sf =
child_ctx.metadata.find(
"suggested_files_json");
1023 auto parsed = nlohmann::json::parse(
1024 sf->second,
nullptr,
false);
1026 result.suggested_files =
1027 parsed.get<std::vector<std::string>>();
1031 result.child_messages = std::move(
child_ctx.messages);
1041void DelegationManager::log_child_result(
1042 const DelegationResult& result) {
1043 if (result.terminal_reason.empty()) {
1044 logger->info(
"Child loop done: tier={} success={} turns={}",
1045 result.target_tier, result.success,
1048 logger->warn(
"Child loop done: tier={} success=false "
1049 "turns={} reason={}",
1050 result.target_tier, result.turns_used,
1051 result.terminal_reason);
1070void DelegationManager::finalize_sandbox_for(
1071 const std::optional<SandboxInfo>&
sb_info,
1072 const DelegationResult& result) {
1073 if (!
sb_info || !sandbox_mgr_) {
1077 if (result.success) {
1082 logger->error(
"Delegation {}: finalize_sandbox failed; "
1083 "no patch delivered",
sb_info->delegation_id);
1086 logger->info(
"Delegation {} failed: discarding sandbox without "
1087 "generating patch",
sb_info->delegation_id);
void set_todo_callbacks(const TodoCallbacks &callbacks)
Set todo list save/restore callbacks.
void set_dir_swap(ScopedSandbox::SwapDirFn swap_fn, void *user_data)
Set directory swap callback for ScopedSandbox.
DelegationManager(RunChildLoopFn run_child, void *run_child_data, const TierResolutionInterface &tier_resolution, const std::filesystem::path &repo_dir={}, SandboxManager *sandbox_mgr=nullptr)
Construct with engine loop callback and tier resolution.
void set_storage(const struct StorageInterface *storage)
Set storage interface for delegation record persistence.
DelegationResult execute_resume_delegation(LoopContext &parent_ctx, const std::string &target_tier, const std::string &task, std::vector< Message > seed_history, std::optional< int > max_turns=std::nullopt, const std::vector< ContextRef > &context={})
Resume a prior delegation with pre-loaded conversation history.
DelegationResult execute_pipeline(LoopContext &parent_ctx, const std::vector< std::string > &stages, const std::string &task, std::vector< DelegationResult > &stage_log, const std::vector< ContextRef > &context={})
Run a multi-stage delegation pipeline sequentially.
void set_delegation_callbacks(ent_decision_t(*on_start)(const ent_delegation_request_t *, void *), ent_decision_t(*on_complete)(const ent_delegation_result_t *, void *), void *user_data)
Set delegation start/complete callbacks (gh#29, v2.1.5).
DelegationResult execute_delegation(LoopContext &parent_ctx, const std::string &target_tier, const std::string &task, std::optional< int > max_turns=std::nullopt, const std::vector< ContextRef > &context={})
Run a child inference loop for the target tier.
Create, finalize, and discard per-delegation filesystem sandboxes.
void discard_sandbox(const SandboxInfo &info)
Remove a sandbox directory.
std::string next_delegation_id(const std::string &prefix)
Mint a sandbox id that no sibling delegation can collide with.
std::optional< SandboxResult > finalize_sandbox(const SandboxInfo &info)
Produce the final patch artifact for a sandbox.
std::optional< SandboxInfo > create_sandbox(const std::string &delegation_id, std::optional< SandboxInfo > chain_from=std::nullopt)
Create a new delegation sandbox.
std::optional< std::filesystem::path > write_pending_patch(const std::string &delegation_id, const std::string &patch)
Write a patch to the session's pending/ directory.
RAII directory swapper for sandbox-scoped tool execution.
void(*)(const std::string &session_key, const std::filesystem::path &path, bool entering, void *user_data) SwapDirFn
Callback type for directory swapping.
DelegationManager — child loop creation and execution.
Types for the agentic loop engine.
ent_decision_t
Consumer decision returned from delegation callbacks.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
@ optional
optional: every escape asks the host's path approver
static std::string pipeline_context(size_t stage_idx, size_t total, const std::vector< std::string > &stages, const std::string &prior_output)
Build pipeline context prefix for a stage.
static ent_delegation_result_t build_delegation_result_struct(const SandboxInfo &sb_info, const SandboxResult &sandbox_result, const DelegationResult &result, const std::vector< const char * > &files_c, size_t files_len)
Deliver a finalized patch to consumer or pending/.
void(*)(LoopContext &ctx, void *user_data) RunChildLoopFn
Callback type for running a child engine loop.
Request describing a delegation that is about to run.
const char * delegation_id
Short id ("d1", "d2", "pipeline")
Result of a finalized delegation, delivered to the consumer.
const char * delegation_id
Short id (matches request)
Resolved tier information for building child delegation contexts.
Result returned from a child delegation loop.
bool success
Whether child reached COMPLETE via real entropic.complete.
std::string summary
Final summary from child.
std::string task
Original task text.
std::string target_tier
Tier that executed.
Mutable state carried through the agentic loop.
int delegation_depth
0 = root, 1+ = child
A message in a conversation.
std::string role
Message role.
Identifies one delegation's sandbox directory.
Final artifact emitted by a finalized sandbox.
Storage interface for conversation persistence.
bool(* create_delegation)(const char *parent_id, const char *delegating_tier, const char *target_tier, const char *task, int max_turns, std::string &delegation_id, std::string &child_conversation_id, void *user_data)
Create a delegation record with child conversation.
bool(* complete_delegation)(const char *delegation_id, const char *status, const char *summary, void *user_data)
Complete a delegation record.
void * user_data
Opaque pointer (storage backend)
Tier resolution callbacks for delegation and auto-chain.
void * user_data
Opaque pointer (facade context)
ChildContextInfo(* resolve_tier)(const std::string &tier_name, void *user_data)
Build context info for a child delegation to the given tier.
Callback type for saving/restoring todo list state.
std::string(* save)(void *user_data)
Save current todo list state. Returns opaque state string.
void(* restore)(const std::string &saved, void *user_data)
Restore a previously saved todo list state.
void(* install_fresh)(void *user_data)
Install a fresh empty todo list for child.
void * user_data
Opaque pointer (facade context)
UTF-8 validation + replacement at every system boundary where bytes change ownership.