Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
delegation.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
20
21#include <nlohmann/json.hpp>
22
23#include <algorithm>
24
25static auto logger = entropic::log::get("core.delegation");
26
27namespace entropic {
28
29// ── Construction ─────────────────────────────────────────
30
42 RunChildLoopFn run_child,
43 void* run_child_data,
44 const TierResolutionInterface& tier_resolution,
45 const std::filesystem::path& repo_dir,
47 : run_child_fn_(run_child),
48 run_child_data_(run_child_data),
49 tier_res_(tier_resolution),
50 sandbox_mgr_(sandbox_mgr),
51 repo_dir_(repo_dir) {
52}
53
61 todo_callbacks_ = callbacks;
62}
63
72 ScopedSandbox::SwapDirFn swap_fn, void* user_data) {
73 swap_dir_fn_ = swap_fn;
74 swap_dir_data_ = user_data;
75}
76
84 storage_ = storage;
85}
86
100std::string DelegationManager::mint_delegation_id(
101 const std::string& prefix) {
102 if (sandbox_mgr_ == nullptr) {
103 return prefix;
104 }
105 return sandbox_mgr_->next_delegation_id(prefix);
106}
107
121std::filesystem::path DelegationManager::restore_root_for(
122 const LoopContext& parent_ctx) const {
123 if (!parent_ctx.active_root.empty()) {
124 return std::filesystem::path(parent_ctx.active_root);
125 }
126 return repo_dir_;
127}
128
140 void* user_data) {
141 delegation_start_cb_ = on_start;
142 delegation_complete_cb_ = on_complete;
143 delegation_cb_data_ = user_data;
144}
145
163ent_decision_t DelegationManager::fire_start_cb(
164 const std::string& delegation_id,
165 const std::string& target_tier,
166 const std::string& task,
167 int depth,
168 bool is_pipeline) {
169 if (delegation_start_cb_ == nullptr) {
170 return ENT_DECISION_ACCEPT;
171 }
173 req.delegation_id = delegation_id.c_str();
174 req.target_tier = target_tier.c_str();
175 req.task = task.c_str();
176 req.depth = depth;
177 req.is_pipeline = is_pipeline ? 1 : 0;
178 // Exception shield: per docs/architecture-cpp.md design rule #6,
179 // exceptions do not cross .so boundaries. A buggy consumer
180 // throwing a C++ exception out of the callback would otherwise
181 // unwind through the engine's stack with undefined cleanup. Fail
182 // safe by treating a throw as REJECT (gh#29 hardening).
183 try {
184 return delegation_start_cb_(&req, delegation_cb_data_);
185 } catch (...) {
186 logger->warn("delegation_start_cb threw for {}; treating as "
187 "REJECT (gh#29 exception shield)",
188 delegation_id);
189 return ENT_DECISION_REJECT;
190 }
191}
192
222 const DelegationResult& result,
223 const std::vector<const char*>& files_c, size_t files_len) {
225 res.delegation_id = sb_info.delegation_id.c_str();
226 res.target_tier = result.target_tier.c_str();
227 res.success = result.success ? 1 : 0;
228 res.summary = result.summary.c_str();
229 res.patch = sandbox_result.patch.c_str();
230 res.patch_len = sandbox_result.patch.size();
231 res.files_touched = files_c.data();
232 res.files_touched_len = files_len;
233 return res;
234}
235
241void DelegationManager::deliver_sandbox_result(
242 const SandboxInfo& sb_info,
243 const SandboxResult& sandbox_result,
244 const DelegationResult& result) {
245
246 if (delegation_complete_cb_ == nullptr) {
247 persist_pending_patch(sb_info, sandbox_result,
248 "no complete callback registered");
249 return;
250 }
251
252 std::vector<std::string> files_owned;
253 files_owned.reserve(sandbox_result.files_touched.size());
254 for (const auto& p : sandbox_result.files_touched) {
255 files_owned.push_back(p.string());
256 }
257 std::vector<const char*> files_c;
258 files_c.reserve(files_owned.size() + 1);
259 for (const auto& s : files_owned) { files_c.push_back(s.c_str()); }
260 files_c.push_back(nullptr);
261
263 sb_info, sandbox_result, result, files_c, files_owned.size());
264
266 invoke_complete_cb(res, sb_info.delegation_id);
267 if (decision == ENT_DECISION_REJECT) {
268 persist_pending_patch(sb_info, sandbox_result,
269 "consumer REJECTED");
270 } else {
271 logger->info("Delegation {}: consumer ACCEPTED ({} files, "
272 "{} bytes)",
273 sb_info.delegation_id,
274 sandbox_result.files_touched.size(),
275 sandbox_result.patch.size());
276 }
277}
278
287ent_decision_t DelegationManager::invoke_complete_cb(
288 const ent_delegation_result_t& res, const std::string& delegation_id) {
289 // Exception shield: a buggy consumer must never unwind through the
290 // engine. Treat throw as REJECT so the patch is preserved on disk
291 // for inspection (gh#29 hardening — same policy as fire_start_cb).
292 try {
293 return delegation_complete_cb_(&res, delegation_cb_data_);
294 } catch (...) {
295 logger->warn("delegation_complete_cb threw for {}; treating as "
296 "REJECT (patch preserved to pending/)", delegation_id);
297 return ENT_DECISION_REJECT;
298 }
299}
300
306void DelegationManager::persist_pending_patch(
307 const SandboxInfo& sb_info,
308 const SandboxResult& sandbox_result,
309 const char* reason) {
310 auto path = sandbox_mgr_->write_pending_patch(
311 sb_info.delegation_id, sandbox_result.patch);
312 if (path) {
313 logger->warn("Delegation {}: {}; patch saved to {} "
314 "({} files, {} bytes)",
315 sb_info.delegation_id, reason, path->string(),
316 sandbox_result.files_touched.size(),
317 sandbox_result.patch.size());
318 }
319}
320
321// ── Single delegation ────────────────────────────────────
322
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,
354 int depth,
355 std::optional<SandboxInfo>& sb_info) {
356 std::optional<DelegationResult> early;
357 if (!info.valid) {
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",
364 del_id, target_tier);
365 early = DelegationResult{
366 "Delegation rejected by consumer", false, target_tier, task};
367 } else if (sandbox_mgr_ != nullptr) {
368 sb_info = sandbox_mgr_->create_sandbox(del_id);
369 // gh#160: `del_id` is already unique per sibling (see
370 // `mint_delegation_id`); the sandbox dir and the pending patch
371 // therefore no longer collide between two delegations at one depth.
372 if (!sb_info.has_value()) {
373 // gh#33 bug 2 (v2.1.6): pre-2.1.6 a failed create_sandbox
374 // silently fell through to running the child against the
375 // parent's cwd and surfaced an opaque
376 // "(No response from delegate)" on later failure. The
377 // sandbox-unavailable mode is now consumer-visible.
378 logger->error(
379 "Delegation {} ({}): session sandbox unavailable",
380 del_id, target_tier);
381 early = DelegationResult{
382 "(DELEGATION FAILED: session sandbox unavailable)",
383 false, target_tier, task};
384 }
385 }
386 return early;
387}
388
401 const std::string& target_tier,
402 const std::string& task,
403 std::optional<int> max_turns,
404 const std::vector<ContextRef>& context) {
405
406 logger->info("Delegation: target_tier='{}', task='{}', depth={}",
407 target_tier, task,
408 parent_ctx.delegation_depth + 1);
409 auto info = tier_res_.resolve_tier
410 ? tier_res_.resolve_tier(target_tier, tier_res_.user_data)
412
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,
418 parent_ctx.delegation_depth + 1, sb_info)) {
419 return *early;
420 }
421
422 auto child_ctx = build_child_context(parent_ctx, info, task, context);
423 child_ctx.locked_tier = target_tier;
424
425 // gh#162: the storage record keeps what the child was actually given,
426 // so "what did this child know" is answerable after the fact.
427 const std::string recorded = format_context_block(context) + task;
428 DelegationResult result;
429
430 if (sb_info && swap_dir_fn_ != nullptr) {
431 child_ctx.active_root = sb_info->path.string();
432 ScopedSandbox scope(swap_dir_fn_, swap_dir_data_,
433 parent_ctx.session_key, sb_info->path,
434 restore_root_for(parent_ctx));
435 result = run_child(child_ctx, target_tier, recorded, max_turns);
436 } else {
437 result = run_child(child_ctx, target_tier, recorded, max_turns);
438 }
439
440 finalize_sandbox_for(sb_info, result);
441 return result;
442}
443
469LoopContext DelegationManager::build_resumed_child_context(
470 const LoopContext& parent_ctx,
471 const ChildContextInfo& info,
472 const std::string& target_tier,
473 const std::string& task,
474 std::vector<Message> seed_history) {
476 child_ctx.delegation_depth = parent_ctx.delegation_depth + 1;
477 child_ctx.delegation_ancestor_tiers =
478 parent_ctx.delegation_ancestor_tiers;
479 child_ctx.delegation_ancestor_tiers.push_back(target_tier);
480 child_ctx.parent_conversation_id = parent_ctx.conversation_id;
481 // gh#158: see build_child_context — both builders inherit the key, or
482 // the resumed path reintroduces the shared-bucket collision on its own.
483 child_ctx.session_key = parent_ctx.session_key;
484 child_ctx.active_phase = "default";
485 child_ctx.locked_tier = target_tier;
486 child_ctx.messages = std::move(seed_history);
487 bool has_system = !child_ctx.messages.empty()
488 && child_ctx.messages.front().role == "system";
489 if (!has_system && !info.system_prompt.empty()) {
490 Message sys;
491 sys.role = "system";
492 sys.content = info.system_prompt;
493 child_ctx.messages.insert(child_ctx.messages.begin(),
494 std::move(sys));
495 }
496 Message user;
497 user.role = "user";
498 user.content = task;
499 if (!info.completion_instructions.empty()) {
500 user.content += "\n\n" + info.completion_instructions;
501 }
502 child_ctx.messages.push_back(std::move(user));
503 return child_ctx;
504}
505
515 const std::string& target_tier,
516 const std::string& task,
517 std::vector<Message> seed_history,
518 std::optional<int> max_turns,
519 const std::vector<ContextRef>& context) {
520
521 logger->info("Resume delegation: target_tier='{}' task='{}' "
522 "history_messages={}",
523 target_tier, task, seed_history.size());
524 auto info = tier_res_.resolve_tier
525 ? tier_res_.resolve_tier(target_tier, tier_res_.user_data)
527
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,
533 parent_ctx.delegation_depth + 1, sb_info)) {
534 return *early;
535 }
536
537 const std::string recorded = format_context_block(context) + task;
538 auto child_ctx = build_resumed_child_context(
539 parent_ctx, info, target_tier, recorded, // gh#162
540 std::move(seed_history));
541
542 DelegationResult result;
543 if (sb_info && swap_dir_fn_ != nullptr) {
544 child_ctx.active_root = sb_info->path.string();
545 ScopedSandbox scope(swap_dir_fn_, swap_dir_data_,
546 parent_ctx.session_key, sb_info->path,
547 restore_root_for(parent_ctx));
548 result = run_child(child_ctx, target_tier, recorded, max_turns);
549 } else {
550 result = run_child(child_ctx, target_tier, recorded, max_turns);
551 }
552 finalize_sandbox_for(sb_info, result);
553 return result;
554}
555
556// ── Pipeline ─────────────────────────────────────────────
557
579static std::string pipeline_context(
580 size_t stage_idx, size_t total,
581 const std::vector<std::string>& stages,
582 const std::string& prior_output) {
583 std::string ctx = "[PIPELINE CONTEXT]\n";
584 ctx += "Stage " + std::to_string(stage_idx + 1) +
585 " of " + std::to_string(total) + "\n";
586 ctx += "Role: " + stages[stage_idx] + "\n";
587 ctx += "Stay within your role. Do not perform work "
588 "outside your stage's responsibility.\n";
589 if (!prior_output.empty()) {
590 ctx += "\n[PRIOR STAGE OUTPUT]\n";
591 ctx += prior_output;
592 ctx += "\n";
593 }
594 ctx += "\n";
595 return ctx;
596}
597
616 const std::vector<std::string>& stages,
617 const std::string& task,
618 std::vector<DelegationResult>& stage_log,
619 const std::vector<ContextRef>& context) {
620
621 logger->info("Pipeline: {} stages, task='{}'", stages.size(), task);
622
623 // gh#29 (v2.1.5): single pre-flight gate for the whole pipeline.
624 auto first = stages.empty() ? std::string{} : stages.front();
625 if (fire_start_cb("pipeline", first, task,
626 parent_ctx.delegation_depth + 1, true)
627 == ENT_DECISION_REJECT) {
628 logger->info("Pipeline rejected by start callback");
629 return {"Pipeline rejected by consumer", false, first, task};
630 }
631
632 // Shared sandbox for the entire pipeline (gh#29). Each stage runs
633 // inside the same directory so later stages observe earlier stages'
634 // file edits — preserving the v2.1.4 forward-carry behavior.
635 std::optional<SandboxInfo> shared_sb;
636 if (sandbox_mgr_ != nullptr) {
637 shared_sb = sandbox_mgr_->create_sandbox(
638 mint_delegation_id("pipeline"));
639 if (!shared_sb.has_value()) {
640 // gh#33 bug 2 (v2.1.6): see execute_delegation comment.
641 logger->error(
642 "Pipeline ({} stages): session sandbox unavailable",
643 stages.size());
644 return {"(DELEGATION FAILED: session sandbox unavailable)",
645 false, first, task};
646 }
647 }
648
649 DelegationResult last_result;
650 last_result.task = task;
651
652 for (size_t i = 0; i < stages.size(); ++i) {
653 // gh#162: every stage opens with the same seeded references.
654 if (!run_pipeline_stage(parent_ctx, stages, i,
655 format_context_block(context) + task,
656 shared_sb, stage_log, last_result)) {
657 break;
658 }
659 }
660
661 finalize_sandbox_for(shared_sb, last_result);
662 return last_result;
663}
664
684bool DelegationManager::run_pipeline_stage(
686 const std::vector<std::string>& stages,
687 size_t stage_idx,
688 const std::string& task,
689 const std::optional<SandboxInfo>& shared_sb,
690 std::vector<DelegationResult>& stage_log,
691 DelegationResult& last_result) {
692
693 const auto& tier_name = stages[stage_idx];
694 std::string prior = (stage_idx == 0) ? std::string{} : last_result.summary;
695 std::string stage_task =
696 pipeline_context(stage_idx, stages.size(), stages, prior) + task;
697
698 auto info = tier_res_.resolve_tier
699 ? tier_res_.resolve_tier(tier_name, tier_res_.user_data)
700 : ChildContextInfo{};
701
702 if (!info.valid) {
703 logger->error("Pipeline stage {}: tier '{}' not found",
704 stage_idx, tier_name);
705 last_result.success = false;
706 last_result.summary = "Unknown tier: " + tier_name;
707 return false;
708 }
709
710 auto child_ctx = build_child_context(parent_ctx, info, stage_task);
711 child_ctx.locked_tier = tier_name;
712
713 if (shared_sb && swap_dir_fn_ != nullptr) {
714 child_ctx.active_root = shared_sb->path.string();
715 ScopedSandbox scope(swap_dir_fn_, swap_dir_data_,
716 parent_ctx.session_key, shared_sb->path,
717 restore_root_for(parent_ctx));
718 last_result = run_child(child_ctx, tier_name, stage_task,
719 std::nullopt);
720 } else {
721 last_result = run_child(child_ctx, tier_name, stage_task,
722 std::nullopt);
723 }
724
725 stage_log.push_back(last_result);
726 logger->info("Pipeline stage {} ({}): {}", stage_idx, tier_name,
727 last_result.success ? "complete" : "failed");
728 return last_result.success;
729}
730
731// ── Child context ────────────────────────────────────────
732
742LoopContext DelegationManager::build_child_context(
743 const LoopContext& parent_ctx,
744 const ChildContextInfo& info,
745 const std::string& task,
746 const std::vector<ContextRef>& context) {
747
748 LoopContext child;
749 child.delegation_depth = parent_ctx.delegation_depth + 1;
750 // gh#48 (regression fix, v2.7.4): propagate the parent conversation id so
751 // the storage create_delegation INSERT satisfies the delegations→
752 // conversations FK. Without this the row is rejected, the delegations
753 // table stays empty, and entropic.followup returns the no-matches
754 // sentinel. (Present in build_resumed_child_context; lost here when the
755 // child-context builders were extracted.)
756 child.parent_conversation_id = parent_ctx.conversation_id;
757 // gh#158 (v2.13.0, also gh#162): children inherit the parent's session
758 // key. It was left "" here, so every delegated child of every parent ran
759 // under the DEFAULT session — one shared KV slot and one shared bucket.
760 // Harmless while a handle ran one turn at a time; with concurrent runs
761 // two parents' children would collide in that single bucket.
762 child.session_key = parent_ctx.session_key;
763 // P1-9: propagate ancestor chain + append parent tier so the
764 // child can reject cycles (A→B→A) before executing.
765 child.delegation_ancestor_tiers = parent_ctx.delegation_ancestor_tiers;
766 if (!parent_ctx.locked_tier.empty()) {
767 child.delegation_ancestor_tiers.push_back(parent_ctx.locked_tier);
768 }
769 child.locked_tier = info.system_prompt.empty()
770 ? parent_ctx.locked_tier : "";
771 child.active_phase = "default";
772
773 // System prompt as first message
774 Message sys;
775 sys.role = "system";
776 sys.content = info.system_prompt;
777 child.messages.push_back(std::move(sys));
778
779 // Task as user message (context references, then task, then any
780 // completion instructions). gh#162: the block comes FIRST because a
781 // small model that reads the task and starts searching has already
782 // spent the turn the references were meant to save.
783 std::string user_content = format_context_block(context) + task;
784 if (!info.completion_instructions.empty()) {
785 user_content += "\n\n" + info.completion_instructions;
786 }
787 Message user;
788 user.role = "user";
789 user.content = std::move(user_content);
790 child.messages.push_back(std::move(user));
791
792 return child;
793}
794
802std::string DelegationManager::format_context_block(
803 const std::vector<ContextRef>& context) {
804 if (context.empty()) {
805 return "";
806 }
807 std::string block =
808 "[CONTEXT] The lead already located these files. Read them "
809 "directly instead of searching:\n";
810 for (const auto& ref : context) {
811 block += "- " + ref.path;
812 if (!ref.lines.empty()) { block += " (lines " + ref.lines + ")"; }
813 if (!ref.note.empty()) { block += " — " + ref.note; }
814 block += "\n";
815 }
816 block += "\n";
817 return block;
818}
819
827std::string DelegationManager::extract_summary(
828 const LoopContext& child_ctx) const {
829 // Prefer explicit completion summary
830 auto it = child_ctx.metadata.find("explicit_completion_summary");
831 if (it != child_ctx.metadata.end() && !it->second.empty()) {
832 return mcp::sanitize_utf8(it->second);
833 }
834
835 // Fall back to last assistant message
836 for (auto rit = child_ctx.messages.rbegin();
837 rit != child_ctx.messages.rend(); ++rit) {
838 if (rit->role == "assistant" && !rit->content.empty()) {
839 return mcp::sanitize_utf8(rit->content);
840 }
841 }
842
843 return "(No response from delegate)";
844}
845
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) {
869 if (!storage_ || !storage_->create_delegation) {
870 return "";
871 }
872 std::string del_id, child_conv_id;
873 auto src_tier = child_ctx.locked_tier.empty()
874 ? "root" : child_ctx.locked_tier.c_str();
875 storage_->create_delegation(
876 child_ctx.parent_conversation_id.c_str(),
877 src_tier, target_tier.c_str(), task.c_str(),
878 max_turns.value_or(0), del_id, child_conv_id,
879 storage_->user_data);
880 child_ctx.conversation_id = child_conv_id;
881 return del_id;
882}
883
891void DelegationManager::complete_storage_record(
892 const std::string& delegation_id,
893 const DelegationResult& result) {
894 if (!storage_ || !storage_->complete_delegation
895 || delegation_id.empty()) {
896 return;
897 }
898 const char* status = result.success ? "completed" : "failed";
899 storage_->complete_delegation(
900 delegation_id.c_str(), status,
901 result.summary.c_str(), storage_->user_data);
902}
903
917DelegationResult DelegationManager::run_child(
918 LoopContext& child_ctx,
919 const std::string& target_tier,
920 const std::string& task,
921 std::optional<int> max_turns) {
922
923 // Save parent todo list
924 std::string saved_todo;
925 if (todo_callbacks_.save != nullptr) {
926 saved_todo = todo_callbacks_.save(todo_callbacks_.user_data);
927 }
928 if (todo_callbacks_.install_fresh != nullptr) {
929 todo_callbacks_.install_fresh(todo_callbacks_.user_data);
930 }
931
932 auto delegation_id = create_storage_record(
933 child_ctx, target_tier, task, max_turns);
934
935 // gh#182 (v2.13.0): until now `max_turns` reached the storage row and
936 // stopped — `run_child_fn_` takes no limit, so an argument the model is
937 // SHOWN in delegate.json (minimum 1, maximum 30) and allowed to set
938 // bounded nothing. Carrying it on the child context is what lets
939 // AgentEngine::resolve_max_iterations see it; that resolver takes the
940 // stricter of this and the operator's own limit, so a request larger
941 // than the tier or engine allows still cannot raise the bound.
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);
946 }
947
948 logger->info("Running child loop: tier={} depth={} msgs={} "
949 "system_hash={:016x}",
950 target_tier, child_ctx.delegation_depth,
951 child_ctx.messages.size(),
952 std::hash<std::string>{}(
953 child_ctx.messages.empty()
954 ? std::string{}
955 : child_ctx.messages[0].content));
956
957 if (run_child_fn_ != nullptr) {
958 run_child_fn_(child_ctx, run_child_data_);
959 }
960
961 // Restore parent todo list
962 if (todo_callbacks_.restore != nullptr && !saved_todo.empty()) {
963 todo_callbacks_.restore(saved_todo, todo_callbacks_.user_data);
964 }
965
966 auto result = build_child_result(
967 target_tier, task, child_ctx);
968 complete_storage_record(delegation_id, result);
969 log_child_result(result);
970 return result;
971}
972
994DelegationResult DelegationManager::build_child_result(
995 const std::string& target_tier,
996 const std::string& task,
997 LoopContext& child_ctx) {
998 DelegationResult result;
999 result.target_tier = target_tier;
1000 result.task = task;
1001 auto tr = child_ctx.metadata.find("terminal_reason");
1002 if (tr != child_ctx.metadata.end()) {
1003 result.terminal_reason = tr->second;
1004 }
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);
1009 // Issue #10 (v2.1.4): hoist coverage_gap signal from child
1010 // metadata onto DelegationResult so the parent's
1011 // finalize_delegation_result can branch on it without re-parsing
1012 // ctx.metadata. dir_complete writes these keys when the child
1013 // calls entropic.complete with coverage_gap=true.
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");
1018 if (gd != child_ctx.metadata.end()) {
1019 result.gap_description = gd->second;
1020 }
1021 auto sf = child_ctx.metadata.find("suggested_files_json");
1022 if (sf != child_ctx.metadata.end()) {
1023 auto parsed = nlohmann::json::parse(
1024 sf->second, nullptr, false);
1025 if (parsed.is_array()) {
1026 result.suggested_files =
1027 parsed.get<std::vector<std::string>>();
1028 }
1029 }
1030 }
1031 result.child_messages = std::move(child_ctx.messages);
1032 return result;
1033}
1034
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,
1046 result.turns_used);
1047 } else {
1048 logger->warn("Child loop done: tier={} success=false "
1049 "turns={} reason={}",
1050 result.target_tier, result.turns_used,
1051 result.terminal_reason);
1052 }
1053}
1054
1070void DelegationManager::finalize_sandbox_for(
1071 const std::optional<SandboxInfo>& sb_info,
1072 const DelegationResult& result) {
1073 if (!sb_info || !sandbox_mgr_) {
1074 return;
1075 }
1076
1077 if (result.success) {
1078 auto patch_result = sandbox_mgr_->finalize_sandbox(*sb_info);
1079 if (patch_result) {
1080 deliver_sandbox_result(*sb_info, *patch_result, result);
1081 } else {
1082 logger->error("Delegation {}: finalize_sandbox failed; "
1083 "no patch delivered", sb_info->delegation_id);
1084 }
1085 } else {
1086 logger->info("Delegation {} failed: discarding sandbox without "
1087 "generating patch", sb_info->delegation_id);
1088 }
1089 sandbox_mgr_->discard_sandbox(*sb_info);
1090}
1091
1092} // namespace entropic
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.
Definition sandbox.h:116
void discard_sandbox(const SandboxInfo &info)
Remove a sandbox directory.
Definition sandbox.cpp:541
std::string next_delegation_id(const std::string &prefix)
Mint a sandbox id that no sibling delegation can collide with.
Definition sandbox.cpp:618
std::optional< SandboxResult > finalize_sandbox(const SandboxInfo &info)
Produce the final patch artifact for a sandbox.
Definition sandbox.cpp:514
std::optional< SandboxInfo > create_sandbox(const std::string &delegation_id, std::optional< SandboxInfo > chain_from=std::nullopt)
Create a new delegation sandbox.
Definition sandbox.cpp:398
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.
Definition sandbox.cpp:560
RAII directory swapper for sandbox-scoped tool execution.
Definition sandbox.h:331
void(*)(const std::string &session_key, const std::filesystem::path &path, bool entering, void *user_data) SwapDirFn
Callback type for directory swapping.
Definition sandbox.h:361
DelegationManager — child loop creation and execution.
Types for the agentic loop engine.
ent_decision_t
Consumer decision returned from delegation callbacks.
Definition entropic.h:1614
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
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.
Definition delegation.h:70
Request describing a delegation that is about to run.
Definition entropic.h:1628
const char * delegation_id
Short id ("d1", "d2", "pipeline")
Definition entropic.h:1629
Result of a finalized delegation, delivered to the consumer.
Definition entropic.h:1651
const char * delegation_id
Short id (matches request)
Definition entropic.h:1652
Resolved tier information for building child delegation contexts.
Result returned from a child delegation loop.
Definition delegation.h:33
bool success
Whether child reached COMPLETE via real entropic.complete.
Definition delegation.h:35
std::string summary
Final summary from child.
Definition delegation.h:34
std::string task
Original task text.
Definition delegation.h:37
std::string target_tier
Tier that executed.
Definition delegation.h:36
Mutable state carried through the agentic loop.
int delegation_depth
0 = root, 1+ = child
A message in a conversation.
Definition message.h:36
std::string role
Message role.
Definition message.h:37
Identifies one delegation's sandbox directory.
Definition sandbox.h:66
Final artifact emitted by a finalized sandbox.
Definition sandbox.h:83
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.
Definition delegation.h:79
std::string(* save)(void *user_data)
Save current todo list state. Returns opaque state string.
Definition delegation.h:81
void(* restore)(const std::string &saved, void *user_data)
Restore a previously saved todo list state.
Definition delegation.h:85
void(* install_fresh)(void *user_data)
Install a fresh empty todo list for child.
Definition delegation.h:83
void * user_data
Opaque pointer (facade context)
Definition delegation.h:86
UTF-8 validation + replacement at every system boundary where bytes change ownership.