Skip to content

Commit af3ed54

Browse files
authored
Merge pull request #5752 from jackie-cqz/feat/zcode-host-diagnostics
feat(zcode): add host diagnostics and managed native Goal control
2 parents b77f634 + 47f233d commit af3ed54

96 files changed

Lines changed: 6439 additions & 289 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/python-tests.yml‎

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ jobs:
242242
npm run smoke:personal-workspace-packaged
243243
npm run smoke:chat-turn-acceptance-retry
244244
npm run smoke:chat-upgrade
245+
- name: Qualify packaged ZCode recovery against its persistent owner
246+
env:
247+
LOOPX_USAGE_PING: "0"
248+
DO_NOT_TRACK: "1"
249+
run: |
250+
python -m pip install --disable-pip-version-check -e ".[test]"
251+
npm ci --ignore-scripts
252+
node examples/zcode-persistent-browser-smoke.mjs
245253
246254
kernel-static-checks:
247255
needs: [changes, chat-bundle]
@@ -398,11 +406,15 @@ jobs:
398406
if-no-files-found: error
399407
retention-days: 3
400408

401-
dashboard-acceptance:
409+
dashboard-browser:
402410
needs: [changes, chat-bundle]
403411
if: needs.changes.outputs.core_tests == 'true'
404412
runs-on: ubuntu-latest
405413
timeout-minutes: 25
414+
strategy:
415+
fail-fast: false
416+
matrix:
417+
shard: [1, 2, 3]
406418
steps:
407419
- name: Check out repository
408420
uses: actions/checkout@v7
@@ -443,16 +455,47 @@ jobs:
443455

444456
- name: Measure Dashboard unit and browser interactions
445457
env:
458+
LOOPX_PERSONAL_WORKSPACE_SHARD: "${{ matrix.shard }}/3"
446459
LOOPX_DASHBOARD_COVERAGE: "1"
447460
LOOPX_PLAYWRIGHT_PACKAGE: ${{ github.workspace }}/apps/presentation/dashboard/node_modules/playwright
448461
run: |
462+
node --test examples/personal-workspace-browser/shard.test.mjs
449463
npm run test:dashboard:coverage
450464
cd apps/presentation/dashboard
451465
npx playwright install --with-deps chromium --only-shell
452466
node ../../../examples/personal-workspace-browser-smoke.mjs
467+
cp ../../../output/playwright/personal-workspace/acceptance-results.json ../../../coverage/dashboard/acceptance-results.json
453468
454469
- name: Upload Dashboard coverage
455470
uses: actions/upload-artifact@v7
471+
with:
472+
name: dashboard-coverage-${{ matrix.shard }}
473+
path: |
474+
coverage/dashboard/lcov.info
475+
coverage/dashboard/browser-coverage.json
476+
coverage/dashboard/acceptance-results.json
477+
if-no-files-found: error
478+
retention-days: 3
479+
480+
dashboard-acceptance:
481+
needs: [changes, dashboard-browser]
482+
if: needs.changes.outputs.core_tests == 'true'
483+
runs-on: ubuntu-latest
484+
timeout-minutes: 5
485+
steps:
486+
- uses: actions/checkout@v7
487+
- uses: actions/setup-node@v6
488+
with:
489+
node-version: "22.22.3"
490+
- run: npm ci --ignore-scripts
491+
- run: node --test scripts/ci/merge-dashboard-coverage.test.mjs
492+
- uses: actions/download-artifact@v7
493+
with:
494+
pattern: dashboard-coverage-*
495+
path: coverage/dashboard-shards
496+
- name: Require every acceptance shard and merge source coverage
497+
run: node scripts/ci/merge-dashboard-coverage.mjs coverage/dashboard-shards coverage/dashboard
498+
- uses: actions/upload-artifact@v7
456499
with:
457500
name: dashboard-coverage
458501
path: coverage/dashboard/*.info
@@ -541,7 +584,7 @@ jobs:
541584
strategy:
542585
fail-fast: false
543586
matrix:
544-
shard: [1, 2, 3, 4]
587+
shard: [1, 2, 3, 4, 5, 6]
545588
steps:
546589
- uses: actions/checkout@v7
547590
with:
@@ -588,7 +631,7 @@ jobs:
588631
run: >-
589632
python -m pytest -v -n 2 -m "not stage2c_e2e"
590633
-p scripts.ci.pytest_timing
591-
--splits 4 --group ${{ matrix.shard }}
634+
--splits 6 --group ${{ matrix.shard }}
592635
--splitting-algorithm least_duration
593636
--durations=25 --durations-min=1
594637
--junitxml=junit.xml
@@ -638,7 +681,7 @@ jobs:
638681
- name: Combine complete coverage and enforce the existing floor
639682
run: |
640683
shards=()
641-
for shard in 1 2 3 4; do
684+
for shard in 1 2 3 4 5 6; do
642685
path="coverage-shards/python-coverage-${shard}/.coverage"
643686
test -s "$path"
644687
shards+=("$path")

‎apps/presentation/dashboard/smoke/chat-turn-acceptance-http-fixture.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from loopx.chat_runtime import ChatRuntimeController
1313
from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
14-
from loopx.chat_store import ChatSessionStore
14+
from loopx.chat_store import CHAT_TURN_SCHEMA_VERSION, ChatSessionStore
1515

1616

1717
class _HealthyAdapter:
@@ -39,11 +39,14 @@ def close_session(self) -> None:
3939

4040

4141
def _turn_count(store: ChatSessionStore, session_id: str) -> int:
42-
return sum(
43-
1
44-
for path in (store.sessions_root / session_id / "turns").glob("*.json")
45-
if not path.name.endswith(".events.json")
46-
)
42+
count = 0
43+
for path in (store.sessions_root / session_id / "turns").glob("*.json"):
44+
payload = json.loads(path.read_text(encoding="utf-8"))
45+
if not isinstance(payload, dict):
46+
raise TypeError("persisted turn-directory JSON must be an object")
47+
if payload.get("schema_version") == CHAT_TURN_SCHEMA_VERSION:
48+
count += 1
49+
return count
4750

4851

4952
def main() -> int:
@@ -59,6 +62,7 @@ def main() -> int:
5962
json.dumps(
6063
{
6164
"schema_version": "0.1",
65+
"common_runtime_root": str(root),
6266
"goals": [
6367
{
6468
"id": "goal-one",

‎apps/presentation/dashboard/src/data/chat.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HANDOFF_MODES, EXECUTION_HANDOFF_MODES } from "../../../../../loopx/control_plane/coordination/handoff_mode_vocabulary.js";
22
import { normalizeGoalDraft } from "../../../../../loopx/control_plane/collaboration/goal_draft.js";
3-
import { parseTurnStep, type TurnStep } from "./turn-steps";
3+
import { parseTurnStep, type TurnStep } from "./turn-steps.js";
44
import { z } from "zod";
55
import { actionSourceBasisSchema } from "./action-source-basis.js";
66

@@ -2416,7 +2416,7 @@ export async function disconnectLarkGoalTopic(goalId: string, connectionId: stri
24162416
);
24172417
}
24182418

2419-
export { CONTEXTS as usageContexts } from "../../../../../loopx/control_plane/runtime/usage_statistics_contract";
2419+
export { CONTEXTS as usageContexts } from "../../../../../loopx/control_plane/runtime/usage_statistics_contract.js";
24202420
const usageStatisticsSchema = z.object({
24212421
consent: z.enum(["default", "enabled", "disabled"]),
24222422
sending: z.boolean(), blocked_by: z.string().nullable(), endpoint: z.string().nullable(),
@@ -2527,6 +2527,7 @@ export async function changePrivateAgentTarget(bindingId: string, revision: numb
25272527
}));
25282528
}
25292529

2530+
25302531
// Display validation only; finding status semantics remain owned by Explore.
25312532
const exploreResultPageSchema = z.object({
25322533
ok: z.literal(true), goal_id: z.string(), total: z.number().int().nonnegative(),

‎apps/presentation/dashboard/src/data/status.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ export const runRecordSchema = z.object({
537537
});
538538

539539
export const runGoalSchema = z.object({
540+
zcode_goal_eligible_agent_ids: z.array(z.string()).optional().default([]).catch([]),
540541
acceptance_observation: goalAcceptanceObservationSchema.optional().nullable().catch(null),
541542
id: z.string(),
542543
activation_state: z.enum(["active", "stopped"]).optional().default("active"),
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ZCODE_GOAL_ACTIONS, ZCODE_NATIVE_GOAL_STATUSES, ZCODE_IDENTITY_SCOPES, type ZCodeGoalAction, type ZCodeGoalReadback, type ZCodeModelSelection } from "../../../../../loopx/zcode_goal_mode/contract.js";
2+
import {z} from "zod";
3+
import {requestJson} from "./chat.js";
4+
5+
const zcodeModelSelectionSchema = z.object({
6+
providerId: z.string().min(1), modelId: z.string().min(1),
7+
options: z.object({reasoningLevel: z.string().min(1)}).optional(),
8+
});
9+
10+
// Validate provider transport observations; action permission remains with the typed provider owner.
11+
const zcodeGoalReadbackSchema = z.object({
12+
ok: z.boolean(), available: z.boolean(), reason: z.string().optional(),
13+
goal_id: z.string(), agent_id: z.string(), goal_creation_operation_id: z.string().nullable(),
14+
goal_ref: z.object({goal_id: z.string(), goal_instance_id: z.string().min(1).optional()}),
15+
identity_scope: z.enum(ZCODE_IDENTITY_SCOPES).optional(),
16+
binding: z.object({mode: z.literal("managed_cli"), connected: z.boolean(), cli_path: z.string(), protocol: z.string()}).nullable(),
17+
native: z.object({
18+
session_id: z.string(), target_id: z.string().nullable(),
19+
status: z.enum(ZCODE_NATIVE_GOAL_STATUSES).nullable(), running: z.boolean(),
20+
session_status: z.string().optional(), raw_status: z.string().nullable().optional(), usage: z.null().optional(),
21+
objective_sha256: z.string().nullable().optional(), selected_model: zcodeModelSelectionSchema.nullable().optional(),
22+
available_models: z.array(z.object({selection: zcodeModelSelectionSchema, label: z.string(), provider_label: z.string().optional(),
23+
reasoning_levels: z.array(z.string()), default_reasoning_level: z.string().nullable(), disabled: z.boolean()})).optional(),
24+
}).nullable(),
25+
quota: z.object({should_run: z.boolean(), reason: z.string().optional(), checked_at: z.string()}).nullable(),
26+
actions: z.array(z.enum(ZCODE_GOAL_ACTIONS)),
27+
});
28+
29+
function zcodeGoalUrl(goalId: string, agentId: string) {
30+
return `/api/goals/${encodeURIComponent(goalId)}/agents/${encodeURIComponent(agentId)}/zcode-goal`;
31+
}
32+
33+
function zcodeGoalReadback(payload: unknown, goalId: string, agentId: string): ZCodeGoalReadback {
34+
const result = zcodeGoalReadbackSchema.parse(payload);
35+
if (result.goal_id !== goalId || result.goal_ref.goal_id !== goalId || result.agent_id !== agentId) {
36+
throw new Error("ZCode Goal source changed; read the current Goal and Agent again.");
37+
}
38+
return result;
39+
}
40+
41+
export async function fetchZCodeGoal(goalId: string, agentId: string, signal?: AbortSignal) {
42+
return zcodeGoalReadback(await requestJson<unknown>(zcodeGoalUrl(goalId, agentId), {signal}), goalId, agentId);
43+
}
44+
45+
export async function updateZCodeGoal(goalId: string, agentId: string, action: Exclude<ZCodeGoalAction, "status">,
46+
expectedBinding: Pick<ZCodeGoalReadback, "goal_ref" | "goal_creation_operation_id">,
47+
options?: {cliPath?: string; modelSelection?: ZCodeModelSelection}, signal?: AbortSignal) {
48+
const result = zcodeGoalReadback(await requestJson<unknown>(zcodeGoalUrl(goalId, agentId), {
49+
method: "POST", signal,
50+
body: JSON.stringify({action, expected_binding: expectedBinding,
51+
...(action === "bind" && options?.cliPath?.trim() ? {cli_path: options.cliPath.trim()} : {}),
52+
...(action === "select_model" && options?.modelSelection ? {model_selection: options.modelSelection} : {}),
53+
}),
54+
}), goalId, agentId);
55+
if (result.goal_ref.goal_instance_id !== expectedBinding.goal_ref.goal_instance_id
56+
|| result.goal_creation_operation_id !== expectedBinding.goal_creation_operation_id) {
57+
throw new Error("ZCode Goal source changed; read the current Goal and Agent again.");
58+
}
59+
return result;
60+
}

‎apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ZCodeGoalControl } from "./zcode-goal-control";
12
import { GoalAcceptanceObservationCard } from "./goal-acceptance-observation-card";
23
import { AttentionActions } from "./attention-actions";
34
import { AttentionDetailCard } from "./attention-detail-card";
@@ -828,6 +829,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
828829
</>
829830
)}
830831
</section>
832+
{selection.item.zcodeGoalEligibleAgentIds?.length ? <ZCodeGoalControl key={selection.item.goalId} goal={selection.item} readOnly={readOnly} /> : null}
831833
{!readOnly ? <div className="personal-drawer-action-grid">
832834
<button className="personal-secondary-action" onClick={() => callbacks.onRequestScheduleConfig?.("heartbeat", selection.item.goalId)} type="button"><Radio size={16} />{t("drawer.setupHeartbeat")}</button>
833835
<button className="personal-secondary-action" onClick={() => callbacks.onRequestScheduleConfig?.("monitor", selection.item.goalId)} type="button"><CalendarClock size={16} />{t("drawer.scheduleAdd")}</button>

‎apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,62 @@ export const workspaceLocaleStorageKey = "loopx-pw-locale";
1212
export type WorkspaceLocale = "en" | "zh-CN";
1313

1414
const en = {
15+
"zcode.executionFailed": "ZCode native execution failed. Check the host session and model configuration, then read status again.",
16+
"zcode.currentModel": "Current model",
17+
"zcode.modelNotSelected": "Not selected",
18+
"zcode.model": "Configured model",
19+
"zcode.chooseModel": "Choose an existing model",
20+
"zcode.modelDisabled": "Unavailable",
21+
"zcode.reasoning": "Reasoning level",
22+
"zcode.chooseReasoning": "Choose a reasoning level",
23+
"zcode.useModel": "Use this model",
24+
"zcode.modelsUnavailable": "ZCode did not provide an available model. Configure a model in ZCode, then bind the CLI again.",
25+
"zcode.identity": "Goal identity",
26+
"zcode.identity.exact_goal_instance": "Exact Goal instance",
27+
"zcode.identity.legacy_goal_alias": "Legacy Goal alias",
28+
"zcode.identityBoundary": "Identity boundary",
29+
"zcode.legacyBoundary": "This binding follows the legacy alias and creation witness. Recreating the same alias is not an exact lifetime guarantee.",
30+
"zcode.title": "ZCode native Goal",
31+
"zcode.scope": "Bind a managed ZCode CLI session to a registered Agent in this Goal. Binding does not start model work; Start executes the Goal.",
32+
"zcode.remote": "Switch to the local workspace to bind and control a CLI session.",
33+
"zcode.noAgents": "No registered Agent is available for this Goal. Register an Agent before binding a session.",
34+
"zcode.agent": "Registered Agent",
35+
"zcode.cli": "CLI path",
36+
"zcode.pathPlaceholder": "Use ZCode from PATH",
37+
"zcode.pathHelp": "Leave blank to use PATH. A new path takes effect only after binding.",
38+
"zcode.bind": "Bind CLI",
39+
"zcode.refresh": "Read status",
40+
"zcode.pending": "Waiting for host readback…",
41+
"zcode.failed": "Operation could not be confirmed.",
42+
"zcode.readAgain": "Read status before continuing.",
43+
"zcode.unavailable": "Native control unavailable",
44+
"zcode.binding": "CLI binding",
45+
"zcode.connected": "Connected",
46+
"zcode.disconnected": "Disconnected",
47+
"zcode.unbound": "Not bound",
48+
"zcode.native": "Native Goal state",
49+
"zcode.unknown": "Unknown",
50+
"zcode.state.active": "Active",
51+
"zcode.state.paused": "Paused",
52+
"zcode.state.completed": "Completed",
53+
"zcode.state.budget_limited": "Native budget limit reached",
54+
"zcode.execution": "Observed execution",
55+
"zcode.running": "Running",
56+
"zcode.idle": "Not running",
57+
"zcode.quota": "LoopX quota admission",
58+
"zcode.quotaAllowed": "Continuation permitted",
59+
"zcode.quotaHeld": "Continuation held",
60+
"zcode.quotaBoundary": "LoopX checks quota before start and continuation, and monitors revocation during execution. ZCode manages internal model calls.",
61+
"zcode.usageUnknown": "Native token usage and a model-call token limit are not provided.",
62+
"zcode.checked": "Host state read at {time}",
63+
"zcode.diagnostics": "Connection details",
64+
"zcode.sessionState": "Session state",
65+
"zcode.rawState": "Raw Goal state",
66+
"zcode.pathChanged": "Bind the edited CLI path before starting or resuming.",
67+
"zcode.start": "Start",
68+
"zcode.pause": "Pause",
69+
"zcode.resume": "Resume",
70+
"zcode.stop": "Stop",
1571
"storage.title": "Goal data storage",
1672
"storage.boundary": "Confirmation changes only this existing Goal. New-Goal defaults and task ownership are separate. Preserve journal, metadata and receipts; no execution authority is granted.",
1773
"storage.current": "Current source (fresh readback)",
@@ -1430,6 +1486,62 @@ const en = {
14301486
export type WorkspaceMessageKey = keyof typeof en;
14311487

14321488
const zhCN: Record<WorkspaceMessageKey, string> = {
1489+
"zcode.executionFailed": "ZCode 原生执行失败。请检查宿主会话与模型配置,再回读状态。",
1490+
"zcode.currentModel": "当前模型",
1491+
"zcode.modelNotSelected": "未选择",
1492+
"zcode.model": "已配置模型",
1493+
"zcode.chooseModel": "选择已有模型",
1494+
"zcode.modelDisabled": "不可用",
1495+
"zcode.reasoning": "推理级别",
1496+
"zcode.chooseReasoning": "选择推理级别",
1497+
"zcode.useModel": "使用此模型",
1498+
"zcode.modelsUnavailable": "ZCode 未提供可用模型。请先在 ZCode 配置模型,再绑定 CLI。",
1499+
"zcode.identity": "Goal 身份",
1500+
"zcode.identity.exact_goal_instance": "精确 Goal 实例",
1501+
"zcode.identity.legacy_goal_alias": "旧版 Goal 别名",
1502+
"zcode.identityBoundary": "身份边界",
1503+
"zcode.legacyBoundary": "此绑定沿用旧版别名与创建凭据;同名重建不具备精确生命周期保证。",
1504+
"zcode.title": "ZCode 原生 Goal",
1505+
"zcode.scope": "为当前 Goal 的已注册 Agent 绑定托管 ZCode CLI 会话。绑定不启动模型;点击启动会执行 Goal。",
1506+
"zcode.remote": "请切回本地工作区后绑定和控制 CLI 会话。",
1507+
"zcode.noAgents": "此 Goal 没有可用的已注册 Agent。请先注册 Agent,再绑定会话。",
1508+
"zcode.agent": "已注册 Agent",
1509+
"zcode.cli": "CLI 路径",
1510+
"zcode.pathPlaceholder": "使用 PATH 中的 ZCode",
1511+
"zcode.pathHelp": "留空使用 PATH。新路径在绑定后生效。",
1512+
"zcode.bind": "绑定 CLI",
1513+
"zcode.refresh": "回读状态",
1514+
"zcode.pending": "等待宿主回读…",
1515+
"zcode.failed": "无法确认操作结果。",
1516+
"zcode.readAgain": "请先回读状态,再继续操作。",
1517+
"zcode.unavailable": "原生控制不可用",
1518+
"zcode.binding": "CLI 绑定",
1519+
"zcode.connected": "已连接",
1520+
"zcode.disconnected": "未连接",
1521+
"zcode.unbound": "未绑定",
1522+
"zcode.native": "原生 Goal 状态",
1523+
"zcode.unknown": "未知",
1524+
"zcode.state.active": "活跃",
1525+
"zcode.state.paused": "已暂停",
1526+
"zcode.state.completed": "已完成",
1527+
"zcode.state.budget_limited": "原生预算已达限额",
1528+
"zcode.execution": "观察到的执行",
1529+
"zcode.running": "正在运行",
1530+
"zcode.idle": "未运行",
1531+
"zcode.quota": "LoopX 配额准入",
1532+
"zcode.quotaAllowed": "允许续跑",
1533+
"zcode.quotaHeld": "续跑受限",
1534+
"zcode.quotaBoundary": "LoopX 检查启动与续跑配额,并在运行中监测撤销。ZCode 管理内部模型调用。",
1535+
"zcode.usageUnknown": "原生 Token 用量与逐次模型调用的 Token 限额未提供。",
1536+
"zcode.checked": "宿主状态回读于 {time}",
1537+
"zcode.diagnostics": "连接详情",
1538+
"zcode.sessionState": "会话状态",
1539+
"zcode.rawState": "原始 Goal 状态",
1540+
"zcode.pathChanged": "启动或恢复前,请先绑定修改后的 CLI 路径。",
1541+
"zcode.start": "启动",
1542+
"zcode.pause": "暂停",
1543+
"zcode.resume": "恢复",
1544+
"zcode.stop": "停止",
14331545
"storage.title": "Goal 数据存储",
14341546
"storage.boundary": "明确确认后仅切换此既有 Goal。新 Goal 默认值与任务所有权是各自的设置。保留日志、metadata 和回执,不授予执行权限。",
14351547
"storage.current": "当前来源(实时读回)",

‎apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ export type WorkspaceGoal = {
168168
lastActivityAt?: string | null;
169169
state?: string | null;
170170
}>;
171+
/** Canonical registered identities; discovered task claimants do not grant binding authority. */
172+
registeredAgentIds?: string[];
173+
/** Host-compatible registered IDs from the canonical backend; absence grants no controls. */
174+
zcodeGoalEligibleAgentIds?: string[];
171175
agentLaneCount?: number;
172176
agentLabel?: string;
173177
agentSentence: string;

0 commit comments

Comments
 (0)