Skip to content
Open
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
42 changes: 39 additions & 3 deletions docs/integrations/host-native-child-receipts.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ Agent 明确记录验收;宿主完成不代表证据已被采纳。
公开安全的证据与验证引用。结果和不采用评审的原因码可选,只需是紧凑的
不透明诊断标识,不限定供应商词表,也不授予权限;未知时省略。结果与评审
可重复携带一致的 `operation`、`entrypoint-id`,冲突值包括重放时仍被拒绝。
原始提示、宿主错误、对话、本地路径和凭据不进入回执。
原始提示、宿主错误、对话、本地路径和凭据不进入公开回执。支持的宿主若能
直接观察最终结果正文,会把正文写入按 Goal 实例和主 Agent 隔离的本机私有
inbox;公开活动投影仍只保留类型化终态和不透明身份。
4. 同一身份和内容重放幂等,内容冲突会被拒绝。`read` 与带 Turn ID 的
`agent-context` 读取同一模型。Goal 状态的 JSON 与 Markdown 只在能力启用且确有决策时投影
最近一轮;仪表板读取该投影,未配置或无关 Goal 不显示活动行。
Expand All @@ -102,6 +104,38 @@ Agent 明确记录验收;宿主完成不代表证据已被采纳。
新写入在既有事件流锁内重新核对读回,每次报告只调用一个粗粒度 TS 边界,
不增加第二套 Python 阶段判断。

## Next-Turn result delivery / 下一 Turn 结果投递

Supported managed hosts persist an observed child's final text before exposing
the matching typed terminal receipt. The text is stored only in the
owner-private local runtime, partitioned by the captured Goal lifetime and
coordinator (or by Goal ID for a legacy registry without lifetime identity). It
never enters the public rollout log, status projection, telemetry, or repository
state.

The existing `manager-inbox` turn-start hook detects unread native results and
adds one `before_work` read. A normal `manager-inbox read` returns short results
inline and gives a digest-bound private file reference for larger results. That
read records delivery but does not consume the result. Until the parent records
an `accepted`, `deferred`, or `rejected` disposition with the supplied
`native-child acknowledge-result` command, a restart may deliver the same stable
result ID again. Exact ingest and acknowledgement replay are idempotent;
conflicting text or disposition is rejected. A child conclusion remains
candidate evidence and never performs parent work, validation, or adoption.

支持的托管宿主会先把观察到的子 Agent 最终正文写入主 Agent 的本机私有
runtime,再公开对应的类型化终态回执。存储按已捕获的 Goal 生命周期和主 Agent
隔离;缺少生命周期身份的旧 registry 按 Goal ID 隔离。正文不会进入公开 rollout
日志、状态投影、遥测或仓库状态。

既有 `manager-inbox` Turn-start hook 会发现未读结果,并在下一 Turn 的普通工作
之前要求读取。`manager-inbox read` 对短结果内联返回,对长结果返回带摘要校验的
私有文件引用。读取只记录 delivery,不会消费结果。主 Agent 使用返回的
`native-child acknowledge-result` 命令明确记录 `accepted`、`deferred` 或
`rejected` 之前,重启后仍可能看到同一个稳定结果 ID。相同写入与确认重放幂等,
正文或 disposition 冲突会被拒绝。子 Agent 结论始终只是候选证据,不会替主
Agent 执行工作、验证或采纳。

After a lawful deferred-Todo recovery, rerun `quota should-run` with the same
explicit Turn and Todo. If the original bound guard denied delivery and the
current guard explicitly admits work, quota appends a work-qualification receipt
Expand Down Expand Up @@ -196,7 +230,8 @@ replaying the same binding and item remains idempotent. Direct enabled CLI
adapter calls require that attempt; feature-off calls retain their original
request. Only compact hashed child references are retained for correlation;
they do not enter public activity rows or replace independent parent review.
No prompt or raw result is added to a receipt.
No prompt or raw result is added to a public receipt. Supported managed hosts
retain final result text only in the private next-Turn inbox described above.

恢复 CLI 时可能只收到新的 `wait` 完成事件。适配器从同一已准入 Goal 实例、
主 Agent、LoopX Turn 的最近一次已启动宿主 spawn 或 followup 回执恢复关联,
Expand All @@ -211,7 +246,8 @@ No prompt or raw result is added to a receipt.
app-server 使用其原生 Turn ID。真实重试在启动前递增尝试次数;同一绑定与
事件的重放仍幂等。直接调用已启用的 CLI 适配器须提供该尝试次数,关闭能力
时请求不变。关联只保留紧凑的子代理哈希引用,不进入公开活动行,
也不替代独立父任务验收。回执不增加原始提示或结果内容。
也不替代独立父任务验收。公开回执不增加原始提示或结果内容;支持的托管宿主仅在
上述私有下一 Turn inbox 中保留最终结果正文。

A live isolated Codex 0.142.5 test observed one successful spawn, a second failed
spawn at `agents.max_threads=1`, child completion and independent parent
Expand Down
3 changes: 3 additions & 0 deletions loopx/capabilities/manager_context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ def produce():
count = (
max(len(inbox["items"]), agenda["incoming_count"])
+ max(len(inbox.get("peer_returns", {}).get("items", [])), agenda["outgoing_count"])
+ len(inbox.get("native_child_results", {}).get("items", []))
+ int(inbox.get("native_child_results", {}).get("unavailable_count", 0))
+ int(inbox.get("native_child_results", {}).get("scan_complete") is False)
+ len(inbox.get("operation_handoffs", []))
)
incomplete = not agenda["scan_complete"]
Expand Down
48 changes: 38 additions & 10 deletions loopx/capabilities/multi_subagent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
from ...control_plane.goals.source_session_registry_state import exact_goal_ref
from ...orchestration import compact_orchestration_policy
from .native_child_receipts import load_native_child_activity, record_native_child
from .native_child_result_inbox import acknowledge_native_child_result


def register_native_child_commands(subparsers, add_format):
parser = subparsers.add_parser(
"native-child", help="Record or read typed host-native child activity for an admitted Turn."
)
add_format(parser)
parser.add_argument("native_child_action", choices=("record", "read"))
parser.add_argument("native_child_action", choices=("record", "read", "acknowledge-result"))
parser.add_argument("--goal-id", required=True)
parser.add_argument("--agent-id", required=True)
parser.add_argument("--turn-instance-id", required=True)
Expand All @@ -35,6 +36,9 @@ def register_native_child_commands(subparsers, add_format):
"for result/deferred/rejected. Accepted reviews use evidence/validation refs instead.")
parser.add_argument("--evidence-ref", help="Public-safe opaque reference for accepted evidence.")
parser.add_argument("--validation-ref", help="Public-safe opaque reference for parent validation.")
parser.add_argument("--result-id", help="Stable native child result identity returned by manager-inbox read.")
parser.add_argument("--disposition", choices=("accepted", "deferred", "rejected"),
help="Parent disposition after the result was delivered and reviewed.")
parser.add_argument("--execute", action="store_true", help="Append the receipt; otherwise preview only.")


Expand All @@ -56,27 +60,46 @@ def handle_native_child_command(args, registry_path, runtime_root, print_payload
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):
raise ValueError("coordinator is not registered for this Goal")
orchestration = compact_orchestration_policy(goal.get("spawn_policy"))
if not (
orchestration.get("mode") == "multi_subagent"
and orchestration.get("spawn_allowed") is True
and int(orchestration.get("max_children") or 0) > 0
):
raise ValueError("native child receipts require enabled multi_subagent policy")
configured_limit = int(orchestration["max_children"])
configured_limit = None
if args.native_child_action != "acknowledge-result":
orchestration = compact_orchestration_policy(goal.get("spawn_policy"))
if not (
orchestration.get("mode") == "multi_subagent"
and orchestration.get("spawn_allowed") is True
and int(orchestration.get("max_children") or 0) > 0
):
raise ValueError("native child receipts require enabled multi_subagent policy")
configured_limit = int(orchestration["max_children"])
if args.native_child_action == "read":
if any((args.operation_id, args.stage, args.operation, args.outcome,
args.entrypoint_id, args.reason_code, args.evidence_ref,
args.validation_ref, args.execute)):
args.validation_ref, args.result_id, args.disposition, args.execute)):
raise ValueError("read accepts only goal, agent and Turn identity")
payload = {"ok": True, "native_child_activity": load_native_child_activity(
runtime_root, goal_id=args.goal_id, agent_id=args.agent_id,
turn_instance_id=args.turn_instance_id, configured_limit=configured_limit,
registry_path=registry_path, goal_ref=goal_ref,
)}
elif args.native_child_action == "acknowledge-result":
if any((args.operation_id, args.stage, args.operation, args.outcome,
args.entrypoint_id, args.reason_code, args.evidence_ref,
args.validation_ref)) or not args.result_id or not args.disposition:
raise ValueError("acknowledge-result requires only result-id and disposition")
if not args.execute:
payload = {"ok": True, "preview": True, "result_id": args.result_id,
"disposition": args.disposition}
else:
payload = acknowledge_native_child_result(
runtime_root=runtime_root, goal_id=args.goal_id,
agent_id=args.agent_id, turn_instance_id=args.turn_instance_id,
result_id=args.result_id,
disposition=args.disposition, goal_ref=goal_ref,
)
else:
if not args.operation_id or not args.stage or not args.outcome:
raise ValueError("record requires operation-id, stage and outcome")
if args.result_id or args.disposition:
raise ValueError("record does not accept result acknowledgement fields")
payload = record_native_child(
runtime_root=runtime_root, goal_id=args.goal_id, agent_id=args.agent_id,
turn_instance_id=args.turn_instance_id, operation_id=args.operation_id,
Expand All @@ -96,6 +119,11 @@ def handle_native_child_command(args, registry_path, runtime_root, print_payload
def render_native_child(payload):
if not payload["ok"]:
return str(payload["error"])
if "native_child_activity" not in payload:
return (
f"Native child result {payload['result_id']} "
f"{'would be ' if payload.get('preview') else ''}{payload['disposition']}."
)
activity = payload["native_child_activity"]
return (
f"Native child activity for {activity['turn_instance_id']}: "
Expand Down
49 changes: 49 additions & 0 deletions loopx/capabilities/multi_subagent/native_child_result_inbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Thin Python adapter for the TypeScript native-child result inbox owner."""

from __future__ import annotations

from collections.abc import Mapping
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from ...control_plane.effect_runtime import EffectRuntimeRejected, effect_runtime_result


def _now() -> str:
return datetime.now(timezone.utc).isoformat()


def ingest_native_child_result(
*, runtime_root: Path, goal_id: str, agent_id: str,
turn_instance_id: str, operation_id: str, outcome: str, text: str,
goal_ref: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
try:
return effect_runtime_result("multi_subagent.native_result_inbox", {
"operation": "ingest", "runtime_root": str(runtime_root.resolve()),
"goal_id": goal_id, "agent_id": agent_id,
"goal_ref": dict(goal_ref) if goal_ref is not None else None,
"turn_instance_id": turn_instance_id, "operation_id": operation_id,
"outcome": outcome, "text": text, "observed_at": _now(),
})
except EffectRuntimeRejected as exc:
raise ValueError(str(exc)) from exc


def acknowledge_native_child_result(
*, runtime_root: Path, goal_id: str, agent_id: str,
turn_instance_id: str, result_id: str, disposition: str,
goal_ref: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
try:
return effect_runtime_result("multi_subagent.native_result_inbox", {
"operation": "acknowledge", "runtime_root": str(runtime_root.resolve()),
"goal_id": goal_id, "agent_id": agent_id,
"goal_ref": dict(goal_ref) if goal_ref is not None else None,
"turn_instance_id": turn_instance_id,
"result_id": result_id, "disposition": disposition,
"observed_at": _now(),
})
except EffectRuntimeRejected as exc:
raise ValueError(str(exc)) from exc
Loading
Loading