fix(agent): align tools + preserve indentation
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
|
||||
import type { AgentMessage, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type {
|
||||
AgentMessage,
|
||||
AgentToolResult,
|
||||
AgentToolUpdateCallback,
|
||||
ThinkingLevel,
|
||||
} from "@mariozechner/pi-agent-core";
|
||||
import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
buildSystemPrompt,
|
||||
@@ -11,6 +16,7 @@ import {
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
type Skill,
|
||||
type ToolDefinition,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js";
|
||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||
@@ -52,6 +58,35 @@ import {
|
||||
import { buildAgentSystemPromptAppend } from "./system-prompt.js";
|
||||
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||
|
||||
function toToolDefinitions(tools: { execute: unknown }[]): ToolDefinition[] {
|
||||
return tools.map((tool) => {
|
||||
const record = tool as {
|
||||
name?: unknown;
|
||||
label?: unknown;
|
||||
description?: unknown;
|
||||
parameters?: unknown;
|
||||
execute: (
|
||||
toolCallId: string,
|
||||
params: unknown,
|
||||
signal?: AbortSignal,
|
||||
onUpdate?: AgentToolUpdateCallback<unknown>,
|
||||
) => Promise<AgentToolResult<unknown>>;
|
||||
};
|
||||
const name = typeof record.name === "string" ? record.name : "tool";
|
||||
return {
|
||||
name,
|
||||
label: typeof record.label === "string" ? record.label : name,
|
||||
description:
|
||||
typeof record.description === "string" ? record.description : "",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema from pi-agent-core uses a different module instance.
|
||||
parameters: record.parameters as any,
|
||||
execute: async (toolCallId, params, onUpdate, _ctx, signal) => {
|
||||
return await record.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
} satisfies ToolDefinition;
|
||||
});
|
||||
}
|
||||
|
||||
export type EmbeddedPiAgentMeta = {
|
||||
sessionId: string;
|
||||
provider: string;
|
||||
@@ -412,7 +447,9 @@ export async function runEmbeddedPiAgent(params: {
|
||||
// Split tools into built-in (recognized by pi-coding-agent SDK) and custom (clawdbot-specific)
|
||||
const builtInToolNames = new Set(["read", "bash", "edit", "write"]);
|
||||
const builtInTools = tools.filter((t) => builtInToolNames.has(t.name));
|
||||
const customTools = tools.filter((t) => !builtInToolNames.has(t.name));
|
||||
const customTools = toToolDefinitions(
|
||||
tools.filter((t) => !builtInToolNames.has(t.name)),
|
||||
);
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
cwd: resolvedWorkspace,
|
||||
|
||||
@@ -780,9 +780,7 @@ describe("subscribeEmbeddedPiSession", () => {
|
||||
handler?.({ type: "message_end", message: assistantMessage });
|
||||
|
||||
expect(onBlockReply).toHaveBeenCalledTimes(3);
|
||||
expect(onBlockReply.mock.calls[1][0].text).toBe(
|
||||
"~~~sh\nline1\nline2\n~~~",
|
||||
);
|
||||
expect(onBlockReply.mock.calls[1][0].text).toBe("~~~sh\nline1\nline2\n~~~");
|
||||
});
|
||||
|
||||
it("keeps indented fenced blocks intact", () => {
|
||||
|
||||
@@ -284,30 +284,42 @@ export function subscribeEmbeddedPiSession(params: {
|
||||
const isSafeBreak = (spans: FenceSpan[], index: number): boolean =>
|
||||
!findFenceSpanAt(spans, index);
|
||||
|
||||
const pickSoftBreakIndex = (buffer: string): BreakResult => {
|
||||
const stripLeadingNewlines = (value: string): string => {
|
||||
let i = 0;
|
||||
while (i < value.length && value[i] === "\n") i++;
|
||||
return i > 0 ? value.slice(i) : value;
|
||||
};
|
||||
|
||||
const pickSoftBreakIndex = (
|
||||
buffer: string,
|
||||
minCharsOverride?: number,
|
||||
): BreakResult => {
|
||||
if (!blockChunking) return { index: -1 };
|
||||
const minChars = Math.max(1, Math.floor(blockChunking.minChars));
|
||||
const minChars = Math.max(
|
||||
1,
|
||||
Math.floor(minCharsOverride ?? blockChunking.minChars),
|
||||
);
|
||||
if (buffer.length < minChars) return { index: -1 };
|
||||
const fenceSpans = parseFenceSpans(buffer);
|
||||
const preference = blockChunking.breakPreference ?? "paragraph";
|
||||
|
||||
if (preference === "paragraph") {
|
||||
let paragraphIdx = buffer.lastIndexOf("\n\n");
|
||||
while (paragraphIdx >= minChars) {
|
||||
if (isSafeBreak(fenceSpans, paragraphIdx)) {
|
||||
let paragraphIdx = buffer.indexOf("\n\n");
|
||||
while (paragraphIdx !== -1) {
|
||||
if (paragraphIdx >= minChars && isSafeBreak(fenceSpans, paragraphIdx)) {
|
||||
return { index: paragraphIdx };
|
||||
}
|
||||
paragraphIdx = buffer.lastIndexOf("\n\n", paragraphIdx - 1);
|
||||
paragraphIdx = buffer.indexOf("\n\n", paragraphIdx + 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (preference === "paragraph" || preference === "newline") {
|
||||
let newlineIdx = buffer.lastIndexOf("\n");
|
||||
while (newlineIdx >= minChars) {
|
||||
if (isSafeBreak(fenceSpans, newlineIdx)) {
|
||||
let newlineIdx = buffer.indexOf("\n");
|
||||
while (newlineIdx !== -1) {
|
||||
if (newlineIdx >= minChars && isSafeBreak(fenceSpans, newlineIdx)) {
|
||||
return { index: newlineIdx };
|
||||
}
|
||||
newlineIdx = buffer.lastIndexOf("\n", newlineIdx - 1);
|
||||
newlineIdx = buffer.indexOf("\n", newlineIdx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +434,7 @@ export function subscribeEmbeddedPiSession(params: {
|
||||
) {
|
||||
const breakResult =
|
||||
force && blockBuffer.length <= maxChars
|
||||
? pickSoftBreakIndex(blockBuffer)
|
||||
? pickSoftBreakIndex(blockBuffer, 1)
|
||||
: pickBreakIndex(blockBuffer);
|
||||
if (breakResult.index <= 0) {
|
||||
if (force) {
|
||||
@@ -434,7 +446,9 @@ export function subscribeEmbeddedPiSession(params: {
|
||||
const breakIdx = breakResult.index;
|
||||
let rawChunk = blockBuffer.slice(0, breakIdx);
|
||||
if (rawChunk.trim().length === 0) {
|
||||
blockBuffer = blockBuffer.slice(breakIdx).trimStart();
|
||||
blockBuffer = stripLeadingNewlines(
|
||||
blockBuffer.slice(breakIdx),
|
||||
).trimStart();
|
||||
continue;
|
||||
}
|
||||
let nextBuffer = blockBuffer.slice(breakIdx);
|
||||
@@ -457,7 +471,7 @@ export function subscribeEmbeddedPiSession(params: {
|
||||
breakIdx < blockBuffer.length && /\s/.test(blockBuffer[breakIdx])
|
||||
? breakIdx + 1
|
||||
: breakIdx;
|
||||
blockBuffer = blockBuffer.slice(nextStart).trimStart();
|
||||
blockBuffer = stripLeadingNewlines(blockBuffer.slice(nextStart));
|
||||
}
|
||||
if (blockBuffer.length < minChars && !force) return;
|
||||
if (blockBuffer.length < maxChars && !force) return;
|
||||
|
||||
Reference in New Issue
Block a user