diff --git a/packages/junior/src/chat/prompt.ts b/packages/junior/src/chat/prompt.ts
index b373e4d04..562bd4ee3 100644
--- a/packages/junior/src/chat/prompt.ts
+++ b/packages/junior/src/chat/prompt.ts
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
-import { botConfig, getRuntimeMetadata } from "@/chat/config";
+import { botConfig } from "@/chat/config";
import { TURN_CONTEXT_TAG } from "@/chat/turn-context-tag";
import {
listReferenceFiles,
@@ -171,7 +171,7 @@ function formatSkillEntry(skill: SkillMetadata): string[] {
function formatAvailableSkillsForPrompt(
skills: SkillMetadata[],
invocation: SkillInvocation | null,
-): string {
+): string | null {
const autoSelectable = skills.filter(
(s) => s.disableModelInvocation !== true,
);
@@ -184,20 +184,18 @@ function formatAvailableSkillsForPrompt(
const sections: string[] = [];
- // Available skills: model may load these when they match the request.
- const available = [
- "",
- ...(autoSelectable.length > 0
- ? [
- "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. If none fits, do not load a skill.",
- ]
- : []),
- ];
- for (const skill of autoSelectable) {
- available.push(...formatSkillEntry(skill));
+ if (autoSelectable.length > 0) {
+ // Available skills: model may load these when they match the request.
+ const available = [
+ "",
+ "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. If none fits, do not load a skill.",
+ ];
+ for (const skill of autoSelectable) {
+ available.push(...formatSkillEntry(skill));
+ }
+ available.push("");
+ sections.push(available.join("\n"));
}
- available.push("");
- sections.push(available.join("\n"));
// User-callable skills: model must not auto-select these.
if (invokedExplicitOnly.length > 0) {
@@ -212,12 +210,12 @@ function formatAvailableSkillsForPrompt(
sections.push(userCallable.join("\n"));
}
- return sections.join("\n");
+ return sections.length > 0 ? sections.join("\n") : null;
}
-function formatLoadedSkillsForPrompt(skills: Skill[]): string {
+function formatLoadedSkillsForPrompt(skills: Skill[]): string | null {
if (skills.length === 0) {
- return "\n";
+ return null;
}
const lines = [""];
@@ -379,23 +377,6 @@ function formatConfigurationLines(
);
}
-function formatSlackCapabilityNames(
- capabilities:
- | {
- canAddReactions?: boolean;
- canCreateCanvas?: boolean;
- canPostToChannel?: boolean;
- }
- | undefined,
-): string {
- const names = [
- capabilities?.canCreateCanvas ? "canvas_create" : "",
- capabilities?.canPostToChannel ? "channel_post" : "",
- capabilities?.canAddReactions ? "reaction_add" : "",
- ].filter(Boolean);
- return names.length > 0 ? names.join(", ") : "none";
-}
-
const HEADER =
"You are a Slack-based helper assistant. Follow the personality block for voice and tone in every reply. The behavior and output blocks define platform mechanics and override personality only when those mechanics conflict.";
@@ -500,32 +481,20 @@ function buildIdentitySection(): string {
}
function buildRuntimeSection(params: {
- channelId?: string;
- fastModelId?: string;
- modelId?: string;
- slackCapabilities?: {
- canAddReactions?: boolean;
- canCreateCanvas?: boolean;
- canPostToChannel?: boolean;
- };
- thinkingLevel?: string;
-}): string {
+ conversationId?: string;
+ traceId?: string;
+}): string | null {
const lines = [
- `- version: ${escapeXml(getRuntimeMetadata().version ?? "unknown")}`,
- params.modelId ? `- model: ${escapeXml(params.modelId)}` : "",
- params.fastModelId ? `- fast_model: ${escapeXml(params.fastModelId)}` : "",
- params.thinkingLevel
- ? `- thinking: ${escapeXml(params.thinkingLevel)}`
+ params.conversationId
+ ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}`
: "",
- params.channelId ? "- channel: slack" : "",
- params.channelId
- ? `- slack_capabilities: ${escapeXml(
- formatSlackCapabilityNames(params.slackCapabilities),
- )}`
- : "",
- `- sandbox_workspace: ${escapeXml(SANDBOX_WORKSPACE_ROOT)}`,
+ params.traceId ? `- trace_id: ${escapeXml(params.traceId)}` : "",
].filter(Boolean);
+ if (lines.length === 0) {
+ return null;
+ }
+
return renderTagBlock("runtime", lines.join("\n"));
}
@@ -535,7 +504,7 @@ function buildContextSection(params: {
configuration?: Record;
invocation: SkillInvocation | null;
turnState?: "fresh" | "resumed";
-}): string {
+}): string | null {
const blocks: string[][] = [];
if (JUNIOR_WORLD) {
@@ -593,6 +562,10 @@ function buildContextSection(params: {
}
const body = blocks.map((block) => block.join("\n")).join("\n\n");
+ if (!body) {
+ return null;
+ }
+
return renderTagBlock("context", body);
}
@@ -602,12 +575,20 @@ function buildCapabilitiesSection(params: {
activeMcpCatalogs: ActiveMcpCatalogSummary[];
invocation: SkillInvocation | null;
toolGuidance?: ToolPromptContext[];
-}): string {
+}): string | null {
const blocks: string[] = [];
- blocks.push(
- formatAvailableSkillsForPrompt(params.availableSkills, params.invocation),
+ const availableSkills = formatAvailableSkillsForPrompt(
+ params.availableSkills,
+ params.invocation,
);
- blocks.push(formatLoadedSkillsForPrompt(params.activeSkills));
+ if (availableSkills) {
+ blocks.push(availableSkills);
+ }
+
+ const loadedSkills = formatLoadedSkillsForPrompt(params.activeSkills);
+ if (loadedSkills) {
+ blocks.push(loadedSkills);
+ }
const activeCatalogs = formatActiveMcpCatalogsForPrompt(
params.activeMcpCatalogs,
@@ -626,6 +607,10 @@ function buildCapabilitiesSection(params: {
blocks.push(renderTagBlock("providers", providerCatalog));
}
+ if (blocks.length === 0) {
+ return null;
+ }
+
return renderTagBlock("capabilities", blocks.join("\n\n"));
}
@@ -635,15 +620,8 @@ type TurnContextPromptInput = {
activeMcpCatalogs?: ActiveMcpCatalogSummary[];
toolGuidance?: ToolPromptContext[];
runtime?: {
- channelId?: string;
- fastModelId?: string;
- modelId?: string;
- slackCapabilities?: {
- canAddReactions?: boolean;
- canCreateCanvas?: boolean;
- canPostToChannel?: boolean;
- };
- thinkingLevel?: string;
+ conversationId?: string;
+ traceId?: string;
};
invocation: SkillInvocation | null;
requester?: {
@@ -702,7 +680,7 @@ export function buildTurnContextPrompt(params: TurnContextPromptInput): string {
}),
buildRuntimeSection(params.runtime ?? {}),
`${TURN_CONTEXT_TAG}>`,
- ];
+ ].filter((section): section is string => Boolean(section));
return sections.join("\n\n");
}
diff --git a/packages/junior/src/chat/respond-helpers.ts b/packages/junior/src/chat/respond-helpers.ts
index baeb04326..2afb363da 100644
--- a/packages/junior/src/chat/respond-helpers.ts
+++ b/packages/junior/src/chat/respond-helpers.ts
@@ -149,62 +149,28 @@ export function summarizeMessageText(text: string): string {
}
/**
- * 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.
+ * Put prior thread text before the current instruction when no Pi history
+ * exists. These are top-level sibling blocks in the user message.
*/
export function buildUserTurnText(
userInput: string,
conversationContext?: string,
- metadata?: {
- sessionContext?: { conversationId?: string };
- turnContext?: { traceId?: string };
- },
): string {
const trimmedContext = conversationContext?.trim();
- const conversationId = metadata?.sessionContext?.conversationId;
- const traceId = metadata?.turnContext?.traceId;
- if (!trimmedContext && !conversationId && !traceId) {
+ if (!trimmedContext) {
return userInput;
}
- const sections: string[] = [];
-
- if (trimmedContext) {
- sections.push(
- "",
- trimmedContext,
- "",
- "",
- );
- }
-
- if (conversationId) {
- sections.push(
- "",
- `- gen_ai.conversation.id: ${conversationId}`,
- "",
- "",
- );
- }
-
- if (traceId) {
- sections.push(
- "",
- `- trace_id: ${traceId}`,
- "",
- "",
- );
- }
-
- sections.push(
- '',
+ return [
+ "",
+ trimmedContext,
+ "",
+ "",
+ "",
userInput,
"",
- );
-
- return sections.join("\n");
+ ].join("\n");
}
/** Encode a non-image attachment as base64 XML for the prompt. */
diff --git a/packages/junior/src/chat/respond.ts b/packages/junior/src/chat/respond.ts
index ee76f2796..d94152749 100644
--- a/packages/junior/src/chat/respond.ts
+++ b/packages/junior/src/chat/respond.ts
@@ -556,10 +556,6 @@ export async function generateAssistantReply(
const userTurnText = buildUserTurnText(
userInput,
promptConversationContext,
- {
- sessionContext: { conversationId: sessionConversationId },
- turnContext: { traceId: getActiveTraceId() },
- },
);
const { routerBlocks, userContentParts } = buildUserTurnInput({
omittedImageAttachmentCount: context.omittedImageAttachmentCount ?? 0,
@@ -770,11 +766,8 @@ export async function generateAssistantReply(
activeMcpCatalogs,
toolGuidance,
runtime: {
- channelId: toolChannelId,
- fastModelId: botConfig.fastModelId,
- modelId: botConfig.modelId,
- slackCapabilities: channelCapabilities,
- thinkingLevel: thinkingSelection.thinkingLevel,
+ conversationId: spanContext.conversationId,
+ traceId: getActiveTraceId(),
},
invocation: skillInvocation,
requester: context.requester,
diff --git a/packages/junior/tests/unit/misc/respond-helpers-user-turn.test.ts b/packages/junior/tests/unit/misc/respond-helpers-user-turn.test.ts
index b0a2ddc43..47d4b12e7 100644
--- a/packages/junior/tests/unit/misc/respond-helpers-user-turn.test.ts
+++ b/packages/junior/tests/unit/misc/respond-helpers-user-turn.test.ts
@@ -5,4 +5,18 @@ describe("buildUserTurnText", () => {
it("returns raw input when no context or metadata is provided", () => {
expect(buildUserTurnText("hello")).toBe("hello");
});
+
+ it("keeps only causal thread context around the current instruction", () => {
+ expect(buildUserTurnText("what now?", "alice: budget is due Friday")).toBe(
+ [
+ "",
+ "alice: budget is due Friday",
+ "",
+ "",
+ "",
+ "what now?",
+ "",
+ ].join("\n"),
+ );
+ });
});
diff --git a/packages/junior/tests/unit/prompt.test.ts b/packages/junior/tests/unit/prompt.test.ts
index 3741b3719..d32e36558 100644
--- a/packages/junior/tests/unit/prompt.test.ts
+++ b/packages/junior/tests/unit/prompt.test.ts
@@ -18,9 +18,8 @@ describe("prompt builders", () => {
invocation: null,
requester: { userId: "U_ALPHA" },
runtime: {
- channelId: "C_ALPHA",
- modelId: "model-alpha",
- thinkingLevel: "medium",
+ conversationId: "conversation-alpha",
+ traceId: "trace-alpha",
},
turnState: "fresh",
});
@@ -40,9 +39,7 @@ describe("prompt builders", () => {
invocation: null,
requester: { userId: "U_BETA" },
runtime: {
- channelId: "C_BETA",
- modelId: "model-beta",
- thinkingLevel: "high",
+ conversationId: "conversation-beta",
},
turnState: "resumed",
});
@@ -55,6 +52,17 @@ describe("prompt builders", () => {
expect(firstTurnContext).not.toContain("");
expect(firstTurnContext).not.toContain("");
expect(firstTurnContext).toContain("");
+ expect(firstTurnContext).toContain(
+ "- gen_ai.conversation.id: conversation-alpha",
+ );
+ expect(firstTurnContext).toContain("- trace_id: trace-alpha");
+ expect(firstTurnContext).not.toContain("- model:");
+ expect(firstTurnContext).not.toContain("- fast_model:");
+ expect(firstTurnContext).not.toContain("- channel:");
+ expect(firstTurnContext).not.toContain("- slack_capabilities:");
+ expect(firstTurnContext).not.toContain("- thinking:");
+ expect(firstTurnContext).not.toContain("- sandbox_workspace:");
+ expect(firstSystemPrompt).not.toContain("trace-alpha");
expect(buildSystemPrompt()).toBe(firstSystemPrompt);
});
@@ -68,6 +76,9 @@ describe("prompt builders", () => {
});
expect(turnContext).not.toContain("");
+ expect(turnContext).not.toContain("");
+ expect(turnContext).not.toContain("");
+ expect(turnContext).not.toContain("");
});
it("puts tool guidance in turn context, not the static system prompt", () => {
diff --git a/packages/junior/tests/unit/runtime/respond-mcp-progressive-loading.test.ts b/packages/junior/tests/unit/runtime/respond-mcp-progressive-loading.test.ts
index 1ab3bde7c..40d43b0e9 100644
--- a/packages/junior/tests/unit/runtime/respond-mcp-progressive-loading.test.ts
+++ b/packages/junior/tests/unit/runtime/respond-mcp-progressive-loading.test.ts
@@ -17,6 +17,7 @@ const {
omitFinalAssistantAfterTool,
pushPreToolAssistantMessage,
promptCallCount,
+ promptMessages,
promptSeedMessages,
recordToolResultMessage,
resumeTurnContextCounts,
@@ -42,6 +43,7 @@ const {
loadSkillsByNameMock: vi.fn(),
omitFinalAssistantAfterTool: { value: false },
promptCallCount: { value: 0 },
+ promptMessages: [] as unknown[],
promptSeedMessages: [] as unknown[][],
pushPreToolAssistantMessage: { value: false },
recordToolResultMessage: { value: false },
@@ -143,6 +145,7 @@ vi.mock("@earendil-works/pi-agent-core", () => {
async prompt(message: unknown) {
promptCallCount.value += 1;
this.aborted = false;
+ promptMessages.push(message);
promptSeedMessages.push([...this.state.messages]);
this.state.messages.push(message);
@@ -564,6 +567,7 @@ describe("generateAssistantReply progressive MCP loading", () => {
loadSkillsByNameMock.mockReset();
omitFinalAssistantAfterTool.value = false;
promptCallCount.value = 0;
+ promptMessages.length = 0;
promptSeedMessages.length = 0;
pushPreToolAssistantMessage.value = false;
recordToolResultMessage.value = false;
@@ -759,6 +763,12 @@ describe("generateAssistantReply progressive MCP loading", () => {
});
expect(promptSeedMessages[0]).toEqual(priorMessages);
+ expect(JSON.stringify(promptMessages[0])).not.toContain(
+ "duplicated prior transcript",
+ );
+ expect(JSON.stringify(promptMessages[0])).not.toContain(
+ "",
+ );
});
it("parks for auth when MCP auth is requested during a tool call", async () => {