diff --git a/README.md b/README.md index ddaccce..898e97a 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,12 @@ Restart `pnpm dev` after changing them. Keep credentials server-side. A direct Core URL in the connection dialog is only for a compatible Core that explicitly allows the Web origin, methods, and headers through CORS. -Do not have a Core running yet? Follow [Connecting Agent Core](docs/core-connection.md). -That guide contains the complete PostgreSQL, caller key, device, daemon, native -harness, `CODEX_HOME`, security, verification, and shutdown procedures. +Do not have a Core running yet? Use the immutable +[current Parsar setup guide](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service). +The repository's [legacy Web connection runbook](docs/core-connection.md) is pinned +to the older revision stated at its top; revalidate its PostgreSQL, caller-key, +device, daemon, native-harness, `CODEX_HOME`, verification, and shutdown steps before +applying them to a newer Core. ## First use @@ -110,16 +113,18 @@ See [Architecture](docs/architecture.md) for the full component and trust bounda | --- | --- | | Web cannot reach Core | Confirm the Core address and `AGENTS_API_PROXY_TARGET`, then restart Vite | | `401 invalid_api_key` | The plaintext caller bearer must match the current Core key binding | -| `503 execution_unavailable` / `Execution is not enabled` | Core is reachable but has no enabled execution path; connect its configured executor/daemon | +| `503 execution_unavailable` / `Execution is not enabled` | Core rejected execution; inspect its safe error plus runtime and ownership state. A worker, executor, or daemon may be unconfigured or disconnected, or an execution lease may have been lost | | Agent saves but its model fails | Use a model ID and provider credential supported by the connected runtime | -`/healthz` proves HTTP liveness only, not chat readiness. See the -[full troubleshooting guide](docs/core-connection.md#troubleshooting) before retrying -an uncertain request. +`/healthz` proves HTTP liveness only, not chat readiness. Check durable Core state and +the current pinned Parsar guide before retrying an uncertain request; the +[legacy 043 troubleshooting snapshot](docs/core-connection.md#troubleshooting) is +historical context only. ## Documentation -- [Connect Agent Core](docs/core-connection.md) — full local and deployment setup +- [Current Parsar Core setup](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service) — immutable current upstream guide +- [Legacy Web connection runbook](docs/core-connection.md) — historical `0438880` snapshot; revalidate before use - [Protocol coverage](docs/protocol-coverage.md) — exact supported API surface - [Architecture](docs/architecture.md) — ownership, runtime, and trust boundaries - [Roadmap](docs/roadmap.md) — planned Web and Core integrations diff --git a/README.zh-CN.md b/README.zh-CN.md index a186461..badac83 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -70,9 +70,11 @@ AGENTS_API_PROXY_TOKEN_FILE=/absolute/private/path/to/web-token 修改后重启 `pnpm dev`。凭据应保留在服务端。只有兼容 Core 通过 CORS 明确允许 Web 的源、方法和请求头时,才能在连接对话框中使用 Core 直连 URL。 -还没有运行中的 Core?请参阅[连接 Agent Core](docs/core-connection.md)。 -该文档包含完整的 PostgreSQL、调用方凭据、执行设备、daemon、原生执行适配层(harness)、 -`CODEX_HOME`、安全、验证和停止流程。 +还没有运行中的 Core?请使用不可变的 +[当前 Parsar 配置指南](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service)。 +仓库内的[旧版 Web 连接手册](docs/core-connection.md)固定在文首标注的旧 revision; +将其中 PostgreSQL、调用方凭据、执行设备、daemon、原生执行适配层(harness)、 +`CODEX_HOME`、验证和停止流程用于更新版 Core 前必须重新核对。 ## 第一次使用 @@ -108,15 +110,17 @@ WebSocket 当作 API URL。 | --- | --- | | Web 无法访问 Core | 确认 Core 地址和 `AGENTS_API_PROXY_TARGET`,然后重启 Vite | | `401 invalid_api_key` | 明文调用方 Bearer 凭据必须与 Core 当前的密钥绑定匹配 | -| `503 execution_unavailable` / `Execution is not enabled` | Core 可以访问,但没有已启用的执行链路;请连接其配置的 executor/daemon | +| `503 execution_unavailable` / `Execution is not enabled` | Core 拒绝执行;请检查其安全错误、运行时和 ownership 状态。worker、executor 或 daemon 可能未配置或已断连,也可能丢失了执行 lease | | Agent 保存成功但模型运行失败 | 使用已连接运行时支持的 model ID 和提供商凭据 | `/healthz` 只能证明 HTTP 存活,不能证明聊天已就绪。重试结果不确定的请求前, -请先查看[完整故障排查](docs/core-connection.md#troubleshooting)。 +请先核对 Core 持久状态和当前固定版本的 Parsar 指南; +[旧版 043 故障排查快照](docs/core-connection.md#troubleshooting)仅供历史参考。 ## 文档入口 -- [连接 Agent Core](docs/core-connection.md) — 完整本地和部署配置 +- [当前 Parsar Core 配置](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service) — 不可变的当前上游指南 +- [旧版 Web 连接手册](docs/core-connection.md) — 历史 `0438880` 快照,使用前必须重新核对 - [协议覆盖范围](docs/protocol-coverage.md) — 准确的已支持 API 范围 - [架构说明](docs/architecture.md) — 所有权、运行时和信任边界 - [路线图](docs/roadmap.md) — 计划中的 Web 和 Core 集成 diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index a157cc0..29ac6c2 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -503,8 +503,7 @@ test("keeps the Agent ledger and dialogs usable at 390 px in light and dark mode expect(metrics.ledger?.right).toBeLessThanOrEqual(390); await attachScreenshot(page, testInfo, "narrow-light-agent-ledger"); - await page.getByRole("button", { name: "Environments", exact: true }).click(); - await expect(page.getByRole("heading", { name: "Environments Observed" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Environments", exact: true })).toHaveCount(0); const sessionsNavigation = page.getByRole("button", { name: "Sessions", exact: true }); await sessionsNavigation.click(); await expect(sessionsNavigation).toHaveAttribute("aria-current", "page"); @@ -515,6 +514,9 @@ test("keeps the Agent ledger and dialogs usable at 390 px in light and dark mode await globalCreate.click(); const createPanel = page.getByRole("menu", { name: "Create" }); await expect(createPanel).toBeVisible(); + await expect(createPanel.getByRole("menuitem")).toHaveCount(2); + await expect(createPanel.getByRole("menuitem", { name: /Environment template/i })).toHaveCount(0); + await expect(createPanel.getByRole("menuitem", { name: /Environment key/i })).toHaveCount(0); const createPanelBox = await createPanel.boundingBox(); expect(createPanelBox).not.toBeNull(); expect(createPanelBox?.x ?? -1).toBeGreaterThanOrEqual(0); @@ -1043,7 +1045,10 @@ test("renders self-hosted Environment and Workspace state safely across reconnec await expect(panel.getByRole("link", { name: "Launcher setup" })).toBeVisible(); await expect(page.getByRole("region", { name: "Environment connection required" })).toBeVisible(); await expect(page.getByRole("region", { name: "Function result required" })).toBeVisible(); - await expect(page.getByLabel("Function result or error")).toBeVisible(); + await expect(page.getByLabel("Function result or error")).toBeEnabled(); + await expect(page.getByRole("button", { name: "Return error" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "Submit result" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "Cancel active Turn" })).toBeEnabled(); await expect(page.locator("body")).not.toContainText("launcher:private"); await expect(page.locator("body")).not.toContainText("executor_token=secret"); await expect(page.locator('a[href^="file:"]')).toHaveCount(0); @@ -1404,3 +1409,25 @@ test("manually retries uncertain sends with the original key only while the payl expect(sends[5]?.idempotencyKey).not.toBe(sends[4]?.idempotencyKey); await attachScreenshot(page, testInfo, "desktop-send-recovery"); }); + +test("keeps cancellation available for an Environment-only required action", async ({ page, request }) => { + await resetFixture(request); + await controlFixture(request, { environmentScenario: 6 }); + await page.goto("/"); + await expect(page.getByText("listening", { exact: true })).toBeVisible(); + await expect(page.getByRole("region", { name: "Environment connection required" })).toBeVisible(); + await expect(page.getByRole("region", { name: "Function result required" })).toHaveCount(0); + const writesBefore = (await fixtureRequests(request)).filter( + (entry) => entry.method === "POST" && entry.path.endsWith("/events"), + ).length; + const cancel = page.getByRole("button", { name: "Cancel active Turn" }); + await expect(cancel).toBeEnabled(); + await cancel.click(); + await expect.poll(async () => (await fixtureRequests(request)).filter( + (entry) => entry.method === "POST" && entry.path.endsWith("/events"), + ).length).toBe(writesBefore + 1); + const writes = (await fixtureRequests(request)).filter( + (entry) => entry.method === "POST" && entry.path.endsWith("/events"), + ); + expect(writes.at(-1)?.body).toEqual({ events: [{ type: "agent.session.input.cancel" }] }); +}); diff --git a/apps/web/e2e/core-connection.spec.ts b/apps/web/e2e/core-connection.spec.ts index c7b86c4..e357bcd 100644 --- a/apps/web/e2e/core-connection.spec.ts +++ b/apps/web/e2e/core-connection.spec.ts @@ -303,7 +303,7 @@ test("announces loading, authenticated access, and each safe failure state from methods.push(route.request().method()); const reply = replies.shift(); if (!reply) return route.abort("failed"); - await new Promise((resolve) => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 150)); if ("abort" in reply) return route.abort("failed"); return route.fulfill({ status: reply.status, @@ -333,7 +333,8 @@ test("announces loading, authenticated access, and each safe failure state from await expect(loading).toContainText("Testing Core connection…"); const terminal = dialog.getByRole(expected.role); await expect(terminal).toContainText(expected.text); - await expect(terminal).toContainText("Execution readiness: Unknown / not verified"); + await expect(terminal).toContainText("Chat uses the current Agents API contract"); + await expect(terminal).toContainText("does not start a Turn or verify its runtime dependencies"); if (expected.absentText) await expect(terminal).not.toContainText(expected.absentText); } @@ -373,7 +374,8 @@ test("turns a stalled probe into one bounded unreachable result", async ({ page, (window as ProbeInstrumentationWindow).__stalledProbeCallCount ?? 0 ))).toBe(1); await expect(dialog.getByRole("alert")).toContainText("Core unreachable", { timeout: 7_500 }); - await expect(dialog.getByRole("alert")).toContainText("Execution readiness: Unknown / not verified"); + await expect(dialog.getByRole("alert")).toContainText("Chat uses the current Agents API contract"); + await expect(dialog.getByRole("alert")).toContainText("does not start a Turn or verify its runtime dependencies"); expect(await page.evaluate(() => (window as ProbeInstrumentationWindow).__stalledProbeCallCount)).toBe(1); }); diff --git a/apps/web/e2e/fixture-core.mjs b/apps/web/e2e/fixture-core.mjs index e6dfee1..577736b 100644 --- a/apps/web/e2e/fixture-core.mjs +++ b/apps/web/e2e/fixture-core.mjs @@ -253,7 +253,7 @@ function applyEnvironmentScenario(value) { const session = state.sessions[0]; if (!session) return; const hostileRemote = "https://launcher:private@executor.example.test/connect?executor_token=secret#credential"; - if (value === 1 || value === 4 || value === 5) { + if (value === 1 || value === 4 || value === 5 || value === 6) { session.environment = { type: "self_hosted", id: value === 5 ? canonicalEnvironmentUuid.toUpperCase() : "environment_fixture", @@ -261,11 +261,15 @@ function applyEnvironmentScenario(value) { workspace_directory: `/workspace//${"long/".repeat(45)}project`, capability_directories: ["/capabilities/read-only", `/capabilities/${"wide/".repeat(55)}`], }; - session.status = value === 1 ? "requires_action" : "idle"; - session.required_actions = value === 1 ? [ - { type: "environment_connection", environment_id: "environment_fixture" }, - { type: "function_call", call_id: "call_fixture", turn_id: "turn_fixture", name: "confirm", arguments: { safe: true } }, - ] : []; + session.status = value === 1 || value === 6 ? "requires_action" : "idle"; + session.required_actions = value === 1 + ? [ + { type: "environment_connection", environment_id: "environment_fixture" }, + { type: "function_call", call_id: "call_fixture", turn_id: "turn_fixture", name: "confirm", arguments: { safe: true } }, + ] + : value === 6 + ? [{ type: "environment_connection", environment_id: "environment_fixture" }] + : []; return; } if (value === 2) { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index bccdca3..c9fb58e 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -29,7 +29,6 @@ import { requestAgentDetail, requestAgentUpdate, } from "./features/agents/agent-actions"; -import { EnvironmentsView } from "./features/environments/EnvironmentsView"; import { SessionsView, type SessionDetailState, @@ -1264,16 +1263,6 @@ export function App() { onUpdate={updateAgent} /> ) : null} - {view === "environments" ? ( - { - setSelectedId(sessionId); - setView("sessions"); - }} - /> - ) : null} {view === "system" ? : null} diff --git a/apps/web/src/components/ConnectionModal.test.tsx b/apps/web/src/components/ConnectionModal.test.tsx index b4536fd..7c07ea6 100644 --- a/apps/web/src/components/ConnectionModal.test.tsx +++ b/apps/web/src/components/ConnectionModal.test.tsx @@ -89,11 +89,12 @@ describe("Agent Core connection modes", () => { const markup = renderModal("/v1", true); expect(markup).toContain("Operator-owned setup"); - expect(markup).toContain("Connection guide"); - expect(markup).toContain("Troubleshooting"); + expect(markup).toContain("Legacy Web guide · 043 snapshot"); + expect(markup).toContain("Legacy troubleshooting · 043 snapshot"); expect(markup).toContain("Parsar Core setup"); expect(markup).toContain("98c5b3312ad33e1fae8b94283a011eb3e5f4ee2c"); - expect(markup).toContain("0438880ab21aa16d05cb91a4c7f91cc0abc12358"); + expect(markup).toContain("d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee"); + expect(markup).not.toContain("0438880ab21aa16d05cb91a4c7f91cc0abc12358"); expect(markup).not.toContain("f7cdf591396529880d80f8211fc7a0f4768fdf46"); expect(markup).not.toContain("8cc2898ca42b272cb3771234ee6a0ad0d2e932ba"); expect(markup).not.toContain("7409e00ca25311805a9f8f0d03614f820e407642"); @@ -104,14 +105,14 @@ describe("Agent Core connection modes", () => { expect(markup).not.toContain("AGENTS_API_DAEMON_WS_URL"); }); - it("states the GET-only probe boundary and unknown execution readiness", () => { + it("states the GET-only probe boundary without disabling the current chat contract", () => { const markup = renderModal("/v1", true); expect(markup).toContain("Test connection"); expect(markup).toContain("with one GET"); expect(markup).toContain("never creates an Agent, Session, Turn, or Item"); - expect(markup).toContain("Execution readiness remains Unknown / not verified"); - expect(markup).toContain("does not prove a daemon, model, or provider is ready"); + expect(markup).toContain("Chat uses the current Core events contract"); + expect(markup).toContain("a real request can still fail"); }); }); @@ -172,8 +173,8 @@ describe("Connection probe status", () => { const markup = renderToStaticMarkup(); expect(markup).toContain(expected); - expect(markup).toContain("Execution readiness: Unknown / not verified"); - expect(markup).not.toContain("Execution readiness: Ready"); + expect(markup).toContain("Chat uses the current Agents API contract"); + expect(markup).toContain("does not start a Turn or verify its runtime dependencies"); }); it("uses an alert for failures and a polite status for authenticated access", () => { diff --git a/apps/web/src/components/ConnectionModal.tsx b/apps/web/src/components/ConnectionModal.tsx index 63ccc76..a63ac08 100644 --- a/apps/web/src/components/ConnectionModal.tsx +++ b/apps/web/src/components/ConnectionModal.tsx @@ -29,7 +29,7 @@ export type ConnectionProbeState = | { status: "complete"; result: CoreProbeResult }; const webBaseline = "98c5b3312ad33e1fae8b94283a011eb3e5f4ee2c"; -const parsarBaseline = "0438880ab21aa16d05cb91a4c7f91cc0abc12358"; +const parsarBaseline = "d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee"; const operatorGuideUrl = `https://github.com/MiniMax-AI-Dev/agents-core-web/blob/${webBaseline}/docs/core-connection.md`; const troubleshootingUrl = `${operatorGuideUrl}#troubleshooting`; const parsarCoreSetupUrl = `https://github.com/MiniMax-AI-Dev/parsar/blob/${parsarBaseline}/services/agents-api/README.md#standalone-http-service`; @@ -102,7 +102,7 @@ export function ConnectionProbeStatus({ state }: { state: ConnectionProbeState } > {copy.title}

{copy.detail}

- Execution readiness: Unknown / not verified. + Chat uses the current Agents API contract. This read-only probe does not start a Turn or verify its runtime dependencies. ); } @@ -316,11 +316,11 @@ export function ConnectionModal({
- Connection guide + Legacy Web guide · 043 snapshot - Troubleshooting + Legacy troubleshooting · 043 snapshot @@ -331,7 +331,7 @@ export function ConnectionModal({
); diff --git a/apps/web/src/components/CreateMenu.test.tsx b/apps/web/src/components/CreateMenu.test.tsx index 1c3d411..13b7c0b 100644 --- a/apps/web/src/components/CreateMenu.test.tsx +++ b/apps/web/src/components/CreateMenu.test.tsx @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { CreateMenuContent } from "./CreateMenu"; describe("Create menu", () => { - it("keeps supported actions separate from explicit Core-unavailable resources", () => { + it("shows only actions supported by the current Core contract", () => { const html = renderToStaticMarkup( { ); expect(html).toContain('role="menu"'); - expect(html).toContain("Environment template"); - expect(html).toContain("Core exposes no template API"); - expect(html).toContain("operator-owned, never browser-managed"); - expect(html.match(/aria-disabled="true"/g)).toHaveLength(2); + expect(html).toContain("Agent"); + expect(html).toContain("Start Session"); + expect(html).not.toContain("Environment template"); + expect(html).not.toContain("Environment key"); + expect(html).not.toContain('aria-disabled="true"'); expect(html).not.toContain("executor_token"); }); }); diff --git a/apps/web/src/components/CreateMenu.tsx b/apps/web/src/components/CreateMenu.tsx index 738f84e..ee67745 100644 --- a/apps/web/src/components/CreateMenu.tsx +++ b/apps/web/src/components/CreateMenu.tsx @@ -1,8 +1,6 @@ import { Bot, ChevronDown, - FileBox, - KeyRound, MessageSquare, Plus, } from "lucide-react"; @@ -27,14 +25,6 @@ export function CreateMenuContent({
- ) : ( -
-
- )} - - - - ); -} diff --git a/apps/web/src/features/sessions/SessionsView.tsx b/apps/web/src/features/sessions/SessionsView.tsx index 2969e90..4d7e91f 100644 --- a/apps/web/src/features/sessions/SessionsView.tsx +++ b/apps/web/src/features/sessions/SessionsView.tsx @@ -80,7 +80,7 @@ interface SessionsViewProps { ) => Promise; } -const executorSetupUrl = "https://github.com/MiniMax-AI-Dev/parsar/blob/main/services/agents-api/README.md#internal-execution-device-connection"; +const coreRuntimeSetupUrl = "https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#public-text-execution"; function SessionsListSkeleton() { return ( @@ -192,6 +192,30 @@ function UnsupportedActionNotice() { ); } +function CancelActiveTurnButton({ busy, onCancel }: { busy: boolean; onCancel: () => void }) { + return ( + + ); +} + +function CancelOnlyBar({ busy, onCancel }: { busy: boolean; onCancel: () => void }) { + return ( +
+

Turn continuation is unavailable, but cancellation remains available.

+ +
+ ); +} + function FunctionActionBar({ actions, agentName, @@ -261,9 +285,7 @@ function FunctionActionBar({ > Submit result - + ); @@ -434,6 +456,10 @@ export function SessionsView({ const unsupportedActionCount = requiredActions.length - environmentConnections.length - functionActions.length + ( !requiredActionsAreValid || selected?.status === "requires_action" && !requiredActions.length ? 1 : 0 ); + const showCancelOnly = Boolean( + (selected?.status === "in_progress" || selected?.status === "requires_action") && + (unsupportedActionCount > 0 || environmentConnections.length > 0 && functionActions.length === 0), + ); return (
@@ -617,21 +643,21 @@ export function SessionsView({ {sendError ? ( - Executor setup + + Core runtime setup ) : undefined} @@ -673,6 +699,9 @@ export function SessionsView({ ))} {unsupportedActionCount ? : null} + {showCancelOnly ? ( + + ) : null} {!unsupportedActionCount && functionActions.length ? ( {selected.agent.name || "Untitled Agent"} {selected.status === "in_progress" || selected.status === "requires_action" ? ( - + ) : (