Skip to content

Commit 2201772

Browse files
authored
Update @runt/schema to commit 14c2c60 with type safety improvements (#204)
* Update @runt/schema to commit 14c2c60f5576eebc509ed76b5cde0c3cab4246a4 * Fix type safety issues with updated @runt/schema - Update RichOutput to use proper MediaContainer types - Fix AI tool output components to use correct schema field names - Remove unused imports and fix linter errors - Update output index to re-export schema types - All tests passing (58/58) * Use exported MIME type constants from @runt/schema - Replace raw strings with AI_TOOL_CALL_MIME_TYPE and AI_TOOL_RESULT_MIME_TYPE - Use TEXT_MIME_TYPES, APPLICATION_MIME_TYPES, IMAGE_MIME_TYPES arrays - Add support for Jupyter MIME types (Plotly, Vega-Lite, etc.) - Improve type safety and consistency with schema definitions * Fix AI tool result validation to handle actual runtime data structure - Use flexible validation instead of strict schema type guard - Handle actual data structure: {tool_call_id, result, status} - Remove unused imports - Tool results now display correctly with success/error icons
1 parent 1daf323 commit 2201772

7 files changed

Lines changed: 148 additions & 120 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@radix-ui/react-separator": "^1.1.7",
6161
"@radix-ui/react-slot": "^1.2.3",
6262
"@radix-ui/react-tooltip": "^1.1.6",
63-
"@runt/schema": "github:runtimed/runt#9d5449bdc2133079f6fd5097d7c3549e412ce5dd&path:/packages/schema",
63+
"@runt/schema": "github:runtimed/runt#14c2c60f5576eebc509ed76b5cde0c3cab4246a4&path:/packages/schema",
6464
"@tanstack/react-virtual": "^3.13.12",
6565
"@types/react-syntax-highlighter": "^15.5.13",
6666
"@uiw/codemirror-theme-github": "^4.23.14",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/notebook/RichOutput.tsx

Lines changed: 112 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import React, { Suspense } from "react";
22

33
import {
4-
AnsiStreamOutput,
5-
OutputData,
6-
ToolCallData,
7-
ToolResultData,
8-
} from "../outputs/index.js";
4+
MediaContainer,
5+
isInlineContainer,
6+
isArtifactContainer,
7+
isAiToolCallData,
8+
AI_TOOL_CALL_MIME_TYPE,
9+
AI_TOOL_RESULT_MIME_TYPE,
10+
TEXT_MIME_TYPES,
11+
APPLICATION_MIME_TYPES,
12+
IMAGE_MIME_TYPES,
13+
JUPYTER_MIME_TYPES,
14+
} from "@runt/schema";
15+
import { AnsiStreamOutput } from "../outputs/index.js";
916
import { AnsiErrorOutput } from "./AnsiOutput.js";
1017
import "../outputs/outputs.css";
1118

@@ -44,7 +51,7 @@ const PlainTextOutput = React.lazy(() =>
4451
);
4552

4653
interface RichOutputProps {
47-
data: Record<string, unknown>;
54+
data: string | Record<string, MediaContainer>;
4855
metadata?: Record<string, unknown>;
4956
outputType?:
5057
| "multimedia_display"
@@ -99,37 +106,31 @@ export const RichOutput: React.FC<RichOutputProps> = ({
99106
}
100107

101108
// Handle multimedia outputs (multimedia_display, multimedia_result)
102-
let outputData: OutputData;
109+
let outputData: Record<string, unknown> = {};
103110

104-
// Check if data contains representations (new format)
111+
// Check if data contains media containers (new format)
105112
if (data && typeof data === "object" && !Array.isArray(data)) {
106-
const potentialRepresentations = data as Record<string, any>;
107-
108-
// Check if this looks like representations (has MediaRepresentation structure)
109-
const hasRepresentations = Object.values(potentialRepresentations).some(
110-
(value: any) =>
111-
value &&
112-
typeof value === "object" &&
113-
(value.type === "inline" || value.type === "artifact")
113+
const potentialContainers = data as Record<string, MediaContainer>;
114+
115+
// Check if this looks like media containers
116+
const hasContainers = Object.values(potentialContainers).some(
117+
(value: any) => isInlineContainer(value) || isArtifactContainer(value)
114118
);
115119

116-
if (hasRepresentations) {
117-
// Convert from representations to rendering format
118-
outputData = {};
119-
for (const [mimeType, representation] of Object.entries(
120-
potentialRepresentations
121-
)) {
122-
if (
123-
representation &&
124-
typeof representation === "object" &&
125-
representation.data !== undefined
126-
) {
127-
outputData[mimeType] = representation.data;
120+
if (hasContainers) {
121+
// Convert from media containers to rendering format
122+
for (const [mimeType, container] of Object.entries(potentialContainers)) {
123+
if (isInlineContainer(container)) {
124+
outputData[mimeType] = container.data;
125+
} else if (isArtifactContainer(container)) {
126+
// For artifacts, we'll need to handle them differently
127+
// For now, just mark as artifact reference
128+
outputData[mimeType] = `[Artifact: ${container.artifactId}]`;
128129
}
129130
}
130131
} else {
131-
// Direct data format
132-
outputData = potentialRepresentations as OutputData;
132+
// Direct data format (legacy support)
133+
outputData = potentialContainers as Record<string, unknown>;
133134
}
134135
} else {
135136
// Fallback for simple data
@@ -139,16 +140,21 @@ export const RichOutput: React.FC<RichOutputProps> = ({
139140
// Determine the best media type to render, in order of preference
140141
const getPreferredMediaType = (): string | null => {
141142
const preferenceOrder = [
142-
"application/vnd.anode.aitool+json",
143-
"application/vnd.anode.aitool.result+json",
144-
"text/markdown",
145-
"text/html",
146-
"image/png",
147-
"image/jpeg",
148-
"image/svg+xml",
149-
"image/svg",
150-
"application/json",
151-
"text/plain",
143+
AI_TOOL_CALL_MIME_TYPE,
144+
AI_TOOL_RESULT_MIME_TYPE,
145+
// Jupyter rich formats (plots, widgets, etc.)
146+
...JUPYTER_MIME_TYPES,
147+
// Text formats
148+
TEXT_MIME_TYPES[2], // text/markdown
149+
TEXT_MIME_TYPES[1], // text/html
150+
// Images
151+
IMAGE_MIME_TYPES[0], // image/png
152+
IMAGE_MIME_TYPES[1], // image/jpeg
153+
IMAGE_MIME_TYPES[2], // image/svg+xml
154+
"image/svg", // legacy SVG format
155+
// Application formats
156+
APPLICATION_MIME_TYPES[0], // application/json
157+
TEXT_MIME_TYPES[0], // text/plain
152158
];
153159

154160
for (const mediaType of preferenceOrder) {
@@ -175,25 +181,39 @@ export const RichOutput: React.FC<RichOutputProps> = ({
175181

176182
const renderContent = () => {
177183
switch (mediaType) {
178-
case "application/vnd.anode.aitool+json":
179-
return (
180-
<Suspense fallback={<LoadingSpinner />}>
181-
<AiToolCallOutput
182-
toolData={outputData[mediaType] as ToolCallData}
183-
/>
184-
</Suspense>
185-
);
184+
case AI_TOOL_CALL_MIME_TYPE: {
185+
const toolData = outputData[mediaType];
186+
if (isAiToolCallData(toolData)) {
187+
return (
188+
<Suspense fallback={<LoadingSpinner />}>
189+
<AiToolCallOutput toolData={toolData} />
190+
</Suspense>
191+
);
192+
}
193+
return <div className="text-red-500">Invalid tool call data</div>;
194+
}
186195

187-
case "application/vnd.anode.aitool.result+json":
188-
return (
189-
<Suspense fallback={<LoadingSpinner />}>
190-
<AiToolResultOutput
191-
resultData={outputData[mediaType] as ToolResultData}
192-
/>
193-
</Suspense>
194-
);
196+
case AI_TOOL_RESULT_MIME_TYPE: {
197+
const resultData = outputData[mediaType];
198+
// Handle actual runtime data structure (more flexible than strict schema)
199+
if (
200+
resultData &&
201+
typeof resultData === "object" &&
202+
"tool_call_id" in resultData &&
203+
"status" in resultData &&
204+
typeof (resultData as any).tool_call_id === "string" &&
205+
typeof (resultData as any).status === "string"
206+
) {
207+
return (
208+
<Suspense fallback={<LoadingSpinner />}>
209+
<AiToolResultOutput resultData={resultData as any} />
210+
</Suspense>
211+
);
212+
}
213+
return <div className="text-red-500">Invalid tool result data</div>;
214+
}
195215

196-
case "text/markdown":
216+
case TEXT_MIME_TYPES[2]: // text/markdown
197217
return (
198218
<Suspense fallback={<LoadingSpinner />}>
199219
<MarkdownRenderer
@@ -203,15 +223,15 @@ export const RichOutput: React.FC<RichOutputProps> = ({
203223
</Suspense>
204224
);
205225

206-
case "text/html":
226+
case TEXT_MIME_TYPES[1]: // text/html
207227
return (
208228
<Suspense fallback={<LoadingSpinner />}>
209229
<HtmlOutput content={String(outputData[mediaType] || "")} />
210230
</Suspense>
211231
);
212232

213-
case "image/png":
214-
case "image/jpeg":
233+
case IMAGE_MIME_TYPES[0]: // image/png
234+
case IMAGE_MIME_TYPES[1]: // image/jpeg
215235
return (
216236
<Suspense fallback={<LoadingSpinner />}>
217237
<ImageOutput
@@ -221,22 +241,50 @@ export const RichOutput: React.FC<RichOutputProps> = ({
221241
</Suspense>
222242
);
223243

224-
case "image/svg+xml":
225-
case "image/svg":
244+
case IMAGE_MIME_TYPES[2]: // image/svg+xml
245+
case "image/svg": // legacy SVG format
226246
return (
227247
<Suspense fallback={<LoadingSpinner />}>
228248
<SvgOutput content={String(outputData[mediaType] || "")} />
229249
</Suspense>
230250
);
231251

232-
case "application/json":
252+
case "application/vnd.plotly.v1+json":
253+
return (
254+
<Suspense fallback={<LoadingSpinner />}>
255+
<JsonOutput data={outputData[mediaType]} />
256+
</Suspense>
257+
);
258+
259+
case "application/vnd.vegalite.v2+json":
260+
case "application/vnd.vegalite.v3+json":
261+
case "application/vnd.vegalite.v4+json":
262+
case "application/vnd.vegalite.v5+json":
263+
case "application/vnd.vegalite.v6+json":
264+
case "application/vnd.vega.v3+json":
265+
case "application/vnd.vega.v4+json":
266+
case "application/vnd.vega.v5+json":
267+
return (
268+
<Suspense fallback={<LoadingSpinner />}>
269+
<JsonOutput data={outputData[mediaType]} />
270+
</Suspense>
271+
);
272+
273+
case "application/geo+json":
274+
return (
275+
<Suspense fallback={<LoadingSpinner />}>
276+
<JsonOutput data={outputData[mediaType]} />
277+
</Suspense>
278+
);
279+
280+
case APPLICATION_MIME_TYPES[0]: // application/json
233281
return (
234282
<Suspense fallback={<LoadingSpinner />}>
235283
<JsonOutput data={outputData[mediaType]} />
236284
</Suspense>
237285
);
238286

239-
case "text/plain":
287+
case TEXT_MIME_TYPES[0]: // text/plain
240288
default:
241289
return (
242290
<Suspense fallback={<LoadingSpinner />}>

src/components/outputs/AiToolCallOutput.tsx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,14 @@ import React from "react";
22
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
33
import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
44
import { ChevronDown, Edit, FilePlus, Info } from "lucide-react";
5+
import { AiToolCallData } from "@runt/schema";
56

67
interface AiToolCallOutputProps {
7-
toolData: {
8-
tool_call_id: string;
9-
tool_name: string;
10-
arguments: Record<string, any>;
11-
status: "success" | "error";
12-
timestamp: string;
13-
execution_time_ms?: number;
14-
};
8+
toolData: AiToolCallData;
159
}
1610

1711
// Tool icon and action mapping for AI tools
18-
const getToolConfig = (toolName: string, status: "success" | "error") => {
12+
const getToolConfig = (toolName: string, status: string) => {
1913
const toolConfigs: Record<
2014
string,
2115
{
@@ -55,8 +49,9 @@ const getToolConfig = (toolName: string, status: "success" | "error") => {
5549
export const AiToolCallOutput: React.FC<AiToolCallOutputProps> = ({
5650
toolData,
5751
}) => {
58-
const isSuccess = toolData.status === "success";
59-
const toolConfig = getToolConfig(toolData.tool_name, toolData.status);
52+
// AiToolCallData doesn't have status, so we'll assume success for tool calls
53+
const isSuccess = true;
54+
const toolConfig = getToolConfig(toolData.tool_name, "success");
6055
const ToolIcon = toolConfig.icon;
6156

6257
return (
@@ -76,10 +71,7 @@ export const AiToolCallOutput: React.FC<AiToolCallOutputProps> = ({
7671
</summary>
7772
<div className="bg-card/30 border-border/50 mt-2 ml-6 rounded border p-3 text-xs">
7873
<div className="text-muted-foreground mb-2">
79-
{new Date(toolData.timestamp).toLocaleTimeString()}
80-
{toolData.execution_time_ms && (
81-
<span className="ml-2">({toolData.execution_time_ms}ms)</span>
82-
)}
74+
Tool Call ID: {toolData.tool_call_id}
8375
</div>
8476
<SyntaxHighlighter
8577
language="json"
@@ -104,11 +96,9 @@ export const AiToolCallOutput: React.FC<AiToolCallOutputProps> = ({
10496
<span className="text-muted-foreground">
10597
{toolConfig.displayVerb} {toolConfig.label}
10698
</span>
107-
{toolData.execution_time_ms && (
108-
<span className="text-muted-foreground text-xs">
109-
({toolData.execution_time_ms}ms)
110-
</span>
111-
)}
99+
<span className="text-muted-foreground text-xs">
100+
({toolData.tool_call_id})
101+
</span>
112102
</div>
113103
)}
114104
</div>

src/components/outputs/AiToolResultOutput.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ interface AiToolResultOutputProps {
66
tool_call_id: string;
77
result?: string;
88
status: string;
9+
// Optional fields from full schema
10+
tool_name?: string;
11+
arguments?: Record<string, unknown>;
12+
timestamp?: string;
913
};
1014
}
1115

@@ -53,6 +57,11 @@ export const AiToolResultOutput: React.FC<AiToolResultOutputProps> = ({
5357
{resultData.result}
5458
</div>
5559
)}
60+
{!resultData.result && (
61+
<div className="text-muted-foreground text-sm italic">
62+
{config.label}
63+
</div>
64+
)}
5665
</div>
5766
);
5867
};

0 commit comments

Comments
 (0)