|
1 | 1 | import { spawn } from "node:child_process"; |
2 | 2 | import os from "node:os"; |
3 | | -import type { AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-agent-core"; |
| 3 | +import type { |
| 4 | + AgentMessage, |
| 5 | + AgentToolResult, |
| 6 | + AgentToolUpdateCallback, |
| 7 | +} from "@mariozechner/pi-agent-core"; |
4 | 8 | import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; |
5 | 9 | import { Type } from "typebox"; |
6 | 10 | import type { OpenClawConfig } from "../config/types.openclaw.js"; |
@@ -37,11 +41,22 @@ export type ToolSearchCatalogToolExecutor = (params: { |
37 | 41 | tool: CatalogTool; |
38 | 42 | toolName: string; |
39 | 43 | toolCallId: string; |
| 44 | + parentToolCallId?: string; |
40 | 45 | input: unknown; |
41 | 46 | signal?: AbortSignal; |
42 | 47 | onUpdate?: AgentToolUpdateCallback<unknown>; |
43 | 48 | }) => Promise<AgentToolResult<unknown>>; |
44 | 49 |
|
| 50 | +export type ToolSearchTargetTranscriptProjection = { |
| 51 | + parentToolCallId?: string; |
| 52 | + toolCallId: string; |
| 53 | + toolName: string; |
| 54 | + input: unknown; |
| 55 | + result?: unknown; |
| 56 | + isError?: boolean; |
| 57 | + timestamp?: number; |
| 58 | +}; |
| 59 | + |
45 | 60 | export type ToolSearchConfig = { |
46 | 61 | enabled: boolean; |
47 | 62 | mode: ToolSearchMode; |
@@ -518,6 +533,149 @@ function dropToolSearchControlTools(tools: AnyAgentTool[]): AnyAgentTool[] { |
518 | 533 | return tools.filter((tool) => !TOOL_SEARCH_CONTROL_TOOL_NAMES.has(tool.name)); |
519 | 534 | } |
520 | 535 |
|
| 536 | +function readMessageToolResultId(message: AgentMessage): string | undefined { |
| 537 | + const record = message as unknown as Record<string, unknown>; |
| 538 | + const role = typeof record.role === "string" ? record.role : ""; |
| 539 | + const canUseDirectId = role === "toolResult" || role === "tool"; |
| 540 | + const direct = record.toolCallId ?? record.toolUseId ?? record.tool_use_id; |
| 541 | + if (canUseDirectId && typeof direct === "string" && direct.trim()) { |
| 542 | + return direct; |
| 543 | + } |
| 544 | + const content = record.content; |
| 545 | + if (!Array.isArray(content)) { |
| 546 | + return undefined; |
| 547 | + } |
| 548 | + for (const block of content) { |
| 549 | + if (!isRecord(block)) { |
| 550 | + continue; |
| 551 | + } |
| 552 | + if (block.type !== "toolResult") { |
| 553 | + continue; |
| 554 | + } |
| 555 | + const nested = block.toolCallId ?? block.toolUseId ?? block.tool_use_id ?? block.id; |
| 556 | + if (typeof nested === "string" && nested.trim()) { |
| 557 | + return nested; |
| 558 | + } |
| 559 | + } |
| 560 | + return undefined; |
| 561 | +} |
| 562 | + |
| 563 | +function textFromToolSearchProjectionResult(result: unknown, isError: boolean): string { |
| 564 | + if (isRecord(result)) { |
| 565 | + const details = isRecord(result.details) ? result.details : undefined; |
| 566 | + const detailError = details?.error; |
| 567 | + if (typeof detailError === "string" && detailError.trim()) { |
| 568 | + return detailError; |
| 569 | + } |
| 570 | + const content = result.content; |
| 571 | + if (Array.isArray(content)) { |
| 572 | + const text = content |
| 573 | + .map((item) => (isRecord(item) && typeof item.text === "string" ? item.text : "")) |
| 574 | + .filter(Boolean) |
| 575 | + .join("\n"); |
| 576 | + if (text.trim()) { |
| 577 | + return text; |
| 578 | + } |
| 579 | + } |
| 580 | + } |
| 581 | + const safe = toJsonSafe(result); |
| 582 | + if (typeof safe === "string") { |
| 583 | + return safe; |
| 584 | + } |
| 585 | + const encoded = JSON.stringify(safe); |
| 586 | + if (typeof encoded === "string") { |
| 587 | + return encoded; |
| 588 | + } |
| 589 | + return isError ? "Tool Search target tool failed." : "Tool Search target tool completed."; |
| 590 | +} |
| 591 | + |
| 592 | +function buildToolSearchTargetTranscriptMessages( |
| 593 | + projection: ToolSearchTargetTranscriptProjection, |
| 594 | +): AgentMessage[] { |
| 595 | + const input = toJsonSafe(projection.input); |
| 596 | + const timestamp = projection.timestamp ?? Date.now(); |
| 597 | + const resultRecord = isRecord(projection.result) ? projection.result : undefined; |
| 598 | + const resultContent = |
| 599 | + Array.isArray(resultRecord?.content) && resultRecord.content.length > 0 |
| 600 | + ? toJsonSafe(resultRecord.content) |
| 601 | + : [ |
| 602 | + { |
| 603 | + type: "text", |
| 604 | + text: textFromToolSearchProjectionResult( |
| 605 | + projection.result, |
| 606 | + projection.isError === true, |
| 607 | + ), |
| 608 | + }, |
| 609 | + ]; |
| 610 | + return [ |
| 611 | + { |
| 612 | + role: "assistant", |
| 613 | + content: [ |
| 614 | + { |
| 615 | + type: "toolCall", |
| 616 | + id: projection.toolCallId, |
| 617 | + name: projection.toolName, |
| 618 | + arguments: input, |
| 619 | + input, |
| 620 | + }, |
| 621 | + ], |
| 622 | + stopReason: "toolUse", |
| 623 | + timestamp, |
| 624 | + } as unknown as AgentMessage, |
| 625 | + { |
| 626 | + role: "toolResult", |
| 627 | + toolCallId: projection.toolCallId, |
| 628 | + toolName: projection.toolName, |
| 629 | + isError: projection.isError === true, |
| 630 | + content: resultContent, |
| 631 | + timestamp, |
| 632 | + } as unknown as AgentMessage, |
| 633 | + ]; |
| 634 | +} |
| 635 | + |
| 636 | +export function projectToolSearchTargetTranscriptMessages( |
| 637 | + messages: AgentMessage[], |
| 638 | + projections: readonly ToolSearchTargetTranscriptProjection[], |
| 639 | +): AgentMessage[] { |
| 640 | + if (projections.length === 0) { |
| 641 | + return messages; |
| 642 | + } |
| 643 | + const byParent = new Map<string, ToolSearchTargetTranscriptProjection[]>(); |
| 644 | + const unmatched: ToolSearchTargetTranscriptProjection[] = []; |
| 645 | + for (const projection of projections) { |
| 646 | + const parent = projection.parentToolCallId?.trim(); |
| 647 | + if (!parent) { |
| 648 | + unmatched.push(projection); |
| 649 | + continue; |
| 650 | + } |
| 651 | + const group = byParent.get(parent) ?? []; |
| 652 | + group.push(projection); |
| 653 | + byParent.set(parent, group); |
| 654 | + } |
| 655 | + const inserted = new Set<ToolSearchTargetTranscriptProjection>(); |
| 656 | + const projected: AgentMessage[] = []; |
| 657 | + for (const message of messages) { |
| 658 | + projected.push(message); |
| 659 | + const toolResultId = readMessageToolResultId(message); |
| 660 | + const group = toolResultId ? byParent.get(toolResultId) : undefined; |
| 661 | + if (!group) { |
| 662 | + continue; |
| 663 | + } |
| 664 | + for (const projection of group) { |
| 665 | + projected.push(...buildToolSearchTargetTranscriptMessages(projection)); |
| 666 | + inserted.add(projection); |
| 667 | + } |
| 668 | + } |
| 669 | + for (const projection of [...unmatched, ...projections]) { |
| 670 | + if (inserted.has(projection)) { |
| 671 | + continue; |
| 672 | + } |
| 673 | + projected.push(...buildToolSearchTargetTranscriptMessages(projection)); |
| 674 | + inserted.add(projection); |
| 675 | + } |
| 676 | + return projected; |
| 677 | +} |
| 678 | + |
521 | 679 | export function createToolSearchCatalogRef(): ToolSearchCatalogRef { |
522 | 680 | return {}; |
523 | 681 | } |
@@ -888,6 +1046,7 @@ class ToolSearchRuntime { |
888 | 1046 | tool: entry.tool, |
889 | 1047 | toolName: entry.name, |
890 | 1048 | toolCallId, |
| 1049 | + parentToolCallId: options?.parentToolCallId, |
891 | 1050 | input: input ?? {}, |
892 | 1051 | signal: options?.signal ?? this.ctx.abortSignal, |
893 | 1052 | onUpdate: options?.onUpdate, |
|
0 commit comments