Merge pull request #688 from pikasTech/fix/v02-steer-multiturn-685-686
feat: support AgentRun steer and idle continuation
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
- 提供 WEB 等价的非视觉业务入口:登录鉴权、session 恢复、Device Pod 看板、Code Agent 对话、trace/result 轮询、logout 和工作台 live summary。
|
||||
- 只走 Cloud Web 同源 API surface;默认 base URL 是 `http://74.48.78.17:19666`,也可通过 `--base-url` 或 `HWLAB_CLIENT_BASE_URL` 指向其他 Cloud Web 入口。
|
||||
- Web/CLI 路径一致性优先于继续 Web 修复。Cloud Web 暴露 Code Agent、AgentRun、continuation、trace/result 或 provider 问题后,必须先能用 `bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send/result/trace/inspect --base-url http://74.48.78.17:19666 ...` 对同一 Cloud Web base URL、同一 `/v1/agent/chat`、同一 `conversationId/sessionId/threadId/retryOf` 复现或解释,再继续修 Web 状态机。`19667` Cloud API 只用于显式 admin/setup/gateway 诊断,不得替代 WEB 同源路径验收。
|
||||
- Web/CLI 路径一致性优先于继续 Web 修复。Cloud Web 暴露 Code Agent、AgentRun、continuation、steer、trace/result 或 provider 问题后,必须先能用 `bun tools/hwlab-cli/bin/hwlab-cli.ts client agent send/result/trace/inspect/steer --base-url http://74.48.78.17:19666 ...` 对同一 Cloud Web base URL、同一 `/v1/agent/chat*`、同一 `conversationId/sessionId/threadId/retryOf` 复现或解释,再继续修 Web 状态机。`19667` Cloud API 只用于显式 admin/setup/gateway 诊断,不得替代 WEB 同源路径验收。
|
||||
- 从 Web trace 回放 Code Agent 问题时,优先用 `client agent send --from-trace <traceId>` 读取 Cloud Web 的 `/v1/agent/chat/inspect`,自动带回原 `conversationId/sessionId/threadId` 并把 `retryOf` 指向来源 trace;只有 inspect 缺失时才手动传 `--conversation-id`、`--session-id`、`--thread-id` 和 `--retry-of`。CLI 输出必须包含 replay 来源、inspect 状态和 redacted continuation 摘要。
|
||||
- 默认业务子命令不直连 Postgres、Kubernetes Service、Secret、device-pod 内部 Service、gateway RPC 或本地 fixture;需要鉴权的请求使用 `/auth/*` 返回的 cookie 或显式 `--cookie`。唯一例外是 `client gateway` 诊断族:它允许显式 `--api-base-url` 指向 Cloud API,用于短连接观测 gateway session、单次 shell invoke 和 transport 压测;该入口只验证底层传输稳定性,不替代 Web 用户流程授权,也不发布镜像或常驻服务。
|
||||
- Pod 内透传执行不放进 `hwlab-cli`;需要进入正在工作的 Code Agent/Cloud API pod 时,`hwlab-cli` 只查询并输出 UniDesk 标准 route,实际透传由 UniDesk `bun scripts/cli.ts ssh 'G14:k3s:hwlab-v02:pod:<pod>:<container>' ...` 完成。`pod:` 是 route 语法,`/` 只用于 pod 内文件系统路径。
|
||||
@@ -26,6 +26,7 @@
|
||||
- `client agent send` 是 Cloud Web Code Agent composer 的非视觉等价入口。它必须支持 `--from-trace`、`--conversation-id`、`--session-id`、`--thread-id` 和 `--retry-of`,并在输出中返回 redacted continuation 摘要,证明本次 CLI 请求是否覆盖 Web 的继续会话路径。浏览器 issue 中已经给出 traceId 时,复现命令优先使用 `--from-trace <traceId>`,让 CLI 先走 `/v1/agent/chat/inspect` 读取 Web 上下文,再提交同源 `/v1/agent/chat`。
|
||||
- Code Agent continuation 的 thread 字段只有 `threadId` 一个标准名称。CLI 读取 inspect、`--from-trace` 回放、手动 `--thread-id` 提交和输出摘要都必须以该字段为唯一 thread identity;服务端响应也应保持同一字段口径。
|
||||
- `client agent send` 默认先恢复账号 workspace,再向 `/v1/agent/chat` 发送 `workspaceId` 和 `expectedWorkspaceRevision`;服务端接受后 CLI 保存新的 workspace revision,终态轮询后再 PATCH workspace 清理终态 `activeTraceId`。只有显式 `--no-workspace` 才跳过这一默认恢复路径。
|
||||
- `client agent steer <traceId>` 是运行中引导入口,必须调用 Cloud Web 同源 `POST /v1/agent/chat/steer`,把 steer 文本装配成 AgentRun `type=steer` command 作用到目标 trace 的 active turn。CLI 不手动穿内部 URL;验收使用当前 runtime namespace/lane 自动解析的 `19666` Web 入口,并通过原 trace 的 result/trace 观察 steer 是否被 runner 接收和应用。
|
||||
- `client agent trace <traceId> --render web` 必须调用 Cloud Web trace row 的同一纯转换路径,输出 `render="web"`、renderer 标识、source event count、rendered row count、默认压制的 noise event count 和 row 摘要。浏览器 trace 展示错乱时,必须先用该 CLI 入口确认 Web 渲染转换是否已经乱序、重复、缺 final response、吞掉关键 row 或只显示泛化 tool call,再继续修浏览器 DOM/CSS。
|
||||
- AgentRun v0.1 短连接 runner 已要求支持同 run/runner 多轮 command。CLI 仍应把 Web 提交的 `conversationId/sessionId/threadId` 原样送到 Cloud Web API,用于验证 adapter 是否在 runner lease 有效时复用同一个 AgentRun `runId` / `jobName` 并创建新 `commandId`;每轮都新建 runner 或重新 bundle 不是通过状态,trace 中的原因说明只能用于定位。
|
||||
- `client harness`、`client harness-ops` 和 `client harness-opt` 吸收 G14 harness-ops 的短连接业务能力:health、submit、result、trace、wait 和 audit。
|
||||
@@ -58,6 +59,7 @@
|
||||
| `hwlab-cli client gateway pressure` | `POST Cloud API /v1/rpc/hardware.invoke.shell` | 显式 Cloud API 压测入口,真实验证大输出、长单行、stderr、timeout 和并发超容量不会造成黑洞。 |
|
||||
| `hwlab-cli client agent send` | `GET /v1/agent/chat/inspect` + `POST /v1/agent/chat` + `GET /result/{trace}` | 以 short connection 提交 Code Agent 消息并轮询结果,默认输出 assistant 回复文本摘要;支持 `--from-trace` 自动复现 Web continuation,也支持手动 `--conversation-id`、`--session-id`、`--thread-id`、`--retry-of`。 |
|
||||
| `hwlab-cli client agent trace TRACE [--render web]` | `GET /v1/agent/chat/trace/{trace}` | 回放 trace,默认输出状态、事件摘要和 assistant stream 文本;`--render web` 复用 Cloud Web trace row 转换,便于 CLI 复现 Web trace 渲染问题。 |
|
||||
| `hwlab-cli client agent steer TRACE` | `POST /v1/agent/chat/steer` | 对目标 trace 的运行中 Code Agent turn 发送 steer 文本;默认短连接返回 accepted 和 steer command 摘要,后续观察原 trace 的 result/trace。 |
|
||||
| `hwlab-cli client agent cancel TRACE` | `POST /v1/agent/chat/cancel` | 取消当前 Code Agent 请求。 |
|
||||
| `hwlab-cli client harness submit` | `POST /v1/agent/chat` | G14 harness-ops 的短连接提交入口,默认 provider profile 为 `deepseek`,返回 trace/result URL;`harness-ops` 和 `harness-opt` 是同义别名。 |
|
||||
| `hwlab-cli client harness wait/result/trace` | `GET /v1/agent/chat/result/{trace}`、`GET /trace/{trace}` | 轮询或读取一次 Code Agent 结果和 trace;单次 wait 最长 60 秒。 |
|
||||
@@ -99,6 +101,10 @@
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cli.md,然后在 `G14:/root/hwlab-v02` 用 cli 手动测试以下内容:对一个来自 Cloud Web 的真实 trace 运行 `client agent trace <traceId> --render web --limit 80`,确认输出 `renderer=web/hwlab-cloud-web/app-trace:traceDisplayRows`、source event count、rendered row count 和 row 摘要;如果 Web trace 展示错乱,应先用该命令复现并定位到 Web row 转换还是浏览器 DOM/CSS 层。
|
||||
|
||||
## T3.3
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cli.md,然后在 `G14:/root/hwlab-v02` 用 cli 手动测试以下内容:先运行 `client agent send --message "执行一个会持续运行的任务,等待后续 steer" --provider-profile deepseek` 获得运行中 trace,再运行 `client agent steer <traceId> --message "请把最终回复包含 STEER_ACCEPTED 标记"`,最后用 `client agent result <traceId>` 和 `client agent trace <traceId> --render web` 确认同一 target trace 出现 AgentRun steer command 事件且最终回复或 trace 可见 steer 处理结果。该验收必须使用真实 `http://74.48.78.17:19666` 或 runtime namespace 自动解析出的同源 Web 入口,不能 mock,也不能用自动交互脚本。
|
||||
|
||||
## T4
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cli.md,然后在 `G14:/root/hwlab-v02` 用 cli 手动测试以下内容:运行 `client request GET /v1/access/status`、`client request GET /v1/diagnostics/gate` 和 `client rpc system.health`,确认它们全部走 `19666` Cloud Web 同源 API,返回 JSON route/httpStatus/body,且 `client request` 传入绝对 URL 会被拒绝。
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
| `GET /v1/diagnostics/gate`、`GET /v1/live-builds` | 诊断和 live build inventory。 |
|
||||
| `GET /v1/gateway/sessions`、`POST /v1/gateway/poll`、`POST /v1/gateway/result` | gateway 主动出站注册、取任务和回传结果。 |
|
||||
| `POST /v1/internal/device-pod/gateway-dispatch` | 仅接受 `hwlab-device-pod` 内部服务凭据,用于把 executor job dispatch 到 gateway poll/result;普通用户和 Code Agent 不可调用。 |
|
||||
| `POST /v1/agent/chat`、`GET /v1/agent/chat/result/{traceId}`、`GET /v1/agent/chat/trace/{traceId}`、`POST /v1/agent/chat/cancel` | Code Agent 短连接提交、轮询、trace 和取消。 |
|
||||
| `POST /v1/agent/chat`、`POST /v1/agent/chat/steer`、`GET /v1/agent/chat/result/{traceId}`、`GET /v1/agent/chat/trace/{traceId}`、`POST /v1/agent/chat/cancel` | Code Agent 短连接提交、运行中 steer、轮询、trace 和取消。 |
|
||||
|
||||
用户、权限、device-pod 管理 API 的最终规格见 [spec-user-access.md](spec-user-access.md) 和 [spec-device-pod.md](spec-device-pod.md)。
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
## T2
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-api.md,然后用 cli 手动测试以下内容:使用短连接 `POST /v1/agent/chat` 提交一次对话,再轮询 `/v1/agent/chat/result/{traceId}`,只有 `status=completed` 且 assistant reply 非空才算 Code Agent 通过。
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-api.md,然后用 cli 手动测试以下内容:使用短连接 `POST /v1/agent/chat` 提交一次对话,再轮询 `/v1/agent/chat/result/{traceId}`,只有 `status=completed` 且 assistant reply 非空才算 Code Agent 通过;对运行中请求使用 `POST /v1/agent/chat/steer` 时,响应必须包含 target trace、steerTraceId、AgentRun runId/targetCommandId/steerCommandId 和继续观察原 trace 的 result/trace URL。
|
||||
|
||||
## T2.1
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
| 规格项 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| health/readiness | 已实现 | `/health/live` 汇总 DB、runtime、Code Agent 和 blocker。 |
|
||||
| Code Agent 短连接 submit/result/trace/cancel | 已实现 | repo-owned Codex stdio 和 provider profile 已接入。 |
|
||||
| Code Agent 短连接 submit/steer/result/trace/cancel | 已实现 | repo-owned Codex stdio、运行中 steer 和 provider profile 已接入。 |
|
||||
| Postgres durable runtime | 已实现 | 通过 v02 独立 DB SecretRef 和 migration ledger 判定。 |
|
||||
| gateway outbound poll/result | 已实现 | 支持 gateway 主动轮询和 `hardware.invoke.shell` 分发。 |
|
||||
| device-pod 正式权限/profile/job | 部分实现 | profile/grant/list/status/job 持久化在 cloud-api;用户态 probe GET 已收敛为只读 job;已提供内部 gateway dispatch route 供 `hwlab-device-pod` executor 下发到 device-host-cli,无在线 gateway/device-host-cli 时返回 blocker。 |
|
||||
|
||||
@@ -94,9 +94,9 @@ test("workbench workspace is account-scoped, persistent, and gates active agent
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_issue655_shared",
|
||||
threadId: "thread-issue-655",
|
||||
sessionStatus: "active",
|
||||
sessionStatus: "running",
|
||||
lastTraceId: "trc_issue655_previous",
|
||||
messages: [{ role: "user", text: "persist me", traceId: "trc_issue655_previous" }]
|
||||
messages: [{ role: "agent", text: "persist me", status: "running", traceId: "trc_issue655_active" }]
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(conversation.status, 200);
|
||||
assert.equal(conversation.body.conversation.conversationId, "cnv_issue655_shared");
|
||||
@@ -401,6 +401,86 @@ test("workbench workspace ignores stale active trace when selected conversation
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench workspace permits continuation when stale active trace has idle active conversation", async () => {
|
||||
const workspaceDir = await mkdtemp("/tmp/hwlab-stale-active-idle-");
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspaceDir,
|
||||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
||||
OPENAI_API_KEY: "test-openai-key-material"
|
||||
},
|
||||
codexStdioManager: {
|
||||
describe: () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
|
||||
probe: async () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
|
||||
chat: async (params = {}) => ({
|
||||
status: "completed",
|
||||
traceId: params.traceId,
|
||||
conversationId: params.conversationId,
|
||||
sessionId: params.sessionId,
|
||||
threadId: params.threadId,
|
||||
reply: { role: "assistant", content: "ok" },
|
||||
runnerTrace: { traceId: params.traceId, status: "completed", events: [], eventCount: 0 },
|
||||
session: { sessionId: params.sessionId, conversationId: params.conversationId, threadId: params.threadId, status: "idle" }
|
||||
}),
|
||||
cancel() {},
|
||||
reapIdle() {}
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
|
||||
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", adminLogin.cookie);
|
||||
const conversation = await putJson(port, "/v1/agent/conversations/cnv_stale_active_idle", {
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_stale_active_idle",
|
||||
threadId: "thread-stale-active-idle",
|
||||
status: "active",
|
||||
sessionStatus: "active",
|
||||
lastTraceId: "trc_stale_active_idle_missing",
|
||||
messages: [{ role: "agent", text: "previous completed turn", status: "completed", traceId: "trc_stale_active_idle_missing" }]
|
||||
}, adminLogin.cookie);
|
||||
assert.equal(conversation.status, 200);
|
||||
|
||||
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
|
||||
expectedRevision: 1,
|
||||
selectedConversationId: "cnv_stale_active_idle",
|
||||
selectedAgentSessionId: "ses_stale_active_idle",
|
||||
activeTraceId: "trc_stale_active_idle_missing",
|
||||
sessionStatus: "running",
|
||||
updatedByClient: "test-suite"
|
||||
}, adminLogin.cookie);
|
||||
assert.equal(update.status, 200);
|
||||
|
||||
const next = await postJson(port, "/v1/agent/chat", {
|
||||
message: "new turn after stale active idle trace",
|
||||
traceId: "trc_stale_active_idle_next",
|
||||
conversationId: "cnv_stale_active_idle",
|
||||
sessionId: "ses_stale_active_idle",
|
||||
threadId: "thread-stale-active-idle",
|
||||
workspaceId: workspace.body.workspace.workspaceId,
|
||||
expectedWorkspaceRevision: 2,
|
||||
shortConnection: true
|
||||
}, adminLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_stale_active_idle_next" });
|
||||
|
||||
assert.equal(next.status, 202);
|
||||
assert.equal(next.body.traceId, "trc_stale_active_idle_next");
|
||||
assert.equal(next.body.conversationId, "cnv_stale_active_idle");
|
||||
assert.equal(next.body.sessionId, "ses_stale_active_idle");
|
||||
|
||||
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", adminLogin.cookie);
|
||||
assert.equal(restored.body.workspace.activeTraceId, "trc_stale_active_idle_next");
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench workspace status clears completed AgentRun active trace on read", async () => {
|
||||
const agentRunCalls = [];
|
||||
const agentSessions = new Map();
|
||||
|
||||
@@ -314,6 +314,87 @@ export async function cancelAgentRunChatTurn({ traceId, currentResult = null, op
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function steerAgentRunChatTurn({ traceId, currentResult = null, params = {}, options = {}, traceStore = defaultCodeAgentTraceStore }) {
|
||||
const mapped = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
|
||||
if (!safeTraceId(traceId) || !mapped?.agentRun?.runId || !mapped?.agentRun?.commandId) return null;
|
||||
const message = firstNonEmpty(params.message, params.prompt, params.text);
|
||||
if (!message) {
|
||||
throw Object.assign(new Error("steer command requires non-empty message text"), { code: "steer_message_missing", statusCode: 400 });
|
||||
}
|
||||
const env = options.env ?? process.env;
|
||||
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
||||
const managerUrl = resolveAgentRunManagerUrl(env, mapped.agentRun.managerUrl);
|
||||
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
|
||||
const steerTraceId = safeTraceId(params.steerTraceId ?? params.steerId) || `trc_steer_${randomUUID().replace(/-/gu, "")}`;
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:steer:accepted",
|
||||
message: "HWLAB accepted an in-flight steer request and will create an AgentRun type=steer command on the existing run.",
|
||||
runId: mapped.agentRun.runId,
|
||||
commandId: mapped.agentRun.commandId,
|
||||
steerTraceId,
|
||||
waitingFor: "agentrun-steer-command-create",
|
||||
valuesPrinted: false
|
||||
});
|
||||
const commandInput = buildAgentRunSteerCommandInput({ params: { ...params, message }, traceId, steerTraceId, mapped });
|
||||
const command = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(mapped.agentRun.runId)}/commands`, {
|
||||
method: "POST",
|
||||
body: commandInput,
|
||||
timeoutMs
|
||||
});
|
||||
const steerCommandId = requiredString(command?.id, "command.id");
|
||||
const now = nowIso(options.now);
|
||||
const agentRun = {
|
||||
...mapped.agentRun,
|
||||
lastSteerCommandId: steerCommandId,
|
||||
lastSteerTraceId: steerTraceId,
|
||||
updatedAt: now,
|
||||
valuesPrinted: false
|
||||
};
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:steer:command-created",
|
||||
message: `AgentRun steer command ${steerCommandId} created on run ${mapped.agentRun.runId}; runner will apply it if the target Codex turn is still active.`,
|
||||
runId: mapped.agentRun.runId,
|
||||
commandId: mapped.agentRun.commandId,
|
||||
steerCommandId,
|
||||
targetCommandId: mapped.agentRun.commandId,
|
||||
steerTraceId,
|
||||
waitingFor: "agentrun-steer-apply",
|
||||
valuesPrinted: false
|
||||
});
|
||||
const payload = {
|
||||
ok: true,
|
||||
accepted: true,
|
||||
status: "running",
|
||||
shortConnection: true,
|
||||
controlSemantics: "steer-active-turn-and-poll-target-trace",
|
||||
route: "/v1/agent/chat/steer",
|
||||
traceId,
|
||||
targetTraceId: traceId,
|
||||
steerTraceId,
|
||||
conversationId: safeConversationId(mapped.conversationId ?? params.conversationId) || null,
|
||||
sessionId: safeSessionId(mapped.sessionId ?? params.sessionId) || null,
|
||||
threadId: safeOpaqueId(mapped.threadId ?? params.threadId) || null,
|
||||
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
|
||||
traceUrl: `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`,
|
||||
agentRun: {
|
||||
...agentRun,
|
||||
steerCommandId,
|
||||
targetCommandId: mapped.agentRun.commandId,
|
||||
steerCommandState: command.state ?? null,
|
||||
valuesPrinted: false
|
||||
},
|
||||
runnerTrace: traceStore.snapshot(traceId, agentRunTraceMeta({}, {})),
|
||||
updatedAt: now,
|
||||
valuesPrinted: false
|
||||
};
|
||||
options.codeAgentChatResults?.set?.(traceId, { ...mapped, agentRun, runnerTrace: payload.runnerTrace, updatedAt: now });
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function loadPersistedAgentRunResult(traceId, options = {}) {
|
||||
if (!safeTraceId(traceId) || typeof options.accessController?.getAgentSessionByTraceId !== "function") return null;
|
||||
const session = await options.accessController.getAgentSessionByTraceId(traceId);
|
||||
@@ -459,6 +540,28 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgentRunSteerCommandInput({ params, traceId, steerTraceId, mapped }) {
|
||||
const prompt = String(params.message ?? params.prompt ?? params.text ?? "").trim();
|
||||
return {
|
||||
type: "steer",
|
||||
payload: {
|
||||
prompt,
|
||||
message: prompt,
|
||||
text: prompt,
|
||||
traceId: steerTraceId,
|
||||
targetTraceId: traceId,
|
||||
targetCommandId: mapped.agentRun.commandId,
|
||||
conversationId: safeConversationId(mapped.conversationId ?? params.conversationId) || null,
|
||||
sessionId: mapped.agentRun.sessionId ?? safeSessionId(mapped.sessionId ?? params.sessionId) ?? null,
|
||||
hwlabSessionId: safeSessionId(mapped.sessionId ?? params.sessionId) || null,
|
||||
threadId: safeOpaqueId(mapped.threadId ?? params.threadId) || null,
|
||||
source: "hwlab-cloud-api",
|
||||
valuesPrinted: false
|
||||
},
|
||||
idempotencyKey: steerTraceId
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgentRunRunnerJobInput({ env, traceId, commandId }) {
|
||||
const transientEnv = buildAgentRunTransientEnv(env);
|
||||
return {
|
||||
|
||||
@@ -184,6 +184,17 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
return send({ id: "run_hwlab_adapter", status: "pending", backendProfile: "deepseek", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands") {
|
||||
if (body.type === "steer") {
|
||||
assert.equal(body.payload.targetTraceId, "trc_server-test-agentrun-adapter");
|
||||
assert.equal(body.payload.targetCommandId, "cmd_hwlab_adapter");
|
||||
assert.equal(body.payload.traceId, "trc_steer_server_test");
|
||||
assert.match(body.payload.prompt, /STEER_MARK/u);
|
||||
assert.equal(body.payload.sessionId, "ses_agentrun_deepseek_server_test_agentrun");
|
||||
assert.equal(body.payload.hwlabSessionId, "ses_server-test-agentrun");
|
||||
assert.equal(body.payload.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
||||
assert.equal(body.idempotencyKey, "trc_steer_server_test");
|
||||
return send({ id: "cmd_hwlab_adapter_steer", runId: "run_hwlab_adapter", state: "pending", type: "steer", seq: 7 });
|
||||
}
|
||||
assert.equal(body.type, "turn");
|
||||
assert.match(body.payload.prompt, /AgentRun adapter/u);
|
||||
assert.equal(body.payload.sessionId, "ses_agentrun_deepseek_server_test_agentrun");
|
||||
@@ -381,6 +392,29 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.match(commandTraceEvent.command, /hwpod profile list/u);
|
||||
assert.match(commandTraceEvent.stdoutSummary, /profile\.list/u);
|
||||
|
||||
const steer = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/steer`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "x-trace-id": traceId },
|
||||
body: JSON.stringify({
|
||||
traceId,
|
||||
steerTraceId: "trc_steer_server_test",
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
message: "请按 STEER_MARK 调整最终回复"
|
||||
})
|
||||
});
|
||||
assert.equal(steer.status, 202);
|
||||
const steerBody = await steer.json();
|
||||
assert.equal(steerBody.accepted, true);
|
||||
assert.equal(steerBody.route, "/v1/agent/chat/steer");
|
||||
assert.equal(steerBody.traceId, traceId);
|
||||
assert.equal(steerBody.steerTraceId, "trc_steer_server_test");
|
||||
assert.equal(steerBody.agentRun.runId, "run_hwlab_adapter");
|
||||
assert.equal(steerBody.agentRun.targetCommandId, "cmd_hwlab_adapter");
|
||||
assert.equal(steerBody.agentRun.steerCommandId, "cmd_hwlab_adapter_steer");
|
||||
assert.ok(steerBody.runnerTrace.events.some((event) => event.label === "agentrun:steer:command-created"));
|
||||
|
||||
const secondTraceId = "trc_server-test-agentrun-adapter-second";
|
||||
const second = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||||
method: "POST",
|
||||
@@ -413,7 +447,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.equal(secondPayload.runnerTrace.events.some((event) => String(event.text ?? event.message ?? "").includes("旧 command 尾部")), false);
|
||||
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1);
|
||||
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs/run_hwlab_adapter/runner-jobs").length, 1);
|
||||
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs/run_hwlab_adapter/commands").length, 2);
|
||||
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs/run_hwlab_adapter/commands").length, 3);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
initialAgentRunChatResult,
|
||||
loadPersistedAgentRunResult,
|
||||
refreshAgentRunTrace,
|
||||
steerAgentRunChatTurn,
|
||||
submitAgentRunChatTurn,
|
||||
syncAgentRunChatResult
|
||||
} from "./code-agent-agentrun-adapter.ts";
|
||||
@@ -874,6 +875,130 @@ function recordCodeAgentConversationFact(payload = {}, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleCodeAgentSteerHttp(request, response, options) {
|
||||
const body = await readBody(request, options.bodyLimitBytes);
|
||||
let params = {};
|
||||
|
||||
try {
|
||||
params = body ? JSON.parse(body) : {};
|
||||
} catch (error) {
|
||||
sendJson(response, 400, {
|
||||
...createCodeAgentErrorPayload({
|
||||
code: "parse_error",
|
||||
message: "Invalid JSON body",
|
||||
reason: error.message,
|
||||
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
|
||||
layer: "api",
|
||||
retryable: true,
|
||||
route: "/v1/agent/chat/steer"
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
||||
sendJson(response, 400, {
|
||||
...createCodeAgentErrorPayload({
|
||||
code: "invalid_params",
|
||||
message: "Code Agent steer body must be a JSON object",
|
||||
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
|
||||
layer: "api",
|
||||
retryable: true,
|
||||
route: "/v1/agent/chat/steer"
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId || params.targetTraceId);
|
||||
const message = firstNonEmptyValue(params.message, params.prompt, params.text);
|
||||
if (!traceId || !message) {
|
||||
sendJson(response, 400, {
|
||||
...createCodeAgentErrorPayload({
|
||||
code: !traceId ? "steer_trace_missing" : "steer_message_missing",
|
||||
message: !traceId ? "traceId is required to steer the current Code Agent turn." : "message is required to steer the current Code Agent turn.",
|
||||
traceId: traceId || "trc_unassigned",
|
||||
layer: "api",
|
||||
retryable: true,
|
||||
route: "/v1/agent/chat/steer"
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
||||
const currentResult = options.codeAgentChatResults?.get?.(traceId) ?? await loadPersistedAgentRunResult(traceId, options);
|
||||
if (!canAccessOwnedResult(currentResult, options.actor)) {
|
||||
sendJson(response, 403, {
|
||||
ok: false,
|
||||
status: "forbidden",
|
||||
traceId,
|
||||
error: { code: "code_agent_trace_forbidden", message: "Code Agent trace is not visible to the current actor" }
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!currentResult?.agentRun?.runId || !currentResult?.agentRun?.commandId) {
|
||||
sendJson(response, 404, {
|
||||
ok: false,
|
||||
status: "not_found",
|
||||
traceId,
|
||||
error: { code: "steer_trace_not_found", message: "No AgentRun-backed Code Agent turn was found for the requested traceId." },
|
||||
route: "/v1/agent/chat/steer",
|
||||
runnerTrace: traceStore.snapshot(traceId)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await steerAgentRunChatTurn({
|
||||
traceId,
|
||||
currentResult,
|
||||
params: {
|
||||
...params,
|
||||
message,
|
||||
ownerUserId: options.actor?.id ?? params.ownerUserId,
|
||||
ownerRole: options.actor?.role ?? params.ownerRole
|
||||
},
|
||||
options,
|
||||
traceStore
|
||||
});
|
||||
await recordCodeAgentSessionOwner({ payload: currentResult, params: { ...params, traceId, ownerUserId: options.actor?.id, ownerRole: options.actor?.role }, options, status: "running" });
|
||||
sendJson(response, 202, payload);
|
||||
} catch (error) {
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "failed",
|
||||
label: "agentrun:steer:failed",
|
||||
errorCode: error?.code ?? "agentrun_steer_failed",
|
||||
message: error?.message ?? "AgentRun steer command failed.",
|
||||
terminal: false,
|
||||
valuesPrinted: false
|
||||
});
|
||||
sendJson(response, error?.statusCode === 404 ? 404 : 409, {
|
||||
ok: false,
|
||||
accepted: false,
|
||||
status: "failed",
|
||||
traceId,
|
||||
route: "/v1/agent/chat/steer",
|
||||
conversationId: safeConversationId(currentResult.conversationId ?? params.conversationId) || null,
|
||||
sessionId: safeSessionId(currentResult.sessionId ?? params.sessionId) || null,
|
||||
threadId: safeOpaqueId(currentResult.threadId ?? params.threadId) || null,
|
||||
agentRun: currentResult.agentRun,
|
||||
runnerTrace: traceStore.snapshot(traceId),
|
||||
error: {
|
||||
code: error?.code ?? "agentrun_steer_failed",
|
||||
layer: "agentrun",
|
||||
category: "steer_failed",
|
||||
retryable: true,
|
||||
message: error?.message ?? "AgentRun steer command failed.",
|
||||
traceId,
|
||||
route: "/v1/agent/chat/steer",
|
||||
toolName: "agentrun.command.steer"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options = {}, status = "active" } = {}) {
|
||||
const ownerUserId = options.actor?.id ?? params.ownerUserId;
|
||||
if (!ownerUserId || !options.accessController?.recordAgentSessionOwner) return null;
|
||||
@@ -950,6 +1075,9 @@ async function claimWorkbenchWorkspaceTurn({ params = {}, options = {}, traceId,
|
||||
activeTraceId && (activeResult?.status === "running" || (!activeResult && isActiveWorkbenchConversation(selectedConversation)))
|
||||
);
|
||||
if (activeTraceBlocksTurn) {
|
||||
const conversationId = safeConversationId(params.conversationId) || selectedConversationId || activeResult?.conversationId || null;
|
||||
const sessionId = safeSessionId(params.sessionId) || safeSessionId(workspace.selectedAgentSessionId) || activeResult?.sessionId || null;
|
||||
const threadId = safeOpaqueId(params.threadId) || activeResult?.threadId || null;
|
||||
sendJson(response, 409, {
|
||||
ok: false,
|
||||
status: "workspace_agent_busy",
|
||||
@@ -957,6 +1085,10 @@ async function claimWorkbenchWorkspaceTurn({ params = {}, options = {}, traceId,
|
||||
workspaceId,
|
||||
workspaceRevision: workspace.revision,
|
||||
activeTraceId,
|
||||
conversationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
providerTrace: activeResult?.providerTrace ?? activeResult?.agentRun?.providerTrace ?? null,
|
||||
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(activeTraceId)}`,
|
||||
traceUrl: `/v1/agent/chat/trace/${encodeURIComponent(activeTraceId)}`
|
||||
});
|
||||
@@ -994,7 +1126,7 @@ function isActiveWorkbenchConversation(conversation = null) {
|
||||
for (const message of Array.isArray(conversation.messages) ? conversation.messages.slice(-5) : []) {
|
||||
values.push(message?.status, message?.runnerTrace?.sessionStatus, message?.sessionStatus);
|
||||
}
|
||||
return values.some((value) => ["running", "busy", "pending", "active", "queued"].includes(textValue(value).toLowerCase()));
|
||||
return values.some((value) => ["running", "busy", "pending", "queued"].includes(textValue(value).toLowerCase()));
|
||||
}
|
||||
|
||||
async function resolveWorkbenchActiveTraceResult(traceId, options = {}) {
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
handleCodeAgentChatHttp,
|
||||
handleCodeAgentChatResultHttp,
|
||||
handleCodeAgentInspectHttp,
|
||||
handleCodeAgentSteerHttp,
|
||||
handleCodeAgentTraceHttp
|
||||
} from "./server-code-agent-http.ts";
|
||||
import { handleM3IoControlHttp } from "./server-m3-http.ts";
|
||||
@@ -452,6 +453,13 @@ async function handleRestAdapter(request, response, url, options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url.pathname === "/v1/agent/chat/steer") {
|
||||
const nextOptions = await codeAgentOptions(request, response, options);
|
||||
if (!nextOptions) return;
|
||||
await handleCodeAgentSteerHttp(request, response, nextOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/chat/trace/")) {
|
||||
const nextOptions = await codeAgentOptions(request, response, options);
|
||||
if (!nextOptions) return;
|
||||
|
||||
@@ -499,6 +499,66 @@ test("hwlab-cli client agent send can replay continuation from an existing trace
|
||||
assert.equal(JSON.stringify(result.payload).includes("hwlab_session=session-a"), false);
|
||||
});
|
||||
|
||||
test("hwlab-cli client agent steer posts to Web-equivalent steer route", async () => {
|
||||
const calls: any[] = [];
|
||||
const result = await runHwlabCli([
|
||||
"client",
|
||||
"agent",
|
||||
"steer",
|
||||
"trc_target_steer",
|
||||
"--base-url",
|
||||
"http://web.test",
|
||||
"--cookie",
|
||||
"hwlab_session=session-a",
|
||||
"--message",
|
||||
"please include STEER_MARK",
|
||||
"--steer-trace-id",
|
||||
"trc_steer_cli",
|
||||
"--conversation-id",
|
||||
"cnv_steer",
|
||||
"--session-id",
|
||||
"ses_steer",
|
||||
"--thread-id",
|
||||
"thread-steer"
|
||||
], {
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url: String(url), init, body: init?.body ? JSON.parse(String(init.body)) : null });
|
||||
return new Response(JSON.stringify({
|
||||
ok: true,
|
||||
accepted: true,
|
||||
status: "running",
|
||||
route: "/v1/agent/chat/steer",
|
||||
traceId: "trc_target_steer",
|
||||
steerTraceId: "trc_steer_cli",
|
||||
conversationId: "cnv_steer",
|
||||
sessionId: "ses_steer",
|
||||
agentRun: { runId: "run_steer", targetCommandId: "cmd_turn", steerCommandId: "cmd_steer" },
|
||||
traceUrl: "/v1/agent/chat/trace/trc_target_steer",
|
||||
resultUrl: "/v1/agent/chat/result/trc_target_steer"
|
||||
}), { status: 202 });
|
||||
},
|
||||
sleep: async () => {}
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(calls[0].url, "http://web.test/v1/agent/chat/steer");
|
||||
assert.equal(calls[0].init.headers["x-trace-id"], "trc_target_steer");
|
||||
assert.equal(calls[0].init.headers.prefer, "respond-async");
|
||||
assert.equal(calls[0].body.traceId, "trc_target_steer");
|
||||
assert.equal(calls[0].body.targetTraceId, "trc_target_steer");
|
||||
assert.equal(calls[0].body.steerTraceId, "trc_steer_cli");
|
||||
assert.equal(calls[0].body.message, "please include STEER_MARK");
|
||||
assert.equal(calls[0].body.conversationId, "cnv_steer");
|
||||
assert.equal(calls[0].body.sessionId, "ses_steer");
|
||||
assert.equal(calls[0].body.threadId, "thread-steer");
|
||||
assert.equal(result.payload.action, "client.agent.steer");
|
||||
assert.equal(result.payload.route.path, "/v1/agent/chat/steer");
|
||||
assert.equal(result.payload.traceId, "trc_target_steer");
|
||||
assert.equal(result.payload.steerTraceId, "trc_steer_cli");
|
||||
assert.equal(result.payload.body.agentRun.steerCommandId, "cmd_steer");
|
||||
assert.equal(result.payload.waitPolicy.webEquivalent, true);
|
||||
});
|
||||
|
||||
test("hwlab-cli client agent trace shows assistant stream text in compact output", async () => {
|
||||
const result = await runHwlabCli(["client", "agent", "trace", "trc_test", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a"], {
|
||||
fetchImpl: async () => new Response(JSON.stringify({
|
||||
|
||||
@@ -129,6 +129,7 @@ function help() {
|
||||
"hwlab-cli client agent trace TRACE_ID [--render web]",
|
||||
"hwlab-cli client agent inspect --trace-id TRACE_ID",
|
||||
"hwlab-cli client agent cancel TRACE_ID",
|
||||
"hwlab-cli client agent steer TRACE_ID --message TEXT|--message-file PATH",
|
||||
"hwlab-cli client harness submit --message TEXT --provider-profile deepseek|codex-api|minimax-m3",
|
||||
"hwlab-cli client harness wait TRACE_ID --timeout-ms 50000",
|
||||
"hwlab-cli client harness audit TRACE_ID --require-bootsharp"
|
||||
@@ -603,6 +604,7 @@ async function agentCommand(context: any) {
|
||||
const response = await requestJson({ ...context, method: "GET", path: pathName });
|
||||
return responsePayload("client.agent.inspect", response, context, { route: route("GET", pathName), body: responseBodyForCli(response.body, context.parsed) });
|
||||
}
|
||||
if (subcommand === "steer") return agentSteer(context);
|
||||
if (subcommand === "cancel") {
|
||||
const traceId = requiredText(context.rest[1] ?? context.parsed.traceId, "traceId");
|
||||
const response = await requestJson({ ...context, method: "POST", path: "/v1/agent/chat/cancel", body: clean({ traceId, conversationId: text(context.parsed.conversationId), sessionId: text(context.parsed.sessionId), threadId: text(context.parsed.threadId) }), extraHeaders: { "x-trace-id": traceId } });
|
||||
@@ -621,11 +623,55 @@ function agentHelp() {
|
||||
"result TRACE_ID [--full]",
|
||||
"trace TRACE_ID [--render web] [--limit N] [--full]",
|
||||
"inspect --trace-id TRACE_ID|--conversation-id ID|--session-id ID|--thread-id ID",
|
||||
"steer TRACE_ID --message TEXT|--message-file PATH [--conversation-id ID] [--session-id ID] [--thread-id ID]",
|
||||
"cancel TRACE_ID"
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async function agentSteer(context: any) {
|
||||
const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId ?? context.parsed.targetTraceId);
|
||||
const message = await messageValue(context);
|
||||
if (!message) throw cliError("message_required", "client agent steer requires --message or stdin text");
|
||||
const steerTraceId = text(context.parsed.steerTraceId) || makeId("trc_steer");
|
||||
const body = clean({
|
||||
traceId,
|
||||
targetTraceId: traceId,
|
||||
steerTraceId,
|
||||
message,
|
||||
conversationId: text(context.parsed.conversationId),
|
||||
sessionId: text(context.parsed.sessionId),
|
||||
threadId: text(context.parsed.threadId)
|
||||
});
|
||||
const response = await requestJson({
|
||||
...context,
|
||||
method: "POST",
|
||||
path: "/v1/agent/chat/steer",
|
||||
body,
|
||||
timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS,
|
||||
extraHeaders: {
|
||||
"x-trace-id": traceId,
|
||||
"prefer": "respond-async",
|
||||
"x-hwlab-short-connection": "1"
|
||||
}
|
||||
});
|
||||
return responsePayload("client.agent.steer", response, context, {
|
||||
route: route("POST", "/v1/agent/chat/steer"),
|
||||
traceId,
|
||||
steerTraceId,
|
||||
body: responseBodyForCli(response.body, context.parsed),
|
||||
waitPolicy: {
|
||||
defaultWait: false,
|
||||
webEquivalent: true,
|
||||
reason: "Steer is applied to the active Code Agent turn; observe the original target trace/result to confirm whether the runner accepted and applied it.",
|
||||
nextCommands: [
|
||||
`hwlab-cli client agent result ${traceId}`,
|
||||
`hwlab-cli client agent trace ${traceId} --render web`
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function agentInspectPath(context: any) {
|
||||
const params = new URLSearchParams();
|
||||
const traceId = text(context.rest[1] ?? context.parsed.traceId);
|
||||
@@ -1786,6 +1832,7 @@ function compactApiBody(body: any): any {
|
||||
adapter: body.adapter,
|
||||
sourceKind: body.sourceKind,
|
||||
liveBackend: body.liveBackend,
|
||||
agentRun: compactAgentRun(body.agentRun),
|
||||
rowCount: body.rowCount,
|
||||
columns: Array.isArray(body.columns) ? body.columns : undefined,
|
||||
roles: Array.isArray(body.roles) ? body.roles : undefined,
|
||||
@@ -1809,6 +1856,29 @@ function compactApiBody(body: any): any {
|
||||
});
|
||||
}
|
||||
|
||||
function compactAgentRun(value: any) {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
return pruneUndefined({
|
||||
adapter: value.adapter,
|
||||
runId: value.runId,
|
||||
commandId: value.commandId,
|
||||
targetCommandId: value.targetCommandId,
|
||||
steerCommandId: value.steerCommandId,
|
||||
lastSteerCommandId: value.lastSteerCommandId,
|
||||
attemptId: value.attemptId,
|
||||
runnerId: value.runnerId,
|
||||
jobName: value.jobName,
|
||||
namespace: value.namespace,
|
||||
backendProfile: value.backendProfile,
|
||||
status: value.status,
|
||||
runStatus: value.runStatus,
|
||||
commandState: value.commandState,
|
||||
terminalStatus: value.terminalStatus,
|
||||
reuseEligible: value.reuseEligible,
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
|
||||
function assistantText(body: any) {
|
||||
const direct = text(body?.reply?.content ?? body?.message?.content ?? body?.assistantText);
|
||||
if (direct) return direct;
|
||||
|
||||
Reference in New Issue
Block a user