feat: streaming agent card with tool-use trace, reasoning panel, and tool results

Replace text-only PatchableTextStream with a unified StreamingAgentCard that
renders the full agent run lifecycle in a single Feishu interactive card:

- Tool-use panel: collapsible, shows each tool call with status (running/
  success/error), input summary, and result/error in code blocks
- Reasoning panel: collapsible, streams thinking text inline then collapses
  on completion
- Answer text: rendered via native Feishu markdown component (no post-row
  round-trip)

Runner now captures previously-discarded SDK events:
- thinking_delta → reasoning stream
- tool_use id + input on content_block_start / assistant message
- tool_result from user messages (was entirely unhandled)

New files:
- card/trace-store.ts: per-run in-memory tool-use trace with secret redaction
- card/builder.ts: Feishu card JSON builder (tool panel + reasoning + text)
- card/streaming-card.ts: StreamingAgentCard controller (400ms throttled)

client.ts: add sendCard/patchCard raw JSON primitives
This commit is contained in:
2026-07-09 17:18:40 +08:00
parent 346aee5a68
commit d6724e5a83
7 changed files with 964 additions and 32 deletions
+12 -3
View File
@@ -167,10 +167,19 @@ function textFromCard(card: unknown): string | null {
const parts: string[] = [];
for (const element of elements) {
if (typeof element !== "object" || element === null) continue;
// New format: { tag: "markdown", content: string }
const tag = (element as { tag?: unknown }).tag;
const directContent = (element as { content?: unknown }).content;
if (tag === "markdown" && typeof directContent === "string") {
parts.push(directContent);
continue;
}
// Old format: { text: { content: string } }
const text = (element as { text?: unknown }).text;
if (typeof text !== "object" || text === null) continue;
const content = (text as { content?: unknown }).content;
if (typeof content === "string") parts.push(content);
if (typeof text === "object" && text !== null) {
const content = (text as { content?: unknown }).content;
if (typeof content === "string") parts.push(content);
}
}
return parts.length === 0 ? null : parts.join("\n");
}