|
|
@@ -56,6 +56,7 @@ from cyber_agent.core.resource_budget import (
|
|
|
from cyber_agent.core.memory import compute_memory_identity
|
|
|
from cyber_agent.core.run_snapshot import (
|
|
|
RunConfigSnapshotV1,
|
|
|
+ RunConfigSnapshotV2,
|
|
|
persist_run_config_snapshot,
|
|
|
)
|
|
|
from cyber_agent.trace.models import Trace, Messages
|
|
|
@@ -779,6 +780,49 @@ async def _run_agents(
|
|
|
policy = require_mutable_trace_policy(parent_trace.context)
|
|
|
except ValueError as exc:
|
|
|
return {"status": "failed", "error": str(exc)}
|
|
|
+ application_binding = getattr(runner, "application_binding", None)
|
|
|
+ application_ref = parent_trace.context.get("application_ref")
|
|
|
+ target_role_binding = None
|
|
|
+ target_role_id = agent_type
|
|
|
+ if application_ref is not None:
|
|
|
+ if application_binding is None:
|
|
|
+ return {
|
|
|
+ "status": "failed",
|
|
|
+ "error": "Application child creation requires the bound ApplicationRuntime",
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ application_ref
|
|
|
+ != application_binding.application_ref.model_dump(mode="json")
|
|
|
+ ):
|
|
|
+ return {
|
|
|
+ "status": "failed",
|
|
|
+ "error": "Parent ApplicationRef does not match Runner binding",
|
|
|
+ }
|
|
|
+ if skills is not None:
|
|
|
+ return {
|
|
|
+ "status": "failed",
|
|
|
+ "error": "Application roles do not allow per-call skill overrides",
|
|
|
+ }
|
|
|
+ parent_role_id = parent_trace.context.get("application_role_id")
|
|
|
+ try:
|
|
|
+ parent_role = application_binding.role(parent_role_id)
|
|
|
+ except ValueError as exc:
|
|
|
+ return {"status": "failed", "error": str(exc)}
|
|
|
+ allowed_child_roles = parent_role.role.allowed_child_roles
|
|
|
+ if target_role_id is None and len(allowed_child_roles) == 1:
|
|
|
+ target_role_id = allowed_child_roles[0]
|
|
|
+ if target_role_id not in allowed_child_roles:
|
|
|
+ return {
|
|
|
+ "status": "failed",
|
|
|
+ "error": (
|
|
|
+ f"Application role {parent_role_id} cannot create child role: "
|
|
|
+ f"{target_role_id}"
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ try:
|
|
|
+ target_role_binding = application_binding.role(target_role_id)
|
|
|
+ except ValueError as exc:
|
|
|
+ return {"status": "failed", "error": str(exc)}
|
|
|
root_task_anchor = None
|
|
|
if policy.requires_task_protocol:
|
|
|
try:
|
|
|
@@ -816,6 +860,12 @@ async def _run_agents(
|
|
|
)
|
|
|
except ValueError as exc:
|
|
|
return {"status": "failed", "error": str(exc)}
|
|
|
+ effective_limits = parent_trace.context.get("effective_run_limits") or {}
|
|
|
+ if effective_limits:
|
|
|
+ max_parallel_children = min(
|
|
|
+ max_parallel_children,
|
|
|
+ int(effective_limits["max_parallel_children"]),
|
|
|
+ )
|
|
|
|
|
|
protocol_state = None
|
|
|
approved_action = None
|
|
|
@@ -953,6 +1003,17 @@ async def _run_agents(
|
|
|
"status": "failed",
|
|
|
"error": "continue_from trace Agent mode does not match the current trace",
|
|
|
}
|
|
|
+ if target_role_binding is not None and (
|
|
|
+ existing.context.get("application_ref") != application_ref
|
|
|
+ or existing.context.get("application_role_id")
|
|
|
+ != target_role_binding.role.role_id
|
|
|
+ or existing.context.get("application_role_hash")
|
|
|
+ != target_role_binding.role_hash
|
|
|
+ ):
|
|
|
+ return {
|
|
|
+ "status": "failed",
|
|
|
+ "error": "continue_from application role binding does not match",
|
|
|
+ }
|
|
|
|
|
|
sub_trace_id = continue_from
|
|
|
continued = True
|
|
|
@@ -970,12 +1031,18 @@ async def _run_agents(
|
|
|
"status": "failed",
|
|
|
"error": "continue_from trace lineage does not match the current trace",
|
|
|
}
|
|
|
- if child_depth > policy.max_depth:
|
|
|
+ effective_max_depth = int(
|
|
|
+ (parent_trace.context.get("effective_run_limits") or {}).get(
|
|
|
+ "max_depth",
|
|
|
+ policy.max_depth,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ if child_depth > min(policy.max_depth, effective_max_depth):
|
|
|
return {
|
|
|
"status": "failed",
|
|
|
"error": (
|
|
|
"continue_from trace exceeds the persisted Agent mode depth: "
|
|
|
- f"depth={child_depth}, max={policy.max_depth}"
|
|
|
+ f"depth={child_depth}, max={min(policy.max_depth, effective_max_depth)}"
|
|
|
),
|
|
|
}
|
|
|
all_sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
|
|
|
@@ -1004,7 +1071,8 @@ async def _run_agents(
|
|
|
root_task_anchor=root_task_anchor,
|
|
|
granted_at_sequence=(existing.last_sequence or 0) + 1,
|
|
|
)
|
|
|
- if child_state.get("task_brief") != normalized_dump:
|
|
|
+ brief_changed = child_state.get("task_brief") != normalized_dump
|
|
|
+ if brief_changed:
|
|
|
replace_task_brief(
|
|
|
child_state,
|
|
|
task_briefs[0],
|
|
|
@@ -1012,16 +1080,50 @@ async def _run_agents(
|
|
|
)
|
|
|
child_context[CONTEXT_ACCESS_KEY] = new_context_access
|
|
|
persist_root_task_anchor(child_context, root_task_anchor)
|
|
|
+ if (
|
|
|
+ target_role_binding is not None
|
|
|
+ and getattr(runner, "context_provider", None) is not None
|
|
|
+ and brief_changed
|
|
|
+ ):
|
|
|
+ from cyber_agent.application.context import load_application_context
|
|
|
+ from cyber_agent.application.ports import ContextRequest
|
|
|
+
|
|
|
+ await load_application_context(
|
|
|
+ application_binding,
|
|
|
+ child_context,
|
|
|
+ ContextRequest(
|
|
|
+ application_ref=application_binding.application_ref,
|
|
|
+ root_trace_id=root_trace_id,
|
|
|
+ trace_id=existing.trace_id,
|
|
|
+ parent_trace_id=trace_id,
|
|
|
+ uid=parent_trace.uid,
|
|
|
+ role_id=target_role_binding.role.role_id,
|
|
|
+ task_brief=task_briefs[0],
|
|
|
+ task_brief_revision=child_state["task_brief_version"],
|
|
|
+ authorized_context_refs=tuple(
|
|
|
+ task_briefs[0].context_refs
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ root_task_anchor=root_task_anchor,
|
|
|
+ task_brief=task_briefs[0],
|
|
|
+ granted_at_sequence=(existing.last_sequence or 0) + 1,
|
|
|
+ )
|
|
|
existing.context = child_context
|
|
|
await store.update_trace(existing.trace_id, context=existing.context)
|
|
|
else:
|
|
|
parent_depth, root_trace_id = await _resolve_trace_lineage(store, parent_trace)
|
|
|
- if parent_depth >= policy.max_depth:
|
|
|
+ effective_max_depth = int(
|
|
|
+ (parent_trace.context.get("effective_run_limits") or {}).get(
|
|
|
+ "max_depth",
|
|
|
+ policy.max_depth,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ if parent_depth >= min(policy.max_depth, effective_max_depth):
|
|
|
return {
|
|
|
"status": "failed",
|
|
|
"error": (
|
|
|
f"Local Sub-Agent depth limit reached: "
|
|
|
- f"depth={parent_depth}, max={policy.max_depth}, "
|
|
|
+ f"depth={parent_depth}, max={min(policy.max_depth, effective_max_depth)}, "
|
|
|
f"mode={policy.mode.value}"
|
|
|
),
|
|
|
}
|
|
|
@@ -1056,7 +1158,9 @@ async def _run_agents(
|
|
|
if task_briefs
|
|
|
else task_item
|
|
|
)
|
|
|
- resolved_agent_type = agent_type or ("delegate" if single else "explore")
|
|
|
+ resolved_agent_type = target_role_id or (
|
|
|
+ "delegate" if single else "explore"
|
|
|
+ )
|
|
|
suffix = "delegate" if single else f"explore-{i+1:03d}"
|
|
|
stid = generate_sub_trace_id(trace_id, suffix)
|
|
|
child_context = apply_policy_to_context({
|
|
|
@@ -1073,6 +1177,47 @@ async def _run_agents(
|
|
|
persist_root_task_anchor(child_context, root_task_anchor)
|
|
|
if policy.requires_task_progress:
|
|
|
initialize_task_progress(child_context["task_protocol"])
|
|
|
+ if target_role_binding is not None:
|
|
|
+ parent_limits = parent_trace.context["effective_run_limits"]
|
|
|
+ role_limits = target_role_binding.effective_limits.model_dump(
|
|
|
+ mode="json"
|
|
|
+ )
|
|
|
+ child_limits = {
|
|
|
+ name: min(parent_limits[name], value)
|
|
|
+ for name, value in role_limits.items()
|
|
|
+ }
|
|
|
+ child_context.update({
|
|
|
+ "application_ref": application_ref,
|
|
|
+ "application_role_id": target_role_binding.role.role_id,
|
|
|
+ "application_role_hash": target_role_binding.role_hash,
|
|
|
+ "effective_run_limits": child_limits,
|
|
|
+ })
|
|
|
+ if getattr(runner, "context_provider", None) is not None:
|
|
|
+ from cyber_agent.application.context import (
|
|
|
+ load_application_context,
|
|
|
+ )
|
|
|
+ from cyber_agent.application.ports import ContextRequest
|
|
|
+
|
|
|
+ await load_application_context(
|
|
|
+ application_binding,
|
|
|
+ child_context,
|
|
|
+ ContextRequest(
|
|
|
+ application_ref=application_binding.application_ref,
|
|
|
+ root_trace_id=root_trace_id,
|
|
|
+ trace_id=stid,
|
|
|
+ parent_trace_id=trace_id,
|
|
|
+ uid=parent_trace.uid,
|
|
|
+ role_id=target_role_binding.role.role_id,
|
|
|
+ task_brief=task_briefs[i],
|
|
|
+ task_brief_revision=1,
|
|
|
+ authorized_context_refs=tuple(
|
|
|
+ task_briefs[i].context_refs
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ root_task_anchor=root_task_anchor,
|
|
|
+ task_brief=task_briefs[i],
|
|
|
+ granted_at_sequence=0,
|
|
|
+ )
|
|
|
sub_trace = Trace(
|
|
|
trace_id=stid,
|
|
|
mode="agent",
|
|
|
@@ -1081,7 +1226,11 @@ async def _run_agents(
|
|
|
parent_goal_id=goal_id,
|
|
|
agent_type=resolved_agent_type,
|
|
|
uid=parent_trace.uid,
|
|
|
- model=parent_trace.model,
|
|
|
+ model=(
|
|
|
+ target_role_binding.role.model
|
|
|
+ if target_role_binding is not None
|
|
|
+ else parent_trace.model
|
|
|
+ ),
|
|
|
status="running",
|
|
|
context=child_context,
|
|
|
created_at=datetime.now(),
|
|
|
@@ -1105,6 +1254,14 @@ async def _run_agents(
|
|
|
sub_trace_id = child_records[0]["trace_id"]
|
|
|
|
|
|
child_limit = policy.max_children_per_parent
|
|
|
+ effective_child_limit = (
|
|
|
+ parent_trace.context.get("effective_run_limits") or {}
|
|
|
+ ).get("max_children_per_parent")
|
|
|
+ if effective_child_limit is not None:
|
|
|
+ child_limit = min(
|
|
|
+ child_limit if child_limit is not None else effective_child_limit,
|
|
|
+ int(effective_child_limit),
|
|
|
+ )
|
|
|
if child_limit is None:
|
|
|
await create_child_traces()
|
|
|
else:
|
|
|
@@ -1220,6 +1377,10 @@ async def _run_agents(
|
|
|
)
|
|
|
agent_msgs = list(msgs) + [{"role": "user", "content": task_item}]
|
|
|
allowed_tools = _get_allowed_tools(context, child_depth, policy)
|
|
|
+ if target_role_binding is not None:
|
|
|
+ allowed_tools = sorted(
|
|
|
+ set(allowed_tools or []) & set(target_role_binding.tool_names)
|
|
|
+ )
|
|
|
|
|
|
debug = getattr(runner, 'debug', False)
|
|
|
agent_label = (agent_type or ("delegate" if single else f"explore-{i+1}"))
|
|
|
@@ -1244,6 +1405,23 @@ async def _run_agents(
|
|
|
child_execution_mode=child_execution_mode,
|
|
|
max_parallel_children=max_parallel_children,
|
|
|
)
|
|
|
+ if target_role_binding is not None:
|
|
|
+ application_binding.configure_run_config(
|
|
|
+ child_config,
|
|
|
+ target_role_binding.role.role_id,
|
|
|
+ )
|
|
|
+ child_config.tools = allowed_tools
|
|
|
+ child_config.context = child_record["context"]
|
|
|
+ child_config.child_execution_mode = child_execution_mode
|
|
|
+ child_config.effective_run_limits = dict(
|
|
|
+ child_record["context"]["effective_run_limits"]
|
|
|
+ )
|
|
|
+ child_config.max_iterations = child_config.effective_run_limits[
|
|
|
+ "max_iterations"
|
|
|
+ ]
|
|
|
+ child_config.max_parallel_children = child_config.effective_run_limits[
|
|
|
+ "max_parallel_children"
|
|
|
+ ]
|
|
|
# Recursive children are persisted before their coroutine is scheduled so
|
|
|
# the parent can account for queued work. Bind the immutable run snapshot
|
|
|
# before execution; otherwise the first local resume would look exactly
|
|
|
@@ -1257,9 +1435,16 @@ async def _run_agents(
|
|
|
if child_config.memory
|
|
|
else None
|
|
|
)
|
|
|
- child_snapshot = RunConfigSnapshotV1.from_run_config(
|
|
|
- child_config,
|
|
|
- memory_identity=child_memory_identity,
|
|
|
+ child_snapshot = (
|
|
|
+ RunConfigSnapshotV2.from_run_config(
|
|
|
+ child_config,
|
|
|
+ memory_identity=child_memory_identity,
|
|
|
+ )
|
|
|
+ if target_role_binding is not None
|
|
|
+ else RunConfigSnapshotV1.from_run_config(
|
|
|
+ child_config,
|
|
|
+ memory_identity=child_memory_identity,
|
|
|
+ )
|
|
|
)
|
|
|
persist_run_config_snapshot(persisted_child.context, child_snapshot)
|
|
|
await store.update_trace(
|