mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 20:46:28 +01:00
fix(hermes): project native completion and tool activity
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
300afb3fe3
commit
f628d54bc8
12 files changed
+225
-12
No files matched your search
@@ -248,14 +248,14 @@ describe("omitProgressRepeatedByResponseAcrossSegments", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function toolCall(name: string, input?: unknown): TranscriptEntry {
|
||||
function toolCall(name: string, input?: unknown): Extract<TranscriptEntry, { kind: "tool_call" }> {
|
||||
return {
|
||||
kind: "tool_call",
|
||||
ts: TS,
|
||||
name,
|
||||
toolUseId: `tool-${name}`,
|
||||
input,
|
||||
} as TranscriptEntry;
|
||||
};
|
||||
}
|
||||
|
||||
describe("splitTranscriptAtAnchors", () => {
|
||||
@@ -366,6 +366,43 @@ describe("toolDisplayName", () => {
|
||||
describe("transcriptToTaskChatItems tool_call updates", () => {
|
||||
const opts = { runId: "run-1", running: false };
|
||||
|
||||
it("renders serialized native command arguments as the invocation", () => {
|
||||
const items = transcriptToTaskChatItems([
|
||||
toolCall("terminal", JSON.stringify({ cmd: "printf 'hello'", cwd: "/workspace" })),
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "tool-terminal", content: "hello", isError: false },
|
||||
], opts);
|
||||
expect(items).toMatchObject([{ kind: "tool", target: "printf 'hello'", detail: "hello", status: "completed" }]);
|
||||
});
|
||||
|
||||
it("updates a missing invocation without merging overlapping same-name calls", () => {
|
||||
const items = transcriptToTaskChatItems([
|
||||
toolCall("terminal"),
|
||||
{ ...toolCall("terminal", { command: "echo second" }), toolUseId: "second-call" },
|
||||
toolCall("terminal", JSON.stringify({ command: "echo first" })),
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "second-call", content: "second", isError: false },
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "tool-terminal", content: "first", isError: false },
|
||||
], opts);
|
||||
expect(items).toMatchObject([
|
||||
{ kind: "tool", target: "echo first", detail: "first", status: "completed" },
|
||||
{ kind: "tool", target: "echo second", detail: "second", status: "completed" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows native output and failures while retaining unknown structured details", () => {
|
||||
const metadata = JSON.stringify({ disposition: "applied", document: { key: "plan" } });
|
||||
const items = transcriptToTaskChatItems([
|
||||
toolCall("terminal", { command: "run checks" }),
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "tool-terminal", isError: true,
|
||||
content: JSON.stringify({ stdout: "Checking\nTwo checks failed", stderr: "Missing input", exit_code: 2 }) },
|
||||
toolCall("write_document", { key: "plan" }),
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "tool-write_document", content: metadata, isError: false },
|
||||
], opts);
|
||||
expect(items).toMatchObject([
|
||||
{ kind: "tool", status: "failed", detail: "Checking\nTwo checks failed\nMissing input\nExit code: 2" },
|
||||
{ kind: "tool", status: "completed", detail: metadata },
|
||||
]);
|
||||
});
|
||||
|
||||
function update(toolUseId: string, status: string): TranscriptEntry {
|
||||
// Mirrors what a persisted acpx tool_call_update line parses to: the
|
||||
// literal placeholder name plus a synthesized { text, status } input.
|
||||
|
||||
@@ -139,6 +139,7 @@ const TARGET_KEYS = [
|
||||
"path",
|
||||
"notebook_path",
|
||||
"command",
|
||||
"cmd",
|
||||
"pattern",
|
||||
"query",
|
||||
"url",
|
||||
@@ -162,6 +163,12 @@ function clip(text: string, max: number): string {
|
||||
*/
|
||||
export function summarizeToolInput(input: unknown): string | undefined {
|
||||
if (input == null) return undefined;
|
||||
if (typeof input === "string") {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(input);
|
||||
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) input = parsed;
|
||||
} catch { /* Ordinary text remains a valid tool input. */ }
|
||||
}
|
||||
if (typeof input === "string")
|
||||
return input.trim() ? clip(input, TARGET_MAX) : undefined;
|
||||
if (typeof input !== "object") return clip(String(input), TARGET_MAX);
|
||||
@@ -555,7 +562,19 @@ function mergeProviderActivityItem(
|
||||
/** Result content → the expandable mono detail block (clipped, trimmed). */
|
||||
function formatToolResultDetail(content: unknown): string | undefined {
|
||||
if (content == null) return undefined;
|
||||
const text = typeof content === "string" ? content : String(content);
|
||||
let text = typeof content === "string" ? content : String(content);
|
||||
try {
|
||||
const result: unknown = typeof content === "string" ? JSON.parse(content) : content;
|
||||
const output = objectRecord(result);
|
||||
const parts = [output.output ?? output.stdout ?? output.content, output.stderr, output.error]
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
||||
if (parts.length > 0) {
|
||||
text = [...new Set(parts)].join("\n");
|
||||
if (typeof output.exit_code === "number" && output.exit_code !== 0) {
|
||||
text += `\nExit code: ${output.exit_code}`;
|
||||
}
|
||||
}
|
||||
} catch { /* Preserve plain output and unrecognized structured results. */ }
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return undefined;
|
||||
return trimmed.length > DETAIL_MAX
|
||||
@@ -733,6 +752,8 @@ export function transcriptToTaskChatItems(
|
||||
existing.target = clip(entry.name!, TARGET_MAX);
|
||||
}
|
||||
}
|
||||
const incomingTarget = summarizeToolInput(entry.input);
|
||||
if (incomingTarget) existing.target = incomingTarget;
|
||||
lastToolIndex = existingIndex!;
|
||||
} else {
|
||||
items.push({
|
||||
|
||||
@@ -118,6 +118,30 @@ describe("provider notice presentation", () => {
|
||||
});
|
||||
|
||||
describe("nativeRunEventsToTranscript", () => {
|
||||
it.each(["command", "cmd"])("uses native %s arguments for process previews and preserves plain results", (key) => {
|
||||
const input = { [key]: "printf 'native output'", cwd: "/workspace" };
|
||||
const transcript = nativeRunEventsToTranscript([
|
||||
event(1, "tool.execution.started", {
|
||||
schema: "paperclip.tool.execution.v1", executionId: "hermes-terminal",
|
||||
transport: "process", operation: "execute", name: "terminal", status: "running",
|
||||
}),
|
||||
itemEvent(2, "item.started", "hermes-terminal", {
|
||||
item: { type: "tool_use", id: "hermes-terminal", name: "terminal", input: JSON.stringify(input) },
|
||||
}),
|
||||
event(3, "tool.execution.completed", {
|
||||
schema: "paperclip.tool.execution.v1", executionId: "hermes-terminal",
|
||||
transport: "process", operation: "execute", name: "terminal", status: "completed",
|
||||
}),
|
||||
itemEvent(4, "item.completed", "hermes-terminal", {
|
||||
item: { type: "tool_result", tool_use_id: "hermes-terminal", result: "native output\nnext line" },
|
||||
}),
|
||||
]);
|
||||
expect(transcript).toMatchObject([
|
||||
{ kind: "tool_call", toolUseId: "hermes-terminal", name: "Bash", input },
|
||||
{ kind: "tool_result", toolUseId: "hermes-terminal", content: "native output\nnext line", isError: false },
|
||||
]);
|
||||
});
|
||||
|
||||
describe("accepted response-wake authority", () => {
|
||||
function fixture() {
|
||||
const result = {
|
||||
|
||||
@@ -547,6 +547,7 @@ function nativeToolItemDetails(
|
||||
|
||||
function serializedNativeToolResult(item: NativeToolItemDetails): string {
|
||||
if (item.result === undefined) return "";
|
||||
if (typeof item.result === "string") return item.result;
|
||||
try {
|
||||
return JSON.stringify(item.result) ?? "";
|
||||
} catch {
|
||||
@@ -562,6 +563,14 @@ function toolPresentation(
|
||||
const operation = text(payload.operation);
|
||||
const reportedName = text(payload.name) ?? item?.name ?? null;
|
||||
if (transport === "process") {
|
||||
let input = item?.input;
|
||||
if (typeof input === "string") {
|
||||
try { input = JSON.parse(input); } catch { /* Keep the reported invocation fallback. */ }
|
||||
}
|
||||
const argumentsRecord = record(input);
|
||||
if (text(argumentsRecord?.command) || text(argumentsRecord?.cmd)) {
|
||||
return { name: "Bash", input };
|
||||
}
|
||||
return {
|
||||
name: "Bash",
|
||||
input: reportedName ? { command: reportedName } : { operation: operation ?? "execute" },
|
||||
|
||||
Reference in new issue
Block a user