Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
}
},
"devDependencies": {
"@biomejs/biome": "2.4.12",
"@biomejs/biome": "2.5.6",
"@changesets/cli": "^2.31.0",
"@types/node": "^25.9.3",
"tsx": "^4.21.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/cea/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"devDependencies": {
"@ai-sdk/provider": "^4.0.0",
"@ai-sdk/provider-utils": "^4.0.23",
"@biomejs/biome": "2.4.12",
"@biomejs/biome": "2.5.6",
"@types/node": "^25.9.3",
"tsx": "^4.21.1",
"typescript": "^6.0.3",
Expand Down
5 changes: 2 additions & 3 deletions packages/cea/src/entrypoints/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,7 @@ const collectReadCallPathMap = (
for (const part of content) {
const record = toRecord(part);
if (
!record ||
record.type !== "tool-call" ||
record?.type !== "tool-call" ||
record.toolName !== "read_file" ||
typeof record.toolCallId !== "string"
) {
Expand All @@ -233,7 +232,7 @@ const trackReadResultPart = (
readCallPaths: Map<string, string>
): void => {
const record = toRecord(part);
if (!record || record.type !== "tool-result") {
if (record?.type !== "tool-result") {
return;
}

Expand Down
16 changes: 11 additions & 5 deletions packages/cea/src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import type { LanguageModelV3Middleware } from "@ai-sdk/provider";
import type { LanguageModelV4Middleware } from "@ai-sdk/provider";
import {
hermesToolMiddleware,
morphXmlToolMiddleware,
qwen3CoderToolMiddleware,
} from "@ai-sdk-tool/parser";
import type { LanguageModelMiddleware } from "ai";
import type { ToolFallbackMode } from "../tool-fallback-mode";
import { trimLeadingNewlinesMiddleware as trimMiddleware } from "./trim-leading-newlines";

export interface MiddlewareOptions {
toolFallbackMode: ToolFallbackMode;
}

/**
* Parser/tool middlewares are LanguageModelV4Middleware; ai@6 wrapLanguageModel
* still types its parameter as LanguageModelV3Middleware. Runtime only uses the
* hook functions, so we cast at the boundary.
*/
const TOOL_FALLBACK_MIDDLEWARES: Readonly<
Record<Exclude<ToolFallbackMode, "disable">, LanguageModelV3Middleware>
Record<Exclude<ToolFallbackMode, "disable">, LanguageModelV4Middleware>
> = {
morphxml: morphXmlToolMiddleware,
hermes: hermesToolMiddleware,
Expand All @@ -21,12 +27,12 @@ const TOOL_FALLBACK_MIDDLEWARES: Readonly<

export function buildMiddlewares(
options: MiddlewareOptions
): LanguageModelV3Middleware[] {
const middlewares: LanguageModelV3Middleware[] = [trimMiddleware];
): LanguageModelMiddleware[] {
const middlewares: LanguageModelV4Middleware[] = [trimMiddleware];

if (options.toolFallbackMode !== "disable") {
middlewares.push(TOOL_FALLBACK_MIDDLEWARES[options.toolFallbackMode]);
}

return middlewares;
return middlewares as unknown as LanguageModelMiddleware[];
}
18 changes: 9 additions & 9 deletions packages/cea/src/middleware/trim-leading-newlines.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type {
LanguageModelV3Content,
LanguageModelV3Middleware,
LanguageModelV3StreamPart,
LanguageModelV4Content,
LanguageModelV4Middleware,
LanguageModelV4StreamPart,
} from "@ai-sdk/provider";

const LEADING_NEWLINES = /^\n+/;

function trimContentLeadingNewlines(
content: LanguageModelV3Content[]
): LanguageModelV3Content[] {
content: LanguageModelV4Content[]
): LanguageModelV4Content[] {
if (content.length === 0) {
return content;
}
Expand All @@ -24,8 +24,8 @@ function trimContentLeadingNewlines(
return content;
}

export const trimLeadingNewlinesMiddleware: LanguageModelV3Middleware = {
specificationVersion: "v3",
export const trimLeadingNewlinesMiddleware: LanguageModelV4Middleware = {
specificationVersion: "v4",
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate();
return {
Expand All @@ -40,8 +40,8 @@ export const trimLeadingNewlinesMiddleware: LanguageModelV3Middleware = {
let hasTrimmed = false;

const transformStream = new TransformStream<
LanguageModelV3StreamPart,
LanguageModelV3StreamPart
LanguageModelV4StreamPart,
LanguageModelV4StreamPart
>({
transform(chunk, controller) {
if (chunk.type === "text-delta" && !hasTrimmed) {
Expand Down
5 changes: 2 additions & 3 deletions packages/harness/src/checkpoint-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1472,8 +1472,7 @@ export class CheckpointHistory {
message: CheckpointMessage | undefined
): boolean {
if (
!message ||
message.message.role !== "assistant" ||
message?.message.role !== "assistant" ||
typeof message.message.content !== "string"
) {
return false;
Expand Down Expand Up @@ -2594,7 +2593,7 @@ export class CheckpointHistory {
if (current && hasToolCalls(current.message)) {
const nextIndex = index + 1;
const next = messages[nextIndex];
if (!next || next.message.role !== "tool") {
if (next?.message.role !== "tool") {
messages.splice(index, 1);
continue;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/harness/src/tool-pruning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,11 @@ describe("pruneToolOutputs", () => {
const nextMessage = result.messages[i + 1]?.message;
expect(nextMessage?.role).toBe("tool");
expect(Array.isArray(nextMessage?.content)).toBe(true);
if (!(nextMessage && Array.isArray(nextMessage.content))) {
continue;
}

const matchingResult = (nextMessage?.content as any[]).find(
const matchingResult = (nextMessage.content as any[]).find(
(part) =>
part.type === "tool-result" &&
part.toolCallId === toolCall.toolCallId
Expand Down
2 changes: 1 addition & 1 deletion packages/minimal-agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ try {
},
preprocessCommand: async (input, hooks) => {
const parsed = parseCommand(input);
if (!parsed || parsed.name !== "reasoning" || parsed.args.length > 0) {
if (parsed?.name !== "reasoning" || parsed.args.length > 0) {
return input;
}
const selected = await showReasoningSelector(hooks);
Expand Down
15 changes: 7 additions & 8 deletions packages/tui/src/stream-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,14 +421,13 @@ const FLAGS_ALL_OFF: PiTuiRenderFlags = {
describe("isVisibleStreamPart — reasoning parts must never trigger first-visible", () => {
// Regression: if reasoning parts become visible, clearStreamingLoader fires
// on reasoning-start and the "Thinking..." spinner label is lost.
it.each([
"reasoning-start",
"reasoning-delta",
"reasoning-end",
] as const)("%s is invisible regardless of flags", (type) => {
expect(isVisibleStreamPart({ type } as never, FLAGS_ALL_ON)).toBe(false);
expect(isVisibleStreamPart({ type } as never, FLAGS_ALL_OFF)).toBe(false);
});
it.each(["reasoning-start", "reasoning-delta", "reasoning-end"] as const)(
"%s is invisible regardless of flags",
(type) => {
expect(isVisibleStreamPart({ type } as never, FLAGS_ALL_ON)).toBe(false);
expect(isVisibleStreamPart({ type } as never, FLAGS_ALL_OFF)).toBe(false);
}
);

it("tool-input-end is always invisible", () => {
expect(
Expand Down
Loading