skip to content
$worker

fp

v0.2.1

Lodash-style value transforms (fp::get/pick/take/…) and fp::pipe — worker-side pipelines that move big values function→function without routing them through the model.

iiiverified
341 installs18 in 7d1 today
install
$iii worker add fp@0.2.1
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

README.md

fp

Lodash-style value transforms and fp::pipe — worker-side pipelines that move big values function→function over the iii bus without routing them through the model.

Install

iii worker add fp

The worker is a deploy: binary Rust worker with no configuration.

Why

An agent that fetches a document and re-types it into the next call's arguments burns its context window and stalls the provider stream mid-call. fp::pipe runs the whole move in one call — each step triggers a function and its result lands in the next step's payload — so the value flows worker→worker and the chat only ever sees per-step sizes and a preview.

While the worker is connected it also injects a usage section into the agent system prompt via the harness pre-generate hook (fp::inject-guidance), so the guidance is presence-gated: no fp worker, no prompt text. The binding is one-shot at startup and relies on the engine's recoverable triggers (iii #1962): bound before the harness is up, it parks as a pending intent and activates when the harness registers the trigger type.

Functions

function lodash request
fp::pipe { through: [{function, payload?, into?}], preview_chars? }
fp::get _.get { value, path } — JSON pointer; a miss names the available keys
fp::pick _.pick { value, paths } — subset an object by top-level keys
fp::omit _.omit { value, paths } — drop top-level keys
fp::take _.take { value, n } — first n array elements / string chars
fp::drop _.drop { value, n } — skip the first n
fp::map _.map { value, path } — pluck a pointer from each element; misses → null
fp::filter _.filter { value, matches } — keep elements matching a partial object
fp::split _.split { value, separator }
fp::join _.join { value, separator? } (default ,)
fp::uniq _.uniq { value } — dedupe, first occurrences win
fp::size _.size { value } — array length / string chars / object key count
fp::compact _.compact { value } — remove null elements (0/false/"" are kept)
fp::nth _.nth { value, n } — element at index; negative counts from the end
fp::getOr fp.getOr { value, path, default } — pointer value, or default on a miss
fp::flatten _.flatten { value } — unnest one level
fp::sortBy _.sortBy { value, path } — stable ascending sort by a plucked pointer ("" = the element itself)
fp::reverse _.reverse { value } — reversed copy (immutable, fp-style)

Transforms take their input at value and return { value }. They deviate from lodash where silence would thread garbage through a pipe: a type mismatch is an error (not a silent {}); a map path matching NO element errors naming what was available (pointers pluck stored fields — no computed properties like /length; fp::size counts); compact removes only null (lodash's full-falsey removal would eat legitimate 0/false/""); nth out of bounds and size on a non-collection error instead of returning undefined/0; sortBy requires every element to carry the key with one comparable type. Sporadic map misses still become null, like lodash — fp::compact drops them.

The pipe

fp::pipe { through: [
  { function: "scrapling::fetch",
    payload:  { url: "https://…", format: "markdown", main_content_only: true } },
  { function: "fp::get",  payload: { path: "/content" } },
  { function: "fp::take", payload: { n: 20000 } },
  { function: "state::set",   payload: { scope: "research", key: "article" } }
]}
  • Step N's result lands in step N+1's payload at into (a JSON pointer, default /value — which is exactly where the transforms and state::set read their input, so most pipes need no into at all).
  • A transform step threads the transformed value itself — the { value } wrapper in the function table is only its direct-call response shape, so there is never a {value} layer to unwrap between steps.
  • The FIRST step receives no threaded value: start with a producing function (a fetch, a state::get) or seed a leading transform via payload.value. An unseeded leading transform fails validation before anything runs.
  • fp::* transform steps run inline in this worker; every other step is one bus trigger with a 120 s budget. The whole pipe must also fit the caller's own dispatch timeout on fp::pipe.
  • 1–12 steps. The response is receipts only: { steps: [{function, chars}], value_preview } (preview_chars sizes the preview, default 400, capped at 8000 — the receipt must never become the bulk channel it replaces).
  • A failing step stops the pipe; the error carries the completed-step trail.

Boundaries

  • Steps run with THIS worker's authority, not the calling agent's per-step dispatch policy — the fp::pipe call itself is the policy/approval surface (an approver sees the full step list). For that reason the pipe is not agent-callable without approval by default; the pure transforms are. See iii-permissions.yaml.
  • Statically refused as steps: shell::*/coder::* (need the harness fs_scope stamp), engine::register_trigger/engine::unregister_trigger (need the harness trusted-session stamp), nested pipes, and every class the agent policy hard-denies — session::*/approval::*, configuration::*/oauth::* (credentials), router::*/provider::* (model spend), harness::*/run::* (turn control), stream::* and bus internals, *::on-config-change — because steps run with worker authority and must not ride past those denies. state::* is the deliberate exception: persisting the threaded value is the pipe's purpose, and the pipe call itself is the approval surface.