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
18 changes: 17 additions & 1 deletion docs/reference/goal-acceptance-observations.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,19 @@ and check** once. A configured contract needs only **Check delivery**. The
advanced JSON import remains available for technical migration; it is not a
prerequisite for this path.

Agent preparation uses the existing typed action preview store:
In an explicitly enabled Goal LoopX conversation whose native tool advertises
`prepare_check`, the Agent can call `loopx_collaboration` with
`action=prepare_check`, a stable `operation_id` and the existing acceptance
`document`. The host binds the Goal, registered preparing Agent and runtime;
the model cannot replace those identities or roots. Paused, stopped, obsolete
Turns and revoked execution configuration cannot prepare a plan. A conflicting
retry retains the original proposal and requires reconciliation.

This reuses the existing typed action preview store. It does not add a second
configuration owner or enable LoopX mode automatically. Native thread tool
schemas are fixed when created: an older thread may not advertise this action,
and an unfinished native Goal is never replaced just to add tools. The existing
registered-Agent CLI preparation remains available independently:

```bash
loopx --format json goal-acceptance prepare --goal-id example-goal \
Expand Down Expand Up @@ -122,6 +134,10 @@ No-plan/default-off behavior remains unchanged, and no Lark action is introduced
`prepare --execute` 只保存可审阅方案,不修改 Goal 规则、不执行检查。丢失回执先对账,
不能自动重跑;历史通过不能认证变化后的版本。交互次数从原始意图算起,前置缺失的意图或
授权不能从计数中省略。此入口沿用可信本机所有者边界,不证明远程身份、独立复核或实际采用。
显式启用的 Goal LoopX 会话在原生工具声明 `prepare_check` 时,可直接准备相同方案;
Goal、Agent 与运行目录由宿主固定,暂停、过期 Turn 或撤销配置后不能继续准备。
旧原生线程的工具声明不会自动升级,也不会为新增工具替换未完成线程;未声明时保留 CLI
准备路径。该入口不自动启用 LoopX、不配置或运行检查。

For advanced CLI configuration, inspect the current provider revision:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
import json
from pathlib import Path
import sys
from threading import Event
from types import SimpleNamespace

from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
from loopx.chat_action_store import ChatActionStore
from loopx.chat_actions import ChatActionService
from loopx.chat_loopx_mode import TOOL
from loopx.chat_runtime import ChatRuntimeController
from loopx.chat_store import ChatSessionStore
from loopx.control_plane.coordination.runtime_shadow import (
build_todo_runtime_shadow_projection,
)
Expand All @@ -32,8 +37,21 @@
domain="acceptance",
adapter_kind="generic_project_goal_v0",
state_file=str(state),
registered_agents=["agent-a"],
registered_agents=["coordinator", "agent-a"],
)
registered = json.loads(registry.read_text())
registered["goals"][0]["spawn_policy"] = {
"mode": "multi_subagent", "allowed": True, "max_children": 1,
"execution_config": ".loopx/config/delegations.json",
}
registry.write_text(json.dumps(registered))
config = project / ".loopx/config/delegations.json"
config.parent.mkdir(parents=True)
config.write_text(json.dumps({"schema_version": "loopx_local_delegation_v0", "bindings": [{
"id": "export", "agent_id": "agent-a", "todo_id": "todo_work", "requesters": ["coordinator"],
"workspace": str(project), "host_args": ["--host", "dsh"], "timeout_seconds": 60,
"output_refs": ["result.txt"],
}]}))
todo = {
"schema_version": "todo_item_v0",
"todo_id": "todo_work",
Expand Down Expand Up @@ -92,6 +110,23 @@
"bindings": [{"todo_id": "todo_work", "criterion_ids": ["export"]}],
}
(root / "owner-contract.json").write_text(json.dumps(document))
# Exercise the admitted native tool callback, not a pre-seeded CLI proposal.
# Native transport/model interpretation remains scripted; no Host is launched.
store = ChatSessionStore(runtime)
controller = ChatRuntimeController(store=store, codex_bin="unused", registry_path=registry)
session = store.create_session(goal_id="multi-agent-projection", agent_id="codex",
channel_id="goal.multi-agent-projection", upstream_thread_id="fixture", upstream_mode="chat",
adapter_kind="codex_app_server")
sid, turn = session["session_id"], "admitted-preparation-turn"
store.update_session(sid, active_turn_id=turn, loopx_mode={"enabled": True, "paused": False,
"settings": {"agent_id": "coordinator", "token_budget": 10000}})
adapter = SimpleNamespace(goal_driver=SimpleNamespace(stopped=Event()), session=SimpleNamespace())
lock = controller.loopx_mode.prepare(sid, turn, adapter, lambda *_: {}, lambda *_: None)
for operation in ("older-export-plan", "prepare-export-check"):
prepared = adapter.session.read_tool_handler(TOOL["name"], {
"action": "prepare_check", "operation_id": operation, "document": document})
if not prepared.get("ok"):
raise RuntimeError(f"Conversation preparation failed: {prepared}")
server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
server.registry_path, server.runtime_root = registry, runtime
server.runtime_root_override, server.verbose = str(runtime), False
Expand All @@ -101,11 +136,14 @@
server.action_service = ChatActionService(store=server.action_store, registry_path=registry)
print(
json.dumps(
{"port": server.server_port, "registry": str(registry), "runtime": str(runtime)}
{"port": server.server_port, "registry": str(registry), "runtime": str(runtime),
"prepared": prepared}
),
flush=True,
)
try:
server.serve_forever()
finally:
server.server_close()
lock.__exit__(None, None, None)
controller.close()
14 changes: 7 additions & 7 deletions examples/personal-workspace-browser/goal-acceptance-oracle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ try {
assert.equal(read().goal_acceptance_contract.enabled, false);
await page.locator(".personal-goal-link", {hasText: "Multi Agent Projection"}).click();
await page.getByRole("button", {name: "概览", exact: true}).click();
// The Agent does the technical preparation. The owner does not enter settings,
// inspect revisions, upload JSON or confirm the same facts in a second form.
cli("prepare", "--agent-id", "agent-a", "--document", resolve(root, "owner-contract.json"),
"--operation-id", "older-export-plan", "--execute");
const prepared = cli("prepare", "--agent-id", "agent-a", "--document", resolve(root, "owner-contract.json"),
"--operation-id", "prepare-export-check", "--execute");
// The admitted conversation tool prepares the plan through its real owner.
// The model is scripted; the owner does not repeat the facts in a JSON form.
const prepared = authority.prepared;
assert.equal(prepared.executed, false);
assert.equal(prepared.proposal.normalized_parameters.prepared_by, "coordinator");
assert.equal(read().goal_acceptance_contract.enabled, false, "preparation is not owner approval");
const panel = page.locator(".delivery-acceptance-contract");
const adopt = panel.getByRole("button", {name: "采用并检查", exact: true});
Expand All @@ -64,6 +62,7 @@ try {
await panel.getByText("当前版本已核对", {exact: true}).waitFor();
assert.equal(applyCount, 1, "one owner action adopts and checks the prepared plan");
assert.equal(read().goal_acceptance_contract.criteria[0].oracle.state, "current");
const firstOperation = read().goal_acceptance_contract.verification.operation_id;
assert.equal(await panel.locator('input[type="file"]').isVisible(), false, "technical import is optional");
await panel.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "oracle-current-desktop.png"), animations: "disabled"});
Expand All @@ -79,6 +78,7 @@ try {
assert.equal(await panel.getByText("产物检查通过", {exact: true}).count(), 0);
const oldOperation = read().goal_acceptance_contract.verification.operation_id;
assert.equal(applyCount, 1);
assert.equal(oldOperation, firstOperation, "readback does not execute a new check");
await run.click();
await panel.getByText("检查失败;修复交付物后可再次检查", {exact: true}).waitFor();
assert.notEqual(read().goal_acceptance_contract.verification.operation_id, oldOperation);
Expand All @@ -104,7 +104,7 @@ try {
const expected = "Failed to load resource: the server responded with a status of 409 (Conflict)";
const at = context.errors.indexOf(expected); if (at >= 0) context.errors.splice(at, 1);
assert.deepEqual(context.errors.filter(value => !value.includes("404")), []);
console.log(JSON.stringify({status: "PASS", boundary: "Packaged UI with actual disposable SQLite/HTTP/CLI; Agent preparation is scripted, not live model or installed App evidence", checks: ["automatic prepared-plan discovery", "one contextual owner action", "no mandatory JSON/revision form", "exact native readback", "changed output clears green without execution", "failure/recovery", "advanced import rejection", "disable/default-off", "390px", "keyboard/reduced-motion"]}));
console.log(JSON.stringify({status: "PASS", boundary: "Packaged UI and admitted conversation tool with actual disposable SQLite/HTTP/CLI; model/tool transport is scripted, not live model or installed App evidence", checks: ["native conversation preparation without execution", "automatic prepared-plan discovery", "one contextual owner action", "no mandatory JSON/revision form", "exact native readback", "changed output clears green without execution", "failure/recovery", "advanced import rejection", "disable/default-off", "390px", "keyboard/reduced-motion"]}));
} catch (error) {
if (context?.page) {
await mkdir(outputDir, {recursive: true});
Expand Down
52 changes: 47 additions & 5 deletions loopx/chat_loopx_mode.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Owner-local conversation control over native continuation and scoped delegation.

The host binds the sender and execution configuration; models cannot select
identities, commands or filesystem roots. Task acceptance remains with Delegations.
The host binds the sender and worker execution configuration; models cannot
select worker identities, commands or filesystem roots. A prepared delivery
check is only a proposal for the existing local-owner acceptance action.
Task acceptance remains with Delegations.
"""

from __future__ import annotations
Expand Down Expand Up @@ -42,19 +44,26 @@
"are accepted and the consumer has that exact uses input. Reading alone is not adoption. "
"Read/wait/resume use the original operation_id. Running is not failure; do not duplicate it. "
"After context loss, action=operations recovers this requester's durable work. Follow "
"next_cursor for more; unavailable means reconcile, not redispatch. Action=inspect with binding_id checks the actual Turn/profile before new dispatch; unknown availability is not readiness.",
"next_cursor for more; unavailable means reconcile, not redispatch. Action=inspect with binding_id checks the actual Turn/profile before new dispatch; unknown availability is not readiness. "
"Action=prepare_check takes a stable operation_id and an acceptance document prepared from the owner's requested outcome and current task/file facts. "
"The host supplies the Goal and preparing Agent. It saves only a reviewable goal.acceptance.check proposal; it does not configure acceptance or run commands. "
"The local owner can use that plan beside the delivery in the workspace.",
"inputSchema": {
"type": "object",
"additionalProperties": False,
"properties": {
"action": {
"type": "string",
"enum": ["bindings", "operations", "inspect", "start", "read", "wait", "resume", "revalidate", "adopt", "messages"],
"enum": ["bindings", "operations", "inspect", "start", "read", "wait", "resume", "revalidate", "adopt", "messages", "prepare_check"],
},
"binding_id": {"type": "string"},
"operation_id": {"type": "string"},
"consumer_operation_id": {"type": "string"},
"brief": {"type": "object"},
"document": {
"type": "object",
"description": "Existing acceptance document: objective (string), non_goals (strings), scope ({kind: selected_work, todo_ids: existing advancement Todo ids} or {kind: all_advancement}), criteria (id, description, validation_argv string array, optional validation_timeout_seconds), bindings (todo_id, criterion_ids). Checks use the registered Goal workspace or its pinned oracle snapshot only after local-owner apply. Use known requirements and approved scope; do not invent an oracle or authorization.",
},
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
"cursor": {"type": "string"},
},
Expand All @@ -72,6 +81,9 @@
"For task_failure, repair the original artifact or validation environment before action=revalidate; it reruns checks without Host work. "
"When current_use.state=unavailable, repair the declared source or receiver input and read again; do not propagate those artifacts as current facts. Read action=messages between work steps for owner inbox additions. "
"Do not claim the whole canonical Goal is complete. Keep independent analysis substantive. "
"When the owner requests delivery checks and prepare_check is advertised by your tool, prepare the existing acceptance document from the known intent, tasks and files; "
"do not ask the owner to assemble JSON or internal identifiers. Reuse stated criteria and scope, and ask only for an unresolved consequential fact or authority. "
"Use prepare_check to save the plan, then explain its purpose and command effect. Preparation never approves or executes it, and a check never establishes independent review or adoption. "
"Before completing or blocking, return a self-contained report with substantive accepted findings, "
"exact artifact hashes and remaining gaps; do not require readers to reconstruct earlier streamed replies. "
"A missing report-writing grant must not prevent returning the complete analysis in this conversation. "
Expand Down Expand Up @@ -834,19 +846,49 @@ def dispatch(name, arguments):
"operation_id",
"consumer_operation_id",
"brief",
"document",
"limit",
"cursor",
}:
raise ValueError("invalid collaboration arguments")
action = arguments.get("action")
if action != "prepare_check" and "document" in arguments:
raise ValueError("document is only valid for prepare_check")
if action != "operations" and ("limit" in arguments or "cursor" in arguments):
raise ValueError("pagination is only valid for operations")
if action == "operations" and set(arguments) - {"action", "limit", "cursor"}:
raise ValueError("operations reads a page; use read to select an operation")
if action != "adopt" and "consumer_operation_id" in arguments:
raise ValueError("consumer_operation_id is only valid for adopt")
operation_id = arguments.get("operation_id", "")
if action == "messages":
if action == "prepare_check":
if (set(arguments) != {"action", "operation_id", "document"}
or not isinstance(operation_id, str) or not operation_id.strip()
or not isinstance(arguments["document"], dict)):
raise ValueError("prepare_check requires only a stable operation_id and document")
from .control_plane.goals.acceptance_actions import prepare_acceptance_check
from .chat_action_store import ActionConflictError

try:
result = prepare_acceptance_check(
registry_path=self.controller.registry_path,
runtime_root=str(self.store.root.parent), goal_id=current["goal_id"],
agent_id=settings["agent_id"], document=arguments["document"],
operation_id=operation_id, execute=True,
)
except ActionConflictError:
return {
"ok": False,
"error": "collaboration_request_rejected",
"next_action": "Read the saved check plan and current Goal basis. Reuse its operation id only for an unchanged request; a new intent needs a new operation id.",
}
except ValueError as exc:
return {
"ok": False,
"error": "collaboration_request_rejected",
"next_action": f"The acceptance owner rejected the document ({exc}). Check its objective, scope, criteria, bindings and current task/file facts against the acceptance document contract; do not change authority to make it pass.",
}
elif action == "messages":
rows = [
r
for r in self.store.loopx_ingress(session_id)
Expand Down
Loading
Loading