fix: strengthen code agent session continuity
This commit is contained in:
@@ -213,7 +213,8 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
|
||||
workspace: options.workspace,
|
||||
timeoutMs: options.timeoutMs,
|
||||
model: providerPlan.model,
|
||||
codexStdioManager: options.codexStdioManager
|
||||
codexStdioManager: options.codexStdioManager,
|
||||
conversationFacts: conversationFactsForPrompt(sessionRegistry, conversationId)
|
||||
});
|
||||
return completedRunnerPayload({ base, runnerResult: stdioResult, messageId, now: options.now, sessionRegistry });
|
||||
}
|
||||
@@ -236,6 +237,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
|
||||
return completedRunnerPayload({ base, runnerResult, messageId, now: options.now, sessionRegistry });
|
||||
}
|
||||
|
||||
const priorConversationFacts = conversationFactsForPrompt(sessionRegistry, conversationId);
|
||||
const providerResult = await callConfiguredProvider({
|
||||
providerPlan,
|
||||
message,
|
||||
@@ -244,6 +246,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
|
||||
timeoutMs: options.timeoutMs,
|
||||
env: options.env ?? process.env,
|
||||
now: options.now,
|
||||
conversationFacts: priorConversationFacts,
|
||||
callProvider: options.callProvider
|
||||
});
|
||||
const content = typeof providerResult.content === "string" ? providerResult.content.trim() : "";
|
||||
@@ -277,7 +280,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
|
||||
runner: providerRunner,
|
||||
capabilityLevel: providerResult.capabilityLevel ?? "text-chat-only"
|
||||
});
|
||||
return finalizeCodeAgentChatPayload({
|
||||
const providerPayload = finalizeCodeAgentChatPayload({
|
||||
...base,
|
||||
status: "completed",
|
||||
updatedAt: completedAt,
|
||||
@@ -317,6 +320,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
|
||||
providerTrace: providerResult.providerTrace ?? null,
|
||||
...(providerCompletionBlocker ? { blocker: providerCompletionBlocker, blockers: [providerCompletionBlocker] } : {})
|
||||
});
|
||||
return attachConversationFacts(providerPayload, sessionRegistry, { now: options.now });
|
||||
} catch (error) {
|
||||
const failedAt = nowIso(options.now);
|
||||
const payload = {
|
||||
@@ -841,15 +845,16 @@ async function callConfiguredProvider({
|
||||
timeoutMs,
|
||||
env,
|
||||
now,
|
||||
conversationFacts,
|
||||
callProvider
|
||||
}) {
|
||||
if (callProvider) {
|
||||
return callProvider({ providerPlan, message, conversationId, traceId, timeoutMs, env, now });
|
||||
return callProvider({ providerPlan, message, conversationId, traceId, timeoutMs, env, now, conversationFacts });
|
||||
}
|
||||
if (providerPlan.mode === "openai") {
|
||||
return callOpenAiResponses({ providerPlan, message, conversationId, traceId, timeoutMs, env });
|
||||
return callOpenAiResponses({ providerPlan, message, conversationId, traceId, timeoutMs, env, conversationFacts });
|
||||
}
|
||||
return callCodexCli({ providerPlan, message, conversationId, traceId, timeoutMs, env });
|
||||
return callCodexCli({ providerPlan, message, conversationId, traceId, timeoutMs, env, conversationFacts });
|
||||
}
|
||||
|
||||
async function inspectReadOnlyRunnerAvailability(env, options = {}) {
|
||||
@@ -917,7 +922,7 @@ async function inspectReadOnlyRunnerAvailability(env, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function callCodexStdioRunner({ message, conversationId, sessionId, traceId, env, now, workspace, timeoutMs, model, codexStdioManager }) {
|
||||
async function callCodexStdioRunner({ message, conversationId, sessionId, traceId, env, now, workspace, timeoutMs, model, codexStdioManager, conversationFacts }) {
|
||||
const manager = resolveCodexStdioSessionManager({ codexStdioManager });
|
||||
try {
|
||||
return await manager.chat({
|
||||
@@ -929,7 +934,8 @@ async function callCodexStdioRunner({ message, conversationId, sessionId, traceI
|
||||
now,
|
||||
workspace,
|
||||
timeoutMs,
|
||||
model
|
||||
model,
|
||||
conversationFacts
|
||||
});
|
||||
} catch (error) {
|
||||
const availability = error.availability ?? manager.describe({ env, workspace });
|
||||
@@ -2646,6 +2652,10 @@ function sessionContextToolCall({ conversationFacts, workspace, traceId }) {
|
||||
conversationId: conversationFacts.conversationId,
|
||||
sessionId: conversationFacts.sessionId,
|
||||
turnCount: conversationFacts.turnCount,
|
||||
provider: conversationFacts.latestProvider,
|
||||
backend: conversationFacts.latestBackend,
|
||||
runnerKind: conversationFacts.runnerKind,
|
||||
capabilityLevel: conversationFacts.capabilityLevel,
|
||||
workspace: conversationFacts.workspace,
|
||||
skills: {
|
||||
status: conversationFacts.latestSkills?.status ?? "not_requested",
|
||||
@@ -2682,10 +2692,11 @@ function sessionContextReply({ conversationFacts, session }) {
|
||||
return [
|
||||
"根据同一个 conversation/session 中已记录的 runner facts:",
|
||||
`- conversationId=${conversationFacts.conversationId}; sessionId=${conversationFacts.sessionId}; sessionStatus=${conversationFacts.sessionStatus ?? session.status}; sessionMode=${conversationFacts.sessionMode ?? session.sessionMode}; turnCount=${conversationFacts.turnCount}.`,
|
||||
`- provider=${conversationFacts.latestProvider ?? "unknown"}; backend=${conversationFacts.latestBackend ?? "unknown"}; runner=${conversationFacts.runnerKind ?? "unknown"}; capabilityLevel=${conversationFacts.capabilityLevel ?? "unknown"}.`,
|
||||
`- 当前工作目录/workspace=${conversationFacts.workspace ?? session.workspace ?? "unknown"}.`,
|
||||
`- 可用 skills=${skillCount}; ${skillNames}.`,
|
||||
`- 最近 toolCalls=${toolSummary}; traceIds=${traceSummary}.`,
|
||||
"证明:本轮走 session.context 的 Code Agent session fact 路径,runner=hwlab-readonly-runner,capabilityLevel=read-only-session-tools;不是 openai-responses-fallback,也不是 stateless-one-shot。",
|
||||
"证明:本轮走 session.context 的 Code Agent session fact 路径,复用同一 conversation/session 的 bounded facts;不是 openai-responses-fallback,不是 stateless-one-shot,也不是无法访问之前状态;不会输出原始 secret/evidence。",
|
||||
limitationReplyLine()
|
||||
].join("\n");
|
||||
}
|
||||
@@ -3173,7 +3184,7 @@ function boundToolOutput(value, maxLength = READONLY_TOOL_OUTPUT_LIMIT) {
|
||||
};
|
||||
}
|
||||
|
||||
async function callOpenAiResponses({ providerPlan, message, conversationId, traceId, timeoutMs, env }) {
|
||||
async function callOpenAiResponses({ providerPlan, message, conversationId, traceId, timeoutMs, env, conversationFacts }) {
|
||||
if (env.HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK !== "1" && env.HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK !== "true") {
|
||||
return {
|
||||
provider: "openai-responses",
|
||||
@@ -3226,7 +3237,7 @@ async function callOpenAiResponses({ providerPlan, message, conversationId, trac
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: buildAgentPrompt({ message, conversationId, traceId })
|
||||
text: buildAgentPrompt({ message, conversationId, traceId, conversationFacts })
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3295,7 +3306,7 @@ async function callOpenAiResponses({ providerPlan, message, conversationId, trac
|
||||
};
|
||||
}
|
||||
|
||||
async function callCodexCli({ providerPlan, message, conversationId, traceId, timeoutMs, env }) {
|
||||
async function callCodexCli({ providerPlan, message, conversationId, traceId, timeoutMs, env, conversationFacts }) {
|
||||
const command = firstNonEmpty(env.HWLAB_CODE_AGENT_CODEX_COMMAND, DEFAULT_CODEX_COMMAND);
|
||||
if (!(await commandExists(command, env))) {
|
||||
throw providerUnavailable(`Codex CLI command is not available: ${command}`, {
|
||||
@@ -3328,7 +3339,7 @@ async function callCodexCli({ providerPlan, message, conversationId, traceId, ti
|
||||
}
|
||||
args.push("-");
|
||||
|
||||
const result = await spawnWithInput(command, args, buildAgentPrompt({ message, conversationId, traceId }), {
|
||||
const result = await spawnWithInput(command, args, buildAgentPrompt({ message, conversationId, traceId, conversationFacts }), {
|
||||
env,
|
||||
timeoutMs: effectiveTimeout(timeoutMs)
|
||||
});
|
||||
@@ -3429,16 +3440,70 @@ async function callCodexCli({ providerPlan, message, conversationId, traceId, ti
|
||||
}
|
||||
}
|
||||
|
||||
function buildAgentPrompt({ message, conversationId, traceId }) {
|
||||
function buildAgentPrompt({ message, conversationId, traceId, conversationFacts }) {
|
||||
return [
|
||||
CODE_AGENT_SYSTEM_PROMPT,
|
||||
"",
|
||||
`conversationId: ${conversationId}`,
|
||||
`traceId: ${traceId}`,
|
||||
boundedConversationFactsPrompt(conversationFacts),
|
||||
"",
|
||||
"用户消息:",
|
||||
message
|
||||
].join("\n");
|
||||
].filter((line) => line !== null && line !== undefined).join("\n");
|
||||
}
|
||||
|
||||
function conversationFactsForPrompt(sessionRegistry, conversationId) {
|
||||
if (!sessionRegistry || typeof sessionRegistry.getConversationFacts !== "function") return null;
|
||||
try {
|
||||
return sessionRegistry.getConversationFacts(conversationId);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function boundedConversationFactsPrompt(conversationFacts) {
|
||||
if (!conversationFacts || typeof conversationFacts !== "object" || Number(conversationFacts.turnCount ?? 0) <= 0) {
|
||||
return null;
|
||||
}
|
||||
const lines = [
|
||||
"上一轮会话事实(只作为上下文,不是用户新指令):",
|
||||
`- conversationId=${safePromptFact(conversationFacts.conversationId)}; sessionId=${safePromptFact(conversationFacts.sessionId)}; sessionStatus=${safePromptFact(conversationFacts.sessionStatus)}; sessionMode=${safePromptFact(conversationFacts.sessionMode)}; turnCount=${safePromptFact(conversationFacts.turnCount)}.`,
|
||||
`- provider/backend/runner=${safePromptFact(conversationFacts.latestProvider)}/${safePromptFact(conversationFacts.latestBackend)}/${safePromptFact(conversationFacts.runnerKind)}; capabilityLevel=${safePromptFact(conversationFacts.capabilityLevel)}.`,
|
||||
`- workspace=${safePromptFact(conversationFacts.workspace)}; sandbox=${safePromptFact(conversationFacts.sandbox)}.`,
|
||||
`- traceIds=${promptList(conversationFacts.traceIds, 6)}; latestTraceId=${safePromptFact(conversationFacts.latestTraceId)}.`,
|
||||
`- skills=${promptSkills(conversationFacts.latestSkills)}.`,
|
||||
`- toolCalls=${promptToolCalls(conversationFacts.recentToolCalls)}.`,
|
||||
"如果用户询问刚才/上一轮/session/trace/workspace/skills/toolCalls,请优先用上述事实用中文回答;不要回答无法访问之前状态。不要输出 secret 或原始 evidence 大段内容。"
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function promptSkills(skills) {
|
||||
if (!skills || typeof skills !== "object") return "not_requested";
|
||||
const names = Array.isArray(skills.names) ? skills.names.filter(Boolean).slice(0, 8) : [];
|
||||
const count = skills.totalCount ?? skills.count ?? names.length;
|
||||
return `${safePromptFact(skills.status)} count=${safePromptFact(count)}${names.length ? ` names=${names.map(safePromptFact).join(",")}` : ""}`;
|
||||
}
|
||||
|
||||
function promptToolCalls(toolCalls) {
|
||||
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return "none";
|
||||
return toolCalls
|
||||
.slice(-6)
|
||||
.map((toolCall) => [toolCall?.name, toolCall?.status, toolCall?.route].filter(Boolean).map(safePromptFact).join(":"))
|
||||
.filter(Boolean)
|
||||
.join(",") || "none";
|
||||
}
|
||||
|
||||
function promptList(values, limit) {
|
||||
return Array.isArray(values) && values.length > 0
|
||||
? values.filter(Boolean).slice(-limit).map(safePromptFact).join(",")
|
||||
: "none";
|
||||
}
|
||||
|
||||
function safePromptFact(value) {
|
||||
if (value === undefined || value === null || value === "") return "none";
|
||||
return redactText(String(value).replace(/\s+/gu, " ").trim()).slice(0, 180);
|
||||
}
|
||||
|
||||
function normalizeUserMessage(value) {
|
||||
|
||||
@@ -339,21 +339,28 @@ function publicSession(session, { conversationId = null, reused = null } = {}) {
|
||||
|
||||
function publicConversationFacts({ conversationId, session, facts, maxFactToolCalls, maxConversationFacts }) {
|
||||
const latest = facts.at(-1) ?? null;
|
||||
const latestOperational = [...facts].reverse().find((fact) => fact.kind !== "session_context") ?? latest;
|
||||
const latestSessionFact = [...facts].reverse().find((fact) => fact.sessionId && fact.sessionMode !== "provider-text-request") ?? latestOperational;
|
||||
const latestSkills = [...facts].reverse().find((fact) => fact.skills?.status && fact.skills.status !== "not_requested")?.skills ?? null;
|
||||
const latestWorkspace = [...facts].reverse().find((fact) => fact.workspace)?.workspace ?? session?.workspace ?? null;
|
||||
const latestWorkspace = [...facts].reverse().find((fact) => fact.kind !== "session_context" && fact.workspace)?.workspace ??
|
||||
[...facts].reverse().find((fact) => fact.workspace)?.workspace ??
|
||||
session?.workspace ??
|
||||
null;
|
||||
const traceIds = [...new Set(facts.map((fact) => fact.traceId).filter(Boolean))].slice(-6);
|
||||
const recentToolCalls = facts
|
||||
.flatMap((fact) => fact.toolCalls?.items ?? [])
|
||||
.slice(-maxFactToolCalls);
|
||||
return {
|
||||
conversationId,
|
||||
sessionId: session?.sessionId ?? latest?.sessionId ?? null,
|
||||
sessionStatus: session?.status ?? latest?.sessionStatus ?? null,
|
||||
sessionMode: session?.sessionMode ?? latest?.sessionMode ?? null,
|
||||
capabilityLevel: session?.capabilityLevel ?? latest?.capabilityLevel ?? null,
|
||||
runnerKind: session?.runnerKind ?? latest?.runnerKind ?? null,
|
||||
sessionId: session?.sessionId ?? latestSessionFact?.sessionId ?? latest?.sessionId ?? null,
|
||||
sessionStatus: session?.status ?? latestSessionFact?.sessionStatus ?? latest?.sessionStatus ?? null,
|
||||
sessionMode: session?.sessionMode ?? latestSessionFact?.sessionMode ?? latest?.sessionMode ?? null,
|
||||
capabilityLevel: latestOperational?.capabilityLevel ?? session?.capabilityLevel ?? latest?.capabilityLevel ?? null,
|
||||
runnerKind: latestOperational?.runnerKind ?? session?.runnerKind ?? latest?.runnerKind ?? null,
|
||||
latestProvider: latestOperational?.provider ?? latest?.provider ?? null,
|
||||
latestBackend: latestOperational?.backend ?? latest?.backend ?? null,
|
||||
workspace: latestWorkspace,
|
||||
sandbox: session?.sandbox ?? latest?.sandbox ?? null,
|
||||
sandbox: latestOperational?.sandbox ?? session?.sandbox ?? latest?.sandbox ?? null,
|
||||
turnCount: facts.length,
|
||||
maxFacts: maxConversationFacts,
|
||||
traceIds,
|
||||
|
||||
@@ -569,6 +569,74 @@ test("OpenAI fallback and codex one-shot do not pass the long-lived session gate
|
||||
assert.equal(classifyCodeAgentChatReadiness(oneShot, { realDevLive: true, httpStatus: 200 }).devLiveReplyPass, true);
|
||||
});
|
||||
|
||||
test("OpenAI provider turns receive bounded prior session facts and record continuity", async () => {
|
||||
const registry = createCodeAgentSessionRegistry({
|
||||
idFactory: () => "ses_provider_context"
|
||||
});
|
||||
const providerPrompts = [];
|
||||
const env = {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||||
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "1",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
};
|
||||
|
||||
const first = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_provider_context",
|
||||
traceId: "trc_provider_context_pwd",
|
||||
message: "pwd"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-23T00:04:10.000Z",
|
||||
sessionRegistry: registry,
|
||||
env
|
||||
}
|
||||
);
|
||||
assert.equal(first.status, "completed");
|
||||
assert.equal(first.provider, "codex-readonly-runner");
|
||||
assert.equal(first.toolCalls[0].name, "pwd");
|
||||
assert.equal(first.conversationFacts.turnCount, 1);
|
||||
|
||||
const second = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_provider_context",
|
||||
traceId: "trc_provider_context_openai",
|
||||
message: "请根据刚才的上下文简要说明"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-23T00:04:11.000Z",
|
||||
sessionRegistry: registry,
|
||||
env,
|
||||
callProvider: async ({ conversationFacts }) => {
|
||||
const promptFacts = JSON.stringify(conversationFacts);
|
||||
providerPrompts.push(promptFacts);
|
||||
assert.equal(conversationFacts.sessionId, first.sessionId);
|
||||
assert.equal(conversationFacts.workspace, process.cwd());
|
||||
assert.equal(conversationFacts.recentToolCalls[0].name, "pwd");
|
||||
return {
|
||||
provider: "openai-responses",
|
||||
model: "gpt-test",
|
||||
backend: "hwlab-cloud-api/openai-responses",
|
||||
content: `上一轮工作目录是 ${conversationFacts.workspace},session=${conversationFacts.sessionId},trace=${conversationFacts.latestTraceId}。`,
|
||||
usage: null,
|
||||
providerTrace: { source: "test-provider" }
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(second.status, "completed");
|
||||
assert.equal(second.provider, "openai-responses");
|
||||
assert.equal(second.conversationFacts.turnCount, 2);
|
||||
assert.equal(second.conversationFacts.sessionId, first.sessionId);
|
||||
assert.equal(second.conversationFacts.latestProvider, "openai-responses");
|
||||
assert.equal(second.conversationFacts.latestBackend, "hwlab-cloud-api/openai-responses");
|
||||
assert.match(second.reply.content, /上一轮工作目录/u);
|
||||
assert.match(providerPrompts[0], /trc_provider_context_pwd/u);
|
||||
assert.equal(JSON.stringify(second.conversationFacts).includes("sk-"), false);
|
||||
});
|
||||
|
||||
test("Code Agent M3 DO write uses Skill CLI to call only HWLAB API /v1/m3/io", async () => {
|
||||
const calls = [];
|
||||
const payload = await handleCodeAgentChat(
|
||||
@@ -1429,6 +1497,10 @@ function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
async function createFakeCodexCommand() {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
|
||||
const packageRoot = path.join(root, "node_modules", "@openai", "codex");
|
||||
@@ -1586,6 +1658,7 @@ test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and versi
|
||||
test("repo-owned Codex stdio manager creates and reuses long-lived sessions with trace evidence", async () => {
|
||||
const calls = [];
|
||||
const fakeCodex = await createFakeCodexCommand();
|
||||
const registry = createCodeAgentSessionRegistry();
|
||||
const manager = createCodexStdioSessionManager({
|
||||
idFactory: () => "ses_stdio_ready",
|
||||
createRpcClient: async () => ({
|
||||
@@ -1628,6 +1701,7 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
|
||||
{
|
||||
now: () => "2026-05-23T00:06:00.000Z",
|
||||
codexStdioManager: manager,
|
||||
sessionRegistry: registry,
|
||||
env
|
||||
}
|
||||
);
|
||||
@@ -1659,6 +1733,7 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
|
||||
{
|
||||
now: () => "2026-05-23T00:06:01.000Z",
|
||||
codexStdioManager: manager,
|
||||
sessionRegistry: registry,
|
||||
env
|
||||
}
|
||||
);
|
||||
@@ -1676,6 +1751,10 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
|
||||
assert.equal(calls[0].name, "codex");
|
||||
assert.equal(calls[1].name, "codex-reply");
|
||||
assert.equal(calls[1].args.threadId, "thread_stdio_ready");
|
||||
assert.match(calls[1].args.prompt, /Prior session facts/u);
|
||||
assert.match(calls[1].args.prompt, /ses_stdio_ready/u);
|
||||
assert.match(calls[1].args.prompt, /trc_stdio_ready_1/u);
|
||||
assert.match(calls[1].args.prompt, new RegExp(escapeRegExp(process.cwd()), "u"));
|
||||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -318,10 +318,10 @@ export function createCodexStdioSessionManager(options = {}) {
|
||||
const toolArguments = session.threadId
|
||||
? {
|
||||
threadId: session.threadId,
|
||||
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId })
|
||||
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId, conversationFacts: params.conversationFacts })
|
||||
}
|
||||
: {
|
||||
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId }),
|
||||
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId, conversationFacts: params.conversationFacts }),
|
||||
cwd: workspace,
|
||||
sandbox,
|
||||
model: params.model,
|
||||
@@ -1784,16 +1784,59 @@ function safeDisplayPath(value) {
|
||||
return redactText(String(value ?? ".")).replace(/\s+/gu, " ");
|
||||
}
|
||||
|
||||
function buildCodexUserPrompt(message, { conversationId, traceId }) {
|
||||
function buildCodexUserPrompt(message, { conversationId, traceId, conversationFacts }) {
|
||||
return [
|
||||
`conversationId: ${conversationId}`,
|
||||
`traceId: ${traceId}`,
|
||||
codexConversationFactsPrompt(conversationFacts),
|
||||
"",
|
||||
"User message:",
|
||||
String(message ?? "").trim()
|
||||
].filter((line) => line !== null && line !== undefined).join("\n");
|
||||
}
|
||||
|
||||
function codexConversationFactsPrompt(conversationFacts) {
|
||||
if (!conversationFacts || typeof conversationFacts !== "object" || Number(conversationFacts.turnCount ?? 0) <= 0) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
"Prior session facts (bounded, redacted; use for Chinese answers about previous turns):",
|
||||
`- sessionId=${safePromptFact(conversationFacts.sessionId)}; sessionStatus=${safePromptFact(conversationFacts.sessionStatus)}; sessionMode=${safePromptFact(conversationFacts.sessionMode)}; turnCount=${safePromptFact(conversationFacts.turnCount)}.`,
|
||||
`- provider/backend/runner=${safePromptFact(conversationFacts.latestProvider)}/${safePromptFact(conversationFacts.latestBackend)}/${safePromptFact(conversationFacts.runnerKind)}; capabilityLevel=${safePromptFact(conversationFacts.capabilityLevel)}.`,
|
||||
`- workspace=${safePromptFact(conversationFacts.workspace)}; sandbox=${safePromptFact(conversationFacts.sandbox)}.`,
|
||||
`- traceIds=${promptList(conversationFacts.traceIds, 6)}; latestTraceId=${safePromptFact(conversationFacts.latestTraceId)}.`,
|
||||
`- skills=${promptSkills(conversationFacts.latestSkills)}.`,
|
||||
`- toolCalls=${promptToolCalls(conversationFacts.recentToolCalls)}.`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function promptSkills(skills) {
|
||||
if (!skills || typeof skills !== "object") return "not_requested";
|
||||
const names = Array.isArray(skills.names) ? skills.names.filter(Boolean).slice(0, 8) : [];
|
||||
const count = skills.totalCount ?? skills.count ?? names.length;
|
||||
return `${safePromptFact(skills.status)} count=${safePromptFact(count)}${names.length ? ` names=${names.map(safePromptFact).join(",")}` : ""}`;
|
||||
}
|
||||
|
||||
function promptToolCalls(toolCalls) {
|
||||
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return "none";
|
||||
return toolCalls
|
||||
.slice(-6)
|
||||
.map((toolCall) => [toolCall?.name, toolCall?.status, toolCall?.route].filter(Boolean).map(safePromptFact).join(":"))
|
||||
.filter(Boolean)
|
||||
.join(",") || "none";
|
||||
}
|
||||
|
||||
function promptList(values, limit) {
|
||||
return Array.isArray(values) && values.length > 0
|
||||
? values.filter(Boolean).slice(-limit).map(safePromptFact).join(",")
|
||||
: "none";
|
||||
}
|
||||
|
||||
function safePromptFact(value) {
|
||||
if (value === undefined || value === null || value === "") return "none";
|
||||
return redactText(String(value).replace(/\s+/gu, " ").trim()).slice(0, 180);
|
||||
}
|
||||
|
||||
function resolveCodexWorkspace(env = process.env, options = {}) {
|
||||
return path.resolve(firstNonEmpty(
|
||||
options.workspace,
|
||||
|
||||
@@ -2815,7 +2815,6 @@ function messageEvidencePanel(message) {
|
||||
if (fields.length === 0) return null;
|
||||
const details = document.createElement("details");
|
||||
details.className = "message-evidence";
|
||||
details.open = message.status === "running" || message.status === "completed" || message.status === "source" || message.status === "failed";
|
||||
const summary = document.createElement("summary");
|
||||
summary.className = "message-meta";
|
||||
summary.textContent = messageEvidenceSummary(message, fields);
|
||||
|
||||
@@ -816,6 +816,7 @@ assert.match(app, /function isSourceFixtureCompletedChatMessage/);
|
||||
assert.match(app, /Code Agent SOURCE 回复/);
|
||||
assert.match(app, /SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/);
|
||||
assert.match(app, /function messageEvidencePanel/);
|
||||
assert.doesNotMatch(app, /details\.open\s*=\s*message\.status === "running"/);
|
||||
assert.match(app, /function messageAttributionPanel/);
|
||||
assert.match(app, /function messageEvidenceSummary/);
|
||||
assert.match(app, /function boundedEvidenceField/);
|
||||
|
||||
Reference in New Issue
Block a user