Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/argent-cli/test/run-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ describe("CLI run — artifact materialization end-to-end", () => {
return {
[ARTIFACT_MARKER]: true,
id: "loc-1",
kind: "screenshot",
filename: "shot.png",
mimeType: "image/png",
size: st.size,
Expand Down Expand Up @@ -159,6 +160,7 @@ describe("CLI run — artifact materialization end-to-end", () => {
image: {
[ARTIFACT_MARKER]: true,
id: "rem-1",
kind: "screenshot",
filename: "shot.png",
mimeType: "image/png",
size: PNG.length,
Expand Down
13 changes: 11 additions & 2 deletions packages/argent-mcp/test/content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ import {
flowRunToMcpContent,
type FlowExecuteResult,
} from "../src/content.js";
import { ARTIFACT_MARKER, type ArtifactHandle } from "@argent/tools-client";
import { ARTIFACT_MARKER, type ArtifactHandle, type ArtifactKind } from "@argent/tools-client";

const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];

function artifactKind(filename: string, mimeType: string): ArtifactKind {
if (mimeType.startsWith("image/")) return "screenshot";
if (filename.includes("cpu")) return "native-profile-cpu";
if (filename.includes("hangs")) return "native-profile-hangs";
if (filename.includes("leaks")) return "native-profile-leaks";
return "native-profile-trace";
}

function artifactHandle(id: string, filename: string, mimeType: string): ArtifactHandle {
return { [ARTIFACT_MARKER]: true, id, filename, mimeType, size: 0 };
const kind = artifactKind(filename, mimeType);
return { [ARTIFACT_MARKER]: true, id, kind, filename, mimeType, size: 0 };
}

function fetchReturning(bytes: number[]): typeof fetch {
Expand Down
15 changes: 15 additions & 0 deletions packages/argent-tools-client/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,24 @@ const execFileAsync = promisify(execFile);
/** Must match the tool-server's wire contract (`tool-server/src/artifacts.ts`). */
export const ARTIFACT_MARKER = "__argentArtifact" as const;

export type ArtifactKind =
| "screenshot"
| "screenshot-diff"
| "screenshot-diff-context"
| "native-profile-trace"
| "native-profile-cpu"
| "native-profile-hangs"
| "native-profile-leaks"
| "native-profile-report"
| "react-profile-cpu"
| "react-profile-commits"
| "react-profile-report";

export interface ArtifactHandle {
[ARTIFACT_MARKER]: true;
id: string;
/** Semantic artifact category, distinct from MIME type. */
kind: ArtifactKind;
filename: string;
mimeType: string;
size: number;
Expand Down
1 change: 1 addition & 0 deletions packages/argent-tools-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export {
artifactsRoot,
ARTIFACT_MARKER,
type ArtifactHandle,
type ArtifactKind,
type MaterializeContext,
type MaterializedImage,
} from "./artifacts.js";
Expand Down
17 changes: 16 additions & 1 deletion packages/argent-tools-client/test/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@ import {
getDeviceIdFromArgs,
artifactDir,
ARTIFACT_MARKER,
type ArtifactKind,
type ArtifactHandle,
} from "../src/artifacts.js";

function artifactKind(filename: string, mimeType: string): ArtifactKind {
if (mimeType.startsWith("image/")) return "screenshot";
if (filename.includes("cpu")) return "native-profile-cpu";
if (filename.includes("hangs")) return "native-profile-hangs";
if (filename.includes("leaks")) return "native-profile-leaks";
return "native-profile-trace";
}

function handle(id: string, filename: string, mimeType: string): ArtifactHandle {
return { [ARTIFACT_MARKER]: true, id, filename, mimeType, size: 0 };
const kind = artifactKind(filename, mimeType);
return { [ARTIFACT_MARKER]: true, id, kind, filename, mimeType, size: 0 };
}

const PNG = [0x89, 0x50, 0x4e, 0x47, 0x01, 0x02];
Expand Down Expand Up @@ -141,6 +151,7 @@ describe("materializeArtifacts", () => {
const h: ArtifactHandle = {
[ARTIFACT_MARKER]: true,
id: "trunc",
kind: "native-profile-cpu",
filename: "data.xml",
mimeType: "application/xml",
size: 99, // server announced 99 bytes…
Expand Down Expand Up @@ -190,6 +201,7 @@ describe("materializeArtifacts local short-circuit", () => {
return {
[ARTIFACT_MARKER]: true,
id,
kind: artifactKind(filename, mimeType),
filename,
mimeType,
size: st.size,
Expand Down Expand Up @@ -237,6 +249,7 @@ describe("materializeArtifacts local short-circuit", () => {
const h: ArtifactHandle = {
[ARTIFACT_MARKER]: true,
id: "img2",
kind: "screenshot",
filename: "shot.png",
mimeType: "image/png",
size: PNG.length,
Expand All @@ -258,6 +271,7 @@ describe("materializeArtifacts local short-circuit", () => {
const h: ArtifactHandle = {
[ARTIFACT_MARKER]: true,
id: "img3",
kind: "screenshot",
filename: "shot.png",
mimeType: "image/png",
size: PNG.length,
Expand Down Expand Up @@ -320,6 +334,7 @@ describe("materializeArtifacts directory bundles", () => {
return {
[ARTIFACT_MARKER]: true,
id,
kind: "native-profile-trace",
filename: "session.trace",
mimeType: "application/octet-stream",
size: 0,
Expand Down
44 changes: 39 additions & 5 deletions packages/registry/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,28 @@ import { basename, extname } from "node:path";
/** Discriminant key identifying an artifact handle inside a tool result. */
export const ARTIFACT_MARKER = "__argentArtifact" as const;

/**
* Semantic artifact category. MIME type tells consumers how to read the bytes;
* kind tells them what the artifact represents.
*/
export type ArtifactKind =
| "screenshot"
| "screenshot-diff"
| "screenshot-diff-context"
| "native-profile-trace"
| "native-profile-cpu"
| "native-profile-hangs"
| "native-profile-leaks"
| "native-profile-report"
| "react-profile-cpu"
| "react-profile-commits"
| "react-profile-report";

/** Wire contract: what a tool returns in place of a host path. */
export interface ArtifactHandle {
[ARTIFACT_MARKER]: true;
id: string;
kind: ArtifactKind;
filename: string;
mimeType: string;
size: number;
Expand All @@ -59,6 +77,7 @@ export interface ArtifactHandle {
/** Internal entry the HTTP route reads to stream a registered artifact. */
export interface ArtifactEntry {
path: string;
kind: ArtifactKind;
filename: string;
mimeType: string;
size: number;
Expand All @@ -68,6 +87,7 @@ export interface ArtifactEntry {
/** Public metadata returned by the artifact inventory endpoint. */
export interface ArtifactListItem {
id: string;
kind: ArtifactKind;
filename: string;
mimeType: string;
size: number;
Expand All @@ -92,6 +112,10 @@ function inferMimeType(filePath: string): string {
}

export interface RegisterArtifactOptions {
/** Absolute path of the file or directory on the tool-server host. */
hostPath: string;
/** Semantic category of the artifact, distinct from its MIME type. */
kind: ArtifactKind;
/** Override the basename presented to the client. Defaults to the host basename. */
filename?: string;
/** Override the inferred MIME type. */
Expand All @@ -112,12 +136,13 @@ export interface RegisterArtifactOptions {
export class ArtifactStore {
private readonly entries = new Map<string, ArtifactEntry>();

async register(hostPath: string, opts?: RegisterArtifactOptions): Promise<ArtifactHandle> {
const filename = opts?.filename ?? basename(hostPath);
const mimeType = opts?.mimeType ?? inferMimeType(hostPath);
async register(opts: RegisterArtifactOptions): Promise<ArtifactHandle> {
const { hostPath } = opts;
const filename = opts.filename ?? basename(hostPath);
const mimeType = opts.mimeType ?? inferMimeType(hostPath);
let size = 0;
let mtimeMs: number | undefined;
let isDirectory = opts?.archive === "tar.gz";
let isDirectory = opts.archive === "tar.gz";
try {
const st = await stat(hostPath);
size = st.size;
Expand All @@ -129,10 +154,18 @@ export class ArtifactStore {
// they don't match what's on disk at read time.
}
const id = randomUUID();
this.entries.set(id, { path: hostPath, filename, mimeType, size, isDirectory });
this.entries.set(id, {
path: hostPath,
kind: opts.kind,
filename,
mimeType,
size,
isDirectory,
});
const handle: ArtifactHandle = {
[ARTIFACT_MARKER]: true,
id,
kind: opts.kind,
filename,
mimeType,
size,
Expand All @@ -150,6 +183,7 @@ export class ArtifactStore {
list(): ArtifactListItem[] {
return [...this.entries.entries()].map(([id, entry]) => ({
id,
kind: entry.kind,
filename: entry.filename,
mimeType: entry.mimeType,
size: entry.size,
Expand Down
1 change: 1 addition & 0 deletions packages/registry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type {
ArtifactHandle,
ArtifactEntry,
ArtifactListItem,
ArtifactKind,
RegisterArtifactOptions,
} from "./artifacts";
export {
Expand Down
7 changes: 4 additions & 3 deletions packages/registry/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,10 @@ export interface InvokeToolOptions {
* this for every invocation: it carries the caller's {@link InvokeToolOptions}
* (e.g. `signal`) plus cross-cutting context the registry owns — currently the
* {@link ArtifactStore}, so any tool that produces a host file can register it
* (`ctx.artifacts.register(path)`) without declaring a per-tool service. The
* registry always populates `artifacts`; it is only ever absent when `execute`
* is called directly (bypassing `invokeTool`), e.g. in a unit test.
* (`ctx.artifacts.register({ hostPath, kind })`) without declaring a per-tool
* service. The registry always populates `artifacts`; it is only ever absent
* when `execute` is called directly (bypassing `invokeTool`), e.g. in a unit
* test.
*/
export interface ToolContext extends InvokeToolOptions {
artifacts: ArtifactStore;
Expand Down
1 change: 1 addition & 0 deletions packages/tool-server/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
type ArtifactHandle,
type ArtifactEntry,
type ArtifactListItem,
type ArtifactKind,
type RegisterArtifactOptions,
} from "@argent/registry";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ Fails if native-profiler-stop has not been called first to export trace data.`,
return {
...result,
reportFile: result.reportFile
? await requireArtifacts(ctx).register(result.reportFile)
? await requireArtifacts(ctx).register({
hostPath: result.reportFile,
kind: "native-profile-report",
})
: null,
};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { stopNativeProfilerIos, type IosStopResult } from "./platforms/ios";
import { stopNativeProfilerAndroid, type AndroidStopResult } from "./platforms/android";
import type { ExportDiagnostics } from "../../../utils/ios-profiler/export";
import { requireArtifacts, type ArtifactHandle } from "../../../artifacts";
import type { ArtifactStore } from "@argent/registry";
import type { ArtifactKind, ArtifactStore } from "@argent/registry";

const zodSchema = z.object({
device_id: z
Expand Down Expand Up @@ -55,14 +55,26 @@ const capability = {
android: { emulator: true, device: true, unknown: true },
} as const;

const EXPORTED_FILE_KIND_BY_KEY: Record<string, ArtifactKind> = {
cpu: "native-profile-cpu",
hangs: "native-profile-hangs",
leaks: "native-profile-leaks",
pftrace: "native-profile-trace",
};

/** Register each non-null exported file path as a downloadable artifact. */
async function exportedFilesToArtifacts(
store: ArtifactStore,
files: Record<string, string | null>
): Promise<Record<string, ArtifactHandle | null>> {
const out: Record<string, ArtifactHandle | null> = {};
for (const [key, filePath] of Object.entries(files)) {
out[key] = filePath ? await store.register(filePath) : null;
out[key] = filePath
? await store.register({
hostPath: filePath,
kind: EXPORTED_FILE_KIND_BY_KEY[key] ?? "native-profile-trace",
})
: null;
}
return out;
}
Expand All @@ -73,7 +85,7 @@ async function exportedFilesToArtifacts(
* be stat'd at registration (e.g. a recovered session).
*/
function registerTrace(store: ArtifactStore, traceFile: string): Promise<ArtifactHandle> {
return store.register(traceFile, { archive: "tar.gz" });
return store.register({ hostPath: traceFile, kind: "native-profile-trace", archive: "tar.gz" });
}

export const nativeProfilerStopTool: ToolDefinition<z.infer<typeof zodSchema>, StopResult> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from "../../../utils/react-profiler/debug/dump";
import { serializeCpuSampleIndex } from "../../../utils/react-profiler/pipeline/00-cpu-correlate";
import { requireArtifacts, type ArtifactHandle } from "../../../artifacts";
import type { ArtifactStore } from "@argent/registry";
import type { ArtifactKind, ArtifactStore } from "@argent/registry";

/**
* Register a server-side file path as a downloadable artifact (or pass through
Expand All @@ -32,9 +32,10 @@ import type { ArtifactStore } from "@argent/registry";
*/
async function fileArtifact(
store: ArtifactStore,
p: string | null | undefined
p: string | null | undefined,
kind: ArtifactKind
): Promise<ArtifactHandle | null> {
return p ? store.register(p) : null;
return p ? store.register({ hostPath: p, kind }) : null;
}

const annotationSchema = z.object({
Expand Down Expand Up @@ -227,13 +228,13 @@ Fails if react-profiler-stop has not been called or no profiling data is stored.
const artifacts = requireArtifacts(ctx);
const result: Record<string, unknown> = {
report,
reportFile: await fileArtifact(artifacts, reportFile),
reportFile: await fileArtifact(artifacts, reportFile, "react-profile-report"),
hotCommitsTotal,
hotCommitsShown,
sessionFiles: {
sessionId: sessionPaths.sessionId,
cpuProfile: await fileArtifact(artifacts, sessionPaths.cpuProfilePath),
commits: await fileArtifact(artifacts, sessionPaths.commitsPath),
cpuProfile: await fileArtifact(artifacts, sessionPaths.cpuProfilePath, "react-profile-cpu"),
commits: await fileArtifact(artifacts, sessionPaths.commitsPath, "react-profile-commits"),
},
};

Expand Down
12 changes: 10 additions & 2 deletions packages/tool-server/src/tools/screenshot-diff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,19 @@ export async function executeScreenshotDiffTool(
return {
summary: result.summary,
...(result.diffPath
? { diffPath: await artifacts.register(result.diffPath, { mimeType: "image/png" }) }
? {
diffPath: await artifacts.register({
hostPath: result.diffPath,
kind: "screenshot-diff",
mimeType: "image/png",
}),
}
: {}),
...(result.contextDiffPath
? {
contextDiffPath: await artifacts.register(result.contextDiffPath, {
contextDiffPath: await artifacts.register({
hostPath: result.contextDiffPath,
kind: "screenshot-diff-context",
mimeType: "image/png",
}),
}
Expand Down
Loading
Loading