Oshun Platform · Architecture

Agentic AI Studio

A focused page within the Oshun Platform Architecture documentation. The full map and every sibling page live in the Architecture hub.

18sections21 minread8tables

On this page

The Agentic AI Studio is V1's governed autonomy plane — the registry, run-envelope, budget, kill-switch, grant-resolver, hand-off, mode, and champion-challenger machinery that lets agents do real work (research, drafting, fact-checking, narration, course generation, support triage) without ever escaping the platform's tone, grounding, entitlement, and cost guardrails. It serves operators (who arm kill switches and promote challengers), creators and customers (who invoke entitlement-gated pipelines), and every domain that wants to ship an autonomous workflow. It sits beside the platform substrates hubbed at ../ARCHITECTURE.md and composes them: it grounds through Sophia, enforces tone through Lilith, generates through Isis, and remembers through Iris.

Read this page for what is shipping vs. spec / provider-gated. Unusually for this doc set, the Studio is overwhelmingly real. The entire governance/orchestration brain lives in libs/oshun/agentic-studio (@oshun/agentic-studio v0.1.0), a pure-TypeScript package whose index.ts re-exports 12 subsystemsregistry, runs, plans, dashboard, invocation, budgets, capabilities, feedback, modes, pipelines, grants, handoff — backed by 271 passing tests across 16 files that assert real computed values, not truthiness. Three things are genuinely thinner than the prose below might imply: (1) the persistence is in-memory — the AgentRegistry is a Map, and the BFF run-lifecycle store is in-process; the hub's old "durable jobs through @oshun/queue" line is not wired here (agentic-studio imports no @oshun/queue). (2) "Replay and time-travel" / "streaming progress" / "operator dashboard" are data and query layers (dashboard/dashboard-query.ts, runs/replay.ts, runs/streaming-progress.ts), not a rich admin React page — the admin surface is API routes, and the only agent page is a single apps/oshun/tenant-admin/src/app/agents/page.tsx. (3) Tool grants are declared and validated, but actual tool execution (the real web.fetch, sophia.ground, …) is injected at the app boundary and fail-closed by default. So: the brain is real and tested; the "autonomous content gets produced end-to-end through live providers" story is seam-and-fail-loud, not wired to live vendors. Backlog: §18; product scope: V1/features.md § Agentic AI Studio.

Where the prose used to be wrong#

Three claims on the hub page are corrected here so readers aren't sent to the wrong code:

  • No durable queue at this layer. ARCHITECTURE's "Job orchestration — durable jobs through @oshun/queue with priority classes, replay, DLQ, SLA monitor" is aspirational at the agentic-studio layer. The queue library is libs/shared/queue; agentic-studio's runs/dispatcher.ts, runs/orchestrator.ts, runs/checkpoint.ts, and runs/replay.ts are in-process pure-function logic with no @oshun/queue binding, no DLQ, and no SLA monitor.
  • No .../agents/ operator dashboard page in admin. The hub points at apps/oshun/admin/src/.../agents/; that directory does not exist. The admin agentic surface is the API route group apps/oshun/admin/src/app/api/admin/agentic-operations/ (kill-switches, snapshot, gold-sets/promote, champion-challengers/rollout). The lone agent page is apps/oshun/tenant-admin/src/app/agents/page.tsx.
  • Budgets are self-contained, not "integrated with metis cost-tracking." libs/metis/cost-tracking exists, but src/budgets/budgets.ts (BUDGET_CATEGORIES / BUDGET_SCOPES / checkBudget / consumeBudget) does not import it. "Integrated with" overstates a dependency that is not present.

Agent registry and catalog#

Every agent is a versioned AgentRegistryEntry registered into an AgentRegistry (src/registry/agent-registry.ts). Registration is validating and fail-closed: register(entry) returns a typed AgentRegistryValidationError[] and only inserts into the backing Map when the array is empty. The eight validation codes are real and individually tested: duplicate-agent-id, unknown-family, tool-grant-exceeds-family-ceiling, invalid-semver (for the agent, a capability, or the model binding), unknown-data-scope, lifecycle-experimental-mismatch, duplicate-capability-id, and champion-without-disclosure. Semver is checked against a real regex (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/), so a champion-tier agent cannot ship with a placeholder version or without its declared disclosure requirements.

The 20 agent families and their tool ceilings#

AGENT_FAMILIES (src/registry/agent-families.ts) is exactly 20 canonical families. Each carries AgentFamilyMeta with a toolGrantCeiling the registry enforces as a hard ceiling — a declared toolGrant outside its family's ceiling is rejected with tool-grant-exceeds-family-ceiling, so an editing agent cannot quietly request web.fetch.

Family Family Family Family
research drafting editing fact-checking
citation-verification illustration narration translation
course-generation lesson-scaffolding assessment-generation study-plan-synthesis
recommendation-explanation moderation-triage support-triage ritual-scriptwriting
sky-event-briefing claim-extraction source-ingestion kg-promotion

For example, research's ceiling is ['web.fetch', 'source.fetch', 'sophia.ground', 'memory.read'] with a strict-cite default grounding policy, while editing's is ['memory.read', 'sophia.fact_check', 'persona.invoke'] — narrower, and read-only on sources. Each family also declares primaryDomains, defaultGroundingPolicy (strict-cite / cite-where-available / no-citation), and defaultPersonaPolicy (one of the Lilith voices lilith.teacher / lilith.guide / lilith.scribe / lilith.host).

Lifecycle, cost class, and data scope enumerations#

Enumeration Values
AGENT_LIFECYCLE_STATES draft, rehearsal, shadow, champion, challenger, deprecated, retired
AGENT_COST_CLASSES micro, small, medium, large, flagship
DATA_SCOPES (8) tenant.public, tenant.private, user.public, user.private, editorial.draft, editorial.published, platform.metadata, platform.curated

An AgentRegistryEntry additionally pins a modelBinding (provider, model, semver, default max tokens / temperature, and an explicit fallbackProviderId / fallbackModelId), a personaBinding, a tonePolicyId, a groundingPolicyId, an auditPolicyId, a tenantScope ('platform' or { tenantId }), a visibility set (platform / tenant / creator / customer), and disclosureRequirements. list(viewer) filters the catalog by tenant and role so a customer never sees a platform-only agent.

The AgentRun envelope and its lifecycle#

Every invocation produces an AgentRunEnvelope (src/runs/agent-run.ts) — the durable, operator-visible shape of a run. It is the contract the hub gestures at, named field by field:

Field Meaning
runId, rootAgentId, rootAgentSemver identity of the run and its root agent
parentRunId, pipelineId, pipelineSemver attribution into a hand-off chain / pipeline
status one of RUN_STATUSES (below)
plan RunPlanStep[] — DAG of steps with dependsOnStepIds
toolCalls RunToolCallRecord[] — each with argsHash, outcome, costUnits, scopeCheckResultId, auditEventId
intermediateArtifacts checkpointable artifacts with a persistedKey for resume
evidenceTrail per-stage evidencePackId, groundingState, and citationCount
costLedger the RunCostLedger (below)
decisionRationale per-stage rationale text and a decisionLabel (e.g. plan-replanned, gate-failed, abstained)
outputs RunOutputBundle[] (final / intermediate / preview / partial-on-failure)
provenanceBundleId provenance handle carried through the hand-off chain

Run statuses — the real enum vs. the simplified diagram#

The hub's mermaid state machine is a simplified, aspirational model: it uses pending, planning, and executing states that do not exist in code. The real RUN_STATUSES enum is:

text
queued · starting · running · paused · awaiting_approval ·
awaiting_tool · awaiting_branch_selection · cancelled · killed ·
failed · completed

TERMINAL_STATUSES is the set { completed, failed, cancelled, killed }. killed is deliberately distinct from cancelled: a cancelled run is an operator's deliberate stop, a failed run errored, and a killed run was terminated mid-flight by the governance executor (an armed kill switch or an exhausted budget). The diagram also omits paused, awaiting_tool, and awaiting_branch_selection — states that exist because runs really do suspend on back-pressure, on a tool boundary, and on a branch decision.

The cost ledger#

RunCostLedger is a structured meter, not a single number — it tracks totalCostUnits, per-stage and per-tool cost maps (perStageCostUnits, perToolCostUnits), and physical resource counters: inputTokenCount, outputTokenCount, gpuMinutes, voiceSeconds, avatarSeconds, retrievalCount, and externalCallCount. These line up one-to-one with the budget categories below, so a budget on voice-seconds meters against the same quantity the ledger records.

Run controls and the operator console#

src/runs/run-controls.ts exposes the operator console as pure functions over (envelope, command) → ControlResult. Each returns the next envelope plus a typed RunControlAuditEvent, or a typed ControlDenialReason for an invalid transition (run-terminal, invalid-status-for-pause, no-such-branch, fork-without-fresh-id, …). The controls and their audit kinds:

Control Behavior Audit kind
pause suspend a pausable run (queued/starting/running/awaiting_tool/awaiting_branch_selection) pause
resume resume a paused run resume
cancel operator-deliberate terminal stop cancel
kill governance terminal stop kill
chooseBranch pick a branch at awaiting_branch_selection branch-chosen
fork clone a run — requires a fresh runId (else fork-without-fresh-id) fork
retryWithChanges rewind and apply overrides retry-with-changes
operatorStep single-step a run operator-step

The runtime governance seam (the real enforcement story)#

This is the concrete enforcement the hub describes abstractly but never names. Two pure modules and one orchestrator wrap every tool call:

  1. admitToolCall (src/runs/executor.ts) runs the governance checks in a fixed safety → cost → rate order and returns the next state:
    • Safety first. decideExecution(killSwitches, target) — if any switch is active for the target, the run is kill-terminated with the switch's userVisibleStatusCopy and optional safeDegradationModeId. Kill switches take precedence over everything.
    • Cost next. For each budget, checkBudget is evaluated; an exceeded verdict is a hard stop (the run is killed with a budget-exhausted status copy), while warn / grace accumulate as budgetWarnings and let the run continue.
    • Rate last. An optional token-bucket throttle yields a throttled outcome with retryAfterSecondsback-pressure, never termination.
  2. dispatchGuardedToolCall (src/runs/dispatcher.ts) invokes the supplied runTool(run) only when admitToolCall returns admit; a denied, throttled, killed, or run-terminal admission never touches the tool.
  3. runGuardedToolPlan (src/runs/orchestrator.ts) walks a PlannedToolCall[] plan through the dispatcher, producing ExecutedToolCall[] and a GuardedToolPlanResult.

Where it mounts: POST /v1/agentic/runs/execute#

The seam is mounted in the BFF at apps/oshun/bff/src/agentic/runs-route.ts (registerAgenticRunsRoute). It is fail-closed and server-authoritative:

  • Fail-closed by default. The default tool registry (notConfiguredAgentToolRegistry) has no tools, so any planned toolId that isn't a configured function returns 503 agent_tools_not_configured — the route never fabricates a run. A live web.fetch / sophia.ground is only swapped in at deploy time.
  • Malformed → 400. Missing runId / tenantId / rootAgentId / non-empty plan of { toolId }.
  • Scope check (S7) → 403 tool_scope_missing. A tool with a scope requirement runs only when the caller's real authContext.scopes satisfy it; the executor never invents an operator tier for whoever shows up.
  • Server-authoritative identity (S8). actorId and tenantId are taken from authContext (the auth claim), not the request body — so a caller can't reshape the target an armed switch matches. Kill-switch target metadata (family / providerId / region) is keyed server-side per toolId.
  • Ownership (S9) → 409 run_id_conflict. A runId already persisted by another member is never overwritable.
  • Live kill-switch propagation. Operator-armed switches are read from adminAgenticOperationsStore on every request, so arming a switch takes effect at the next tool-call boundary (the ≤5s propagation budget).

Budgets, quotas, throttles, and kill switches#

Budgets#

src/budgets/budgets.ts is self-contained. BUDGET_CATEGORIES (9) are cost-units, input-tokens, output-tokens, gpu-minutes, voice-seconds, avatar-seconds, retrievals, external-calls, storage-bytes — matching the cost-ledger counters. BUDGET_SCOPES (7) are tenant, role, agent, run, tool, user, pipeline. checkBudget returns one of four verdicts: ok, warn, grace (carrying a graceEndUnixSeconds so a soft overrun has a deadline rather than a cliff), and exceeded. Period rollover is supported, so a budget refills at the next window.

Kill switches#

src/budgets/kill-switch.ts has KILL_SWITCH_SCOPES (7): agent, family, tenant, provider, region, tool, global. decideExecution picks the broadest active switch as the "blamed" switch for user-facing copy, using a real blame priority:

text
global 6 > region 5 > provider 4 > tenant 3 > family 2 > agent 1 > tool 0

so a tenant operator sees the global-outage copy rather than a confusing per-tool message when a global switch is armed. The decision returns a userVisibleStatusCopy and an optional safeDegradationModeId (a graceful fallback rather than a blank failure).

Tool catalog, grants, and the grant resolver#

The 21-id catalog#

The canonical tool catalog lives in contracts, not the studio: libs/contracts/src/agent/tools.ts. V1_TOOL_IDS is exactly 21 entries (the grouping obscures the count — generate.image/generate.video/generate.audio are three, memory.read/memory.write are two, calendar.read/calendar.write are two):

text
web.fetch · source.fetch · sophia.ground · sophia.fact_check ·
memory.read · memory.write · persona.invoke · generate.image ·
generate.video · generate.audio · composition.suggest ·
themis.adjudicate · code.exec · file.read · file.write ·
calendar.read · calendar.write · notify.send · handoff ·
approval.request · agent.terminate

V1_TOOL_CATALOG carries catalogVersion: '1.0.0' and publishedAt: '2026-05-11T00:00:00Z'. Two real functions back the "tool versioning / deprecations surfaced at registry-review time" promise: resolveToolDependencies walks per-tool semver dependencies (returning unknown-tool or semver-range-no-match), and surfaceDeprecations lists tools whose deprecation.deprecatedAt falls inside a review window, along with their replacementToolId and dependent tools.

Grant scopes and resolver priority#

GrantScopeSchema (libs/contracts/src/agent/tool-grants.ts) enumerates the five scopes; the resolver (src/grants/resolver.ts) orders them by GRANT_SCOPE_PRIORITYthe most specific grant wins:

Scope Priority
per-run 100
per-pipeline-instance 80
per-session 60
per-user 40
per-tenant 20

resolveGrant returns the highest-priority unexpired, unrevoked match, or a typed GrantDenialReason: no-matching-grant, all-matches-expired, or all-matches-revoked. The resolver also provides the lifecycle plumbing: recheckQueuedRunAtDispatch (a grant revoked while a run sat queued is caught at dispatch), emitRevocationCascade / emitExpiryCascade (propagate to active sub-runs), renewGrant (rejects with rationale-required on empty rationale and audit-required on a missing audit event id), and emitToolKillSwitchCascade, which stamps each cascaded event with withinFiveSecondSlo (latency <= 5) — the concrete ≤5s revocation SLO.

Multi-agent hand-off protocol#

src/handoff/handoff-protocol.ts enforces the parent→sub-agent contract. The APPROVAL_REQUIREMENTS are none, operator, teacher, guardian, and user-confirm-before-proceed; DEFAULT_DEPTH_CAP = 5. validateDispatch rejects a dispatch with a typed DispatchDenialReason:

Reason Rule enforced
cycle-detected re-entering an ancestor agentId is blocked unless allowCycle is declared at the chain root
depth-cap-exceeded hand-off depth must stay within the cap (default 5)
authority-widening-rejected the sub-agent's tool authority must be a subset of the parent's, its maxFanoutDegree no larger, and its capabilityModifiers no broader — authority can narrow, never widen
budget-non-positive a sub-budget must be positive
budget-exceeds-parent a sub-budget cannot exceed the parent's remaining budget

Budget is deducted at dispatch (deductDispatchBudget) and reconciled at termination (reconcileTerminalBudget), which returns unused budget to the parent and, on overrun, emits an overshootEvent of kind subagent_budget_exceeded. Attribution is preserved via deriveAttributionChain so artifact provenance carries the rootAgent → producingAgent chain.

Cost-quality modes#

src/modes/modes.ts defines five COST_QUALITY_MODES with concrete declared thresholds (not just qualitative labels):

Mode maxCostUnits p95LatencyBudgetMs minEvaluationScore multiAgent humanCheckpointCount hasExternalSideEffects
fast 50 1 500 0.6 false 0 true
balanced 500 10 000 0.7 false 0 true
deep 5 000 120 000 0.8 true 1 true
exhaustive 50 000 1 800 000 0.85 true 3 true
rehearsal-dry-run 1 000 60 000 0 true 0 false

rehearsal-dry-run is the safety valve: it has no external side effects, so a pipeline can be exercised end-to-end without touching live providers or spending real resources. checkModeCompliance flags cost-exceeded, latency-exceeded, and a below-bar evaluation score; applyOverride is tighten-only — a customer or tenant operator may switch modes only within the domain's allowedModes, and a cost-raise-rejected result blocks raising the cost ceiling under tightenOnly.

Capability isolation, secret access, and adversarial tests#

src/capabilities/ is the security boundary. tool-isolation.ts ships real SSRF guards: isPrivateOrLoopbackHost rejects RFC-1918 ranges (10.0.0.0/8 → 0x0a0000000x0affffff; 172.16.0.0/12; 192.168.0.0/16), loopback (127/8, localhost, ::1, 0.0.0.0), link-local (169.254.*, fe80:), IPv6 unique-local (fc/fd), and IPv4-mapped IPv6 (::ffff: is unwrapped and re-checked), returning a typed EgressDenialReason (ssrf-private-ip / ssrf-link-local / ssrf-loopback / host-not-in-allowlist / scheme-not-allowed / method-not-allowed / invalid-url). matchHostPattern supports *. wildcard subdomains. checkSecretAccess denies with cross-tenant, agent-not-permitted, or tool-not-permitted.

adversarial-tests.ts is the concrete security-test catalog — ADVERSARIAL_CATEGORIES covers prompt-injection, tool-call-exfiltration, scope-escalation, sandbox-escape, persona-bypass, policy-bypass, and source-fabrication, each fixture declaring an expectedVerdict of refuse, sanitize, or comply-and-disclose.

Champion-challenger rollout and gold sets#

src/feedback/champion-challenger.ts runs a real two-proportion z-test on task-success rate, not a toy comparison. evaluateReadiness requires MIN_SAMPLES = 100; below that it returns not-enough-samples. The z-statistic is converted to a two-sided p-value via a genuine Abramowitz & Stegun 26.2.17 normal survival-function approximation (normalSf), compared against P_THRESHOLD = 0.05. Verdicts: not-enough-samples, inconclusive, challenger-worse, challenger-ready. Rollout runs in shadow (challenger runs in parallel but never returns to the user) or canary (a bounded canaryFraction of live traffic) mode via routeInvocation. ROLLOUT_TRANSITIONS are promote-challenger-to-champion (readiness-required — promotion is refused unless the readiness verdict is challenger-ready), freeze-rollout, and rollback-to-prior-champion. The durable gold-set pipeline (feedback/gold-set-pipeline.ts) round-trips operator-promoted gold-set entries through DurableGoldSetEntryRow records, surfaced via the admin gold-sets/promote and champion-challengers/rollout routes.

Customer-/creator-facing invocation#

src/invocation/invocation.ts gates invocation behind entitlements and a preflight declaration. INVOCATION_SURFACES enumerates where a run can be triggered; validatePreflight checks the InvocationEnvelope's declared entitlements set against required scopes and returns missing-entitlement per gap, alongside the declared source set, declared tone, declared persona, and a ConsentConfirmation. This is the layer that binds a customer invocation to Lilith tone policy and entitlement before any tool ever runs — the studio counterpart of the BFF's S7 scope check.

The seven V1 pipelines#

src/pipelines/v1-pipelines.ts declares seven PipelineSpecs, all lifecycleState: 'live'. Each pins a DAG plan, participatingAgentIds, expectedHandoffDepth, mandatory approval gates, an evaluationFixtureSetId, and a totalBudgetCap:

Pipeline (pipelineId) Invocation tier totalBudgetCap
veritas.weekly_briefing_pack creator 5 000
veritas.story_drafting platform-operator 12 000
tara.seasonal_program platform-operator 10 000
nisaba.edition_study_guide customer 3 000
nyx.event_explainer_set tenant-operator 2 500
arete.weekly_review_draft customer 500
metis.course_from_byom customer 20 000

There are two distinct Veritas pipelines that a reader could conflate: VERITAS_WEEKLY_BRIEFING_PACK (creator-tier, aggregates a week's stories into a multi-format briefing pack) and VERITAS_STORY_DRAFTING (platform-operator-tier: enumerate sources → ingest → extract claims → fact-check → contradiction check → counterclaim → Lilith tone review → editorial inbox). v1-pipelines.ts flags this overlap in a comment; they are not duplicates.

The pipeline SPECS live in @oshun/agentic-studio, not in libs/oshun/agent-pipelines/. The @oshun/agent-pipelines package (v0.1.0) is a 4-file re-export shim whose src/index.ts simply re-exports the resolver and the pipeline specs from @oshun/agentic-studio; it is not an independent implementation. Companion-doc references that send readers to agent-pipelines/ for the implementation point at the wrong package.

PipelineRegistry (src/pipelines/pipeline-registry.ts) validates and stores specs; PIPELINE_TIERS, effectivePipelineForTenant, and TenantPipelineCustomization support tenant-scoped customization. Scheduling and observability are real symbols: PipelineSchedule and nextFireFor compute the next fire time, and buildPipelineObservabilityReport / detectPipelineDeviation (pipelines/observability.ts) surface drift between expected and observed runs.

Dashboard, replay, and streaming progress (data layers)#

These are query and data layers, not a rich React admin page. dashboard/dashboard-query.ts materializes DashboardEntry rows with RUN_LIST_BUCKETS, bucketForStatus, cost bands (costBandFor), and latency bands (latencyBandFor), and filterDashboard applies a DashboardFilter. runs/replay.ts provides buildReplayTimeline and reconstructStateAt — the "time-travel" is a pure reconstruction over the recorded envelope, not a separate durable replay engine. runs/streaming-progress.ts defines RUN_PROGRESS_KINDS, PROGRESS_VISIBILITY, shouldDeliverToSubscriber (so a customer never sees an internal operator event), and summarizeForCustomer. The operator console is the admin agentic-operations API routes plus tenant-admin's single agents/page.tsx.

The autonomous creative orchestrator (undocumented until now)#

libs/oshun/creative-orchestrator (@oshun/creative-orchestrator v0.0.1, depending on @oshun/ai) is the actual brief → produced-content engine — fully real with 58 passing tests — and is not mentioned anywhere else in the V1 docs. It is built on the shared @oshun/ai/agent-loop primitives (runStructuredOutput, runReflexion) and is fail-loud throughout: it never fabricates a plan or an artifact, and it fails loudly when no provider or generator is wired. Exported pieces:

  • decomposeBrief — turns a brief into a schema-validated, acyclic CreativePlan DAG (CREATIVE_PLAN_SCHEMA), with real DAG validation (validateDagStructure, detectCycle, topologicalOrder).
  • routePlan / CreativeOrchestrator / orchestrateBrief — governed dispatch of plan nodes to domain generators under real budget/kill-switch/throttle governance (BudgetGovernanceGate, with ALLOW_ALL_GATE for tests).
  • reviseArtifact — a bounded generate → critique → revise (Reflexion) loop around every artifact, with critics createMetricCritic, createLlmJudgeCritic, and the non-provider-gated default createContentEvalCritic.
  • Domain adapterscreateYemayaAgentGenerator wires Yemaya specialized agents in; createMetisNarrator wires Metis narration in.

This is the engine that the Studio's pipelines orchestrate against. Like the rest of the autonomy story, its planner and generators are real seams, and the end-to-end "content actually gets produced through live providers" path depends on those providers being wired at deploy time (the e2e walkthrough agentic-pipeline-customer-invocation is classified "deep" precisely because its tool DAG is creds-bound).

The autonomous creative direction plane#

Everything above is trigger-driven — a human, customer, or schedule invokes a pipeline. libs/oshun/creative-autonomy (@oshun/creative-autonomy, 100 passing tests across 8 files) is the plane that closes the loop the other way: it decides what to create, produces it, judges it, routes it past humans, and learns from their reactions. One AutonomousCreativeDirector.runCycle() pass:

  1. Sense — registered SignalSources emit evidence-bearing OpportunitySignals. Four deterministic sources ship: catalog gaps (demand-weighted coverage deficit + staleness), engagement decay (historical-peak decline with residual demand), calendar events (triangular lead-time fit), and audience requests (rater-count saturation × rating need, clustered from real FeedbackEvents). Signals decay exponentially by half-life and dedupe by (kind, topic); zero sources is a typed error, never "no opportunities."
  2. Ideate — signals become ContentIdeas two ways: deterministic derivation via a signal-kind → angle affinity model with per-angle brief templates, and an optional LLM IdeaSynthesizer seam whose proposals are admitted only if they name a registered category, an unbanned topic, and an actual sensed signal (unknown-signal rejection otherwise). Each idea gets an auditable compellingness prior: a steering-weighted blend of timeliness, novelty (token-Jaccard vs. the recent portfolio), audience value, strategic fit, and feasibility.
  3. Select — a Thompson-sampling bandit (real Beta posteriors, Marsaglia–Tsang gamma sampling, injected RNG) draws per-category acceptance beliefs; ideas pack greedily by priority-per-cost under the cycle budget, per-category caps, and a topic-cluster guard. Every skipped idea carries a typed reason.
  4. Produce — each selected idea is produced as K variants through the ContentProducer seam (canonically createOrchestratorProducer over the creative orchestrator above). Every provider-shaped call flows through a ConcurrencyGovernor: bounded slots (default 2), token-bucket launch rate, and AIMD adaptation — a classified 429/session-limit error halves effective concurrency and opens a cooldown; consecutive successes earn slots back. The plane cannot hammer a session limit by construction.
  5. Judge — variants meet in a round-robin pairwise tournament fitted with Bradley–Terry MM iterations (ε-regularized); the champion faces a 7-dimension compellingness rubric (hook, clarity, novelty, emotional-resonance, grounding, structure, audience-fit) scored by an injected judge panel with a cross-judge disagreement penalty and a human-calibration offset learned from review outcomes. The publish gate demands score ≥ bar and panel confidence — an unconfident pass is not a pass; failures park with named failing dimensions.
  6. Route past humans — per-category AutonomyLevel: human-approval (nothing moves without an explicit decision), review-window (veto window; silence auto-publishes), full-auto (publishes immediately, still listed for after-the-fact feedback). The ReviewQueueStore holds every item with its full ProvenanceChain (signal → idea → score → selection rationale → tournament strength → gate verdict) and an immutable audit history.
  7. Learn — approve/reject/veto/tweak/window-expiry outcomes update the bandit posteriors (documented reward constants), per-category calibration EMAs, and typed SteeringSuggestions. Human steering is a versioned CreativeDirection document (tenets, emphasize/avoid themes, banned topics, priorities, tighten-only autonomy overrides, quality bars); the learner proposes changes but never edits human-owned lists.

The BFF mount is apps/oshun/bff/src/agentic/autonomy-route.ts (/v1/agentic/autonomy/*): cycle trigger + reports + status (operator-gated), the review queue with approve/reject/veto/request-tweak actions, steering GET/PUT, learner suggestions, and published-item feedback. Same posture as the runs route: the loop/gate machinery is real and in-process; the producer/publisher/judge seams bind at the deployable boundary, and with none bound the default plane is fail-closedPOST cycles/run returns 503 autonomy_director_not_configured, never a fabricated cycle. Launch categories (V1_AUTONOMY_LAUNCH_CATEGORIES) are conservative: everything requires human approval except Nyx sky-event briefs, which get a veto window.

The seams are now bound to the real monorepo systems (apps/oshun/bff/src/agentic/autonomy-bindings/, wired by createBoundCreativeAutonomyPlane in server.ts): opportunity signals from the Nyx ephemeris (credential-free lunar phases / season markers / featured events through the lead-time model); production through the creative orchestrator over an LLM text generator (structured {title, text, citations[]}) plus the generation-job pipeline (image/audio/video jobs drained programmatically via processQueuedGenerationJobs — the Isis release gate and released-output catalog apply to autonomous work identically); a specialist judge panel — the content-quality-judge engine (N-sample pointwise craft scoring + position-bias-mitigated pairwise for the tournament, over an @oshun/ai@iris/agents-core provider bridge), the deterministic slop novelty judge, the Lilith content-QA tone judge, and a Sophia declared-citation grounding judge (partial panels: a judge scores only its competent dimensions, and an uncovered dimension is a typed configuration error, never a gap-filled score); publication through the studio-authoring editorial lifecycle (draft → in-review → approved → published under the Lilith gate — a blocker refuses the publish and the item honestly retries); and the admin kill-switch store as the cycle abort signal (global or agent-family:creative-autonomy). The LLM-shaped pieces resolve from OPENROUTER_API_KEY (OSHUN_AUTONOMY_LLM_MODEL override); the startup log states BOUND vs fail-closed. Per-system findings and the honest remaining-gap ledger: CREATIVE_AUTONOMY_UNDERLYING_AUDIT_2026-07-02.md.

How the pieces fit (a single guarded tool call)#

  1. A trigger (user, pipeline, or schedule) lands on POST /v1/agentic/runs/execute. The BFF derives actorId/tenantId from the auth claim, fails closed (503) on any unconfigured tool, checks scopes (403), and checks ownership (409).
  2. The route builds an AgentRunEnvelope plus server-policy-derived GovernedPlannedToolCall[], materializes exact per-run grants for bounded low/medium catalog tools (or requires an injected pre-approved broker for secrets, egress, or high/critical authority), and calls runGovernedToolPlan.
  3. At each boundary, the runtime refreshes pause/cancel/kill state, resolves a fresh exact capability grant, applies secret scope, filesystem/process/network isolation, egress allowlists and the two-phase SSRF guard, then runs admitToolCall for kill-switch and latest-meter budget admission.
  4. The boundary persists a pending call before dispatch. The injected tool runs only after grant, isolation, kill, and budget checks pass. Its ok, partial, or error result replaces the pending record with redacted arguments, result or error, resolved grant, capability audit ids, evidence, effects, artifacts, latency, and measured usage. Denied attempts are recorded too.
  5. Sub-agent hand-offs go through validateDispatch (subset authority, depth cap, budget ≤ parent remaining), deduct budget at dispatch, and reconcile unused budget back to the parent at termination.
  6. Every attempt's actual or policy-defined denied/failed charge updates the total, per-stage, per-tool, and category cost ledger and consumes the mutable budget meters. That measured usage is consumed before the next admission, so a multi-step plan cannot reuse its initial balance. The lifecycle store writes through the updated envelope and retains kill/budget audit events; the route returns the same telemetry-bearing run as a JSON-safe projection.

The generated field-by-field boundary is Agentic Run Execution Fields.