inmake DOCS
Docs / Generative / drop-core-generative

drop-core-generative

The package behind the agents: layout, commands, how the console embeds it.

Tools, rules, agent runners and an MCP server for AI agents working on drop-core stores. This package is the server-side AI layer that used to live in admin/server/agent-*.mjs: the Gemini / Claude tool loop, the admin agents, all their tools, and the system-prompt rules — extracted so the same capabilities are consumable by the admin backend, external AI agents (Claude Code, Cursor, …) via MCP, and scripts.

Shipping note: docs/AGENTS.md and rules/*.md are deployed artifacts, not just docs — they ship inside the hosted MCP bundle and the admin agents' system prompts. Editing them requires re-vendoring before the next deploy: npm run vendor:mcp in functions/ (AGENTS.md + rules ride the /mcp endpoint) and npm run vendor:generative-sdk in admin/ (rules ride the admin agents).

Layout#

  • rules/*.md — the canonical rule fragments (Handlebars binding, CSS scoping, site structure, assets, template bundles, schema field kinds, forms, design onboarding, preview components, data collections, store templates, element kits — the ONE kit-authoring guide that replaced templates/HOWTO.md — the commerce product/cart model, and load-more paging). scripts/build-rules.mjs inlines them into the generated src/rules/content.ts (machine-written — edit the fragments, never content.ts) at build time, so both the tsc lib and the esbuild bundles work without runtime fs reads. Fragments may contain {%placeholder%} markers that src/rules/compose.ts substitutes with per-turn context — the marker syntax is deliberately not Handlebars ({{…}}), because the fragments are full of literal Handlebars examples that must ride through untouched (fill() in compose.ts is the authority). Anything handing a fragment to a model must go through composeRule(name, ctx) (as the code agent's readRule does), not raw RULES[name], or the markers arrive verbatim.

  • src/rules/compose.ts — assembles the fragments + dynamic context into the agents' system prompts (prompt-equivalent to the historical buildSystem() bodies). Also exports FIELD_KINDS (see Sync notes).

  • src/rules/preview_base.ts — the design-preview specimen CSS + component vocabulary, plus a re-export of TOKEN_VAR_DOC. Browser-safe: the admin SPA imports it as drop-core-generative/preview (Vite alias + tsconfig path — do NOT alias the package root into the SPA, it pulls node:net/sharp paths).

  • src/rules/token_var_doc.ts — TOKEN_VAR_DOC, the canonical design-token CSS variable vocabulary that compose.ts feeds into the design/code prompts and that the generated setup/design.css must stick to. Owned here (this package is its only consumer); import-free so the browser-safe preview path can re-export it.

  • src/loop/tool_loop.ts — the shared streaming tool loop, Gemini or Claude through a ModelDriver (see the drivers below) (SSE events reasoning/text/tool/interactive/design/truncated/done/error, pause/resume on interactive picker tools, per-turn token-usage accumulation on result.usage). Retained-reasoning contract (.dev/plans/agent-context-retention.md): the model turn is recorded part- faithfully (streamed order, one part per logical part, each keeping its own thoughtSignature — never merged), thought parts stay in contents for the whole turn (hosts strip them cross-turn via stripThoughtParts unless opted in), duplicate reads are stubbed deterministically (supersedeToolReads), dummy-signature substitutions are counted on usage.dummySignatures, and step-cap exhaustion reports status: 'truncated' instead of a fake done. A step's calls answer in call order, but a RUN of consecutive read-only calls (readOnlyHint, when the caller passes declarations and a guard — the code agent does) executes concurrently, at most READ_CONCURRENCY (8) at once, each with its own 403 slot (CoreApiGuard.run, AsyncLocalStorage); writes keep strict order. The code agent's confirmSummary (the confirm card's argument view) is JSON all the way down — nested maps never render as [object Object], string leaves are clamped inside the JSON, the whole value at 300 chars. The code-agent runner (runCodeAgentTurn, the GoogleGenAI-injectable core of runCodeAgentStream) also reports result.firstWriteSeen (the chat's first-write state from the SDK's view — the host stamps the session from it; a confirm card answered allow, or a write the gate let through, flips it in-loop so one chat asks once, not once per write tool across a pause/ resume, and a tool in approval.allowed never gets the card) and refuses a request with nothing to run (a resume with no open call and no message) as {status: 'error', error: 'nothing_pending'} — no model call, history untouched — instead of letting Gemini 400 on a history ending with a model turn.

    Stopping a turn (.dev/plans/assistant-persistent-sessions.md §5.3): every runner and runToolLoop take an optional signal: AbortSignal. The loop checks it at the top of every step and before/after every executor, and passes it as config.abortSignal on every generateContentStream call (a per-call shallow copy — the caller's config is never mutated; @google/genai ≥ 2.10 honours it client-side only, so a Gemini call already in flight is still billed). A fired signal ends the turn with an error whose name is 'AbortError' (isAbortError(e); abortErrorFor( signal) normalises a string / custom abort reason into one) thrown before the step's model turn lands in contents: the loop truncates contents back to where the aborted step began, so the history the host holds ends on the last complete step — never a half-streamed model turn, never a functionCall turn with no response. Writes an executor already made stand. result.status is 'aborted' and result.text is the text streamed so far in the turn when the error surfaces. Five runners (runAgentStream, runDesignAgentStream, runSchemaAgentStream, runTemplateAgentStream, runElementAgentStream) reject with that error — the host catches it with isAbortError, persists the user message plus a "stopped" marker and keeps the history it passed in. The code agent (runCodeAgentStream / runCodeAgentTurn) returns instead: {status: 'aborted', text: <streamed so far>, usage, firstWriteSeen} with no confirmAnswer and contents = the prior history exactly as passed in (the aborted turn never enters the model's memory; a signal already fired at entry returns the same shape before any settlement or model call). The signal also reaches maybeCompactContents({signal}) (an abort rejects instead of the stub cut) and the two in-turn generateContent sites — the fetch tool's injection classifier (FetchPolicy.signal) and generateImage (buildImageExecutors(…, signal)).

  • src/loop/model_driver.ts / gemini_driver.ts / anthropic_driver.ts — the model seam (plans/console/assistant-claude-provider.md). Everything the loop does with the model — one streamed step, the reasoning / text deltas, the step's parts, its usage and why it stopped (end / tool_use / refusal / max_tokens) — goes through a ModelDriver; pauses, the confirm gate, executors and abort stay in the loop. ToolLoopOptions.driver absent = a GeminiDriver built from ai / model / config (the pre-seam code, moved verbatim: per-part signature aggregation, the thinkingLevel retry, the dummy-signature backfill). History stays Gemini-shaped Content[] for both providers — every runner, history helper and the persisted session keep one format. AnthropicDriver translates it on every step with the pure, deterministic toClaudeMessages (functionCall.id = the tool_use id, else call_{i}_{k}; responses paired by id, else name + order; only thought parts carrying the driver's anthropic marker replay as thinking / redacted_thinking blocks; inlineData → image / PDF document / decoded text) and maps the final message back into parts. Each request: the runner's system prompt as a cached system block + an automatic tail breakpoint, adaptive thinking with display: 'summarized' and block_binding.prefix_mismatch_behavior: 'drop_block' (beta thinking-binding-controls-2026-08-01 — a dropped block is counted on usage.thinkingDropped), output_config.effort = the runner's Gemini thinking level (web / code medium, design high, schema / template / element low), server-side fallbacks: 'default' (beta server-side-fallback-2026-07-01), max_tokens 64000, no temperature. A refusal / max_tokens stop ends the turn {status: 'error', error} with the declined step NOT in contents and none of its tools run. Runners get the driver from claudeDriverFor({model, keys, config, effort}) — built from the runner's own Gemini config, so both providers get the very same system prompt and tool set (one composition contract); undefined for a Gemini model, a typed claude_unavailable error without keys. adaptHistoryForProvider(contents, from, to) (in tool_loop.ts) neutralises a stored history for the other provider (all thought parts out; Gemini call signatures out going to Claude, Claude tool ids out going to Gemini).

  • src/loop/compaction.ts — maybeCompactContents: the hosts' session- history bound. Over a byte budget (~400 KB default) the oldest turns are summarized into one [compacted history] brief (flash-lite, minimal thinking) and the tail kept verbatim, cutting only at turn boundaries — replaces the old slice(-40) truncation.

  • src/models/gemini_models.ts — model aliases, the curated model sets (AGENT_MODELS: Gemini Pro / Gemini Flash / Gemini Argon; AGENT_IMAGE_MODELS: Nanobanana Lite / Nanobanana Pro — the only ids resolveModel / resolveImageModel accept, any other runs the default; mirrored in the admin agent store), their listing narrowed to the cached live Gemini catalog (listGeminiModels / listGeminiImageModels), providerOf(model), costUsd (kept for compatibility — turnUsd at today's prices; see Pricing below), plus agentThinkingConfig(model, level) — the per-agent thinkingLevel pin (Gemini 3+/rolling aliases only; flash/pro reject minimal, and the loop degrades a rejected level at run time instead of failing the turn).

  • src/models/claude_models.ts — the curated Claude agent models (CLAUDE_AGENT_MODELS: Claude Sonnet claude-sonnet-5-5, Claude Opus claude-opus-5-5, Claude Fable claude-fable-5-1 — resolveModel accepts them, isClaudeModel), and listClaudeModels({apiKey, workspaceId}) — the curated ids the workspace serves (Anthropic models.list, cached 1 h per workspace, stale beats broken).

  • src/models/model_pricing.ts — the price catalog (see Pricing below).

  • src/types.ts — the consumer contract: the CoreApi seam type, the SSE AgentEvent/Emit types, ToolDeclaration/ExecMap, the mutable AgentResult accumulator (its shape is part of the persisted agent_sessions format), AgentUsage (the raw material for host-side credit metering), FieldSchema, and the per-agent turn contexts (WebBuilderCtx, DesignAgentCtx, SchemaAgentCtx, TemplateAgentCtx, ElementAgentCtx, CodeAgentCtx). Hosts embedding the runners should treat these types as the API surface.

  • src/tools/ — tool declarations + executors:

    • store_read — readStoreDocument / listStoreCollection, guarded reads (resolveStorePath confines every path to the active store, customers/** refused).
    • site_checks — checkSite: the whole-site completeness check in ONE call (documents missing an SEO description across every SEO-enabled collection and locale, noindex skipped; the favicon; the share image; article covers), each finding a ready-to-write path + label, with excerpt: true a ~300-character plain-text excerpt to write a description from. The rules are core-lib's web/site_checks.ts, shared with the admin Overview's "Assistant — what's left" widget, so both count the same documents for those checks. Wider than the widget since .dev/plans/website-audit.md R14: every PAGE document (pages, routed collections) is judged by the schema it names — per-document general/pages/{doc} schemas included — with seo.noContainer for page documents whose schema has no SEO container, plus titles (missing / duplicate per locale / over 60 characters), descriptions (under 50 / over 160 characters, duplicate, a copy of the title), sitemap (routed or slug-bearing collections missing from setup/web.sitemap.collections) and structuredData (the jsonld container and records per collection).
    • audit — auditProject: the project audit's COLLECT step in ONE read-only call (rules/audit-website.md, .dev/plans/website-audit.md R12) — element placements per distinct document, positional keys, literal ids and text, same-value / label-shaped / split-link / URL / contact fields per shared schema, picklist-shaped collections, schema locks and reference fields without collection_ref, image variants (declared sizes the stored images lack, {{#each}} listings and galleries that render the original), {{t}} usage + translation groups, the design sheet's facts (vocabulary flag vs the sheet, own rules, unregistered tokens, seeds vs the sheet), Data groups, the pinned menu, stale JSON-LD, code placement (code, audit_code.ts: shared CSS / JS one element or template alone uses, units reaching into or relying on another unit's own code, duplicated and unmatched shared code) and the locale split (locales, audit_locales.ts: every root field compared across the locale documents — language-neutral fields kept per language, general fields that drift between locales or hold copy, nested general). Signals with evidence paths, never verdicts.
    • store_write — updateStoreDocuments (the batch form: up to 50 {path, values} entries in one call, each through the single write's executor and guards, distinct paths written concurrently, a path named twice refused, one {path, ok, error?, warning?} result per entry; the confirm gate judges it entry by entry and a grant of either merge tool covers the other) + updateStoreDocument (guarded deep-merge, creates the doc when missing, expands dot-path keys, validates form pattern regexes; store-root writes masked to name/locales, setup/elements writes masked to js/css — the shared element runtime; a headTags write that links store files as site-wide scripts returns a warning steering to the element channels; a data/element/product write whose PLAIN strings carry HTML tags is refused — rich text is the {type:"html"} envelope, see binding_lint; an element placement whose data contradicts the element's declared fields is refused with the declaration to write first — placement_lint) + deleteStoreKeys (per-field delete via /admin/update {action:'delete'}, dot-paths reach nested keys; same setup/elements mask) + deleteStoreDocument (whole-document delete via /admin/delete; store root + customers/** refused) + writeSchema (direct SchemaEntity write — a flat id for a SHARED collection schema, or general/{collection}/{document} for a per-document one, which also stamps the schema ref onto that document in every locale; locked sets / clears the structure lock, SchemaEntity.locked — shared with the schema agent's proposeSchema through parseSchemaValues).
    • data_collections — listCollections (read the curated setup/data.collections[] registry) + registerCollection (make agent-created collections visible to the admin UI, incl. an optional columns pick of the admin listing's columns — entries may reach one level into an object field, e.g. cover.src — a reorder flag enabling the admin's built-in manual drag ordering and groups, the Data page group ids the row sits in) + writeDataGroups (replace the Data page's group list, {id?, name, members?} — members sets a group's collections / custom collections exactly; removed ids are pruned from every row; one read-modify-write of setup/data); a call that REMOVES a stored category — or a kept category's stored locale segment — answers {needsConfirmation: true, key, removed, removedSegments?} until confirmRemove: true, and one documents still carry in seo.category is refused with the per-locale /admin/counts unless force — url-admin D7. Under an agent host the flags count only once the USER confirmed in chat: the executor takes a turn.removalConfirmed(key) probe the code agent answers from the history (confirmedRemovals(contents): the ask, then a user message, not yet consumed by a successful write), so a flag sent on the first call or re-sent in the same turn gets the same needsConfirmation and writes nothing; the MCP server passes no turn state and takes the flags as sent). Also exports RESERVED_KEYS (see Sync notes).
    • url_routes — the slug registry's two human-editable parts, through crud's registry normalization (core-lib normalizeSlugDoc, run client-side first): setRedirect (manual redirect XOR redirects.{locale} + temporary, or clear; refuses a language with a live page slot unless overridePage, and a root key that is a routed collection's segment / alias or a URL language prefix — url-admin D8; returns each language's answer via core-lib slotsFor) and retireFormerUrl (deletes one moved slot; live slots refused), plus the read-only resolveAddress (/public/slug per store locale, a path-mode language prefix stripped). Generic registry writes stay refused in store_write (registryWriteError). Exposed to the MCP server and the code agent; the web builder gets resolveAddress only.
    • console — the store's console settings over /project/console/{read,menu,overview}: readConsoleSettings (the pinned side menu + the Overview widgets, with the vocabularies to write them — page routes, item kinds, widget kinds and their discriminators, day windows) and the whole-document replaces writeConsoleMenu / writeConsoleOverview. Both docs are shared by Demo and PROD; the executors run core-lib's validateMenuSettings / validateOverviewSettings before any I/O, the backend runs them again. Writes need the owner role or the project.console grant. Exposed to the MCP server and the code agent (where the two writes are in CONFIRMABLE_WRITE_TOOLS); the contract is rules/console.md.
    • services — the store's services (its own backend code at /fn/{id}/…, .dev/plans/modules/services.md) over /project/services/{list,read,write,check,publish,logs,rollback} and /public/service: listServices, readService (code: true / version: n adds that version's code) and writeService — settings (routes, egress, env, origins, secretNames → the doc's secrets, enabled) through write, then code without publish = check only (the D13 report, nothing goes live — the agent iterates on it), code with publish: {note?} = publish (an error finding refuses it; applied {type: 'service', id, version}; the answer lists errors, warnings and secretsToSet); callService runs one route of the LIVE version for real on the core path (/public/service, host: 'studio', the guard vouched for on a non-GET; a JSON body parsed, a long one cut), readServiceLogs the log lines + route health, rollbackService makes an earlier version live. Secret values never pass (plan Q4): secrets: {…}, secretValues / values and a credential-named env key are refused before any I/O; the answers carry has_secrets (name → boolean) only. Settings are validated with core-lib validateService first. Exposed to the MCP server and the code agent only (writeService, callService and rollbackService are in CONFIRMABLE_WRITE_TOOLS; a code + publish call, a non-GET callService and every rollbackService are code pattern rules of the confirm gate); the contract is rules/services.md.
    • assets — listTemplateAssets / readTemplateAsset (read uploaded template bundles).
    • files — listStoreFiles / readStoreFile / writeStoreFile general store-file read/write + writeTemplateAsset single bundle-entry edit.
    • local_upload — uploadStoreFile, the stdio-only binary upload of one LOCAL file (any type, up to 1 GiB) over the resumable large-file path (/admin/upload/start → chunked PUTs straight to Cloud Storage through drop-core-sdk's ResumableUpload + fileSource → /admin/upload/finish). Fenced: the path resolves (symlinks followed) inside the upload root, no hidden segment (.env, .git/…) is ever read, and the bytes go only to a https://storage.googleapis.com/upload/… session URI.
    • images — generateImage + the shared field-aware upload pipeline (converts to the field's configured format, fits its max size, and renders every declared size variant, denormalized onto the returned imageValue so templates can address {{field.thumb}} / {{field.srcset}}).
    • fetch — fetchWebPage / importImageFromUrl (SSRF-guarded external fetch/import).
    • web_template — writeWebTemplate, versioned page-template writes (returns a warning when the fragment's braces disagree with the schema's field kinds — binding_lint). Takes either the full html + css or edits — verbatim search/replace spans against the stored base version, applied all-or-nothing by the pure edits.ts helper (applyEdits / validateEdits; the shape every "full body" write tool will share — plans/template-diff-writes.md).
    • store_template — writeStoreTemplate, invoice / e-mail template writes: the HTML data, subject, name and the record's sample (the editor preview's render variables, replaced whole; data may be omitted for a sample-only write). Editor-bound in the template agent; the explicit-templateId variant serves the code agent and MCP.
    • elements — writeElement, the element agent's single write tool (MERGE-writes the ONE store/{sid}/elements/{elementId} document open in the editor) + writeStoreElement, its id-as-argument twin for the code agent and MCP (same validation, /admin/create merge so it also CREATES an element; element behavior JS rides the element — see rules/elements.md for the channel contract; a sampleJson whose plain strings carry tags, or whose shape contradicts the fields, is refused; a braces/field-kind mismatch in html, or a fieldsJson-only write that leaves the stored sample out of shape, comes back as a warning).
    • placement_lint — the placement-shape guard over core-lib's validatePlacement: findElementPlacements (every {type:"element"} in a write, nested ones included), lintPlacements (one masked fields read per distinct element, issues with full paths) and the refusal / warning texts (placementShapeError, sampleShapeWarning).
    • binding_lint — the braces ↔ value-shape contract, enforced at the write boundary: findMarkupInPlainStrings (refuse tag-carrying plain strings on content surfaces — rich text is ALWAYS the {type:"html", value} envelope, the shape the admin editor writes and the one the renderer's Live Edit can address under either brace form) and lintTemplateBindings (a dependency-free mustache scan against the field definitions: {{{ }}} on a plain field, {{ }} on an icon or a rendered .html fragment → warnings, never a refusal — the schema walk is heuristic). Unit-covered by .dev/binding-lint-test.mjs.
    • design_tools — the interactive pickers (pickColor / pickFont / pickRadius / pickChoice / confirmDesign — the UI_TOOLS set that pauses the loop), parseReference, writePreview, and previewDesign / commitDesign — these expand the onboarding locally through core-lib's expandDesign (the paid /gen/design endpoint was removed); commitDesign persists setup/design itself and is hard-gated against committing in the same turn as its preview.
  • src/agents/ — six turn runners, all streaming:

    runner agent MAX_STEPS where it's used
    runAgentStream web builder 12 admin DocumentView (web mode)
    runDesignAgentStream design system (legacy) 12 only continues old design threads — design work runs in runCodeAgentStream since 2026-10-24 (same design tools; the unsaved Properties draft rides ctx.designDraft)
    runSchemaAgentStream schema builder 10 schema editing
    runTemplateAgentStream template assistant 8 Templates editor
    runElementAgentStream element agent 8 the admin element editor (Design System → Elements)
    runCodeAgentStream code agent 32 the admin's general assistant — Code Studio (/store/:sid/code), the Design System and Welcome pages, every other console page

    MAX_STEPS bounds the tool-loop steps of one turn (a step = one Gemini call); the code agent gets the largest budget because it chains schema → collection → documents → template in a single turn. Independent reads are meant to go out as parallel tool calls in one step (the code agent's WORKFLOW line says so; the loop then runs them concurrently), known writes to several documents as ONE updateStoreDocuments call, and a "what is missing" question as ONE checkSite call — the budget is the backstop, not the plan; a turn that still runs dry reports status: 'truncated' and the admin offers Continue.

    The element agent edits one ElementEntity open in the admin element editor (see Elements below): its only write tool is writeElement (merge-write of that one document), plus the store-read tools and a readContext replay of the open element (including unsaved editor changes). No interactive tools.

    The code agent is the whole-store editor and a superset of the other agents: guarded direct writes, schema design + registerCollection, versioned web templates and store templates by explicit target, the store's /fn services (listServices / readService / writeService / callService / readServiceLogs / rollbackService, rule services linked in its RULES INDEX), and the full interactive design flow. Its system prompt is cache-stable and its per-turn context — the open focus file and the contexts list (preview regions the user attached in the admin Studio: a document + field the write tools address, or a markup CONTAINER named by its selector plus the web template schemaId/templateVersion that renders it) — rides in the user content instead of the prompt (see Prompt budget & cache stability below).

    Every runner takes apiKey (Gemini — always required: image generation, the fetch classifier and compaction run on Gemini) and an optional anthropic: {apiKey, workspaceId} (required when ctx.model is a Claude id; the key is not workspace-scoped, so the client sends anthropic-workspace-id on every request).

    All I/O goes through the caller-supplied coreApi(path, body) => Promise<Response> seam — no Firebase/Express dependency (see src/core_api.ts makeCoreApi for a standalone factory).

  • src/mcp/ — the drop-core-mcp server + stdio bin (see MCP server below).

  • docs/AGENTS.md — guide for external AI agents (also served as the drop-core://docs/agents MCP resource — see the shipping note at the top).

Commands#

  • npm run build — build-rules + tsc → lib/ (declarations included — types points at lib/index.d.ts).
  • npm run dev — build-rules + tsc --watch (pair with a full relaunch of npm run dev in admin/ to pick up changes — the dev middleware require()s lib/ and Vite's restart keeps the require cache).
  • npm run bundle — builds core-lib → sdk-js → this package, then esbuild index.ts → dist/drop_core_generative.js (CJS; @google/genai + sharp external). This is the artifact admin/'s vendor:generative-sdk copies.
  • npm run bundle:mcp — same chain, but esbuild mcp-http.ts → dist/drop_core_mcp.cjs (CJS; @modelcontextprotocol/sdk, @google/genai, sharp external). This is the artifact functions/'s vendor:mcp copies.
  • npm run mcp — run the stdio MCP server locally (env vars below).
  • npm run docs:mcp — export the MCP tool + resource reference to the repo-root docs/site/generated/mcp-tools.json (reads the built lib/; buildMcpTools is exported for it). The public docs site (inmake.dev/docs/mcp/tools) renders that file — re-run and commit it after any tool or rule change.

One measurement script lives in the gitignored repo-root .dev/ (it reads the built lib/, so npm run build first); the prompt-stability check is the unit test src/rules/compose.test.ts plus the live usage.steps[0].cached ratio (see "The composition contract"):

  • node .dev/agent-turn.mjs "message" — drives ONE web-builder turn against Demo/instore and prints the tool call order and per-step usage (the admin chat only renders applied chips, not the trace). Writes are blocked unless --write; --fail-read forces the template read to fail, exercising the writeWebTemplate guard.

See the repo-root docs/sdk-development.md for the full build-order / bundle / vendoring runbook.

Admin integration#

The admin backend consumes the vendored bundle (admin/server/vendor/drop_core_generative.cjs, rebuilt via npm run vendor:generative-sdk in admin/) so server.mjs keeps zero workspace runtime deps on App Hosting. The Vite dev middleware (admin/vite.config.ts devAgent) require()s this package's compiled lib/ directly. The SPA imports only the browser-safe drop-core-generative/preview subpath.

server.mjs exposes the runners at POST /agent/{env} as an SSE stream and picks one by mode (web / design / schema / template / element / code). Every turn is credit-metered there, in every mode: token usage (AgentResult.usage) is priced per model call from the catalog (turnCredits(model, usage, turnStart, margin), see Pricing) and post-paid per turn against the central incore wallet. The admin passes anthropic: {apiKey, workspaceId} — the CLAUDE_API key plus the workspace id fixed in admin/server/claude-keys.mjs — and @anthropic-ai/sdk is an esbuild external of the vendored bundle (an admin/package.json dependency, like @google/genai). Sessions are listed/replayed through /agent/{env}/sessions.

Public behavior is unchanged from the pre-extraction admin code: tool names, SSE event types, result shapes and the persisted agent_sessions format all round-trip.