20#include <nlohmann/json.hpp>
44 const std::filesystem::path& repo_dir,
46 : run_child_fn_(run_child),
47 run_child_data_(run_child_data),
48 tier_res_(tier_resolution),
49 sandbox_mgr_(sandbox_mgr),
60 todo_callbacks_ = callbacks;
72 swap_dir_fn_ = swap_fn;
73 swap_dir_data_ = user_data;
98 delegation_start_cb_ = on_start;
99 delegation_complete_cb_ = on_complete;
100 delegation_cb_data_ = user_data;
120 const std::string& delegation_id,
121 const std::string& target_tier,
122 const std::string& task,
125 if (delegation_start_cb_ ==
nullptr) {
126 return ENT_DECISION_ACCEPT;
130 req.target_tier = target_tier.c_str();
131 req.task = task.c_str();
133 req.is_pipeline = is_pipeline ? 1 : 0;
140 return delegation_start_cb_(&req, delegation_cb_data_);
142 logger->warn(
"delegation_start_cb threw for {}; treating as "
143 "REJECT (gh#29 exception shield)",
145 return ENT_DECISION_REJECT;
179 const std::vector<const char*>& files_c,
size_t files_len) {
183 res.success = result.
success ? 1 : 0;
184 res.summary = result.
summary.c_str();
185 res.patch = sandbox_result.
patch.c_str();
186 res.patch_len = sandbox_result.
patch.size();
187 res.files_touched = files_c.data();
188 res.files_touched_len = files_len;
197void DelegationManager::deliver_sandbox_result(
198 const SandboxInfo& sb_info,
199 const SandboxResult& sandbox_result,
200 const DelegationResult& result) {
202 if (delegation_complete_cb_ ==
nullptr) {
203 persist_pending_patch(sb_info, sandbox_result,
204 "no complete callback registered");
208 std::vector<std::string> files_owned;
209 files_owned.reserve(sandbox_result.files_touched.size());
210 for (
const auto& p : sandbox_result.files_touched) {
211 files_owned.push_back(p.string());
213 std::vector<const char*> files_c;
214 files_c.reserve(files_owned.size() + 1);
215 for (
const auto& s : files_owned) { files_c.push_back(s.c_str()); }
216 files_c.push_back(
nullptr);
219 sb_info, sandbox_result, result, files_c, files_owned.size());
222 invoke_complete_cb(res, sb_info.delegation_id);
223 if (decision == ENT_DECISION_REJECT) {
224 persist_pending_patch(sb_info, sandbox_result,
225 "consumer REJECTED");
227 logger->info(
"Delegation {}: consumer ACCEPTED ({} files, "
229 sb_info.delegation_id,
230 sandbox_result.files_touched.size(),
231 sandbox_result.patch.size());
249 return delegation_complete_cb_(&res, delegation_cb_data_);
251 logger->warn(
"delegation_complete_cb threw for {}; treating as "
252 "REJECT (patch preserved to pending/)", delegation_id);
253 return ENT_DECISION_REJECT;
262void DelegationManager::persist_pending_patch(
263 const SandboxInfo& sb_info,
264 const SandboxResult& sandbox_result,
265 const char* reason) {
267 sb_info.delegation_id, sandbox_result.patch);
269 logger->warn(
"Delegation {}: {}; patch saved to {} "
270 "({} files, {} bytes)",
271 sb_info.delegation_id, reason, path->string(),
272 sandbox_result.files_touched.size(),
273 sandbox_result.patch.size());
302std::optional<DelegationResult>
303DelegationManager::check_delegation_preconditions(
304 const ChildContextInfo& info,
305 const std::string& target_tier,
306 const std::string& task,
307 const std::string& del_id,
309 std::optional<SandboxInfo>& sb_info) {
310 std::optional<DelegationResult> early;
312 logger->error(
"Tier '{}' not found", target_tier);
313 early = DelegationResult{
314 "Unknown tier: " + target_tier,
false, target_tier, task};
315 }
else if (fire_start_cb(del_id, target_tier, task, depth,
false)
316 == ENT_DECISION_REJECT) {
317 logger->info(
"Delegation {} ({}) rejected by start callback",
318 del_id, target_tier);
319 early = DelegationResult{
320 "Delegation rejected by consumer",
false, target_tier, task};
321 }
else if (sandbox_mgr_ !=
nullptr) {
323 if (!sb_info.has_value()) {
330 "Delegation {} ({}): session sandbox unavailable",
331 del_id, target_tier);
332 early = DelegationResult{
333 "(DELEGATION FAILED: session sandbox unavailable)",
334 false, target_tier, task};
352 const std::string& target_tier,
353 const std::string& task,
354 std::optional<int> max_turns) {
356 logger->info(
"Delegation: target_tier='{}', task='{}', depth={}",
365 std::optional<SandboxInfo> sb_info;
366 if (
auto early = check_delegation_preconditions(
367 info, target_tier, task, del_id,
372 auto child_ctx = build_child_context(parent_ctx, info, task);
373 child_ctx.locked_tier = target_tier;
377 if (sb_info && swap_dir_fn_ !=
nullptr) {
379 sb_info->path, repo_dir_);
380 result = run_child(child_ctx, target_tier, task, max_turns);
382 result = run_child(child_ctx, target_tier, task, max_turns);
385 finalize_sandbox_for(sb_info, result);
412LoopContext DelegationManager::build_resumed_child_context(
415 const std::string& target_tier,
416 const std::string& task,
417 std::vector<Message> seed_history) {
427 child_ctx.
messages = std::move(seed_history);
428 bool has_system = !child_ctx.
messages.empty()
429 && child_ctx.
messages.front().role ==
"system";
430 if (!has_system && !info.system_prompt.empty()) {
433 sys.
content = info.system_prompt;
440 if (!info.completion_instructions.empty()) {
441 user.content +=
"\n\n" + info.completion_instructions;
443 child_ctx.
messages.push_back(std::move(user));
454 const std::string& target_tier,
455 const std::string& task,
456 std::vector<Message> seed_history,
457 std::optional<int> max_turns) {
459 logger->info(
"Resume delegation: target_tier='{}' task='{}' "
460 "history_messages={}",
461 target_tier, task, seed_history.size());
468 std::optional<SandboxInfo> sb_info;
469 if (
auto early = check_delegation_preconditions(
470 info, target_tier, task, del_id,
475 auto child_ctx = build_resumed_child_context(
476 parent_ctx, info, target_tier, task, std::move(seed_history));
479 if (sb_info && swap_dir_fn_ !=
nullptr) {
481 sb_info->path, repo_dir_);
482 result = run_child(child_ctx, target_tier, task, max_turns);
484 result = run_child(child_ctx, target_tier, task, max_turns);
486 finalize_sandbox_for(sb_info, result);
514 size_t stage_idx,
size_t total,
515 const std::vector<std::string>& stages,
516 const std::string& prior_output) {
517 std::string ctx =
"[PIPELINE CONTEXT]\n";
518 ctx +=
"Stage " + std::to_string(stage_idx + 1) +
519 " of " + std::to_string(total) +
"\n";
520 ctx +=
"Role: " + stages[stage_idx] +
"\n";
521 ctx +=
"Stay within your role. Do not perform work "
522 "outside your stage's responsibility.\n";
523 if (!prior_output.empty()) {
524 ctx +=
"\n[PRIOR STAGE OUTPUT]\n";
549 const std::vector<std::string>& stages,
550 const std::string& task) {
552 logger->info(
"Pipeline: {} stages, task='{}'", stages.size(), task);
555 auto first = stages.empty() ? std::string{} : stages.front();
556 if (fire_start_cb(
"pipeline", first, task,
558 == ENT_DECISION_REJECT) {
559 logger->info(
"Pipeline rejected by start callback");
560 return {
"Pipeline rejected by consumer",
false, first, task};
566 std::optional<SandboxInfo> shared_sb;
567 if (sandbox_mgr_ !=
nullptr) {
569 if (!shared_sb.has_value()) {
572 "Pipeline ({} stages): session sandbox unavailable",
574 return {
"(DELEGATION FAILED: session sandbox unavailable)",
580 last_result.
task = task;
582 for (
size_t i = 0; i < stages.size(); ++i) {
583 if (!run_pipeline_stage(parent_ctx, stages, i, task,
584 shared_sb, last_result)) {
589 finalize_sandbox_for(shared_sb, last_result);
611bool DelegationManager::run_pipeline_stage(
613 const std::vector<std::string>& stages,
615 const std::string& task,
616 const std::optional<SandboxInfo>& shared_sb,
619 const auto& tier_name = stages[stage_idx];
620 std::string prior = (stage_idx == 0) ? std::string{} : last_result.
summary;
621 std::string stage_task =
626 : ChildContextInfo{};
629 logger->error(
"Pipeline stage {}: tier '{}' not found",
630 stage_idx, tier_name);
632 last_result.
summary =
"Unknown tier: " + tier_name;
636 auto child_ctx = build_child_context(parent_ctx, info, stage_task);
639 if (shared_sb && swap_dir_fn_ !=
nullptr) {
640 ScopedSandbox scope(swap_dir_fn_, swap_dir_data_,
641 shared_sb->path, repo_dir_);
642 last_result = run_child(child_ctx, tier_name, stage_task,
645 last_result = run_child(child_ctx, tier_name, stage_task,
649 logger->info(
"Pipeline stage {} ({}): {}", stage_idx, tier_name,
650 last_result.
success ?
"complete" :
"failed");
665LoopContext DelegationManager::build_child_context(
666 const LoopContext& parent_ctx,
667 const ChildContextInfo& info,
668 const std::string& task) {
671 child.delegation_depth = parent_ctx.delegation_depth + 1;
674 child.delegation_ancestor_tiers = parent_ctx.delegation_ancestor_tiers;
675 if (!parent_ctx.locked_tier.empty()) {
676 child.delegation_ancestor_tiers.push_back(parent_ctx.locked_tier);
678 child.locked_tier = info.system_prompt.empty()
679 ? parent_ctx.locked_tier :
"";
680 child.all_tools = info.tools;
681 child.active_phase =
"default";
686 sys.content = info.system_prompt;
687 child.messages.push_back(std::move(sys));
690 std::string user_content = task;
691 if (!info.completion_instructions.empty()) {
692 user_content +=
"\n\n" + info.completion_instructions;
696 user.content = std::move(user_content);
697 child.messages.push_back(std::move(user));
709std::string DelegationManager::extract_summary(
710 const LoopContext& child_ctx)
const {
712 auto it = child_ctx.metadata.find(
"explicit_completion_summary");
713 if (it != child_ctx.metadata.end() && !it->second.empty()) {
718 for (
auto rit = child_ctx.messages.rbegin();
719 rit != child_ctx.messages.rend(); ++rit) {
720 if (rit->role ==
"assistant" && !rit->content.empty()) {
725 return "(No response from delegate)";
748std::string DelegationManager::create_storage_record(
749 LoopContext& child_ctx,
const std::string& target_tier,
750 const std::string& task, std::optional<int> max_turns) {
754 std::string del_id, child_conv_id;
755 auto src_tier = child_ctx.locked_tier.empty()
756 ?
"root" : child_ctx.locked_tier.c_str();
758 child_ctx.parent_conversation_id.c_str(),
759 src_tier, target_tier.c_str(), task.c_str(),
760 max_turns.value_or(0), del_id, child_conv_id,
762 child_ctx.conversation_id = child_conv_id;
773void DelegationManager::complete_storage_record(
774 const std::string& delegation_id,
775 const DelegationResult& result) {
777 || delegation_id.empty()) {
780 const char* status = result.success ?
"completed" :
"failed";
782 delegation_id.c_str(), status,
783 result.summary.c_str(), storage_->
user_data);
796DelegationResult DelegationManager::run_child(
797 LoopContext& child_ctx,
798 const std::string& target_tier,
799 const std::string& task,
800 std::optional<int> max_turns) {
803 std::string saved_todo;
804 if (todo_callbacks_.
save !=
nullptr) {
805 saved_todo = todo_callbacks_.
save(todo_callbacks_.
user_data);
811 auto delegation_id = create_storage_record(
812 child_ctx, target_tier, task, max_turns);
814 logger->info(
"Running child loop: tier={} depth={} msgs={} "
815 "system_hash={:016x}",
816 target_tier, child_ctx.delegation_depth,
817 child_ctx.messages.size(),
818 std::hash<std::string>{}(
819 child_ctx.messages.empty()
821 : child_ctx.messages[0].content));
823 if (run_child_fn_ !=
nullptr) {
824 run_child_fn_(child_ctx, run_child_data_);
828 if (todo_callbacks_.
restore !=
nullptr && !saved_todo.empty()) {
832 auto result = build_child_result(
833 target_tier, task, child_ctx);
834 complete_storage_record(delegation_id, result);
835 log_child_result(result);
859DelegationResult DelegationManager::build_child_result(
860 const std::string& target_tier,
861 const std::string& task,
862 LoopContext& child_ctx) {
863 DelegationResult result;
864 result.target_tier = target_tier;
866 auto tr = child_ctx.metadata.find(
"terminal_reason");
867 if (tr != child_ctx.metadata.end()) {
868 result.terminal_reason = tr->second;
870 result.success = (child_ctx.state == AgentState::COMPLETE
871 && result.terminal_reason.empty());
872 result.turns_used = child_ctx.metrics.iterations;
873 result.summary = extract_summary(child_ctx);
879 auto cg = child_ctx.metadata.find(
"coverage_gap");
880 if (cg != child_ctx.metadata.end() && cg->second ==
"true") {
881 result.coverage_gap =
true;
882 auto gd = child_ctx.metadata.find(
"gap_description");
883 if (gd != child_ctx.metadata.end()) {
884 result.gap_description = gd->second;
886 auto sf = child_ctx.metadata.find(
"suggested_files_json");
887 if (sf != child_ctx.metadata.end()) {
888 auto parsed = nlohmann::json::parse(
889 sf->second,
nullptr,
false);
890 if (parsed.is_array()) {
891 result.suggested_files =
892 parsed.get<std::vector<std::string>>();
896 result.child_messages = std::move(child_ctx.messages);
906void DelegationManager::log_child_result(
907 const DelegationResult& result) {
908 if (result.terminal_reason.empty()) {
909 logger->info(
"Child loop done: tier={} success={} turns={}",
910 result.target_tier, result.success,
913 logger->warn(
"Child loop done: tier={} success=false "
914 "turns={} reason={}",
915 result.target_tier, result.turns_used,
916 result.terminal_reason);
935void DelegationManager::finalize_sandbox_for(
936 const std::optional<SandboxInfo>& sb_info,
937 const DelegationResult& result) {
938 if (!sb_info || !sandbox_mgr_) {
942 if (result.success) {
945 deliver_sandbox_result(*sb_info, *patch_result, result);
947 logger->error(
"Delegation {}: finalize_sandbox failed; "
948 "no patch delivered", sb_info->delegation_id);
951 logger->info(
"Delegation {} failed: discarding sandbox without "
952 "generating patch", sb_info->delegation_id);
DelegationResult execute_delegation(LoopContext &parent_ctx, const std::string &target_tier, const std::string &task, std::optional< int > max_turns=std::nullopt)
Run a child inference loop for the target tier.
void set_todo_callbacks(const TodoCallbacks &callbacks)
Set todo list save/restore callbacks.
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)
Resume a prior delegation with pre-loaded conversation history.
void set_dir_swap(ScopedSandbox::SwapDirFn swap_fn, void *user_data)
Set directory swap callback for ScopedSandbox.
DelegationResult execute_pipeline(LoopContext &parent_ctx, const std::vector< std::string > &stages, const std::string &task)
Run a multi-stage delegation pipeline sequentially.
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.
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).
Create, finalize, and discard per-delegation filesystem sandboxes.
void discard_sandbox(const SandboxInfo &info)
Remove a sandbox directory.
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::filesystem::path &path, 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).
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.
std::vector< std::string > delegation_ancestor_tiers
Tier stack from root to this loop (P1-9, 2.0.6-rc16)
std::string active_phase
Active inference phase.
std::string conversation_id
Conversation ID for storage (v1.8.8)
int delegation_depth
0 = root, 1+ = child
std::vector< Message > messages
Conversation history.
std::string locked_tier
Tier locked for this loop ("" = none)
std::string parent_conversation_id
Parent conv ID (delegation)
std::vector< std::string > all_tools
Full tool list as raw JSON strings.
A message in a conversation.
std::string content
Message text content (always populated)
std::string role
Message role.
Identifies one delegation's sandbox directory.
std::string delegation_id
Short delegation id (e.g. "d1", "pipeline")
Final artifact emitted by a finalized sandbox.
std::string patch
Unified diff text.
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)