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
6 changes: 6 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,12 @@ export const capabilityConfigurationCatalogSchema = z.object({
effective_revision: z.string(),
}).optional(),
documentation: z.record(z.string(), z.unknown()).optional(),
context_contribution: z.object({
supported_phases: z.array(z.enum(["before_plan", "before_delegate", "after_delegate_result"])),
target: z.literal("coordinator"),
activation: z.literal("with_capability"),
receipt_required: z.literal(true),
}).optional(),
configuration_editor: capabilityConfigurationEditorSchema,
})),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ export function CapabilityDetailHeader({ capability, locale, source }: Readonly<
<h2>{localized.display_name}</h2>
<CapabilityEffectiveSource source={source} t={t} />
</div>
{capability.context_contribution && (
<details className="personal-capability-help" data-testid="capability-context-phases">
<summary>{locale === "zh-CN" ? "主 Agent 协作指导" : "Coordinator workflow guidance"}</summary>
<p>{locale === "zh-CN"
? "随能力开启。支持以下阶段;此处展示能力范围,不能证明某次运行已读取或采纳。"
: "Enabled with this capability. These are supported phases, not proof that a run read or adopted the guidance."}</p>
<dl>{capability.context_contribution.supported_phases.map((phase) => (
<div key={phase}>
<dt><code>{phase}</code></dt>
<dd>{contextPhaseCopy[phase][locale === "zh-CN" ? "zh" : "en"]}</dd>
</div>
))}</dl>
<p>{locale === "zh-CN"
? "LoopX Turn 在请求与结果中提供上下文;原生工具入口由 LoopX skill 调用同一只读接口。执行与采纳需另看运行证据。"
: "LoopX Turn includes context in requests and results. For native tools, the LoopX skill reads the same interface. Execution and adoption require run evidence."}</p>
</details>
)}
<details className="personal-capability-help" key={capability.capability_id}>
<summary>{locale === "zh-CN" ? "配置说明" : "Configuration help"}</summary>
<p>{localized.description}</p>
Expand All @@ -139,3 +156,10 @@ export function CapabilityDetailHeader({ capability, locale, source }: Readonly<
</header>
);
}


const contextPhaseCopy = {
before_plan: { zh: "规划前:识别独立问题并保留主 Agent 的核验与整合职责。", en: "Before planning: identify independent questions and retain coordinator validation and integration." },
before_delegate: { zh: "委派前:明确子任务边界、模型偏好及预期证据。", en: "Before delegation: specify task boundaries, model preferences and expected evidence." },
after_delegate_result: { zh: "回收后:核验结果,说明采纳决定并关联计划与成果。", en: "After results: validate evidence, explain acceptance and link plans and deliverables." },
} as const;
58 changes: 58 additions & 0 deletions docs/integrations/codex-subagent-orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ model from response style.

### Frontend configuration

The capability detail page also lists **Coordinator workflow guidance** in
English / **主 Agent 协作指导** in Chinese. It shows the three supported phases
and their purpose, using the shared capability catalog. It is read-only metadata,
not a per-run delivery indicator. The existing capability switch controls this
guidance together with child capacity; there is no second enable switch.

Goal settings → Goal details exposes child model and reasoning effort next to
the existing execution boundary. “Use Luna / max” fills the draft only; use
“Preview configuration update” to save it, including while execution is off.
Expand All @@ -379,3 +385,55 @@ preview/readback validation; a model change invalidates an older preview.
The model field is a preference, not a discovery menu or an execution receipt.
The CLI accepts an empty `--subagent-reasoning-effort ''` to clear effort while
retaining the model; `--clear-subagent-model-config` clears both.

### Capability context lifecycle

An enabled `multi_subagent` capability contributes to the coordinator through
the generic bounded provider contract in `control_plane/agent_context.ts`.
The capability owns its guidance in `control_plane/subagent_context.ts`; other
capabilities can implement the same typed provider interface and register with
their runtime owner. This initial version registers this built-in provider,
not arbitrary manifest scripts or external plugins.

| Phase | Managed LoopX Turn call site | Coordinator responsibility |
| --- | --- | --- |
| `before_plan` | Live quota decision → `interaction_contract.agent_context` → signed `turn_envelope.agent_context` | Prefer parallel delegation for read-heavy tasks with independent questions, within the configured child limit; keep useful validation and integration work with the parent. |
| `before_delegate` | Admitted child operations → plan and host request `delegation_context` | Bound briefs, expected evidence and model preferences before selecting/launching children. |
| `after_delegate_result` | Host receipt reconciliation → journal `host_result.agent_context` → executor result `agent_context` | Validate receipts and original evidence, then accept/defer/reject and link outcomes. |

Planning guidance does not require two persistent Todos. Managed automatic
child-lane admission still requires its existing prerequisites; this change
does not grant spawning rights or force concurrency. Disabling the capability
omits the context. Model preferences alone do not enable it.

For a native-tool host such as a Codex App session, the LoopX project skill reads
the same interface at delegation and result boundaries:

```bash
loopx agent-context --goal-id example-peer-task-goal --agent-id coordinator \
--phase before_delegate --format json
loopx agent-context --goal-id example-peer-task-goal --agent-id coordinator \
--phase after_delegate_result --format json
```

The coordinator must be registered. The command reads current registry policy
without writing a Todo, starting a turn or spending quota. It has no native
execution receipt input; return-phase facts explicitly say `not_supplied`.
LoopX cannot transparently intercept arbitrary host tools. The managed return
packet is returned to the caller, not automatically sent as another model turn.

Each packet binds Goal/Agent/optional Todo, phase, provider revision and a stable
content ID. When the envelope approaches its existing budget, it carries a
signed content hash and a read instruction pointing to the existing full-decision
route instead of duplicating all guidance. Providers return only guidance, bounded facts and source references;
they cannot replace action, permission or priority fields. Per-provider and
aggregate limits are 2,048 and 3,072 UTF-8 bytes. Failures are isolated and
diagnostic text excludes raw provider errors. These scoped context projections
are not a new progress store and should not be exported as public evidence.
`delivery: projected` means generated, not delivered/read/adopted. Read actual
host receipts and parent validation evidence for those conclusions. Replaying
a journal preserves the same packet; no new execution is inferred.

中文:三个阶段已落到真实控制面接口,开启 capability 自动提供主 Agent
协作指导。原生工具通过 skill 在边界读取同一接口,不声称拦截宿主工具。
关闭 capability 即停止提供;前端展示支持范围,执行、采纳仍须看实际证据。
37 changes: 37 additions & 0 deletions examples/control_plane/cli-output-probe-runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,42 @@ def _blocking_gate_rows(
]



def _multi_subagent_rows(probe, semantics, fixture_root):
"""Run the same enabled public fixture on base and head, not default-off only."""
project, runtime, registry_path, state_file = probe._write_fixture(
fixture_root / "multi_subagent_enabled", probe.SCENARIOS[0]
)
registry = json.loads(registry_path.read_text())
registry["goals"][0]["spawn_policy"] = {
"mode": "multi_subagent", "spawn_allowed": True, "max_children": 4,
"model_config": {"model": "example-small", "reasoning_effort": "max"},
}
registry_path.write_text(json.dumps(registry))
variant_id = "quota_should_run_turn_envelope"
command = probe._mode_variant_commands(
project=project, runtime=runtime, registry_path=registry_path,
state_file=state_file, output_format="json",
)[variant_id]
exit_code, output = probe._invoke_cli(command)
if exit_code != 0:
raise AssertionError("enabled multi_subagent turn envelope failed")
measurement = probe.measure_cli_output(output, output_format="json")
variant = probe.CLI_OUTPUT_MODE_VARIANT_BY_ID[variant_id]
probe.assert_cli_output_mode_variant(
variant, output_format="json", text=output, measurement=measurement,
)
return [_receipt_row(
semantics=semantics,
row_id="variant/quota_should_run_turn_envelope_multi_subagent/small/json",
surface_id=variant.parent_surface_id,
variant_id="quota_should_run_turn_envelope_multi_subagent", scenario="small",
output_format="json", qualification_policy="explicit_opt_in_cold_path",
semantic_json_keys=variant.semantic_json_keys, markdown_anchor=variant.markdown_anchor,
measurement=measurement, text=output,
)]


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--test-source", type=Path, required=True)
Expand All @@ -292,6 +328,7 @@ def main() -> int:
*_default_rows(probe, semantics, stable_root),
*_variant_rows(probe, semantics, stable_root),
*_blocking_gate_rows(probe, semantics, stable_root),
*_multi_subagent_rows(probe, semantics, stable_root),
]
args.receipt.parent.mkdir(parents=True, exist_ok=True)
args.receipt.write_text(
Expand Down
18 changes: 18 additions & 0 deletions examples/personal-workspace-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ function multiSubagentCapability({ current } = {}) {
const effective = current ?? fallback;
return {
capability_id: "multi_subagent",
context_contribution: {
supported_phases: ["before_plan", "before_delegate", "after_delegate_result"],
target: "coordinator", activation: "with_capability", receipt_required: true,
},
display_name: "Adaptive child capacity",
description: "Bound child-agent capacity and eligible responsibility domains.",
available_scopes: ["goal"],
Expand Down Expand Up @@ -2262,6 +2266,20 @@ async function main() {

await page.getByRole("button", { name: /自适应子 Agent 容量/u }).click();
await page.getByRole("heading", { level: 2, name: /^自适应子 Agent 容量/ }).waitFor({ state: "visible" });
const contextHelp = page.getByTestId("capability-context-phases");
await contextHelp.locator("summary").click();
for (const phase of ["before_plan", "before_delegate", "after_delegate_result"]) {
await contextHelp.getByText(phase, { exact: true }).waitFor({ state: "visible" });
}
await contextHelp.getByText(/不能证明某次运行已读取或采纳/u).waitFor({ state: "visible" });
await page.screenshot({ path: resolve(outputDir, "capability-context-phases-desktop.png"), fullPage: false, animations: "disabled" });
await page.setViewportSize({ width: 390, height: 844 });
if (await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth)) {
throw new Error("Capability lifecycle guidance overflows mobile viewport");
}
await page.screenshot({ path: resolve(outputDir, "capability-context-phases-mobile.png"), fullPage: false, animations: "disabled" });
await page.setViewportSize({ width: 1440, height: 1000 });
await contextHelp.locator("summary").click();
const multiSubagentEnabled = page.getByLabel(/^启用$/u);
const multiSubagentMaxChildren = page.getByLabel(/^最大子 Agent 数/u);
const multiSubagentDomains = page.getByLabel(/^允许的职责域/u);
Expand Down
2 changes: 1 addition & 1 deletion loopx/capabilities/configuration_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def _merge_goal_feature(
entry["available_scopes"] = [*entry["available_scopes"], "goal"]
entry["goal_feature_id"] = capability_id
entry["availability"] = feature.get("availability")
for field in ("default", "current", "documentation"):
for field in ("default", "current", "documentation", "context_contribution"):
if isinstance(feature.get(field), Mapping):
entry[field] = deepcopy(feature[field])
entry["configuration_editor"] = capability_configuration_editor(
Expand Down
5 changes: 5 additions & 0 deletions loopx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import sys

from .cli_commands.agent_context import register_agent_context, handle_agent_context
from .cli_commands.manager_inbox import register_manager_inbox, handle_manager_inbox

from .capabilities.content_ops.cli import (
Expand Down Expand Up @@ -325,6 +326,7 @@ def build_parser() -> LoopXArgumentParser:
register_project_lifecycle_commands(sub, add_subcommand_format)
register_goal_channel_commands(sub, add_subcommand_format)
register_manager_inbox(sub, add_subcommand_format)
register_agent_context(sub, add_subcommand_format)
register_lark_inbox_commands(sub, add_subcommand_format)
register_lark_kanban_commands(sub, add_subcommand_format)

Expand Down Expand Up @@ -754,6 +756,9 @@ def main(argv: list[str] | None = None) -> int:
if lark_kanban_result is not None:
return lark_kanban_result

if args.command == "agent-context":
return handle_agent_context(args, registry_path, print_payload, output_format)

if args.command == "manager-inbox":
return handle_manager_inbox(args, registry_path, effective_runtime_root(registry_path, args.runtime_root))

Expand Down
66 changes: 66 additions & 0 deletions loopx/cli_commands/agent_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Read-only lifecycle context for hosts whose native tools bypass LoopX Turn."""

from ..agent_registry import load_goal_from_registry, registered_agent_ids_for_goal
from ..control_plane.agent_context import project_agent_context
from ..orchestration import compact_orchestration_policy


def register_agent_context(subparsers, add_format):
parser = subparsers.add_parser(
"agent-context", help="Read enabled capability guidance for the coordinator."
)
add_format(parser)
parser.add_argument("--goal-id", required=True)
parser.add_argument("--agent-id", required=True)
parser.add_argument(
"--phase",
required=True,
choices=("before_plan", "before_delegate", "after_delegate_result"),
)


def handle_agent_context(args, registry_path, print_payload, output_format):
goal = load_goal_from_registry(registry_path, args.goal_id)
if goal is None or args.agent_id not in registered_agent_ids_for_goal(goal):
print_payload(
{"ok": False, "error": "coordinator is not registered for this Goal"},
output_format(args),
render_agent_context,
)
return 1
context = project_agent_context(
phase=args.phase,
scope={"goal_id": args.goal_id, "agent_id": args.agent_id, "todo_id": None},
orchestration=compact_orchestration_policy(goal.get("spawn_policy")),
)
print_payload(
{
"ok": True,
"agent_context": context,
"source": "registry.spawn_policy",
"read_only": True,
"host_receipts_observed": False,
},
output_format(args),
render_agent_context,
)
return 0


def render_agent_context(payload):
if not payload["ok"]:
return str(payload["error"])
context = payload.get("agent_context")
if not context:
return "No enabled capability contributes coordinator context at this phase."
lines = [
f"Coordinator context: {context['phase']} (projected guidance; not delivery or adoption)"
]
for contribution in context["contributions"]:
lines.append(f"{contribution['capability_id']} / {contribution['revision']}")
lines.extend(f"- {text}" for text in contribution["guidance"])
for key, value in contribution["facts"].items():
lines.append(f"- {key}: {value}")
if context["failures"]:
lines.append("Some context providers failed; inspect JSON diagnostics.")
return "\n".join(lines)
2 changes: 2 additions & 0 deletions loopx/configuration_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from collections.abc import Mapping, Sequence
from typing import Any

from .control_plane.agent_context import agent_context_descriptor
from .capabilities.configuration_ui import build_capability_configuration_catalog
from .control_plane.goals.goal_vision_policy import completed_todo_replan_threshold

Expand Down Expand Up @@ -220,6 +221,7 @@ def build_goal_configuration_catalog(
},
{
"feature_id": "multi_subagent",
"context_contribution": agent_context_descriptor(),
"display_name": "Adaptive child capacity",
"availability": "supported_opt_in",
"default": {
Expand Down
47 changes: 47 additions & 0 deletions loopx/control_plane/agent_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Transport lifecycle inputs to the typed capability context owner."""

from collections.abc import Mapping
from typing import Any

from .effect_runtime import effect_runtime_result


def project_agent_context(
*,
phase: str,
scope: Mapping[str, Any],
orchestration: Mapping[str, Any],
observations: Mapping[str, Any] | None = None,
):
return effect_runtime_result(
"capability_hook.agent_context.project",
{
"phase": phase,
"scope": dict(scope),
"orchestration": dict(orchestration),
"observations": dict(observations or {}),
},
)


def envelope_agent_context(
envelope: Mapping[str, Any],
*,
phase: str,
observations: Mapping[str, Any] | None = None,
):
"""The signed planning contribution binds later phases to the coordinator."""
context = envelope.get("agent_context")
if not isinstance(context, Mapping):
return None
boundary = envelope.get("boundary") or {}
return project_agent_context(
phase=phase,
scope=context["scope"],
orchestration=boundary.get("orchestration") or {},
observations=observations,
)


def agent_context_descriptor():
return effect_runtime_result("capability_hook.agent_context.describe", {})
Loading
Loading