Skip to content
43 changes: 24 additions & 19 deletions packages/junior/src/chat/respond-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ export function summarizeMessageText(text: string): string {
: normalized;
}

/** Wrap user input with conversation context and observability metadata XML tags. */
/**
* Wrap the current user turn with self-describing marker blocks: background
* first, current instruction last. Ordering matches long-context attention
* guidance for Sonnet and GPT-5.
*/
export function buildUserTurnText(
userInput: string,
conversationContext?: string,
Expand All @@ -153,47 +157,48 @@ export function buildUserTurnText(
},
): string {
const trimmedContext = conversationContext?.trim();
const hasSessionContext = Boolean(metadata?.sessionContext?.conversationId);
const hasTurnContext = Boolean(metadata?.turnContext?.traceId);
const conversationId = metadata?.sessionContext?.conversationId;
const traceId = metadata?.turnContext?.traceId;

if (!trimmedContext && !hasSessionContext && !hasTurnContext) {
if (!trimmedContext && !conversationId && !traceId) {
return userInput;
}

const sections: string[] = [
"<current-message>",
userInput,
"</current-message>",
];
const sections: string[] = [];

if (trimmedContext) {
sections.push(
"",
"<thread-conversation-context>",
"Use this context for continuity across prior thread turns.",
"<thread-background>",
trimmedContext,
"</thread-conversation-context>",
"</thread-background>",
"",
);
}

if (metadata?.sessionContext?.conversationId) {
if (conversationId) {
sections.push(
"",
"<session-context>",
`- gen_ai.conversation.id: ${metadata.sessionContext.conversationId}`,
`- gen_ai.conversation.id: ${conversationId}`,
"</session-context>",
"",
);
}

if (metadata?.turnContext?.traceId) {
if (traceId) {
sections.push(
"",
"<turn-context>",
`- trace_id: ${metadata.turnContext.traceId}`,
`- trace_id: ${traceId}`,
"</turn-context>",
"",
);
}

sections.push(
'<current-instruction priority="highest">',
userInput,
"</current-instruction>",
);

return sections.join("\n");
}

Expand Down
43 changes: 30 additions & 13 deletions packages/junior/src/chat/services/conversation-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from "@/chat/state/conversation";
import { toOptionalString } from "@/chat/coerce";
import { logWarn, setSpanAttributes } from "@/chat/logging";
import { escapeXml } from "@/chat/xml";

const CONTEXT_COMPACTION_TRIGGER_TOKENS = 9000;
const CONTEXT_COMPACTION_TARGET_TOKENS = 7000;
Expand Down Expand Up @@ -152,6 +153,11 @@ export function markConversationMessage(
updateConversationStats(conversation);
}

/**
* Render thread history as structured XML. Each compaction and message is
* wrapped with index/ts metadata so the model can reference prior items
* individually instead of treating the whole block as one flat narrative.
*/
export function buildConversationContext(
conversation: ThreadConversationState,
options: {
Expand All @@ -166,25 +172,31 @@ export function buildConversationContext(
}

const lines: string[] = [];

if (conversation.compactions.length > 0) {
lines.push("<thread-compactions>");
for (const [index, compaction] of conversation.compactions.entries()) {
lines.push(
[
`summary_${index + 1}:`,
compaction.summary,
`covered_messages: ${compaction.coveredMessageIds.length}`,
`created_at: ${new Date(compaction.createdAtMs).toISOString()}`,
].join(" "),
` <compaction index="${index + 1}" covered_messages="${compaction.coveredMessageIds.length}" created_at="${new Date(compaction.createdAtMs).toISOString()}">`,
compaction.summary,
" </compaction>",
);
}
lines.push("</thread-compactions>");
lines.push("");
lines.push("</thread-compactions>", "");
}

lines.push("<thread-transcript>");
for (const message of messages) {
lines.push(renderConversationMessageLine(message, conversation));
for (const [index, message] of messages.entries()) {
const author = escapeXml(message.author?.userName ?? message.role);
const ts = new Date(message.createdAtMs).toISOString();
const slackTsAttr = message.meta?.slackTs
? ` slack_ts="${escapeXml(message.meta.slackTs)}"`
: "";
lines.push(
` <message index="${index + 1}" ts="${ts}" role="${message.role}" author="${author}"${slackTsAttr}>`,
renderConversationMessageLine(message, conversation),
" </message>",
);
}
lines.push("</thread-transcript>");
return lines.join("\n");
Expand Down Expand Up @@ -240,9 +252,14 @@ async function summarizeConversationChunk(
role: "user",
content: [
"Summarize the following older Slack thread transcript segment for future assistant turns.",
"Keep the summary factual and concise.",
"Preserve decisions, commitments, constraints, locations, hiring criteria, and unresolved asks.",
"Do not invent details.",
"Keep the summary factual and concise. Do not invent details.",
"",
"Output exactly three XML sections in this order:",
"<active-asks> one bullet per outstanding user ask that has not been narrowed, answered, or superseded by a later turn. Omit the section body if none. </active-asks>",
"<superseded-or-completed-asks> one bullet per ask that has been rescoped, narrowed, answered, or already acted on in this segment. Include the replacement/outcome inline. Omit the section body if none. </superseded-or-completed-asks>",
"<facts> one bullet per durable fact useful regardless of scope: names, ids, URLs, decisions, locations, preferences, constraints that remain true. Omit the section body if none. </facts>",
"",
"Do not output any text outside the three sections.",
"",
transcript,
].join("\n"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { describe, expect, it } from "vitest";
import { buildUserTurnText } from "@/chat/respond-helpers";

describe("buildUserTurnText", () => {
it("returns raw input when no context or metadata is provided", () => {
expect(buildUserTurnText("hello")).toBe("hello");
});
});
12 changes: 11 additions & 1 deletion packages/junior/tests/unit/services/conversation-memory.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { getThreadTitleSourceMessage } from "@/chat/services/conversation-memory";
import {
buildConversationContext,
getThreadTitleSourceMessage,
} from "@/chat/services/conversation-memory";
import { coerceThreadConversationState } from "@/chat/state/conversation";

describe("conversation memory title source", () => {
Expand Down Expand Up @@ -58,3 +61,10 @@ describe("conversation memory title source", () => {
);
});
});

describe("buildConversationContext", () => {
it("returns undefined for an empty conversation", () => {
const conversation = coerceThreadConversationState({});
expect(buildConversationContext(conversation)).toBeUndefined();
});
});
Loading