Skip to content
Merged
34 changes: 34 additions & 0 deletions docs/agent-definition-status-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Agent definition status lifecycle

Notes on `@corbits/agent-directory`'s `routes.ts`, `PUT /:definitionId/status`.

## What it does

Archive and restore is the whole lifecycle a person controls from the agent
detail page. `stopped` drops a definition out of every launchable listing
while leaving the row, its asset, and its git history untouched — which is
what makes the same route, with `deployed`, a restore rather than a
re-create.

## The invariant this route relies on

Two other writers of `workflow_definition.status` exist in this build:

1. Row creation. `ensureWorkflowDefinitionForAsset` inserts with the column
default (`deployed`) under `onConflictDoNothing` on `(assetId,
wireHash)`, so re-deploying the same definition body over an archived
row is a no-op and can never silently un-archive it. Editing an agent
through this package's own routes only repopulates the asset and never
re-projects a definition row at all.
2. `apps/hub`'s `undeployAgentDefinition`, which writes `stopped` — the
same direction as archiving, so the two can't fight.

## A known hole

The one way an archived agent reappears as launchable is a deploy of a
changed body over the same asset: a new `wireHash` misses the unique
constraint and inserts a second definition row, `deployed` by default,
beside the archived one. Nothing in this build takes that path for a
hand-authored agent — only `createAgentDefinitionCore` and Interchange's own
selector deploys call the ensure — but it's a real hole in the
status-as-lifecycle model, not a hypothetical one.
37 changes: 37 additions & 0 deletions docs/agent-session-provisioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Agent session provisioning

Notes on `@corbits/workflows`'s `launch/agent-session.ts`.

## Why this module exists

Every Workbench launch path provisions through Interchange's
`prepareProvisionedDeployment` with a `sessionId` it mints itself, but
nothing writes that id into `agent_session` — the table
`resolveRunSessionId` reads to route a run's outbound mail. Every launcher
must do this itself, through this one shared helper, so mail and spans
persist against a real session from the run's first turn.

## Why timing forces a two-step write

`workflow_run.principal_id` (the FK `agent_session` keys on) is still null
the instant `prepareProvisionedDeployment` returns — a provisioned anchor is
born "deployed" with no principal, and only the run's first trigger
reconciles one onto it. Pre-creating the run's principal to dodge this isn't
an option either: Interchange's own grant materialization inserts the
principal row `onConflictDoNothing` and treats a conflict as "grants already
committed," throwing rather than re-materializing — so nothing upstream of
that first trigger may write the principal first.

`recordAgentSessionAtProvision` writes the row immediately after
`prepareProvisionedDeployment` returns, keyed on the deploying principal
since the run principal doesn't exist yet. `ensureRunSession` re-keys onto
the run's own principal once one is anchored, called from every seam that
might be a run's first mail-routable moment, and is a true upsert so a run
deployed straight through Interchange's own deployments route (which skips
the eager write) still gets a row.

## `endAgentSessionFor*`

Called only from places that already know a run died — a chat relaunch or a
one-shot prompt's teardown. No sweeper; a run nobody reacts to stays
`active` until something does.
28 changes: 28 additions & 0 deletions docs/inference-concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Inference concept vocabulary

Design notes for `@corbits/inference-catalog`'s `concepts.ts`, the words an
agent uses to ask for a model.

## Why plain data, not a table

Pure data with no branching or network — the `CATALOG_SEEDS` idiom — so it's
importable on its own. It's deliberately not a table: this is product
vocabulary that ships with the build and needs to be reviewable in a diff. A
bench deviates through its policy row's concept ceilings and allow/deny
lists, never by forking the vocabulary.

## Ceilings

USD per million tokens, input and output kept separate — a blended number
would hide which axis a workload actually spends on. Ceilings are soft by
default: a model over ceiling is flagged and sorted last, not dropped, so a
bench whose only provider is expensive still gets an answer.

## Capabilities vs. reference mix

Capabilities are `@intx/types`' vocabulary — narrower than the pinned
catalog's, since `long-context` and `prompt-caching` are baked onto catalog
offerings but aren't storable capability values. Where a concept wants
"handles a huge input," its reference mix carries that meaning instead: a
10M-token input mix ranks the model that's cheap on enormous inputs first,
which is the decision the capability would have stood in for.
32 changes: 32 additions & 0 deletions docs/offering-capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Offering capability resolution

How `@corbits/inference-catalog`'s `offering-capabilities.ts` decides what a
deployment can do.

## Why this exists

`model_offering.capabilities` is the column every capability filter reads —
this package's chain resolution and the platform's own source resolution
alike. Nothing populated it before, so it was empty everywhere. This module
is the source of truth, and it only ever reports capabilities the pinned
catalog observed on the wire:

- `exact-deployment` — this exact (baseURL, model) was probed; use its list.
- `same-model-wire` — the model was probed on other deployments speaking the
same wire, and this one relays it; use the intersection across those
deployments, so a relay never claims more than every probed deployment
demonstrated.
- `unknown` — no probe covers it; empty list, said plainly.

An empty list is deliberately not softened with a per-adapter baseline: "an
OpenAI-compatible endpoint serves plain text" is false for the embedding
models such endpoints also serve, and a wrong capability tag routes real
work to a model that cannot do it.

## Storable vocabulary

The pinned catalog's vocabulary is a superset of what the platform can
store: it bakes in `long-context` and `prompt-caching`, which `@intx/types`'
`Capability` does not accept. Everything this module reports is filtered
down to the storable vocabulary, so a seed never posts a value the hub will
reject.
39 changes: 39 additions & 0 deletions docs/ollama-context-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Ollama context defaults

Why `@corbits/inference-catalog`'s `ollama-context-defaults.ts` exists and
how it plugs into inference.

## The problem

`@intx/inference`'s built-in OpenAI-shaped adapter defaults output tokens to
`options.maxTokens ?? 4096` when a caller sets none, and Ollama's own
OpenAI-compatible endpoint silently defaults `options.num_ctx` (its real
context window) to a small built-in size when nothing sets it either —
truncating real conversations for every locally served chat agent, with no
error to say so.

## Where the fix lands

`@corbits/ollama-adapter`'s `OllamaAdapterConfig` is where an override
actually reaches the built request body (`resolveOverride` and
`createOllamaAdapter`). This module supplies the real per-model values,
stored as a `model_offering`'s `quirks` column so it reaches
`InferenceSource.quirks` — and from there the adapter — through the
platform's existing resolution, with no parallel override path.

## Table entries are native windows, not extended ceilings

Each table entry is the model's advertised *native* context window, not a
YaRN/rope-extended ceiling: requesting a `num_ctx` past what a model (and
the host's memory) can back can fail allocation or force heavy swap, a real
operational risk rather than a convention to enforce. A model absent from
the table gets no override — Ollama's own default stands rather than this
module guessing at a ceiling it cannot back up.

## `default` vs. `perModel`

Each offering is already scoped to one exact model, so `quirksForDeployment`
wraps the resolved override in `default` — the field `resolveOverride`
applies when no `perModel` entry is present — rather than `perModel`, which
exists for a single shared connection serving several models through one
quirks bag.
29 changes: 29 additions & 0 deletions docs/run-outcome-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Run outcome status

Notes on `@corbits/workflows`'s `schedule/run-outcome.ts`: how a platform
run's raw `status` column becomes what a surface should show.

## Why this exists

Warm-keep leaves a fire's delivery agent deployed after it replies, so
`workflow_run.status` never settles out of `running` on its own. Insights,
Mission Control, and the shell activity feed all read a listing-shaped
payload through this module rather than badging the raw column, so a
lingering `running` status past `FIRE_RUNNING_WINDOW_MS` reads as completed
instead of still in flight. This logic was originally
`@corbits/routines`'s `health.ts`/`run-language.ts`, carried over verbatim
when routines cut over to native `ScheduleTrigger` definitions.

## In-flight signal and abandonment

A finished fire is supposed to land `completed`/`failed`/`cancelled` plus
`endedAt` via `markTerminal`. `FIRE_RUNNING_WINDOW_MS` is a last resort for a
fire already known abandoned that never got that write — it is never
applied to a live in-flight fire, since a tool loop can outlast ten minutes
before persist catches up.

Absent `turns`/`hasInFlightTurn` is treated as unknown, never as "no
in-flight turn." Treating an omission as empty would revert the
live-tool-loop false-complete this module exists to prevent, so a listing
row is abandoned only when the producer explicitly said there is no
in-flight turn, and the fire is older than the window.
33 changes: 33 additions & 0 deletions docs/workflow-authoring-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Workflow authoring registry

Notes on `@corbits/workflows`'s `authoring/registry.ts`.

## What it is

An agent's in-tenant surface for publishing a workflow codebase as a native
`kind:"workflow"` hub asset, republishing it, and reading it back. Every
write is gated by own-tenant scoping plus an explicit grant-store
authorization call, and by `validateWorkflowSourceTree` before anything
reaches `RepoStore`. Deploying is not this registry's job — an agent deploys
a committed asset through the stock deployments route with the run bearer.

## Why `populateAsset` uses the `hub` principal

`workflowKindHandler.workflowAuthorize` only recognizes three principal
kinds for a workflow-asset write: `hub` (full access), `sidecar`
(read-only), and `user` (gated by git-token-shaped bearer claims a
sidecar-authenticated caller never carries). There's no fourth
"workflow-run" principal kind the substrate understands, so the real
per-write authorization decision has to be made here, by this registry,
against the grant store and the resolved caller identity — before the
already-authorized write is handed to the substrate as a hub-mediated
commit under `hub` (the same principal `@corbits/skills`'s `writeSkillMd`
uses).

## Why head-sha reads go through `RepoStore` directly

`AssetService` exposes blob and directory reads pinned to a ref but never
the sha that ref resolves to, and `listAssetBlobs` lists blobs only (no
subtrees), so a full tree walk needs `RepoStore.openCommittedReads`. The
repo id is the asset id under the `workflow` kind, exactly as `AssetService`
composes it internally.
36 changes: 36 additions & 0 deletions docs/workflow-capability-authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Workflow capability route authorization

Why `@corbits/agent-directory`'s `workflow-capability-routes.ts` skips a
grant-store check.

## The gap

The vendored grant-materialization path never seeds a `kind: "workflow"`
run's own principal a `workflow-definition: <its own id>/update` grant, so
`requireGrant` would 403 every self-update call a run makes for its own
definition until that vendor gap closes.

## The interim rule

Rather than block `request_capability` on an unpublished vendor change,
this route skips the grant-store check entirely for the one case it
accepts: a call whose authenticated run targets its own definitionId. That
narrow case is already gated by a stronger control than a grant row —
`@corbits/capability-tools`'s `request_capability` tool declares `approval:
"ask"`, so the reactor suspends every call as a pending approval and
renders it in-chat before this route ever runs. A human already had to
approve the specific addition; the human is the authorizer here, not a
grant row.

This route still enforces, unconditionally:

1. The caller's run must resolve to a live tenant/principal/run via the
sidecar-token + run-address check.
2. The path `definitionId` must equal the resolved run's own definitionId
(rejected 403 otherwise).
3. The addition must fail closed against the tenant's live capability
inventory.

Once the real self-update grant is seeded in vendor grant materialization,
this route can route through `requireGrant` like every other
definition-mutating surface instead of carrying this interim rule.
34 changes: 34 additions & 0 deletions docs/workflow-definition-projection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Workflow definition projection

Notes on `@corbits/workflows`'s `definition-projection.ts`, the pure reader
that turns an already-resolved inert wire projection into a `FoldedBody`.

## Why this exists

There's no hub-side row to read a launch body back off of. This module is
the pure half: the schema-validated reader for anything holding a
projection value from elsewhere (chat, in particular). Nothing here queries
a database.

`grantRequirements` is deliberately excluded from the projection: it does
not survive the live-to-inert projector and lives instead on the
`workflow_definition.grant_requirements` column, so callers pass it in
alongside the projection rather than reading it off.

## Step shapes

An inert projection's launch step comes in two shapes: a bare `step` (the
conversational case) or an `onTrigger` section whose inline body carries the
one agent-bearing step that answers each turn. `extractAgentBearingStep`
reads through either shape with one reader, so neither call site duplicates
the other's parsing.

## Why multi-step definitions are rejected

The launch target (`@corbits/agent-runtime`'s `AgentRuntimeConfig`) renders
exactly one `systemPrompt` into one mailbox-triggered turn — it has no
notion of step order. Reading past `stepOrder[0]` would silently drop every
later step's behavior rather than run it, so `MultiStepFoldUnsupportedError`
is thrown instead. Genuine multi-step launch needs a different deploy front
(Interchange's native workflow-run trigger, `@intx/workflow-host`'s DAG
supervisor) than this module provides.
34 changes: 7 additions & 27 deletions packages/agent-directory/src/agent-definition-draft-routes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
// The HTTP surface for Myra-backed agent-definition drafting
//: tenant-scoped, `requireGrant`-gated, personal to the
// requesting principal, request parsing via arktype at the boundary,
// route registration only. Error copy at this boundary is plain
// language for the person who typed the description; the technical
// detail every fail-closed error class carries goes to the server log
// instead.
//
// Relocated out of `@corbits/task-planner` (deleted along with the
// rest of the tasks primitive) because this drafting flow was never a
// task concept — it backs the "Describe" step of agent creation
// (`CreateAgentPanel` and onboarding), which stays.
// The HTTP surface for Myra-backed agent-definition drafting. Error copy is
// plain language for the person who typed the description; detail is logged.
import { type } from "arktype";
import { Hono } from "hono";

Expand Down Expand Up @@ -37,14 +27,8 @@ const CreateAgentDefinitionDraftBody = type({

export type CreateAgentDefinitionDraftRoutesDeps = {
requireGrant: RequireGrant;
/**
* The agent-definition drafting port — omitted entirely on
* a host that hasn't wired Myra drafting up yet, in which case this
* route 404s rather than pretending to draft and always failing.
* The route never touches `./agent-definition-drafting.ts`'s
* runner/inventory machinery directly, so it stays testable with a
* plain stub — no database, no one-shot runner.
*/
/** Omitted on a host that hasn't wired Myra drafting up yet, in which
* case the route answers 503 instead of pretending to draft. */
draftAgentDefinition?(input: {
readonly tenantId: string;
readonly principalId: string;
Expand All @@ -53,13 +37,9 @@ export type CreateAgentDefinitionDraftRoutesDeps = {
}): Promise<AgentDefinitionDraft>;
};

/** Every fail-closed error the agent-definition drafting path can throw
* — Myra unresolvable, the run timing out or failing, an unparseable
* reply, or an out-of-inventory model/tool package/skill reference —
* reads as the same honest "couldn't draft" 422 to the person who typed
* the description: from their point of view it is still "Myra's draft
* didn't work out," never a REST-shaped bad request they authored
* themselves. */
/** Every fail-closed error the drafting path can throw reads as the same
* honest "couldn't draft" 422, never a REST-shaped bad request the person
* authored themselves. */
function isDraftingFailure(err: unknown): boolean {
return (
err instanceof MyraAgentDefinitionDraftingUnavailableError ||
Expand Down
Loading
Loading