@agent-scope/core
v1.20.0
Published
Core types, schemas, and serialization for Scope — zero dependencies
Readme
@agent-scope/core
Zero-dependency type foundation for the Scope React instrumentation platform.
@agent-scope/core defines every shared TypeScript type, the serialize() function, a complete set of runtime type-guards, and schema version constants. Nothing in this package references the DOM, React, or any browser API — it is safe to import in any environment (browser, Node.js, edge runtimes, test runners).
Installation
npm install @agent-scope/coreWhat it does / when to use it
@agent-scope/core is the contract layer of the Scope monorepo. You need it when:
- Building a consumer of
PageReportobjects (analytics, DevTools, CI reporters) — import the types and type-guards. - Serialising arbitrary React props or state — call
serialize()to produce a safely-serialisedSerializedValuethat handles circular references, depth limiting, and every JavaScript type. - Validating untrusted data — use the
is*guards to verify that a stored report still matches the current schema before processing it.
@agent-scope/runtime and @agent-scope/sourcemap both depend on this package and re-export the types they need.
API Reference
PageReport
The top-level transport and storage unit. Every other type is a node or sub-node of this structure.
interface PageReport {
/** Full URL at capture time (no fragment). */
url: string;
/**
* Router-level metadata. null when no routing adapter is installed or
* the URL could not be matched to a known route.
*/
route: RouteInfo | null;
/** Unix timestamp (ms) when the capture was initiated. */
timestamp: number;
/**
* Wall-clock time (ms) from capture start to PageReport assembly.
* Use this to monitor Scope runtime overhead.
*/
capturedIn: number;
/** Root node of the captured React component tree. */
tree: ComponentNode;
/**
* All JS errors observed during the capture window:
* unhandled exceptions, unhandled rejections, ErrorBoundary catches,
* and console.error calls attributed to an error.
*/
errors: CapturedError[];
/** All <Suspense> boundaries, flattened for O(1) access. */
suspenseBoundaries: SuspenseBoundaryInfo[];
/** All console.* calls intercepted during the capture window, ascending by timestamp. */
consoleEntries: ConsoleEntry[];
}Example payload (from src/__tests__/types.test.ts):
const report: PageReport = {
url: "https://example.com/dashboard",
route: {
pattern: "/dashboard",
params: null,
query: {},
name: "dashboard",
},
timestamp: 1700000000000,
capturedIn: 42,
tree: {
id: 1,
name: "App",
type: "function",
source: { fileName: "/app/src/Foo.tsx", lineNumber: 10, columnNumber: 5 },
props: { type: "object", value: {}, preview: "{}" },
state: [
{
type: "useState",
name: null,
value: { type: "string", value: "hello", preview: '"hello"' },
deps: null,
hasCleanup: null,
},
],
context: [
{
contextName: "ThemeContext",
value: { type: "string", value: "hello", preview: '"hello"' },
didTriggerRender: false,
},
],
renderCount: 1,
renderDuration: 0.5,
children: [],
},
errors: [
{
message: "Something went wrong",
name: "Error",
stack: "Error: Something\n at App.tsx:42",
source: { fileName: "/app/src/Foo.tsx", lineNumber: 10, columnNumber: 5 },
componentName: "App",
timestamp: 1700000000000,
capturedBy: "boundary",
},
],
suspenseBoundaries: [
{
id: 5,
componentName: "LazySection",
isSuspended: false,
suspendedDuration: null,
fallbackName: "Spinner",
source: { fileName: "/app/src/Foo.tsx", lineNumber: 10, columnNumber: 5 },
},
],
consoleEntries: [
{
level: "warn",
args: [{ type: "string", value: "hello", preview: '"hello"' }],
timestamp: 1700000000001,
componentName: "App",
preview: "Something is odd",
},
],
};ComponentNode
One node in the captured component tree, corresponding to a single React fiber.
interface ComponentNode {
/** Fiber's internal numeric ID. Stable for the lifetime of a mounted component. */
id: number;
/**
* Resolved display name.
* Resolution order: displayName → function/class name → JSX tag → "Anonymous".
*/
name: string;
/** Component implementation strategy. */
type: ComponentType; // "function" | "class" | "forward_ref" | "memo" | "host"
/**
* Source location from babel's __source prop or DevTools fiber data.
* null in production builds or for host elements.
*/
source: SourceLocation | null;
/** Serialized snapshot of the component's props at capture time. */
props: SerializedValue;
/**
* Ordered hook slots (hook call-order, slot 0 = first use* call).
* Empty for class components and host elements.
*/
state: HookState[];
/** All React contexts consumed by this component during the capture window. */
context: ContextConsumption[];
/** Number of renders during the capture window (>1 indicates re-renders). */
renderCount: number;
/**
* Total wall-clock time (ms) in this component's render, excluding children.
* Measured from beginWork to completeWork.
*/
renderDuration: number;
/** Ordered child nodes (depth-first). Empty array = leaf node. */
children: ComponentNode[];
}
type ComponentType = "function" | "class" | "forward_ref" | "memo" | "host";LightweightComponentNode
A stripped-down node emitted when capture({ lightweight: true }) is used. Omits props, state, context, source, renderCount, and renderDuration, producing ~99% smaller payloads for large trees.
interface LightweightComponentNode {
id: number;
name: string;
type: ComponentType;
/** Total hooks count (no values serialised). */
hookCount: number;
/** Hook types in call order, no values. */
hookTypes: HookType[];
/** Equals children.length. */
childCount: number;
/** Tree depth; root = 0. */
depth: number;
children: LightweightComponentNode[];
}HookState
A snapshot of a single hook slot.
interface HookState {
/** Hook type. "custom" for third-party / project-local hooks. */
type: HookType;
/**
* Inferred name of a custom hook (e.g. "useTheme").
* null for built-in hooks where type is already informative.
*/
name: string | null;
/**
* Serialized current value.
* - useState/useReducer: current state
* - useMemo/useCallback: memoized value / function ref
* - useRef: .current value
* - useContext: current context value
* - effect hooks: undefined (no meaningful value)
*/
value: SerializedValue;
/**
* Serialized dependency array.
* Present for useEffect, useLayoutEffect, useMemo, useCallback, useSyncExternalStore.
* null for hooks that do not accept deps.
*/
deps: SerializedValue[] | null;
/**
* Whether the effect registered a cleanup function on its last run.
* true = cleanup returned. false = no cleanup. null = not applicable.
*/
hasCleanup: boolean | null;
}
type HookType =
| "useState" | "useReducer" | "useEffect" | "useLayoutEffect"
| "useMemo" | "useCallback" | "useRef" | "useContext"
| "useId" | "useSyncExternalStore" | "useTransition" | "useDeferredValue"
| "custom";Example (from src/__tests__/types.test.ts):
// useState
{ type: "useState", name: null, value: { type: "number", value: 42 }, deps: null, hasCleanup: null }
// useEffect with deps and cleanup
{ type: "useEffect", name: null, value: { type: "undefined" }, deps: [{ type: "string", value: "dep1" }], hasCleanup: true }
// custom hook
{ type: "custom", name: "useTheme", value: { type: "object", preview: '{ mode: "dark" }' }, deps: null, hasCleanup: null }SerializedValue
An envelope for any JavaScript value that may not round-trip through JSON (functions, symbols, circular references, etc.).
interface SerializedValue {
/** Original JavaScript type discriminant. */
type: SerializedValueType;
/**
* Round-trippable value for primitives and simple composites.
* Absent for function, symbol, circular, truncated — use preview instead.
*/
value?: unknown;
/**
* Human-readable string for display. Always present when value is absent.
*/
preview?: string;
}
type SerializedValueType =
| "string" | "number" | "boolean" | "null" | "undefined"
| "object" | "array" | "function" | "symbol" | "bigint"
| "date" | "map" | "set"
| "circular" // circular reference was detected at this position
| "truncated"; // value exceeded depth/size limits and was cut offExamples (from src/__tests__/serialization.test.ts):
// Primitives
{ type: "null", value: null, preview: "null" }
{ type: "boolean", value: true, preview: "true" }
{ type: "number", value: 42, preview: "42" }
{ type: "string", value: "hi", preview: '"hi"' }
{ type: "bigint", value: "123n", preview: "123n" }
// Function (no value)
{ type: "function", preview: "function myFunc(a, b) { return a + …" }
// Date
{ type: "date", value: "2024-01-01T00:00:00.000Z", preview: "2024-01-01T00:00:00.000Z" }
// Map
{ type: "map", value: [/* entry objects */], preview: "Map(2)" }
// Set
{ type: "set", value: [/* items */], preview: "Set(3)" }
// Circular reference
{ type: "circular" }
// Depth-exceeded
{ type: "truncated", preview: "Array(50)" }serialize(value, options?)
Convert any JavaScript value to a SerializedValue snapshot.
function serialize(value: unknown, options?: SerializeOptions): SerializedValue;
interface SerializeOptions {
/** Maximum recursion depth for nested objects/arrays. Default: 5. */
maxDepth?: number;
/** Maximum string length before truncation. Default: 200. */
maxStringLength?: number;
/** Maximum array items to serialize. Default: 100. */
maxArrayLength?: number;
/** Maximum object properties to serialize. Default: 50. */
maxProperties?: number;
}Behaviour summary:
| Input | Output type | Notes |
|---|---|---|
| null | "null" | value: null |
| undefined | "undefined" | no value field |
| boolean | "boolean" | |
| number | "number" | NaN, Infinity included |
| string | "string" | truncated at maxStringLength with "..." suffix |
| bigint | "bigint" | stored as "123n" string |
| Symbol | "symbol" | preview only, no value |
| function | "function" | raw source preview, truncated at 50 chars |
| Date | "date" | ISO 8601 string in value and preview |
| Map | "map" | entries as objects, truncated at maxProperties |
| Set | "set" | items as array, truncated at maxArrayLength |
| Array | "array" | truncated at maxArrayLength |
| object | "object" | {key: SerializedValue} map, truncated at maxProperties |
| Error | "object" | {name, message, stack} extracted |
| WeakMap/WeakSet/WeakRef | "object" | opaque, no entries |
| circular ref | "circular" | no value or preview |
| depth exceeded | "truncated" | preview describes the cut-off value |
Circular reference detection uses a WeakSet that tracks the current ancestor chain. The same object can legitimately appear in sibling branches without being flagged as circular (verified by tests).
Depth counting — depth 0 is the root call. An object value encountered at depth >= maxDepth is returned as { type: "truncated" } rather than being expanded.
// Self-referential object
const obj: Record<string, unknown> = { a: 1 };
obj.self = obj;
serialize(obj);
// → { type: "object", value: { a: { type: "number", ... }, self: { type: "circular" } } }
// Deep nesting with maxDepth: 2
const deep = { a: { b: { c: "leaf" } } };
serialize(deep, { maxDepth: 2 });
// root(depth=0) → a(depth=1) → b is { type: "truncated" } at depth=2
// Shared object in sibling branches (NOT flagged as circular)
const shared = { x: 1 };
serialize({ left: shared, right: shared });
// → { left: { type: "object", ... }, right: { type: "object", ... } } ← both are "object"Type Guards
All guards accept unknown and return a type predicate. Use them to validate data crossing process or storage boundaries.
import {
isPageReport, isPageReportDeep,
isComponentNode, isComponentNodeDeep,
isCapturedError, isSuspenseBoundaryInfo,
isConsoleEntry, isContextConsumption,
isHookState, isRouteInfo,
isSerializedValue, isSourceLocation,
} from "@agent-scope/core";| Guard | Validates | Depth |
|---|---|---|
| isPageReport(v) | Full PageReport shape | Shallow ComponentNode |
| isPageReportDeep(v) | Full PageReport + entire tree | Recursive |
| isComponentNode(v) | Single ComponentNode (children not recursed) | O(1) per node |
| isComponentNodeDeep(v) | ComponentNode + all descendants | O(n) |
| isCapturedError(v) | CapturedError | — |
| isSuspenseBoundaryInfo(v) | SuspenseBoundaryInfo | — |
| isConsoleEntry(v) | ConsoleEntry | — |
| isContextConsumption(v) | ContextConsumption | — |
| isHookState(v) | HookState | — |
| isRouteInfo(v) | RouteInfo | — |
| isSerializedValue(v) | SerializedValue | — |
| isSourceLocation(v) | SourceLocation | — |
Constants
Readonly arrays useful for exhaustive switch/if checks and validation loops.
import {
SCHEMA_VERSION, // "0.1.0" — bump on breaking schema changes
HOOK_TYPES, // readonly HookType[]
COMPONENT_TYPES, // readonly ComponentType[]
SERIALIZED_VALUE_TYPES, // readonly SerializedValueType[]
CONSOLE_LEVELS, // readonly ConsoleLevel[]
} from "@agent-scope/core";SCHEMA_VERSION follows semver; store it in PageReport metadata to detect schema drift when loading reports from storage.
Supporting Types
interface SourceLocation {
/** Absolute or relative path to the source file. */
fileName: string;
/** 1-based line number of the JSX element. */
lineNumber: number;
/** 1-based column number of the JSX element. */
columnNumber: number;
}Populated from React's __source prop (injected by babel-plugin-transform-react-jsx-source in development mode).
interface ContextConsumption {
/**
* The context's displayName. null when the context has no display name.
*/
contextName: string | null;
/** Serialized snapshot of the context value at capture time. */
value: SerializedValue;
/**
* Whether this context consumption triggered a re-render during the
* capture window (i.e. value changed between previous and current render).
*/
didTriggerRender: boolean;
}interface CapturedError {
message: string;
name: string; // e.g. "TypeError", "RangeError"
stack: string | null; // raw V8 stack; not source-mapped
source: SourceLocation | null; // parsed from top stack frame
componentName: string | null; // nearest React component
timestamp: number; // Unix ms
capturedBy: "boundary" | "unhandled" | "rejection" | "console";
}interface RouteInfo {
pattern: string | null; // e.g. "/users/:id"
params: Record<string, string> | null; // e.g. { id: "42" }
query: Record<string, string>; // parsed query string
name: string | null; // named route from router config
}interface SuspenseBoundaryInfo {
id: number;
componentName: string; // nearest named ancestor
isSuspended: boolean;
suspendedDuration: number | null; // ms suspended; null if not triggered
fallbackName: string | null; // name of the fallback element
source: SourceLocation | null;
}interface ConsoleEntry {
level: ConsoleLevel; // "log" | "warn" | "error" | "info" | "debug" | "group" | ...
args: SerializedValue[]; // each argument, serialized
timestamp: number; // Unix ms
componentName: string | null; // attributed React component
preview: string; // single-line condensed text, trimmed to 200 chars
}Internal Architecture
src/
├── index.ts ← barrel export (types, guards, serialize, constants)
├── serialization.ts ← serialize() + SerializeOptions
├── guards.ts ← all is*() type-guard functions
├── constants.ts ← SCHEMA_VERSION, HOOK_TYPES, COMPONENT_TYPES, …
└── types/
├── page-report.ts ← PageReport
├── component-node.ts ← ComponentNode, ComponentType
├── lightweight-component-node.ts ← LightweightComponentNode
├── hook-state.ts ← HookState, HookType
├── serialized-value.ts ← SerializedValue, SerializedValueType
├── source-location.ts ← SourceLocation
├── context.ts ← ContextConsumption
├── errors.ts ← CapturedError
├── route.ts ← RouteInfo
├── console.ts ← ConsoleEntry, ConsoleLevel
└── suspense.ts ← SuspenseBoundaryInfoThe package has zero runtime dependencies. It ships both ESM (dist/index.js) and CJS (dist/index.cjs) builds via tsup.
Used by
| Package | How |
|---|---|
| @agent-scope/runtime | Imports all types; calls serialize() for props, state, and context values |
| @agent-scope/sourcemap | Imports ComponentNode, PageReport to type the resolution input/output |
