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
5 changes: 3 additions & 2 deletions apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
"smoke:goal-order": "tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --skipLibCheck --strict --outDir node_modules/.cache/loopx-goal-order src/features/personal-workspace/goal-order.ts && node src/features/personal-workspace/goal-order.test.mjs",
"smoke:home-browser": "node ../../../examples/dashboard-home-browser-smoke.mjs",
"smoke:home-route": "rm -rf /tmp/loopx-home-route-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-home-route-smoke smoke/home-route-smoke.ts && node /tmp/loopx-home-route-smoke/home-route-smoke.js",
"smoke:personal-workspace": "npm run smoke:goal-order && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:delegation-preflight": "rm -rf node_modules/.cache/loopx-delegation-preflight-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --jsx react-jsx --skipLibCheck --strict --rootDir . --outDir node_modules/.cache/loopx-delegation-preflight-smoke smoke/delegation-preflight-smoke.tsx src/data/delegation-preflight.ts src/features/personal-workspace/delegation-preflight-status.tsx && node node_modules/.cache/loopx-delegation-preflight-smoke/smoke/delegation-preflight-smoke.js",
"smoke:personal-workspace": "npm run smoke:goal-order && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-router": "rm -rf /tmp/loopx-personal-workspace-router-smoke && tsc --ignoreConfig --target ES2022 --module commonjs --moduleResolution node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-personal-workspace-router-smoke smoke/personal-workspace-router-smoke.ts src/features/personal-workspace/personal-workspace-router.ts && node /tmp/loopx-personal-workspace-router-smoke/smoke/personal-workspace-router-smoke.js",
"smoke:presentation-surface-schema": "rm -rf /tmp/loopx-presentation-surface-schema-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --resolveJsonModule --esModuleInterop --outDir /tmp/loopx-presentation-surface-schema-smoke smoke/presentation-surface-schema-smoke.ts src/data/status.ts src/data/decision-research.ts src/data/goal-channel-frontstage.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-presentation-surface-schema-smoke/apps/presentation/dashboard/smoke/presentation-surface-schema-smoke.js",
"smoke:projection-localization": "rm -rf /tmp/loopx-projection-localization-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck --strict --outDir /tmp/loopx-projection-localization-smoke smoke/projection-localization-smoke.ts src/features/personal-workspace/projection-localization.ts && node /tmp/loopx-projection-localization-smoke/smoke/projection-localization-smoke.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {renderToStaticMarkup} from "react-dom/server";
import type {DelegationPreflight} from "../src/data/delegation-preflight.js";
import {DelegationPreflightStatus} from "../src/features/personal-workspace/delegation-preflight-status.js";

const unavailable: DelegationPreflight = {
state: "authority_unavailable",
turn_eligible: false,
acceptance_ready: false,
turn_route: null,
authority_ready: false,
authority_reason: "Goal acceptance requires an existing canonical authority",
executor: null,
effects: {
host_invoked: false,
state_written: false,
quota_spent: false,
scheduler_acknowledged: false,
},
};

for (const [zh, expectedLabel, expectedBoundary] of [
[true, "缺少规范权限状态", "未检查或启动执行器"],
[false, "Canonical authority unavailable", "No executor was inspected or launched"],
] as const) {
const html = renderToStaticMarkup(<DelegationPreflightStatus check={unavailable} zh={zh}/>);
if (!html.includes(expectedLabel)) throw new Error(`missing state label: ${expectedLabel}`);
if (!html.includes("Goal acceptance requires an existing canonical authority")) throw new Error("missing authority reason");
if (!html.includes(expectedBoundary)) throw new Error(`missing boundary copy: ${expectedBoundary}`);
if (/Local launch prerequisites met|本机启动条件已满足/.test(html)) throw new Error("rendered launchable copy for unavailable authority");
}
if (Object.values(unavailable.effects).some(Boolean)) throw new Error("authority-unavailable response must report zero effects");

console.log("delegation authority-unavailable preflight smoke passed");
7 changes: 2 additions & 5 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type TodoApplyResult,
type TodoPreview,
} from "./chat-model.js";
import type {DelegationPreflight} from "./delegation-preflight.js";

const configuredChatOrigin = String(import.meta.env?.VITE_LOOPX_CHAT_ORIGIN ?? "")
.trim()
Expand Down Expand Up @@ -895,11 +896,7 @@ export type DelegationInventory = {
artifacts?: Array<{ref: string; sha256: string}>}>;
has_more: boolean; next_cursor: string | null; page_readback_complete: boolean;
};
export type DelegationPreflight = {
state: "turn_blocked" | "acceptance_unavailable" | "runtime_unavailable" | "runtime_unverified" | "launchable";
turn_eligible: boolean; acceptance_ready: boolean; turn_route: string;
executor: {host: string; available: boolean | null; reason: string | null; profile: string | null};
};
export type {DelegationPreflight} from "./delegation-preflight.js";
export function fetchLoopXTeamWork(sessionId: string, cursor?: string) {
return requestJson<DelegationInventory>(`/api/chat/sessions/${sessionId}/loopx`, {
method: "POST", body: JSON.stringify({operation: "operations", limit: 10, ...(cursor ? {cursor} : {})}),
Expand Down
10 changes: 10 additions & 0 deletions apps/presentation/dashboard/src/data/delegation-preflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type DelegationPreflight = {
state: "authority_unavailable" | "turn_blocked" | "acceptance_unavailable" | "runtime_unavailable" | "runtime_unverified" | "launchable";
turn_eligible: boolean;
acceptance_ready: boolean;
turn_route: string | null;
authority_ready: boolean;
authority_reason: string | null;
executor: {host: string; available: boolean | null; reason: string | null; profile: string | null} | null;
effects: {host_invoked: boolean; state_written: boolean; quota_spent: boolean; scheduler_acknowledged: boolean};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {DelegationPreflight} from "../../data/delegation-preflight.js";

const labels: Record<DelegationPreflight["state"], {en: string; zh: string}> = {
authority_unavailable: {en: "Canonical authority unavailable", zh: "缺少规范权限状态"},
turn_blocked: {en: "Task admission blocked", zh: "当前任务未获准执行"},
acceptance_unavailable: {en: "Acceptance binding unavailable", zh: "缺少有效验收绑定"},
runtime_unavailable: {en: "Runtime unavailable", zh: "运行时不可用"},
runtime_unverified: {en: "Runtime availability unverified", zh: "运行时可用性尚未验证"},
launchable: {en: "Local launch prerequisites met", zh: "本机启动条件已满足"},
};

export function DelegationPreflightStatus({check, zh}: {check: DelegationPreflight; zh: boolean}) {
const detail = check.executor
? [check.executor.host, check.executor.reason].filter(Boolean).join(" · ")
: check.authority_reason;
const disclaimer = check.state === "authority_unavailable"
? (zh ? "未检查或启动执行器" : "No executor was inspected or launched")
: (zh ? "不代表正在执行" : "Does not mean executing");
return <p role="status">
{zh ? labels[check.state].zh : labels[check.state].en}
{detail ? ` · ${detail}` : ""}
{` · ${disclaimer}`}
</p>;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useEffect, useRef, useState} from "react";
import {fetchLoopXTeamWork, inspectLoopXMember, type DelegationInventory, type DelegationPreflight} from "../../data/chat";
import {DelegationPreflightStatus} from "./delegation-preflight-status";

type Member = {id: string; agent_id: string; todo_id: string};

Expand Down Expand Up @@ -36,11 +37,6 @@ export function GoalTeamWork({sessionId, members, zh}: {sessionId: string; membe
if (current === generation.current) setError(failure instanceof Error ? failure.message : String(failure));
} finally {if (current === generation.current) setBusy(false);}
}
const labels: Record<DelegationPreflight["state"], string> = zh ? {
turn_blocked: "当前任务未获准执行", acceptance_unavailable: "缺少有效验收绑定",
runtime_unavailable: "运行时不可用", runtime_unverified: "运行时可用性尚未验证", launchable: "本机启动条件已满足",
} : {turn_blocked: "Task admission blocked", acceptance_unavailable: "Acceptance binding unavailable",
runtime_unavailable: "Runtime unavailable", runtime_unverified: "Runtime availability unverified", launchable: "Local launch prerequisites met"};
const stateLabel = (row: DelegationInventory["items"][number]) => {
if (row.status === "unavailable") return zh ? "无法核验" : "Unavailable";
if (row.status === "accepted") return zh ? "已通过当前验收" : "Currently accepted";
Expand All @@ -57,11 +53,8 @@ export function GoalTeamWork({sessionId, members, zh}: {sessionId: string; membe
<ul className="goal-team-bindings">{members.map(member => <li key={member.id}>
<div><strong>{member.agent_id}</strong>
<button type="button" disabled={busy} onClick={() => void inspect(member.id)}>{zh ? "检查启动条件" : "Check prerequisites"}</button></div>
<details><summary>{zh ? "任务与执行配置" : "Task and execution details"}</summary><code>{member.todo_id}</code>{checks[member.id]?.executor.profile ? <code>{checks[member.id].executor.profile}</code> : null}</details>
{checks[member.id] ? <p role="status">{labels[checks[member.id].state] ?? (zh ? "状态未知" : "Unknown")}
{" · "}{checks[member.id].executor.host}{checks[member.id].executor.reason ? ` · ${checks[member.id].executor.reason}` : ""}

{" · "}{zh ? "不代表正在执行" : "Does not mean executing"}</p> : null}
<details><summary>{zh ? "任务与执行配置" : "Task and execution details"}</summary><code>{member.todo_id}</code>{checks[member.id]?.executor?.profile ? <code>{checks[member.id]?.executor?.profile}</code> : null}</details>
{checks[member.id] ? <DelegationPreflightStatus check={checks[member.id]} zh={zh}/> : null}
</li>)}</ul>
<div className="goal-team-work-actions"><strong>{zh ? "此协调身份的持久工作" : "Durable work for this coordinator"}</strong>
<button type="button" disabled={busy} onClick={() => {setChecks({}); void read();}}>{zh ? "重新核验" : "Refresh"}</button>
Expand Down
25 changes: 20 additions & 5 deletions loopx/collaboration_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,21 @@ def inspect(self, binding_id: str) -> dict:
binding = self.binding(binding_id, require_active=True)
if not Path(binding["workspace"]).is_dir():
raise ValueError("delegation workspace unavailable")
acceptance = inspect_goal_acceptance(registry_path=self.registry, goal_id=self.goal_id,
runtime_root=str(self.root))
files_current = goal_task_validation_files_current(registry_path=self.registry,
runtime_root=str(self.root), goal_id=self.goal_id, agent_id=binding["agent_id"], todo_id=binding["todo_id"])
try:
acceptance = inspect_goal_acceptance(registry_path=self.registry, goal_id=self.goal_id,
runtime_root=str(self.root))
files_current = goal_task_validation_files_current(registry_path=self.registry,
runtime_root=str(self.root), goal_id=self.goal_id, agent_id=binding["agent_id"], todo_id=binding["todo_id"])
except (OSError, ValueError) as exc:
# Authority admission is a readiness observation, not a reason for
# inspection to invent a provider launch or collapse into a raw CLI error.
if self.binding(binding_id, require_active=True) != binding:
raise ValueError("delegation preflight source changed; retry inspection")
return effect_runtime_result("collaboration.delegation.preflight", {
"binding": {key: binding[key] for key in ("id", "agent_id", "todo_id")},
"authority": {"ready": False, "reason": str(exc)},
"preview": None, "acceptance": None, "validation_files_current": False,
})
operation = "inspect-" + _hash(binding_id)[:32]
arguments = ["turn", "run-once", "--goal-id", self.goal_id,
"--agent-id", binding["agent_id"], "--todo-id", binding["todo_id"],
Expand Down Expand Up @@ -210,6 +221,7 @@ def inspect(self, binding_id: str) -> dict:
if row.get("todo_id") == binding["todo_id"]), None)
return effect_runtime_result("collaboration.delegation.preflight", {
"binding": {key: binding[key] for key in ("id", "agent_id", "todo_id")},
"authority": {"ready": True, "reason": None},
"preview": preview, "acceptance": task, "validation_files_current": files_current,
})

Expand Down Expand Up @@ -405,7 +417,10 @@ def _execute(self, path: Path, row: dict, binding: dict) -> None:
"--host", selected_host,
"--iteration-context", iteration_context,
"--include-transaction-detail")
row["turn_key"] = plan["transaction"]["turn_key"]
decision = effect_runtime_result("collaboration.delegation.turn_plan", {"plan": plan})
if decision["state"] != "planned":
raise ValueError(f"delegation Turn plan rejected: {decision['reason']}")
row["turn_key"] = decision["turn_key"]
self._observe(path, row, "running")
try:
if row["status"] == "running":
Expand Down
48 changes: 47 additions & 1 deletion loopx/control_plane/collaboration/delegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,56 @@ export function selectDelegationBinding(params: JsonObject): JsonObject {

type Observation = "prepared" | "running" | "turn_returned" | "accepted" | "rejected";

function boundedReason(value: unknown, fallback: string): string {
if (typeof value !== "string") return fallback;
const reason = value.trim();
return reason.length > 0 ? reason.slice(0, 4096) : fallback;
}

/** Preserve a rejected Turn plan as data before the host adapter reads its transaction. */
export function delegationTurnPlanDecision(params: JsonObject): JsonObject {
const plan = requireJsonObject(params.plan, "Turn plan");
if (plan.ok !== true) return {
schema_version: "loopx_delegation_turn_plan_decision_v0",
state: "rejected",
turn_key: null,
reason: boundedReason(plan.error ?? plan.reason, "Turn plan rejected without a reason"),
};
const transaction = requireJsonObject(plan.transaction, "Turn plan transaction");
requireThat(typeof transaction.turn_key === "string"
&& /^sha256:[a-f0-9]{64}$/.test(transaction.turn_key), "Turn plan transaction requires a valid turn_key");
return {
schema_version: "loopx_delegation_turn_plan_decision_v0",
state: "planned",
turn_key: transaction.turn_key,
reason: null,
};
}

/** Read the actual dry-run route/profile, never infer readiness from assignment. */
export function delegationPreflight(params: JsonObject): JsonObject {
const binding = requireJsonObject(params.binding, "binding identity");
requireThat([binding.id, binding.agent_id, binding.todo_id].every(text), "binding identities required");
const authority = params.authority === undefined
? {ready: true, reason: null}
: requireJsonObject(params.authority, "canonical authority readiness");
requireThat(typeof authority.ready === "boolean", "canonical authority readiness required");
if (authority.ready === false) {
requireThat(params.preview === null && params.acceptance === null
&& params.validation_files_current === false,
"unavailable authority cannot claim a Turn preview or task acceptance");
const effects = {host_invoked: false, state_written: false, quota_spent: false,
scheduler_acknowledged: false};
return {
schema_version: "loopx_delegation_preflight_v0", binding,
state: "authority_unavailable", turn_eligible: false, turn_route: null,
acceptance_ready: false, authority_ready: false,
authority_reason: boundedReason(authority.reason, "canonical authority unavailable"),
executor: null, effects,
note: "Canonical authority is unavailable, so no Turn or provider was inspected or launched. "
+ "Promote or repair authority explicitly before retrying; inspection never promotes a provider.",
};
}
const preview = requireJsonObject(params.preview, "Turn preview");
const effects = requireJsonObject(preview.effects, "preview effects");
requireThat(preview.dry_run === true && preview.status === "preview"
Expand All @@ -56,7 +102,7 @@ export function delegationPreflight(params: JsonObject): JsonObject {
return {
schema_version: "loopx_delegation_preflight_v0", binding,
state, turn_eligible: eligible, turn_route: route.kind,
acceptance_ready: pinned,
acceptance_ready: pinned, authority_ready: true, authority_reason: null,
executor: {host: executor.executor, available: executor.available,
reason: executor.unavailable_reason, profile: executor.execution_profile},
effects,
Expand Down
3 changes: 2 additions & 1 deletion loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {projectTodoSummaryLanes, projectLegacyTodoWorkCounts} from "./todos/summary_lanes.ts";
import {delegationInventoryItem, delegationInventoryQuery, delegationPreflight, selectDelegationBinding, transitionDelegationObservation} from "./collaboration/delegation.ts";
import {delegationInventoryItem, delegationInventoryQuery, delegationPreflight, delegationTurnPlanDecision, selectDelegationBinding, transitionDelegationObservation} from "./collaboration/delegation.ts";
import {planChatMode} from "./collaboration/chat_mode.ts";
import {resolveConversationScope} from "./collaboration/conversation_scope.ts";
import {previewTeamPlan, planTeamTransaction, teamTransactionIdentity} from "./work_items/team_plan.ts";
Expand Down Expand Up @@ -634,6 +634,7 @@ export function createEffectRuntimeHandlers(
],
["collaboration.delegation.binding", selectDelegationBinding],
["collaboration.delegation.preflight", delegationPreflight],
["collaboration.delegation.turn_plan", delegationTurnPlanDecision],
["collaboration.delegation.inventory_query", delegationInventoryQuery],
["collaboration.delegation.inventory_item", delegationInventoryItem],
["collaboration.chat_mode", planChatMode],
Expand Down
Loading
Loading