Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
868 changes: 91 additions & 777 deletions src/agent_env/explorer/ui/src/components/task-instance-viewer.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
// (byte range), resolved to `/trajectory-image` for lazy loading. Small trajectories carry raw base64.
const SCREENSHOT_PLACEHOLDER_PREFIX = '__AEHIMG__';

// The trajectory's S3 URI — lets descendants build lazy-image URLs without
// The trajectory's object URL — lets descendants build lazy-image URLs without
// prop-drilling through every nested component.
const ScreenshotBaseUriContext = createContext<string | undefined>(undefined);

Expand Down Expand Up @@ -1802,7 +1802,7 @@ interface TrajectoryViewerProps {
trajectory: ParsedTrajectory;
timelineScrollRef?: React.MutableRefObject<HTMLDivElement | null>;
isInProgress?: boolean;
// S3 URI of this trajectory; only used to lazy-load screenshot-trimmed frames.
// Object URL of this trajectory; only used to lazy-load screenshot-trimmed frames.
screenshotBaseUri?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export function UniverseDetailPage({
version?: number;
content_type?: string;
object_url?: string;
s3_url?: string;
}[];
const metadataRefs = (artifact?.metadata_refs ?? null) as Record<
string,
Expand Down Expand Up @@ -240,9 +239,9 @@ export function UniverseDetailPage({
{f.content_type ? ` · ${f.content_type}` : ''}
</span>
</div>
{(f.object_url ?? f.s3_url) && (
{f.object_url && (
<a
href={objectContentUrl((f.object_url ?? f.s3_url) as string)}
href={objectContentUrl(f.object_url)}
download={f.filename}
className="shrink-0 inline-flex items-center gap-1 px-2 py-1 rounded text-xs text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--accent)] transition-colors"
title={`Download ${f.filename}`}
Expand Down
54 changes: 6 additions & 48 deletions src/agent_env/explorer/ui/src/lib/bar-downloads.smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,13 @@ function main(): void {
{
const downloads = buildBarDownloads(
[{ agent_trajectory_s3_uri: 'obj://t/one', step_id: 'run-solver' }],
undefined,
);
assert(downloads.length === 1, 'single trajectory yields one download');
assert(
downloads[0]?.label === 'Trajectory',
'single trajectory drops the step-name suffix',
);
assert(downloads[0]?.kind === 'trajectory', 'tagged as a trajectory');
assert(downloads[0]?.s3Uri === 'obj://t/one', 'carries the trajectory URI');
assert(downloads[0]?.objectUrl === 'obj://t/one', 'carries the trajectory URL');
assert(
!shouldCollapseBarDownloads(downloads.length),
'one download stays inline',
Expand All @@ -53,7 +51,6 @@ function main(): void {
{ agent_trajectory_s3_uri: 'obj://t/2', prompt_id: 'legacy-prompt' },
{ agent_trajectory_s3_uri: 'obj://t/3' },
],
undefined,
);
assert(
downloads.map(d => d.label).join(',') ===
Expand All @@ -73,7 +70,6 @@ function main(): void {
{ agent_trajectory_s3_uri: 'obj://t/a', prompt_id: null },
{ agent_trajectory_s3_uri: 'obj://t/b', prompt_id: null },
],
undefined,
);
assert(only?.label === 'Trajectory 1', 'null prompt_id falls back to position');
}
Expand All @@ -85,7 +81,6 @@ function main(): void {
{ step_id: 'no-trajectory' },
{ agent_trajectory_s3_uri: 'obj://t/only', step_id: 'has-one' },
],
undefined,
);
assert(downloads.length === 1, 'drops responses without a trajectory URI');
assert(
Expand All @@ -101,7 +96,6 @@ function main(): void {
{ agent_trajectory_s3_uri: 'obj://t/1', step_id: 'a', model: 'opus-4.8' },
{ agent_trajectory_s3_uri: 'obj://t/2', step_id: 'b' },
],
undefined,
);
assert(entry?.label === 'a', 'model is kept out of the label');
assert(
Expand All @@ -110,44 +104,9 @@ function main(): void {
);
}

// Results are appended last, and count toward the collapse threshold.
{
const downloads = buildBarDownloads(
[
{ agent_trajectory_s3_uri: 'obj://t/1', step_id: 'a' },
{ agent_trajectory_s3_uri: 'obj://t/2', step_id: 'b' },
],
'obj://results/run.json',
);
assert(downloads.length === 3, 'results appended to the trajectories');
const last = downloads[downloads.length - 1];
assert(last?.kind === 'results', 'results entry comes last');
assert(last?.label === 'Results', 'results entry is labelled "Results"');
assert(
last?.key === 'results:obj://results/run.json',
'results key is namespaced so it cannot collide with a trajectory URI',
);
assert(
shouldCollapseBarDownloads(downloads.length),
'two trajectories plus results collapses',
);
}

// No results URI → no results entry.
{
const downloads = buildBarDownloads(
[{ agent_trajectory_s3_uri: 'obj://t/1', step_id: 'a' }],
undefined,
);
assert(
downloads.every(d => d.kind === 'trajectory'),
'absent results URI adds no entry',
);
}

// A run with no downloads at all renders an empty bar, not a dropdown.
{
const downloads = buildBarDownloads([], undefined);
const downloads = buildBarDownloads([]);
assert(downloads.length === 0, 'no responses yields no downloads');
assert(!shouldCollapseBarDownloads(0), 'empty set does not collapse');
}
Expand All @@ -164,14 +123,14 @@ function main(): void {
);
}

// A task with many steps + results.
// A task with many steps.
{
const many = Array.from({ length: 12 }, (_, i) => ({
agent_trajectory_s3_uri: `obj://bucket/${i}`,
step_id: `step-${i}`,
}));
const downloads = buildBarDownloads(many, 'obj://bucket/results.json');
assert(downloads.length === 13, 'all 12 trajectories plus results kept');
const downloads = buildBarDownloads(many);
assert(downloads.length === 12, 'all 12 trajectories kept');
assert(
shouldCollapseBarDownloads(downloads.length),
'a many-step task collapses into a dropdown',
Expand All @@ -190,10 +149,9 @@ function main(): void {
{ agent_trajectory_s3_uri: 'obj://t/2', step_id: 'b' },
{ agent_trajectory_s3_uri: null, agent_trajectory_object_url: 'obj://t/3', step_id: 'c' },
],
undefined,
);
assert(
downloads.map(d => d.s3Uri).join(',') === 'obj://t/1,obj://t/2,obj://t/3',
downloads.map(d => d.objectUrl).join(',') === 'obj://t/1,obj://t/2,obj://t/3',
'a trajectory under either key is downloadable',
);
}
Expand Down
30 changes: 7 additions & 23 deletions src/agent_env/explorer/ui/src/lib/bar-downloads.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/** Download entries for the task-instance tab bar. Every prompt_agent step with `trajectory_output_prefix`
* uploads its own trajectory; the bar exposes one per trajectory (the last/solver step is usually most
* interesting), plus the verifier results file. Past MAX_INLINE_BAR_DOWNLOADS the caller collapses them into a
* dropdown. Built here (not inline) so the row and dropdown share entries/order and the labels are testable. */
* interesting). Past MAX_INLINE_BAR_DOWNLOADS the caller collapses them into a dropdown. Built here (not inline) so the row and dropdown share entries/order and the labels are testable. */

/** How many downloads may sit inline in the bar before they collapse. */
export const MAX_INLINE_BAR_DOWNLOADS = 2;
Expand All @@ -21,45 +20,30 @@ export interface BarDownload {
label: string;
/** Tooltip — spells out the action, and the model for trajectories. */
title: string;
kind: 'trajectory' | 'results';
s3Uri: string;
objectUrl: string;
}

/** Build the bar's download list from a run's prompt responses and its verifier results URI, if any.
/** Build the bar's download list from a run's prompt responses.
* Trajectories are labelled by step_id, else prompt_id, else "Trajectory N"; a single trajectory gets the bare
* "Trajectory". Entries with no URI are dropped. */
export function buildBarDownloads(
promptResponses: BarDownloadTrajectory[],
resultS3Uri?: string,
): BarDownload[] {
const withTrajectory = promptResponses.filter(pr => trajectoryUrl(pr));
const single = withTrajectory.length === 1;

const downloads: BarDownload[] = withTrajectory.map((pr, i) => {
return withTrajectory.map((pr, i) => {
const label = single
? 'Trajectory'
: pr.step_id || pr.prompt_id || `Trajectory ${i + 1}`;
const s3Uri = trajectoryUrl(pr) as string;
const objectUrl = trajectoryUrl(pr) as string;
return {
key: s3Uri,
key: objectUrl,
label,
title: pr.model ? `Download ${label} (${pr.model})` : `Download ${label}`,
kind: 'trajectory',
s3Uri,
objectUrl,
};
});

if (resultS3Uri) {
downloads.push({
key: `results:${resultS3Uri}`,
label: 'Results',
title: 'Download Results',
kind: 'results',
s3Uri: resultS3Uri,
});
}

return downloads;
}

/** Whether the bar should collapse its downloads into a dropdown. */
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/explorer/ui/src/lib/coerce.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** Narrowing helpers for payloads that arrive as `unknown`. Trigger state comes from a gateway response or an S3 object, so every field is a claim — coercing beats asserting. */
/** Narrowing helpers for payloads that arrive as `unknown`. Trigger state comes from a gateway response or a stored object, so every field is a claim — coercing beats asserting. */

export function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function main(): void {
},
'/app',
);
const uris = files.map(f => f.s3Uri);
const uris = files.map(f => f.objectUrl);
assert(uris.length === 3, 'three artifacts have a url');
assert(uris.includes('file:///state/objects/run/out/data.csv'), 'a local file:// url is included');
assert(uris.includes('gs://bucket/run/shot.png'), 'another backend\'s url is included');
Expand All @@ -46,7 +46,7 @@ function main(): void {
},
'/app/',
);
const byUri = Object.fromEntries(files.map(f => [f.s3Uri, f.path]));
const byUri = Object.fromEntries(files.map(f => [f.objectUrl, f.path]));
assert(byUri['fake://home/a'] === 'rel/a.txt', 'a relative key is kept');
assert(byUri['fake://home/b'] === 'abs/b.txt', 'an absolute key under base_path is made relative');
assert(byUri['fake://home/c'] === 'elsewhere/c.txt', 'an absolute key elsewhere loses its leading slash');
Expand Down
4 changes: 2 additions & 2 deletions src/agent_env/explorer/ui/src/lib/collected-zip-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

export interface CollectedZipFile {
path: string;
s3Uri: string;
objectUrl: string;
}

/** Artifact keys are absolute source paths or relative ones (joined under base_path). */
Expand All @@ -23,6 +23,6 @@ export function collectedZipFiles(
: key.startsWith(`${base}/`)
? key.slice(base.length + 1)
: key.replace(/^[/\\]+/, ''),
s3Uri: uri,
objectUrl: uri,
}));
}
Loading
Loading