Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions loopx/control_plane/subagent_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@ import { isAbsolute } from "node:path";
import { delegationRuntimeFacts, selectDelegationBinding } from "./collaboration/delegation.ts";

export const subagentContextProvider: AgentContextProvider = {
hookId: "multi_subagent.coordinator", capabilityId: "multi_subagent", revision: "v6",
hookId: "multi_subagent.coordinator", capabilityId: "multi_subagent", revision: "v8",
phases: AGENT_CONTEXT_PHASES,
produce(input, config) {
const nativeCapacity = input.phase === "after_delegate_result"
? boundedNativeCapacityObservation(input.observations.native_host_capacity)
: null;
const capacityExhausted = nativeCapacity?.outcome === "agent_thread_limit_reached";
// Leave room for the complete model preference and bounded route or receipt facts.
const guidance = {
before_plan: [
"Prefer bounded independent delegation; max_children is a configured ceiling, not live availability. Admit native children incrementally, avoid duplicate reads, and keep one parent question.",
"Host-native child work uses loopx native-child record/read for bounded Turn receipts; these calls do not start Turns or spend quota.",
"Choose independent work from current routes and user preferences. Runtime availability is not entrypoint admission; inspect bound delegation. Peer-activation blocks do not assess native children or delegation. Do not relaunch every route on every heartbeat.",
"Delegate coherent independent outcomes and leave methods to children. Add them incrementally within max_children. Avoid duplicate reads and keep useful parent work.",
"Honor user preferences. Use authorized host-native tools directly. Inspect the chosen binding for admission. Peer activation gates apply only to peers. Don't relaunch all routes each heartbeat.",
"Use loopx native-child record/read for Turn receipts without starting Turns or spending quota.",
],
before_delegate: [
"Give each child a bounded question, sources, read/write limits, expected evidence and stopping condition; identify dependencies and the coordinator's concurrent question.",
"For an authorized route, use its binding entrypoint and recheck the chosen runtime, execution profile and budget. Never silently substitute a runtime/model; record the selection reason and stable operation id. Report each native decision or bounded skip; missing reports stay unknown.",
"Give each child a question, acceptance, sources, read/write limits and stop condition. Invite inquiry and challenge. Name dependencies and parallel parent work.",
"Use authorized host-native tools directly. Only bound delegation needs its entrypoint and runtime/profile/budget preflight. Honor model preferences. Never silently substitute runtime/model. Record why and a stable operation ID. Report native decisions/skips. Missing reports stay unknown.",
],
after_delegate_result: [
"Check returned sources, omissions and contradictions against the question. Record native child result and parent review separately, then reconcile fresh Turn receipts and bound delegation receipts; missing or rejected receipts do not establish completed work.",
"On typed agent_thread_limit_reached, stop same-Turn spawn/followup retries, mark unlaunched work incomplete, and continue useful parent work.",
"Verify decisive sources and record accept/defer/reject with reasons. Link accepted evidence to the deliverable and run parent validation before writeback; opinions are not independent evidence.",
"Reconcile fresh receipts for the route used. Missing/rejected receipts can't prove completion. Separate native result and parent review.",
// The exhaustion observation already carries the stop and recovery actions.
...(capacityExhausted ? [] : [
"On typed agent_thread_limit_reached, stop same-Turn spawn/followup retries. Mark unlaunched work incomplete and continue useful parent work.",
]),
"Check originals, gaps and conflicts against acceptance. Opinions aren't independent evidence. Log accept/defer/reject reasons. Link evidence. Validate before writeback.",
],
}[input.phase];
const facts: JsonObject = {
Expand All @@ -46,14 +54,10 @@ export const subagentContextProvider: AgentContextProvider = {
facts.delegation_context = delegation;
}
if (input.phase === "after_delegate_result") {
const nativeCapacity = boundedNativeCapacityObservation(
input.observations.native_host_capacity,
);
if (nativeCapacity) {
facts.native_host_capacity = nativeCapacity;
const contract = facts.capacity_contract as JsonObject;
contract.live_availability = nativeCapacity.outcome === "agent_thread_limit_reached"
? "capacity_exhausted" : "attempt_observed";
contract.live_availability = capacityExhausted ? "capacity_exhausted" : "attempt_observed";
}
const nativeActivity = boundedNativeChildActivity(input.observations.native_child_activity);
if (nativeActivity) {
Expand Down
84 changes: 84 additions & 0 deletions tests/control_plane/test_agent_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,90 @@ def test_native_cli_is_read_only_and_does_not_claim_native_receipts(tmp_path, ph
assert json.loads(rejected.stdout)["ok"] is False


@pytest.mark.parametrize(
("binding_length", "model"),
[(94, "gpt-6.1-terra"), (95, "gpt-6.1-terra"), (95, "m" * 160)],
)
def test_native_cli_keeps_complete_context_with_large_bound_routes(
tmp_path, binding_length, model,
):
config_ref = ".loopx/config/delegations.json"
config = tmp_path / config_ref
config.parent.mkdir(parents=True)
bindings = []
for index in range(3):
prefix = f"binding-{index}-"
bindings.append({
"id": prefix + "a" * (binding_length - len(prefix)),
"agent_id": f"worker-{index}-" + "a" * (40 if index else 41),
"todo_id": f"todo_review000{index}",
"requesters": [SCOPE["agent_id"]],
"workspace": str(tmp_path / f"worker-{index}"),
"host_args": ["--host", "codex-cli"],
"timeout_seconds": 60,
"output_refs": ["result.json"],
})
config.write_text(json.dumps({
"schema_version": "loopx_local_delegation_v0", "bindings": bindings,
}))
model_config = {"model": model, "reasoning_effort": "max"}
registry = tmp_path / "registry.json"
registry.write_text(json.dumps({
"schema_version": 1,
"goals": [{
"id": SCOPE["goal_id"], "repo": str(tmp_path), "status": "active",
"registered_agents": [SCOPE["agent_id"], *(b["agent_id"] for b in bindings)],
"spawn_policy": {
**POLICY, "max_children": 3, "model_config": model_config,
"execution_config": config_ref,
},
}],
}))
command = [
sys.executable, "-m", "loopx.cli",
"--registry", str(registry), "--runtime-root", str(tmp_path / "runtime"),
"agent-context", "--goal-id", SCOPE["goal_id"], "--agent-id", SCOPE["agent_id"],
"--format", "json",
]
for phase in ("before_plan", "before_delegate", "after_delegate_result"):
args = [*command, "--phase", phase]
if phase == "after_delegate_result":
args += [
"--native-child-operation", "followup",
"--native-child-outcome", "agent_thread_limit_reached",
]
result = subprocess.run(args, capture_output=True, text=True, check=True)
payload = json.loads(result.stdout)
assert payload["ok"] is True
context = payload["agent_context"]
assert context["phase"] == phase
assert context["authority"] == "guidance_only"
assert context["failures"] == []
[contribution] = context["contributions"]
assert contribution["guidance"]
facts = contribution["facts"]
assert facts["model_preference"] == model_config
assert len(json.dumps(contribution, separators=(",", ":")).encode()) <= 2048
assert len(json.dumps(context, separators=(",", ":")).encode()) <= 3072
if phase == "after_delegate_result":
assert "delegation_context" not in facts
assert facts["delegation_receipts"]["operation_receipts"] == {"observed": 0}
assert facts["native_host_capacity"]["retry_same_turn"] is False
assert facts["receipt_observation"] == "not_supplied"
else:
routes = facts["delegation_context"]
assert routes["execution_scope"] == "bound_delegation"
assert routes["preflight"] == "required"
assert routes["authorized_count"] == 3
assert routes["projected_count"] == 2
assert routes["routes_truncated"] is True
assert [route["binding_id"] for route in routes["routes"]] == [
b["id"] for b in bindings[:2]
]
assert all(route["readiness"] == "unknown" for route in routes["routes"])
assert str(tmp_path) not in result.stdout


def test_native_cli_projects_typed_capacity_exhaustion_without_raw_error(tmp_path):
registry = tmp_path / "registry.json"
registry.write_text(
Expand Down
111 changes: 94 additions & 17 deletions tests/control_plane_ts/agent_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ test("stable context ids bind phase, scope, content and policy revision", () =>
assert.notEqual(id(original), id(projectAgentContext({ ...input, ...patch }, [provider])));
}
assert.notEqual(id(original), id(projectAgentContext(input, [{ ...provider, revision: "v2" }])));
for (const patch of [{ guidance: ["Inspect contradictory evidence."] }, { facts: { observed: 1 } }]) {
assert.notEqual(id(original), id(projectAgentContext(input, [{ ...provider,
produce: () => ({ ...provider.produce({} as any, {}), ...patch }),
}])));
}
});

test("provider failures, forged control fields and oversize context are isolated", () => {
Expand Down Expand Up @@ -168,20 +173,27 @@ test("maximum delegation directory stays within provider budget", () => {
executor_kind: "managed", readiness: "ready",
execution_profile: "fixture-provider/fixture-model@max",
}));
const packet = evaluateSubagentContext({ phase: "before_plan", scope,
orchestration: { ...policy, max_children: 6 }, observations: {
delegation_context: {
schema_version: "loopx_delegation_context_v0", configuration_state: "ready",
observed_at: "2026-09-19T04:20:00+00:00", authorized_count: 6,
projected_count: 6, routes,
},
} })!;
assert.deepEqual(packet.failures, []);
const delegation = (packet.contributions as any[])[0].facts.delegation_context;
assert.ok(delegation.projected_count >= 1 && delegation.projected_count <= 6);
assert.equal(delegation.routes.length, delegation.projected_count);
assert.equal(delegation.routes_truncated, delegation.projected_count < 6 || undefined);
assert.ok(Buffer.byteLength(JSON.stringify(packet.contributions[0])) <= 2048);
const model_config = { model: "m".repeat(160), reasoning_effort: "medium" };
for (const phase of ["before_plan", "before_delegate"]) {
const packet = evaluateSubagentContext({ phase, scope,
orchestration: { ...policy, max_children: 6, model_config }, observations: {
...(phase === "before_delegate" ? { child_count: 6 } : {}),
delegation_context: {
schema_version: "loopx_delegation_context_v0", configuration_state: "ready",
observed_at: "2026-09-19T04:20:00+00:00", authorized_count: 6,
projected_count: 6, routes,
},
} })!;
assert.deepEqual(packet.failures, []);
const [contribution] = packet.contributions as Record<string, any>[];
const delegation = contribution.facts.delegation_context;
assert.deepEqual(contribution.facts.model_preference, model_config);
assert.ok(delegation.projected_count >= 2 && delegation.projected_count <= 6);
assert.equal(delegation.routes.length, delegation.projected_count);
assert.equal(delegation.routes_truncated, delegation.projected_count < 6 || undefined);
assert.ok(Buffer.byteLength(JSON.stringify(contribution)) <= 2048);
assert.ok(Buffer.byteLength(JSON.stringify(packet)) <= 3072);
}
});

test("planning progressively discloses probe scope without crowding out a second route", () => {
Expand Down Expand Up @@ -228,8 +240,11 @@ test("coordinator participation guidance survives all bounded lifecycle projecti
} })!;
assert.deepEqual(packet.failures, []);
const [contribution] = packet.contributions as Record<string, any>[];
assert.equal(contribution.revision, "v6");
assert.equal(contribution.revision, "v8");
assert.equal(packet.authority, "guidance_only");
if (phase === "after_delegate_result") {
assert.match(contribution.guidance.join(" "), /stop same-Turn spawn\/followup retries/);
}
assert.ok(Buffer.byteLength(JSON.stringify(contribution)) <= 2048);
assert.ok(Buffer.byteLength(JSON.stringify(packet)) <= 3072);
}
Expand Down Expand Up @@ -358,16 +373,78 @@ const blocked = (phase = "before_plan") => ({
reason_code: "delegation_context_unavailable",
});

test("delegation facts reproduce the pinned Python projection byte for byte, context_id included", (t) => {
test("delegation facts preserve the pinned migration contract across guidance revisions", (t) => {
for (const [name, scenario] of Object.entries<any>(fixture.scenarios)) {
t.mock.timers.enable({ apis: ["Date"], now: Date.parse(scenario.observed_at) });
const packet = evaluateGoalAgentContext({ phase: scenario.phase, scope: fixture.scope,
orchestration: scenario.orchestration, observations: {}, delegation_facts: scenario.delegation_facts });
assert.equal(JSON.stringify(packet), JSON.stringify(scenario.expected_context), name);
// The fixture preserves the pre-migration observation, not a permanent
// wording freeze. Guidance/revision are deliberately versioned; context id
// content binding is covered separately above.
const factsOnly = (value: any) => ({ ...value, contributions: value.contributions.map(
({ guidance, revision, context_id, ...observation }: any) => observation,
) });
assert.deepEqual(factsOnly(packet), factsOnly(scenario.expected_context), name);
t.mock.timers.reset();
}
});

test("native guidance keeps binding preflight scoped even when delegation is unavailable", () => {
for (const delegation_facts of [undefined, { config_error: "unavailable" }, delegationFacts([])]) {
const packet = evaluateGoalAgentContext({ phase: "before_delegate", scope: fixture.scope,
orchestration: { ...policy, model_config: null }, observations: {}, delegation_facts })!;
assert.equal(packet.authority, "guidance_only");
assert.deepEqual(packet.failures, []);
const [{ guidance, facts }] = packet.contributions as Record<string, any>[];
assert.match(guidance.join(" "), /authorized host-native tools directly/);
assert.match(guidance.join(" "), /Only bound delegation needs its entrypoint and runtime\/profile\/budget preflight/);
assert.match(guidance.join(" "), /never silently substitute runtime\/model/i);
assert.equal(facts.capacity_contract.live_availability, "not_observed");
if (facts.delegation_context) {
assert.equal(facts.delegation_context.execution_scope, "bound_delegation");
assert.equal(facts.delegation_context.preflight, "required");
assert.deepEqual(facts.delegation_context.routes, []);
}
}
});

test("return context keeps native capacity stops and receipt sources with a long model preference", () => {
const model_config = { model: "m".repeat(160), reasoning_effort: "medium" };
const items = ["prepared", "running", "turn_returned", "accepted", "rejected", "stopped", "unavailable"]
.map(status => ({ status, recovery_required: status === "stopped", private_result: "raw child material" }));
const packet = evaluateGoalAgentContext({ phase: "after_delegate_result", scope: fixture.scope,
orchestration: { ...policy, model_config }, delegation_facts: delegationFacts([binding(0)], {
operation_inventory: { items, has_more: true },
}), observations: {
native_host_capacity: {
schema_version: "native_subagent_capacity_observation_v0", operation: "followup",
outcome: "agent_thread_limit_reached", child_count: 2,
},
native_child_activity: {
schema_version: "native_subagent_activity_v0", entrypoint_scope: "host_native_child_tools",
observation: "coordinator_reported", configured_limit: 2, attempted_count: 3,
launched_count: 2, capacity_rejected_count: 1, parent_accepted_count: 1,
retry_same_turn: false,
},
} })!;
assert.deepEqual(packet.failures, []);
const [contribution] = packet.contributions as Record<string, any>[];
const guidance = contribution.guidance.join(" ");
assert.match(guidance, /Missing\/rejected receipts can't prove completion/);
assert.match(guidance, /Opinions aren't independent evidence/);
assert.deepEqual(contribution.facts.model_preference, model_config);
assert.equal(contribution.facts.native_host_capacity.retry_same_turn, false);
assert.equal(contribution.facts.native_child_activity.host_attested, false);
assert.equal(contribution.facts.native_child_activity.parent_accepted_count, 1);
assert.equal(contribution.facts.receipt_observation, "not_supplied");
assert.equal(contribution.facts.delegation_receipts.operation_receipts.observed, 7);
assert.equal(contribution.facts.delegation_receipts.operation_receipts.accepted, 1);
assert.equal(contribution.facts.delegation_context, undefined);
assert.ok(!JSON.stringify(packet).includes("raw child material"));
assert.ok(Buffer.byteLength(JSON.stringify(contribution)) <= 2048);
assert.ok(Buffer.byteLength(JSON.stringify(packet)) <= 3072);
});

test("ready routes derive readiness once from the executor fact and join one probe per host argv", () => {
const managed = { ...genericExecutor, executor: "dsh", executor_kind: "managed", execution_profile: "model-a@high",
available: false, unavailable_reason: "dsh_runtime_unavailable",
Expand Down
Loading