Merge pull request #698 from pikasTech/fix/v02-context-completed-693
fix: preserve AgentRun completed turn context
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
- `internal/cloud/code-agent-*.ts` 负责 Codex stdio session、trace store、result cache、provider profile 和取消/轮询。
|
||||
- AgentRun v0.1 接入只使用标准 `threadId` 路径:`POST /v1/agent/chat` 收到的 `conversationId/sessionId/threadId` 必须写入 AgentRun command `payload.threadId` 和 `SessionRef.threadId`;协议字段、trace、result 和 conversation facts 都以该字段为唯一 thread identity。
|
||||
- AgentRun run 级 events 写回 HWLAB trace 时必须按当前 `commandId` 归属过滤;同一 run 的旧 command 尾部事件不能混入后续 command trace。取消、失败或 blocked 轮次如果已有 assistant/tool 可读上下文,必须以脱敏、限长的 partial context 写入 conversation facts,供后续 `inspect`/`--from-trace` 和同一 thread 的下一轮使用。
|
||||
- AgentRun completed 轮次续接不能只依赖 runner 内部 `threadId` 记忆;Cloud API 必须从请求、account workspace 或 account conversation 中取同一 `conversationId/sessionId/threadId` 的历史消息,生成脱敏、限长的 `conversationContext`,并把它并入 AgentRun command `payload.prompt`,同时保留 `payload.message` 为本轮原始用户消息。该上下文不得包含 device-pod session token、provider key 或其他 Secret 值;trace 只记录上下文来源和条数。
|
||||
- Cloud API 通过 AgentRun v0.1 `runner-jobs.transientEnv` 传递本次 Code Agent turn 的短期上下文,例如 `HWLAB_RUNTIME_*`、`HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME` 和 device-pod session token。`transientEnv` 不设固定 8 项上限,新增短期上下文时必须按 name 去重、只传本次 Job 需要的 value,并继续禁止承载 GitHub token、provider key、长期 SSH key 或其他可复用 credential;文档、日志和 trace 只允许保留脱敏后的 name、来源或摘要,不打印 Secret 值。
|
||||
- 同 Pod sidecar `hwlab-codex-api-forwarder` 监听 `127.0.0.1:49280/responses`,用于 `codex-api` profile 直连 hyueapi,并保持 hyueapi 在 `NO_PROXY` 中。
|
||||
- `hwlab-code-agent-workspace` PVC 挂载到 `/workspace/hwlab`,用于长会话 workspace;它是 cloud-api 运行资源,不是独立用户入口。
|
||||
@@ -58,6 +59,10 @@
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-api.md,然后用 cli 手动测试以下内容:在同一 `conversationId/sessionId/threadId` 中先发起一轮会产生可见 assistant/tool 进展的 Code Agent 请求并取消,再发送“回答一下刚才调查结果”这类后续消息;第二轮必须继续使用同一个标准 `threadId`,且 trace/result 不能混入上一 command 的尾部事件。
|
||||
|
||||
## T2.2
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-api.md,然后用 cli 手动测试以下内容:在同一 `conversationId/sessionId/threadId` 中先完成一轮“看看 device-pod 可用性?”这类 Code Agent 请求,再发送“总结我们刚才的对话内容”;第二轮 AgentRun command 必须包含脱敏历史上下文,并且 assistant reply 不能回答“这是本会话第一条消息”或等价的新会话结论。
|
||||
|
||||
## T3
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-api.md,然后用 cli 手动测试以下内容:未登录访问 `/v1/device-pods` 必须返回认证错误;登录后访问 device-pod list/status 时必须显示 `contractVersion=device-pod-authority-v1` 和 `fake=false` 来源,不得出现 fake fallback。
|
||||
|
||||
@@ -33,6 +33,9 @@ const AGENTRUN_BACKENDS = Object.freeze(["codex", "deepseek", "minimax-m3"]);
|
||||
const THREAD_CONTINUITY_POLICY = "hwlab-agentrun-v01-reuse-runner-thread";
|
||||
const SESSION_POLICY_RUN_LOCAL = "hwlab-agentrun-v01-session-runner-reuse";
|
||||
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "blocked", "cancelled", "canceled"]);
|
||||
const DEFAULT_CONVERSATION_CONTEXT_MESSAGE_LIMIT = 8;
|
||||
const DEFAULT_CONVERSATION_CONTEXT_MESSAGE_TEXT_LIMIT = 900;
|
||||
const DEFAULT_CONVERSATION_CONTEXT_TOTAL_TEXT_LIMIT = 4_800;
|
||||
const HWLAB_RESOURCE_TOOL_ALIASES = Object.freeze([
|
||||
Object.freeze({ name: "hwpod", path: "tools/device-pod-cli.mjs", kind: "node-script" })
|
||||
]);
|
||||
@@ -519,12 +522,26 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses
|
||||
|
||||
function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId }) {
|
||||
const prompt = String(params.message ?? params.prompt ?? "").trim();
|
||||
const conversationContext = buildAgentRunConversationContext(params, prompt, traceId);
|
||||
const commandPrompt = conversationContext.promptPrefix ? `${conversationContext.promptPrefix}\n\n[HWLAB 当前用户消息]\n${prompt}` : prompt;
|
||||
const threadId = safeOpaqueId(params.threadId);
|
||||
return {
|
||||
type: "turn",
|
||||
payload: {
|
||||
prompt,
|
||||
prompt: commandPrompt,
|
||||
message: prompt,
|
||||
...(conversationContext.messages.length > 0
|
||||
? {
|
||||
originalPrompt: prompt,
|
||||
conversationContext: {
|
||||
source: conversationContext.source,
|
||||
messageCount: conversationContext.messages.length,
|
||||
truncated: conversationContext.truncated,
|
||||
valuesRedacted: true,
|
||||
messages: conversationContext.messages
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
traceId,
|
||||
conversationId: safeConversationId(params.conversationId) || null,
|
||||
sessionId: sessionId ?? scopedAgentRunSessionIdForParams(params, traceId, backendProfile),
|
||||
@@ -540,6 +557,98 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgentRunConversationContext(params = {}, currentPrompt = "", traceId = "") {
|
||||
const messages = conversationContextMessages(params, currentPrompt, traceId);
|
||||
if (messages.length === 0) return { source: "none", messages: [], promptPrefix: "", truncated: false };
|
||||
const lines = [
|
||||
"[HWLAB 同一会话的已脱敏历史上下文]",
|
||||
"以下内容来自当前 conversation/session/thread 的上一轮消息;请用它延续对话,不要把本轮当作新会话。"
|
||||
];
|
||||
for (const message of messages) {
|
||||
const role = message.role === "assistant" ? "Assistant" : "User";
|
||||
lines.push(`${role}: ${message.text}`);
|
||||
}
|
||||
return {
|
||||
source: conversationContextSource(params),
|
||||
messages,
|
||||
promptPrefix: lines.join("\n"),
|
||||
truncated: Boolean(params.conversationContext?.truncated || params.conversationContextTruncated)
|
||||
};
|
||||
}
|
||||
|
||||
function conversationContextMessages(params = {}, currentPrompt = "", traceId = "") {
|
||||
const rawMessages = Array.isArray(params.conversationContext?.messages)
|
||||
? params.conversationContext.messages
|
||||
: Array.isArray(params.messages)
|
||||
? params.messages
|
||||
: [];
|
||||
if (rawMessages.length === 0) return [];
|
||||
const current = normalizeContextText(currentPrompt);
|
||||
const currentTraceId = safeTraceId(traceId ?? params.traceId);
|
||||
const normalized = [];
|
||||
let totalTextLength = 0;
|
||||
for (const raw of rawMessages) {
|
||||
const message = normalizeConversationContextMessage(raw);
|
||||
if (!message) continue;
|
||||
if (currentTraceId && safeTraceId(raw?.traceId ?? raw?.runnerTrace?.traceId) === currentTraceId) continue;
|
||||
if (message.text === current) continue;
|
||||
normalized.push(message);
|
||||
}
|
||||
const selected = [];
|
||||
for (const message of normalized.slice(-DEFAULT_CONVERSATION_CONTEXT_MESSAGE_LIMIT).reverse()) {
|
||||
if (totalTextLength >= DEFAULT_CONVERSATION_CONTEXT_TOTAL_TEXT_LIMIT) break;
|
||||
const remaining = DEFAULT_CONVERSATION_CONTEXT_TOTAL_TEXT_LIMIT - totalTextLength;
|
||||
const text = boundContextText(message.text, Math.min(DEFAULT_CONVERSATION_CONTEXT_MESSAGE_TEXT_LIMIT, remaining));
|
||||
if (!text) continue;
|
||||
selected.push({ ...message, text });
|
||||
totalTextLength += text.length;
|
||||
}
|
||||
return selected.reverse();
|
||||
}
|
||||
|
||||
function normalizeConversationContextMessage(raw) {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const role = normalizeConversationContextRole(raw.role ?? raw.author ?? raw.source ?? raw.type);
|
||||
if (!role) return null;
|
||||
const text = boundContextText(normalizeContextText(firstNonEmpty(raw.text, raw.content, raw.message, raw.reply?.content, raw.result?.reply?.content)), DEFAULT_CONVERSATION_CONTEXT_MESSAGE_TEXT_LIMIT);
|
||||
if (!text) return null;
|
||||
return {
|
||||
role,
|
||||
text,
|
||||
traceId: safeTraceId(raw.traceId ?? raw.runnerTrace?.traceId) || null,
|
||||
status: firstNonEmpty(raw.status, raw.runnerTrace?.sessionStatus, raw.sessionStatus) || null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeConversationContextRole(value) {
|
||||
const role = String(value ?? "").trim().toLowerCase();
|
||||
if (["user", "human", "operator"].includes(role)) return "user";
|
||||
if (["assistant", "agent", "code-agent", "code_agent", "model"].includes(role)) return "assistant";
|
||||
return null;
|
||||
}
|
||||
|
||||
function conversationContextSource(params = {}) {
|
||||
return firstNonEmpty(params.conversationContext?.source, params.contextSource, Array.isArray(params.conversationContext?.messages) ? "server" : "request-messages") || "request-messages";
|
||||
}
|
||||
|
||||
function normalizeContextText(value) {
|
||||
return String(value ?? "").replace(/\s+/gu, " ").trim();
|
||||
}
|
||||
|
||||
function boundContextText(value, limit) {
|
||||
const text = redactContextSecrets(normalizeContextText(value));
|
||||
if (!text) return "";
|
||||
if (text.length <= limit) return text;
|
||||
return `${text.slice(0, Math.max(0, limit - 15)).trimEnd()} ...[truncated]`;
|
||||
}
|
||||
|
||||
function redactContextSecrets(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/\b(sk|pk|ak|pat|ghp|github_pat|glpat|xox[baprs]?|AIza)[A-Za-z0-9_\-]{8,}\b/gu, "[REDACTED_SECRET]")
|
||||
.replace(/\b[A-Za-z0-9+/]{32,}={0,2}\b/gu, "[REDACTED_TOKEN]")
|
||||
.replace(/(token|secret|password|passwd|api[_-]?key|credential)\s*[:=]\s*[^\s,;]+/giu, "$1=[REDACTED]");
|
||||
}
|
||||
|
||||
function buildAgentRunSteerCommandInput({ params, traceId, steerTraceId, mapped }) {
|
||||
const prompt = String(params.message ?? params.prompt ?? params.text ?? "").trim();
|
||||
return {
|
||||
|
||||
@@ -140,6 +140,7 @@ test("cloud api /v1/agent/chat supports short submit and result polling", async
|
||||
test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapter", async () => {
|
||||
const calls = [];
|
||||
const ownerSessions = new Map();
|
||||
const conversationMessages = [];
|
||||
const agentRunServer = createHttpServer(async (request, response) => {
|
||||
const url = new URL(request.url || "/", "http://127.0.0.1");
|
||||
const chunks = [];
|
||||
@@ -202,7 +203,23 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.equal(body.payload.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
||||
assert.equal(body.payload.threadContinuityPolicy, "hwlab-agentrun-v01-reuse-runner-thread");
|
||||
assert.equal(body.payload.sessionPolicy, "hwlab-agentrun-v01-session-runner-reuse");
|
||||
const secondTurn = /第二轮/u.test(body.payload.prompt);
|
||||
const secondTurn = /第二轮/u.test(body.payload.message ?? body.payload.prompt);
|
||||
if (secondTurn) {
|
||||
assert.match(body.payload.prompt, /HWLAB 同一会话的已脱敏历史上下文/u);
|
||||
assert.match(body.payload.prompt, /看看device-pod可用性/u);
|
||||
assert.match(body.payload.prompt, /v0\.2 device-pod 可用性快照/u);
|
||||
assert.match(body.payload.prompt, /总结我们刚才的对话内容/u);
|
||||
assert.equal(body.payload.message, "AgentRun adapter smoke 第二轮:总结我们刚才的对话内容");
|
||||
assert.equal(body.payload.originalPrompt, body.payload.message);
|
||||
assert.equal(body.payload.conversationContext.source, "account-conversation");
|
||||
assert.equal(body.payload.conversationContext.messageCount, 2);
|
||||
assert.equal(body.payload.conversationContext.valuesRedacted, true);
|
||||
assert.equal(JSON.stringify(body.payload.conversationContext).includes("test-device-pod-session-token"), false);
|
||||
assert.equal(body.payload.prompt.includes("test-device-pod-session-token"), false);
|
||||
assert.equal(body.payload.prompt.includes("sk-test-device-secret"), false);
|
||||
} else {
|
||||
assert.equal(Object.hasOwn(body.payload, "conversationContext"), false);
|
||||
}
|
||||
return send({ id: secondTurn ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", runId: "run_hwlab_adapter", state: "pending", type: "turn", seq: secondTurn ? 2 : 1 });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/runner-jobs") {
|
||||
@@ -316,7 +333,7 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
accessController: {
|
||||
required: false,
|
||||
async authenticate() {
|
||||
return { ok: true, actor: null, session: null };
|
||||
return { ok: true, actor: { id: "usr_agent_owner", username: "agent-owner", displayName: "Agent Owner", role: "user", status: "active" }, session: { id: "auth_ses_agent_owner" } };
|
||||
},
|
||||
async createCodeAgentDevicePodSession() {
|
||||
return { token: "test-device-pod-session-token" };
|
||||
@@ -327,6 +344,13 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
},
|
||||
async getAgentSession(sessionId) {
|
||||
return ownerSessions.get(sessionId) ?? null;
|
||||
},
|
||||
async visibleConversationForActor(actor, conversationId, projectId) {
|
||||
assert.equal(actor.id, "usr_agent_owner");
|
||||
assert.equal(actor.role, "user");
|
||||
assert.equal(conversationId, "cnv_server-test-agentrun");
|
||||
assert.equal(projectId, "pikasTech/HWLAB");
|
||||
return { conversationId, messages: conversationMessages };
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -340,6 +364,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 },
|
||||
body: JSON.stringify({
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
projectId: "pikasTech/HWLAB",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
ownerUserId: "usr_agent_owner",
|
||||
ownerRole: "user",
|
||||
@@ -392,6 +417,30 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.equal(commandTraceEvent.createdAt, "2026-06-01T00:00:00.500Z");
|
||||
assert.match(commandTraceEvent.command, /hwpod profile list/u);
|
||||
assert.match(commandTraceEvent.stdoutSummary, /profile\.list/u);
|
||||
conversationMessages.push(
|
||||
{
|
||||
id: "msg_693_user",
|
||||
role: "user",
|
||||
text: "看看device-pod可用性?",
|
||||
status: "sent",
|
||||
traceId,
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
createdAt: "2026-06-02T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
id: "msg_693_agent",
|
||||
role: "agent",
|
||||
text: "v0.2 device-pod 可用性快照:可用;token=test-device-pod-session-token sk-test-device-secret-000000",
|
||||
status: "completed",
|
||||
traceId,
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
createdAt: "2026-06-02T00:00:01.000Z"
|
||||
}
|
||||
);
|
||||
|
||||
const steer = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/steer`, {
|
||||
method: "POST",
|
||||
@@ -400,6 +449,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",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
message: "请按 STEER_MARK 调整最终回复"
|
||||
@@ -422,11 +472,12 @@ 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 },
|
||||
body: JSON.stringify({
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
projectId: "pikasTech/HWLAB",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
ownerUserId: "usr_agent_owner",
|
||||
ownerRole: "user",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
message: "AgentRun adapter smoke 第二轮"
|
||||
message: "AgentRun adapter smoke 第二轮:总结我们刚才的对话内容"
|
||||
})
|
||||
});
|
||||
assert.equal(second.status, 202);
|
||||
|
||||
@@ -88,10 +88,11 @@ export async function handleCodeAgentChatHttp(request, response, options) {
|
||||
};
|
||||
const workspaceClaim = await claimWorkbenchWorkspaceTurn({ params: chatParams, options, traceId, response });
|
||||
if (workspaceClaim?.blocked) return;
|
||||
const hydratedChatParams = await hydrateCodeAgentConversationContext({ params: chatParams, options, workspaceClaim, traceId });
|
||||
|
||||
if (codeAgentChatShortConnectionRequested(request, params, options)) {
|
||||
submitCodeAgentChatTurn({
|
||||
params: chatParams,
|
||||
params: hydratedChatParams,
|
||||
options,
|
||||
traceId
|
||||
});
|
||||
@@ -104,8 +105,8 @@ export async function handleCodeAgentChatHttp(request, response, options) {
|
||||
traceId,
|
||||
workspaceId: workspaceClaim?.workspace?.id ?? null,
|
||||
workspaceRevision: workspaceClaim?.workspace?.revision ?? null,
|
||||
conversationId: safeConversationId(params.conversationId) || null,
|
||||
sessionId: safeSessionId(params.sessionId) || null,
|
||||
conversationId: safeConversationId(hydratedChatParams.conversationId) || null,
|
||||
sessionId: safeSessionId(hydratedChatParams.sessionId) || null,
|
||||
traceUrl,
|
||||
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
|
||||
streamUrl: `${traceUrl}/stream`,
|
||||
@@ -119,9 +120,9 @@ export async function handleCodeAgentChatHttp(request, response, options) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await runCodeAgentChat(chatParams, options);
|
||||
await recordCodeAgentSessionOwner({ payload, params: chatParams, options, status: payload.status === "completed" ? "active" : payload.status });
|
||||
const responsePayload = annotateOwner(payload, chatParams);
|
||||
const payload = await runCodeAgentChat(hydratedChatParams, options);
|
||||
await recordCodeAgentSessionOwner({ payload, params: hydratedChatParams, options, status: payload.status === "completed" ? "active" : payload.status });
|
||||
const responsePayload = annotateOwner(payload, hydratedChatParams);
|
||||
|
||||
sendJson(response, responsePayload.status === "failed" && responsePayload.error?.code === "invalid_params" ? 400 : 200, responsePayload);
|
||||
}
|
||||
@@ -130,6 +131,82 @@ async function runCodeAgentChat(params, options) {
|
||||
return handleCodeAgentChat(params, await codeAgentChatExecutionOptions(options, params));
|
||||
}
|
||||
|
||||
async function hydrateCodeAgentConversationContext({ params = {}, options = {}, workspaceClaim = null, traceId } = {}) {
|
||||
if (Array.isArray(params.conversationContext?.messages) && params.conversationContext.messages.length > 0) return params;
|
||||
const directMessages = Array.isArray(params.messages) ? params.messages : [];
|
||||
if (directMessages.length > 0) return { ...params, conversationContext: codeAgentConversationContextPayload({ messages: directMessages, source: "request-messages" }) };
|
||||
const workspaceMessages = messagesFromWorkbenchWorkspace(workspaceClaim?.workspace);
|
||||
if (workspaceMessages.length > 0) {
|
||||
return attachCodeAgentConversationContext(params, workspaceMessages, "workbench-workspace", traceId, options);
|
||||
}
|
||||
const conversationId = safeConversationId(params.conversationId ?? workspaceClaim?.workspace?.selectedConversationId);
|
||||
if (!options.actor || !conversationId || typeof options.accessController?.visibleConversationForActor !== "function") return params;
|
||||
try {
|
||||
const conversation = await options.accessController.visibleConversationForActor(options.actor, conversationId, params.projectId ?? workspaceClaim?.workspace?.projectId);
|
||||
const messages = Array.isArray(conversation?.messages) ? conversation.messages : Array.isArray(conversation?.snapshot?.messages) ? conversation.snapshot.messages : [];
|
||||
return messages.length > 0 ? attachCodeAgentConversationContext(params, messages, "account-conversation", traceId, options) : params;
|
||||
} catch (error) {
|
||||
(options.traceStore ?? defaultCodeAgentTraceStore).append(traceId, {
|
||||
type: "conversation-context",
|
||||
status: "degraded",
|
||||
label: "conversation-context:lookup-failed",
|
||||
errorCode: error?.code ?? "conversation_context_lookup_failed",
|
||||
message: "Stored conversation context lookup failed; this turn will continue with threadId/sessionId only.",
|
||||
valuesPrinted: false
|
||||
});
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
function messagesFromWorkbenchWorkspace(workspace = null) {
|
||||
const json = workspace?.workspace && typeof workspace.workspace === "object" ? workspace.workspace : {};
|
||||
if (Array.isArray(json.messages)) return json.messages;
|
||||
if (Array.isArray(workspace?.messages)) return workspace.messages;
|
||||
return [];
|
||||
}
|
||||
|
||||
function attachCodeAgentConversationContext(params, messages, source, traceId, options = {}) {
|
||||
const payload = codeAgentConversationContextPayload({ messages, source });
|
||||
if (payload.messages.length === 0) return params;
|
||||
(options.traceStore ?? defaultCodeAgentTraceStore).append(traceId, {
|
||||
type: "conversation-context",
|
||||
status: "running",
|
||||
label: "conversation-context:attached",
|
||||
message: `Attached ${payload.messages.length} redacted prior conversation messages for AgentRun thread continuation.`,
|
||||
source,
|
||||
messageCount: payload.messages.length,
|
||||
valuesPrinted: false
|
||||
});
|
||||
return { ...params, conversationContext: payload };
|
||||
}
|
||||
|
||||
function codeAgentConversationContextPayload({ messages = [], source = "unknown" } = {}) {
|
||||
const selected = Array.isArray(messages) ? messages.slice(-12).map(publicConversationContextMessage).filter(Boolean) : [];
|
||||
return {
|
||||
source,
|
||||
messages: selected,
|
||||
truncated: Array.isArray(messages) && messages.length > selected.length,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function publicConversationContextMessage(message) {
|
||||
if (!message || typeof message !== "object") return null;
|
||||
const role = textValue(message.role);
|
||||
const text = textValue(firstNonEmptyValue(message.text, message.content, message.message, message.reply?.content));
|
||||
if (!role || !text) return null;
|
||||
return {
|
||||
role,
|
||||
text,
|
||||
status: textValue(message.status),
|
||||
traceId: safeTraceId(message.traceId ?? message.runnerTrace?.traceId) || null,
|
||||
sessionId: safeSessionId(message.sessionId) || null,
|
||||
threadId: safeOpaqueId(message.threadId) || null,
|
||||
createdAt: textValue(message.createdAt),
|
||||
updatedAt: textValue(message.updatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
async function codeAgentChatExecutionOptions(options = {}, params = {}) {
|
||||
const env = codeAgentProviderProfileEnv(options.env ?? process.env, params);
|
||||
Object.assign(env, await codeAgentDevicePodAuthEnv(options, env));
|
||||
|
||||
Reference in New Issue
Block a user