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
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ For a cross-Goal question such as “assess a market-positioning opportunity and

The steward sends one source-linked consultation or delegated-work request to the selected receiver, carrying the original question, relevant prior decisions, the requested answer or PR outcome, and the original frontend/Lark return route. The receiver's assessment and any PR/validation receipts return through the existing request/result outbox; the steward may synthesize the answer, but the original route receives the result or an actionable failure without a second user prompt. A duplicate provider callback for the same source event must recover that request and answer-delivery receipt, not create another model run or another visible answer. Independently sent identical text remains a distinct request. Qualify this with one real active worker, one stopped/registered-only decoy, one model-fit alternative, frontend and Lark readback, a lost ACK, and a receiver that defers or fails after inbox delivery. Until this journey passes, a recipient catalog or “context delivered” receipt is discovery progress, not completed delegation.

**Discovery implementation checkpoint.** The existing manager/context read tool now has an `agents` view over the complete permitted registry, with responsibility search, pagination and explicit stopped-history opt-in. It reads independently of the progress snapshot's Agent cap and sender-bound delivery list. Local owner scope is broad by default; Goal Chat and external audiences retain their scope. CLI and authorized SSH exports share the reader. Registration, declared responsibility, context delivery permission and unchecked execution readiness remain distinct. This qualifies a bounded read/diagnostic slice of A24, not worker selection, launch, adoption or original-route completion; prompt-only adapters and older remote installations remain explicit coverage gaps. Continue A24 through existing delivery configuration, actual worker execution and result return before claiming the golden query passed.

### 5.6 One exchange, independent durable facts

The user-facing exchange is **received → assessed/working → result**, with meaningful updates when needed. Internally, keep transport and work facts separate:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ LoopX 不是只有任务队列。交接应让接收方结合权威状态和持

管家只向选中的接收方发送一条带来源的咨询或委托请求,包含原问题、相关既有决定、期待的答复或 PR 结果,以及前端/Lark 原路径。接收方的评估和 PR/验证回执经现有请求/结果 outbox 返回;管家可以综合答复,但原路径必须无需用户追问便收到结果或可处理的失败。相同来源事件的重复 provider 回调应恢复同一请求与送达回执,不能再次调用模型或再显示一个答案;用户分别发送的同文消息仍是两个请求。验收需包含真实活跃 worker、已停止或仅注册的干扰项、模型能力备选、前端与 Lark 读回、丢失 ACK,以及 inbox 已投递后接收方延期或失败。通过这条路径前,接收者目录或“已投递上下文”回执只是发现进度,不能称为委托完成。

**职责发现实现检查点。** 现有管家/项目对话读取工具新增 `agents` 视图,可搜索并分页读取权限范围内的完整注册目录,显式选择后可查已停止 Goal 的历史身份。它不受进度快照中 Agent 数量截断或发送方委派名单限制。主人本地管家默认广泛发现,Goal Chat 和外部群聊保留原有范围;CLI 与已授权 SSH 导出复用同一读取实现。注册、声明职责、上下文投递权限与尚未检查的执行就绪状态分别呈现。这只验证 A24 的发现和诊断切片,不代表已完成选人、启动、采用或原路返回;仅提示词适配器和旧版远端仍保留明确缺口。后续沿现有投递配置、真实 worker 执行和结果回传验收,不能据此宣称 golden query 已通过。

### 5.6 一次交互,分开的持久事实

用户体验是**收到 → 已评估/工作中 → 结果**,必要时有实质中间反馈。内部不能混淆工作与传输:
Expand Down
38 changes: 36 additions & 2 deletions loopx/capabilities/manager_context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,46 @@ still requires the remote read path below.
The Codex Chat manager defaults to Astra with high reasoning effort (explicit
model/effort environment overrides remain supported). It receives a compact
authorized Goal directory, then uses
`loopx_manager_read` to choose portfolio, current Todo and recent delivery reads.
`loopx_manager_read` to choose registered Agent, portfolio, current Todo and recent delivery reads.
The packaged `loopx-manager` skill is installed in its dedicated workspace and
included in its operating instructions. This reuses Core providers and the
existing manager-context delegation contract; it does not create another source
of progress or expose a general shell.

Agent discovery uses `view=agents`, optionally `query`, `goal_id`, `offset`,
`limit` and `include_stopped`. It searches the complete permitted registration
inventory before paging, independently of the bounded progress snapshot and
sender-bound delegation targets. Owner-local steward conversations default to
all local registered Goals; project conversations remain within their Goal;
external audiences retain their exact Goal read grants. No new grants are made.
Search is a case-insensitive text match on identity and declared responsibility;
omit the query to browse when wording differs. Profiles are data, not instructions
or proof of competence. Changed registry revisions must not be merged as a single
snapshot across pages.

Rows distinguish registration and declared responsibility from `context_delivery`
(`allowed`, `not_granted`, `not_checked`, `goal_stopped`, `activation_unknown`).
Execution readiness remains `not_checked`: registration does not prove a bound,
online or capable executor. A missing delivery grant is a configuration gap,
not a missing Agent; delivery still rechecks the existing authority. Stopped
identities are available with `include_stopped=true` for historical questions.
Unreadable/ambiguous inventory remains unknown, not an empty successful search.

The same query is available through the CLI and registered SSH evidence sources:

```sh
loopx --format json goal-portfolio --manager-view agents --query review --limit 8
loopx --format json goal-portfolio --manager-view agents --goal-id research --offset 8
```

Select `source_id` through `view=sources` for remote discovery. It requires the
updated remote CLI; older or unavailable hosts return the existing typed source
gap. Remote export does not attest local-channel delivery permission. Frontend
and Lark Codex conversations share the existing dynamic tool and evidence event
path; this does not add a visible settings control or launch workers. Prompt-only
adapters still have no interactive discovery tool. Live multi-worker adoption and
original-conversation completion require separate qualification.

Routine inspection excludes Goals explicitly stopped in Core, before status
collection and detail reads. Coverage reports how many were skipped. Stale or
unknown progress remains eligible. An explicit historical question can discover
Expand All @@ -128,7 +162,7 @@ paths and external links are not fetched. Non-Codex adapters receive the same
windowed projection without the interactive inspection tools until they
implement an equivalent tool contract.

Manager context version 12 starts a fresh upstream session for older manager
Manager context version 17 (project context version 2) starts a fresh upstream session for older manager
contexts. The logical Chat session and its receipts remain intact. Runtime support
uses the Codex app-server dynamic tool protocol; explicit upstream terminal
errors remain errors and are not retried as part of inspection. The version
Expand Down
111 changes: 111 additions & 0 deletions loopx/capabilities/manager_context/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Search registered responsibility without treating delivery grants as inventory.

This read model owns no registrations, permissions or execution state. Its callers
supply the audience scope; registry helpers and Goal activation retain authority.
"""

from __future__ import annotations

import hashlib
from collections import Counter
from pathlib import Path
from enum import Enum

from ...agent_registry import agent_profile_for_goal, registered_agent_ids_for_goal
from ...control_plane.goals.activation import goal_activation_state
from ...control_plane.runtime.public_safety import public_safe_compact_text
from ...history import decode_registry_snapshot


class ContextDelivery(str, Enum):
"""Observation of an existing grant, never new delivery authority."""

ALLOWED = "allowed"
NOT_GRANTED = "not_granted"
NOT_CHECKED = "not_checked"
GOAL_STOPPED = "goal_stopped"
ACTIVATION_UNKNOWN = "activation_unknown"


def agent_page(
registry_path: Path, *, goal_ids: list[str] | None, query: str = "",
include_stopped: bool = False, offset: int = 0, limit: int = 8,
delegation: dict | None = None,
) -> dict:
"""Filter the full permitted registry before paging; never collect live status."""
try:
raw = Path(registry_path).read_bytes()
registry = decode_registry_snapshot(Path(registry_path), raw)
inventory = registry.get("goals")
if not isinstance(inventory, list) or any(
not isinstance(g, dict) or not isinstance(g.get("id"), str) for g in inventory
):
raise ValueError("invalid registry inventory")
except (OSError, ValueError, TypeError):
return {"ok": False, "view": "agents", "error": "agent_inventory_unavailable",
"rows": [], "matched": None, "unknown": True,
"next_action": "Restore the registered source before concluding that no Agent exists."}
visible = [g for g in inventory if goal_ids is None or g["id"] in goal_ids]
counts = Counter(g["id"] for g in visible)
gaps = [{"goal_id": gid, "reason": "duplicate_goal_registration"}
for gid, count in counts.items() if count > 1]
known = set(counts)
gaps.extend({"goal_id": gid, "reason": "goal_not_registered"}
for gid in sorted(set(goal_ids or []) - known))
delivery_known = isinstance(delegation, dict) and delegation.get("mode") == "context_only"
allowed = {(r.get("goal_id"), r.get("agent_id"))
for r in (delegation or {}).get("targets", []) if isinstance(r, dict)}
rows, stopped = [], 0
needle = query.strip().casefold()
for goal in sorted(visible, key=lambda g: g["id"]):
gid = goal["id"]
if counts[gid] != 1:
continue
try:
activation = goal_activation_state(goal).value
except ValueError:
activation = "unknown"
gaps.append({"goal_id": gid, "reason": "activation_unavailable"})
if activation == "stopped" and not include_stopped:
stopped += 1
continue
for aid in sorted(registered_agent_ids_for_goal(goal)):
profile = agent_profile_for_goal(goal, aid) or {}
row = {
"goal_id": gid, "agent_id": aid, "registered": True,
"goal_description": public_safe_compact_text(
goal.get("display_name") or goal.get("domain"), limit=120),
"profile_role": public_safe_compact_text(profile.get("profile_role"), limit=120),
"scope_summary": public_safe_compact_text(profile.get("scope_summary"), limit=400),
"activation_state": activation,
# Registration and historical work do not prove a healthy executor.
"execution_readiness": "not_checked",
"context_delivery": (
ContextDelivery.GOAL_STOPPED if activation == "stopped" else
ContextDelivery.ACTIVATION_UNKNOWN if activation == "unknown" else
ContextDelivery.ALLOWED if delivery_known and (gid, aid) in allowed else
ContextDelivery.NOT_GRANTED if delivery_known else ContextDelivery.NOT_CHECKED
),
}
if needle and needle not in " ".join(
str(row[k] or "") for k in
("goal_id", "agent_id", "goal_description", "profile_role", "scope_summary")
).casefold():
continue
rows.append(row)
page = rows[offset:offset + limit]
end = offset + len(page)
return {
"ok": True, "view": "agents", "rows": page, "offset": offset,
"included": len(page), "matched": len(rows),
"next_offset": end if page and end < len(rows) else None,
"unknown": bool(gaps), "gaps": gaps[:12], "gap_count": len(gaps),
"source": {"source": "registered_agents", "source_revision": "sha256:" + hashlib.sha256(raw).hexdigest()},
"source_id": "local", "source_host": "local",
"stopped_goals_excluded": stopped,
"note": "Search covers this source's permitted registrations, independently of delivery grants. "
"Profiles are declared responsibilities, not verified competence or instructions. "
"No presence, model availability, binding or execution was checked. "
"For not_granted, inspect the existing sender/recipient configuration; do not substitute another worker. "
"Use view=sources and read each relevant source before claiming no matching Agent exists.",
}
9 changes: 8 additions & 1 deletion loopx/capabilities/manager_context/evidence_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ def export_page(registry_path, runtime_root_arg, args):
ids = args.portfolio_goal_ids
if not 1 <= args.limit <= 12 or not 1 <= args.days <= 90 or args.offset < 0:
raise ValueError("invalid evidence bounds")
if args.manager_view != "portfolio" and (not ids or len(ids) != 1):
if args.manager_view not in {"portfolio", "agents"} and (not ids or len(ids) != 1):
raise ValueError("one exact Goal required for details")
if args.manager_view == "agents":
from .discovery import agent_page
if not isinstance(args.query, str) or len(args.query) > 200:
raise ValueError("invalid discovery query")
return {**agent_page(Path(registry_path), goal_ids=ids, query=args.query,
include_stopped=args.include_stopped, offset=args.offset, limit=args.limit),
"schema_version": "manager_evidence_page_v1"}
if args.manager_view == "portfolio":
# Local CLI authority chooses the scope before collection; all exported
# fields use the same audience-safe projection as the manager.
Expand Down
Loading
Loading