feat(pi-local): fix bugs, add RPC mode, improve cost tracking and output handling
Major improvements to the Pi local adapter:
Bug Fixes (Greptile-identified):
- Fix string interpolation in models.ts error message (was showing literal ${detail})
- Fix tool matching in parse.ts to use toolCallId instead of toolName for correct
multi-call handling and result assignment
- Fix dead code in execute.ts by tracking instructionsReadFailed flag
Feature Improvements:
- Switch from print mode (-p) to RPC mode (--mode rpc) to prevent agent from
exiting prematurely and ensure proper lifecycle completion
- Add stdin command sending via JSON-RPC format for prompt delivery
- Add line buffering in execute.ts to handle partial JSON chunks correctly
- Filter RPC protocol messages (response, extension_ui_request, etc.) from transcript
Cost Tracking:
- Extract cost and usage data from turn_end assistant messages
- Support both Pi format (input/output/cacheRead/cost.total) and generic format
- Add tests for cost extraction and accumulation across multiple turns
All tests pass (12/12), typecheck clean, server builds successfully.
This commit is contained in:
@@ -246,6 +246,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
||||
|
||||
let systemPromptExtension = "";
|
||||
let instructionsReadFailed = false;
|
||||
if (resolvedInstructionsFilePath) {
|
||||
try {
|
||||
const instructionsContents = await fs.readFile(resolvedInstructionsFilePath, "utf8");
|
||||
@@ -259,6 +260,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
`[paperclip] Loaded agent instructions file: ${resolvedInstructionsFilePath}\n`,
|
||||
);
|
||||
} catch (err) {
|
||||
instructionsReadFailed = true;
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
await onLog(
|
||||
"stderr",
|
||||
@@ -294,19 +296,22 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
|
||||
const commandNotes = (() => {
|
||||
if (!resolvedInstructionsFilePath) return [] as string[];
|
||||
if (systemPromptExtension.length > 0) {
|
||||
if (instructionsReadFailed) {
|
||||
return [
|
||||
`Loaded agent instructions from ${resolvedInstructionsFilePath}`,
|
||||
`Appended instructions + path directive to system prompt (relative references from ${instructionsFileDir}).`,
|
||||
`Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
||||
`Loaded agent instructions from ${resolvedInstructionsFilePath}`,
|
||||
`Appended instructions + path directive to system prompt (relative references from ${instructionsFileDir}).`,
|
||||
];
|
||||
})();
|
||||
|
||||
const buildArgs = (sessionFile: string): string[] => {
|
||||
const args: string[] = ["-p", userPrompt];
|
||||
const args: string[] = [];
|
||||
|
||||
// Use RPC mode for proper lifecycle management (waits for agent completion)
|
||||
args.push("--mode", "rpc");
|
||||
|
||||
// Use --append-system-prompt to extend Pi's default system prompt
|
||||
args.push("--append-system-prompt", renderedSystemPromptExtension);
|
||||
@@ -315,7 +320,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
if (modelId) args.push("--model", modelId);
|
||||
if (thinking) args.push("--thinking", thinking);
|
||||
|
||||
args.push("--mode", "json");
|
||||
args.push("--tools", "read,bash,edit,write,grep,find,ls");
|
||||
args.push("--session", sessionFile);
|
||||
|
||||
@@ -324,6 +328,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
return args;
|
||||
};
|
||||
|
||||
const buildRpcStdin = (): string => {
|
||||
// Send the prompt as an RPC command
|
||||
const promptCommand = {
|
||||
type: "prompt",
|
||||
message: userPrompt,
|
||||
};
|
||||
return JSON.stringify(promptCommand) + "\n";
|
||||
};
|
||||
|
||||
const runAttempt = async (sessionFile: string) => {
|
||||
const args = buildArgs(sessionFile);
|
||||
if (onMeta) {
|
||||
@@ -339,13 +352,43 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
});
|
||||
}
|
||||
|
||||
// Buffer stdout by lines to handle partial JSON chunks
|
||||
let stdoutBuffer = "";
|
||||
const bufferedOnLog = async (stream: "stdout" | "stderr", chunk: string) => {
|
||||
if (stream === "stderr") {
|
||||
// Pass stderr through immediately (not JSONL)
|
||||
await onLog(stream, chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buffer stdout and emit only complete lines
|
||||
stdoutBuffer += chunk;
|
||||
const lines = stdoutBuffer.split("\n");
|
||||
// Keep the last (potentially incomplete) line in the buffer
|
||||
stdoutBuffer = lines.pop() || "";
|
||||
|
||||
// Emit complete lines
|
||||
for (const line of lines) {
|
||||
if (line) {
|
||||
await onLog(stream, line + "\n");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const proc = await runChildProcess(runId, command, args, {
|
||||
cwd,
|
||||
env: runtimeEnv,
|
||||
timeoutSec,
|
||||
graceSec,
|
||||
onLog,
|
||||
onLog: bufferedOnLog,
|
||||
stdin: buildRpcStdin(),
|
||||
});
|
||||
|
||||
// Flush any remaining buffer content
|
||||
if (stdoutBuffer) {
|
||||
await onLog("stdout", stdoutBuffer);
|
||||
}
|
||||
|
||||
return {
|
||||
proc,
|
||||
rawStderr: proc.stderr,
|
||||
|
||||
Reference in New Issue
Block a user