diff --git a/.changeset/remove-use-runtime.md b/.changeset/remove-use-runtime.md
new file mode 100644
index 00000000..e815bbea
--- /dev/null
+++ b/.changeset/remove-use-runtime.md
@@ -0,0 +1,12 @@
+---
+"@perstack/react": patch
+---
+
+Remove unused `useRuntime` hook and related types from public API
+
+The `useRuntime` hook was not used in any actual code. Runtime information display is handled by application-specific hooks.
+
+Removed exports:
+- `useRuntime` hook
+- `RuntimeResult`, `RuntimeState`, `SkillState`, `DockerBuildState`, `DockerContainerState`, `ProxyAccessState` types
+- `createInitialRuntimeState` function
diff --git a/docs/references/events.md b/docs/references/events.md
index 2e2a7506..81771c46 100644
--- a/docs/references/events.md
+++ b/docs/references/events.md
@@ -480,37 +480,33 @@ rl.on("line", (line) => {
Use the provided hooks from `@perstack/react`:
```typescript
-import { useRun, useRuntime } from "@perstack/react"
+import { useRun } from "@perstack/react"
function ExpertRunner() {
// RunEvents → accumulated activities + streaming state
- const { activities, streaming, addEvent } = useRun()
-
- // RuntimeEvents → current runtime environment state
- const { runtimeState, handleRuntimeEvent } = useRuntime()
-
- const handleEvent = (event: PerstackEvent) => {
- // Try RuntimeEvent first (returns false if not handled)
- if (!handleRuntimeEvent(event)) {
- // Must be RunEvent, add to run state
- addEvent(event)
+ const { activities, streaming, isComplete, addEvent } = useRun()
+
+ useEffect(() => {
+ const eventSource = new EventSource("/api/events")
+ eventSource.onmessage = (e) => {
+ addEvent(JSON.parse(e.data))
}
- }
+ return () => eventSource.close()
+ }, [addEvent])
return (
- {/* Show current runtime state */}
-
-
{/* Show streaming content (grouped by run for parallel execution) */}
{Object.entries(streaming.runs).map(([runId, run]) => (
run.isReasoningActive && (
)
))}
-
+
{/* Show accumulated activities */}
+
+ {isComplete &&
Run complete!
}
)
}
diff --git a/packages/react/README.md b/packages/react/README.md
index eca12bb3..48141829 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -12,18 +12,18 @@ pnpm add @perstack/react
## Usage
-### useLogStore
+### useRun
-The main hook for managing Perstack events. It separates events into:
-- **LogEntry[]** - Accumulated log from RunEvent (state machine transitions)
-- **RuntimeState** - Current state from RuntimeEvent (runtime environment)
+The main hook for managing Perstack run state. It processes events into:
+
+- **activities** - Accumulated activities from RunEvent (append-only)
+- **streaming** - Current streaming state for real-time display
```tsx
-import { useLogStore } from "@perstack/react"
+import { useRun } from "@perstack/react"
-function MyComponent() {
- const { logs, runtimeState, isComplete, eventCount, addEvent, appendHistoricalEvents } =
- useLogStore()
+function ExpertRunner() {
+ const { activities, streaming, isComplete, addEvent, appendHistoricalEvents } = useRun()
// Add events from your event source
useEffect(() => {
@@ -36,19 +36,17 @@ function MyComponent() {
return (
- {logs.map((entry) => (
-
- ))}
- {Object.entries(runtimeState.streaming.runs).map(([runId, run]) => (
-
- {run.isReasoningActive && (
-
[{run.expertKey}] Reasoning: {run.reasoning}
- )}
- {run.isRunResultActive && (
-
[{run.expertKey}] Generating: {run.runResult}
- )}
-
+ {/* Show streaming content (grouped by run for parallel execution) */}
+ {Object.entries(streaming.runs).map(([runId, run]) => (
+ run.isReasoningActive && (
+
[{run.expertKey}] Reasoning: {run.reasoning}
+ )
))}
+
+ {/* Show accumulated activities */}
+
+
+ {isComplete &&
Run complete!
}
)
}
@@ -89,58 +87,44 @@ function JobActivityView({ jobId, isRunning }: { jobId: string; isRunning: boole
}
```
-### useRuntimeState
-
-A lower-level hook for managing RuntimeState separately.
-
-```tsx
-import { useRuntimeState } from "@perstack/react"
-
-function MyComponent() {
- const { runtimeState, handleRuntimeEvent, clearStreaming, resetRuntimeState } = useRuntimeState()
-
- // Returns true if the event was handled (RuntimeEvent)
- // Returns false if the event should be processed elsewhere (RunEvent)
- const wasHandled = handleRuntimeEvent(event)
-}
-```
-
### Utility Functions
For advanced use cases, you can use the utility functions directly:
```tsx
import {
- createInitialLogProcessState,
- processRunEventToLog,
- toolToCheckpointAction,
+ createInitialActivityProcessState,
+ processRunEventToActivity,
+ toolToActivity,
+ groupActivitiesByRun,
} from "@perstack/react"
// Create processing state
-const state = createInitialLogProcessState()
+const state = createInitialActivityProcessState()
-// Process RunEvent into LogEntry
-const logs = []
-processRunEventToLog(state, event, (entry) => logs.push(entry))
+// Process RunEvent into Activity
+const activities = []
+processRunEventToActivity(state, event, (activity) => activities.push(activity))
-// Convert a single tool call + result to CheckpointAction
-const action = toolToCheckpointAction(toolCall, toolResult, reasoning)
+// Group activities by run ID
+const grouped = groupActivitiesByRun(activities)
```
## API
-### useLogStore()
+### useRun()
Returns an object with:
-- `logs`: Array of `LogEntry` representing completed actions (append-only)
-- `runtimeState`: Current `RuntimeState` including streaming state
+- `activities`: Array of `ActivityOrGroup` representing completed actions (append-only)
+- `streaming`: Current `StreamingState` for real-time display
- `isComplete`: Whether the run is complete
- `eventCount`: Total number of processed events
- `addEvent(event)`: Add a new event to process
-- `appendHistoricalEvents(events)`: Append historical events to logs
+- `appendHistoricalEvents(events)`: Bulk load historical events
+- `clearStreaming()`: Clear streaming state
-**Note:** Logs are append-only and never cleared. This is required for compatibility with Ink's `` component.
+**Note:** Activities are append-only and never cleared. This is required for compatibility with Ink's `` component.
### useEventStream(options)
@@ -163,47 +147,8 @@ The hook automatically:
- Processes events through `useRun` internally
- Clears error state on reconnection
-### useRuntimeState()
-
-Returns an object with:
-
-- `runtimeState`: Current `RuntimeState`
-- `handleRuntimeEvent(event)`: Process a RuntimeEvent, returns `true` if handled
-- `clearStreaming()`: Reset streaming state
-- `resetRuntimeState()`: Reset entire runtime state
-
## Types
-### LogEntry
-
-Wraps `CheckpointAction` with an ID for React key purposes:
-
-```typescript
-type LogEntry = {
- id: string
- action: CheckpointAction
-}
-```
-
-### RuntimeState
-
-Captures current runtime environment state:
-
-```typescript
-type RuntimeState = {
- query?: string
- expertName?: string
- model?: string
- runtime?: string
- runtimeVersion?: string
- skills: Map
- dockerBuild?: DockerBuildState
- dockerContainers: Map
- proxyAccess?: ProxyAccessState
- streaming: StreamingState
-}
-```
-
### StreamingState
Real-time streaming state, organized by run ID to support parallel execution:
diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts
index 371af867..efec4836 100644
--- a/packages/react/src/hooks/index.ts
+++ b/packages/react/src/hooks/index.ts
@@ -6,4 +6,3 @@ export {
useEventStream,
} from "./use-event-stream.js"
export { type ActivityProcessState, type RunResult, useRun } from "./use-run.js"
-export { type RuntimeResult, useRuntime } from "./use-runtime.js"
diff --git a/packages/react/src/hooks/use-runtime.test.ts b/packages/react/src/hooks/use-runtime.test.ts
deleted file mode 100644
index 6be35628..00000000
--- a/packages/react/src/hooks/use-runtime.test.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import type { PerstackEvent, RuntimeEvent } from "@perstack/core"
-import { describe, expect, it } from "vitest"
-import { createInitialRuntimeState } from "../types/index.js"
-
-function createRuntimeEvent(
- type: T,
- data: Omit, "id" | "timestamp" | "jobId" | "runId" | "type">,
-): Extract {
- return {
- id: "e-1",
- timestamp: Date.now(),
- jobId: "job-1",
- runId: "run-1",
- type,
- ...data,
- } as Extract
-}
-
-describe("useRuntime state updates", () => {
- describe("initializeRuntime", () => {
- it("captures query, expertName, model, and runtime from event", () => {
- const state = createInitialRuntimeState()
- const event = createRuntimeEvent("initializeRuntime", {
- runtimeVersion: "1.0.0",
- expertName: "test-expert",
- model: "claude-sonnet-4-20250514",
- runtime: "docker",
- experts: [],
- maxRetries: 3,
- timeout: 30000,
- query: "Hello world",
- })
-
- const newState = {
- ...state,
- query: event.query,
- expertName: event.expertName,
- model: event.model,
- runtime: event.runtime,
- runtimeVersion: event.runtimeVersion,
- }
-
- expect(newState.query).toBe("Hello world")
- expect(newState.expertName).toBe("test-expert")
- expect(newState.model).toBe("claude-sonnet-4-20250514")
- expect(newState.runtime).toBe("docker")
- })
- })
-
- describe("skill events", () => {
- it("tracks skill starting state", () => {
- const state = createInitialRuntimeState()
- const event = createRuntimeEvent("skillStarting", {
- skillName: "@perstack/base",
- command: "node",
- args: ["server.js"],
- })
-
- const skills = new Map(state.skills)
- skills.set(event.skillName, { name: event.skillName, status: "starting" })
-
- expect(skills.get("@perstack/base")?.status).toBe("starting")
- })
-
- it("tracks skill connected state", () => {
- const state = createInitialRuntimeState()
- const event = createRuntimeEvent("skillConnected", {
- skillName: "@perstack/base",
- serverInfo: { name: "base", version: "1.0.0" },
- })
-
- const skills = new Map(state.skills)
- skills.set(event.skillName, {
- name: event.skillName,
- status: "connected",
- serverInfo: event.serverInfo,
- })
-
- expect(skills.get("@perstack/base")?.status).toBe("connected")
- expect(skills.get("@perstack/base")?.serverInfo?.version).toBe("1.0.0")
- })
- })
-
- describe("docker events", () => {
- it("tracks docker build progress", () => {
- const state = createInitialRuntimeState()
- const event = createRuntimeEvent("dockerBuildProgress", {
- stage: "building",
- service: "runtime",
- message: "Building image...",
- progress: 50,
- })
-
- const newState = {
- ...state,
- dockerBuild: {
- stage: event.stage,
- service: event.service,
- message: event.message,
- progress: event.progress,
- },
- }
-
- expect(newState.dockerBuild?.stage).toBe("building")
- expect(newState.dockerBuild?.progress).toBe(50)
- })
-
- it("tracks docker container status", () => {
- const state = createInitialRuntimeState()
- const event = createRuntimeEvent("dockerContainerStatus", {
- status: "running",
- service: "runtime",
- message: "Container started",
- })
-
- const dockerContainers = new Map(state.dockerContainers)
- dockerContainers.set(event.service, {
- status: event.status,
- service: event.service,
- message: event.message,
- })
-
- expect(dockerContainers.get("runtime")?.status).toBe("running")
- })
- })
-
- describe("proxy events", () => {
- it("tracks proxy access", () => {
- const state = createInitialRuntimeState()
- const event = createRuntimeEvent("proxyAccess", {
- action: "allowed",
- domain: "example.com",
- port: 443,
- reason: "Allowlisted",
- })
-
- const newState = {
- ...state,
- proxyAccess: {
- action: event.action,
- domain: event.domain,
- port: event.port,
- reason: event.reason,
- },
- }
-
- expect(newState.proxyAccess?.action).toBe("allowed")
- expect(newState.proxyAccess?.domain).toBe("example.com")
- })
- })
-
- describe("event filtering", () => {
- it("returns false for non-RuntimeEvent (RunEvent)", () => {
- // Minimal RunEvent for testing - only need expertKey to distinguish from RuntimeEvent
- const event = {
- id: "e-1",
- runId: "run-1",
- expertKey: "test-expert@1.0.0",
- jobId: "job-1",
- stepNumber: 1,
- timestamp: Date.now(),
- type: "startRun",
- initialCheckpoint: {},
- inputMessages: [],
- } as unknown as PerstackEvent
-
- expect("expertKey" in event).toBe(true)
- })
- })
-})
diff --git a/packages/react/src/hooks/use-runtime.ts b/packages/react/src/hooks/use-runtime.ts
deleted file mode 100644
index e78ab4c7..00000000
--- a/packages/react/src/hooks/use-runtime.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import type { PerstackEvent, RuntimeEvent } from "@perstack/core"
-import { useCallback, useState } from "react"
-import type { RuntimeState } from "../types/index.js"
-import { createInitialRuntimeState } from "../types/index.js"
-
-const RUNTIME_EVENT_TYPES = new Set([
- "initializeRuntime",
- "skillStarting",
- "skillConnected",
- "skillDisconnected",
- "skillStderr",
- "dockerBuildProgress",
- "dockerContainerStatus",
- "proxyAccess",
-])
-
-const isRuntimeEvent = (event: PerstackEvent): event is RuntimeEvent =>
- "type" in event && RUNTIME_EVENT_TYPES.has(event.type)
-
-export type RuntimeResult = {
- runtimeState: RuntimeState
- handleRuntimeEvent: (event: PerstackEvent) => boolean
- resetRuntimeState: () => void
-}
-
-/**
- * Hook for managing RuntimeState from RuntimeEvent stream.
- * Only handles infrastructure-level events (skills, docker, proxy).
- * Streaming events are now handled by useRun.
- */
-export function useRuntime(): RuntimeResult {
- const [runtimeState, setRuntimeState] = useState(createInitialRuntimeState)
-
- const handleRuntimeEvent = useCallback((event: PerstackEvent): boolean => {
- if (!isRuntimeEvent(event)) {
- return false
- }
-
- switch (event.type) {
- case "initializeRuntime": {
- if (event.type !== "initializeRuntime") return false
- setRuntimeState((prev) => ({
- ...prev,
- query: event.query,
- expertName: event.expertName,
- model: event.model,
- runtime: event.runtime,
- runtimeVersion: event.runtimeVersion,
- }))
- return true
- }
-
- case "skillStarting": {
- if (event.type !== "skillStarting") return false
- setRuntimeState((prev) => {
- const skills = new Map(prev.skills)
- skills.set(event.skillName, { name: event.skillName, status: "starting" })
- return { ...prev, skills }
- })
- return true
- }
-
- case "skillConnected": {
- if (event.type !== "skillConnected") return false
- setRuntimeState((prev) => {
- const skills = new Map(prev.skills)
- skills.set(event.skillName, {
- name: event.skillName,
- status: "connected",
- serverInfo: event.serverInfo,
- })
- return { ...prev, skills }
- })
- return true
- }
-
- case "skillDisconnected": {
- if (event.type !== "skillDisconnected") return false
- setRuntimeState((prev) => {
- const skills = new Map(prev.skills)
- skills.set(event.skillName, { name: event.skillName, status: "disconnected" })
- return { ...prev, skills }
- })
- return true
- }
-
- case "skillStderr":
- // skillStderr events are informational only (for logging)
- // No state update needed, but we still return true to indicate it's handled
- return true
-
- case "dockerBuildProgress": {
- if (event.type !== "dockerBuildProgress") return false
- setRuntimeState((prev) => ({
- ...prev,
- dockerBuild: {
- stage: event.stage,
- service: event.service,
- message: event.message,
- progress: event.progress,
- },
- }))
- return true
- }
-
- case "dockerContainerStatus": {
- if (event.type !== "dockerContainerStatus") return false
- setRuntimeState((prev) => {
- const dockerContainers = new Map(prev.dockerContainers)
- dockerContainers.set(event.service, {
- status: event.status,
- service: event.service,
- message: event.message,
- })
- return { ...prev, dockerContainers }
- })
- return true
- }
-
- case "proxyAccess": {
- if (event.type !== "proxyAccess") return false
- setRuntimeState((prev) => ({
- ...prev,
- proxyAccess: {
- action: event.action,
- domain: event.domain,
- port: event.port,
- reason: event.reason,
- },
- }))
- return true
- }
-
- default:
- return false
- }
- }, [])
-
- const resetRuntimeState = useCallback(() => {
- setRuntimeState(createInitialRuntimeState())
- }, [])
-
- return {
- runtimeState,
- handleRuntimeEvent,
- resetRuntimeState,
- }
-}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 1c164be5..9d8951dc 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -5,24 +5,13 @@ export {
type EventStreamOptions,
type EventStreamState,
type RunResult,
- type RuntimeResult,
type UseEventStreamOptions,
useEventStream,
useRun,
- useRuntime,
} from "./hooks/index.js"
// Types
-export type {
- DockerBuildState,
- DockerContainerState,
- PerRunStreamingState,
- ProxyAccessState,
- RuntimeState,
- SkillState,
- StreamingState,
-} from "./types/index.js"
-export { createInitialRuntimeState } from "./types/index.js"
+export type { PerRunStreamingState, StreamingState } from "./types/index.js"
// Utils
export {
diff --git a/packages/react/src/types/index.ts b/packages/react/src/types/index.ts
index 3d327a62..e011fe16 100644
--- a/packages/react/src/types/index.ts
+++ b/packages/react/src/types/index.ts
@@ -1,10 +1 @@
-export type {
- DockerBuildState,
- DockerContainerState,
- PerRunStreamingState,
- ProxyAccessState,
- RuntimeState,
- SkillState,
- StreamingState,
-} from "./runtime-state.js"
-export { createInitialRuntimeState } from "./runtime-state.js"
+export type { PerRunStreamingState, StreamingState } from "./runtime-state.js"
diff --git a/packages/react/src/types/runtime-state.ts b/packages/react/src/types/runtime-state.ts
index 3dbd3c0d..13057b1e 100644
--- a/packages/react/src/types/runtime-state.ts
+++ b/packages/react/src/types/runtime-state.ts
@@ -1,43 +1,3 @@
-/**
- * RuntimeState represents the current state of the runtime environment.
- * This is derived from RuntimeEvent and only the latest state matters.
- *
- * Unlike LogEntry (which accumulates), RuntimeState is replaced on each update.
- */
-
-/** Skill connection state */
-export type SkillState = {
- name: string
- status: "starting" | "connected" | "disconnected"
- serverInfo?: {
- name: string
- version: string
- }
-}
-
-/** Docker build progress state */
-export type DockerBuildState = {
- stage: "pulling" | "building" | "complete" | "error"
- service: string
- message: string
- progress?: number
-}
-
-/** Docker container status state */
-export type DockerContainerState = {
- status: "starting" | "running" | "healthy" | "unhealthy" | "stopped" | "error"
- service: string
- message?: string
-}
-
-/** Proxy access state (most recent) */
-export type ProxyAccessState = {
- action: "allowed" | "blocked"
- domain: string
- port: number
- reason?: string
-}
-
/** Per-run streaming state for real-time display */
export type PerRunStreamingState = {
/** Expert key for this run */
@@ -57,45 +17,3 @@ export type StreamingState = {
/** Per-run streaming state, keyed by runId */
runs: Record
}
-
-/**
- * RuntimeState captures the current state of the runtime environment.
- * All fields represent the latest state from RuntimeEvent.
- */
-export type RuntimeState = {
- // From initializeRuntime
- /** Current query being processed */
- query?: string
- /** Current expert name */
- expertName?: string
- /** Model being used */
- model?: string
- /** Runtime type (e.g., "docker", "local") */
- runtime?: string
- /** Runtime version */
- runtimeVersion?: string
-
- // Skill states (keyed by skill name)
- skills: Map
-
- // Docker states
- /** Docker build progress (latest) */
- dockerBuild?: DockerBuildState
- /** Docker container states (keyed by service name) */
- dockerContainers: Map
-
- // Proxy access (latest)
- proxyAccess?: ProxyAccessState
-
- // Streaming state
- streaming: StreamingState
-}
-
-/** Creates an empty initial runtime state */
-export function createInitialRuntimeState(): RuntimeState {
- return {
- skills: new Map(),
- dockerContainers: new Map(),
- streaming: { runs: {} },
- }
-}