chore(deps): upgrade pi-ai and pi-agent-core to 0.52.9

Upgrade @mariozechner/pi-ai and @mariozechner/pi-agent-core from 0.50.3
to 0.52.9 to support latest models (claude-opus-4-6, o3, o3-mini).

Breaking type changes addressed:
- exactOptionalPropertyTypes: use conditional spread or `| undefined`
- TOOL_PROFILES removed: strip all profile references from CLI
- AgentMessage union requires timestamp: cast test fixtures
- AsyncAgent.id → sessionId
- Add explicit callback parameter types for SDK event handlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jiang Bohan 2026-02-09 19:13:38 +08:00
parent 28a14a4beb
commit 5380b146b3
28 changed files with 1095 additions and 327 deletions

View file

@ -48,9 +48,9 @@
"vitest": "^4.0.18"
},
"dependencies": {
"@mariozechner/pi-agent-core": "^0.50.3",
"@mariozechner/pi-ai": "^0.50.3",
"@mariozechner/pi-coding-agent": "^0.50.3",
"@mariozechner/pi-agent-core": "^0.52.9",
"@mariozechner/pi-ai": "^0.52.9",
"@mariozechner/pi-coding-agent": "^0.52.9",
"@mozilla/readability": "^0.6.0",
"@multica/sdk": "workspace:*",
"@nestjs/common": "^11.1.12",

1022
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -21,13 +21,13 @@ import {
} from "../../providers/index.js";
type ChatOptions = {
profile?: string;
provider?: string;
model?: string;
system?: string;
thinking?: string;
cwd?: string;
session?: string;
profile?: string | undefined;
provider?: string | undefined;
model?: string | undefined;
system?: string | undefined;
thinking?: string | undefined;
cwd?: string | undefined;
session?: string | undefined;
help?: boolean;
};

View file

@ -19,8 +19,8 @@ interface CredentialsOptions {
force: boolean;
coreOnly: boolean;
skillsOnly: boolean;
pathOverride?: string;
skillsPathOverride?: string;
pathOverride?: string | undefined;
skillsPathOverride?: string | undefined;
}
function printHelp() {

View file

@ -12,18 +12,17 @@ import type { ToolsConfig } from "../../tools/policy.js";
import { cyan, yellow, dim } from "../colors.js";
type RunOptions = {
profile?: string;
provider?: string;
model?: string;
apiKey?: string;
baseUrl?: string;
system?: string;
thinking?: string;
reasoning?: string;
cwd?: string;
session?: string;
profile?: string | undefined;
provider?: string | undefined;
model?: string | undefined;
apiKey?: string | undefined;
baseUrl?: string | undefined;
system?: string | undefined;
thinking?: string | undefined;
reasoning?: string | undefined;
cwd?: string | undefined;
session?: string | undefined;
debug?: boolean;
toolsProfile?: string;
toolsAllow?: string[];
toolsDeny?: string[];
help?: boolean;
@ -49,7 +48,6 @@ ${cyan("Options:")}
${yellow("--help")}, -h Show this help
${cyan("Tools Configuration:")}
${yellow("--tools-profile")} P Tool profile (minimal, coding, web, full)
${yellow("--tools-allow")} T Allow specific tools (comma-separated)
${yellow("--tools-deny")} T Deny specific tools (comma-separated)
@ -125,10 +123,6 @@ function parseArgs(argv: string[]): { opts: RunOptions; prompt: string } {
opts.debug = true;
continue;
}
if (arg === "--tools-profile") {
opts.toolsProfile = args.shift();
continue;
}
if (arg === "--tools-allow") {
const value = args.shift();
opts.toolsAllow = value?.split(",").map((s) => s.trim()) ?? [];
@ -178,11 +172,8 @@ export async function runCommand(args: string[]): Promise<void> {
// Build tools config if any tools options are set
let toolsConfig: ToolsConfig | undefined;
if (opts.toolsProfile || opts.toolsAllow || opts.toolsDeny) {
if (opts.toolsAllow || opts.toolsDeny) {
toolsConfig = {};
if (opts.toolsProfile) {
toolsConfig.profile = opts.toolsProfile as ToolsConfig["profile"];
}
if (opts.toolsAllow) {
toolsConfig.allow = opts.toolsAllow;
}

View file

@ -147,7 +147,7 @@ function cmdShow(sessionId: string | undefined, showInternal = false) {
process.exit(1);
}
const session = matches[0];
const session = matches[0]!;
const content = readFileSync(session.path, "utf8");
const lines = content.trim().split("\n").filter(Boolean);
@ -235,7 +235,7 @@ function cmdDelete(sessionId: string | undefined) {
process.exit(1);
}
const session = matches[0];
const session = matches[0]!;
try {
unlinkSync(session.path);

View file

@ -29,7 +29,7 @@ interface ParsedArgs {
args: string[];
verbose: boolean;
force: boolean;
profile?: string;
profile?: string | undefined;
}
function printHelp() {

View file

@ -4,22 +4,20 @@
* Usage:
* multica tools list [options] List available tools
* multica tools groups Show all tool groups
* multica tools profiles Show all tool profiles
*/
import { createAllTools } from "../../tools.js";
import { filterTools, type ToolsConfig } from "../../tools/policy.js";
import { TOOL_GROUPS, TOOL_PROFILES, expandToolGroups } from "../../tools/groups.js";
import { TOOL_GROUPS, expandToolGroups } from "../../tools/groups.js";
import { cyan, yellow, green, dim } from "../colors.js";
type Command = "list" | "groups" | "profiles" | "help";
type Command = "list" | "groups" | "help";
interface ToolsOptions {
command: Command;
profile?: string;
allow?: string[];
deny?: string[];
provider?: string;
provider?: string | undefined;
isSubagent?: boolean;
}
@ -30,11 +28,9 @@ ${cyan("Usage:")} multica tools <command> [options]
${cyan("Commands:")}
${yellow("list")} List available tools (with optional filtering)
${yellow("groups")} Show all tool groups
${yellow("profiles")} Show all tool profiles
${yellow("help")} Show this help
${cyan("Options for 'list':")}
${yellow("--profile")} PROFILE Apply profile filter (minimal, coding, web, full)
${yellow("--allow")} TOOLS Allow specific tools (comma-separated)
${yellow("--deny")} TOOLS Deny specific tools (comma-separated)
${yellow("--provider")} NAME Apply provider-specific rules
@ -44,11 +40,8 @@ ${cyan("Examples:")}
${dim("# List all tools")}
multica tools list
${dim("# List tools with profile")}
multica tools list --profile coding
${dim("# List tools with allow/deny")}
multica tools list --profile coding --deny exec
multica tools list --deny exec
multica tools list --allow group:fs,web_fetch
${dim("# Show tool groups")}
@ -58,12 +51,13 @@ ${cyan("Examples:")}
function parseArgs(argv: string[]): ToolsOptions {
const args = [...argv];
const command = (args.shift() || "help") as Command;
const raw = args.shift() || "help";
if (command === "--help" || command === "-h") {
if (raw === "--help" || raw === "-h") {
return { command: "help" };
}
const command = raw as Command;
const opts: ToolsOptions = { command };
while (args.length > 0) {
@ -73,10 +67,6 @@ function parseArgs(argv: string[]): ToolsOptions {
if (arg === "--help" || arg === "-h") {
return { command: "help" };
}
if (arg === "--profile") {
opts.profile = args.shift();
continue;
}
if (arg === "--allow") {
const value = args.shift();
opts.allow = value?.split(",").map((s) => s.trim()) ?? [];
@ -108,11 +98,8 @@ function cmdList(opts: ToolsOptions) {
// Build config
let config: ToolsConfig | undefined;
if (opts.profile || opts.allow || opts.deny) {
if (opts.allow || opts.deny) {
config = {};
if (opts.profile) {
config.profile = opts.profile as ToolsConfig["profile"];
}
if (opts.allow) {
config.allow = opts.allow;
}
@ -136,7 +123,6 @@ function cmdList(opts: ToolsOptions) {
if (config || opts.provider || opts.isSubagent) {
console.log("Applied filters:");
if (opts.profile) console.log(` ${dim("Profile:")} ${yellow(opts.profile)}`);
if (opts.allow) console.log(` ${dim("Allow:")} ${opts.allow.join(", ")}`);
if (opts.deny) console.log(` ${dim("Deny:")} ${opts.deny.join(", ")}`);
if (opts.provider) console.log(` ${dim("Provider:")} ${opts.provider}`);
@ -171,24 +157,6 @@ function cmdGroups() {
}
}
function cmdProfiles() {
console.log(`\n${cyan("Tool Profiles:")}\n`);
for (const [name, policy] of Object.entries(TOOL_PROFILES)) {
console.log(` ${yellow(name)}:`);
if (policy.allow) {
const expanded = expandToolGroups(policy.allow);
console.log(` ${dim("Allow:")} ${policy.allow.join(", ")}`);
console.log(` ${dim("Expands to:")} ${expanded.join(", ")}`);
} else {
console.log(` ${dim("Allow:")} (all tools)`);
}
if (policy.deny) {
console.log(` ${dim("Deny:")} ${policy.deny.join(", ")}`);
}
console.log("");
}
}
export async function toolsCommand(args: string[]): Promise<void> {
const opts = parseArgs(args);
@ -199,9 +167,6 @@ export async function toolsCommand(args: string[]): Promise<void> {
case "groups":
cmdGroups();
break;
case "profiles":
cmdProfiles();
break;
case "help":
default:
printHelp();

View file

@ -156,11 +156,8 @@ async function main() {
// Build tools config if any tools options are set
let toolsConfig: import("../tools/policy.js").ToolsConfig | undefined;
if (opts.toolsProfile || opts.toolsAllow || opts.toolsDeny) {
if (opts.toolsAllow || opts.toolsDeny) {
toolsConfig = {};
if (opts.toolsProfile) {
toolsConfig.profile = opts.toolsProfile as any;
}
if (opts.toolsAllow) {
toolsConfig.allow = opts.toolsAllow;
}

View file

@ -4,24 +4,22 @@
*
* Usage:
* pnpm tools:cli list # List all available tools
* pnpm tools:cli list --profile coding # List tools after applying profile
* pnpm tools:cli list --allow group:fs # List tools after allowing fs group
* pnpm tools:cli list --deny exec # List tools after denying exec
* pnpm tools:cli groups # Show all tool groups
* pnpm tools:cli profiles # Show all profiles
*/
import { createAllTools } from "../tools.js";
import { filterTools, type ToolsConfig } from "../tools/policy.js";
import { TOOL_GROUPS, TOOL_PROFILES, expandToolGroups } from "../tools/groups.js";
import { TOOL_GROUPS, expandToolGroups } from "../tools/groups.js";
type Command = "list" | "groups" | "profiles" | "help";
type Command = "list" | "groups" | "help";
interface CliOptions {
command: Command;
profile?: string;
allow?: string[];
deny?: string[];
provider?: string;
provider?: string | undefined;
isSubagent?: boolean;
}
@ -31,11 +29,9 @@ function printUsage() {
console.log("Commands:");
console.log(" list List available tools (with optional filtering)");
console.log(" groups Show all tool groups");
console.log(" profiles Show all profiles");
console.log(" help Show this help");
console.log("");
console.log("Options for 'list':");
console.log(" --profile PROFILE Apply profile filter (minimal, coding, web, full)");
console.log(" --allow TOOLS Allow specific tools (comma-separated)");
console.log(" --deny TOOLS Deny specific tools (comma-separated)");
console.log(" --provider NAME Apply provider-specific rules");
@ -43,8 +39,7 @@ function printUsage() {
console.log("");
console.log("Examples:");
console.log(" pnpm tools:cli list");
console.log(" pnpm tools:cli list --profile coding");
console.log(" pnpm tools:cli list --profile coding --deny exec");
console.log(" pnpm tools:cli list --deny exec");
console.log(" pnpm tools:cli list --allow group:fs,web_fetch");
console.log(" pnpm tools:cli groups");
}
@ -59,11 +54,6 @@ function parseArgs(argv: string[]): CliOptions {
const arg = args.shift();
if (!arg) break;
if (arg === "--profile") {
const value = args.shift();
if (value) opts.profile = value;
continue;
}
if (arg === "--allow") {
const value = args.shift();
opts.allow = value?.split(",").map((s) => s.trim()) ?? [];
@ -75,8 +65,7 @@ function parseArgs(argv: string[]): CliOptions {
continue;
}
if (arg === "--provider") {
const value = args.shift();
if (value) opts.provider = value;
opts.provider = args.shift();
continue;
}
if (arg === "--subagent") {
@ -96,11 +85,8 @@ function listTools(opts: CliOptions) {
// Build config
let config: ToolsConfig | undefined;
if (opts.profile || opts.allow || opts.deny) {
if (opts.allow || opts.deny) {
config = {};
if (opts.profile) {
config.profile = opts.profile as any;
}
if (opts.allow) {
config.allow = opts.allow;
}
@ -124,7 +110,6 @@ function listTools(opts: CliOptions) {
if (config || opts.provider || opts.isSubagent) {
console.log("Applied filters:");
if (opts.profile) console.log(` Profile: ${opts.profile}`);
if (opts.allow) console.log(` Allow: ${opts.allow.join(", ")}`);
if (opts.deny) console.log(` Deny: ${opts.deny.join(", ")}`);
if (opts.provider) console.log(` Provider: ${opts.provider}`);
@ -160,25 +145,6 @@ function showGroups() {
}
}
function showProfiles() {
console.log("Tool Profiles:");
console.log("");
for (const [name, policy] of Object.entries(TOOL_PROFILES)) {
console.log(` ${name}:`);
if (policy.allow) {
const expanded = expandToolGroups(policy.allow);
console.log(` Allow: ${policy.allow.join(", ")}`);
console.log(` Expands to: ${expanded.join(", ")}`);
} else {
console.log(` Allow: (all tools)`);
}
if (policy.deny) {
console.log(` Deny: ${policy.deny.join(", ")}`);
}
console.log("");
}
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
@ -189,9 +155,6 @@ async function main() {
case "groups":
showGroups();
break;
case "profiles":
showProfiles();
break;
case "help":
default:
printUsage();

View file

@ -82,10 +82,10 @@ describe("token-estimation", () => {
describe("estimateTokenUsage", () => {
it("should calculate token usage correctly", () => {
const messages: AgentMessage[] = [
const messages = [
{ role: "user", content: "Hello world" }, // ~3 tokens
{ role: "assistant", content: "Hi there!" }, // ~3 tokens
];
] as AgentMessage[];
const result = estimateTokenUsage({
messages,
@ -130,9 +130,9 @@ describe("token-estimation", () => {
});
it("should calculate utilization ratio with safety margin", () => {
const messages: AgentMessage[] = [
const messages = [
{ role: "user", content: "a".repeat(400) }, // ~100 tokens
];
] as AgentMessage[];
const result = estimateTokenUsage({
messages,
@ -184,7 +184,7 @@ describe("token-estimation", () => {
return Array.from({ length: count }, (_, i) => ({
role: "user" as const,
content: `Message ${i}: ${"x".repeat(100)}`, // Each ~28 tokens
}));
})) as AgentMessage[];
}
it("should return null if too few messages", () => {
@ -228,7 +228,7 @@ describe("token-estimation", () => {
});
it("should keep newest messages (from the end)", () => {
const messages: AgentMessage[] = [
const messages = [
{ role: "user", content: "Old message 1" },
{ role: "user", content: "Old message 2" },
{ role: "user", content: "Old message 3" },
@ -242,7 +242,7 @@ describe("token-estimation", () => {
{ role: "user", content: "Old message 11" },
{ role: "user", content: "Newer message 12" },
{ role: "user", content: "Newest message 13" },
];
] as AgentMessage[];
const result = compactMessagesTokenAware(messages, 50, {
targetRatio: 0.5,
@ -268,29 +268,29 @@ describe("token-estimation", () => {
describe("isMessageOversized", () => {
it("should return true for oversized message", () => {
const message: AgentMessage = {
const message = {
role: "user",
content: "x".repeat(4000), // ~1000 tokens
};
} as AgentMessage;
// With default maxRatio 0.5, 1000 tokens in 1000 context = 100% > 50%
expect(isMessageOversized(message, 1000)).toBe(true);
});
it("should return false for small message", () => {
const message: AgentMessage = {
const message = {
role: "user",
content: "Hello", // ~2 tokens
};
} as AgentMessage;
expect(isMessageOversized(message, 10000)).toBe(false);
});
it("should use custom maxRatio", () => {
const message: AgentMessage = {
const message = {
role: "user",
content: "x".repeat(400), // ~100 tokens
};
} as AgentMessage;
// With safety margin 1.2, 100 * 1.2 = 120 tokens
// 120 > 1000 * 0.1 = 100, so oversized
@ -301,10 +301,10 @@ describe("token-estimation", () => {
});
it("should apply safety margin to token count", () => {
const message: AgentMessage = {
const message = {
role: "user",
content: "x".repeat(400), // ~100 tokens, with margin ~120
};
} as AgentMessage;
// Without margin: 100 < 250 (50% of 500)
// With margin: 120 < 250, still ok

View file

@ -50,8 +50,8 @@ const PROVIDER_REGISTRY: Record<string, ProviderMeta> = {
id: "claude-code",
name: "Claude Code (OAuth)",
authMethod: "oauth",
defaultModel: "claude-opus-4-5",
models: ["claude-opus-4-5", "claude-opus-4-1", "claude-sonnet-4-5", "claude-sonnet-4-0", "claude-haiku-4-5"],
defaultModel: "claude-opus-4-6",
models: ["claude-opus-4-6", "claude-opus-4-5", "claude-sonnet-4-5", "claude-sonnet-4-0", "claude-haiku-4-5"],
loginCommand: "claude login",
},
"openai-codex": {
@ -67,7 +67,7 @@ const PROVIDER_REGISTRY: Record<string, ProviderMeta> = {
name: "Anthropic (API Key)",
authMethod: "api-key",
defaultModel: "claude-sonnet-4-5",
models: ["claude-opus-4-5", "claude-opus-4-1", "claude-sonnet-4-5", "claude-sonnet-4-0", "claude-haiku-4-5"],
models: ["claude-opus-4-6", "claude-opus-4-5", "claude-sonnet-4-5", "claude-sonnet-4-0", "claude-haiku-4-5"],
loginUrl: "https://console.anthropic.com/",
},
"openai": {
@ -75,7 +75,7 @@ const PROVIDER_REGISTRY: Record<string, ProviderMeta> = {
name: "OpenAI",
authMethod: "api-key",
defaultModel: "gpt-4o",
models: ["gpt-5.2", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini"],
models: ["gpt-5.2", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini"],
loginUrl: "https://platform.openai.com/api-keys",
},
"kimi-coding": {

View file

@ -66,7 +66,7 @@ describe("compaction", () => {
return Array.from({ length: count }, (_, i) => ({
role: (i % 2 === 0 ? "user" : "assistant") as "user" | "assistant",
content: `${prefix} ${i}`,
}));
})) as AgentMessage[];
}
function createMessagesWithToolUse(): AgentMessage[] {

View file

@ -28,13 +28,13 @@ describe("SessionManager display content view", () => {
const entries: SessionEntry[] = [
{
type: "message",
message: { role: "user", content: "[Mon 2026-02-09 14:37 GMT+8] hi" },
message: { role: "user", content: "[Mon 2026-02-09 14:37 GMT+8] hi" } as any,
displayContent: "hi",
timestamp: 1,
},
{
type: "message",
message: { role: "assistant", content: "hello there" },
message: { role: "assistant", content: "hello there" } as any,
timestamp: 2,
},
];
@ -43,9 +43,9 @@ describe("SessionManager display content view", () => {
const raw = session.loadMessages();
const display = session.loadMessagesForDisplay();
expect(raw[0]?.content).toBe("[Mon 2026-02-09 14:37 GMT+8] hi");
expect(display[0]?.content).toBe("hi");
expect(display[1]?.content).toBe("hello there");
expect((raw[0] as any)?.content).toBe("[Mon 2026-02-09 14:37 GMT+8] hi");
expect((display[0] as any)?.content).toBe("hi");
expect((display[1] as any)?.content).toBe("hello there");
});
it("keeps internal filtering behavior in display view", async () => {
@ -54,14 +54,14 @@ describe("SessionManager display content view", () => {
const entries: SessionEntry[] = [
{
type: "message",
message: { role: "user", content: "[Mon 2026-02-09 14:37 GMT+8] hidden" },
message: { role: "user", content: "[Mon 2026-02-09 14:37 GMT+8] hidden" } as any,
displayContent: "hidden",
internal: true,
timestamp: 1,
},
{
type: "message",
message: { role: "user", content: "[Mon 2026-02-09 14:38 GMT+8] visible" },
message: { role: "user", content: "[Mon 2026-02-09 14:38 GMT+8] visible" } as any,
displayContent: "visible",
timestamp: 2,
},
@ -72,9 +72,9 @@ describe("SessionManager display content view", () => {
const includeInternalView = session.loadMessagesForDisplay({ includeInternal: true });
expect(defaultView).toHaveLength(1);
expect(defaultView[0]?.content).toBe("visible");
expect((defaultView[0] as any)?.content).toBe("visible");
expect(includeInternalView).toHaveLength(2);
expect(includeInternalView[0]?.content).toBe("hidden");
expect((includeInternalView[0] as any)?.content).toBe("hidden");
});
it("persists displayContent on saveMessage", async () => {
@ -82,7 +82,7 @@ describe("SessionManager display content view", () => {
const session = new SessionManager({ sessionId, baseDir: testBaseDir });
session.saveMessage(
{ role: "user", content: "[Mon 2026-02-09 14:39 GMT+8] save me" },
{ role: "user", content: "[Mon 2026-02-09 14:39 GMT+8] save me" } as any,
{ displayContent: "save me" },
);
await session.flush();

View file

@ -164,7 +164,7 @@ export class SessionManager {
async repairIfNeeded(warn?: (message: string) => void): Promise<RepairReport> {
const filePath = resolveSessionPath(this.sessionId, { baseDir: this.baseDir });
return repairSessionFileIfNeeded({ sessionFile: filePath, warn });
return repairSessionFileIfNeeded({ sessionFile: filePath, ...(warn !== undefined ? { warn } : {}) });
}
loadMessages(options?: { includeInternal?: boolean }): AgentMessage[] {
@ -274,7 +274,7 @@ export class SessionManager {
const pruneResult = pruneToolResults({
messages: workingMessages,
contextWindowTokens: this.contextWindowTokens,
settings: this.toolResultPruning,
...(this.toolResultPruning !== undefined ? { settings: this.toolResultPruning } : {}),
});
if (pruneResult.changed) {

View file

@ -23,7 +23,7 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: "ok" }],
isError: false,
},
] satisfies AgentMessage[];
] as AgentMessage[];
const out = sanitizeToolUseResultPairing(input);
expect(out[0]?.role).toBe("assistant");
@ -55,7 +55,7 @@ describe("sanitizeToolUseResultPairing", () => {
isError: false,
},
{ role: "user", content: "ok" },
] satisfies AgentMessage[];
] as AgentMessage[];
const out = sanitizeToolUseResultPairing(input);
expect(out.filter((m) => m.role === "toolResult")).toHaveLength(1);
@ -82,7 +82,7 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: "second (duplicate)" }],
isError: false,
},
] satisfies AgentMessage[];
] as AgentMessage[];
const out = sanitizeToolUseResultPairing(input);
const results = out.filter((m) => m.role === "toolResult") as Array<{
@ -106,7 +106,7 @@ describe("sanitizeToolUseResultPairing", () => {
role: "assistant",
content: [{ type: "text", text: "ok" }],
},
] satisfies AgentMessage[];
] as AgentMessage[];
const out = sanitizeToolUseResultPairing(input);
expect(out.some((m) => m.role === "toolResult")).toBe(false);
@ -116,20 +116,20 @@ describe("sanitizeToolUseResultPairing", () => {
describe("sanitizeToolCallInputs", () => {
it("drops tool calls missing input or arguments", () => {
const input: AgentMessage[] = [
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read" }],
},
{ role: "user", content: "hello" },
];
] as AgentMessage[];
const out = sanitizeToolCallInputs(input);
expect(out.map((m) => m.role)).toEqual(["user"]);
});
it("keeps valid tool calls and preserves text blocks", () => {
const input: AgentMessage[] = [
const input = [
{
role: "assistant",
content: [
@ -138,7 +138,7 @@ describe("sanitizeToolCallInputs", () => {
{ type: "toolCall", id: "call_drop", name: "read" },
],
},
];
] as AgentMessage[];
const out = sanitizeToolCallInputs(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;

View file

@ -2,7 +2,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
type ToolCallLike = {
id: string;
name?: string;
name?: string | undefined;
};
const TOOL_CALL_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
@ -72,7 +72,7 @@ function extractToolResultId(msg: Extract<AgentMessage, { role: "toolResult" }>)
function makeMissingToolResult(params: {
toolCallId: string;
toolName?: string;
toolName?: string | undefined;
}): Extract<AgentMessage, { role: "toolResult" }> {
return {
role: "toolResult",
@ -188,7 +188,6 @@ export function repairToolUseResultPairing(messages: AgentMessage[]): ToolUseRep
for (let i = 0; i < messages.length; i += 1) {
const msg = messages[i];
if (!msg || typeof msg !== "object") {
out.push(msg);
continue;
}
@ -219,7 +218,6 @@ export function repairToolUseResultPairing(messages: AgentMessage[]): ToolUseRep
for (; j < messages.length; j += 1) {
const next = messages[j];
if (!next || typeof next !== "object") {
remainder.push(next);
continue;
}

View file

@ -108,12 +108,12 @@ describe("session/storage", () => {
const entry1: SessionEntry = {
type: "message",
message: { role: "user", content: "Hello" },
message: { role: "user", content: "Hello" } as any,
timestamp: 1000,
};
const entry2: SessionEntry = {
type: "message",
message: { role: "assistant", content: "Hi there" },
message: { role: "assistant", content: "Hi there" } as any,
timestamp: 2000,
};
@ -135,7 +135,7 @@ describe("session/storage", () => {
const validEntry: SessionEntry = {
type: "message",
message: { role: "user", content: "Valid" },
message: { role: "user", content: "Valid" } as any,
timestamp: 1000,
};
@ -195,7 +195,7 @@ describe("session/storage", () => {
const sessionId = "append-session";
const entry: SessionEntry = {
type: "message",
message: { role: "user", content: "Hello" },
message: { role: "user", content: "Hello" } as any,
timestamp: 1000,
};
@ -212,12 +212,12 @@ describe("session/storage", () => {
const sessionId = "append-existing";
const entry1: SessionEntry = {
type: "message",
message: { role: "user", content: "First" },
message: { role: "user", content: "First" } as any,
timestamp: 1000,
};
const entry2: SessionEntry = {
type: "message",
message: { role: "assistant", content: "Second" },
message: { role: "assistant", content: "Second" } as any,
timestamp: 2000,
};
@ -235,8 +235,8 @@ describe("session/storage", () => {
it("should write all entries to file", async () => {
const sessionId = "write-session";
const entries: SessionEntry[] = [
{ type: "message", message: { role: "user", content: "One" }, timestamp: 1000 },
{ type: "message", message: { role: "assistant", content: "Two" }, timestamp: 2000 },
{ type: "message", message: { role: "user", content: "One" } as any, timestamp: 1000 },
{ type: "message", message: { role: "assistant", content: "Two" } as any, timestamp: 2000 },
];
await writeEntries(sessionId, entries, { baseDir: testBaseDir });
@ -251,12 +251,12 @@ describe("session/storage", () => {
await writeEntries(
sessionId,
[{ type: "message", message: { role: "user", content: "Old" }, timestamp: 1000 }],
[{ type: "message", message: { role: "user", content: "Old" } as any, timestamp: 1000 }],
{ baseDir: testBaseDir }
);
const newEntries: SessionEntry[] = [
{ type: "message", message: { role: "user", content: "New" }, timestamp: 2000 },
{ type: "message", message: { role: "user", content: "New" } as any, timestamp: 2000 },
];
await writeEntries(sessionId, newEntries, { baseDir: testBaseDir });

View file

@ -33,10 +33,10 @@ export interface CreateToolsOptions {
type ToolErrorPayload = {
error: true;
message: string;
name?: string;
code?: string;
retryable?: boolean;
details?: Record<string, unknown>;
name?: string | undefined;
code?: string | undefined;
retryable?: boolean | undefined;
details?: Record<string, unknown> | undefined;
};
function toToolErrorPayload(error: unknown): ToolErrorPayload {
@ -130,12 +130,12 @@ export function createAllTools(options: CreateToolsOptions | string): AgentTool<
// Add sessions_spawn tool (will be filtered by policy for subagents)
const sessionsSpawnTool = createSessionsSpawnTool({
isSubagent: isSubagent ?? false,
sessionId,
...(sessionId !== undefined ? { sessionId } : {}),
});
tools.push(sessionsSpawnTool as AgentTool<any>);
// Add sessions_list tool
const sessionsListTool = createSessionsListTool({ sessionId });
const sessionsListTool = createSessionsListTool({ ...(sessionId !== undefined ? { sessionId } : {}) });
tools.push(sessionsListTool as AgentTool<any>);
return tools;

View file

@ -27,7 +27,7 @@ export interface ExecApprovalRequest {
/** Shell command to execute */
command: string;
/** Working directory */
cwd?: string;
cwd?: string | undefined;
/** Evaluated risk level */
riskLevel: "safe" | "needs-review" | "dangerous";
/** Reasons for the risk assessment */

View file

@ -23,12 +23,12 @@ type SessionsListArgs = {
export type SessionsListResult = {
runs: Array<{
runId: string;
label?: string;
label?: string | undefined;
task: string;
status: "running" | "ok" | "error" | "timeout" | "unknown";
startedAt?: number;
endedAt?: number;
findings?: string;
startedAt?: number | undefined;
endedAt?: number | undefined;
findings?: string | undefined;
}>;
};

View file

@ -44,7 +44,7 @@ export class AppController {
@Post("agents")
createAgent(@Body() body?: { id?: string }) {
const agent = this.hub.createAgent(body?.id);
return { id: agent.id };
return { id: agent.sessionId };
}
@Delete("agents/:id")

View file

@ -1,11 +1,15 @@
import {
GatewayClient,
HelloAction,
HelloResponseAction,
type HelloPayload,
type HelloResponsePayload,
type ConnectionState,
type RoutedMessage,
type SendErrorResponse,
} from "@multica/sdk";
const HelloAction = "hello";
const HelloResponseAction = "hello:response";
type HelloPayload = { greeting: string };
type HelloResponsePayload = { reply: string };
// 模拟一个 Client
const client = new GatewayClient({
url: "http://localhost:3000",
@ -22,11 +26,11 @@ const agent = new GatewayClient({
// Agent 监听消息
agent
.onStateChange((state) => console.log("[Agent] State:", state))
.onRegistered((deviceId) => {
.onStateChange((state: ConnectionState) => console.log("[Agent] State:", state))
.onRegistered((deviceId: string) => {
console.log("[Agent] Registered as:", deviceId);
})
.onMessage((message) => {
.onMessage((message: RoutedMessage) => {
console.log("[Agent] Received message:", message);
// 回复消息
@ -38,13 +42,13 @@ agent
});
}
})
.onSendError((error) => console.error("[Agent] Send error:", error))
.onSendError((error: SendErrorResponse) => console.error("[Agent] Send error:", error))
.connect();
// Client 监听消息
client
.onStateChange((state) => console.log("[Client] State:", state))
.onRegistered((deviceId) => {
.onStateChange((state: ConnectionState) => console.log("[Client] State:", state))
.onRegistered((deviceId: string) => {
console.log("[Client] Registered as:", deviceId);
// 注册后发送消息给 Agent
@ -55,10 +59,10 @@ client
});
}, 500);
})
.onMessage((message) => {
.onMessage((message: RoutedMessage) => {
console.log("[Client] Received message:", message);
})
.onSendError((error) => console.error("[Client] Send error:", error))
.onSendError((error: SendErrorResponse) => console.error("[Client] Send error:", error))
.connect();
// 5秒后断开

View file

@ -44,9 +44,9 @@ function detectFenceAt(text: string, upTo: number): FenceInfo | null {
for (const line of lines) {
const match = line.match(/^(`{3,}|~{3,})(\S*)\s*$/);
if (!match) continue;
const marker = match[1];
const marker = match[1]!;
const lang = match[2] ?? "";
const markerChar = marker[0];
const markerChar = marker[0]!;
if (openFence === null) {
// Opening a new fence
@ -242,7 +242,7 @@ function findSentenceBreak(buffer: string, start: number, end: number, bufLen: n
const ch = buffer[i];
if (ch === "." || ch === "!" || ch === "?") {
const next = i + 1;
if (next < bufLen && /\s/.test(buffer[next])) {
if (next < bufLen && /\s/.test(buffer[next]!)) {
// Break after the whitespace
const idx = next + 1;
if (idx < bufLen) return idx;
@ -261,7 +261,7 @@ function findSentenceBreak(buffer: string, start: number, end: number, bufLen: n
*/
function findWordBreak(buffer: string, start: number, end: number, bufLen: number): number {
for (let i = end - 1; i >= start; i--) {
if (/\s/.test(buffer[i])) {
if (/\s/.test(buffer[i]!)) {
const idx = i + 1;
if (idx < bufLen) return idx;
}

View file

@ -20,7 +20,7 @@ export interface DeviceEntry {
deviceId: string;
agentId: string;
addedAt: number;
meta?: DeviceMeta;
meta?: DeviceMeta | undefined;
}
// ============ Persistence ============

View file

@ -8,7 +8,7 @@ describe("ExecApprovalManager", () => {
beforeEach(() => {
vi.useFakeTimers();
sendToClient = vi.fn();
manager = new ExecApprovalManager(sendToClient, 5000); // 5s timeout for tests
manager = new ExecApprovalManager(sendToClient as any, 5000); // 5s timeout for tests
});
afterEach(() => {

View file

@ -2,6 +2,8 @@ import { v7 as uuidv7 } from "uuid";
import {
GatewayClient,
type ConnectionState,
type RoutedMessage,
type SendErrorResponse,
RequestAction,
ResponseAction,
StreamAction,
@ -204,22 +206,22 @@ export class Hub {
reconnectDelay: 1000,
});
client.onStateChange((state) => {
client.onStateChange((state: ConnectionState) => {
console.log(`[Hub] Connection state: ${state}`);
for (const listener of this._stateChangeListeners) {
listener(state);
}
});
client.onRegistered((deviceId) => {
client.onRegistered((deviceId: string) => {
console.log(`[Hub] Registered as: ${deviceId}`);
});
client.onError((err) => {
client.onError((err: Error) => {
console.error(`[Hub] Connection error:`, err.message);
});
client.onMessage((msg) => {
client.onMessage((msg: RoutedMessage) => {
console.log(`[Hub] Received message: id=${msg.id} from=${msg.from} to=${msg.to} action=${msg.action} payload=${JSON.stringify(msg.payload)}`);
// RPC request
@ -272,7 +274,7 @@ export class Hub {
}
});
client.onSendError((err) => {
client.onSendError((err: SendErrorResponse) => {
console.error(`[Hub] Send error: messageId=${err.messageId} code=${err.code} error=${err.error}`);
});
@ -592,12 +594,12 @@ export class Hub {
const result = await this.approvalManager.requestApproval({
agentId: sessionId,
command,
cwd,
...(cwd !== undefined ? { cwd } : {}),
riskLevel: evaluation.riskLevel,
riskReasons: evaluation.reasons,
timeoutMs: config.timeoutMs,
askFallback: config.askFallback,
allowlistSatisfied: evaluation.allowlistSatisfied,
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
...(config.askFallback !== undefined ? { askFallback: config.askFallback } : {}),
...(evaluation.allowlistSatisfied !== undefined ? { allowlistSatisfied: evaluation.allowlistSatisfied } : {}),
});
// Handle allow-always: persist to profile allowlist

View file

@ -85,8 +85,8 @@ function smallConfig(overrides?: Partial<BlockChunkerConfig>): BlockChunkerConfi
describe("MessageAggregator", () => {
let blocks: BlockReply[];
let passedThrough: Array<AgentEvent | MulticaEvent>;
let onBlock: ReturnType<typeof vi.fn>;
let onPassthrough: ReturnType<typeof vi.fn>;
let onBlock: any;
let onPassthrough: any;
beforeEach(() => {
blocks = [];
@ -154,8 +154,8 @@ describe("MessageAggregator", () => {
agg.handleEvent(endEvent);
expect(blocks).toHaveLength(1);
expect(blocks[0].text).toBe("Hello world");
expect(blocks[0].isFinal).toBe(true);
expect(blocks[0]!.text).toBe("Hello world");
expect(blocks[0]!.isFinal).toBe(true);
// message_start + message_end both passed through
const passthroughTypes = passedThrough.map((e) => e.type);
@ -178,7 +178,7 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd("Hello world"));
expect(blocks).toHaveLength(1);
expect(blocks[0].text).toBe("Hello world");
expect(blocks[0]!.text).toBe("Hello world");
});
it("ignores ThinkingContent blocks, only extracts text", () => {
@ -190,8 +190,8 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd("visible text"));
expect(blocks).toHaveLength(1);
expect(blocks[0].text).toBe("visible text");
expect(blocks[0].text).not.toContain("internal thinking");
expect(blocks[0]!.text).toBe("visible text");
expect(blocks[0]!.text).not.toContain("internal thinking");
});
it("handles empty delta (duplicate event) gracefully", () => {
@ -205,7 +205,7 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd("Hello"));
expect(blocks).toHaveLength(1);
expect(blocks[0].text).toBe("Hello");
expect(blocks[0]!.text).toBe("Hello");
});
it("handles monotonically growing text correctly", () => {
@ -221,7 +221,7 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd("Hello"));
expect(blocks).toHaveLength(1);
expect(blocks[0].text).toBe("Hello");
expect(blocks[0]!.text).toBe("Hello");
});
});
@ -241,8 +241,8 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageUpdate(text));
expect(blocks.length).toBeGreaterThanOrEqual(1);
expect(blocks[0].text).toContain("first paragraph");
expect(blocks[0].isFinal).toBe(false);
expect(blocks[0]!.text).toContain("first paragraph");
expect(blocks[0]!.isFinal).toBe(false);
});
it("emits multiple blocks for very long text", () => {
@ -255,7 +255,7 @@ describe("MessageAggregator", () => {
expect(blocks.length).toBeGreaterThanOrEqual(2);
// All blocks except the last should have isFinal=false
for (let i = 0; i < blocks.length; i++) {
expect(blocks[i].isFinal).toBe(false);
expect(blocks[i]!.isFinal).toBe(false);
}
});
@ -268,7 +268,7 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd(text));
const finalBlock = blocks[blocks.length - 1];
expect(finalBlock.isFinal).toBe(true);
expect(finalBlock!.isFinal).toBe(true);
});
it("increments block index for each emitted block", () => {
@ -280,7 +280,7 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd(text));
for (let i = 0; i < blocks.length; i++) {
expect(blocks[i].index).toBe(i);
expect(blocks[i]!.index).toBe(i);
}
});
@ -293,7 +293,7 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd("First message text."));
expect(blocks).toHaveLength(1);
expect(blocks[0].index).toBe(0);
expect(blocks[0]!.index).toBe(0);
// Second message cycle — index should reset
agg.handleEvent(makeMessageStart("msg-2"));
@ -301,7 +301,7 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd("Second message text."));
expect(blocks).toHaveLength(2);
expect(blocks[1].index).toBe(0); // Reset after new message_start
expect(blocks[1]!.index).toBe(0); // Reset after new message_start
});
it("does not emit empty block on message_end with no content", () => {
@ -335,7 +335,7 @@ describe("MessageAggregator", () => {
// Final block should contain all text
expect(blocks).toHaveLength(1);
expect(blocks[0].text).toBe("Before tool call. After tool result.");
expect(blocks[0]!.text).toBe("Before tool call. After tool result.");
});
it("handles multiple message cycles (reset between)", () => {
@ -352,11 +352,11 @@ describe("MessageAggregator", () => {
agg.handleEvent(makeMessageEnd("Second response."));
expect(blocks).toHaveLength(2);
expect(blocks[0].text).toBe("First response.");
expect(blocks[1].text).toBe("Second response.");
expect(blocks[0]!.text).toBe("First response.");
expect(blocks[1]!.text).toBe("Second response.");
// Both should be final (flushed on message_end)
expect(blocks[0].isFinal).toBe(true);
expect(blocks[1].isFinal).toBe(true);
expect(blocks[0]!.isFinal).toBe(true);
expect(blocks[1]!.isFinal).toBe(true);
});
it("handles compaction events between messages", () => {