From 8c4253719adb58cf34dcebf5ae95ebedc68b2f5d Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 23 May 2026 12:07:46 -0700 Subject: [PATCH 1/7] fix(chat): Keep prompt transcripts faithful Remove trace and conversation identifiers from user prompt text so the model only sees causal thread context and the current instruction. Keep span message attributes faithful to the actual model transcript instead of display-cleaning them. Add regression coverage for avoiding duplicated thread context when Pi history is already present and for preserving chat input text in GenAI span attributes. Supersedes GH-404 Co-Authored-By: OpenAI GPT-5 Codex --- packages/junior/src/chat/respond-helpers.ts | 32 +------------ packages/junior/src/chat/respond.ts | 5 -- .../tests/unit/chat/pi/traced-stream.test.ts | 48 +++++++++++++++++++ .../misc/respond-helpers-user-turn.test.ts | 14 ++++++ .../respond-mcp-progressive-loading.test.ts | 10 ++++ specs/logging/tracing-spec.md | 1 + 6 files changed, 75 insertions(+), 35 deletions(-) diff --git a/packages/junior/src/chat/respond-helpers.ts b/packages/junior/src/chat/respond-helpers.ts index baeb0432..a5f11c4f 100644 --- a/packages/junior/src/chat/respond-helpers.ts +++ b/packages/junior/src/chat/respond-helpers.ts @@ -156,16 +156,10 @@ export function summarizeMessageText(text: string): string { 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; } @@ -180,29 +174,7 @@ export function buildUserTurnText( ); } - if (conversationId) { - sections.push( - "", - `- gen_ai.conversation.id: ${conversationId}`, - "", - "", - ); - } - - if (traceId) { - sections.push( - "", - `- trace_id: ${traceId}`, - "", - "", - ); - } - - sections.push( - '', - userInput, - "", - ); + sections.push("", userInput, ""); return sections.join("\n"); } diff --git a/packages/junior/src/chat/respond.ts b/packages/junior/src/chat/respond.ts index ee76f279..9210d7de 100644 --- a/packages/junior/src/chat/respond.ts +++ b/packages/junior/src/chat/respond.ts @@ -4,7 +4,6 @@ import { botConfig } from "@/chat/config"; import { extractGenAiUsageAttributes, extractGenAiUsageSummary, - getActiveTraceId, logException, logInfo, logWarn, @@ -556,10 +555,6 @@ export async function generateAssistantReply( const userTurnText = buildUserTurnText( userInput, promptConversationContext, - { - sessionContext: { conversationId: sessionConversationId }, - turnContext: { traceId: getActiveTraceId() }, - }, ); const { routerBlocks, userContentParts } = buildUserTurnInput({ omittedImageAttachmentCount: context.omittedImageAttachmentCount ?? 0, diff --git a/packages/junior/tests/unit/chat/pi/traced-stream.test.ts b/packages/junior/tests/unit/chat/pi/traced-stream.test.ts index c49a0c49..e779b3ee 100644 --- a/packages/junior/tests/unit/chat/pi/traced-stream.test.ts +++ b/packages/junior/tests/unit/chat/pi/traced-stream.test.ts @@ -116,6 +116,54 @@ describe("createTracedStreamFn", () => { expect(opts.attributes["gen_ai.request.model"]).toBe("openai/gpt-5.4"); }); + it("serializes the actual chat input messages without text normalization", async () => { + const { createTracedStreamFn } = await import("@/chat/pi/traced-stream"); + const stream = createAssistantMessageEventStream(); + const base = vi.fn(() => stream); + + const userText = [ + "", + "prior user message", + "", + "", + "", + "what is sentry?", + "", + ].join("\n"); + + const traced = createTracedStreamFn(base as unknown as StreamFn); + await traced( + fakeModel("openai/gpt-5.4"), + { + systemPrompt: "you are junior", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "\nskills and config\n", + }, + { type: "text", text: userText }, + ], + timestamp: 0, + }, + ], + }, + undefined, + ); + + const opts = startInactiveSpan.mock.calls[0]?.[0] as unknown as { + attributes: Record; + }; + const inputMessages = opts.attributes["gen_ai.input.messages"] as string; + expect(inputMessages).toContain("runtime-turn-context"); + expect(inputMessages).toContain("thread-background"); + expect(inputMessages).toContain("current-instruction"); + expect(inputMessages).toContain("prior user message"); + expect(inputMessages).toContain("what is sentry?"); + }); + it("sets output.messages, usage tokens, finish_reasons, response.model after stream completion", async () => { const { createTracedStreamFn } = await import("@/chat/pi/traced-stream"); const stream = createAssistantMessageEventStream(); 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 b0a2ddc4..47d4b12e 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/runtime/respond-mcp-progressive-loading.test.ts b/packages/junior/tests/unit/runtime/respond-mcp-progressive-loading.test.ts index 1ab3bde7..40d43b0e 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 () => { diff --git a/specs/logging/tracing-spec.md b/specs/logging/tracing-spec.md index 93430aef..b52ef676 100644 --- a/specs/logging/tracing-spec.md +++ b/specs/logging/tracing-spec.md @@ -191,6 +191,7 @@ semantic conventions: - A `gen_ai.invoke_agent` span MUST have at least one `gen_ai.chat` child covering the LLM call(s) issued during its agent loop. - A `gen_ai.chat` span MAY appear at the top level (as a sibling of `gen_ai.invoke_agent`, or under a non-`gen_ai.*` parent such as `chat.route_thinking`) only when it represents an LLM call that is independent of an agent loop, for example a routing or classification pre-flight. - Every `gen_ai.chat` span MUST carry `gen_ai.input.messages` and `gen_ai.output.messages`. +- `gen_ai.input.messages` / `gen_ai.output.messages` MUST represent the actual model transcript for that call; do not rewrite or clean message text for display-only concerns. Apply only the shared safety serialization limits for unsafe blobs, length, depth, or circular values. - The parent `gen_ai.invoke_agent` MAY also carry `gen_ai.input.messages` / `gen_ai.output.messages` as a high-level rollup; this is optional. - A `gen_ai.chat` span MUST have its status set to error (code 2) when the underlying LLM call fails — either because `streamFn` itself throws or because the returned stream rejects. - The per-iteration `gen_ai.chat` child span is created in `packages/junior/src/chat/pi/traced-stream.ts` via the `streamFn` injected into `pi-agent-core`'s `Agent`. This applies to both the main agent and the advisor agent. From 91e83e3a410ffde9ab9cff9eb876f5ca029e37bf Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 23 May 2026 12:13:02 -0700 Subject: [PATCH 2/7] ref(chat): Simplify user turn context assembly Remove a redundant guard after the early return in buildUserTurnText. This keeps the prompt behavior unchanged while addressing review feedback. Co-Authored-By: OpenAI GPT-5 Codex --- packages/junior/src/chat/respond-helpers.ts | 23 ++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/junior/src/chat/respond-helpers.ts b/packages/junior/src/chat/respond-helpers.ts index a5f11c4f..968eb2b6 100644 --- a/packages/junior/src/chat/respond-helpers.ts +++ b/packages/junior/src/chat/respond-helpers.ts @@ -163,20 +163,15 @@ export function buildUserTurnText( return userInput; } - const sections: string[] = []; - - if (trimmedContext) { - sections.push( - "", - trimmedContext, - "", - "", - ); - } - - sections.push("", userInput, ""); - - return sections.join("\n"); + return [ + "", + trimmedContext, + "", + "", + "", + userInput, + "", + ].join("\n"); } /** Encode a non-image attachment as base64 XML for the prompt. */ From 3cd52a2917a61b19bd10793c176d127bdd3520cb Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 23 May 2026 12:43:05 -0700 Subject: [PATCH 3/7] fix(chat): Keep trace id in runtime context Expose the active trace id through the per-turn runtime context so the bot can look up its own debug information. Keep it out of buildUserTurnText so the current instruction wrapper only contains causal thread context and user text. Co-Authored-By: OpenAI GPT-5 Codex --- packages/junior/src/chat/prompt.ts | 3 +++ packages/junior/src/chat/respond.ts | 2 ++ packages/junior/tests/unit/prompt.test.ts | 3 +++ 3 files changed, 8 insertions(+) diff --git a/packages/junior/src/chat/prompt.ts b/packages/junior/src/chat/prompt.ts index b373e4d0..df3a9674 100644 --- a/packages/junior/src/chat/prompt.ts +++ b/packages/junior/src/chat/prompt.ts @@ -509,9 +509,11 @@ function buildRuntimeSection(params: { canPostToChannel?: boolean; }; thinkingLevel?: string; + traceId?: string; }): string { const lines = [ `- version: ${escapeXml(getRuntimeMetadata().version ?? "unknown")}`, + params.traceId ? `- trace_id: ${escapeXml(params.traceId)}` : "", params.modelId ? `- model: ${escapeXml(params.modelId)}` : "", params.fastModelId ? `- fast_model: ${escapeXml(params.fastModelId)}` : "", params.thinkingLevel @@ -644,6 +646,7 @@ type TurnContextPromptInput = { canPostToChannel?: boolean; }; thinkingLevel?: string; + traceId?: string; }; invocation: SkillInvocation | null; requester?: { diff --git a/packages/junior/src/chat/respond.ts b/packages/junior/src/chat/respond.ts index 9210d7de..0d5fb3da 100644 --- a/packages/junior/src/chat/respond.ts +++ b/packages/junior/src/chat/respond.ts @@ -4,6 +4,7 @@ import { botConfig } from "@/chat/config"; import { extractGenAiUsageAttributes, extractGenAiUsageSummary, + getActiveTraceId, logException, logInfo, logWarn, @@ -770,6 +771,7 @@ export async function generateAssistantReply( modelId: botConfig.modelId, slackCapabilities: channelCapabilities, thinkingLevel: thinkingSelection.thinkingLevel, + traceId: getActiveTraceId(), }, invocation: skillInvocation, requester: context.requester, diff --git a/packages/junior/tests/unit/prompt.test.ts b/packages/junior/tests/unit/prompt.test.ts index 3741b371..9f584b5b 100644 --- a/packages/junior/tests/unit/prompt.test.ts +++ b/packages/junior/tests/unit/prompt.test.ts @@ -21,6 +21,7 @@ describe("prompt builders", () => { channelId: "C_ALPHA", modelId: "model-alpha", thinkingLevel: "medium", + traceId: "trace-alpha", }, turnState: "fresh", }); @@ -55,6 +56,8 @@ describe("prompt builders", () => { expect(firstTurnContext).not.toContain(""); expect(firstTurnContext).not.toContain(""); expect(firstTurnContext).toContain(""); + expect(firstTurnContext).toContain("- trace_id: trace-alpha"); + expect(firstSystemPrompt).not.toContain("trace-alpha"); expect(buildSystemPrompt()).toBe(firstSystemPrompt); }); From 4aeaf885dea629a60e08b588037eee53725a416f Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 23 May 2026 12:59:36 -0700 Subject: [PATCH 4/7] docs(chat): Clarify top-level user turn blocks Clarify that thread background and current instruction are top-level sibling blocks in the user message, not nested runtime metadata. This keeps the prompt shape simple while preserving the existing delimiter boundaries. Co-Authored-By: OpenAI GPT-5 Codex --- packages/junior/src/chat/respond-helpers.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/junior/src/chat/respond-helpers.ts b/packages/junior/src/chat/respond-helpers.ts index 968eb2b6..2afb363d 100644 --- a/packages/junior/src/chat/respond-helpers.ts +++ b/packages/junior/src/chat/respond-helpers.ts @@ -149,9 +149,8 @@ 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, From 56816cecbaf3ebb23acb2ff3208e7c56ea4d255e Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 23 May 2026 13:06:30 -0700 Subject: [PATCH 5/7] fix(chat): Trim runtime prompt context Remove internal model, channel, and Slack capability fields from the model-visible runtime context. Keep the conversation id and trace id so the agent can correlate its own turn with telemetry and debug data. Co-Authored-By: GPT-5 Codex --- packages/junior/src/chat/prompt.ts | 46 +++-------------------- packages/junior/src/chat/respond.ts | 5 +-- packages/junior/tests/unit/prompt.test.ts | 13 +++++-- 3 files changed, 15 insertions(+), 49 deletions(-) diff --git a/packages/junior/src/chat/prompt.ts b/packages/junior/src/chat/prompt.ts index df3a9674..fb0adc41 100644 --- a/packages/junior/src/chat/prompt.ts +++ b/packages/junior/src/chat/prompt.ts @@ -379,23 +379,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,31 +483,19 @@ function buildIdentitySection(): string { } function buildRuntimeSection(params: { - channelId?: string; - fastModelId?: string; - modelId?: string; - slackCapabilities?: { - canAddReactions?: boolean; - canCreateCanvas?: boolean; - canPostToChannel?: boolean; - }; + conversationId?: string; thinkingLevel?: string; traceId?: string; }): string { const lines = [ `- version: ${escapeXml(getRuntimeMetadata().version ?? "unknown")}`, + params.conversationId + ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` + : "", params.traceId ? `- trace_id: ${escapeXml(params.traceId)}` : "", - params.modelId ? `- model: ${escapeXml(params.modelId)}` : "", - params.fastModelId ? `- fast_model: ${escapeXml(params.fastModelId)}` : "", params.thinkingLevel ? `- thinking: ${escapeXml(params.thinkingLevel)}` : "", - params.channelId ? "- channel: slack" : "", - params.channelId - ? `- slack_capabilities: ${escapeXml( - formatSlackCapabilityNames(params.slackCapabilities), - )}` - : "", `- sandbox_workspace: ${escapeXml(SANDBOX_WORKSPACE_ROOT)}`, ].filter(Boolean); @@ -637,14 +608,7 @@ type TurnContextPromptInput = { activeMcpCatalogs?: ActiveMcpCatalogSummary[]; toolGuidance?: ToolPromptContext[]; runtime?: { - channelId?: string; - fastModelId?: string; - modelId?: string; - slackCapabilities?: { - canAddReactions?: boolean; - canCreateCanvas?: boolean; - canPostToChannel?: boolean; - }; + conversationId?: string; thinkingLevel?: string; traceId?: string; }; diff --git a/packages/junior/src/chat/respond.ts b/packages/junior/src/chat/respond.ts index 0d5fb3da..770906e5 100644 --- a/packages/junior/src/chat/respond.ts +++ b/packages/junior/src/chat/respond.ts @@ -766,10 +766,7 @@ export async function generateAssistantReply( activeMcpCatalogs, toolGuidance, runtime: { - channelId: toolChannelId, - fastModelId: botConfig.fastModelId, - modelId: botConfig.modelId, - slackCapabilities: channelCapabilities, + conversationId: spanContext.conversationId, thinkingLevel: thinkingSelection.thinkingLevel, traceId: getActiveTraceId(), }, diff --git a/packages/junior/tests/unit/prompt.test.ts b/packages/junior/tests/unit/prompt.test.ts index 9f584b5b..1a94513b 100644 --- a/packages/junior/tests/unit/prompt.test.ts +++ b/packages/junior/tests/unit/prompt.test.ts @@ -18,8 +18,7 @@ describe("prompt builders", () => { invocation: null, requester: { userId: "U_ALPHA" }, runtime: { - channelId: "C_ALPHA", - modelId: "model-alpha", + conversationId: "conversation-alpha", thinkingLevel: "medium", traceId: "trace-alpha", }, @@ -41,8 +40,7 @@ describe("prompt builders", () => { invocation: null, requester: { userId: "U_BETA" }, runtime: { - channelId: "C_BETA", - modelId: "model-beta", + conversationId: "conversation-beta", thinkingLevel: "high", }, turnState: "resumed", @@ -56,7 +54,14 @@ 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(firstSystemPrompt).not.toContain("trace-alpha"); expect(buildSystemPrompt()).toBe(firstSystemPrompt); }); From 857b730211740f993e2ff1cb725e08c6e980ffb3 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 23 May 2026 13:12:43 -0700 Subject: [PATCH 6/7] test(chat): Drop unrelated transcript assertions Remove the broader span transcript policy changes from this prompt-context PR. The remaining tests focus on the prompt context invariants changed by the branch. Co-Authored-By: GPT-5 Codex --- .../tests/unit/chat/pi/traced-stream.test.ts | 48 ------------------- specs/logging/tracing-spec.md | 1 - 2 files changed, 49 deletions(-) diff --git a/packages/junior/tests/unit/chat/pi/traced-stream.test.ts b/packages/junior/tests/unit/chat/pi/traced-stream.test.ts index e779b3ee..c49a0c49 100644 --- a/packages/junior/tests/unit/chat/pi/traced-stream.test.ts +++ b/packages/junior/tests/unit/chat/pi/traced-stream.test.ts @@ -116,54 +116,6 @@ describe("createTracedStreamFn", () => { expect(opts.attributes["gen_ai.request.model"]).toBe("openai/gpt-5.4"); }); - it("serializes the actual chat input messages without text normalization", async () => { - const { createTracedStreamFn } = await import("@/chat/pi/traced-stream"); - const stream = createAssistantMessageEventStream(); - const base = vi.fn(() => stream); - - const userText = [ - "", - "prior user message", - "", - "", - "", - "what is sentry?", - "", - ].join("\n"); - - const traced = createTracedStreamFn(base as unknown as StreamFn); - await traced( - fakeModel("openai/gpt-5.4"), - { - systemPrompt: "you are junior", - messages: [ - { - role: "user", - content: [ - { - type: "text", - text: "\nskills and config\n", - }, - { type: "text", text: userText }, - ], - timestamp: 0, - }, - ], - }, - undefined, - ); - - const opts = startInactiveSpan.mock.calls[0]?.[0] as unknown as { - attributes: Record; - }; - const inputMessages = opts.attributes["gen_ai.input.messages"] as string; - expect(inputMessages).toContain("runtime-turn-context"); - expect(inputMessages).toContain("thread-background"); - expect(inputMessages).toContain("current-instruction"); - expect(inputMessages).toContain("prior user message"); - expect(inputMessages).toContain("what is sentry?"); - }); - it("sets output.messages, usage tokens, finish_reasons, response.model after stream completion", async () => { const { createTracedStreamFn } = await import("@/chat/pi/traced-stream"); const stream = createAssistantMessageEventStream(); diff --git a/specs/logging/tracing-spec.md b/specs/logging/tracing-spec.md index b52ef676..93430aef 100644 --- a/specs/logging/tracing-spec.md +++ b/specs/logging/tracing-spec.md @@ -191,7 +191,6 @@ semantic conventions: - A `gen_ai.invoke_agent` span MUST have at least one `gen_ai.chat` child covering the LLM call(s) issued during its agent loop. - A `gen_ai.chat` span MAY appear at the top level (as a sibling of `gen_ai.invoke_agent`, or under a non-`gen_ai.*` parent such as `chat.route_thinking`) only when it represents an LLM call that is independent of an agent loop, for example a routing or classification pre-flight. - Every `gen_ai.chat` span MUST carry `gen_ai.input.messages` and `gen_ai.output.messages`. -- `gen_ai.input.messages` / `gen_ai.output.messages` MUST represent the actual model transcript for that call; do not rewrite or clean message text for display-only concerns. Apply only the shared safety serialization limits for unsafe blobs, length, depth, or circular values. - The parent `gen_ai.invoke_agent` MAY also carry `gen_ai.input.messages` / `gen_ai.output.messages` as a high-level rollup; this is optional. - A `gen_ai.chat` span MUST have its status set to error (code 2) when the underlying LLM call fails — either because `streamFn` itself throws or because the returned stream rejects. - The per-iteration `gen_ai.chat` child span is created in `packages/junior/src/chat/pi/traced-stream.ts` via the `streamFn` injected into `pi-agent-core`'s `Agent`. This applies to both the main agent and the advisor agent. From b287b42660ef0d826642f25f3002a9e0f31b3c3c Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sat, 23 May 2026 13:41:57 -0700 Subject: [PATCH 7/7] fix(chat): Omit empty prompt context blocks Skip empty capabilities, context, and runtime sections in the turn prompt. Keep runtime context limited to the conversation and trace identifiers that help the agent correlate its own turn with telemetry. Co-Authored-By: GPT-5 Codex --- packages/junior/src/chat/prompt.ts | 75 +++++++++++++---------- packages/junior/src/chat/respond.ts | 1 - packages/junior/tests/unit/prompt.test.ts | 7 ++- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/junior/src/chat/prompt.ts b/packages/junior/src/chat/prompt.ts index fb0adc41..562bd4ee 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 = [""]; @@ -484,21 +482,19 @@ function buildIdentitySection(): string { function buildRuntimeSection(params: { conversationId?: string; - thinkingLevel?: string; traceId?: string; -}): string { +}): string | null { const lines = [ - `- version: ${escapeXml(getRuntimeMetadata().version ?? "unknown")}`, params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "", params.traceId ? `- trace_id: ${escapeXml(params.traceId)}` : "", - params.thinkingLevel - ? `- thinking: ${escapeXml(params.thinkingLevel)}` - : "", - `- sandbox_workspace: ${escapeXml(SANDBOX_WORKSPACE_ROOT)}`, ].filter(Boolean); + if (lines.length === 0) { + return null; + } + return renderTagBlock("runtime", lines.join("\n")); } @@ -508,7 +504,7 @@ function buildContextSection(params: { configuration?: Record; invocation: SkillInvocation | null; turnState?: "fresh" | "resumed"; -}): string { +}): string | null { const blocks: string[][] = []; if (JUNIOR_WORLD) { @@ -566,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); } @@ -575,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, @@ -599,6 +607,10 @@ function buildCapabilitiesSection(params: { blocks.push(renderTagBlock("providers", providerCatalog)); } + if (blocks.length === 0) { + return null; + } + return renderTagBlock("capabilities", blocks.join("\n\n")); } @@ -609,7 +621,6 @@ type TurnContextPromptInput = { toolGuidance?: ToolPromptContext[]; runtime?: { conversationId?: string; - thinkingLevel?: string; traceId?: string; }; invocation: SkillInvocation | null; @@ -669,7 +680,7 @@ export function buildTurnContextPrompt(params: TurnContextPromptInput): string { }), buildRuntimeSection(params.runtime ?? {}), ``, - ]; + ].filter((section): section is string => Boolean(section)); return sections.join("\n\n"); } diff --git a/packages/junior/src/chat/respond.ts b/packages/junior/src/chat/respond.ts index 770906e5..d9415274 100644 --- a/packages/junior/src/chat/respond.ts +++ b/packages/junior/src/chat/respond.ts @@ -767,7 +767,6 @@ export async function generateAssistantReply( toolGuidance, runtime: { conversationId: spanContext.conversationId, - thinkingLevel: thinkingSelection.thinkingLevel, traceId: getActiveTraceId(), }, invocation: skillInvocation, diff --git a/packages/junior/tests/unit/prompt.test.ts b/packages/junior/tests/unit/prompt.test.ts index 1a94513b..d32e3655 100644 --- a/packages/junior/tests/unit/prompt.test.ts +++ b/packages/junior/tests/unit/prompt.test.ts @@ -19,7 +19,6 @@ describe("prompt builders", () => { requester: { userId: "U_ALPHA" }, runtime: { conversationId: "conversation-alpha", - thinkingLevel: "medium", traceId: "trace-alpha", }, turnState: "fresh", @@ -41,7 +40,6 @@ describe("prompt builders", () => { requester: { userId: "U_BETA" }, runtime: { conversationId: "conversation-beta", - thinkingLevel: "high", }, turnState: "resumed", }); @@ -62,6 +60,8 @@ describe("prompt builders", () => { 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); }); @@ -76,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", () => {