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
14 changes: 14 additions & 0 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,19 @@ export const projectAssetTodoProjectionGapSchema = z.object({
recommended_action: z.string().optional().nullable(),
});

export const nativeChildActivitySchema = z.object({
schema_version: z.literal("native_subagent_activity_v0"),
observation: z.enum(["unknown", "coordinator_reported"]),
host_attested: z.literal(false),
configured_limit: z.number().int().nonnegative(),
launched_count: z.number().int().nonnegative(),
skipped_count: z.number().int().nonnegative(),
capacity_rejected_count: z.number().int().nonnegative(),
host_failed_count: z.number().int().nonnegative(),
parent_accepted_count: z.number().int().nonnegative(),
turn_instance_id: z.string(),
});

export const projectAssetSchema = z.object({
owner: z.string(),
gate: z.string(),
Expand All @@ -351,6 +364,7 @@ export const projectAssetSchema = z.object({
quota: quotaSchema.optional().nullable(),
control_plane: controlPlaneSchema.optional().nullable(),
orchestration: orchestrationPolicySchema.optional().nullable(),
native_child_activity: nativeChildActivitySchema.optional().nullable(),
latest_validation: projectAssetLatestValidationSchema.optional().nullable(),
stale_latest_run_warning: staleLatestRunWarningSchema.optional().nullable(),
todo_projection_gap: projectAssetTodoProjectionGapSchema.optional().nullable(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,18 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<button className="personal-secondary-action" onClick={() => callbacks.onRequestScheduleConfig?.("heartbeat", selection.item.goalId)} type="button"><Radio size={16} />{t("drawer.setupHeartbeat")}</button>
<button className="personal-secondary-action" onClick={() => callbacks.onRequestScheduleConfig?.("monitor", selection.item.goalId)} type="button"><CalendarClock size={16} />{t("drawer.scheduleAdd")}</button>
</div> : null}
{selection.item.nativeChildActivity?.observation === "coordinator_reported" ? (
<section className="personal-detail-card personal-native-child-activity">
<h3>{t("drawer.subagentReportTitle")}</h3>
<p>{t("drawer.subagentReportedActivity", {
started: selection.item.nativeChildActivity.launched_count,
skipped: selection.item.nativeChildActivity.skipped_count,
rejected: selection.item.nativeChildActivity.capacity_rejected_count,
failed: selection.item.nativeChildActivity.host_failed_count,
accepted: selection.item.nativeChildActivity.parent_accepted_count,
})}</p>
</section>
) : null}
{selection.item.subagentExecution ? <section className="personal-detail-card personal-goal-subagents">
<div className="personal-subagent-heading">
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ const en = {
"drawer.subagentConfirmEnable": "Confirm turning on sub-agent execution",
"drawer.subagentCurrentBoundary": "Current task-domain restriction",
"drawer.subagentDescription": "Allows the runtime to create temporary child agents for independent tasks only after Todo, quota, capability, and write-scope gates pass. It does not force parallel work or grant durable authority.",
"drawer.subagentReportTitle": "Child activity",
"drawer.subagentReportedActivity": "Latest coordinator report: {started} starts, {skipped} skips, {rejected} capacity rejections, {failed} host failures, {accepted} parent-accepted results. Host verification is unavailable.",
"drawer.subagentDisable": "Preview turning off sub-agent execution",
"drawer.subagentDisableSummary": "New child-agent execution will be disabled for this Goal. Existing Todo ownership and execution records stay unchanged.",
"drawer.subagentDomainInvalid": "The selected task-domain restriction is invalid.",
Expand Down Expand Up @@ -1386,6 +1388,8 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.subagentConfirmEnable": "确认开启子代理执行",
"drawer.subagentCurrentBoundary": "当前任务领域限制",
"drawer.subagentDescription": "仅在 Todo、配额、能力和写入范围门禁全部通过后,允许运行时为相互独立的任务临时创建子代理;不会强制并行,也不会授予持久权限。",
"drawer.subagentReportTitle": "子代理活动",
"drawer.subagentReportedActivity": "最近一轮主 Agent 回报:启动 {started} 次、跳过 {skipped} 次、容量拒绝 {rejected} 次、宿主失败 {failed} 次、主 Agent 验收 {accepted} 项;目前没有宿主核验。",
"drawer.subagentDisable": "预览关闭子代理执行",
"drawer.subagentDisableSummary": "这个 Goal 将不再创建新的子代理;现有 Todo 归属和执行记录不受影响。",
"drawer.subagentDomainInvalid": "所选任务领域限制无效。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ export type WorkspaceGoal = {
repository?: WorkspaceRepositoryContext;
state: WorkspaceGoalState;
subagentExecution?: WorkspaceGoalSubagentConfiguration;
nativeChildActivity?: {
turn_instance_id: string;
observation: "unknown" | "coordinator_reported";
host_attested: false;
launched_count: number;
skipped_count: number;
capacity_rejected_count: number;
host_failed_count: number;
parent_accepted_count: number;
} | null;
title: string;
usage?: WorkspaceGoalUsage | null;
};
Expand Down
11 changes: 11 additions & 0 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,16 @@ type PersonalGoalItem = {
needsYouTodoId?: string | null;
nextSentence: string;
hasRunObservation: boolean;
nativeChildActivity?: {
turn_instance_id: string;
observation: "unknown" | "coordinator_reported";
host_attested: false;
launched_count: number;
skipped_count: number;
capacity_rejected_count: number;
host_failed_count: number;
parent_accepted_count: number;
} | null;
state: PersonalGoalState;
subagentExecution?: {
allowedDomains: string[];
Expand Down Expand Up @@ -1208,6 +1218,7 @@ function buildPersonalHomeModel(
hasRunObservation: Boolean(row.queueItem?.project_asset?.latest_validation
|| row.latestRun
|| payload.event_ledger_summary?.goals.some((item) => item.goal_id === goal.id)),
nativeChildActivity: row.queueItem?.project_asset?.native_child_activity,
state,
...(goalSubagentConfigurationEnabled ? {
subagentExecution: {
Expand Down
88 changes: 88 additions & 0 deletions docs/integrations/host-native-child-receipts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Host-native child receipts / 宿主原生子代理回执

## Contract / 契约

When an enabled `multi_subagent` coordinator works in an admitted Turn, it can
write a bounded `decision → result → parent review` record for a child created
through any host's native tools. The record uses the existing Goal rollout event
log, the Turn instance ID, a stable operation ID, and an opaque `entrypoint_id`.
It does not launch children, schedule another Turn, grant write authority, or
spend quota. The configured `max_children` is an upper bound, never an observed
live capacity or a required launch count.

启用 `multi_subagent` 的主 Agent 在已准入的 Turn 内,可以为任意宿主原生工具
创建的子代理记录有界的“决策 → 结果 → 主 Agent 验收”回执。回执复用现有
Goal 事件流,以 Turn ID、稳定操作 ID 和不限定宿主的 `entrypoint_id` 关联。
记录动作不会启动子代理、调度新 Turn、授予写入权限或消耗配额。
`max_children` 只是配置上限,不代表当前可用槽位,也不是必须启动的数量。

`native-child record` currently accepts the coordinator's typed report. Its
`observation` is `coordinator_reported` and `host_attested` is always `false`.
LoopX cannot intercept an arbitrary external host's native tool call. A future
host adapter must observe that call at its own boundary and extend this event
and read-model contract with a separately verified provenance variant; this
v0 recorder cannot claim host attestation. Missing records remain
`unknown`; neither a missing record nor `max_children > 0` proves that a child
was created or deliberately skipped.

目前 `native-child record` 接受主 Agent 的类型化上报,因此 `observation` 为
`coordinator_reported`,`host_attested` 始终为 `false`。LoopX 无法拦截任意
外部宿主的原生工具调用。后续宿主适配器须在自己的边界观察调用,给事件与
读模型扩展单独核验的来源类型;当前 v0 上报器不能声称宿主核验。缺少回执
就是 `unknown`;没有回执或配置上限大于零,都不能证明已启动或主动跳过。

## Lifecycle / 生命周期

1. The admitted Turn guard must already have a settlement binding. `record`
rejects unregistered coordinators and disabled policy before writing.
2. Use `stage=decision` with a stable `operation-id` for `spawn`, `followup`,
or a bounded `skip` reason. A host capacity rejection maps to the generic
`host_capacity_exhausted` reason. Capacity rejection and typed host failure
stop same-Turn spawn/followup retries while parent work may continue.
3. A started operation may get a typed `result`. Only a completed result may
receive `parent review`; `accepted` requires public-safe evidence and
validation references. Raw prompts, host errors, transcripts, local paths,
and credentials are excluded.
4. Replay with the same identity and payload is idempotent; a conflicting
payload is rejected. `read` and `agent-context --phase after_delegate_result
--turn-instance-id ...` expose the same Turn read model. Goal status (JSON and
Markdown) exposes
the latest reported Turn only when the capability is enabled and a decision
exists. The dashboard uses that status projection, and omits the activity
line for unconfigured or unrelated Goals.

1. Turn 须先有已提交的结算绑定;未注册主 Agent 或未启用策略不能写入。
2. 用稳定 `operation-id` 写 `decision`,区分 `spawn`、`followup` 和有界理由的
`skip`。宿主容量拒绝映射为通用 `host_capacity_exhausted`;容量拒绝和
类型化宿主失败都停止同一 Turn 的启动或跟进重试,主 Agent 仍可继续工作。
3. 已启动操作可记录类型化 `result`;只有完成结果才能进入主 Agent `review`。
`accepted` 必须有公开安全的证据与验证引用。原始提示、宿主错误、对话、
本地路径和凭据不进入回执。
4. 同一身份和内容重放幂等,内容冲突会被拒绝。`read` 与带 Turn ID 的
`agent-context` 读取同一模型。Goal 状态的 JSON 与 Markdown 只在能力启用且确有决策时投影
最近一轮;仪表板读取该投影,未配置或无关 Goal 不显示活动行。

Example / 示例:

```sh
loopx --format json --registry REGISTRY native-child record \
--goal-id GOAL --agent-id COORDINATOR --turn-instance-id TURN \
--operation-id OPERATION --stage decision --operation spawn \
--outcome started --entrypoint-id HOST_ENTRYPOINT --execute
loopx --format json --registry REGISTRY native-child record \
--goal-id GOAL --agent-id COORDINATOR --turn-instance-id TURN \
--operation-id OPERATION --stage result --outcome completed --execute
loopx --format json --registry REGISTRY native-child record \
--goal-id GOAL --agent-id COORDINATOR --turn-instance-id TURN \
--operation-id OPERATION --stage review --outcome accepted \
--evidence-ref EVIDENCE_ID --validation-ref VALIDATION_ID --execute
loopx --format json --registry REGISTRY native-child read \
--goal-id GOAL --agent-id COORDINATOR --turn-instance-id TURN
```

The source of truth for a bound LoopX delegation remains its delegation
operation receipt. A native child report never substitutes for that receipt or
for the parent validation of the underlying work.

已绑定的 LoopX delegation 仍以自身操作回执为权威。原生子代理上报不能代替
delegation 回执,也不能代替主 Agent 对工作结果的实际核验。
52 changes: 52 additions & 0 deletions examples/dashboard-home-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ const goalSpecs = [
max_children: 2,
allowed_domains: ["docs", "validation"],
},
nativeChildActivity: {
schema_version: "native_subagent_activity_v0",
turn_instance_id: "fixture-turn-1",
observation: "coordinator_reported",
host_attested: false,
configured_limit: 2,
launched_count: 1,
skipped_count: 0,
capacity_rejected_count: 1,
host_failed_count: 0,
parent_accepted_count: 0,
},
latest: {
generated_at: "2026-01-01T00:03:00+00:00",
classification: "dashboard_home_chinese_operator_copy_contract",
Expand All @@ -224,6 +236,7 @@ function projectAssetFor(spec) {
quota: spec.quota,
control_plane: spec.controlPlane,
orchestration: spec.orchestration,
native_child_activity: spec.nativeChildActivity,
latest_validation: {
generated_at: spec.latest.generated_at,
classification: spec.latest.classification,
Expand Down Expand Up @@ -1293,6 +1306,42 @@ async function main() {
throw new Error(`Failed waiting for personal-goal-home: ${error.message}; body=${diagnostic.slice(0, 1000)}; pageErrors=${pageErrors.join(" | ")}`);
}

if (process.env.LOOPX_NATIVE_CHILD_SMOKE_ONLY === "1") {
for (const [label, target] of [["desktop", page],
["mobile", await browser.newPage({ isMobile: true, viewport: { width: 390, height: 900 } })]]) {
try {
await target.goto(`${baseUrl}/?goalId=loopx-meta&statusUrl=/${fixtureName}`, { waitUntil: "networkidle" });
await target.waitForSelector('[data-testid="personal-goal-home"]', { timeout: 10_000 });
await target.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: "概览", exact: true }).click();
await target.locator(".goal-overview-heading button").click();
const drawerText = await target.locator(".personal-context-drawer").innerText();
if (!drawerText.includes("主 Agent 回报:启动 1 次、跳过 0 次、容量拒绝 1 次")
|| !drawerText.includes("目前没有宿主核验")) {
throw new Error(`${label} native child report lost provenance: ${drawerText}`);
}
await target.screenshot({
path: resolve(visualOutputDir, `${label}-native-child-activity.png`),
animations: "disabled",
});
await target.locator(".personal-native-child-activity").screenshot({
path: resolve(visualOutputDir, `${label}-native-child-card.png`),
animations: "disabled",
});
} finally {
if (target !== page) await target.close();
}
}
await page.goto(`${baseUrl}/?goalId=showcase-user-gate-safe-side-path&statusUrl=/${fixtureName}`, { waitUntil: "networkidle" });
await page.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: "概览", exact: true }).click();
await page.locator(".goal-overview-heading button").click();
const unconfiguredDrawer = await page.locator(".personal-context-drawer").innerText();
if (unconfiguredDrawer.includes("主 Agent 回报")) {
throw new Error("Unconfigured Goal displayed unrelated native child activity.");
}
console.log("dashboard-native-child-activity-smoke ok");
return;
}

await page.locator(".personal-composer-tools > summary").click();
const body = await page.locator("body").innerText();
const required = [
Expand Down Expand Up @@ -1410,6 +1459,9 @@ async function main() {
if (!drawerText.includes("Goal 详情") && !drawerText.includes("Repository")) {
throw new Error(`Context drawer did not render expected details: ${drawerText}`);
}
if (!drawerText.includes("主 Agent 回报:启动 1 次、跳过 0 次、容量拒绝 1 次")) {
throw new Error(`Native child report was not shown with its provenance: ${drawerText}`);
}
}

// Missing status URL error state & fallback
Expand Down
Loading
Loading