refactor: eliminate ~400 no-explicit-any warnings across the workspace
Parallel subagent pass: four typescript-pro agents on non-overlapping scopes.
Patterns applied:
- catch (err: any) { ... err.message ... } → catch (err) { ... getErrorMessage(err) ... }
using the new @fusion/core helper. Bare catch {} where the error was unused.
- SQLite row types: defined typed XxxRow interfaces per table and cast
.all()/.get() results via `as unknown as XxxRow[]` (the double cast is
required because better-sqlite3 returns Record<string, SQLOutputValue>).
- rowToX(row: any) converters: typed argument with the matching row interface.
- Dynamic settings key writes: (settings as Record<string, unknown>)[key].
- React event handlers and setState callbacks: inferred types or concrete
React.{Mouse,Change,Form}Event<...> where needed.
- pi-claude-cli: local PiMessage / PiContext duck types to avoid re-typing
pi-ai concrete shapes; typed Claude stream event message fields.
72 files changed, ~400 anys eliminated. Typecheck passes across the workspace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { ClaudeApiEvent, TrackedContentBlock } from "./types";
|
||||
import { calculateCost } from "@mariozechner/pi-ai";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
AssistantMessageEventStream,
|
||||
Model,
|
||||
@@ -71,7 +72,7 @@ function mapStopReason(
|
||||
*/
|
||||
export function createEventBridge(
|
||||
stream: AssistantMessageEventStream,
|
||||
model: Model<any>,
|
||||
model: Model<Api>,
|
||||
): EventBridge {
|
||||
// Tracked content blocks indexed by Claude's content_block index
|
||||
const blocks: TrackedBlock[] = [];
|
||||
@@ -270,7 +271,7 @@ export function createEventBridge(
|
||||
// Try to parse accumulated JSON -- on success update args, on failure keep previous
|
||||
try {
|
||||
block.arguments = JSON.parse(block.partialJson);
|
||||
(output.content[idx] as any).arguments = block.arguments;
|
||||
(output.content[idx] as ToolCall).arguments = block.arguments as Record<string, unknown>;
|
||||
} catch {
|
||||
// Partial JSON not yet parseable -- keep previous arguments
|
||||
}
|
||||
@@ -305,7 +306,7 @@ export function createEventBridge(
|
||||
|
||||
const block = blocks[idx];
|
||||
// Clean up the tracking index from the block (no longer needed)
|
||||
delete (block as any).index;
|
||||
delete (block as unknown as Record<string, unknown>).index;
|
||||
|
||||
if (block.type === "text") {
|
||||
stream.push({
|
||||
@@ -333,11 +334,11 @@ export function createEventBridge(
|
||||
|
||||
// Update output.content with final arguments
|
||||
const contentBlock = output.content[idx] as ToolCall;
|
||||
(contentBlock as any).arguments = finalArgs;
|
||||
|
||||
// ToolCall.arguments is typed as Record<string, any> in pi-ai, but we
|
||||
// intentionally emit a raw string when JSON parse fails completely.
|
||||
// Pi handles string arguments gracefully at runtime.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- finalArgs may be a raw string when JSON parse fails; pi-ai handles it at runtime
|
||||
(contentBlock as any).arguments = finalArgs;
|
||||
const toolCall = {
|
||||
type: "toolCall" as const,
|
||||
id: block.id,
|
||||
|
||||
@@ -10,6 +10,24 @@ import { join, dirname } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* A single tool descriptor returned by pi.getAllTools().
|
||||
*/
|
||||
interface PiToolInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal duck-type interface for the pi ExtensionAPI instance.
|
||||
* We only call getAllTools(), so we only declare that method.
|
||||
* The return type is unknown to accommodate defensive runtime checks.
|
||||
*/
|
||||
interface PiInstance {
|
||||
getAllTools(): unknown;
|
||||
}
|
||||
|
||||
/** The 6 built-in tools that pi handles natively (match pi tool names). */
|
||||
const BUILT_IN_TOOL_NAMES = new Set([
|
||||
"read",
|
||||
@@ -33,16 +51,16 @@ export interface McpToolDef {
|
||||
* @param pi - The pi ExtensionAPI instance
|
||||
* @returns Array of custom tool definitions (empty if all tools are built-in)
|
||||
*/
|
||||
export function getCustomToolDefs(pi: any): McpToolDef[] {
|
||||
export function getCustomToolDefs(pi: PiInstance): McpToolDef[] {
|
||||
const allTools = pi.getAllTools();
|
||||
|
||||
if (!Array.isArray(allTools)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return allTools
|
||||
.filter((tool: any) => !BUILT_IN_TOOL_NAMES.has(tool.name))
|
||||
.map((tool: any) => ({
|
||||
return (allTools as PiToolInfo[])
|
||||
.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name))
|
||||
.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.parameters,
|
||||
|
||||
@@ -107,7 +107,7 @@ export function cleanupSystemPromptFile(): void {
|
||||
*/
|
||||
export function writeUserMessage(
|
||||
proc: ChildProcess,
|
||||
prompt: string | any[],
|
||||
prompt: string | unknown[],
|
||||
): void {
|
||||
const message = {
|
||||
type: "user",
|
||||
|
||||
@@ -11,6 +11,19 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve, join, dirname } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
/**
|
||||
* Minimal message shape that prompt-builder accepts.
|
||||
* Uses a wide `role: string` discriminant so tests can pass plain objects
|
||||
* without literal type annotations. Content is typed broadly as
|
||||
* `string | unknown[]` since helper functions narrow at runtime.
|
||||
*/
|
||||
export interface PiMessage {
|
||||
role: string;
|
||||
content: string | unknown[];
|
||||
toolName?: string;
|
||||
}
|
||||
export type PiContext = { systemPrompt?: string; messages: PiMessage[] };
|
||||
import {
|
||||
mapPiToolNameToClaude,
|
||||
translatePiArgsToClaude,
|
||||
@@ -29,9 +42,6 @@ type AnthropicContentBlock =
|
||||
source: { type: "base64"; media_type: string; data: string };
|
||||
};
|
||||
|
||||
// We use `any` for Context to avoid requiring @mariozechner/pi-ai at dev time.
|
||||
// At runtime, pi provides the real Context type.
|
||||
|
||||
/**
|
||||
* Flattens a pi conversation context's messages array into a labeled text prompt
|
||||
* suitable for sending to the Claude CLI subprocess.
|
||||
@@ -51,14 +61,15 @@ let placeholderImageCount = 0;
|
||||
* pi-ai format: { type: "image", data: string (base64), mimeType: string }
|
||||
* Anthropic format: { type: "image", source: { type: "base64", media_type: string, data: string } }
|
||||
*/
|
||||
function translateImageBlock(piBlock: any): AnthropicContentBlock | null {
|
||||
if (piBlock.data && piBlock.mimeType) {
|
||||
function translateImageBlock(piBlock: unknown): AnthropicContentBlock | null {
|
||||
const block = piBlock as Record<string, unknown>;
|
||||
if (typeof block.data === "string" && typeof block.mimeType === "string") {
|
||||
return {
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: piBlock.mimeType,
|
||||
data: piBlock.data,
|
||||
media_type: block.mimeType,
|
||||
data: block.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -72,7 +83,7 @@ function translateImageBlock(piBlock: any): AnthropicContentBlock | null {
|
||||
* @returns Array of AnthropicContentBlock with text and translated images
|
||||
*/
|
||||
function buildFinalUserContent(
|
||||
content: string | any[],
|
||||
content: string | unknown[],
|
||||
): AnthropicContentBlock[] {
|
||||
if (typeof content === "string") {
|
||||
return [{ type: "text", text: content }];
|
||||
@@ -82,9 +93,10 @@ function buildFinalUserContent(
|
||||
}
|
||||
|
||||
const blocks: AnthropicContentBlock[] = [];
|
||||
for (const block of content) {
|
||||
for (const rawBlock of content) {
|
||||
const block = rawBlock as Record<string, unknown>;
|
||||
if (block.type === "text") {
|
||||
blocks.push({ type: "text", text: block.text ?? "" });
|
||||
blocks.push({ type: "text", text: typeof block.text === "string" ? block.text : "" });
|
||||
} else if (block.type === "image") {
|
||||
const translated = translateImageBlock(block);
|
||||
if (translated) {
|
||||
@@ -106,9 +118,9 @@ function buildFinalUserContent(
|
||||
/**
|
||||
* Check if a message content array contains image blocks.
|
||||
*/
|
||||
function contentHasImages(content: string | any[]): boolean {
|
||||
function contentHasImages(content: string | unknown[]): boolean {
|
||||
if (typeof content === "string" || !Array.isArray(content)) return false;
|
||||
return content.some((block) => block.type === "image");
|
||||
return content.some((block) => (block as Record<string, unknown>).type === "image");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +128,7 @@ function contentHasImages(content: string | any[]): boolean {
|
||||
* If so, build a simplified prompt that presents the result directly
|
||||
* instead of replaying the full conversation history with tool labels.
|
||||
*/
|
||||
function buildCustomToolResultPrompt(messages: any[]): string | null {
|
||||
function buildCustomToolResultPrompt(messages: PiMessage[]): string | null {
|
||||
if (messages.length < 3) return null;
|
||||
|
||||
const last = messages[messages.length - 1];
|
||||
@@ -126,8 +138,9 @@ function buildCustomToolResultPrompt(messages: any[]): string | null {
|
||||
// Find the original user message (scan backwards past assistant + toolResult)
|
||||
let userMessage: string | null = null;
|
||||
for (let i = messages.length - 3; i >= 0; i--) {
|
||||
if (messages[i].role === "user") {
|
||||
userMessage = userContentToText(messages[i].content);
|
||||
const msg = messages[i];
|
||||
if (msg.role === "user") {
|
||||
userMessage = userContentToText(msg.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -150,9 +163,7 @@ function buildCustomToolResultPrompt(messages: any[]): string | null {
|
||||
*
|
||||
* Falls back to full prompt if the message structure is unexpected.
|
||||
*/
|
||||
export function buildResumePrompt(context: {
|
||||
messages: any[];
|
||||
}): string | AnthropicContentBlock[] {
|
||||
export function buildResumePrompt(context: PiContext): string | AnthropicContentBlock[] {
|
||||
const messages = context.messages;
|
||||
if (messages.length === 0) return "";
|
||||
|
||||
@@ -162,7 +173,7 @@ export function buildResumePrompt(context: {
|
||||
|
||||
// Collect new messages: everything from the last assistant turn onwards
|
||||
// (tool results from the last assistant + the new user message)
|
||||
const newMessages: any[] = [];
|
||||
const newMessages: PiMessage[] = [];
|
||||
|
||||
// Walk backwards from finalUserIndex to find where new content starts.
|
||||
// Include trailing toolResult messages that follow the last assistant turn.
|
||||
@@ -211,9 +222,7 @@ export function buildResumePrompt(context: {
|
||||
return parts.join("\n") || "";
|
||||
}
|
||||
|
||||
export function buildPrompt(context: {
|
||||
messages: any[];
|
||||
}): string | AnthropicContentBlock[] {
|
||||
export function buildPrompt(context: PiContext): string | AnthropicContentBlock[] {
|
||||
// Reset placeholder counter for each call
|
||||
placeholderImageCount = 0;
|
||||
|
||||
@@ -232,11 +241,13 @@ export function buildPrompt(context: {
|
||||
|
||||
// Determine if any message has images worth passing through
|
||||
const finalUserIndex = findFinalUserMessageIndex(context.messages);
|
||||
const finalUserMsg = finalUserIndex >= 0 ? context.messages[finalUserIndex] : undefined;
|
||||
const finalUserHasImages =
|
||||
finalUserIndex >= 0 &&
|
||||
contentHasImages(context.messages[finalUserIndex].content);
|
||||
finalUserMsg !== undefined &&
|
||||
finalUserMsg.role === "user" &&
|
||||
contentHasImages(finalUserMsg.content);
|
||||
const anyToolResultHasImages = context.messages.some(
|
||||
(m: any) => m.role === "toolResult" && toolResultHasImages(m.content),
|
||||
(m) => m.role === "toolResult" && toolResultHasImages(m.content),
|
||||
);
|
||||
|
||||
if (finalUserHasImages || anyToolResultHasImages) {
|
||||
@@ -265,7 +276,8 @@ export function buildPrompt(context: {
|
||||
historyParts.push(toolResultContentToText(message.content));
|
||||
// Collect image blocks from tool results for passthrough
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
for (const rawBlock of message.content) {
|
||||
const block = rawBlock as Record<string, unknown>;
|
||||
if (block.type === "image") {
|
||||
const translated = translateImageBlock(block);
|
||||
if (translated) {
|
||||
@@ -281,8 +293,8 @@ export function buildPrompt(context: {
|
||||
|
||||
// Build final user message content blocks
|
||||
const finalUserContent =
|
||||
finalUserIndex >= 0
|
||||
? buildFinalUserContent(context.messages[finalUserIndex].content)
|
||||
finalUserMsg?.role === "user"
|
||||
? buildFinalUserContent(finalUserMsg.content)
|
||||
: [];
|
||||
|
||||
// Combine: history text + tool result images + final user content blocks
|
||||
@@ -341,7 +353,7 @@ export function buildPrompt(context: {
|
||||
* Find the index of the last user message in the messages array.
|
||||
* Returns -1 if no user message found.
|
||||
*/
|
||||
function findFinalUserMessageIndex(messages: any[]): number {
|
||||
function findFinalUserMessageIndex(messages: PiMessage[]): number {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === "user") return i;
|
||||
}
|
||||
@@ -354,7 +366,7 @@ function findFinalUserMessageIndex(messages: any[]): number {
|
||||
* Sanitizes .pi references to .claude for Claude Code compatibility.
|
||||
*/
|
||||
export function buildSystemPrompt(
|
||||
context: { systemPrompt?: string; messages: any[] },
|
||||
context: PiContext,
|
||||
cwd: string,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
@@ -377,7 +389,7 @@ export function buildSystemPrompt(
|
||||
|
||||
// When conversation history has tool results, instruct Claude to use them
|
||||
// instead of trying to re-call tools (which may not be available).
|
||||
if (context.messages?.some((m: any) => m.role === "toolResult")) {
|
||||
if (context.messages?.some((m) => m.role === "toolResult")) {
|
||||
parts.push(
|
||||
"IMPORTANT: The conversation history below contains tool results from previously executed tools. " +
|
||||
"Use these results to answer the user's question. Do NOT attempt to re-call tools that already have results.",
|
||||
@@ -393,14 +405,15 @@ export function buildSystemPrompt(
|
||||
* Image blocks are replaced with placeholder text (HIST-02).
|
||||
* Increments the module-level placeholderImageCount for each image.
|
||||
*/
|
||||
function userContentToText(content: string | any[]): string {
|
||||
function userContentToText(content: string | unknown[]): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
|
||||
const texts: string[] = [];
|
||||
for (const block of content) {
|
||||
for (const rawBlock of content) {
|
||||
const block = rawBlock as Record<string, unknown>;
|
||||
if (block.type === "text") {
|
||||
texts.push(block.text ?? "");
|
||||
texts.push(typeof block.text === "string" ? block.text : "");
|
||||
} else if (block.type === "image") {
|
||||
texts.push("[An image was shared here but could not be included]");
|
||||
placeholderImageCount++;
|
||||
@@ -414,37 +427,34 @@ function userContentToText(content: string | any[]): string {
|
||||
* Converts assistant message content to text.
|
||||
* Handles string content and array of content blocks (text, thinking, toolCall).
|
||||
*/
|
||||
function contentToText(content: string | any[]): string {
|
||||
function contentToText(content: string | unknown[]): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
|
||||
return content
|
||||
.map((block) => {
|
||||
if (block.type === "text") return block.text ?? "";
|
||||
.map((rawBlock) => {
|
||||
const block = rawBlock as Record<string, unknown>;
|
||||
if (block.type === "text") return typeof block.text === "string" ? block.text : "";
|
||||
if (block.type === "thinking") return ""; // Skip thinking — internal reasoning, not conversation
|
||||
if (block.type === "toolCall") {
|
||||
const isCustom = isCustomToolName(block.name);
|
||||
const name = typeof block.name === "string" ? block.name : "";
|
||||
const args = block.arguments && typeof block.arguments === "object"
|
||||
? (block.arguments as Record<string, unknown>)
|
||||
: undefined;
|
||||
const isCustom = isCustomToolName(name);
|
||||
if (isCustom) {
|
||||
// Custom tools: don't reference the MCP tool name — Claude might try to re-call it.
|
||||
// Just note what was done. The result follows as a TOOL RESULT message.
|
||||
const argsStr = block.arguments
|
||||
? JSON.stringify(block.arguments)
|
||||
: "{}";
|
||||
return `[Used ${block.name} tool with args: ${argsStr}]`;
|
||||
const argsStr = args ? JSON.stringify(args) : "{}";
|
||||
return `[Used ${name} tool with args: ${argsStr}]`;
|
||||
}
|
||||
const claudeName = mapPiToolNameToClaude(block.name);
|
||||
const claudeArgs =
|
||||
block.arguments && typeof block.arguments === "object"
|
||||
? translatePiArgsToClaude(
|
||||
block.name,
|
||||
block.arguments as Record<string, unknown>,
|
||||
)
|
||||
: block.arguments;
|
||||
const claudeName = mapPiToolNameToClaude(name);
|
||||
const claudeArgs = args ? translatePiArgsToClaude(name, args) : undefined;
|
||||
const argsStr = claudeArgs ? JSON.stringify(claudeArgs) : "{}";
|
||||
return `Historical tool call (non-executable): ${claudeName} args=${argsStr}`;
|
||||
}
|
||||
// Unknown block types are represented as a placeholder
|
||||
return `[${block.type}]`;
|
||||
return `[${String(block.type)}]`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
@@ -454,14 +464,15 @@ function contentToText(content: string | any[]): string {
|
||||
* Handles string content and array of content blocks.
|
||||
* Image blocks get placeholder text (actual image passthrough handled separately).
|
||||
*/
|
||||
function toolResultContentToText(content: string | any[]): string {
|
||||
function toolResultContentToText(content: string | unknown[]): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
|
||||
const texts: string[] = [];
|
||||
for (const block of content) {
|
||||
for (const rawBlock of content) {
|
||||
const block = rawBlock as Record<string, unknown>;
|
||||
if (block.type === "text") {
|
||||
texts.push(block.text ?? "");
|
||||
texts.push(typeof block.text === "string" ? block.text : "");
|
||||
} else if (block.type === "image") {
|
||||
texts.push("[An image was shared here but could not be included]");
|
||||
placeholderImageCount++;
|
||||
@@ -473,9 +484,9 @@ function toolResultContentToText(content: string | any[]): string {
|
||||
/**
|
||||
* Check if a tool result content array contains image blocks.
|
||||
*/
|
||||
function toolResultHasImages(content: string | any[]): boolean {
|
||||
function toolResultHasImages(content: string | unknown[]): boolean {
|
||||
if (typeof content === "string" || !Array.isArray(content)) return false;
|
||||
return content.some((block) => block.type === "image");
|
||||
return content.some((block) => (block as Record<string, unknown>).type === "image");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,13 +17,18 @@
|
||||
import { createInterface } from "node:readline";
|
||||
import {
|
||||
AssistantMessageEventStream,
|
||||
type Api,
|
||||
type Model,
|
||||
type SimpleStreamOptions,
|
||||
type TextContent,
|
||||
type ThinkingContent,
|
||||
type ToolCall,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import {
|
||||
buildPrompt,
|
||||
buildSystemPrompt,
|
||||
buildResumePrompt,
|
||||
type PiContext,
|
||||
} from "./prompt-builder.js";
|
||||
import {
|
||||
spawnClaude,
|
||||
@@ -66,8 +71,8 @@ type StreamViaCLiOptions = SimpleStreamOptions & {
|
||||
* @returns An AssistantMessageEventStream that receives bridged events
|
||||
*/
|
||||
export function streamViaCli(
|
||||
model: Model<any>,
|
||||
context: { messages: any[]; systemPrompt?: string },
|
||||
model: Model<Api>,
|
||||
context: PiContext,
|
||||
options?: StreamViaCLiOptions,
|
||||
): AssistantMessageEventStream {
|
||||
// @ts-expect-error — tsc can't verify AssistantMessageEventStream is a value
|
||||
@@ -153,7 +158,7 @@ export function streamViaCli(
|
||||
type: "done",
|
||||
reason: "stop",
|
||||
message: errorMessage,
|
||||
} as any);
|
||||
});
|
||||
stream.end();
|
||||
}
|
||||
|
||||
@@ -235,7 +240,7 @@ export function streamViaCli(
|
||||
if (msg.type === "stream_event") {
|
||||
// Only forward top-level events to pi's event bridge.
|
||||
// Sub-agent events (parent_tool_use_id !== null) are internal to the CLI.
|
||||
const isTopLevel = !(msg as any).parent_tool_use_id;
|
||||
const isTopLevel = !msg.parent_tool_use_id;
|
||||
if (isTopLevel) {
|
||||
bridge.handleEvent(msg.event);
|
||||
}
|
||||
@@ -296,7 +301,7 @@ export function streamViaCli(
|
||||
// it means only user MCP tools were called (filtered by event bridge).
|
||||
// Override to "stop" so pi doesn't try to execute non-existent tools.
|
||||
const piToolCalls = (output.content || []).filter(
|
||||
(c: any) => c.type === "toolCall",
|
||||
(c: TextContent | ThinkingContent | ToolCall) => c.type === "toolCall",
|
||||
);
|
||||
const effectiveReason =
|
||||
output.stopReason === "toolUse" && piToolCalls.length === 0
|
||||
@@ -316,12 +321,25 @@ export function streamViaCli(
|
||||
});
|
||||
stream.end();
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
// Push a "done" event with a text error so pi gets a valid AssistantMessage.
|
||||
// Pushing type:"error" would require an AssistantMessage in the error field,
|
||||
// but we don't have a full AssistantMessage here.
|
||||
stream.push({
|
||||
type: "error",
|
||||
reason: "error",
|
||||
error: err.message ?? "Unexpected error in streamViaCli",
|
||||
} as any);
|
||||
type: "done",
|
||||
reason: "stop",
|
||||
message: {
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text" as const, text: `Error: ${errMsg}` }],
|
||||
api: "pi-claude-cli",
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
||||
stopReason: "stop" as const,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
stream.end();
|
||||
} finally {
|
||||
// Clean up abort listener
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
export interface ClaudeStreamEventMessage {
|
||||
type: "stream_event";
|
||||
event: ClaudeApiEvent;
|
||||
/** Present on sub-agent stream events; null/undefined for top-level events. */
|
||||
parent_tool_use_id?: string | null;
|
||||
}
|
||||
|
||||
export interface ClaudeResultMessage {
|
||||
|
||||
Reference in New Issue
Block a user