mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 11:50:45 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
365
openclaw/extensions/kimi-coding/stream.test.ts
Normal file
365
openclaw/extensions/kimi-coding/stream.test.ts
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { Context, Model } from "@mariozechner/pi-ai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createKimiThinkingWrapper,
|
||||
createKimiToolCallMarkupWrapper,
|
||||
resolveKimiThinkingType,
|
||||
wrapKimiProviderStream,
|
||||
} from "./stream.js";
|
||||
|
||||
type FakeStream = {
|
||||
result: () => Promise<unknown>;
|
||||
[Symbol.asyncIterator]: () => AsyncIterator<unknown>;
|
||||
};
|
||||
|
||||
function createFakeStream(params: { events: unknown[]; resultMessage: unknown }): FakeStream {
|
||||
return {
|
||||
async result() {
|
||||
return params.resultMessage;
|
||||
},
|
||||
[Symbol.asyncIterator]() {
|
||||
return (async function* () {
|
||||
for (const event of params.events) {
|
||||
yield event;
|
||||
}
|
||||
})();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const KIMI_TOOL_TEXT =
|
||||
' <|tool_calls_section_begin|> <|tool_call_begin|> functions.read:0 <|tool_call_argument_begin|> {"file_path":"./package.json"} <|tool_call_end|> <|tool_calls_section_end|>';
|
||||
const KIMI_MULTI_TOOL_TEXT =
|
||||
' <|tool_calls_section_begin|> <|tool_call_begin|> functions.read:0 <|tool_call_argument_begin|> {"file_path":"./package.json"} <|tool_call_end|> <|tool_call_begin|> functions.write:1 <|tool_call_argument_begin|> {"file_path":"./out.txt","content":"done"} <|tool_call_end|> <|tool_calls_section_end|>';
|
||||
|
||||
describe("kimi tool-call markup wrapper", () => {
|
||||
it("defaults Kimi thinking to disabled unless explicitly enabled", () => {
|
||||
expect(resolveKimiThinkingType({ configuredThinking: undefined })).toBe("disabled");
|
||||
expect(resolveKimiThinkingType({ configuredThinking: undefined, thinkingLevel: "high" })).toBe(
|
||||
"enabled",
|
||||
);
|
||||
expect(resolveKimiThinkingType({ configuredThinking: "off", thinkingLevel: "high" })).toBe(
|
||||
"disabled",
|
||||
);
|
||||
expect(resolveKimiThinkingType({ configuredThinking: "enabled", thinkingLevel: "off" })).toBe(
|
||||
"enabled",
|
||||
);
|
||||
});
|
||||
|
||||
it("converts tagged Kimi tool-call text into structured tool calls", async () => {
|
||||
const partial = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: KIMI_TOOL_TEXT }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
const message = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: KIMI_TOOL_TEXT }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
const finalMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "Need to read the file first." },
|
||||
{ type: "text", text: KIMI_TOOL_TEXT },
|
||||
],
|
||||
stopReason: "stop",
|
||||
};
|
||||
|
||||
const baseStreamFn: StreamFn = () =>
|
||||
createFakeStream({
|
||||
events: [{ type: "message_end", partial, message }],
|
||||
resultMessage: finalMessage,
|
||||
}) as ReturnType<StreamFn>;
|
||||
|
||||
const wrapped = createKimiToolCallMarkupWrapper(baseStreamFn);
|
||||
const stream = wrapped(
|
||||
{ api: "anthropic-messages", provider: "kimi", id: "k2p5" } as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
) as FakeStream;
|
||||
|
||||
const events: unknown[] = [];
|
||||
for await (const event of stream) {
|
||||
events.push(event);
|
||||
}
|
||||
const result = (await stream.result()) as {
|
||||
content: unknown[];
|
||||
stopReason: string;
|
||||
};
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "message_end",
|
||||
partial: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "functions.read:0",
|
||||
name: "functions.read",
|
||||
arguments: { file_path: "./package.json" },
|
||||
},
|
||||
],
|
||||
stopReason: "toolUse",
|
||||
},
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "functions.read:0",
|
||||
name: "functions.read",
|
||||
arguments: { file_path: "./package.json" },
|
||||
},
|
||||
],
|
||||
stopReason: "toolUse",
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "Need to read the file first." },
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "functions.read:0",
|
||||
name: "functions.read",
|
||||
arguments: { file_path: "./package.json" },
|
||||
},
|
||||
],
|
||||
stopReason: "toolUse",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves normal assistant text unchanged", async () => {
|
||||
const finalMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "normal response" }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
const baseStreamFn: StreamFn = () =>
|
||||
createFakeStream({
|
||||
events: [],
|
||||
resultMessage: finalMessage,
|
||||
}) as ReturnType<StreamFn>;
|
||||
|
||||
const wrapped = createKimiToolCallMarkupWrapper(baseStreamFn);
|
||||
const stream = wrapped(
|
||||
{ api: "anthropic-messages", provider: "kimi", id: "k2p5" } as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
) as FakeStream;
|
||||
|
||||
await expect(stream.result()).resolves.toBe(finalMessage);
|
||||
});
|
||||
|
||||
it("supports async stream functions", async () => {
|
||||
const finalMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: KIMI_TOOL_TEXT }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
const baseStreamFn: StreamFn = async () =>
|
||||
createFakeStream({
|
||||
events: [],
|
||||
resultMessage: finalMessage,
|
||||
}) as ReturnType<StreamFn>;
|
||||
|
||||
const wrapped = createKimiToolCallMarkupWrapper(baseStreamFn);
|
||||
const stream = (await wrapped(
|
||||
{ api: "anthropic-messages", provider: "kimi", id: "k2p5" } as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
)) as FakeStream;
|
||||
|
||||
await expect(stream.result()).resolves.toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "functions.read:0",
|
||||
name: "functions.read",
|
||||
arguments: { file_path: "./package.json" },
|
||||
},
|
||||
],
|
||||
stopReason: "toolUse",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses multiple tagged tool calls in one section", async () => {
|
||||
const finalMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: KIMI_MULTI_TOOL_TEXT }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
const baseStreamFn: StreamFn = () =>
|
||||
createFakeStream({
|
||||
events: [],
|
||||
resultMessage: finalMessage,
|
||||
}) as ReturnType<StreamFn>;
|
||||
|
||||
const wrapped = createKimiToolCallMarkupWrapper(baseStreamFn);
|
||||
const stream = wrapped(
|
||||
{ api: "anthropic-messages", provider: "kimi", id: "k2p5" } as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
) as FakeStream;
|
||||
|
||||
await expect(stream.result()).resolves.toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "functions.read:0",
|
||||
name: "functions.read",
|
||||
arguments: { file_path: "./package.json" },
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "functions.write:1",
|
||||
name: "functions.write",
|
||||
arguments: { file_path: "./out.txt", content: "done" },
|
||||
},
|
||||
],
|
||||
stopReason: "toolUse",
|
||||
});
|
||||
});
|
||||
|
||||
it("adapts provider stream context without changing wrapper behavior", async () => {
|
||||
const finalMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: KIMI_TOOL_TEXT }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
const baseStreamFn: StreamFn = () =>
|
||||
createFakeStream({
|
||||
events: [],
|
||||
resultMessage: finalMessage,
|
||||
}) as ReturnType<StreamFn>;
|
||||
|
||||
const wrapped = wrapKimiProviderStream({
|
||||
streamFn: baseStreamFn,
|
||||
} as never);
|
||||
const stream = wrapped(
|
||||
{ api: "anthropic-messages", provider: "kimi", id: "k2p5" } as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
) as FakeStream;
|
||||
|
||||
await expect(stream.result()).resolves.toEqual({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "functions.read:0",
|
||||
name: "functions.read",
|
||||
arguments: { file_path: "./package.json" },
|
||||
},
|
||||
],
|
||||
stopReason: "toolUse",
|
||||
});
|
||||
});
|
||||
|
||||
it("forces Kimi thinking disabled and strips proxy reasoning fields", () => {
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
const baseStreamFn: StreamFn = (model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
reasoning: { effort: "high" },
|
||||
reasoning_effort: "high",
|
||||
reasoningEffort: "high",
|
||||
};
|
||||
options?.onPayload?.(payload as never, model as never);
|
||||
capturedPayload = payload;
|
||||
return createFakeStream({
|
||||
events: [],
|
||||
resultMessage: { role: "assistant", content: [] },
|
||||
}) as never;
|
||||
};
|
||||
|
||||
const wrapped = createKimiThinkingWrapper(baseStreamFn, "disabled");
|
||||
void wrapped(
|
||||
{
|
||||
api: "anthropic-messages",
|
||||
provider: "kimi",
|
||||
id: "kimi-code",
|
||||
} as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(capturedPayload).toEqual({
|
||||
thinking: { type: "disabled" },
|
||||
});
|
||||
});
|
||||
|
||||
it("lets explicit model params keep Kimi thinking disabled even when session thinking is on", () => {
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
const baseStreamFn: StreamFn = (model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {};
|
||||
options?.onPayload?.(payload as never, model as never);
|
||||
capturedPayload = payload;
|
||||
return createFakeStream({
|
||||
events: [],
|
||||
resultMessage: { role: "assistant", content: [] },
|
||||
}) as never;
|
||||
};
|
||||
|
||||
const wrapped = wrapKimiProviderStream({
|
||||
provider: "kimi",
|
||||
modelId: "kimi-code",
|
||||
extraParams: { thinking: "off" },
|
||||
thinkingLevel: "high",
|
||||
streamFn: baseStreamFn,
|
||||
} as never);
|
||||
|
||||
void wrapped(
|
||||
{
|
||||
api: "anthropic-messages",
|
||||
provider: "kimi",
|
||||
id: "kimi-code",
|
||||
} as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(capturedPayload).toEqual({
|
||||
thinking: { type: "disabled" },
|
||||
});
|
||||
});
|
||||
|
||||
it("enables Kimi thinking only when explicitly requested", () => {
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
const baseStreamFn: StreamFn = (model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {};
|
||||
options?.onPayload?.(payload as never, model as never);
|
||||
capturedPayload = payload;
|
||||
return createFakeStream({
|
||||
events: [],
|
||||
resultMessage: { role: "assistant", content: [] },
|
||||
}) as never;
|
||||
};
|
||||
|
||||
const wrapped = wrapKimiProviderStream({
|
||||
provider: "kimi",
|
||||
modelId: "kimi-code",
|
||||
thinkingLevel: "high",
|
||||
streamFn: baseStreamFn,
|
||||
} as never);
|
||||
|
||||
void wrapped(
|
||||
{
|
||||
api: "anthropic-messages",
|
||||
provider: "kimi",
|
||||
id: "kimi-code",
|
||||
} as Model<"anthropic-messages">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(capturedPayload).toEqual({
|
||||
thinking: { type: "enabled" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue