From 4c3a8951f689d78d8db5cff7d73f7313789c2818 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sun, 7 Jun 2026 18:11:43 +0800 Subject: [PATCH] feat: hwlab-code-agent tool and skill for agent scheduling - tools/hwlab-code-agent-cli.ts: thin wrapper importing from skills/ - skills/hwlab-code-agent/: SKILL.md, config.json, scripts/src/client.ts - toolAliases: hwlab-code-agent (spawn/poll/result/trace) - skillRefs: hwlab-code-agent SKILL.md - hwpod spec auto-inheritance: --spec-path, CWD .hwlab/, HWPOD_SPEC_CONTENT - Leader agent can spawn Coder/Reviewer with inherited hwpod spec --- internal/cloud/code-agent-agentrun-adapter.ts | 2 + skills/hwlab-code-agent/SKILL.md | 228 ++++++++++++++++++ skills/hwlab-code-agent/config.json | 6 + .../scripts/hwlab-code-agent-cli.ts | 83 +++++++ skills/hwlab-code-agent/scripts/src/client.ts | 178 ++++++++++++++ tools/hwlab-code-agent-cli.ts | 6 + 6 files changed, 503 insertions(+) create mode 100644 skills/hwlab-code-agent/SKILL.md create mode 100644 skills/hwlab-code-agent/config.json create mode 100644 skills/hwlab-code-agent/scripts/hwlab-code-agent-cli.ts create mode 100644 skills/hwlab-code-agent/scripts/src/client.ts create mode 100644 tools/hwlab-code-agent-cli.ts diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index bbb89b53..cce9e79b 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -36,6 +36,7 @@ const HWLAB_RESOURCE_TOOL_ALIASES = Object.freeze([ Object.freeze({ name: "hwpod-ctl", path: "tools/hwpod-ctl.ts", kind: "bun-script" }), Object.freeze({ name: "hwpod-compiler", path: "tools/hwpod-compiler-cli.ts", kind: "bun-script" }), Object.freeze({ name: "unidesk-ssh", path: "tools/unidesk-ssh.mjs", kind: "bun-script" }) + Object.freeze({ name: "hwlab-code-agent", path: "tools/hwlab-code-agent-cli.ts", kind: "bun-script" }) ]); const HWLAB_RESOURCE_PROMPT_REFS = Object.freeze([ Object.freeze({ name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true }) @@ -44,6 +45,7 @@ const HWLAB_RESOURCE_SKILL_REFS = Object.freeze([ Object.freeze({ name: "hwpod-cli", path: "skills/hwpod-cli/SKILL.md", required: true, aggregateAs: "hwpod-cli" }), Object.freeze({ name: "hwpod-ctl", path: "skills/hwpod-ctl/SKILL.md", required: true, aggregateAs: "hwpod-ctl" }), Object.freeze({ name: "hwlab-agent-runtime", path: "skills/hwlab-agent-runtime/SKILL.md", required: true, aggregateAs: "hwlab-agent-runtime" }) + Object.freeze({ name: "hwlab-code-agent", path: "skills/hwlab-code-agent/SKILL.md", required: true, aggregateAs: "hwlab-code-agent" }) ]); export function codeAgentAgentRunAdapterEnabled(env = process.env) { diff --git a/skills/hwlab-code-agent/SKILL.md b/skills/hwlab-code-agent/SKILL.md new file mode 100644 index 00000000..fec14273 --- /dev/null +++ b/skills/hwlab-code-agent/SKILL.md @@ -0,0 +1,228 @@ +--- +name: hwlab-code-agent +description: HWLAB Code Agent CLI — `hwlab-cli client agent` 的全部操作,包括 session 管理、send、trace/result/inspect、steer、Web 等价路径、auth;以及 spawn/poll/result/trace 直连 cloud-api。用户提到 code agent、hwlab-cli client agent、agent send、agent trace、agent result、agent inspect、agent session、agent steer、拉 trace、查 session、spawn agent、poll agent 时使用。 +--- + +# HWLAB Code Agent CLI + +Skill(cli-spec) + +**Skill 根目录**: `~/.agents/skills/hwlab-code-agent/` +**CLI 入口**: `bun scripts/hwlab-code-agent-cli.ts` +**配置**: `config.json`(单一来源,启动校验) + +## 直接客户端(master server 直连 cloud-api) + +无需透传到 G14,自动识别 `HWLAB_API_KEY` 鉴权调用 HWLAB v0.2 cloud-api。 + +```bash +source /root/.config/hwlab-v02/master-server-admin-api-key.env +cd ~/.agents/skills/hwlab-code-agent + +bun scripts/hwlab-code-agent-cli.ts spawn --message "say hello" [--profile deepseek] +bun scripts/hwlab-code-agent-cli.ts spawn --message "..." --spec-path /path/to/hwpod-spec.yaml +bun scripts/hwlab-code-agent-cli.ts poll [--timeout 600000] +bun scripts/hwlab-code-agent-cli.ts result +bun scripts/hwlab-code-agent-cli.ts trace [--full] +``` + +输出均为 JSON。`spawn` 创建 session + 提交 prompt,立即返回 `sessionId` + `traceId`。`poll` 阻塞轮询至 terminal 或超时。 + +### hwpod spec 自动继承 + +子 agent 需要 `.hwlab/hwpod-spec.yaml` 才能使用 hwpod。spec 按优先级自动注入: + +1. `--spec-path ` — 显式指定本地 spec 文件 +2. CWD 下的 `.hwlab/hwpod-spec.yaml` — caserun 注入后自动检测 +3. 环境变量 `HWPOD_SPEC_CONTENT` — yaml 或 base64 + +Caserun -> Leader -> Coder/Reviewer 继承链: + +``` +caserun 注入 .hwlab/hwpod-spec.yaml 到 Leader workspace + -> Leader CWD 自动有该文件 + -> Leader 调用 spawn 时 auto-detect -> 注入子 agent workspaceFiles + -> Coder/Reviewer 拿到相同 spec +``` + +--- + +## 外部管理 CLI(G14 workspace 透传) + +通过 G14 `/root/hwlab-v02` workspace 的 `hwlab-cli client agent` 管理 Code Agent 的 session、提交、trace 和纠偏。 + +**固定入口**: `bun tools/hwlab-cli/bin/hwlab-cli.ts client agent ...` +**锁定 env**: +```bash +export HWLAB_RUNTIME_WEB_URL=http://74.48.78.17:19666 +export HWLAB_RUNTIME_ENDPOINT_LOCKED=1 +``` +**API key**: `HWLAB_API_KEY` 从 `/root/.config/hwlab-v02/master-server-admin-api-key.env` 加载。 + +--- + +## Auth + +```bash +bun tools/hwlab-cli/bin/hwlab-cli.ts client auth status +``` + +CLI 默认从 `HWLAB_API_KEY` 读取 key,发送 `Authorization: Bearer hwl_live_...`。不支持 `--api-key` / `--bearer-token` 别名。缺少 key 时返回 `api_key_required`。 + +--- + +## Session 管理 + +Code Agent session 是显式资源,必须先创建再发送 turn。 + +```bash +# 创建 session +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent session create \ + [--provider-profile deepseek|codex-api|minimax-m3] + +# 查看状态 +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent session status + +# 选择当前 workspace session +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent session select +``` + +输出直接暴露 `sessionId`、`conversationId`、`threadId`、`providerProfile`、`sessionStatus`。 + +--- + +## 发送 Turn + +```bash +# 短消息 +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \ + --session-id --message "Hello" + +# 长 prompt(文件) +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \ + --session-id --message-file /tmp/prompt.md + +# 从 trace 恢复上下文续跑 +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \ + --from-trace + +# 显式 provider(优先于 session 默认) +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \ + --session-id --message "..." --provider-profile deepseek +``` + +必须携带显式 `--session-id` 或已 select 的 workspace session。未提供时返回 `session_required`。不传 `--provider-profile` 时继承 session 的 provider profile。 + +--- + +## Trace 查询 + +```bash +# 渲染 trace(与 Cloud Web 共享同一 renderer) +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent trace --render web + +# 完整 trace +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent trace --render web --full +``` + +trace rows 包含 `role`(user/assistant/tool)、`status`、`text`。 + +--- + +## Result 查询 + +```bash +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent result +``` + +返回 Agent 的 `assistantText`(最终回复)、`agentRun` 信息(runId/commandId/runnerId/namespace)、`traceSummary`。 + +--- + +## Inspect + +```bash +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent inspect --trace-id +``` + +从 Cloud Web `/v1/agent/chat/inspect` 读取 trace 所属的 `conversationId/sessionId/threadId`、session 状态和 `retryOf` 建议。session 缺失或 failed 时返回结构化 blocker。 + +--- + +## Steer(纠偏) + +```bash +bun tools/hwlab-cli/bin/hwlab-cli.ts client agent steer --message "..." +``` + +向运行中的 Code Agent turn 注入纠偏指令。 + +--- + +## 典型流程 + +### CaseRun 失败调试 + +```bash +# 1. status 拿 sessionId/traceId +trans G14:/root/hwlab-v02 script -- \ + 'hwlab-cli case run status --state-dir .state/hwlab-cli/caserun' + +# 2. 拉 trace 分析 Agent 行为 +trans G14:/root/hwlab-v02 script -- \ + 'export HWLAB_RUNTIME_WEB_URL=http://74.48.78.17:19666 HWLAB_RUNTIME_ENDPOINT_LOCKED=1 && \ + bun tools/hwlab-cli/bin/hwlab-cli.ts client agent trace --render web' + +# 3. 看 Agent 最终回复 +trans G14:/root/hwlab-v02 script -- \ + 'export HWLAB_RUNTIME_WEB_URL=http://74.48.78.17:19666 HWLAB_RUNTIME_ENDPOINT_LOCKED=1 && \ + bun tools/hwlab-cli/bin/hwlab-cli.ts client agent result ' +``` + +### 完整交互链路 + +```bash +# 1. 登录 +source /root/.config/hwlab-v02/master-server-admin-api-key.env + +# 2. 显式创建 session +hwlab-cli client agent session create --provider-profile deepseek + +# 3. 发送 turn(继承 session provider) +hwlab-cli client agent send --session-id --message "在吗?" + +# 4. 等待完成,查 result +hwlab-cli client agent result + +# 5. 拉 trace 看详细过程 +hwlab-cli client agent trace --render web +``` + +### Web 等价验证 + +Web 暴露 Code Agent 问题后,先用 CLI 对同一 Cloud Web origin 和同一 `/v1/agent/chat*` 路径复现: + +```bash +# inspect 取 session 上下文 +hwlab-cli client agent inspect --trace-id + +# 从 trace 恢复上下文续跑 +hwlab-cli client agent send --from-trace +``` + +--- + +## 环境变量 + +| 变量 | 值 | 说明 | +|---|---|---| +| `HWLAB_API_KEY` | `hwl_live_...` | API key,从 key env 文件加载 | +| `HWLAB_RUNTIME_WEB_URL` | `http://74.48.78.17:19666` | v0.2 Cloud Web origin | +| `HWLAB_RUNTIME_API_URL` | `http://74.48.78.17:19667` | v0.2 Cloud API origin | +| `HWLAB_RUNTIME_ENDPOINT_LOCKED` | `1` | 锁定 endpoint,不自动探测 | + +## 相关文档 + +- **hwlab-cli 规格**: `docs/reference/spec-v02-hwlab-cli.md`(auth/session/Web 等价架构) +- **trace 规格**: `docs/reference/spec-v02-code-agent-trace.md`(共享 renderer 语义) +- **dispatch 规格**: `docs/reference/agentrun-code-agent-dispatch.md`(AgentRun 调度) diff --git a/skills/hwlab-code-agent/config.json b/skills/hwlab-code-agent/config.json new file mode 100644 index 00000000..3b762a62 --- /dev/null +++ b/skills/hwlab-code-agent/config.json @@ -0,0 +1,6 @@ +{ + "apiBaseUrl": "http://74.48.78.17:19666", + "defaultProviderProfile": "deepseek", + "defaultProjectId": "prj_hwpod_workbench", + "defaultPollTimeoutMs": 300000 +} diff --git a/skills/hwlab-code-agent/scripts/hwlab-code-agent-cli.ts b/skills/hwlab-code-agent/scripts/hwlab-code-agent-cli.ts new file mode 100644 index 00000000..2a0dc037 --- /dev/null +++ b/skills/hwlab-code-agent/scripts/hwlab-code-agent-cli.ts @@ -0,0 +1,83 @@ +#!/usr/bin/env bun +/** + * hwlab-code-agent CLI — HWLAB Code Agent 客户端 + * Skill(cli-spec) + * + * 通过 hwlab-code-agent 直接调 HWLAB v0.2 cloud-api 管理 agent turn: + * spawn - 创建 session + 提交 prompt,立即返回 traceId + * poll - 轮询等待 turn 完成 + * result - 查询 turn 结果 + * trace - 查询 turn trace + */ + +import path from "node:path"; +import { spawn, poll, getResult, getTrace } from "./src/client.ts"; + +const argv = process.argv.slice(2); +const cmd = argv[0]; + +function parseArgs(from: number): Record { + const args: Record = {}; + for (let i = from; i < argv.length; i++) { + if (argv[i].startsWith("--")) { + const key = argv[i].slice(2); + const val = (i + 1 < argv.length && !argv[i + 1].startsWith("--")) ? argv[i + 1] : "true"; + args[key] = val; + if (val !== "true") i++; + } + } + return args; +} + +async function main() { + switch (cmd) { + case "spawn": { + const args = parseArgs(1); + await spawn(args); + break; + } + case "poll": { + const traceId = argv[1]; + if (!traceId) { + process.stderr.write(JSON.stringify({ ok: false, error: { code: "usage", message: "poll [--timeout N]" } }) + "\n"); + process.exit(1); + } + await poll(traceId, parseArgs(2)); + break; + } + case "result": { + const traceId = argv[1]; + if (!traceId) { + process.stderr.write(JSON.stringify({ ok: false, error: { code: "usage", message: "result " } }) + "\n"); + process.exit(1); + } + await getResult(traceId); + break; + } + case "trace": { + const traceId = argv[1]; + if (!traceId) { + process.stderr.write(JSON.stringify({ ok: false, error: { code: "usage", message: "trace [--full]" } }) + "\n"); + process.exit(1); + } + await getTrace(traceId, parseArgs(2)); + break; + } + default: + process.stderr.write(JSON.stringify({ + ok: false, + usage: { + spawn: "bun scripts/hwlab-code-agent-cli.ts spawn --message '...' [--profile deepseek]", + poll: "bun scripts/hwlab-code-agent-cli.ts poll [--timeout 600000]", + result: "bun scripts/hwlab-code-agent-cli.ts result ", + trace: "bun scripts/hwlab-code-agent-cli.ts trace [--full]" + } + }, null, 2) + "\n"); + process.exit(1); + } +} + +main().catch((e) => { + process.stderr.write(JSON.stringify({ ok: false, error: { code: "unhandled", message: e.message } }) + "\n"); + process.exit(1); +}); diff --git a/skills/hwlab-code-agent/scripts/src/client.ts b/skills/hwlab-code-agent/scripts/src/client.ts new file mode 100644 index 00000000..d1cf2d0e --- /dev/null +++ b/skills/hwlab-code-agent/scripts/src/client.ts @@ -0,0 +1,178 @@ +/** + * hwlab-code-agent 客户端核心逻辑 + * 直接调 HWLAB v0.2 cloud-api,自动识别环境变量 HWLAB_API_KEY + * 遵循 cli-spec:配置从 config.json 单一来源 + */ +import { readFileSync } from "node:fs"; +import { existsSync } from "node:fs"; +import path from "node:path"; + +// ---- config ---- +const SKILL_ROOT = path.resolve(import.meta.dir, "..", ".."); +const configPath = path.join(SKILL_ROOT, "config.json"); + +interface Config { + apiBaseUrl: string; + defaultProviderProfile: string; + defaultProjectId: string; + defaultPollTimeoutMs: number; +} + +let _cfg: Config | null = null; +function config(): Config { + if (!_cfg) { + try { + _cfg = JSON.parse(readFileSync(configPath, "utf-8")) as Config; + } catch { + throw new Error(`hwlab-code-agent: failed to load config from ${configPath}`); + } + if (!_cfg.apiBaseUrl) throw new Error("hwlab-code-agent: config.apiBaseUrl is required"); + if (!_cfg.defaultProviderProfile) throw new Error("hwlab-code-agent: config.defaultProviderProfile is required"); + } + return _cfg; +} + +// ---- workspace files auto-inheritance ---- +type WorkspaceFile = { path: string; content: string; encoding: "utf8" }; + +function collectWorkspaceFiles(args: Record): WorkspaceFile[] { + const files: WorkspaceFile[] = []; + const specPath = args.specPath; + + // 1. explicit --spec-path + if (specPath) { + if (!existsSync(specPath)) throw new Error(`spec file not found: ${specPath}`); + files.push({ path: ".hwlab/hwpod-spec.yaml", content: readFileSync(specPath, "utf-8"), encoding: "utf8" }); + } + + // 2. auto-detect: .hwlab/hwpod-spec.yaml in CWD (casrun-injected) + const cwdSpec = ".hwlab/hwpod-spec.yaml"; + if (!specPath && existsSync(cwdSpec)) { + files.push({ path: cwdSpec, content: readFileSync(cwdSpec, "utf-8"), encoding: "utf8" }); + } + + // 3. env var HWPOD_SPEC_CONTENT (plain yaml or base64) + const envSpec = process.env.HWPOD_SPEC_CONTENT || process.env.HWPOD_SPEC; + if (!specPath && !existsSync(cwdSpec) && envSpec) { + const content = envSpec.startsWith("eyJ") || envSpec.startsWith("YXBp") + ? Buffer.from(envSpec, "base64").toString("utf-8") + : envSpec; + files.push({ path: ".hwlab/hwpod-spec.yaml", content, encoding: "utf8" }); + } + + return files; +} + +function apiKey(): string { + const key = process.env.HWLAB_API_KEY; + if (!key) throw new Error("HWLAB_API_KEY not set in environment"); + return key; +} + +function authHeaders(): Record { + return { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey()}` }; +} + +// ---- JSON out ---- +function out(data: Record, exitCode = 0): void { + process.stdout.write(JSON.stringify(data, null, 2) + "\n"); + if (exitCode !== 0) process.exit(exitCode); +} + +function fail(code: string, msg: string, exitCode = 1): never { + process.stderr.write(JSON.stringify({ ok: false, error: { code, message: msg } }) + "\n"); + process.exit(exitCode); +} + +// ---- API ---- +export interface SpawnResult { + ok: boolean; + action: string; + sessionId: string; + traceId: string; + conversationId: string; + accepted: boolean; + acceptedBody: unknown; +} + +export async function spawn(args: Record): Promise { + const cfg = config(); + const profile = args.profile || cfg.defaultProviderProfile; + const projectId = args.projectId || cfg.defaultProjectId; + const message = args.message || (args.messageFile ? readFileSync(args.messageFile, "utf-8") : null); + if (!message) fail("message_required", "--message or --message-file required"); + + // session + let sessionId = args.sessionId || ""; + let conversationId = args.conversationId || ""; + if (!sessionId) { + const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/sessions`, { + method: "POST", headers: authHeaders(), + body: JSON.stringify({ providerProfile: profile, projectId }) + }); + const b = await r.json() as Record; + if (!r.ok) fail("session_create_failed", JSON.stringify(b)); + const s = (b.session || b) as Record; + sessionId = s.sessionId as string; + conversationId = conversationId || (s.conversationId as string) || ""; + } + if (!conversationId) conversationId = `cnv_cli_${Date.now()}`; + + // submit + const workspaceFiles = collectWorkspaceFiles(args); + const traceId = `trc_${profile}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; + const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat`, { + method: "POST", + headers: { ...authHeaders(), "prefer": "respond-async", "x-trace-id": traceId, "x-hwlab-short-connection": "1" }, + body: JSON.stringify({ message, providerProfile: profile, sessionId, conversationId, projectId, traceId, shortConnection: true, ...(workspaceFiles.length > 0 ? { workspaceFiles } : {}) }) + }); + const b = await r.json(); + out({ + ok: true, action: "spawn", + sessionId, traceId, conversationId, + accepted: r.ok, + acceptedBody: b, + pollCommand: `bun scripts/hwlab-code-agent-cli.ts poll ${traceId}`, + resultCommand: `bun scripts/hwlab-code-agent-cli.ts result ${traceId}` + }); +} + +export async function poll(traceId: string, args: Record): Promise { + const cfg = config(); + const timeout = parseInt(args.timeout || String(cfg.defaultPollTimeoutMs), 10); + const deadline = Date.now() + timeout; + + while (Date.now() < deadline) { + const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat/result/${encodeURIComponent(traceId)}`, { + headers: authHeaders() + }); + const b = await r.json() as Record; + const s = (b.status || b.traceStatus) as string; + if (s === "completed" || s === "result") { + out({ ok: true, action: "poll", traceId, status: "completed", body: b }); + return; + } + if (s === "failed" || s === "error") { + fail("poll_failed", JSON.stringify(b)); + } + await new Promise(r => setTimeout(r, 2000)); + } + fail("poll_timeout", `timeout after ${timeout}ms`); +} + +export async function getResult(traceId: string): Promise { + const cfg = config(); + const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat/result/${encodeURIComponent(traceId)}`, { + headers: authHeaders() + }); + out({ ok: r.ok, action: "result", traceId, body: await r.json() }); +} + +export async function getTrace(traceId: string, args: Record): Promise { + const cfg = config(); + const q = args.full ? "?full=true" : ""; + const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat/trace/${encodeURIComponent(traceId)}${q}`, { + headers: authHeaders() + }); + out({ ok: r.ok, action: "trace", traceId, body: await r.json() }); +} diff --git a/tools/hwlab-code-agent-cli.ts b/tools/hwlab-code-agent-cli.ts new file mode 100644 index 00000000..16b1b69f --- /dev/null +++ b/tools/hwlab-code-agent-cli.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env bun +/** + * hwlab-code-agent — Leader Agent 调度子 agent 的入口 + * 实际逻辑在 skills/hwlab-code-agent/scripts/hwlab-code-agent-cli.ts + */ +import "../skills/hwlab-code-agent/scripts/hwlab-code-agent-cli.ts";