refactor(agent): split runner, tools, output, types

This commit is contained in:
Jiayuan 2026-01-30 01:12:48 +08:00
parent c0e3dabf25
commit 5fdae53687
7 changed files with 104 additions and 70 deletions

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
import { Agent } from "./agent.js";
import { Agent } from "./runner.js";
type CliOptions = {
provider?: string;

View file

@ -1 +1,2 @@
export * from "./agent.js";
export * from "./runner.js";
export * from "./types.js";

View file

@ -1,24 +1,14 @@
import { Agent as PiAgentCore, type AgentEvent, type AgentMessage, type ThinkingLevel } from "@mariozechner/pi-agent-core";
import { getModel } from "@mariozechner/pi-ai";
import { createCodingTools } from "@mariozechner/pi-coding-agent";
import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
export type AgentRunResult = {
text: string;
error?: string;
export type AgentOutputState = {
lastAssistantText: string;
printedLen: number;
streaming: boolean;
};
export type AgentLogger = {
stdout?: NodeJS.WritableStream;
stderr?: NodeJS.WritableStream;
};
export type AgentOptions = {
provider?: string;
model?: string;
systemPrompt?: string;
thinkingLevel?: ThinkingLevel;
cwd?: string;
logger?: AgentLogger;
export type AgentOutput = {
state: AgentOutputState;
handleEvent: (event: AgentEvent) => void;
};
function extractText(message: AgentMessage | undefined): string {
@ -74,50 +64,27 @@ function formatToolLine(name: string, args: unknown): string {
return argText ? `• Used ${title} (${argText})` : `• Used ${title}`;
}
export class Agent {
private readonly agent: PiAgentCore;
private readonly stdout: NodeJS.WritableStream;
private readonly stderr: NodeJS.WritableStream;
private lastAssistantText = "";
private printedLen = 0;
private streaming = false;
export function createAgentOutput(params: {
stdout: NodeJS.WritableStream;
stderr: NodeJS.WritableStream;
}): AgentOutput {
const state: AgentOutputState = {
lastAssistantText: "",
printedLen: 0,
streaming: false,
};
constructor(options: AgentOptions = {}) {
this.stdout = options.logger?.stdout ?? process.stdout;
this.stderr = options.logger?.stderr ?? process.stderr;
this.agent = new PiAgentCore();
if (options.systemPrompt) this.agent.setSystemPrompt(options.systemPrompt);
if (options.thinkingLevel) this.agent.setThinkingLevel(options.thinkingLevel);
if (options.provider && options.model) {
this.agent.setModel(getModel(options.provider, options.model));
} else {
this.agent.setModel(getModel("kimi-coding", "kimi-k2-thinking"));
}
const cwd = options.cwd ?? process.cwd();
this.agent.setTools(createCodingTools(cwd));
this.agent.subscribe((event) => this.handleEvent(event));
}
async run(prompt: string): Promise<AgentRunResult> {
this.lastAssistantText = "";
await this.agent.prompt(prompt);
return { text: this.lastAssistantText, error: this.agent.state.error };
}
private handleEvent(event: AgentEvent) {
const handleEvent = (event: AgentEvent) => {
switch (event.type) {
case "message_start": {
const msg = event.message;
if (msg.role === "assistant") {
this.streaming = true;
this.printedLen = 0;
state.streaming = true;
state.printedLen = 0;
const text = extractText(msg);
if (text.length > 0) {
this.stdout.write(text);
this.printedLen = text.length;
params.stdout.write(text);
state.printedLen = text.length;
}
}
break;
@ -126,9 +93,9 @@ export class Agent {
const msg = event.message;
if (msg.role === "assistant") {
const text = extractText(msg);
if (text.length > this.printedLen) {
this.stdout.write(text.slice(this.printedLen));
this.printedLen = text.length;
if (text.length > state.printedLen) {
params.stdout.write(text.slice(state.printedLen));
state.printedLen = text.length;
}
}
break;
@ -137,27 +104,29 @@ export class Agent {
const msg = event.message;
if (msg.role === "assistant") {
const text = extractText(msg);
if (text.length > this.printedLen) {
this.stdout.write(text.slice(this.printedLen));
this.printedLen = text.length;
if (text.length > state.printedLen) {
params.stdout.write(text.slice(state.printedLen));
state.printedLen = text.length;
}
if (this.streaming) this.stdout.write("\n");
this.streaming = false;
this.lastAssistantText = text;
if (state.streaming) params.stdout.write("\n");
state.streaming = false;
state.lastAssistantText = text;
}
break;
}
case "tool_execution_start":
this.stderr.write(`${formatToolLine(event.toolName, event.args)}\n`);
params.stderr.write(`${formatToolLine(event.toolName, event.args)}\n`);
break;
case "tool_execution_end":
if (event.isError) {
const errorText = extractText(event.result) || "Tool failed";
this.stderr.write(`• Tool error (${toolDisplayName(event.toolName)}): ${errorText}\n`);
params.stderr.write(`• Tool error (${toolDisplayName(event.toolName)}): ${errorText}\n`);
}
break;
default:
break;
}
}
};
return { state, handleEvent };
}

29
src/agent/runner.ts Normal file
View file

@ -0,0 +1,29 @@
import { Agent as PiAgentCore, type AgentEvent } from "@mariozechner/pi-agent-core";
import type { AgentOptions, AgentRunResult } from "./types.js";
import { createAgentOutput } from "./output.js";
import { resolveModel, resolveTools } from "./tools.js";
export class Agent {
private readonly agent: PiAgentCore;
private readonly output;
constructor(options: AgentOptions = {}) {
const stdout = options.logger?.stdout ?? process.stdout;
const stderr = options.logger?.stderr ?? process.stderr;
this.output = createAgentOutput({ stdout, stderr });
this.agent = new PiAgentCore();
if (options.systemPrompt) this.agent.setSystemPrompt(options.systemPrompt);
if (options.thinkingLevel) this.agent.setThinkingLevel(options.thinkingLevel);
this.agent.setModel(resolveModel(options));
this.agent.setTools(resolveTools(options));
this.agent.subscribe((event: AgentEvent) => this.output.handleEvent(event));
}
async run(prompt: string): Promise<AgentRunResult> {
this.output.state.lastAssistantText = "";
await this.agent.prompt(prompt);
return { text: this.output.state.lastAssistantText, error: this.agent.state.error };
}
}

15
src/agent/tools.ts Normal file
View file

@ -0,0 +1,15 @@
import type { AgentOptions } from "./types.js";
import { getModel } from "@mariozechner/pi-ai";
import { createCodingTools } from "@mariozechner/pi-coding-agent";
export function resolveModel(options: AgentOptions) {
if (options.provider && options.model) {
return getModel(options.provider, options.model);
}
return getModel("kimi-coding", "kimi-k2-thinking");
}
export function resolveTools(options: AgentOptions) {
const cwd = options.cwd ?? process.cwd();
return createCodingTools(cwd);
}

20
src/agent/types.ts Normal file
View file

@ -0,0 +1,20 @@
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
export type AgentRunResult = {
text: string;
error?: string;
};
export type AgentLogger = {
stdout?: NodeJS.WritableStream;
stderr?: NodeJS.WritableStream;
};
export type AgentOptions = {
provider?: string;
model?: string;
systemPrompt?: string;
thinkingLevel?: ThinkingLevel;
cwd?: string;
logger?: AgentLogger;
};

View file

@ -1,5 +1,5 @@
import { v7 as uuidv7 } from "uuid";
import { Agent as CoreAgent } from "../agent/agent.js";
import { Agent as CoreAgent } from "../agent/runner.js";
import { Channel } from "./channel.js";
import type { Message } from "./types.js";