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
This commit is contained in:
Codex Agent
2026-06-07 18:11:43 +08:00
parent 2577ecfaf9
commit 4c3a8951f6
6 changed files with 503 additions and 0 deletions
@@ -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) {
+228
View File
@@ -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 <traceId> [--timeout 600000]
bun scripts/hwlab-code-agent-cli.ts result <traceId>
bun scripts/hwlab-code-agent-cli.ts trace <traceId> [--full]
```
输出均为 JSON。`spawn` 创建 session + 提交 prompt,立即返回 `sessionId` + `traceId``poll` 阻塞轮询至 terminal 或超时。
### hwpod spec 自动继承
子 agent 需要 `.hwlab/hwpod-spec.yaml` 才能使用 hwpod。spec 按优先级自动注入:
1. `--spec-path <file>` — 显式指定本地 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
```
---
## 外部管理 CLIG14 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 <sessionId>
# 选择当前 workspace session
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent session select <sessionId>
```
输出直接暴露 `sessionId``conversationId``threadId``providerProfile``sessionStatus`
---
## 发送 Turn
```bash
# 短消息
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \
--session-id <sessionId> --message "Hello"
# 长 prompt(文件)
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \
--session-id <sessionId> --message-file /tmp/prompt.md
# 从 trace 恢复上下文续跑
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \
--from-trace <traceId>
# 显式 provider(优先于 session 默认)
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send \
--session-id <sessionId> --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 <traceId> --render web
# 完整 trace
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent trace <traceId> --render web --full
```
trace rows 包含 `role`user/assistant/tool)、`status``text`
---
## Result 查询
```bash
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent result <traceId>
```
返回 Agent 的 `assistantText`(最终回复)、`agentRun` 信息(runId/commandId/runnerId/namespace)、`traceSummary`
---
## Inspect
```bash
bun tools/hwlab-cli/bin/hwlab-cli.ts client agent inspect --trace-id <traceId>
```
从 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 <traceId> --message "..."
```
向运行中的 Code Agent turn 注入纠偏指令。
---
## 典型流程
### CaseRun 失败调试
```bash
# 1. status 拿 sessionId/traceId
trans G14:/root/hwlab-v02 script -- \
'hwlab-cli case run status <RUN_ID> --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 <traceId> --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 <traceId>'
```
### 完整交互链路
```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 <sessionId> --message "在吗?"
# 4. 等待完成,查 result
hwlab-cli client agent result <traceId>
# 5. 拉 trace 看详细过程
hwlab-cli client agent trace <traceId> --render web
```
### Web 等价验证
Web 暴露 Code Agent 问题后,先用 CLI 对同一 Cloud Web origin 和同一 `/v1/agent/chat*` 路径复现:
```bash
# inspect 取 session 上下文
hwlab-cli client agent inspect --trace-id <traceId>
# 从 trace 恢复上下文续跑
hwlab-cli client agent send --from-trace <traceId>
```
---
## 环境变量
| 变量 | 值 | 说明 |
|---|---|---|
| `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 调度)
+6
View File
@@ -0,0 +1,6 @@
{
"apiBaseUrl": "http://74.48.78.17:19666",
"defaultProviderProfile": "deepseek",
"defaultProjectId": "prj_hwpod_workbench",
"defaultPollTimeoutMs": 300000
}
@@ -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<string, string | undefined> {
const args: Record<string, string | undefined> = {};
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 <traceId> [--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 <traceId>" } }) + "\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 <traceId> [--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 <traceId> [--timeout 600000]",
result: "bun scripts/hwlab-code-agent-cli.ts result <traceId>",
trace: "bun scripts/hwlab-code-agent-cli.ts trace <traceId> [--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);
});
@@ -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<string, string | undefined>): 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<string, string> {
return { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey()}` };
}
// ---- JSON out ----
function out(data: Record<string, unknown>, 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<string, string | undefined>): Promise<void> {
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<string, unknown>;
if (!r.ok) fail("session_create_failed", JSON.stringify(b));
const s = (b.session || b) as Record<string, unknown>;
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<string, string | undefined>): Promise<void> {
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<string, unknown>;
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<void> {
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<string, string | undefined>): Promise<void> {
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() });
}
+6
View File
@@ -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";