Merge pull request #783 from pikasTech/fix-v02-cli-explicit-session
fix: align v0.2 explicit agent session dispatch
This commit is contained in:
@@ -478,7 +478,7 @@ async function ensureAgentRunSessionPersistent({ fetchImpl, managerUrl, sessionI
|
||||
if (defaultPolicy !== "persistent") return;
|
||||
try {
|
||||
const tenantId = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TENANT_ID, DEFAULT_TENANT_ID);
|
||||
const projectId = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROJECT_ID, DEFAULT_PROJECT_ID);
|
||||
const projectId = agentRunProjectIdForEnv(env);
|
||||
const expiresInDays = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_SESSION_TTL_DAYS, 30);
|
||||
const expiresAt = new Date(Date.now() + Math.max(1, expiresInDays) * 24 * 60 * 60 * 1000).toISOString();
|
||||
await agentRunJson(fetchImpl, managerUrl, "/api/v1/sessions", {
|
||||
@@ -506,6 +506,10 @@ async function ensureAgentRunSessionPersistent({ fetchImpl, managerUrl, sessionI
|
||||
}
|
||||
}
|
||||
|
||||
function agentRunProjectIdForEnv(env = process.env) {
|
||||
return firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROJECT_ID, DEFAULT_PROJECT_ID);
|
||||
}
|
||||
|
||||
async function shouldResetSessionAfterEviction(failureKind, failureMessage) {
|
||||
return failureKind === "session-store-evicted" || /session store evicted/i.test(failureMessage ?? "");
|
||||
}
|
||||
@@ -517,6 +521,7 @@ function newSessionIdAfterEviction(baseSessionId, traceId) {
|
||||
function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, sessionId }) {
|
||||
const commitId = fullSourceCommit(env);
|
||||
const threadId = safeOpaqueId(params.threadId);
|
||||
const hwlabProjectId = firstNonEmpty(params.projectId);
|
||||
const resourceBundleRef = commitId
|
||||
? {
|
||||
kind: "git",
|
||||
@@ -531,7 +536,7 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses
|
||||
: null;
|
||||
return {
|
||||
tenantId: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TENANT_ID, DEFAULT_TENANT_ID),
|
||||
projectId: firstNonEmpty(params.projectId, env.HWLAB_CODE_AGENT_AGENTRUN_PROJECT_ID, DEFAULT_PROJECT_ID),
|
||||
projectId: agentRunProjectIdForEnv(env),
|
||||
workspaceRef: {
|
||||
kind: "opaque",
|
||||
repo: DEFAULT_PROJECT_ID,
|
||||
@@ -548,6 +553,7 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses
|
||||
adapter: ADAPTER_ID,
|
||||
hwlabTraceId: traceId,
|
||||
hwlabApi: "/v1/agent/chat",
|
||||
hwlabProjectId,
|
||||
hwlabSessionId: safeSessionId(params.sessionId) || null,
|
||||
threadContinuityPolicy: THREAD_CONTINUITY_POLICY,
|
||||
sessionPolicy: SESSION_POLICY_RUN_LOCAL,
|
||||
@@ -589,6 +595,7 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId
|
||||
prompt,
|
||||
message: prompt,
|
||||
traceId,
|
||||
projectId: firstNonEmpty(params.projectId) || null,
|
||||
conversationId: safeConversationId(params.conversationId) || null,
|
||||
sessionId: sessionId ?? scopedAgentRunSessionIdForParams(params, traceId, backendProfile),
|
||||
hwlabSessionId: safeSessionId(params.sessionId) || null,
|
||||
|
||||
@@ -197,6 +197,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
||||
assert.equal(body.tenantId, "hwlab");
|
||||
assert.equal(body.projectId, "pikasTech/HWLAB");
|
||||
assert.equal(body.backendProfile, "deepseek");
|
||||
assert.equal(body.resourceBundleRef.repoUrl, "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
|
||||
assert.equal(body.resourceBundleRef.commitId, "0123456789abcdef0123456789abcdef01234567");
|
||||
@@ -216,6 +217,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.equal(toolCredentials.some((item) => item.tool === "unidesk-ssh" && item.projection.envName === "UNIDESK_SSH_CLIENT_TOKEN" && item.secretRef.name === "agentrun-v01-tool-unidesk-ssh"), true);
|
||||
assert.equal(body.sessionRef.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
||||
assert.equal(body.sessionRef.sessionId, "ses_agentrun_deepseek_server_test_agentrun");
|
||||
assert.equal(body.sessionRef.metadata.hwlabProjectId, "prj_device_pod_workbench");
|
||||
assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-agentrun");
|
||||
assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "deepseek");
|
||||
assert.equal(body.sessionRef.metadata.agentRunSessionPolicy, "backend-profile-scoped");
|
||||
@@ -237,6 +239,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
}
|
||||
assert.equal(body.type, "turn");
|
||||
assert.match(body.payload.prompt, /AgentRun adapter/u);
|
||||
assert.equal(body.payload.projectId, "prj_device_pod_workbench");
|
||||
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");
|
||||
@@ -419,7 +422,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
headers: { "content-type": "application/json", "x-trace-id": traceId, cookie: "hwlab_session=test-stub-session" },
|
||||
body: JSON.stringify({
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
projectId: "pikasTech/HWLAB",
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
ownerUserId: "usr_agent_owner",
|
||||
ownerRole: "user",
|
||||
@@ -514,7 +517,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
traceId,
|
||||
steerTraceId: "trc_steer_server_test",
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
projectId: "pikasTech/HWLAB",
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
message: "请按 STEER_MARK 调整最终回复"
|
||||
@@ -537,7 +540,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
headers: { "content-type": "application/json", "x-trace-id": secondTraceId, cookie: "hwlab_session=test-stub-session" },
|
||||
body: JSON.stringify({
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
projectId: "pikasTech/HWLAB",
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
ownerUserId: "usr_agent_owner",
|
||||
ownerRole: "user",
|
||||
|
||||
@@ -529,6 +529,77 @@ test("hwlab-cli client agent send does not inherit workspace session for explici
|
||||
assert.equal(calls[0].url, "http://web.test/v1/workbench/workspace?projectId=prj_device_pod_workbench");
|
||||
});
|
||||
|
||||
test("hwlab-cli client agent send resolves explicit session conversation before using workspace conversation", async () => {
|
||||
const calls: any[] = [];
|
||||
const result = await runHwlabCli([
|
||||
"client",
|
||||
"agent",
|
||||
"send",
|
||||
"--base-url",
|
||||
"http://web.test",
|
||||
"--cookie",
|
||||
"hwlab_session=session-a",
|
||||
"--message",
|
||||
"send through explicit session",
|
||||
"--trace-id",
|
||||
"trc_explicit_session",
|
||||
"--session-id",
|
||||
"ses_manual_current"
|
||||
], {
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url: String(url), init, body: init?.body ? JSON.parse(String(init.body)) : null });
|
||||
if (String(url).endsWith("/v1/workbench/workspace?projectId=prj_device_pod_workbench")) {
|
||||
return new Response(JSON.stringify({
|
||||
ok: true,
|
||||
workspace: {
|
||||
workspaceId: "wsp_stale_selection",
|
||||
revision: 12,
|
||||
selectedConversationId: "cnv_stale_workspace",
|
||||
selectedAgentSessionId: "ses_stale_workspace",
|
||||
selectedConversation: {
|
||||
conversationId: "cnv_stale_workspace",
|
||||
sessionId: "ses_stale_workspace",
|
||||
threadId: "thread-stale-workspace"
|
||||
},
|
||||
workspace: { sessionStatus: "idle", threadId: "thread-stale-workspace" }
|
||||
}
|
||||
}), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/sessions/ses_manual_current")) {
|
||||
return new Response(JSON.stringify({
|
||||
ok: true,
|
||||
status: "found",
|
||||
session: {
|
||||
sessionId: "ses_manual_current",
|
||||
conversationId: "cnv_manual_current",
|
||||
threadId: "thread-manual-current",
|
||||
status: "idle",
|
||||
usable: true,
|
||||
providerProfile: "minimax-m3"
|
||||
}
|
||||
}), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/chat")) {
|
||||
return new Response(JSON.stringify({ accepted: true, status: "running", traceId: "trc_explicit_session", resultUrl: "/v1/agent/chat/result/trc_explicit_session" }), { status: 202 });
|
||||
}
|
||||
return new Response(JSON.stringify({ status: "completed", traceId: "trc_explicit_session", conversationId: "cnv_manual_current", sessionId: "ses_manual_current", reply: { role: "assistant", content: "explicit session ok" } }), { status: 200 });
|
||||
},
|
||||
sleep: async () => {}
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(calls[0].url, "http://web.test/v1/workbench/workspace?projectId=prj_device_pod_workbench");
|
||||
assert.equal(calls[1].url, "http://web.test/v1/agent/sessions/ses_manual_current");
|
||||
assert.equal(calls[2].url, "http://web.test/v1/agent/chat");
|
||||
assert.equal(calls[2].body.conversationId, "cnv_manual_current");
|
||||
assert.equal(calls[2].body.sessionId, "ses_manual_current");
|
||||
assert.equal(calls[2].body.threadId, "thread-manual-current");
|
||||
assert.equal(calls[2].body.workspaceId, "wsp_stale_selection");
|
||||
assert.equal(result.payload.continuation.conversationId, "cnv_manual_current");
|
||||
assert.equal(result.payload.continuation.sessionId, "ses_manual_current");
|
||||
assert.equal(result.payload.continuation.threadId, "thread-manual-current");
|
||||
});
|
||||
|
||||
test("hwlab-cli client agent send prefers selected conversation thread over stale workspace thread", async () => {
|
||||
const calls: any[] = [];
|
||||
const result = await runHwlabCli([
|
||||
@@ -805,7 +876,24 @@ test("hwlab-cli client agent send restores workspace when completed patch omits
|
||||
return new Response(JSON.stringify({ status: "completed", traceId: "trc_restore_workspace", conversationId: "cnv_restore_workspace", sessionId: "ses_restore_workspace", reply: { role: "assistant", content: "restore workspace ready" } }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/workbench/workspace/wsp_restore_workspace")) {
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
return new Response(JSON.stringify({
|
||||
ok: true,
|
||||
workspace: {
|
||||
workspaceId: "wsp_restore_workspace",
|
||||
revision: 12,
|
||||
selectedConversationId: "cnv_restore_workspace",
|
||||
selectedAgentSessionId: "ses_restore_workspace",
|
||||
selectedConversation: {
|
||||
conversationId: "cnv_previous_workspace",
|
||||
sessionId: "ses_previous_workspace",
|
||||
threadId: "thread-previous-workspace"
|
||||
},
|
||||
workspace: {
|
||||
sessionStatus: "completed",
|
||||
threadId: "thread-restore-workspace"
|
||||
}
|
||||
}
|
||||
}), { status: 200 });
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: false, error: { code: "unexpected_test_route" } }), { status: 404 });
|
||||
},
|
||||
@@ -817,15 +905,18 @@ test("hwlab-cli client agent send restores workspace when completed patch omits
|
||||
assert.equal(calls[1].body.sessionId, "ses_restore_workspace");
|
||||
assert.equal(Object.hasOwn(calls[1].body, "threadId"), false);
|
||||
assert.equal(calls[3].url, "http://web.test/v1/workbench/workspace/wsp_restore_workspace");
|
||||
assert.equal(calls[4].url, "http://web.test/v1/workbench/workspace?projectId=prj_device_pod_workbench");
|
||||
assert.equal(result.payload.workspace.selectedConversationId, "cnv_restore_workspace");
|
||||
assert.equal(result.payload.workspace.selectedAgentSessionId, "ses_restore_workspace");
|
||||
assert.equal(result.payload.workspace.threadId, "thread-restore-workspace");
|
||||
assert.equal(result.payload.workspace.selectedConversation.conversationId, "cnv_restore_workspace");
|
||||
assert.equal(result.payload.workspace.selectedConversation.sessionId, "ses_restore_workspace");
|
||||
|
||||
const session = JSON.parse(await readFile(path.join(cwd, ".state/hwlab-cli/session.json"), "utf8"));
|
||||
assert.equal(session.workspace.selectedConversationId, "cnv_restore_workspace");
|
||||
assert.equal(session.workspace.selectedAgentSessionId, "ses_restore_workspace");
|
||||
assert.equal(session.workspace.threadId, "thread-restore-workspace");
|
||||
assert.equal(session.workspace.selectedConversation.conversationId, "cnv_restore_workspace");
|
||||
assert.equal(session.workspace.selectedConversation.sessionId, "ses_restore_workspace");
|
||||
});
|
||||
|
||||
test("hwlab-cli client agent send output uses completed result when restore is stale", async () => {
|
||||
|
||||
@@ -865,6 +865,14 @@ async function agentSessionStatus(context: any) {
|
||||
return responsePayload("client.agent.session.status", response, context, { route: route("GET", pathName), ...agentSessionSummary(response.body?.session, { sessionId }), body: responseBodyForCli(response.body, context.parsed) });
|
||||
}
|
||||
|
||||
async function fetchAgentSessionForSend(context: any, sessionId: string) {
|
||||
const pathName = `/v1/agent/sessions/${encodeURIComponent(sessionId)}`;
|
||||
const response = await requestJson({ ...context, method: "GET", path: pathName });
|
||||
if (!isHttpSuccess(response)) return { response, session: null };
|
||||
const session = response.body?.session && typeof response.body.session === "object" ? response.body.session : null;
|
||||
return { response, session };
|
||||
}
|
||||
|
||||
async function agentSessionList(context: any) {
|
||||
const params = new URLSearchParams();
|
||||
params.set("projectId", text(context.parsed.projectId) || DEFAULT_WORKBENCH_PROJECT_ID);
|
||||
@@ -1297,9 +1305,18 @@ async function agentSend(context: any) {
|
||||
const requestedConversationId = explicitConversationId || replayConversationId;
|
||||
const workspaceConversationId = text(workspaceState?.selectedConversationId);
|
||||
const explicitSessionId = text(parsed.sessionId);
|
||||
let explicitSessionRecord = null;
|
||||
if (explicitSessionId && !requestedConversationId) {
|
||||
const lookup = await fetchAgentSessionForSend(context, explicitSessionId);
|
||||
if (!isHttpSuccess(lookup.response)) {
|
||||
return responsePayload("client.agent.send", lookup.response, context, { route: route("GET", `/v1/agent/sessions/${encodeURIComponent(explicitSessionId)}`), traceId, sessionId: explicitSessionId, replay: replay.summary });
|
||||
}
|
||||
explicitSessionRecord = lookup.session;
|
||||
}
|
||||
const explicitSessionConversationId = text(explicitSessionRecord?.conversationId);
|
||||
const allowWorkspaceSession = !requestedConversationId || requestedConversationId === workspaceConversationId;
|
||||
const sessionId = explicitSessionId || replay.sessionId || (allowWorkspaceSession ? text(workspaceState?.selectedAgentSessionId) : "");
|
||||
const conversationId = requestedConversationId || workspaceConversationId;
|
||||
const conversationId = requestedConversationId || explicitSessionConversationId || workspaceConversationId;
|
||||
const workspaceSessionId = text(workspaceState?.selectedAgentSessionId);
|
||||
const workspaceMatchesSelectedSession = workspaceSessionId === sessionId && (!conversationId || workspaceConversationId === conversationId);
|
||||
if (!sessionId) {
|
||||
@@ -1312,7 +1329,7 @@ async function agentSend(context: any) {
|
||||
]
|
||||
});
|
||||
}
|
||||
const sessionStatus = workspaceSessionId === sessionId ? text(workspaceState?.sessionStatus) : "";
|
||||
const sessionStatus = explicitSessionRecord ? text(explicitSessionRecord.status) : (workspaceSessionId === sessionId ? text(workspaceState?.sessionStatus) : "");
|
||||
if (isCodeAgentSessionUnusableStatus(sessionStatus) && !text(parsed.ignoreSessionStatus)) {
|
||||
throw cliError("session_not_usable", `client agent send refuses to reuse ${sessionStatus} Code Agent session; create/select a new session first`, {
|
||||
traceId,
|
||||
@@ -1329,7 +1346,7 @@ async function agentSend(context: any) {
|
||||
nextCommands: ["hwlab-cli client agent session create --provider-profile minimax-m3"]
|
||||
});
|
||||
}
|
||||
const threadId = text(parsed.threadId) || replay.threadId || (workspaceMatchesSelectedSession ? text(workspaceState?.threadId) : "");
|
||||
const threadId = text(parsed.threadId) || replay.threadId || (explicitSessionRecord ? text(explicitSessionRecord.threadId) : "") || (workspaceMatchesSelectedSession ? text(workspaceState?.threadId) : "");
|
||||
const retryOf = text(parsed.retryOf) || replay.retryOf;
|
||||
const continuation = agentContinuationSummary({
|
||||
conversationId,
|
||||
@@ -2150,8 +2167,9 @@ function workspaceWithAgentResult(workspace: any, resultBody: any, fallback: any
|
||||
const resultThreadId = text(resultBody?.threadId ?? resultBody?.session?.threadId ?? resultBody?.providerTrace?.threadId ?? fallback.threadId);
|
||||
const baseConversationMatches = !conversationId || text(base.selectedConversationId) === conversationId;
|
||||
const baseSessionMatches = !sessionId || text(base.selectedAgentSessionId) === sessionId;
|
||||
const nestedConversationMatches = selectedConversationMatches(base.selectedConversation, { conversationId, sessionId });
|
||||
const baseThreadId = baseConversationMatches && baseSessionMatches ? text(base.threadId) : "";
|
||||
const selectedConversation = baseConversationMatches && baseSessionMatches && base.selectedConversation
|
||||
const selectedConversation = baseConversationMatches && baseSessionMatches && nestedConversationMatches
|
||||
? base.selectedConversation
|
||||
: clean({ conversationId, sessionId, threadId: resultThreadId || baseThreadId });
|
||||
return clean({
|
||||
@@ -2166,6 +2184,13 @@ function workspaceWithAgentResult(workspace: any, resultBody: any, fallback: any
|
||||
});
|
||||
}
|
||||
|
||||
function selectedConversationMatches(selectedConversation: any, expected: any) {
|
||||
if (!selectedConversation || typeof selectedConversation !== "object") return false;
|
||||
const conversationId = text(expected.conversationId);
|
||||
const sessionId = text(expected.sessionId);
|
||||
return (!conversationId || text(selectedConversation.conversationId) === conversationId) && (!sessionId || text(selectedConversation.sessionId ?? selectedConversation.session?.sessionId) === sessionId);
|
||||
}
|
||||
|
||||
async function saveWorkspaceState(context: any, workspaceBody: any) {
|
||||
const session = await loadStoredState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }) ?? {};
|
||||
await saveSession(context, { ...session, baseUrl: baseUrl(context.parsed, context.env), workspace: normalizeWorkbenchWorkspace(workspaceBody), updatedAt: context.now() });
|
||||
@@ -2223,9 +2248,17 @@ function workbenchThreadId(workspace: any, workspaceJson: any) {
|
||||
const selectedAgentSession = workspace?.selectedAgentSession && typeof workspace.selectedAgentSession === "object" ? workspace.selectedAgentSession : {};
|
||||
const selectedConversationJson = workspaceJson?.selectedConversation && typeof workspaceJson.selectedConversation === "object" ? workspaceJson.selectedConversation : {};
|
||||
const selectedAgentSessionJson = workspaceJson?.selectedAgentSession && typeof workspaceJson.selectedAgentSession === "object" ? workspaceJson.selectedAgentSession : {};
|
||||
return text(selectedConversation.threadId ?? selectedConversation.session?.threadId)
|
||||
const topConversationId = text(workspace?.selectedConversationId ?? workspaceJson?.selectedConversationId);
|
||||
const topSessionId = text(workspace?.selectedAgentSessionId ?? workspaceJson?.selectedAgentSessionId);
|
||||
const selectedConversationThreadId = selectedConversationMatches(selectedConversation, { conversationId: topConversationId, sessionId: topSessionId })
|
||||
? text(selectedConversation.threadId ?? selectedConversation.session?.threadId)
|
||||
: "";
|
||||
const selectedConversationJsonThreadId = selectedConversationMatches(selectedConversationJson, { conversationId: topConversationId, sessionId: topSessionId })
|
||||
? text(selectedConversationJson.threadId ?? selectedConversationJson.session?.threadId)
|
||||
: "";
|
||||
return selectedConversationThreadId
|
||||
|| text(selectedAgentSession.threadId)
|
||||
|| text(selectedConversationJson.threadId ?? selectedConversationJson.session?.threadId)
|
||||
|| selectedConversationJsonThreadId
|
||||
|| text(selectedAgentSessionJson.threadId)
|
||||
|| text(workspaceJson?.threadId)
|
||||
|| text(workspace?.threadId);
|
||||
|
||||
Reference in New Issue
Block a user