npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flow-state-dev/core

v0.5.0

Published

Isomorphic builders, type contracts, and item taxonomy for the flow-state-dev block framework.

Readme

@flow-state-dev/core

The building blocks. Define handlers, generators, evaluators, sequencers, routers, and flows — all with end-to-end type safety.

This is the foundation package. Every other package depends on it. It's isomorphic — runs in Node, the browser, edge runtimes, anywhere JavaScript runs.

Installation

pnpm add @flow-state-dev/core

What you can build

import { defineFlow, generator, handler, sequencer, router } from "@flow-state-dev/core";
import { z } from "zod";

A generator that calls an LLM with tools, history, and streaming:

const agent = generator({
  name: "agent",
  model: "intent/chat",
  prompt: "You are a helpful assistant.",
  inputSchema: z.object({ message: z.string() }),
  history: true,
  user: (input) => input.message,
  tools: [readDoc, writeDoc],
  itemVisibility: { client: true, history: true },
});

The prompt (and user) slots can also be authored in a separate .md file with YAML frontmatter and a LiquidJS body. Load it with loadPromptFile(...) and spread definePromptFile(pf) into the generator config. See the Prompts as Markdown reference.

A handler that validates and transforms:

const counter = handler({
  name: "counter",
  sessionStateSchema: z.object({ count: z.number().default(0) }),
  execute: async (input, ctx) => {
    await ctx.session.incState({ count: 1 });
    return input;
  },
});

A sequencer that composes them into a pipeline with error recovery:

const pipeline = sequencer({ name: "chat-pipeline", inputSchema })
  .step(analyzeInput)
  .stepIf((result) => result.needsContext, enrichWithContext)
  .step(agent)
  .step(counter)
  .rescue([{ when: [ModelError], block: fallback }]);

.rescue() is also a method on any block. someBlock.rescue([{ block: fallback }]) returns a block that recovers from its own failure and returns the handler's output instead of throwing — so a single step (or one forEach element, parallel branch, or router route) can fail in isolation while the rest of the chain continues. The chain-level .rescue() above is the same operation applied to the whole sequencer.

A later step can check whether an earlier one was recovered with ctx.wasRescued(blockName | blockDef) — without the recovered value carrying any marker.

Sequencers can optionally declare an outputSchema as a runtime contract on the composed output of the whole chain — validated on every exit path (tail, exitIf, rescue). Call .validate() at build time to catch structural drift early.

const summarize = sequencer({
  name: "summarize",
  inputSchema: z.object({ text: z.string() }),
  outputSchema: z.object({ summary: z.string(), wordCount: z.number() }),
}).step(summarizeBlock);

summarize.validate(); // throws if the tail shape drifts from the declared schema

A router that dispatches to different pipelines at runtime:

const dispatch = router({
  name: "mode-router",
  routes: [chatPipeline, planPipeline, reviewPipeline],
  execute: (input, ctx) => {
    const mode = ctx.session.state.mode;
    if (mode === "plan") return planPipeline;
    if (mode === "review") return reviewPipeline;
    return chatPipeline;
  },
});

A flow that ties it all together:

export default defineFlow({
  kind: "my-app",
  requireUser: true,
  actions: {
    chat: {
      inputSchema: z.object({ message: z.string() }),
      block: dispatch,
      userMessage: (input) => input.message,
    },
  },
  resources: {
    artifacts: defineResource({
      scope: "session",
      stateSchema: artifactSchema,
      writable: true,
    }),
  },
  session: {
    stateSchema: z.object({ mode: z.string().default("chat"), count: z.number().default(0) }),
    client: {
      derived: {
        artifactsList: (ctx) => /* derive list from resource state */,
      },
    },
  },
})();

Calling the factory with no argument yields the flow's one instance, whose id is its kind. A definition that should run as several configured copies declares cardinality: "collection" and gives each instance its own id (reviewFlow({ id: "review-east" })); a singleton refuses any other id. See Flow options.

Copies that differ by more than their name declare configSchema on the definition and pass config per copy (reviewFlow({ id: "review-east", config: { model: "opus" } })). The bag is parsed against the schema and frozen at that call; blocks read it as ctx.flow.config, and a block declares what it needs of any flow that installs it with flowConfigSchema. Both options are definition-time and instance-time halves of the same thing — see Flow options. A bag the copy can't take throws FlowConfigRefusalError: its message says why, and its issues list each reason as { path, code, message }, where an empty path means the whole bag and code is Zod's issue code, "no_config_schema" or "block_contributes" (a block's flowConfigSchema would change the bag at that path).

Exports

Main (@flow-state-dev/core)

Block builders:

  • handler(config) — Synchronous/async logic block
  • generator(config) — LLM call with framework-managed tool loop, streaming, and structured output repair (deterministic jsonrepair then LLM coercion that reshapes off-schema output to the schema; on by default, configured via repair.coerce / repair.coerce.model, defaulting to intent/utility)
    • Provider-native web search: set search: true (or a GeneratorSearchConfig). This is the model provider's built-in search, distinct from the @flow-state-dev/tools tools.search tool — the tools tier knob does not apply, and the generator's searchDepth ("low" | "medium" | "high", OpenAI searchContextSize) is a different field from the tools searchDepth ("basic" | "advanced"). See Web search.
    • Human-in-the-loop inside the tool loop: a generator tool can call ctx.suspend() to gate its own call. The request suspends like any sequencer gate, and on a durable resume the tool re-enters past the approval — prior turns and completed sibling tools replay from the item log, so the model is not re-called for them. Constraints: gate before side effects (the tool re-enters from the top on resume, so guard pre-gate work with runOnce), one approval gate per model turn (first-suspension-wins), and a gated tool can't be cacheable (the cache short-circuits before the tool body). See Generator and router suspend/resume.
  • evaluator(config) — a block that asks an evaluation model typed questions (choice, score, boolean) about one state and returns { answers }. Confidence appears on an answer only when the model reported it. See Evaluator.
  • choice(instructions, options) / score(instructions, levels) / boolean(instructions, criteria?) — question builders for evaluator.
  • sequencer(config) — Fluent composition DSL (21 methods: step, stepIf, parallel, forEach, forEachSideChain, doUntil, doWhile, map, tap, tapIf, rescue, branch, sideChain, sideChainIf, waitForSideChain, waitForCondition, loopBack, stepAll, stepAny, race, exitIf)
    • .forEach() / .forEachSideChain() take a per-item factory (item, index, ctx) => block in place of a block. A block built that way does not exist when defineFlow walks the graph, so declare what the factory can produce with blocks: [...] in the trailing options (IterationOptions / SideChainIterationOptions); declared blocks count as the step's children for the dispatcher address check and resource merging. Redundant on a call that passes a block directly.
  • router(config) — Runtime block selection from declared routes. Route names must be unique per router (validated at build). The selected branch dispatches through the same replay seam as sequencer children, so on a durable resume the branch decision stays stable — the framework validates the re-run selection against the recorded decision and throws RouteUnavailableError on a mismatch — and completed work inside the branch replays instead of re-executing. A router whose branch can suspend needs a pure execute selector (no side effects, no ambient state reads); see Control-flow determinism
  • dispatcher(config) — Send one dispatch to one entry the flow declares, in a child session derived from a key or in an existing session named by id. Returns { sessionId, requestId, adopted }; with wait: true, waits for the request it started and returns { status, output?, error?, requestId } (completed, failed, or running at the host's default deadline, which stops nothing). See Dispatches, entries, and dispatcher()

Block methods (available on every BlockDefinition):

  • .connectInput(mapper) — adapt input shape at the call boundary
  • .connectOutput(mapper) — transform output shape at the call boundary
  • .mapModelOutput(mapper) — when the block is used as a generator tool, supply a model-visible string representation of its output
  • .as({ name?, description? }) — a copy of the block under a new name and/or description, for showing it to a model as a tool or registering it under a key that must match its name. The new name is used everywhere (tool calls, items, traces, resume); the original is unchanged. Adds nothing at run time, unlike .asTool()
  • .asTool(opts?) — wrap the block so it emits a tool_output item when run from a sequencer step (same envelope and lifecycle as the AI SDK tool-loop path)

Background work lifetime: .sideChain(), .sideChainIf(), and .forEachSideChain() queue tasks on a per-request pool, not the sequencer that dispatched them. Inner sequencers do not auto-await their own background work before returning; sibling sequencers run their tasks concurrently. The request executor drains the pool before terminal status — on every outcome, and repeatedly until no task is left, so a task that queues more background work is waited on too. Use .waitForSideChain() when an inner step depends on a queued task completing first — it drains only the calling sequencer's contributions.

Event-driven waits: .waitForCondition(predicate, { timeoutMs, wakeOn? }) suspends the sequencer until a synchronous predicate over the request's item stream returns true (or the timeout fires). Yields { timedOut: boolean }. Use it to coordinate with side-channel state — a worker writing an artifact, a task-board flipping a status, a projected actor resuming a paused review. Predicate helpers ship in @flow-state-dev/core/items: whenResourceChanged({ scope, path, changeType? }), whenResourceMatching({ scope, pattern }) (tiny glob with * and **), and whenAnyItem(predicate) as the generic escape hatch. The optional wakeOn filter lets high-fanout patterns skip predicate re-evaluation on irrelevant item types; @flow-state-dev/orchestration ships onTaskChangeFor(collectionId) for collection-bound waiters.

Waiting for a user: ctx.suspendForUser({ kind, prompt, deadline?, title?, resumeSchema?, data? }) stops the request until a person answers, when your code reads the answer on its next line. kind is "approval", "review" or "input". It needs durable execution, as ctx.suspend() does.

const asked = await ctx.suspendForUser({ kind: "input", prompt: "Which region?", deadline: 2 * DAY });
if (asked.cancelled) return { skipped: true }; // the task was cancelled; nothing here is kept
const region = asked.answer;
  • An answer comes back as { cancelled: false, answer }; an approval as { cancelled: false, approved: true } or { cancelled: false, approved: false, deadline }.
  • deadline is milliseconds from the call. Leave it out and the host's userDeadlineMs applies; with neither, the stop waits until it is answered, stopped or cancelled.
  • At a deadline an approval reads as a no with deadline: true. A review or an input throws SuspensionTimeoutError: catch it to go ahead, end or ask again, or leave it and the try fails.
  • When the task the request works is cancelled while it waits, the call returns { cancelled: true }, and any later suspendForUser in that run returns it at once.

A raw ctx.suspend({ reason: "user_approval" | "user_input" }) is the same kind of stop, read by its reason, with its own timeoutMs as its only deadline: an input that passes it throws SuspensionTimeoutError, and a cancel throws SuspensionRejectedError with cancelled: true. isUserSuspension(suspension) and suspendForUserKindOf(suspension) (from @flow-state-dev/core/types) say whether a suspension waits for a user, and for what.

Flow:

Concurrency policy:

Any entry can declare a concurrency policy that decides what happens when two requests collide on the same key (the session by default). Set it on the entry — an action, an internal entry, a task-board entry, or a webhook / schedule binding — or set a flow-wide default via RequestConfig.concurrency (flow.request.concurrency); resolution is entry.concurrency ?? flow.request.concurrency ?? "allow", the same ladder for every dispatch type.

The policy is enforced by the in-process runner, or across worker processes when the worker adapter supplies a shared lease backend (WorkerAdapter.leaseBackend), as bullmqWorker does. A deployment that hands dispatches to an external queue whose adapter supplies no shared lease backend skips it, so requests on the same key can overlap there.

ConcurrencyConfig is either a bare policy name ("allow" | "queue" | "reject" | "hold" | "defer") or { policy, key }, where key is "session" (default), "user", "none", or a (ctx) => string | undefined function. A key that resolves to undefined means no arbitration — the request runs as allow. The default is allow (run concurrently). hold runs at once and marks the key busy; defer waits until nothing on the key is running or waiting, then runs.

defineFlow({
  kind: "support-chat",
  request: { concurrency: "queue" },                              // flow-wide default
  actions: {
    respond:     { block: respondPipeline },                      // inherits "queue"
    syncInvoice: { block: invoicePipeline,
                   concurrency: { policy: "reject", key: "user" } },
  },
});

Exported types: ConcurrencyConfig, ConcurrencyKey, ConcurrencyKeyContext, ConcurrencyPolicyName, plus the validateConcurrencyConfig validator. The policy is enforced once at the host dispatch seam, so every transport inherits it. See the Concurrency policies reference.

Utility block factories (utility.*):

  • utility.contextReducer(config) — Generator factory for distill, denoise, or compress context transformation modes with mode-specific default output schemas ({ distilled, keyPoints }, { cleaned, removedCategories? }, { compressed, compressionRatio?, dropped? })
  • utility.summarizer(config) — Generator factory for brief, detailed, or executive summaries with optional focus objectives and a default { summary, keyPoints? } output contract
  • utility.decomposer(config) — Generator factory that breaks broad requests into executable tasks using a default { tasks: [{ id, goal, deps?, priority? }] } output contract
  • utility.analyzer(config) — Generator factory for artifact critique/evaluation with configurable criteria and a default { findings, score?, recommendation? } output contract
  • utility.combiner(config) — Handler factory for deterministic artifact merging via concatenation, deduplication, and structural normalization with default { combined, mergeNotes? } output
  • utility.intentClassifier(config) — Generator factory for bounded intent classification with required category descriptions and default { category, confidence, reasoning? } output contract
  • utility.intentRouter(config) — Sequencer factory that composes intentClassifier + router into classification-driven branching with category descriptions, handlers, optional confidenceThreshold, and optional fallback routing
  • utility.keyedRouter(config) — Router factory for the "pick a block from a Record<string, Block> by string key" case. Throws with the registered keys (or routes to fallback) when the selected key is unregistered. Input adaptation belongs on the routed blocks via .connectInput (BP-013)
  • utility.cascadingRouter(config) — Sequencer factory that walks a tree of evaluator choice questions. An edge opens only when the model chose it and reported confidence at or above the edge's optional minConfidence; every other outcome runs the required ambiguous block. On evaluation models that report no confidence, every edge goes to ambiguous. See cascadingRouter.
  • utility.memoryExtractor(config) — Generator factory for stateless durable-memory extraction with a default { memories: Array<{ type, content, confidence?, source? }> } output contract (type ∈ fact | preference | constraint | decision)
  • utility.consultSessionHistoryTool({ name?, model?, budgetTokens?, description?, target? }) — A tool for a generator's tools that answers a question from another of the user's conversations without waking it. Input { sessionId, question } (utility.consultSessionHistoryInputSchema). It reads that session's finished turns through ctx.requestHost.sessions.history, newest first, up to budgetTokens (default utility.CONSULT_DEFAULT_BUDGET_TOKENS, 24,000), and answers on one tool-less generator on model (default "intent/utility"). Returns { reading, of, asOf: { requestId, at } | null, turnsRead, earlierTurnsLeftOut, cut, running }, or { ok: false, error } starting self (the asking session) or no_session (not the user's, missing, or a host with no sessions.history). The other session gains no items or requests, and the quoted history reaches the model as a record, not instructions. The answering generator's items stay out of the asking session's history and client view. target: { inputSchema, resolve } lets a wrapper name the session its own way. Types: utility.ConsultSessionHistoryOptions, utility.ConsultTarget. See consultSessionHistoryTool
  • utility.historySummary({ summarize, name? }) — Keeps a running summary of the turns before a conversation's history window. Returns { capability, fold, resource }: install capability in the answering generator's uses (it adds an <earlier-conversation> context section and starts that generator's history after the summarized turns), and run fold before the generator (.tap(earlier.fold)). summarize takes { summary: string | null, turns: LLMMessage[] } and returns { summary }; utility.summarizer fits. A failed summarize call doesn't fail the turn; the next turn retries. Turn the history narrowing off with capability.presets({ history: false }). See Summarizing turns past the window

Every generator-based utility above accepts an optional itemVisibility ({ client: boolean; history: boolean }) to control whether output is surfaced to the client/history. All default to unset (silent — output flows only via graph edges). Set explicitly to opt in when the utility should be user-facing.

Resources:

  • defineResource(config) — Portable resource definition. Requires scope: "session" | "user" | "org". Register on a flow's or block's resources map.
    • Supports optional content/contentFile (mutually exclusive), render, llmReadable, and llmWritable for resource content workflows. contentFile and file-path contentTemplate accept a bare string (resolved from the working directory) or an AnchoredPath — { path, importerUrl: import.meta.url } — resolved relative to the declaring module first, with a working-directory fallback
    • stateSchema may normalize (fill a .default(), strip an undeclared key, map a retired enum value) but has to settle: parsing its own output must yield that same output. A .transform() that returns something new on each pass would move the stored value on every write, so a write that isn't a fixed point of the schema is refused with ValidationError rather than stored. See the schema has to settle.
    • prefetchMode?: 'eager' | 'lazy' (default 'eager') — 'lazy' defers the load until the declaring block dispatches. Once the resource is resolved its ref.state getter is synchronous. Declaring 'lazy' on a flow-level single resource throws at build time (no per-block load trigger).
    • sharedToLineage?: boolean (default false, scope: "session" only) — give the resource ONE identity across a session lineage, so a session, every session dispatched under it, and their own children resolve the same resource through the ordinary resource API. Session state is never shared, and sharing does not serialize writes — two children writing one shared resource is ordinary same-resource contention fenced by expectedVersion. true at user/org scope throws at build time (those scopes already span every session the principal touches). Also accepted by defineResourceCollection, applying to every instance. Named sharedToWorkstream before 0.5.
    • reactTo?: { created?, stateUpdated?, deleted?, contentUpdated? } — bind a block (handler/generator/sequencer) to a mutation. Each entry is a bare block or { block, when }. A state binding (created/stateUpdated/deleted) runs with a ResourceChange payload (key, ref, kind, state, prevState, evicted), typed with resourceChangeSchema(stateSchema). A contentUpdated binding runs after a server-side content write with a minimal ResourceContentChange payload (key, ref, kind), typed with resourceContentChangeSchema(); the block readContent()s for the fresh body. The block runs blocking inside the originating turn. See the Reactive blocks reference.
    • Runtime ResourceRef provides state (sync getter) plus patchState, setState, updateState, incState / pushState (below), and getOrPatchState(key, compute) — get-or-compute over state: returns state[key] if present, else runs compute, patches the result under key, and returns it (callback runs only on a miss, so a fetch happens at most once per stored key and downstream readers reuse the stored copy). A stored null is a hit; a compute resolving to undefined stores nothing. No TTL — a per-resource data spine, not a cache. Concurrent misses on the same key within a request are single-flighted (they share one compute, so a fanned-out read issues one upstream fetch); distinct keys still compute in parallel.
    • incState(increments) / pushState(field, value) — add to number-valued state fields (incState({ calls: 1 })), or append one value to an array-valued one (pushState("errors", "rate_limited")), under the same names scope state uses. Both resolve to void; read the result off ref.state. Each call is a single guarded mutation, so two callers incrementing one counter both land — on the memory, SQLite and Postgres stores, which compare and swap inside the store; the filesystem store holds the guard per key on the store instance, which covers every context sharing that instance but does not coordinate two stores pointed at one directory. Both are keyed to TState on a ResourceRef<TState> / ResourceContext<TState> whose state type is written out — number fields for incState, array fields and their element type for pushState. A handle read off ctx.resources.<name> is ResourceRef<any>, so only the delta-is-a-number check survives there. They refuse rather than corrupt: incrementing a field that holds something other than a number, or appending to one that holds something other than a list, throws FlowError (code: "resource_delta_refused", retryable: false) and leaves the stored value alone. A multi-field incState applies wholly or not at all. An absent or null field is that field's empty state, not a wrong kind of value — it starts from 0 / []. incState also refuses a result that is not finite, since two finite operands can overflow to Infinity, which z.number() accepts but the stores disagree about how to persist. Both honor writable: false, and a delta that commits is validated against stateSchema like any other state write.
    • updateStateWith(ref, updater) / withOutcome(run, updater) (@flow-state-dev/core/helpers) — run a state update whose callback returns what it did, as { state, result }, instead of assigning it to a variable outside the callback. On the CAS path an updater can run more than once, so an outward-assigned value can describe an attempt that never committed; these return the result belonging to the invocation that did (or undefined if none completed). withOutcome takes any mutation runner — ref.updateState, a scope's atomicState, or your own wrapper — so one helper covers every retry entry point. scripts/validate-updater-purity.mjs (in pnpm typecheck) rejects the common outward-write forms as a backstop — it catches the naive shapes, not every possible one.
    • readCommitted(ref, project) (@flow-state-dev/core/helpers) — read a resource's state as it is stored now, not as this request first read it, and return project(state). A request's resource reads are the snapshot it took when it first touched each key, so a value another request wrote since is invisible to ref.state; readCommitted sees it, and writes nothing. Use it when a block polls a value another request will change. It needs a writable resource or collection ref: on one declared writable: false it is refused with resource_read_only and project never runs. On a writable resource, project always runs, so the result is undefined only when project returns undefined; a read that can't happen (a collection row deleted since it was fetched) throws instead.
  • defineResourceCollection(config) — Dynamic resource collection with pattern-based keys (files/*, files/**, [topic]/observations), required scope: "session" | "user" | "org", optional maxInstances/eviction, lifecycle hooks, and reactTo (same { created?, stateUpdated?, deleted?, contentUpdated? } shape as defineResource; supersedes the onInstance* callbacks for the block case)
    • prefetchMode?: 'eager' | 'lazy' (default 'eager') — a loading-cost knob, not an API-shape knob. Eager preloads the whole prefix into a per-request cache so reads resolve instantly; 'lazy' reads per access from the store. The call shape is identical in both modes: get/getOptional/list/count all return Promises (always await them), and the mutations create/getOrCreate/upsert/delete were already async. Flipping prefetchMode needs no call-site changes. 'lazy' requires eviction: 'none' (a partial cache can't drive eviction) and throws at build time otherwise.
    • Runtime ResourceCollectionRef provides create(), get(), getOrCreate(), upsert(), list(), findBy(), delete(), count()
    • indexes?: { name: (key, state) => value | { version, value } } declares fields the items can be found by; findBy(name, value) returns the matching instances in key order, behind the same owner rules as list(). Values are strings, numbers or booleans (or arrays of them), compared by equality with types kept apart. SQLite and Postgres read a stored index; memory and filesystem stores scan the collection. The first lookup in a scope builds the index from existing items; bumping version rebuilds it. See Looking items up by a field
    • create(key, initial, { replace: true }) — overwrites an existing instance instead of throwing. setState semantics; Zod .default(null) fills nullables on both the create and replace branches. maxInstances only checked when adding a new instance. Use for setup/reset paths. A writable: false collection refuses that overwrite the same way setState does; a missing key creates.
    • upsert(key, update, createOnly?) — patch-or-create. On exists: applies update via patchState semantics (other fields preserved). On missing: creates with { ...createOnly, ...update } (update wins on overlap). The createOnly extras fill fields you only need to supply at creation time. Use for incremental-update paths that need to handle first-touch in a single call.
    • "If-exists / if-missing" summary: create throws / create({ replace }) replaces / getOrCreate returns as-is / upsert patches — all four create on missing; replace-on-existing and delete refuse when writable: false.
  • defineFacetedCollection(config) — Defines a resource collection whose bodies are classified once on write by the evaluator you pass (evaluator, with a fixed questions object; it receives the body string), storing its answers as facets beside your stateSchema fields. name is the accessor it registers under. Returns { collection, search, reindex, resources }: search takes one optional value per choice question plus minConfidence and returns { keys } with no model call; reindex takes { force? }, classifies rows without facets (every row with force) and returns { reindexed, failed }, retrying nothing; resources is the collection under name plus the evaluator's declared resources, for defineFlow({ resources }). See Find by facets.
    • Throws when defined, naming the problem, when given: a key pattern with parameters, such as [topic]/observations (use a wildcard pattern like tickets/*); writable: false; client.content.create or client.content.update; your own reactTo.contentUpdated (created, stateUpdated and deleted are yours); a stateSchema that isn't a z.object(), or that already has facets or indexedAs; a question with the id minConfidence; a value that isn't an evaluator block, or an evaluator whose questions are a function; an evaluator whose input schema rejects a string (adapt the body with connectInput); an evaluator that declares flowConfigSchema, a resource under name, or a single resource (not a collection) with prefetchMode: "lazy".
    • Client access: reads and deletes only. Write bodies through a flow action.
  • isDefinedResourceCollection(value) — Type guard for collection definitions
  • isCollectionConfig(value) / resourceStorageKeys(configs) (@flow-state-dev/core/types) — The storage-identity rule the engine persists by and defineFlow's collision check applies. isCollectionConfig is structural: anything with a string pattern is stored as a collection, keyed on that pattern. resourceStorageKeys maps each accessor to the slot it persists to: a single resource's ref, else the first accessor its definition appears under.

Capabilities:

  • defineCapability(config) — Bundle resources, state schemas, targets, and helper functions under a single name. Blocks declare capabilities via uses: [cap] and the framework merges everything transitively.
    • fns: (ctx) => ({ ... }) — Helper functions exposed at ctx.cap.{name}.{fn}, memoized on first access
    • presets — Named opt-in/opt-out bundles of any block config surface. Use .presets({ name: true/false }) to configure
    • config: { schema?, resolve } — Open, typed configuration. The resolver maps a validated value onto a block surface (like a preset, but value-carrying). Consumers pass it with .config(value), which composes with .presets() in either order
    • .with(bag) — The normalized consumer builder. Collapses .config() and .presets() into one flat call: preset-named keys become preset toggles, the rest become the config value. cap.with({ allowed: ["x"], dynamicActivation: true }) ≡ cap.config({ allowed: ["x"] }).presets({ dynamicActivation: true }). .config/.presets remain the underlying primitives; a preset name colliding with a config field is a defineCapability() error
    • uses — Capabilities can depend on other capabilities (transitive composition with diamond dedup)
    • Preset history key (generator-only): an ItemQuery, or a function of the block context returning one, added to every ctx.session.items.history() read of the generator that installs the capability. The read's own keys win; among capabilities, later keys win. utility.historySummary uses it to pass { afterRequestId }
    • Preset tool slots (generator-only): tools is a grant a consuming generator can withhold — a generator that declares its own tools: gets that list and nothing else. controlTools reaches the model whatever the generator declares, for a control the generator's own configuration asked for (a loader the capability builds and never exports, say)
    • Factory pattern: wrap defineCapability() in a function for parameterized capabilities

Capability schema forwarding:

When a block lists a capability in uses, the capability's declared schemas flow into the block's ctx types at factory time. No re-declaration on the block is needed. The forwarded axes are sessionStateSchema, resources (resource handles), targetStateSchemas, sequencerStateSchema (from presets), and stateSchema (the block's own state, ctx.self — valid on any block kind). Block-own declarations merge in; for most axes the block wins on key collision, but stateSchema requires a shared field to be the same schema reference instead (matching the resources/targetStateSchemas merges) — a duplicate field with a different reference throws at build time rather than one side silently winning.

const myCap = defineCapability({
  name: "my-cap",
  sessionStateSchema: z.object({ ticker: z.string() }),
  fns: (ctx) => ({ currentTicker: () => ctx.session.state.ticker }),
});

const myHandler = handler({
  name: "my-handler",
  uses: [myCap],
  execute: async (_input, ctx) => {
    // ctx.session.state.ticker — string, from the capability
    const t = ctx.session.state.ticker;
  },
});

Forwarding is direct-only: inner capabilities used by myCap do not propagate to myHandler. Dynamic uses entries (functions) contribute at runtime but not to types.

Tool-result memoization:

  • createToolCacheCapability(options?) — Capability that installs a per-request LRU store on the active context so any tool block declaring cacheable serves identical calls from cache. Errors are never cached; identical in-flight calls in the same request coalesce.
  • createInMemoryToolCacheStore(options?) — Standalone store factory for advanced wiring (e.g. binding a per-board run-scoped store directly).
  • bindToolCacheStore(ctx, store) — Attach a store to a context without going through the capability path.
  • canonicalizeToolArgs(value) — Deterministic JSON canonicalizer for custom keyFns that want the substrate's default normalization.
  • Types: ToolCacheStore, ToolCacheEntry, ToolCacheAccessor, CreateToolCacheCapabilityOptions.
  • See Flow policy for the full guide, including when to mark a tool cacheable and how Task Board auto-installs the capability.

Context & client data:

  • contextFn(schemas, fn) — Typed context function for generators (scope-aware, portable)
  • client on scope configs — Per-scope client view: expose: string[] (verbatim passthrough by field name) and derived: { name: fn } (compute functions receive { state, resources }). State without a client block is private.

Prompt formatters (@flow-state-dev/core/prompt):

  • section, list, keyValues, table, entries, codeBlock, join, when — Composable text formatters for building clean LLM context. section takes a string title (default ##) or { title, level } to nest under another section; table renders an array of records as a Markdown table. The same keyValues / list / table shapes are auto-registered as fsd_* filters inside .md prompt templates.

Concurrency (@flow-state-dev/core):

  • mapLimit(values, maxConcurrency, mapper) — bounded-concurrency async fan-out preserving input order. Use it for async work inside a handler (.parallel fans out blocks, not in-handler async).
  • xmlTag(name, content), renderTaggedContext(tagged, order) — XML tag rendering used by object-form generator context
  • validateTagName(name), RESERVED_TAG_NAMES — Reserved-tag list and validator for object-form context keys

Once per process (@flow-state-dev/core):

  • firstInProcess(key) — returns true the first time key is claimed in the process and false on every later call. Use it to print a boot diagnostic once per server start. The record is process-global and lasts for the life of the process: it lives on globalThis, so it survives a dev server re-evaluating modules on each edit, and it is never cleared. Every caller shares one set, so namespace keys by package ("my-pkg/unattended-board"). It only answers "first time?". It is not dev-only and ignores FSD_QUIET_WARNINGS; the caller decides what to print.

Object-form generator context:

generator({ context: { ... } }) accepts an object whose keys become XML tag names. Multiple sources (the generator itself plus capabilities installed via uses) that contribute to the same key aggregate inside a single tag, instead of producing scattered sections.

generator({
  prompt: "You are a research assistant.",
  context: {
    documents: [doc1, doc2],
    userPreferences: () => loadPrefs(),
    memory: { shortTerm: items, longTerm: () => loadLongTerm() },
  },
  uses: [capA, capB], // each may also contribute `documents`
});
// Renders as one combined system message:
//   You are a research assistant.
//
//   <documents>
//     ...all documents from the generator + capA + capB...
//   </documents>
//   <user-preferences>...</user-preferences>
//   <memory>
//     <short-term>...</short-term>
//     <long-term>...</long-term>
//   </memory>

Keys may be authored as camelCase, snake_case, or kebab-case (all normalize to kebab-case). Values may be strings, string arrays, nested objects (recursive — produces nested tags), functions resolved at render time, or null placeholders that reserve order but emit nothing if unfilled. String leaves are HTML-escaped so < / > / & in user data don't get read as tags. The original array form is unchanged. See the blocks reference for the full contract.

Slot types:

  • ToolsSlot — Tools accepted by generators: static array or (ctx) => tools[]
  • UsesSlot — Capabilities accepted by blocks: static array or (ctx) => caps[]
  • InstructionsSlot<TInput> — Pattern-level instructions: static string or (input, ctx) => string | Promise<string>. Parameterize with the pattern's input type to recover a typed input in the callback.

Type helpers:

  • StateOf<T> — Extract state type from schema or resource
  • ContextOf<T, Kind> — Get context handle type for scope/resource
  • ResourceContext<T> — Resource context type
  • BlockInput<T> / BlockOutput<T> — Infer block I/O types
  • BlockDefinition — The fully-typed return interface of handler(), generator(), evaluator(), sequencer(), and router(). Generics default to ZodTypeAny, so unparameterized BlockDefinition is the unconstrained "any block" form — useful when an app-level factory needs to accept or return a block without restating the framework's generics.
  • BlockKind — "handler" | "generator" | "evaluator" | "sequencer" | "router" union — useful when writing dispatchers that switch on block.kind.
  • EvaluatorAnswer, EvaluatorAnswers<Q> — the answer types, for code that consumes an evaluator's output. EvaluationModel is the evaluation model an evaluator accepts.
  • ModelResolver.resolveEvaluationModel(modelId, blockName?) — optional. A custom resolver needs it only to resolve evaluator model strings. See With a custom model resolver.
  • BlockContext — The full block-context interface (the type of ctx in execute). Generic over the four scope-state types, declared resources, sequencer state, and parent input.
  • BlockResult<TOutput> — The handler execute return-value union.
  • SessionScopeHandle<TState> / UserScopeHandle<TState> / OrgScopeHandle<TState> / RequestScopeHandle<TState> — The scope handles ctx.session / ctx.user / etc. resolve to. Use to type a ctx slice (e.g. (input, ctx: { session: SessionScopeHandle<MySessionState> }) => …) instead of hand-rolling a { session: { patchState: ... } } shape.
    • ctx.session.window — The history window the request loaded: { turns, startRequestId? }. startRequestId is the first turn the window holds, present only once earlier turns have fallen out of it. ctx.session.items.turns(query?) lists the window's turns, plus any that have not settled, oldest first as HistoryTurn (requestId, startedAtMs, settled, messages); pass { requestIds } to read named turns even after they left the window. items.history({ afterRequestId }) starts the history after the named turn
  • ScopeStateOps<TState> — The state-mutation interface every scope handle exposes (patchState, setState, setStateRecord, etc.).
  • LooseBlockContext<TSessionState> — A partial BlockContext: typed ctx.session state, ctx.user / ctx.request / optional ctx.org with untyped state, ctx.resources as any, and no other slots. Use for helper functions that take a block's ctx as a parameter. A full BlockContext parameter checks every slot, so it rejects the ctx of a block with a typed ctx.targets (one that declares targetStateSchemas or lists a capability in uses). Declared resources don't cause this. A LooseBlockContext helper accepts any block's ctx, and inside the block, ctx keeps its own types.

handler.withDefaults({...})

Partially-applied handler constructor. Bakes in common config so a family of sibling handlers can share scaffolding without restating it per call.

import { handler } from "@flow-state-dev/core";
import { z } from "zod";

const memoHandler = handler.withDefaults({
  sessionStateSchema,
  resources: { memos: memosCollection },
  outputSchema: z.void(),
});

export const commitBullMemo = memoHandler({
  name: "commit-memo-p2-bull",
  inputSchema: bullThesisSchema,
  execute: async (thesis, ctx) => {
    // ctx.session.state is typed from sessionStateSchema
    // ctx.resources.memos is typed from the resources default
    await ctx.resources.memos.get("p2/bull").patchState({ ... });
  },
});

// Per-call overrides win: pass `outputSchema` again to replace the default.
export const markError = memoHandler({
  name: "mark-error",
  inputSchema: z.unknown(),
  outputSchema: z.object({ status: z.literal("error"), text: z.string() }),
  execute: async (_, ctx) => ({ status: "error" as const, text: "..." }),
});

Defaultable fields: sessionStateSchema, userStateSchema, orgStateSchema, requestStateSchema, sequencerStateSchema, resources, outputSchema, uses. name, inputSchema, execute, and description are excluded — those vary per block.

Prompt files (@flow-state-dev/core/prompt-file, @flow-state-dev/engine/prompt-file)

Author a generator's prompt as a .md file. The isomorphic subpath exports parsePromptFile(text, options?), definePromptFile(pf), isPromptFile(value), and the PromptFile / PromptFileConfig / PromptFileParseError / PromptFileLoadError types. The Node-only subpath exports loadPromptFile(specifier, importerUrl, options?), which reads the file and auto-registers sibling .md files as partials; only this subpath imports node:fs, so browser/bundled consumers use parsePromptFile with raw text plus an explicit partials map.

Resolution rule. Relative specifiers resolve against the caller's import.meta.url; absolute specifiers are used as-is (importerUrl ignored); createPromptLoader joins every relPath onto its absolute baseDir. Resolution never consults the process working directory — compute baseDir with resolveBaseDir(candidates, { expect? }) (first candidate dir that exists and contains the expect probe; throws listing all candidates when none qualifies) composed with moduleDir(importerUrl, relative?) (the module's directory, or undefined when a bundler has rewritten import.meta.url to a non-file: URL). Module-relative candidate first, process.cwd()-derived fallback for bundled runtimes that pin cwd (Next.js dev/build).

Two ergonomic shortcuts cut the boilerplate:

  • Pass the PromptFile straight to prompt instead of spreading definePromptFile(pf). generator({ prompt: loadPromptFile(...), model }) expands the file's user / caching / maxTokens / temperature / name / description into the config; any sibling field you set explicitly wins (same precedence as ...definePromptFile(pf), <overrides>).
  • createPromptLoader(baseDir, options?) (Node subpath) captures an absolute baseDir plus shared partialsDir / filters once and returns a load(relPath) function, so call sites drop the repeated import.meta.url argument. Per-call filters merge over the loader's shared filters.
import { generator } from "@flow-state-dev/core";
import {
  createPromptLoader,
  moduleDir,
  resolveBaseDir,
} from "@flow-state-dev/engine/prompt-file";

const PROMPT_ROOT = resolveBaseDir(
  [moduleDir(import.meta.url, "./prompts"), path.resolve(process.cwd(), "src/prompts")],
  { expect: "_partials" },
);
const load = createPromptLoader(PROMPT_ROOT);
const analyst = generator({ name: "analyst", model, prompt: load("analyst.prompt.md") });

Resource content templates. The same .md format can render resource content against state. The Node subpath exports loadResourceTemplate(specifier, importerUrl, options?) and the isomorphic subpath exports parseResourceTemplate(text, options?), renderResourceTemplate(template, state), and the isResourceTemplate(value) guard. Wire them via contentTemplate (build-time file — a parsed template, a working-directory-relative string path, or an AnchoredPath resolved relative to the declaring module) or contentTemplateRef (live-editable resource) on defineResource() and defineResourceCollection(). See the Resource content from Markdown templates reference.

Voice Provider

VoiceProvider is a single, ability-flagged interface a flow wires to handle one or more voice surfaces: speak (batch TTS), speakStream (streaming TTS), transcribe (STT), and listVoices (catalog). Each provider declares which abilities it supports via the abilities field; runtime type guards (canSpeak, canSpeakStream, canTranscribe, canListVoices) narrow the provider so the matching method is callable without !. Errors thrown by providers carry a discriminated VoiceError.kind so callers can branch on category instead of parsing messages.

This surface replaces the previous resolver-factory pattern (createAiSdkSpeechResolver, createAiSdkTranscriptionResolver) — those helpers and their SpeechResolver / TranscriptionResolver types are removed from core. The field is named abilities (not capabilities) to avoid colliding with the framework's first-class Capability concept (defineCapability, uses: [cap]).

import { canSpeak, type VoiceProvider } from "@flow-state-dev/core";

async function maybeSpeak(provider: VoiceProvider, text: string) {
  if (canSpeak(provider)) {
    const { audio, mediaType } = await provider.speak({ text });
    return { audio, mediaType };
  }
  return null;
}

Exports from the main package and @flow-state-dev/core/types:

  • Core contract: VoiceProvider, VoiceAbilities, SpeakOptions, SpeakResult, SpeakChunk, TranscribeOptions, TranscribeResult, VoiceInfo
  • Narrowing interfaces: SpeakCapable, SpeakStreamCapable, TranscribeCapable, ListVoicesCapable
  • Type guards: canSpeak, canSpeakStream, canTranscribe, canListVoices
  • Errors: VoiceError, VoiceErrorKind
  • Composite factory: createCompositeVoiceProvider builds a synthetic provider that delegates each ability to a different underlying provider

Per-provider implementations live in separate packages — @flow-state-dev/voice-openai is the first, with @flow-state-dev/voice-elevenlabs to follow.

Types (@flow-state-dev/core/types)

Block, flow, resource, scope, streaming, and model type definitions. Use this subpath for type-only imports.

The subscription event. subscriptionEventSchema (type SubscriptionEvent) is the one shape an outside event takes before it is routed to the sessions that follow its thread: provider, threads (its thread keys, widest first, at least one), kind, object ({ id, version }), sender ({ id, name, role }, where role is one of SUBSCRIPTION_SENDER_ROLES: member, collaborator, bot, outsider or unknown), audience (public or private), title, text (at most MAX_SUBSCRIPTION_EVENT_TEXT, 4,000 characters), labels, url, received and at (epoch ms), and optional deliveryId and closes: true (the last and narrowest of threads closed with this event, as a merged PR does; its repository stays open). The schema is strict, so a key it doesn't name, such as raw headers, is refused. A webhook or a poll maps each event into it; workforce's subscriptions route it.

defineResourceCollection accepts writable?: boolean (default true) and llmReadable?: boolean / llmWritable?: boolean (default false). Declared once, they apply to every instance. writable: false refuses instance patchState / setState / updateState / incState / pushState, collection upsert on an existing key, create(key, initial, { replace: true }) when that key already exists, delete(key) (including when the key is already absent), and instance writeContent; those writes throw a non-retryable FlowError (code resource_read_only) with Resource "<storageKey>" is read-only (state) or Resource "<storageKey>" content is read-only (content). create of a key that does not exist succeeds, including create(key, initial, { replace: true }) when the key is missing. getOrCreate of a missing key creates; of an existing key it returns the instance and does not write. llmReadable exposes instance content to readResourceContentTool() and content search (grepResourceContent / searchResources); llmWritable lets writeResourceContentTool() overwrite an instance body. The write tool is available when llmWritable is true. The write fails when writable is false. The generic tools address resources by scope-qualified uri (e.g. session/files/readme.md). A content-bearing collection uses those tools; keep collection-specific tools for domain logic they do not cover.

defineResourceCollection accepts a prefetchWindow?: number (default 0) that inlines the first N items in the snapshot's prefetched window in lexicographic storage-key order. Per-item clientData in the window appears only when client.state.read: true is also set. CollectionStateClientConfig controls per-item state visibility separately from content; single resources don't accept client.state (state visibility is governed by client.data on those).

defineResourceCollection accepts ownerPrivate?: { param: string }, which serves each row only to the user its owner segment names. Build that segment with ownerSegment(userId) (~ plus the escaped user id) and pass it as param's value in the key. Key segments beginning ~ are reserved for owner-keyed collections (owner-private and owner-writes) in every app: no other collection lists, reads or writes them. The pattern must name param exactly once and must not use **, and the collection may not enable a browser read; each is refused at definition. Once a flow declaring one is registered, Engine refuses any flow whose collection in the same scope could reach its keys. Engine also refuses, in every app, a flow declaring a single resource whose key (its ref, else its accessor name) has a segment beginning ~. See Owner-private collections.

ownerWrites?: { param: string } takes the same key shape and the same startup fence, but every reader the scope serves reads each row, browser routes included, and only the user its owner segment names creates, updates or deletes it. Anyone else's write, through any handle or ref or the browser update and delete routes, throws A row of an owner-writes collection is written only by the user it belongs to. and changes nothing. A collection can't declare both modes, and an owner-writes collection can't set an eviction policy other than "none". See Owner-writes collections.

Set client: { live: true } to stream each mutation's projected clientData as an inline delta that the client merges mid-stream without a refetch (the resource-side analog of state_change). It requires the resource's clientData to be client-visible (state.read: true or a projection on collections; a projection on single resources). lifecycleSchema(statuses) is a convenience export that returns a status enum plus nullable startedAt / completedAt / errorMessage fields to spread into a status-bearing stateSchema.

defineResource and defineResourceCollection carry a derived client-projection type alongside the state type. ClientDataOf<typeof def> extracts it — the Pick from expose, the Omit from exclude, the return type of data, or the full state for the identity default. Pass it to the React hooks (useResource<T>, useResourceCollectionItem<T>, …) so clientData is typed instead of unknown. This is a type-level brand only; the runtime payload stays JsonValue. For data projections, annotate the function's return so the type is captured precisely.

Projected resource collections. defineProjectedResourceCollection({ pattern, scope, stateSchema, read, search, ... }) defines a read-only view of records the app already owns. The framework does not store a copy: ctx.resources.<coll>.get(key) / .getOptional(key) and the client state/content routes call the required read({ key, ctx }) hook and validate the result through stateSchema. search({ query, ctx }) backs list, searchResources (when llmReadable), and the list route. The runtime ref has no create / upsert / delete; client write routes are closed; client.content.create / update / delete is a build-time error. Patterns are wildcard-only (* or **). Scope is session | user | org. The hook ctx carries a server-derived userId, scope, and tenantId. Content templates and client projection work the same as on a store-backed collection.

The harness contract. A harness is a coding agent driven as a block: you hand it a prompt, it runs its own agentic loop, and it hands back a handle describing the run.

  • Runtime schemas, from the package root: harnessRunInputSchema, harnessRunHandleSchema, harnessRunEnvelopeSchema
  • Types, from @flow-state-dev/core/types: HarnessBlock (its optional stopsOnAbort declares that when its abort signal fires, the block returns only after its process has exited, so a host may stop a run and start the next in the same directory; a command that process started may still be running), HarnessRunInput, HarnessRunHandle, HarnessRunEnvelope, HarnessSource, HarnessRunStatus, HarnessRunOutcome, HarnessRunUsage, HarnessRunCost, HarnessCostBasis, HarnessResolver, HarnessSessionHook, HarnessCallbackContext
  • harnessSteering(), from the package root: one flow's live steering for a harness turn run in a session. Hand onSteerable to the harness block and declare action({ onTaken? }) beside the turn action; it answers { taken, live } and is declared concurrency: "allow", autoResume: false. Offers are kept per session in the server process, so a line sent to another process answers live: false. Pair it with the client's sendHarnessLine.
  • harnessEnv({ pass }), from the package root (options type HarnessEnvOptions): builds a harness's env from an allowlist of variable names. A harness's agent runs as a child process, and with no env it inherits the server's entire process.env. The result holds exactly the named variables that are set, read when harnessEnv is called. Nothing else is passed, PATH and HOME included. Values come from process.env by default; pass env to read from another source. Where there is no process (a browser or edge runtime) and no env is given, the result is empty rather than an error.
  • The plan record (a harness conversation's to-do list), from the package root: OBSERVED_PLAN, observedPlanCollection, observedPlanItemStateSchema, the type ObservedOutcome (with observedOutcomeSchema), the list markers (OBSERVED_PLAN_LIST, observedPlanListCollection) and replacePlanList(plan, marker, namespace, items, { now?, requestId? }), which writes a whole-list plan as entries and stamps requestId on each. One shape for a coding agent's own to-do list, which a harness that reports one declares and writes. A record of what the agent planned, never a work queue. A plan belongs to the harness conversation: harnessConversationNamespace(conversationId) returns the namespace it is keyed under (conversation/<escaped id>), and planConversationPrefix(conversationId) the key prefix of its entries. Each run leaves a pointer marker under its own namespace (harnessRunNamespace(ctx)) whose conversation field names that namespace. To read one request's plan, list its markers with planRowsPrefix(requestId, OBSERVED_PLAN_LIST) as topicPrefix and pass them to readPlanTarget(requestId, markers) (markers typed PlanMarkerRead, { topic, conversation? }): it returns { kind: "conversation", namespace, rowsPrefix } when a run left a pointer, and { kind: "run", rowsPrefix } for a request whose plan is stored under its own runs. planKeySegment(value) is how an id or run name is written as one key segment. The keys, collections, harnessConversationNamespace, planConversationPrefix, planRowsPrefix, planKeySegment and readPlanTarget are also exported from @flow-state-dev/core/types, for browser code. Guide: the run's plan.
  • The pull-request record (the pull requests a session opened), from the package root: OBSERVED_PULL_REQUESTS, observedPullRequestsCollection (session-scoped, live, readable from the browser), observedPullRequestStateSchema (type ObservedPullRequestState: host, repository as owner/name, number, url, title, state, branch, requestId, source ("tool" or "branch") and firstSeenAt), pullRequestKey(ref), pullRequestRefFromUrl(url) (a pull request's link read as a ref, the one reading every writer keys by; type PullRequestLink) and the type PullRequestRef. One row per pull request, keyed by host, repository and number, so a pull request seen twice is one row. writePullRequests(collection, refs, { source, requestId?, now? }) is the one writer: it fills only fields that are still empty, never moves requestId or firstSeenAt, and a "tool" sighting upgrades a "branch" row. requestId is the request that opened the pull request, so a reader narrows a session's rows to one run by it. What counts as opened is a rule: githubPullRequestRule (in DEFAULT_PULL_REQUEST_RULES) reads a gh pr create command or a create_pull_request tool whose output holds the pull request's link. A PullRequestRule is { name, match(call) }; findOpenedPullRequests(item, rules?) applies rules to one tool_output item. recordPullRequests(ctx, { rules? }) starts the request's recorder, which writes each pull request as its call settles and stops when the request ends; the Codex, Claude Code and Cursor blocks call it. pullRequestWritesSettled(response) resolves once the request's recorder has finished the writes it started, for a writer that runs at request end. pullRequestRecorder({ rules?, name? }) is the same recorder as a handler block, for a flow with no harness: add it as a .tap step. With no collection declared, it records nothing and says so in a status note. The key, collection, schema and pullRequestKey are also exported from @flow-state-dev/core/types. Guide: pull requests a session opened.
  • Subscription usage: harnessUsageWindowSchema from the package root, and the types HarnessUsageWindow, HarnessUsageLevel, HarnessUsageReading, HarnessUsageReader, HarnessUsageReport and HarnessUsageHook from @flow-state-dev/core/types. A reader (options?: { signal }) => Promise<HarnessUsageReading> asks a harness how much of each subscription window is used, with no run. A reading is windows (each window's id, label, minutes, usedPercent 0 to 100 or null when the vendor sent none, level ("ok", "warning", "reached" or null) and resetsAt in epoch ms, plus plan and accountId) or one of api-key, signed-out and unavailable, each with a reason and no number. A reader returns those states rather than throwing. A harness that reports usage during a run calls a HarnessUsageHook (report, ctx) with one HarnessUsageReport per window: { harness, window, observedAt, runStartedAt, accountId }. The schema reads a window with no level as level: null. Guide: subscription usage.
  • Run spend: reportHarnessSpend(ctx, spend) and the types HarnessRunSpend and HarnessBilling, from @flow-state-dev/core/types. A harness block calls it as a run goes and once more when the run ends however it ends, with { harness, inputTokens, outputTokens, costUsd, billing, runId, model }; a later report with the same runId replaces the earlier one, never adds to it (a report without one is a settled run of its own, and the request names it with a fresh settled_… id), and model is the model the run named: what the agent reported, each null when it reported none (never 0, never an estimate), and billing "api-key", "subscription" or null. It returns false and counts nothing outside a request. The run is counted in the request's spend, never in ctx.request.tokenUsage or an action's tokenBudget. Guide: what a run spent.
  • harnessFailureKind(error), from the package root (type HarnessFailureKind from @flow-state-dev/core/types): "unknown-conversation" when a harness refused to continue the conversation a run asked to resume, null for any other failure. Follows cause, so it reads through wrapped errors. A harness package sets it on the error it throws.
  • formatRunDuration(ms), from the package root: a readable string for a duration in milliseconds, rounded to the nearest second. formatRunDuration(324_000) is "5 mins, 24 seconds". A run of an hour or more shows hours and minutes only ("1 hr, 3 mins"). A duration under half a second, a negative number, or NaN gives "less than a second". The Claude Code, Codex and Cursor blocks use it for their run-end status.

Two packages implement it: @flow-state-dev/claude-code (claude-code/sdk) and @flow-state-dev/codex (codex/sdk). @flow-state-dev/harness-manager drives either. Full guide: Coding agents.

Items (@flow-state-dev/core/items)

Output item unions, content types, and stream event helpers. Item types: message, reasoning, component, container, tool_output, status, source, state_change, resource_change,