Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/__tests__/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,26 @@ describe("resolveRequestContext", () => {
expect(resolved.tools.map((t) => t.name)).toEqual(["bash"]);
});

it("keeps flat Context tools when the host exports the toolkit but passes a flat Context (omp 18.12.x)", () => {
// omp 18.12.3 exports the whole transcript toolkit to legacy extensions yet
// still calls `stream` with `systemPrompt` / `tools` unfolded. Replaying that
// transcript finds no system message, so the resolver must use the flat
// fields instead of shipping `tools: []` (the model then invents DSML text
// markup and every turn ends empty).
const resolved = resolveRequestContext(PiAi, flatContext);
expect(resolved.systemText).toBe("legacy prompt");
expect(resolved.tools.map((t) => t.name)).toEqual(["bash"]);
expect(resolved.messages).toBe(flatContext.messages);
});

it("keeps the flat prompt when a flat Context carries no tools", () => {
const noTools = { ...flatContext, tools: [] } as unknown as Context;
const resolved = resolveRequestContext(PiAi, noTools);
expect(resolved.systemText).toBe("legacy prompt");
expect(resolved.tools).toEqual([]);
expect(resolved.messages).toBe(noTools.messages);
});

it("resolves tools + prompt from the transcript when helpers are present (pi-ai >=0.86)", () => {
const transcript = PiAi.normalizeContext({
systemPrompt: "you are helpful",
Expand Down
30 changes: 24 additions & 6 deletions src/protocol/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,17 @@ type TranscriptHelpers = Partial<
* message, because Qoder ignores the top-level `system` field).
* - pi-ai <=0.85 hands providers a flat `Context` with top-level
* `systemPrompt` / `tools` and no transcript helpers.
* - Some >=0.86 hosts hand providers a flat `Context` anyway while still
* exporting the transcript toolkit to legacy extensions (omp 18.12.x does:
* its compat root exports the helpers, but `streamQoder` receives
* `systemPrompt` + `tools` unfolded). The leading system message is what
* marks a folded transcript, so its absence means the flat fields still
* carry the prompt and tools.
*
* The helpers are passed in (as the pi-ai namespace) and feature-detected, so a
* single bundle runs on both: on 0.85 the namespace simply lacks them and we use
* the flat fields. Using a namespace import for this is deliberate — a named
* import of a 0.86-only export would throw at module-load time on 0.85.
* single bundle runs on all three: on 0.85 the namespace simply lacks them and
* we use the flat fields. Using a namespace import for this is deliberate — a
* named import of a 0.86-only export would throw at module-load time on 0.85.
*/
export function resolveRequestContext(
piAi: TranscriptHelpers,
Expand Down Expand Up @@ -158,10 +164,22 @@ export function resolveRequestContext(
const collapsed = collapseSystemMessages(context as unknown as TranscriptContext);
const transcriptMessages = collapsed.messages;
const initialSystem = getInitialSystemMessage(transcriptMessages);

// The leading system message is the only proof that this host folded
// `systemPrompt` and `tools` into the transcript. Without it the helpers
// replay nothing — `getCurrentTools` reads `toolsAdded` off system messages —
// so a host that exports the toolkit but still passes a flat `Context` would
// otherwise send `tools: []` and lose the tool-call protocol entirely.
if (!initialSystem) {
return {
messages: context.messages,
systemText: contentToText(context.systemPrompt || ""),
tools: context.tools ?? [],
};
}

const tools = getCurrentTools(transcriptMessages);
const systemText = contentToText(
initialSystem && getSystemMessageText ? getSystemMessageText(initialSystem) : context.systemPrompt || "",
);
const systemText = contentToText(getSystemMessageText?.(initialSystem) ?? context.systemPrompt ?? "");
const messages = withoutInitialSystemMessage(transcriptMessages);
return { messages, systemText, tools };
}
Expand Down