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.mdandrules/*.mdare 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:mcpinfunctions/(AGENTS.md + rules ride the/mcpendpoint) andnpm run vendor:generative-sdkinadmin/(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 replacedtemplates/HOWTO.md— the commerce product/cart model, and load-more paging).scripts/build-rules.mjsinlines them into the generatedsrc/rules/content.ts(machine-written — edit the fragments, nevercontent.ts) at build time, so both the tsc lib and the esbuild bundles work without runtime fs reads. Fragments may contain{%placeholder%}markers thatsrc/rules/compose.tssubstitutes 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()incompose.tsis the authority). Anything handing a fragment to a model must go throughcomposeRule(name, ctx)(as the code agent'sreadRuledoes), not rawRULES[name], or the markers arrive verbatim.src/rules/compose.ts— assembles the fragments + dynamic context into the agents' system prompts (prompt-equivalent to the historicalbuildSystem()bodies). Also exportsFIELD_KINDS(see Sync notes).src/rules/preview_base.ts— the design-preview specimen CSS + component vocabulary, plus a re-export ofTOKEN_VAR_DOC. Browser-safe: the admin SPA imports it asdrop-core-generative/preview(Vite alias + tsconfig path — do NOT alias the package root into the SPA, it pullsnode:net/sharppaths).src/rules/token_var_doc.ts—TOKEN_VAR_DOC, the canonical design-token CSS variable vocabulary thatcompose.tsfeeds into the design/code prompts and that the generatedsetup/design.cssmust 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 aModelDriver(see the drivers below) (SSE eventsreasoning/text/tool/interactive/design/truncated/done/error, pause/resume on interactive picker tools, per-turn token-usage accumulation onresult.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 ownthoughtSignature— never merged), thought parts stay incontentsfor the whole turn (hosts strip them cross-turn viastripThoughtPartsunless opted in), duplicate reads are stubbed deterministically (supersedeToolReads), dummy-signature substitutions are counted onusage.dummySignatures, and step-cap exhaustion reportsstatus: 'truncated'instead of a fakedone. A step's calls answer in call order, but a RUN of consecutive read-only calls (readOnlyHint, when the caller passesdeclarationsand aguard— the code agent does) executes concurrently, at mostREAD_CONCURRENCY(8) at once, each with its own 403 slot (CoreApiGuard.run, AsyncLocalStorage); writes keep strict order. The code agent'sconfirmSummary(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, theGoogleGenAI-injectable core ofrunCodeAgentStream) also reportsresult.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 inapproval.allowednever 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 andrunToolLooptake an optionalsignal: AbortSignal. The loop checks it at the top of every step and before/after every executor, and passes it asconfig.abortSignalon everygenerateContentStreamcall (a per-call shallow copy — the caller'sconfigis 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 whosenameis'AbortError'(isAbortError(e);abortErrorFor( signal)normalises a string / custom abort reason into one) thrown before the step's model turn lands incontents: the loop truncatescontentsback 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.statusis'aborted'andresult.textis 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 withisAbortError, 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 noconfirmAnswerandcontents= 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 reachesmaybeCompactContents({signal})(an abort rejects instead of the stub cut) and the two in-turngenerateContentsites — the fetch tool's injection classifier (FetchPolicy.signal) andgenerateImage(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, thereasoning/textdeltas, the step's parts, its usage and why it stopped (end/tool_use/refusal/max_tokens) — goes through aModelDriver; pauses, the confirm gate, executors and abort stay in the loop.ToolLoopOptions.driverabsent = aGeminiDriverbuilt fromai/model/config(the pre-seam code, moved verbatim: per-part signature aggregation, thethinkingLevelretry, the dummy-signature backfill). History stays Gemini-shapedContent[]for both providers — every runner, history helper and the persisted session keep one format.AnthropicDrivertranslates it on every step with the pure, deterministictoClaudeMessages(functionCall.id= thetool_useid, elsecall_{i}_{k}; responses paired by id, else name + order; only thought parts carrying the driver'santhropicmarker replay asthinking/redacted_thinkingblocks;inlineData→ image / PDF document / decoded text) and maps the final message back into parts. Each request: the runner's system prompt as a cachedsystemblock + an automatic tail breakpoint, adaptive thinking withdisplay: 'summarized'andblock_binding.prefix_mismatch_behavior: 'drop_block'(betathinking-binding-controls-2026-08-01— a dropped block is counted onusage.thinkingDropped),output_config.effort= the runner's Gemini thinking level (web / codemedium, designhigh, schema / template / elementlow), server-sidefallbacks: 'default'(betaserver-side-fallback-2026-07-01),max_tokens64000, notemperature. Arefusal/max_tokensstop ends the turn{status: 'error', error}with the declined step NOT incontentsand none of its tools run. Runners get the driver fromclaudeDriverFor({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);undefinedfor a Gemini model, a typedclaude_unavailableerror without keys.adaptHistoryForProvider(contents, from, to)(intool_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 oldslice(-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 idsresolveModel/resolveImageModelaccept, any other runs the default; mirrored in the adminagentstore), their listing narrowed to the cached live Gemini catalog (listGeminiModels/listGeminiImageModels),providerOf(model),costUsd(kept for compatibility —turnUsdat today's prices; see Pricing below), plusagentThinkingConfig(model, level)— the per-agentthinkingLevelpin (Gemini 3+/rolling aliases only; flash/pro rejectminimal, 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 Sonnetclaude-sonnet-5-5, Claude Opusclaude-opus-5-5, Claude Fableclaude-fable-5-1—resolveModelaccepts them,isClaudeModel), andlistClaudeModels({apiKey, workspaceId})— the curated ids the workspace serves (Anthropicmodels.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: theCoreApiseam type, the SSEAgentEvent/Emittypes,ToolDeclaration/ExecMap, the mutableAgentResultaccumulator (its shape is part of the persistedagent_sessionsformat),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 (resolveStorePathconfines 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,noindexskipped; the favicon; the share image; article covers), each finding a ready-to-writepath+label, withexcerpt: truea ~300-character plain-text excerpt to write a description from. The rules are core-lib'sweb/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-documentgeneral/pages/{doc}schemas included — withseo.noContainerfor page documents whose schema has no SEO container, plustitles(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 fromsetup/web.sitemap.collections) andstructuredData(thejsonldcontainer 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 withoutcollection_ref, image variants (declaredsizesthe 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,generalfields that drift between locales or hold copy, nestedgeneral). 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 formpatternregexes; store-root writes masked toname/locales,setup/elementswrites masked tojs/css— the shared element runtime; aheadTagswrite 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, seebinding_lint; an element placement whosedatacontradicts the element's declaredfieldsis refused with the declaration to write first —placement_lint) +deleteStoreKeys(per-field delete via/admin/update{action:'delete'}, dot-paths reach nested keys; samesetup/elementsmask) +deleteStoreDocument(whole-document delete via/admin/delete; store root +customers/**refused) +writeSchema(direct SchemaEntity write — a flat id for a SHARED collection schema, orgeneral/{collection}/{document}for a per-document one, which also stamps theschemaref onto that document in every locale;lockedsets / clears the structure lock,SchemaEntity.locked— shared with the schema agent'sproposeSchemathroughparseSchemaValues).data_collections—listCollections(read the curatedsetup/data.collections[]registry) +registerCollection(make agent-created collections visible to the admin UI, incl. an optionalcolumnspick of the admin listing's columns — entries may reach one level into an object field, e.g.cover.src— areorderflag enabling the admin's built-in manual drag ordering andgroups, the Data page group ids the row sits in) +writeDataGroups(replace the Data page's group list,{id?, name, members?}—memberssets a group's collections / custom collections exactly; removed ids are pruned from every row; one read-modify-write ofsetup/data); a call that REMOVES a stored category — or a kept category's stored locale segment — answers{needsConfirmation: true, key, removed, removedSegments?}untilconfirmRemove: true, and one documents still carry inseo.categoryis refused with the per-locale/admin/counts unlessforce— url-admin D7. Under an agent host the flags count only once the USER confirmed in chat: the executor takes aturn.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 sameneedsConfirmationand writes nothing; the MCP server passes no turn state and takes the flags as sent). Also exportsRESERVED_KEYS(see Sync notes).url_routes— the slug registry's two human-editable parts, through crud's registry normalization (core-libnormalizeSlugDoc, run client-side first):setRedirect(manualredirectXORredirects.{locale}+temporary, orclear; refuses a language with a live page slot unlessoverridePage, 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-libslotsFor) andretireFormerUrl(deletes onemovedslot; live slots refused), plus the read-onlyresolveAddress(/public/slugper store locale, a path-mode language prefix stripped). Generic registry writes stay refused instore_write(registryWriteError). Exposed to the MCP server and the code agent; the web builder getsresolveAddressonly.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 replaceswriteConsoleMenu/writeConsoleOverview. Both docs are shared by Demo and PROD; the executors run core-lib'svalidateMenuSettings/validateOverviewSettingsbefore any I/O, the backend runs them again. Writes need the owner role or theproject.consolegrant. Exposed to the MCP server and the code agent (where the two writes are inCONFIRMABLE_WRITE_TOOLS); the contract isrules/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: nadds that version's code) andwriteService— settings (routes,egress,env,origins,secretNames→ the doc'ssecrets,enabled) throughwrite, thencodewithoutpublish=checkonly (the D13 report, nothing goes live — the agent iterates on it),codewithpublish: {note?}=publish(anerrorfinding refuses it;applied{type: 'service', id, version}; the answer listserrors,warningsandsecretsToSet);callServiceruns 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),readServiceLogsthe log lines + route health,rollbackServicemakes an earlier version live. Secret values never pass (plan Q4):secrets: {…},secretValues/valuesand a credential-namedenvkey are refused before any I/O; the answers carryhas_secrets(name → boolean) only. Settings are validated with core-libvalidateServicefirst. Exposed to the MCP server and the code agent only (writeService,callServiceandrollbackServiceare inCONFIRMABLE_WRITE_TOOLS; acode+publishcall, a non-GETcallServiceand everyrollbackServiceare code pattern rules of the confirm gate); the contract isrules/services.md.assets—listTemplateAssets/readTemplateAsset(read uploaded template bundles).files—listStoreFiles/readStoreFile/writeStoreFilegeneral store-file read/write +writeTemplateAssetsingle 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 throughdrop-core-sdk'sResumableUpload+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 ahttps://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 returnedimageValueso templates can address{{field.thumb}}/{{field.srcset}}).fetch—fetchWebPage/importImageFromUrl(SSRF-guarded external fetch/import).web_template—writeWebTemplate, versioned page-template writes (returns awarningwhen the fragment's braces disagree with the schema's field kinds —binding_lint). Takes either the fullhtml+cssoredits— verbatim search/replace spans against the stored base version, applied all-or-nothing by the pureedits.tshelper (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 HTMLdata,subject,nameand the record'ssample(the editor preview's render variables, replaced whole;datamay be omitted for a sample-only write). Editor-bound in the template agent; the explicit-templateIdvariant serves the code agent and MCP.elements—writeElement, the element agent's single write tool (MERGE-writes the ONEstore/{sid}/elements/{elementId}document open in the editor) +writeStoreElement, its id-as-argument twin for the code agent and MCP (same validation,/admin/createmerge so it also CREATES an element; element behavior JS rides the element — seerules/elements.mdfor the channel contract; asampleJsonwhose plain strings carry tags, or whose shape contradicts the fields, is refused; a braces/field-kind mismatch inhtml, or afieldsJson-only write that leaves the stored sample out of shape, comes back as awarning).placement_lint— the placement-shape guard over core-lib'svalidatePlacement:findElementPlacements(every{type:"element"}in a write, nested ones included),lintPlacements(one maskedfieldsread 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) andlintTemplateBindings(a dependency-free mustache scan against the field definitions:{{{ }}}on a plain field,{{ }}on an icon or a rendered.htmlfragment → 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— theUI_TOOLSset that pauses the loop),parseReference,writePreview, andpreviewDesign/commitDesign— these expand the onboarding locally through core-lib'sexpandDesign(the paid/gen/designendpoint was removed);commitDesignpersistssetup/designitself and is hard-gated against committing in the same turn as its preview.
src/agents/— six turn runners, all streaming:runner agent MAX_STEPSwhere it's used runAgentStreamweb builder 12 admin DocumentView ( webmode)runDesignAgentStreamdesign system (legacy) 12 only continues old designthreads — design work runs inrunCodeAgentStreamsince 2026-10-24 (same design tools; the unsaved Properties draft ridesctx.designDraft)runSchemaAgentStreamschema builder 10 schema editing runTemplateAgentStreamtemplate assistant 8 Templates editor runElementAgentStreamelement agent 8 the admin element editor (Design System → Elements) runCodeAgentStreamcode agent 32 the admin's general assistant — Code Studio ( /store/:sid/code), the Design System and Welcome pages, every other console pageMAX_STEPSbounds 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 ONEupdateStoreDocumentscall, and a "what is missing" question as ONEcheckSitecall — the budget is the backstop, not the plan; a turn that still runs dry reportsstatus: 'truncated'and the admin offers Continue.The element agent edits one
ElementEntityopen in the admin element editor (see Elements below): its only write tool iswriteElement(merge-write of that one document), plus the store-read tools and areadContextreplay 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/fnservices (listServices/readService/writeService/callService/readServiceLogs/rollbackService, ruleserviceslinked in its RULES INDEX), and the full interactive design flow. Its system prompt is cache-stable and its per-turn context — the openfocusfile and thecontextslist (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 templateschemaId/templateVersionthat 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 optionalanthropic: {apiKey, workspaceId}(required whenctx.modelis a Claude id; the key is not workspace-scoped, so the client sendsanthropic-workspace-idon every request).All I/O goes through the caller-supplied
coreApi(path, body) => Promise<Response>seam — no Firebase/Express dependency (seesrc/core_api.tsmakeCoreApifor a standalone factory).src/mcp/— thedrop-core-mcpserver + stdio bin (see MCP server below).docs/AGENTS.md— guide for external AI agents (also served as thedrop-core://docs/agentsMCP resource — see the shipping note at the top).
Commands#
npm run build— build-rules + tsc →lib/(declarations included —typespoints atlib/index.d.ts).npm run dev— build-rules +tsc --watch(pair with a full relaunch ofnpm run devinadmin/to pick up changes — the dev middlewarerequire()slib/and Vite's restart keeps the require cache).npm run bundle— builds core-lib → sdk-js → this package, then esbuildindex.ts→dist/drop_core_generative.js(CJS;@google/genai+sharpexternal). This is the artifactadmin/'svendor:generative-sdkcopies.npm run bundle:mcp— same chain, but esbuildmcp-http.ts→dist/drop_core_mcp.cjs(CJS;@modelcontextprotocol/sdk,@google/genai,sharpexternal). This is the artifactfunctions/'svendor:mcpcopies.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-rootdocs/site/generated/mcp-tools.json(reads the builtlib/;buildMcpToolsis 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/instoreand prints the tool call order and per-step usage (the admin chat only rendersappliedchips, not the trace). Writes are blocked unless--write;--fail-readforces the template read to fail, exercising thewriteWebTemplateguard.
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.