inspect-ai 0.3.99__py3-none-any.whl → 0.3.101__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. inspect_ai/_cli/eval.py +2 -1
  2. inspect_ai/_display/core/config.py +11 -5
  3. inspect_ai/_display/core/panel.py +66 -2
  4. inspect_ai/_display/core/textual.py +5 -2
  5. inspect_ai/_display/plain/display.py +1 -0
  6. inspect_ai/_display/rich/display.py +2 -2
  7. inspect_ai/_display/textual/widgets/transcript.py +37 -9
  8. inspect_ai/_eval/eval.py +13 -1
  9. inspect_ai/_eval/evalset.py +3 -2
  10. inspect_ai/_eval/run.py +2 -0
  11. inspect_ai/_eval/score.py +2 -4
  12. inspect_ai/_eval/task/log.py +3 -1
  13. inspect_ai/_eval/task/run.py +59 -81
  14. inspect_ai/_util/content.py +11 -6
  15. inspect_ai/_util/interrupt.py +2 -2
  16. inspect_ai/_util/text.py +7 -0
  17. inspect_ai/_util/working.py +8 -37
  18. inspect_ai/_view/__init__.py +0 -0
  19. inspect_ai/_view/schema.py +2 -1
  20. inspect_ai/_view/www/CLAUDE.md +15 -0
  21. inspect_ai/_view/www/dist/assets/index.css +307 -171
  22. inspect_ai/_view/www/dist/assets/index.js +24733 -21641
  23. inspect_ai/_view/www/log-schema.json +77 -3
  24. inspect_ai/_view/www/package.json +9 -5
  25. inspect_ai/_view/www/src/@types/log.d.ts +9 -0
  26. inspect_ai/_view/www/src/app/App.tsx +1 -15
  27. inspect_ai/_view/www/src/app/appearance/icons.ts +4 -1
  28. inspect_ai/_view/www/src/app/content/MetaDataGrid.tsx +24 -6
  29. inspect_ai/_view/www/src/app/content/MetadataGrid.module.css +0 -5
  30. inspect_ai/_view/www/src/app/content/RenderedContent.tsx +220 -205
  31. inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +2 -1
  32. inspect_ai/_view/www/src/app/log-view/tabs/SamplesTab.tsx +5 -0
  33. inspect_ai/_view/www/src/app/log-view/tabs/grouping.ts +4 -4
  34. inspect_ai/_view/www/src/app/routing/navigationHooks.ts +22 -25
  35. inspect_ai/_view/www/src/app/routing/url.ts +84 -4
  36. inspect_ai/_view/www/src/app/samples/InlineSampleDisplay.module.css +0 -5
  37. inspect_ai/_view/www/src/app/samples/SampleDialog.module.css +1 -1
  38. inspect_ai/_view/www/src/app/samples/SampleDisplay.module.css +7 -0
  39. inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +24 -17
  40. inspect_ai/_view/www/src/app/samples/SampleSummaryView.module.css +1 -2
  41. inspect_ai/_view/www/src/app/samples/chat/ChatMessage.tsx +8 -6
  42. inspect_ai/_view/www/src/app/samples/chat/ChatMessageRow.tsx +0 -4
  43. inspect_ai/_view/www/src/app/samples/chat/ChatViewVirtualList.tsx +3 -2
  44. inspect_ai/_view/www/src/app/samples/chat/MessageContent.tsx +2 -0
  45. inspect_ai/_view/www/src/app/samples/chat/MessageContents.tsx +2 -0
  46. inspect_ai/_view/www/src/app/samples/chat/messages.ts +1 -0
  47. inspect_ai/_view/www/src/app/samples/chat/tools/ToolCallView.tsx +1 -0
  48. inspect_ai/_view/www/src/app/samples/list/SampleList.tsx +17 -5
  49. inspect_ai/_view/www/src/app/samples/list/SampleRow.tsx +1 -1
  50. inspect_ai/_view/www/src/app/samples/transcript/ErrorEventView.tsx +1 -2
  51. inspect_ai/_view/www/src/app/samples/transcript/InfoEventView.tsx +1 -1
  52. inspect_ai/_view/www/src/app/samples/transcript/InputEventView.tsx +1 -2
  53. inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.module.css +1 -1
  54. inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.tsx +1 -1
  55. inspect_ai/_view/www/src/app/samples/transcript/SampleInitEventView.tsx +1 -1
  56. inspect_ai/_view/www/src/app/samples/transcript/SampleLimitEventView.tsx +3 -2
  57. inspect_ai/_view/www/src/app/samples/transcript/SandboxEventView.tsx +4 -5
  58. inspect_ai/_view/www/src/app/samples/transcript/ScoreEventView.tsx +1 -1
  59. inspect_ai/_view/www/src/app/samples/transcript/SpanEventView.tsx +1 -2
  60. inspect_ai/_view/www/src/app/samples/transcript/StepEventView.tsx +1 -3
  61. inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +1 -2
  62. inspect_ai/_view/www/src/app/samples/transcript/ToolEventView.tsx +3 -4
  63. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.module.css +42 -0
  64. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +77 -0
  65. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualList.tsx +27 -71
  66. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.module.css +13 -3
  67. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.tsx +27 -2
  68. inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.module.css +1 -0
  69. inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.tsx +21 -22
  70. inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.module.css +45 -0
  71. inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.tsx +223 -0
  72. inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.module.css +10 -0
  73. inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.tsx +258 -0
  74. inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +187 -0
  75. inspect_ai/_view/www/src/app/samples/transcript/state/StateEventRenderers.tsx +8 -1
  76. inspect_ai/_view/www/src/app/samples/transcript/state/StateEventView.tsx +3 -4
  77. inspect_ai/_view/www/src/app/samples/transcript/transform/hooks.ts +78 -0
  78. inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +340 -135
  79. inspect_ai/_view/www/src/app/samples/transcript/transform/utils.ts +3 -0
  80. inspect_ai/_view/www/src/app/samples/transcript/types.ts +2 -0
  81. inspect_ai/_view/www/src/app/types.ts +5 -1
  82. inspect_ai/_view/www/src/client/api/api-browser.ts +2 -2
  83. inspect_ai/_view/www/src/components/LiveVirtualList.tsx +6 -1
  84. inspect_ai/_view/www/src/components/MarkdownDiv.tsx +1 -1
  85. inspect_ai/_view/www/src/components/PopOver.tsx +422 -0
  86. inspect_ai/_view/www/src/components/PulsingDots.module.css +9 -9
  87. inspect_ai/_view/www/src/components/PulsingDots.tsx +4 -1
  88. inspect_ai/_view/www/src/components/StickyScroll.tsx +183 -0
  89. inspect_ai/_view/www/src/components/TabSet.tsx +4 -0
  90. inspect_ai/_view/www/src/state/hooks.ts +52 -2
  91. inspect_ai/_view/www/src/state/logSlice.ts +4 -3
  92. inspect_ai/_view/www/src/state/samplePolling.ts +8 -0
  93. inspect_ai/_view/www/src/state/sampleSlice.ts +53 -9
  94. inspect_ai/_view/www/src/state/scrolling.ts +152 -0
  95. inspect_ai/_view/www/src/utils/attachments.ts +7 -0
  96. inspect_ai/_view/www/src/utils/python.ts +18 -0
  97. inspect_ai/_view/www/yarn.lock +290 -33
  98. inspect_ai/agent/_react.py +12 -7
  99. inspect_ai/agent/_run.py +2 -3
  100. inspect_ai/analysis/beta/__init__.py +2 -0
  101. inspect_ai/analysis/beta/_dataframe/samples/table.py +19 -18
  102. inspect_ai/dataset/_sources/csv.py +2 -6
  103. inspect_ai/dataset/_sources/hf.py +2 -6
  104. inspect_ai/dataset/_sources/json.py +2 -6
  105. inspect_ai/dataset/_util.py +23 -0
  106. inspect_ai/log/_log.py +1 -1
  107. inspect_ai/log/_recorders/eval.py +4 -3
  108. inspect_ai/log/_recorders/file.py +2 -9
  109. inspect_ai/log/_recorders/json.py +1 -0
  110. inspect_ai/log/_recorders/recorder.py +1 -0
  111. inspect_ai/log/_transcript.py +1 -1
  112. inspect_ai/model/_call_tools.py +6 -2
  113. inspect_ai/model/_openai.py +1 -1
  114. inspect_ai/model/_openai_responses.py +85 -41
  115. inspect_ai/model/_openai_web_search.py +38 -0
  116. inspect_ai/model/_providers/azureai.py +72 -3
  117. inspect_ai/model/_providers/openai.py +4 -1
  118. inspect_ai/model/_providers/openai_responses.py +5 -1
  119. inspect_ai/scorer/_metric.py +1 -2
  120. inspect_ai/scorer/_reducer/reducer.py +1 -1
  121. inspect_ai/solver/_task_state.py +2 -2
  122. inspect_ai/tool/_tool.py +6 -2
  123. inspect_ai/tool/_tool_def.py +27 -4
  124. inspect_ai/tool/_tool_info.py +2 -0
  125. inspect_ai/tool/_tools/_web_search/_google.py +43 -15
  126. inspect_ai/tool/_tools/_web_search/_tavily.py +46 -13
  127. inspect_ai/tool/_tools/_web_search/_web_search.py +214 -45
  128. inspect_ai/util/__init__.py +4 -0
  129. inspect_ai/util/_json.py +3 -0
  130. inspect_ai/util/_limit.py +230 -20
  131. inspect_ai/util/_sandbox/docker/compose.py +20 -11
  132. inspect_ai/util/_span.py +1 -1
  133. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/METADATA +3 -3
  134. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/RECORD +138 -124
  135. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/WHEEL +1 -1
  136. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/entry_points.txt +0 -0
  137. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/licenses/LICENSE +0 -0
  138. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,223 @@
1
+ import clsx from "clsx";
2
+ import { FC, ReactNode, useRef } from "react";
3
+ import { Link } from "react-router-dom";
4
+ import { PopOver } from "../../../../components/PopOver";
5
+ import { PulsingDots } from "../../../../components/PulsingDots";
6
+ import {
7
+ useCollapseSampleEvent,
8
+ useSamplePopover,
9
+ } from "../../../../state/hooks";
10
+ import { formatDateTime, formatTime } from "../../../../utils/format";
11
+ import { parsePackageName } from "../../../../utils/python";
12
+ import { ApplicationIcons } from "../../../appearance/icons";
13
+ import { MetaDataGrid } from "../../../content/MetaDataGrid";
14
+ import { useSampleEventUrl } from "../../../routing/url";
15
+ import { kSandboxSignalName } from "../transform/fixups";
16
+ import { EventNode } from "../types";
17
+ import styles from "./OutlineRow.module.css";
18
+
19
+ export interface OutlineRowProps {
20
+ node: EventNode;
21
+ collapseScope: string;
22
+ running?: boolean;
23
+ selected?: boolean;
24
+ }
25
+
26
+ export const OutlineRow: FC<OutlineRowProps> = ({
27
+ node,
28
+ collapseScope,
29
+ running,
30
+ selected,
31
+ }) => {
32
+ const [collapsed, setCollapsed] = useCollapseSampleEvent(
33
+ collapseScope,
34
+ node.id,
35
+ );
36
+ const icon = iconForNode(node);
37
+ const toggle = toggleIcon(node, collapsed);
38
+
39
+ const popoverId = `${node.id}-popover`;
40
+ const { isShowing } = useSamplePopover(popoverId);
41
+
42
+ const ref = useRef(null);
43
+
44
+ // Get all URL parameters at component level
45
+ const sampleEventUrl = useSampleEventUrl(node.id);
46
+
47
+ return (
48
+ <>
49
+ <div
50
+ className={clsx(
51
+ styles.eventRow,
52
+ "text-size-smallest",
53
+ selected ? styles.selected : "",
54
+ )}
55
+ style={{ paddingLeft: `${node.depth * 0.4}em` }}
56
+ >
57
+ <div
58
+ className={clsx(styles.toggle)}
59
+ onClick={() => {
60
+ setCollapsed(!collapsed);
61
+ }}
62
+ >
63
+ {toggle ? <i className={clsx(toggle)} /> : undefined}
64
+ </div>
65
+ <div className={clsx(styles.label)} data-depth={node.depth}>
66
+ {icon ? <i className={clsx(icon, styles.icon)} /> : undefined}
67
+ {sampleEventUrl ? (
68
+ <Link
69
+ to={sampleEventUrl}
70
+ className={clsx(styles.eventLink)}
71
+ ref={ref}
72
+ >
73
+ {parsePackageName(labelForNode(node)).module}
74
+ </Link>
75
+ ) : (
76
+ <span ref={ref}>{parsePackageName(labelForNode(node)).module}</span>
77
+ )}
78
+ {running ? (
79
+ <PulsingDots
80
+ size="small"
81
+ className={clsx(styles.progress)}
82
+ subtle={false}
83
+ />
84
+ ) : undefined}
85
+ </div>
86
+ </div>
87
+ <PopOver
88
+ id={`${node.id}-popover`}
89
+ positionEl={ref.current}
90
+ isOpen={isShowing}
91
+ className={clsx(styles.popper)}
92
+ placement="auto-end"
93
+ >
94
+ {summarizeNode(node)}
95
+ </PopOver>
96
+ </>
97
+ );
98
+ };
99
+
100
+ const toggleIcon = (
101
+ node: EventNode,
102
+ collapsed: boolean,
103
+ ): string | undefined => {
104
+ if (node.children.length > 0) {
105
+ return collapsed
106
+ ? ApplicationIcons.chevron.right
107
+ : ApplicationIcons.chevron.down;
108
+ }
109
+ };
110
+
111
+ const iconForNode = (node: EventNode): string | undefined => {
112
+ switch (node.event.event) {
113
+ case "sample_limit":
114
+ return ApplicationIcons.limits.custom;
115
+
116
+ case "score":
117
+ return ApplicationIcons.scorer;
118
+
119
+ case "error":
120
+ return ApplicationIcons.error;
121
+ }
122
+ };
123
+
124
+ const labelForNode = (node: EventNode): string => {
125
+ if (node.event.event === "span_begin") {
126
+ switch (node.event.type) {
127
+ case "solver":
128
+ return node.event.name;
129
+ case "tool":
130
+ return node.event.name;
131
+ default: {
132
+ if (node.event.name === kSandboxSignalName) {
133
+ return "sandbox events";
134
+ }
135
+ return node.event.name;
136
+ }
137
+ }
138
+ } else {
139
+ switch (node.event.event) {
140
+ case "subtask":
141
+ return node.event.name;
142
+ case "approval":
143
+ switch (node.event.decision) {
144
+ case "approve":
145
+ return "approved";
146
+ case "reject":
147
+ return "rejected";
148
+ case "escalate":
149
+ return "escalated";
150
+ case "modify":
151
+ return "modified";
152
+ case "terminate":
153
+ return "terminated";
154
+ default:
155
+ return node.event.decision;
156
+ }
157
+ case "model":
158
+ return `model${node.event.role ? ` (${node.event.role})` : ""}`;
159
+ case "score":
160
+ return "scoring";
161
+ case "step":
162
+ if (node.event.name === kSandboxSignalName) {
163
+ return "sandbox events";
164
+ }
165
+ return node.event.name;
166
+
167
+ default:
168
+ return node.event.event;
169
+ }
170
+ }
171
+ };
172
+
173
+ export const summarizeNode = (node: EventNode): ReactNode => {
174
+ let entries: Record<string, unknown> = {};
175
+ switch (node.event.event) {
176
+ case "sample_init":
177
+ entries = {
178
+ sample_id: node.event.sample.id,
179
+ sandbox: node.event.sample.sandbox?.type,
180
+ started: formatDateTime(new Date(node.event.timestamp)),
181
+ working_start: formatTime(node.event.working_start),
182
+ };
183
+ break;
184
+
185
+ case "sample_limit":
186
+ entries = {
187
+ type: node.event.type,
188
+ message: node.event.message,
189
+ limit: node.event.limit,
190
+ started: formatDateTime(new Date(node.event.timestamp)),
191
+ working_start: formatTime(node.event.working_start),
192
+ };
193
+ break;
194
+ case "score":
195
+ entries = {
196
+ answer: node.event.score.answer,
197
+ score: node.event.score.value,
198
+ started: formatDateTime(new Date(node.event.timestamp)),
199
+ working_start: formatTime(node.event.working_start),
200
+ };
201
+ break;
202
+ case "span_begin":
203
+ entries = {
204
+ name: node.event.name,
205
+ started: formatDateTime(new Date(node.event.timestamp)),
206
+ working_start: formatTime(node.event.working_start),
207
+ };
208
+ break;
209
+ default:
210
+ entries = {
211
+ started: formatDateTime(new Date(node.event.timestamp)),
212
+ working_start: formatTime(node.event.working_start),
213
+ };
214
+ }
215
+
216
+ return (
217
+ <MetaDataGrid
218
+ entries={entries}
219
+ size="mini"
220
+ className={clsx(styles.popover, "text-size-smallest")}
221
+ />
222
+ );
223
+ };
@@ -0,0 +1,10 @@
1
+ .node {
2
+ display: grid;
3
+ column-gap: 0.3em;
4
+ grid-template-columns: max-content 1fr;
5
+ }
6
+
7
+ .panel {
8
+ margin-top: 0.65rem;
9
+ overflow: visible;
10
+ }
@@ -0,0 +1,258 @@
1
+ import {
2
+ CSSProperties,
3
+ FC,
4
+ RefObject,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ } from "react";
10
+
11
+ import { EventNode } from "../types";
12
+
13
+ import clsx from "clsx";
14
+ import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
15
+ import { useScrollTrack, useVirtuosoState } from "../../../../state/scrolling";
16
+ import { useStore } from "../../../../state/store";
17
+ import { flatTree } from "../transform/treeify";
18
+
19
+ import { useSampleDetailNavigation } from "../../../routing/navigationHooks";
20
+ import { kSandboxSignalName } from "../transform/fixups";
21
+ import { OutlineRow } from "./OutlineRow";
22
+ import styles from "./TranscriptOutline.module.css";
23
+ import {
24
+ collapseScoring,
25
+ collapseTurns,
26
+ makeTurns,
27
+ noScorerChildren,
28
+ removeNodeVisitor,
29
+ removeStepSpanNameVisitor,
30
+ } from "./tree-visitors";
31
+
32
+ const kCollapseScope = "transcript-outline";
33
+ const kFramesToStabilize = 10;
34
+
35
+ interface TranscriptOutlineProps {
36
+ eventNodes: EventNode[];
37
+ defaultCollapsedIds: Record<string, boolean>;
38
+ running?: boolean;
39
+ className?: string | string[];
40
+ scrollRef?: RefObject<HTMLDivElement | null>;
41
+ style?: CSSProperties;
42
+ }
43
+
44
+ // hack: add a padding node to the end of the list so
45
+ // when the tree is positioned at the bottom of the viewport
46
+ // it has some breathing room
47
+ const EventPaddingNode: EventNode = {
48
+ id: "padding",
49
+ event: {
50
+ event: "info",
51
+ source: "",
52
+ data: "",
53
+ timestamp: "",
54
+ pending: false,
55
+ working_start: 0,
56
+ span_id: null,
57
+ },
58
+ depth: 0,
59
+ children: [],
60
+ };
61
+
62
+ export const TranscriptOutline: FC<TranscriptOutlineProps> = ({
63
+ eventNodes,
64
+ defaultCollapsedIds,
65
+ running,
66
+ className,
67
+ scrollRef,
68
+ style,
69
+ }) => {
70
+ const id = "transcript-tree";
71
+ // The virtual list handle and state
72
+ const listHandle = useRef<VirtuosoHandle | null>(null);
73
+ const { getRestoreState } = useVirtuosoState(listHandle, id);
74
+
75
+ // Collapse state
76
+ // The list of events that have been collapsed
77
+ const collapsedEvents = useStore((state) => state.sample.collapsedEvents);
78
+ const setCollapsedEvents = useStore(
79
+ (state) => state.sampleActions.setCollapsedEvents,
80
+ );
81
+
82
+ const selectedOutlineId = useStore((state) => state.sample.selectedOutlineId);
83
+ const setSelectedOutlineId = useStore(
84
+ (state) => state.sampleActions.setSelectedOutlineId,
85
+ );
86
+ const sampleDetailNavigation = useSampleDetailNavigation();
87
+
88
+ // Flag to indicate programmatic scrolling is in progress
89
+ const isProgrammaticScrolling = useRef(false);
90
+ // Last position to check for scroll stabilization
91
+ const lastScrollPosition = useRef<number | null>(null);
92
+ // Frame count for detecting scroll stabilization
93
+ const stableFrameCount = useRef(0);
94
+
95
+ useEffect(() => {
96
+ if (sampleDetailNavigation.event) {
97
+ // Set the flag to indicate we're in programmatic scrolling
98
+ isProgrammaticScrolling.current = true;
99
+ lastScrollPosition.current = null;
100
+ stableFrameCount.current = 0;
101
+
102
+ setSelectedOutlineId(sampleDetailNavigation.event);
103
+
104
+ // Start monitoring to detect when scrolling has stabilized
105
+ const checkScrollStabilized = () => {
106
+ if (!isProgrammaticScrolling.current) return;
107
+
108
+ const currentPosition = scrollRef?.current?.scrollTop ?? null;
109
+
110
+ if (currentPosition === lastScrollPosition.current) {
111
+ stableFrameCount.current++;
112
+
113
+ // If position has been stable for a few frames, consider scrolling complete
114
+ if (stableFrameCount.current >= kFramesToStabilize) {
115
+ isProgrammaticScrolling.current = false;
116
+ return;
117
+ }
118
+ } else {
119
+ // Reset stability counter if position changed
120
+ stableFrameCount.current = 0;
121
+ lastScrollPosition.current = currentPosition;
122
+ }
123
+
124
+ // Continue checking until scrolling stabilizes
125
+ requestAnimationFrame(checkScrollStabilized);
126
+ };
127
+
128
+ // Start the RAF loop to detect scroll stabilization
129
+ requestAnimationFrame(checkScrollStabilized);
130
+ }
131
+ }, [sampleDetailNavigation.event, setSelectedOutlineId, scrollRef]);
132
+
133
+ const outlineNodeList = useMemo(() => {
134
+ // flattten the event tree
135
+ const nodeList = flatTree(
136
+ eventNodes,
137
+ (collapsedEvents ? collapsedEvents[kCollapseScope] : undefined) ||
138
+ defaultCollapsedIds,
139
+ [
140
+ // Strip specific nodes
141
+ removeNodeVisitor("logger"),
142
+ removeNodeVisitor("info"),
143
+ removeNodeVisitor("state"),
144
+ removeNodeVisitor("store"),
145
+ removeNodeVisitor("approval"),
146
+ removeNodeVisitor("input"),
147
+ removeNodeVisitor("sandbox"),
148
+
149
+ // Strip the sandbox wrapper (and children)
150
+ removeStepSpanNameVisitor(kSandboxSignalName),
151
+
152
+ // Remove child events for scorers
153
+ noScorerChildren(),
154
+ ],
155
+ );
156
+
157
+ return collapseScoring(collapseTurns(makeTurns(nodeList)));
158
+ }, [eventNodes, collapsedEvents, defaultCollapsedIds]);
159
+
160
+ // Event node, for scroll tracking
161
+ const allNodesList = useMemo(() => {
162
+ return flatTree(eventNodes, null);
163
+ }, [eventNodes]);
164
+
165
+ const elementIds = allNodesList.map((node) => node.id);
166
+ const findNearestOutlineAbove = useCallback(
167
+ (targetId: string): EventNode | null => {
168
+ const targetIndex = allNodesList.findIndex(
169
+ (node) => node.id === targetId,
170
+ );
171
+ if (targetIndex === -1) return null;
172
+
173
+ const outlineIds = new Set(outlineNodeList.map((node) => node.id));
174
+
175
+ // Search backwards from target position (inclusive)
176
+ for (let i = targetIndex; i >= 0; i--) {
177
+ if (outlineIds.has(allNodesList[i].id)) {
178
+ return allNodesList[i];
179
+ }
180
+ }
181
+
182
+ return null;
183
+ },
184
+ [allNodesList, outlineNodeList],
185
+ );
186
+
187
+ useScrollTrack(
188
+ elementIds,
189
+ (id: string) => {
190
+ if (!isProgrammaticScrolling.current) {
191
+ // If the ID is not in the list, return
192
+ const parentNode = findNearestOutlineAbove(id);
193
+ if (parentNode) {
194
+ setSelectedOutlineId(parentNode.id);
195
+ }
196
+ }
197
+ },
198
+ scrollRef,
199
+ );
200
+
201
+ // Update the collapsed events when the default collapsed IDs change
202
+ // This effect only depends on defaultCollapsedIds, not eventNodes
203
+ useEffect(() => {
204
+ // Only initialize collapsedEvents if it's empty
205
+ if (!collapsedEvents && Object.keys(defaultCollapsedIds).length > 0) {
206
+ setCollapsedEvents(kCollapseScope, defaultCollapsedIds);
207
+ }
208
+ }, [defaultCollapsedIds, collapsedEvents, setCollapsedEvents]);
209
+
210
+ const renderRow = useCallback(
211
+ (index: number, node: EventNode) => {
212
+ if (node === EventPaddingNode) {
213
+ return (
214
+ <div
215
+ className={styles.eventPadding}
216
+ key={node.id}
217
+ style={{ height: "2em" }}
218
+ ></div>
219
+ );
220
+ } else {
221
+ return (
222
+ <OutlineRow
223
+ collapseScope={kCollapseScope}
224
+ node={node}
225
+ key={node.id}
226
+ running={running && index === outlineNodeList.length - 1}
227
+ selected={
228
+ selectedOutlineId ? selectedOutlineId === node.id : index === 0
229
+ }
230
+ />
231
+ );
232
+ }
233
+ },
234
+ [outlineNodeList, running, selectedOutlineId],
235
+ );
236
+
237
+ return (
238
+ <Virtuoso
239
+ ref={listHandle}
240
+ customScrollParent={scrollRef?.current ? scrollRef.current : undefined}
241
+ id={id}
242
+ style={{ ...style }}
243
+ data={[...outlineNodeList, EventPaddingNode]}
244
+ defaultItemHeight={50}
245
+ itemContent={renderRow}
246
+ atBottomThreshold={30}
247
+ increaseViewportBy={{ top: 300, bottom: 300 }}
248
+ overscan={{
249
+ main: 10,
250
+ reverse: 10,
251
+ }}
252
+ className={clsx(className, "transcript-outline")}
253
+ skipAnimationFrameInResizeObserver={true}
254
+ restoreStateFrom={getRestoreState()}
255
+ tabIndex={0}
256
+ />
257
+ );
258
+ };
@@ -0,0 +1,187 @@
1
+ import { ScoreEvent, SpanBeginEvent } from "../../../../@types/log";
2
+ import { TYPE_SCORER, TYPE_SCORERS } from "../transform/utils";
3
+ import { EventNode } from "../types";
4
+
5
+ const kTurnType = "turn";
6
+ const kTurnsType = "turns";
7
+ const kCollapsedScoring = "scorings";
8
+
9
+ // Visitors are used to transform the event tree
10
+ export const removeNodeVisitor = (event: string) => {
11
+ return {
12
+ visit: (node: EventNode): EventNode[] => {
13
+ if (node.event.event === event) {
14
+ return [];
15
+ }
16
+ return [node];
17
+ },
18
+ };
19
+ };
20
+
21
+ export const removeStepSpanNameVisitor = (name: string) => {
22
+ return {
23
+ visit: (node: EventNode): EventNode[] => {
24
+ if (
25
+ (node.event.event === "step" || node.event.event === "span_begin") &&
26
+ node.event.name === name
27
+ ) {
28
+ return [];
29
+ }
30
+ return [node];
31
+ },
32
+ };
33
+ };
34
+
35
+ export const noScorerChildren = () => {
36
+ let inScorers = false;
37
+ let inScorer = false;
38
+ let currentDepth = -1;
39
+ return {
40
+ visit: (node: EventNode): EventNode[] => {
41
+ // Note once we're in the scorers span
42
+ if (
43
+ node.event.event === "span_begin" &&
44
+ node.event.type === TYPE_SCORERS
45
+ ) {
46
+ inScorers = true;
47
+ return [node];
48
+ }
49
+
50
+ if (
51
+ (node.event.event === "step" || node.event.event === "span_begin") &&
52
+ node.event.type === TYPE_SCORER
53
+ ) {
54
+ inScorer = true;
55
+ currentDepth = node.depth;
56
+ return [node];
57
+ }
58
+
59
+ if (inScorers && inScorer && node.depth === currentDepth + 1) {
60
+ return [];
61
+ }
62
+ return [node];
63
+ },
64
+ };
65
+ };
66
+
67
+ export const makeTurns = (eventNodes: EventNode[]): EventNode[] => {
68
+ const results: EventNode[] = [];
69
+ let modelNode: EventNode | null = null;
70
+ const toolNodes: EventNode[] = [];
71
+ let turnCount = 1;
72
+
73
+ const makeTurn = (force?: boolean) => {
74
+ if (modelNode !== null && (force || toolNodes.length > 0)) {
75
+ // Create a new "turn" node based on the model event
76
+ const turnNode = new EventNode(
77
+ modelNode.id,
78
+ {
79
+ id: modelNode.id,
80
+ event: "span_begin",
81
+ type: kTurnType,
82
+ name: `turn ${turnCount++}`,
83
+ pending: false,
84
+ working_start: modelNode.event.working_start,
85
+ timestamp: modelNode.event.timestamp,
86
+ parent_id: null,
87
+ span_id: modelNode.event.span_id,
88
+ },
89
+ modelNode.depth,
90
+ );
91
+
92
+ // Add the original model event and tool events as children
93
+ turnNode.children = [modelNode, ...toolNodes];
94
+ results.push(turnNode);
95
+ }
96
+ modelNode = null;
97
+ toolNodes.length = 0;
98
+ };
99
+
100
+ for (const node of eventNodes) {
101
+ if (node.event.event === "model") {
102
+ if (modelNode !== null && toolNodes.length === 0) {
103
+ // back to back model calls are considered a single turn
104
+ makeTurn(true);
105
+ } else {
106
+ makeTurn();
107
+ modelNode = node;
108
+ }
109
+ } else if (node.event.event === "tool") {
110
+ toolNodes.push(node);
111
+ } else {
112
+ makeTurn();
113
+ results.push(node);
114
+ }
115
+ }
116
+ makeTurn();
117
+
118
+ return results;
119
+ };
120
+
121
+ export const collapseTurns = (eventNodes: EventNode[]): EventNode[] => {
122
+ const results: EventNode[] = [];
123
+ const collecting: EventNode[] = [];
124
+ const collect = () => {
125
+ if (collecting.length > 0) {
126
+ const numberOfTurns = collecting.length;
127
+ const firstTurn = collecting[0];
128
+ const turnNode = new EventNode(
129
+ firstTurn.id,
130
+ {
131
+ ...(firstTurn.event as SpanBeginEvent),
132
+ name: `${numberOfTurns} ${numberOfTurns === 1 ? "turn" : "turns"}`,
133
+ type: kTurnsType,
134
+ },
135
+ firstTurn.depth,
136
+ );
137
+ results.push(turnNode);
138
+ collecting.length = 0;
139
+ }
140
+ };
141
+
142
+ for (const node of eventNodes) {
143
+ if (node.event.event === "span_begin" && node.event.type === kTurnType) {
144
+ collecting.push(node);
145
+ } else {
146
+ collect();
147
+ results.push(node);
148
+ }
149
+ }
150
+ // Handle any remaining collected turns
151
+ collect();
152
+ return results;
153
+ };
154
+
155
+ export const collapseScoring = (eventNodes: EventNode[]): EventNode[] => {
156
+ const results: EventNode[] = [];
157
+ const collecting: EventNode[] = [];
158
+ const collect = () => {
159
+ if (collecting.length > 0) {
160
+ const firstScore = collecting[0];
161
+ const turnNode = new EventNode(
162
+ firstScore.id,
163
+ {
164
+ ...(firstScore.event as ScoreEvent),
165
+ name: "scoring",
166
+ type: kCollapsedScoring,
167
+ },
168
+ firstScore.depth,
169
+ );
170
+ results.push(turnNode);
171
+ collecting.length = 0;
172
+ }
173
+ };
174
+
175
+ for (const node of eventNodes) {
176
+ if (node.event.event === "score") {
177
+ collecting.push(node);
178
+ } else {
179
+ collect();
180
+ results.push(node);
181
+ }
182
+ }
183
+
184
+ // Handle any remaining collected turns
185
+ collect();
186
+ return results;
187
+ };
@@ -35,6 +35,9 @@ const system_msg_added_sig: ChangeType = {
35
35
  render: (_changes, resolvedState) => {
36
36
  const messages = resolvedState["messages"] as Array<unknown>;
37
37
  const message = messages[0];
38
+ if (typeof message !== "object" || !message) {
39
+ return <></>;
40
+ }
38
41
  return (
39
42
  <ChatView
40
43
  key="system_msg_event_preview"
@@ -172,7 +175,11 @@ const renderTools = (
172
175
  return change.path.startsWith("/tool_choice");
173
176
  });
174
177
  if (resolvedState.tool_choice && hasToolChoice) {
175
- toolsInfo["Tool Choice"] = toolName(resolvedState.tool_choice);
178
+ toolsInfo["Tool Choice"] = (
179
+ <span className={clsx("text-size-smaller")}>
180
+ {toolName(resolvedState.tool_choice)}
181
+ </span>
182
+ );
176
183
  }
177
184
 
178
185
  // Show either all tools or just the specific tools