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
4 changes: 2 additions & 2 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,8 @@ export const projectAssetTodoProjectionGapSchema = z.object({

export const nativeChildActivitySchema = z.object({
schema_version: z.literal("native_subagent_activity_v0"),
observation: z.enum(["unknown", "coordinator_reported"]),
host_attested: z.literal(false),
observation: z.enum(["unknown", "coordinator_reported", "host_observed", "mixed"]),
host_attested: z.boolean(),
configured_limit: z.number().int().nonnegative(),
launched_count: z.number().int().nonnegative(),
skipped_count: z.number().int().nonnegative(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,10 +801,12 @@ 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" ? (
{selection.item.nativeChildActivity && selection.item.nativeChildActivity.observation !== "unknown" ? (
<section className="personal-detail-card personal-native-child-activity">
<h3>{t("drawer.subagentReportTitle")}</h3>
<p>{t("drawer.subagentReportedActivity", {
<p>{t(selection.item.nativeChildActivity.observation === "host_observed"
? "drawer.subagentHostActivity" : selection.item.nativeChildActivity.observation === "mixed"
? "drawer.subagentMixedActivity" : "drawer.subagentReportedActivity", {
started: selection.item.nativeChildActivity.launched_count,
skipped: selection.item.nativeChildActivity.skipped_count,
rejected: selection.item.nativeChildActivity.capacity_rejected_count,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ const en = {
"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.subagentHostActivity": "Host-observed activity: {started} starts, {rejected} capacity rejections, {failed} host failures, {accepted} parent-accepted results.",
"drawer.subagentMixedActivity": "Host observations and coordinator reports: {started} starts, {skipped} skips, {rejected} capacity rejections, {failed} host failures, {accepted} parent-accepted results. Some decisions are unverified.",
"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.",
Expand Down Expand Up @@ -1574,6 +1576,8 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.subagentCurrentBoundary": "当前任务领域限制",
"drawer.subagentDescription": "仅在 Todo、配额、能力和写入范围门禁全部通过后,允许运行时为相互独立的任务临时创建子代理;不会强制并行,也不会授予持久权限。",
"drawer.subagentReportTitle": "子代理活动",
"drawer.subagentHostActivity": "宿主已观察:启动 {started} 次、容量拒绝 {rejected} 次、宿主失败 {failed} 次、主 Agent 验收 {accepted} 项。",
"drawer.subagentMixedActivity": "宿主观察与主 Agent 回报:启动 {started} 次、跳过 {skipped} 次、容量拒绝 {rejected} 次、宿主失败 {failed} 次、主 Agent 验收 {accepted} 项;部分决策未经宿主核验。",
"drawer.subagentReportedActivity": "最近一轮主 Agent 回报:启动 {started} 次、跳过 {skipped} 次、容量拒绝 {rejected} 次、宿主失败 {failed} 次、主 Agent 验收 {accepted} 项;目前没有宿主核验。",
"drawer.subagentDisable": "预览关闭子代理执行",
"drawer.subagentDisableSummary": "这个 Goal 将不再创建新的子代理;现有 Todo 归属和执行记录不受影响。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export type WorkspaceGoal = {
subagentExecution?: WorkspaceGoalSubagentConfiguration;
nativeChildActivity?: {
turn_instance_id: string;
observation: "unknown" | "coordinator_reported";
host_attested: false;
observation: "unknown" | "coordinator_reported" | "host_observed" | "mixed";
host_attested: boolean;
launched_count: number;
skipped_count: number;
capacity_rejected_count: number;
Expand Down
4 changes: 2 additions & 2 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,8 @@ type PersonalGoalItem = {
hasRunObservation: boolean;
nativeChildActivity?: {
turn_instance_id: string;
observation: "unknown" | "coordinator_reported";
host_attested: false;
observation: "unknown" | "coordinator_reported" | "host_observed" | "mixed";
host_attested: boolean;
launched_count: number;
skipped_count: number;
capacity_rejected_count: number;
Expand Down
102 changes: 88 additions & 14 deletions docs/integrations/host-native-child-receipts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,32 @@ Goal 事件流,以 Turn ID、稳定操作 ID 和不限定宿主的 `entrypoint
记录动作不会启动子代理、调度新 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`;没有回执或配置上限大于零,都不能证明已启动或主动跳过。
`native-child record` accepts a coordinator report and cannot select host
provenance. Managed Codex CLI and operation-equipped app-server Turns also
observe native collaboration items directly on their owned connection. A
successful spawn, failed call and observed child completion become durable
`host_observed` records before Turn settlement. A parent review is a separate
explicit record; a host completion does not adopt the evidence.

The shared projection distinguishes `host_observed`, `coordinator_reported`,
`mixed` and `unknown`. `host_attested` is true only when every decision in the
Turn came from the host. Configured capacity remains an upper bound. Failed
Codex collaboration items do not carry a typed capacity error, so the adapter
records `host_failed` and forbids report-based same-Turn retry rather than
classifying provider prose. Missing native events stay unknown. Persisted Codex
history is not used to reconstruct native activity because supported host
versions may omit collaboration items from that history.

`native-child record` 仍是主 Agent 上报入口,不能指定宿主来源。托管 Codex CLI
与启用操作工具的 app-server Turn 会在自身连接上直接观察原生协作事件,
把实际启动、宿主失败和观察到的结果写入同一事件流。结果完成之后仍须由主
Agent 明确记录验收;宿主完成不代表证据已被采纳。

共享投影区分宿主观察、主 Agent 上报、混合来源和未知;仅全部决策均来自
宿主时 `host_attested` 为真。Codex 的失败协作项没有容量错误码,适配器保留
通用 `host_failed` 和禁止同 Turn 上报重试的规则,不从错误文字推断容量。
缺少原生事件仍是未知。部分宿主版本的持久化历史会遗漏协作项,因此不用于
重建活动。第三方宿主上报仍不会自动获得宿主核验标记。

## Lifecycle / 生命周期

Expand Down Expand Up @@ -106,3 +118,65 @@ for the parent validation of the underlying work.

已绑定的 LoopX delegation 仍以自身操作回执为权威。原生子代理上报不能代替
delegation 回执,也不能代替主 Agent 对工作结果的实际核验。

## Codex host qualification / Codex 宿主验证

The adapter belongs to the built-in Codex Turn host and the existing
`multi_subagent` receipt owner. It installs no scheduler and makes no additional
provider request. Feature-off Turns create no observer and retain their host
request/result contract. Ordinary CLI and Lark status use the same shared
projection as the dashboard; Lark has no native child configuration to change.

A resumed CLI invocation can receive only a new `wait` completion. The adapter
resolves its hashed child reference against the latest started host-observed
spawn or followup in the
same admitted Goal instance, coordinator and LoopX Turn, including receipts
outside the bounded status window. A spawn/followup item's terminal snapshot
belongs to its own stable decision and receivers; replaying an old spawn never
completes a later followup. A terminal wait first resolves the immutable binding
of its session, invocation, native item and child identity. Only its first
observation uses the latest child association. Each distinct wait retains a
hashed scalar reference on the existing result event; exact replay preserves
that original operation across restart, and changed outcomes or reassignment
are rejected. Multiple waits observing one result do not add operations,
launches, parent acceptance or quota. No raw host identifiers or content enter
the public activity projection.
It does not adopt coordinator reports or scan external host history. Spawn IDs
retain their existing child binding.
CLI followup and failed-call IDs use the Turn journal's durable `host_attempt`
plus the owned parent session and native item ID; app-server calls use their
native Turn ID. A real retry advances the journal attempt before launch;
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.

恢复 CLI 时可能只收到新的 `wait` 完成事件。适配器从同一已准入 Goal 实例、
主 Agent、LoopX Turn 的最近一次已启动宿主 spawn 或 followup 回执恢复关联,
覆盖状态窗口外的操作。spawn/followup 的完成快照只归属自身稳定决策和接收者;
重放旧启动事件不会完成后来的跟进任务。wait 首先恢复其会话、调用、原生项和
接收子 Agent 身份对应的首次结果关联;只有首次观察才使用最近操作。哈希关联
作为标量留在现有结果事件中,重启重放仍归原操作,结果冲突或重新归属会被拒绝。
多个 wait 观察同一结果不会增加操作、启动、主 Agent 验收或配额;公开活动投影
不暴露原始宿主标识或内容。
不采纳主 Agent 上报或扫描外部宿主历史。启动保留既有子代理绑定;CLI 跟进和
失败调用复用 Turn 日志持久化的 `host_attempt`、父会话和原生工具 ID,
app-server 使用其原生 Turn ID。真实重试在启动前递增尝试次数;同一绑定与
事件的重放仍幂等。直接调用已启用的 CLI 适配器须提供该尝试次数,关闭能力
时请求不变。关联只保留紧凑的子代理哈希引用,不进入公开活动行,
也不替代独立父任务验收。回执不增加原始提示或结果内容。

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
acceptance. Durable readback preserved one launch and one accepted result. The
native failure subtype remains unqualified: Codex emitted only `failed`, not
`agent_thread_limit_reached`. Synthetic typed capacity-report tests cover the
existing no-same-Turn retry rule without pretending that this host supplies that
error code. No live production Goal or raw child output is part of this evidence.

适配器复用内置 Codex Turn 宿主与 `multi_subagent` 回执所有者,不新增调度器
或模型调用。关闭能力时不创建观察器。CLI、Lark 状态和仪表板共用同一投影。
隔离的真实 Codex 0.142.5 验证观察到了一个成功启动、上限为 1 时第二次启动
失败、首个子任务完成以及独立的父任务验收。持久回读保留一个启动和一个
采纳结果。失败子类型仍有宿主协议缺口,不能声称已核验容量错误码。
2 changes: 2 additions & 0 deletions examples/personal-workspace-browser-smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env node
import {nativeChildActivityScenario} from "./personal-workspace-browser/native-child-activity.mjs";
import {conversationImageRequestScenario} from "./personal-workspace-browser/conversation-image-request.mjs";
// Isolated browser acceptance scenarios for the personal Agent workspace.

Expand Down Expand Up @@ -72,6 +73,7 @@ scenarioCatalog.push(turnStepsScenario);
scenarioCatalog.push(goalWorkMapScenario);
scenarioCatalog.push(performanceDiagnosisScenario);
scenarioCatalog.push(blockedNoticeSettingsScenario);
scenarioCatalog.push(nativeChildActivityScenario);
const requestedScenario = process.env.LOOPX_PERSONAL_WORKSPACE_SCENARIO;
const scenarios = requestedScenario
? scenarioCatalog.filter((scenario) => scenario.id === requestedScenario)
Expand Down
4 changes: 4 additions & 0 deletions examples/personal-workspace-browser/fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,10 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
});
}
const first = fixture.attention_queue?.items?.[0];
if (first && state.nativeChildActivity) {
first.project_asset ??= {owner: "codex", gate: "ready", next_action: first.recommended_action ?? "Review the fixture", stop_condition: "Fixture accepted"};
first.project_asset.native_child_activity = state.nativeChildActivity;
}
if (first) {
first.waiting_on = "user_or_controller";
const gateDecided = state.decidedGateTodoIds.has("todo-browser-user-gate");
Expand Down
55 changes: 55 additions & 0 deletions examples/personal-workspace-browser/native-child-activity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import {resolve} from "node:path";
import {outputDir} from "./fixture.mjs";
import {openWorkspacePage} from "./scenario-context.mjs";

export const nativeChildActivityScenario = {
id: "native-child-activity",
async run({browser, collectCoverage, url}) {
const coverageEntries = [];
for (const observation of ["host_observed", "coordinator_reported", "mixed", "unknown"]) {
const context = await openWorkspacePage(browser, url, {collectCoverage,
beforeGoto(api, page) {
api.goalSubagentConfigurationEnabled = true;
page.__loopxRuntime.goalSubagentConfigurations.set("loopx-meta",
{mode: "multi_subagent", spawn_allowed: true, max_children: 3, allowed_domains: []});
api.nativeChildActivity = {schema_version: "native_subagent_activity_v0",
observation, host_attested: observation === "host_observed", configured_limit: 3,
launched_count: 1, skipped_count: 0, capacity_rejected_count: 0,
host_failed_count: 1, parent_accepted_count: 1, turn_instance_id: "turn-browser-native"};
},
});
try {
const {page} = context;
await page.locator(".personal-goal-link").filter({hasText: "LoopX meta"}).click();
await page.getByRole("navigation", {name: "Goal 视图"}).getByRole("button", {name: "概览", exact: true}).click();
await page.getByRole("button", {name: "Goal 信息", exact: true}).click();
const drawer = page.locator('.personal-context-drawer[data-context-kind="goal"]');
await drawer.waitFor();
const activity = drawer.locator(".personal-native-child-activity");
if (observation === "unknown") {
assert.equal(await activity.count(), 0);
} else {
await activity.waitFor();
const text = await activity.innerText();
assert.match(text, /启动 1 次/);
assert.match(text, /主 Agent 验收 1 项/);
assert.match(text, observation === "host_observed" ? /宿主已观察/
: observation === "mixed" ? /部分决策未经宿主核验/ : /目前没有宿主核验/);
if (observation === "host_observed") {
await activity.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "native-child-desktop.png"), animations: "disabled"});
await page.setViewportSize({width: 390, height: 844});
await activity.scrollIntoViewIfNeeded();
assert(await activity.evaluate(el => el.scrollWidth <= el.clientWidth));
await page.screenshot({path: resolve(outputDir, "native-child-mobile.png"), animations: "disabled"});
}
}
assert.deepEqual(context.errors, []);
} finally {
coverageEntries.push(...await context.close());
}
}
return {coverageEntries, note: "Host, coordinator, mixed and unknown native child activity preserve provenance in the packaged Goal drawer."};
},
};
Loading
Loading