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: {} }, - } -}