You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Epic: NetScript AI Stack — first-class AI runtime, chat, and plugin seams
Status: architectural research complete (no implementation yet). This epic is the traceable basis from which we iterate into sub-issues → draft PRs → slices. Anchor:#219 (durable-CHAT primitive gap + streams gzip-mislabel bug). Research author: Fable 5 (principal-architect pass), delegating to Opus 4.8 / Sonnet 5 sub-agents. Grounded in the live eis-chat repo @ HEAD 26e1b65 (2026-07-02) and the NetScript reference plugins (auth / workers / streams / kv / sdk / fresh-ui).
Why this epic exists
eis-chat (the flagship consumer) hand-rolled a large "AI stack" — provider/model routing, agent loop, tool + MCP registry, embeddings/vision, durable-chat transport, and the whole presentational chat surface — because NetScript did not yet expose the seams for them. It also carries a live workaround for a real framework bug (#219). This epic captures the architecture for lifting that stack into NetScript better than the hand-rolled original, using the auth plugin / workers / kv patterns as the quality bar.
The plan below is Fable 5's. It is not a locked decision set — it's the strong, grounded basis we now iterate on together (rough brainstorm → sub-issues → draft PRs). Items still needing a human product call are tagged [PRODUCT SIGN-OFF].
The shape, in one paragraph
A five-home split rather than one monolithic plugins/ai: (1) a NEW @netscript/ai standalone engine core (peer to kv/sdk, provider adapters by subpath), because the AI engine is used in-process by Fresh routes, workers, and services — not service-bound like auth; (2) a NEW @netscript/fresh/ai subpath for the durable-chat client/SSR/proxy runtime; (3) extend the existing @netscript/fresh-uiai registry for the presentational surface; (4) a thin packages/plugin-ai-core holding only the optional gateway contract; (5) an optional plugins/ai centralized AI-gateway service. The #219 gzip fix ships first and standalone (~15 LOC).
The remainder of this issue is the research artifact as delivered. Sub-issues will be carved from §7 (migration slices) and §6 (open questions).
NetScript AI Stack — Architecture Plan (rev. 2)
Author: Principal framework architect (Fable 5). Architecture/design pass only, no implementation. Anchor: issue #219 (durable-CHAT primitive gap + gzip-mislabel bug). References: auth plugin (emulation target), workers (gold-standard plugin), @netscript/kv (standalone-core precedent), @netscript/fresh-uiai registry collection (already-shipping chat primitives). Scope basis: the LIVE eis-chat repo @ HEAD 26e1b65 (2026-07-02), incl. mcp-ui DS-token theming, the react-markdown pipeline, paced-reveal streaming, hybrid KB retrieval. Every decision below is mine, with rationale. Items needing eventual human product sign-off are tagged [PRODUCT SIGN-OFF]; I still give a recommendation.
0. The decisive re-architecture (what changed in rev. 2)
Rev. 1 put "everything in plugins/ai." That was wrong. The live-repo evidence forces three corrections:
eis-chat runs the agent loop IN-PROCESS, not through a plugin HTTP service.apps/dashboard/routes/api/chat.ts calls @tanstack/aichat({adapter, tools, mcp, agentLoopStrategy}) directly in the Fresh route, and workers jobs (workers/jobs/embed-document.ts) + the eischat oRPC service (services/eischat/src/embeddings.ts) call embeddings directly. This is fundamentally unlike auth, where the backend is only ever reached through the auth service. The AI engine primitives are runtime-agnostic library code, used in-process across Fresh routes / workers / services — not service-bound. ⇒ they belong in a standalone core package, not a plugin.
@netscript/fresh-ui already ships the presentational chat surface. Its registry.manifest.ts:1102-1116 defines a named ai collection (message, tool-call-card, code-block, chart-block, citation-chip, model-selector, prompt-input, command-palette, search, theme-seed), and eis-chat's apps/dashboard/components/ui/* ARE those copy-source components — extended locally. ⇒ presentational primitives belong in the fresh-ui registry (extend the existing collection), NOT in a plugin or a new package.
@netscript/kv/@netscript/sdk prove the standalone runtime-agnostic core-package pattern is idiomatic (packages/kv/deno.json, packages/sdk/deno.json — zero @netscript/* deps, adapter-registration-by-subpath-import at packages/kv/redis.ts:1-20). ⇒ the AI engine should be a peer core package @netscript/ai, with provider adapters as subpath-registered adapters (@netscript/ai/anthropic), NOT a proliferation of packages/ai-* packages.
presentational; the collection already exists — extend it
The oRPC contract for an optional centralized AI-gateway service
packages/plugin-ai-core (archetype 2, thin)
only needed to back the plugin; re-exports @netscript/ai types
The optional AI-gateway plugin service (centralized BYOK key custody, shared tool/MCP registry, cross-app usage accounting, rate limiting)
plugins/ai (archetype 5)
multi-app / centralized-key deployment topology; NOT required for the single-app embedded case
The plugin is now optional infrastructure, not the center of gravity. The center is @netscript/ai (engine) + @netscript/fresh/ai (chat runtime) + the fresh-ui ai registry (chat UI).
1. @netscript/ai — the standalone AI engine core (NEW package)
Archetype 2 (integration): wraps @tanstack/ai-* + provider SDKs behind ports + adapters. Modeled structurally on @netscript/kv (packages/kv/deno.json:6-11): a root port/lifecycle API plus subpath-registered adapters. Zero Fresh coupling — usable in Fresh routes, workers jobs, oRPC services, or standalone scripts, exactly as eis-chat uses @tanstack/ai today.
The @netscript/kv/redis self-registration pattern (registerKvAdapter, packages/kv/redis.ts:1-20, consumed by getKv() auto-detect at packages/kv/mod.ts:11-22) is copied verbatim: importing @netscript/ai/anthropic self-registers the provider so createAiRuntime() resolves it, and an app that never uses Anthropic never pays for @tanstack/ai-anthropic. This is the key reason adapters are subpaths not packages — AI provider SDKs are heavy optional deps, precisely kv's situation.
1.2 Ports (the AuthBackendPort analogue, cf. packages/plugin-auth-core/src/ports/mod.ts:211-241)
createAiRuntime(config) is the composition root (A10, docs/architecture/doctrine/07-composition-and-extension.md:14-81): constructor-injects the registered adapters, model registry, and telemetry; no module-load side effects except the opt-in adapter self-registration. Model registry = ModelEntry {provider, id, label, reasoning?} (from apps/dashboard/lib/models.ts:17-26), env-overridable via config, replacing eis-chat's EISCHAT_MODELS scraper (lib/models.ts:43-57). Reasoning normalization (Anthropic thinking:{type:'adaptive'} + output_config.effort vs OpenAI-compat reasoning:{effort} — apps/dashboard/routes/api/chat.ts:90-126) and token caps (ANSWER_MAX_TOKENS=8192/REASONING_MAX_TOKENS=16384, chat.ts:66-67) live in -core per the thinness law, hidden behind buildChat.
No provider SDK type ever leaks to userland — userland imports @netscript/ai + optionally @netscript/ai/anthropic (registration only). This is the auth invariant (adapters import only @netscript/plugin-auth-core/contracts/v1).
1.3 Why NOT fold this into packages/plugin-ai-core (the auth shape)?
Auth folds engine+contract into one -core because the auth backend is only invoked via the auth service. The AI engine is invoked in-process, independent of any service (three call sites in eis-chat prove it). Doctrine A9/A11: a package earns its own identity when its consumers are broader than one plugin. @netscript/ai has three non-plugin consumers already (Fresh routes, workers, services), so it is a peer core package, and plugin-ai-core shrinks to just the service contract that re-exports it. This is the single most important structural call in this plan.
2. @netscript/fresh/ai — the durable-chat client runtime (NEW subpath)
Archetype 4, sits at packages/fresh/src/runtime/ai/, sibling to runtime/streams/. It is the exact analogue of createNetScriptStreamDB (packages/fresh/src/runtime/streams/create-stream-db.ts:100-114): a Fresh-runtime factory that composes the streams URL/auth seam. Precedent that this dependency direction is allowed: @netscript/freshalready importsbuildStreamUrl/getStreamsAuth/getStreamsUrl from @netscript/plugin-streams-core (create-stream-db.ts:18). Added to packages/fresh/deno.json exports (currently ., ./server, ./streams, ./query, ... at :6-17; new dep @durable-streams/tanstack-ai-transport@^0.0.8 + @tanstack/ai-preact; @durable-streams/state already present).
2.1 The distinction that must be documented (root-cause fix for #219)
NetScript ships two stream consumers; they are NOT interchangeable. The @module JSDoc (F-5 mandatory) must lead with this table:
Verified transport API (@durable-streams/tanstack-ai-transport@0.0.8): durableStreamConnection({sendUrl,readUrl?,initialOffset?,...})→{subscribe,send}; toDurableChatSessionResponse({stream:Pick<DurableStreamTarget,'writeUrl'|'headers'|'createIfMissing'>, newMessages, responseStream, mode})→Promise<Response> (202 immediate / 200 await); ensureDurableChatSessionStream, materializeSnapshotFromDurableStream({readUrl,headers,offset?})→{messages,offset}. useChat({connection, initialMessages, live:true, forwardedProps}) (@tanstack/ai-preact@0.10.1); live:true holds the subscription open for the component lifetime (multi-tab + resume-in-progress). Offsets are opaque <read-seq>_<byte-offset>; the stream IS the log so a mid-generation reload catches in-flight tokens.
2.3 The proxy recipe (createChatStreamProxyHandler)
eis-chat hand-wrote three same-origin read proxies (routes/api/chat-stream.ts, routes/api/streams/[...path].ts, lib/stream-loaders.ts). Ship ONE that: maps ?id=→buildStreamUrl(chatStreamPath(id)), forwards State-Protocol params (offset/live/handle/cursor) except id (chat-stream.ts:54-56); attaches getStreamsAuth()server-side only (the security reason the proxy exists — keeps STREAMS_SECRET off the browser); strips hop-by-hop + content-encoding/content-length on the way back (streams/[...path].ts:24-31); bridges disconnect via cancel() not req.signal (chat-stream.ts:76-114). After the §3 bug fix it drops the Accept-Encoding: identity request header but keeps the response-header strip as defense-in-depth.
Root cause (empirically reproduced, Deno 2.9.0): decompress-but-don't-relabel at the streams proxy hop (plugins/streams/services/src/main.ts). The engine (@durable-streams/server@0.3.7) gzips correctly only for bodies ≥1024 B; Deno's inner fetch transparently decompresses but retains content-encoding: gzip; the proxy returns it verbatim (main.ts:70-85, return await fetch(proxyReq) at :81, even CORS-exposes the header at :110-123). Compliant decoders then try to gunzip [{... → throw. Size-gated ⇒ intermittent (why it looked random). The identity workaround only makes the engine skip compression.
Fix location: plugins/streams/services/src/main.tsproxyHandler — after the inner fetch, rebuild the Response stripping content-encoding/content-length/hop-by-hop (exactly eis-chat's own client-proxy STRIP set, routes/api/streams/[...path].ts:24-31). NOT engine compression-disable (discards real wire compression, leaves the latent Deno bug), NOT a client change. The engine and client are both correct; the proxy is the wrong actor.
Verification:curl -sD - --compressed '<front>/v1/stream/netscript/<path>' on a >1KB snapshot (pre-fix: header says gzip, body magic 5b 7b not 1f 8b; post-fix: no header, valid JSON). Add a >1KB read case to the scaffold.runtime E2E with a compliant decoder. Acceptance = deleting all three identity sites (chat-stream.ts:72, stream-loaders.ts:82, streams/[...path].ts:60). Ships FIRST, ~15 LOC, standalone — unblocks eis-chat before any AI package exists.
4. @netscript/fresh-uiai registry — the presentational surface (EXTEND existing)
These are copy-source components (netscript ui:add <item>) — an app owns the code after copying, which is exactly why eis-chat could extend them. Preact function components, class?:string merged via cn(), @layer 2, @depends theme-seed, themed only through semantic --ns-* CSS custom properties (packages/fresh-ui/registry/theme/tokens.css), never JS-side.
Gaps to add to the collection (all proven-needed by the live eis-chat build, currently hand-rolled there):
markdown primitive — the biggest gap. eis-chat added apps/dashboard/components/ui/markdown.tsx (react-markdown@9 under preact/compat, remark-gfm/math, rehype-katex/highlight/sanitize, a remarkCitations mdast plugin [n]→<ns-citation>markdown.tsx:53-82, mid-stream stripIncompleteSyntax:287-303). fresh-ui's message.tsx renderInline is inline-only (bold/code/citation); a real Markdown renderer belongs in the registry. Decision: add markdown as a registry item that emits the sanitize-hardened pipeline, with the citations plugin wired to the existing citation-chip.
mcp-ui-widget primitive — eis-chat's mcp-widget.tsx + mcp-sandbox.html.ts (the new HEAD feature) render ui:// resources in a themed sandboxed iframe. The DS-token theming (?theme= query → mcp-sandbox.html.ts:351-354, guest-token <style> injection + data-theme stamp :229-251, CSP header from ?csp=:356-376, MCPAppResource keyed by theme to force remount mcp-widget.tsx:144) is generic and belongs in the registry as a copy-source widget + a companion sandbox-route recipe. Decision: add mcp-ui-widget (the island component) + ship the sandbox-proxy as the @netscript/fresh/aicreateMcpSandboxHandler server helper (§2.2), since it needs the Fresh middleware + CSP-header machinery.
paced-reveal hooks — eis-chat's lib/paced-reveal.ts (useMinVisible/usePacedReveal, thinking-min-duration + token pacing) is a reusable streaming-UX primitive. Decision: add as a registry lib item (copy-source hook), not a runtime export, since it is presentational polish an app will want to tune.
fresh-ui is standalone (no @netscript/* runtime deps), so these additions carry no dependency-direction risk. The mcp-ui-widget + markdown do pull heavy npm deps (@mcp-ui/client, react-markdown + unified stack) — acceptable because copy-source items only add deps to the app that runs ui:add, never to the library's own published graph (F-6 safe).
5. packages/plugin-ai-core + plugins/ai — the OPTIONAL AI-gateway service
This exists for one topology: centralized BYOK key custody + shared tool/MCP registry + cross-app usage accounting + rate limiting, where multiple apps hit one AI gateway instead of each embedding provider keys. The single-app embedded case (eis-chat today) does NOT need it — it uses @netscript/ai + @netscript/fresh/ai directly. Building it emulates auth precisely:
packages/plugin-ai-core (archetype 2, thin): aiContract (oRPC) extends BasePluginContract with mandatory describe, spreads ...BASE_PLUGIN_CONTRACT_ROUTES (packages/plugin/src/contract-base/domain/base-contract.ts:76-122), merges oc.errors({...BASE_PLUGIN_ERRORS, ...AI_ERRORS}) — exactly auth (packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:405-429). Routes under /v1/ai: POST /chat (SSE agent-loop producer), GET /models, POST /tools/:name, POST /embed, POST /transcribe, GET /describe. Re-exports message/part types from @netscript/ai/contracts — never redefines.
plugins/ai (archetype 5): definePlugin('@netscript/plugin-ai', v).withType('api').withService({name:'ai-api', entrypoint, port}).withContractVersions([{version:'v1', loader:'./contracts/v1/mod.ts'}]).withRuntimeConfigTopics([{name:'ai'}])...build() (auth manifest shape, plugins/auth/src/public/mod.ts:23-57). Service = createPluginService(router, {name:'ai', context: () => ({runtime: getAiRuntime(), telemetry}), ...}) (the enforced builder chain, packages/plugin/src/service/presentation/create-plugin-service.ts:125-181; auth usage plugins/auth/services/src/main.ts:70-83). The service is a thin HTTP shell over @netscript/ai — it holds keys server-side and exposes the same ports over oRPC. contracts/v1/mod.ts = export * from '@netscript/plugin-ai-core/contracts/v1' (auth pattern). verify-plugin.ts asserts service + v1 contract + ai runtime-config topic (plugins/workers/verify-plugin.ts:21-96).
Optional background-processor contribution re-declaring embed/transcribe as plugin jobs (workers idiom defineJobHandler + Object.assign(handler,{id})) if the gateway owns ingestion; otherwise those stay app-side workers jobs.
6. Open questions — resolved (my recommendations)
Q1 — Token accounting. eis-chat uses a chars/4 estimate feeding /usage (apps/dashboard/routes/api/chat.ts:203) because it reads nothing from the stream. DECISION: AgentLoopPort.run() must emit a real usage metadata chunk extracted from the @tanstack/ai-* finish event (the adapters carry provider usage). chars/4 is a defect, not a design. [PRODUCT SIGN-OFF] only on whether usage is persisted/billed and at what granularity — the seam is my call and is required.
Q2 — outputSchema vs chart-fence. Not mutually exclusive. Fenced chart is a rendering affordance (fresh-ui chart-block already parses it, model-agnostic); TanStack outputSchema on useChat is a typed data contract. DECISION: support both — keep fenced blocks as the presentational convention in the fresh-ui registry, and expose outputSchema passthrough on createNetScriptChatConnection (§2.2) for apps wanting schema-validated structured output. No human decision needed.
Q3 — Embeddings/vision one adapter or two.DECISION: one subpath @netscript/ai/openai-embeddings covering both (both hit OpenAI-compatible endpoints; services/eischat/src/{embeddings,vision}.ts share openaiKey()), but two distinct ports (EmbeddingProviderPort, VisionProviderPort) so a non-OpenAI vision provider can register independently later (A11 — name the axis now, split the package only when a second impl appears). Also fixes eis-chat's inconsistency where vision is hardcoded gpt-4o-mini (services/eischat/src/vision.ts:9) while embeddings are configurable. My call.
Q4 — VectorStorePort in v1.DECISION: OUT of v1. Ship EmbeddingProviderPort (compute) only; leave vector storage/search to the app (eis-chat's tursodb vector32/vector_distance_cos hybrid retrieval in packages/channel/mod.ts is DB-specific). Coupling a core port to a concrete store is exactly the sagas/triggers engine-lock debt to avoid (user memory plugin-core-depends-on-primitives.md). [PRODUCT SIGN-OFF]: if a first-class KB becomes a framework goal, revisit as a separate @netscript/ai/vector subpath (pgvector/libsql/tursodb adapters) or a plugins/ai-kb — I recommend deferring until a second vector backend exists.
Q5 — Chat-route authz. eis-chat has zero authz on chat/stream/session routes (no ownership checks; @workos-inc/node import-mapped but unused). DECISION: the durable-chat server helper + proxy recipe expose an authorize(req, sessionId) hook (shown in §2.2), and the docs make the secure path the easy path — the proxy already holds the streams secret server-side, so session-ownership verification is a natural extension, composing @netscript/plugin-auth session middleware. The seam is my call (hook provided, secure-by-default docs). [PRODUCT SIGN-OFF]: whether authz is hard-required vs opt-out — some deployments are single-user/local — is a product choice; I recommend hook-provided + documented-required, not enforced-required.
Q6 — Promote thinness + base-contract/base-service laws to written doctrine. They currently live only in user memory + partial code (slice 172a-2), not in docs/architecture/doctrine/. DECISION/recommendation: YES, promote both to a doctrine chapter — a new stack that must be "checked against doctrine" needs them written and enforceable. [PRODUCT SIGN-OFF] — it is the user's doctrine to amend; I recommend it strongly and can draft the chapter.
7. Migration path for eis-chat (slices, smallest-first)
Ordering respects Archetype-5 discipline (contracts → runtime → verification → integration) and front-loads highest value / lowest cost.
Slice 1: @netscript/fresh/ai (subpath, not a package/plugin). Wrap the transport + reuse the streams seam; migrate eis-chat ChatPane.tsx/chat.ts/chat-stream.ts/stream-loaders.ts. Highest value, lowest structural cost.
Slice 2: @netscript/ai engine — ModelProviderPort + @netscript/ai/anthropic + @netscript/ai/openai-compatible, model registry, reasoning/token-cap normalization. Migrate eis-chat buildChat/models.ts/llm.ts; kill the .env scraper via @netscript/config. Then ./agent, ./tools, ./mcp, ./openai-embeddings; migrate the chat() call, KB tool, MCP pool, embeddings/vision.
Slice 3: fresh-ui ai registry additions — markdown, mcp-ui-widget (+ the @netscript/fresh/aicreateMcpSandboxHandler server helper), paced-reveal. Migrate eis-chat's markdown.tsx/mcp-widget.tsx/mcp-sandbox.html.ts/paced-reveal.ts back to ui:add items so the app re-derives from the registry.
Slice 4 (optional): packages/plugin-ai-core + plugins/ai — only if/when a centralized multi-app AI gateway is wanted. verify-plugin.ts, contract re-export, createPluginService shell over @netscript/ai.
Persistence/analytics stays app-owned (the durable stream is the transcript log; tursodb hybrid-KB + analytics are eis-chat domain data).
8. Doctrine conformance & quality bar
Placement vs doctrine: each home is justified by layering + thinness + the "consumers broader than one plugin ⇒ own package" rule (A9/A11). Dependency directions (all downward, acyclic): @netscript/ai → @std/* + @tanstack/ai-* only (peer to kv/sdk, zero @netscript/*); @netscript/fresh/ai → @netscript/plugin-streams-core + @netscript/ai/contracts (precedented — fresh already depends on plugin-streams-core, create-stream-db.ts:18); plugin-ai-core → @netscript/ai + @netscript/plugin; plugins/ai → plugin-ai-core + @netscript/ai; fresh-ui ai registry → standalone copy-source (no lib deps).
Fitness gates:
F-3 (layering):@netscript/ai uses role folders domain/ports/application/adapters; no up-imports; avoid forbidden folder names utils/helpers/common/lib; do not copy workers-core's empty application/ wart.
F-5 (public surface/JSDoc, "single most important"): every export gets @param/@returns/@example; @netscript/fresh/ai@module leads with the shapes-vs-sessions table (§2.1); split surfaces into subpaths (kv/auth reference, ≤20 exports per mod.ts).
F-6 (JSR publishability): explicit return types (root isolatedDeclarations:true), no export default in libs, no any in exported decls. Target publish WITHOUT --allow-slow-types (don't inherit the plugin-core T4 debt). Heavy provider/markdown/mcp deps stay off @netscript/ai's and fresh-ui's published graphs — providers are opt-in subpaths, UI is copy-source. Heed JSR memory traps: relative self-imports, assets via with {type:'text'}.
F-13 (runtime declarations):AgentLoopPort is a state machine with a terminal transition, threads AbortSignal, exposes stop(). Any plugins/ai embed/transcribe jobs use the workers .id-keyed registry code-gen discovery, not runtime FS scan; declaration types in plugin-ai-core/runtime.
verify-plugin.ts modeled on plugins/workers/verify-plugin.ts:21-96.
Quality bar: emulate workers (multi-axis manifest, extends BasePluginContract + ...BASE_PLUGIN_CONTRACT_ROUTES; createPluginService; denoJson.version single-source; registry code-gen; AspireNSPluginContribution). Emulate auth (contract-in-core re-exported by the plugin; base-contract extension; single-active named selection from env). Emulate kv (adapter-registration-by-subpath-import for the AI providers; runtime-agnostic core). AVOID sagas/triggers store/engine-lock coupling — no core port binds to a concrete store (drives Q4).
@durable-streams/tanstack-ai-transport + useChat API surface
Sonnet (high)
§2.2 verified signatures
Live eis-chat inventory diff @ HEAD 26e1b65 (mcp-ui theming, markdown pipeline, paced-reveal, new UI components)
Opus (high)
§0, §4, §6, §7
fresh-ui / fresh / kv / sdk placement survey
Sonnet (high)
§0, §1, §2, §4 (the whole placement re-architecture)
Two prior on-disk research files (research/eis-chat-ai-stack-map.md, research/netscript-reference-patterns.md) were read directly and corroborated.
Synthesis, the five-home placement re-architecture, all six resolved open questions, and this plan: Fable 5.
Sub-issue DAG (rev.4)
Anchor bug #239 (streams gzip-mislabel fix) ships first and standalone. Every AI-stack slice below was filed from the rev.4-E plan. Dependencies are resolved to real issue numbers; wave labels partition the release.
Widest starting front (no deps):#240 (E1), #249 (FA0), #253 (FB0), #263 (P6).
Cluster: ENGINE — @netscript/ai (new peer core, adapters by subpath)
Epic: NetScript AI Stack — first-class AI runtime, chat, and plugin seams
Why this epic exists
eis-chat (the flagship consumer) hand-rolled a large "AI stack" — provider/model routing, agent loop, tool + MCP registry, embeddings/vision, durable-chat transport, and the whole presentational chat surface — because NetScript did not yet expose the seams for them. It also carries a live workaround for a real framework bug (#219). This epic captures the architecture for lifting that stack into NetScript better than the hand-rolled original, using the auth plugin / workers / kv patterns as the quality bar.
The plan below is Fable 5's. It is not a locked decision set — it's the strong, grounded basis we now iterate on together (rough brainstorm → sub-issues → draft PRs). Items still needing a human product call are tagged
[PRODUCT SIGN-OFF].The shape, in one paragraph
A five-home split rather than one monolithic
plugins/ai: (1) a NEW@netscript/aistandalone engine core (peer tokv/sdk, provider adapters by subpath), because the AI engine is used in-process by Fresh routes, workers, and services — not service-bound like auth; (2) a NEW@netscript/fresh/aisubpath for the durable-chat client/SSR/proxy runtime; (3) extend the existing@netscript/fresh-uiairegistry for the presentational surface; (4) a thinpackages/plugin-ai-coreholding only the optional gateway contract; (5) an optionalplugins/aicentralized AI-gateway service. The #219 gzip fix ships first and standalone (~15 LOC).Traceability — sources, issues, PRs, external deps
Anchor & related issues
NetScript reference surfaces the plan is grounded in (emulation targets / precedents)
packages/plugin-auth-core/,plugins/auth/,packages/auth-better-auth|auth-kv-oauth|auth-workos/(contract-in-core + thin manifest + adapters).plugins/workers/(gold-standard multi-axis manifest,verify-plugin.ts, registry code-gen).plugins/streams/services/src/main.ts(proxy — the [AI-stack anchor] streams: no durable-CHAT integration — @netscript/fresh/ai seam (StreamDB shapes → Electric "Decoding failed") #219 fix site) +packages/plugin-streams-core/src/application/stream-url-resolver.ts(getStreamsUrl/getStreamsAuth/buildStreamUrl).packages/fresh/src/runtime/streams/create-stream-db.ts(createNetScriptStreamDB— the sibling the new./aisubpath mirrors).packages/fresh-ui/registry.manifest.ts:1102-1116(theaicollection already shipping — extend it).packages/kv/+packages/sdk/(adapter-registration-by-subpath-import; the@netscript/aishape).docs/architecture/doctrine/{01-thesis-and-axioms,06-archetypes,07-composition-and-extension,09-anti-patterns-and-fitness-functions}.md; harnessARCHETYPE-5-plugin.md; fitness fns F-3/F-5/F-6/F-13.External dependencies / prior art
@durable-streams/tanstack-ai-transport@^0.0.8—durableStreamConnection,toDurableChatSessionResponse,ensureDurableChatSessionStream,materializeSnapshotFromDurableStream(ElectricSQL "Durable Sessions").@tanstack/ai@0.10.x+-anthropic/-openai(/compatible) /-preact/-mcp— provider adapters +useChat+chat()agent loop.@durable-streams/{client,server,state}— the underlying durable-stream transport already used by NetScript streams.@modelcontextprotocol/sdk+@mcp-ui/client— MCP tool transport +ui://widget rendering.Live consumer reference (private)
26e1b65(2026-07-02) — key files:apps/dashboard/islands/ChatPane.tsx,routes/api/chat.ts,routes/api/chat-stream.ts,routes/api/streams/[...path].ts,lib/stream-loaders.ts,lib/chat-render.ts,components/ui/{message,markdown,mcp-widget,chart-block,code-block,tool-call-card}.tsx,lib/{models,paced-reveal,mcp}.ts,services/eischat/src/{embeddings,vision}.ts,packages/channel/mod.ts. The threeAccept-Encoding: identity[AI-stack anchor] streams: no durable-CHAT integration — @netscript/fresh/ai seam (StreamDB shapes → Electric "Decoding failed") #219 workaround sites (chat-stream.ts:72,streams/[...path].ts:60,stream-loaders.ts:82) are the Slice-0 acceptance test.Related tooling PR (context, not a dependency)
Architecture plan (Fable 5, rev. 2) — verbatim
The remainder of this issue is the research artifact as delivered. Sub-issues will be carved from §7 (migration slices) and §6 (open questions).
NetScript AI Stack — Architecture Plan (rev. 2)
Author: Principal framework architect (Fable 5). Architecture/design pass only, no implementation.
Anchor: issue #219 (durable-CHAT primitive gap + gzip-mislabel bug).
References: auth plugin (emulation target), workers (gold-standard plugin),
@netscript/kv(standalone-core precedent),@netscript/fresh-uiairegistry collection (already-shipping chat primitives).Scope basis: the LIVE eis-chat repo @ HEAD
26e1b65(2026-07-02), incl. mcp-ui DS-token theming, the react-markdown pipeline, paced-reveal streaming, hybrid KB retrieval.Every decision below is mine, with rationale. Items needing eventual human product sign-off are tagged [PRODUCT SIGN-OFF]; I still give a recommendation.
0. The decisive re-architecture (what changed in rev. 2)
Rev. 1 put "everything in
plugins/ai." That was wrong. The live-repo evidence forces three corrections:eis-chat runs the agent loop IN-PROCESS, not through a plugin HTTP service.
apps/dashboard/routes/api/chat.tscalls@tanstack/aichat({adapter, tools, mcp, agentLoopStrategy})directly in the Fresh route, and workers jobs (workers/jobs/embed-document.ts) + the eischat oRPC service (services/eischat/src/embeddings.ts) call embeddings directly. This is fundamentally unlike auth, where the backend is only ever reached through the auth service. The AI engine primitives are runtime-agnostic library code, used in-process across Fresh routes / workers / services — not service-bound. ⇒ they belong in a standalone core package, not a plugin.@netscript/fresh-uialready ships the presentational chat surface. Itsregistry.manifest.ts:1102-1116defines a namedaicollection (message,tool-call-card,code-block,chart-block,citation-chip,model-selector,prompt-input,command-palette,search,theme-seed), and eis-chat'sapps/dashboard/components/ui/*ARE those copy-source components — extended locally. ⇒ presentational primitives belong in the fresh-ui registry (extend the existing collection), NOT in a plugin or a new package.@netscript/kv/@netscript/sdkprove the standalone runtime-agnostic core-package pattern is idiomatic (packages/kv/deno.json,packages/sdk/deno.json— zero@netscript/*deps, adapter-registration-by-subpath-import atpackages/kv/redis.ts:1-20). ⇒ the AI engine should be a peer core package@netscript/ai, with provider adapters as subpath-registered adapters (@netscript/ai/anthropic), NOT a proliferation ofpackages/ai-*packages.Net topology (five homes, each justified in §3):
@netscript/ai(standalone core, archetype 2)@netscript/kv/api/chat-streamproxy recipe@netscript/freshnew./aisubpath (archetype 4)./streamsseam@netscript/fresh-uiairegistry (archetype 4, copy-source)packages/plugin-ai-core(archetype 2, thin)@netscript/aitypesplugins/ai(archetype 5)The plugin is now optional infrastructure, not the center of gravity. The center is
@netscript/ai(engine) +@netscript/fresh/ai(chat runtime) + the fresh-uiairegistry (chat UI).1.
@netscript/ai— the standalone AI engine core (NEW package)Archetype 2 (integration): wraps
@tanstack/ai-*+ provider SDKs behind ports + adapters. Modeled structurally on@netscript/kv(packages/kv/deno.json:6-11): a root port/lifecycle API plus subpath-registered adapters. Zero Fresh coupling — usable in Fresh routes, workers jobs, oRPC services, or standalone scripts, exactly as eis-chat uses@tanstack/aitoday.1.1 Exports map (kv-shaped, adapter-by-subpath)
The
@netscript/kv/redisself-registration pattern (registerKvAdapter,packages/kv/redis.ts:1-20, consumed bygetKv()auto-detect atpackages/kv/mod.ts:11-22) is copied verbatim: importing@netscript/ai/anthropicself-registers the provider socreateAiRuntime()resolves it, and an app that never uses Anthropic never pays for@tanstack/ai-anthropic. This is the key reason adapters are subpaths not packages — AI provider SDKs are heavy optional deps, precisely kv's situation.1.2 Ports (the
AuthBackendPortanalogue, cf.packages/plugin-auth-core/src/ports/mod.ts:211-241)createAiRuntime(config)is the composition root (A10,docs/architecture/doctrine/07-composition-and-extension.md:14-81): constructor-injects the registered adapters, model registry, and telemetry; no module-load side effects except the opt-in adapter self-registration. Model registry =ModelEntry {provider, id, label, reasoning?}(fromapps/dashboard/lib/models.ts:17-26), env-overridable via config, replacing eis-chat'sEISCHAT_MODELSscraper (lib/models.ts:43-57). Reasoning normalization (Anthropicthinking:{type:'adaptive'}+output_config.effortvs OpenAI-compatreasoning:{effort}—apps/dashboard/routes/api/chat.ts:90-126) and token caps (ANSWER_MAX_TOKENS=8192/REASONING_MAX_TOKENS=16384,chat.ts:66-67) live in-coreper the thinness law, hidden behindbuildChat.No provider SDK type ever leaks to userland — userland imports
@netscript/ai+ optionally@netscript/ai/anthropic(registration only). This is the auth invariant (adapters import only@netscript/plugin-auth-core/contracts/v1).1.3 Why NOT fold this into
packages/plugin-ai-core(the auth shape)?Auth folds engine+contract into one
-corebecause the auth backend is only invoked via the auth service. The AI engine is invoked in-process, independent of any service (three call sites in eis-chat prove it). Doctrine A9/A11: a package earns its own identity when its consumers are broader than one plugin.@netscript/aihas three non-plugin consumers already (Fresh routes, workers, services), so it is a peer core package, andplugin-ai-coreshrinks to just the service contract that re-exports it. This is the single most important structural call in this plan.2.
@netscript/fresh/ai— the durable-chat client runtime (NEW subpath)Archetype 4, sits at
packages/fresh/src/runtime/ai/, sibling toruntime/streams/. It is the exact analogue ofcreateNetScriptStreamDB(packages/fresh/src/runtime/streams/create-stream-db.ts:100-114): a Fresh-runtime factory that composes the streams URL/auth seam. Precedent that this dependency direction is allowed:@netscript/freshalready importsbuildStreamUrl/getStreamsAuth/getStreamsUrlfrom@netscript/plugin-streams-core(create-stream-db.ts:18). Added topackages/fresh/deno.jsonexports (currently.,./server,./streams,./query, ... at:6-17; new dep@durable-streams/tanstack-ai-transport@^0.0.8+@tanstack/ai-preact;@durable-streams/statealready present).2.1 The distinction that must be documented (root-cause fix for #219)
NetScript ships two stream consumers; they are NOT interchangeable. The
@moduleJSDoc (F-5 mandatory) must lead with this table:createNetScriptStreamDB+useLiveQuery(create-stream-db.ts:100-114)@netscript/fresh/ai@durable-streams/state/db@durable-streams/tanstack-ai-transport+@tanstack/ai-preact{data, status, error}{messages, sendMessage, isLoading, stop}+ optimistic + resume + multi-tabTypeError: Decoding failed(ElectricTextDecoderon gzip'd multibyte)2.2 Surface
Verified transport API (
@durable-streams/tanstack-ai-transport@0.0.8):durableStreamConnection({sendUrl,readUrl?,initialOffset?,...})→{subscribe,send};toDurableChatSessionResponse({stream:Pick<DurableStreamTarget,'writeUrl'|'headers'|'createIfMissing'>, newMessages, responseStream, mode})→Promise<Response>(202 immediate / 200 await);ensureDurableChatSessionStream,materializeSnapshotFromDurableStream({readUrl,headers,offset?})→{messages,offset}.useChat({connection, initialMessages, live:true, forwardedProps})(@tanstack/ai-preact@0.10.1);live:trueholds the subscription open for the component lifetime (multi-tab + resume-in-progress). Offsets are opaque<read-seq>_<byte-offset>; the stream IS the log so a mid-generation reload catches in-flight tokens.2.3 The proxy recipe (
createChatStreamProxyHandler)eis-chat hand-wrote three same-origin read proxies (
routes/api/chat-stream.ts,routes/api/streams/[...path].ts,lib/stream-loaders.ts). Ship ONE that: maps?id=→buildStreamUrl(chatStreamPath(id)), forwards State-Protocol params (offset/live/handle/cursor) exceptid(chat-stream.ts:54-56); attachesgetStreamsAuth()server-side only (the security reason the proxy exists — keepsSTREAMS_SECREToff the browser); strips hop-by-hop +content-encoding/content-lengthon the way back (streams/[...path].ts:24-31); bridges disconnect viacancel()notreq.signal(chat-stream.ts:76-114). After the §3 bug fix it drops theAccept-Encoding: identityrequest header but keeps the response-header strip as defense-in-depth.3. #219 gzip-mislabel bug — root cause, fix, verification
Root cause (empirically reproduced, Deno 2.9.0): decompress-but-don't-relabel at the streams proxy hop (
plugins/streams/services/src/main.ts). The engine (@durable-streams/server@0.3.7) gzips correctly only for bodies ≥1024 B; Deno's innerfetchtransparently decompresses but retainscontent-encoding: gzip; the proxy returns it verbatim (main.ts:70-85,return await fetch(proxyReq)at:81, even CORS-exposes the header at:110-123). Compliant decoders then try to gunzip[{...→ throw. Size-gated ⇒ intermittent (why it looked random). Theidentityworkaround only makes the engine skip compression.Fix location:
plugins/streams/services/src/main.tsproxyHandler— after the inner fetch, rebuild the Response strippingcontent-encoding/content-length/hop-by-hop (exactly eis-chat's own client-proxySTRIPset,routes/api/streams/[...path].ts:24-31). NOT engine compression-disable (discards real wire compression, leaves the latent Deno bug), NOT a client change. The engine and client are both correct; the proxy is the wrong actor.Verification:
curl -sD - --compressed '<front>/v1/stream/netscript/<path>'on a >1KB snapshot (pre-fix: header says gzip, body magic5b 7bnot1f 8b; post-fix: no header, valid JSON). Add a >1KB read case to thescaffold.runtimeE2E with a compliant decoder. Acceptance = deleting all threeidentitysites (chat-stream.ts:72,stream-loaders.ts:82,streams/[...path].ts:60). Ships FIRST, ~15 LOC, standalone — unblocks eis-chat before any AI package exists.4.
@netscript/fresh-uiairegistry — the presentational surface (EXTEND existing)These are copy-source components (
netscript ui:add <item>) — an app owns the code after copying, which is exactly why eis-chat could extend them. Preact function components,class?:stringmerged viacn(),@layer 2,@depends theme-seed, themed only through semantic--ns-*CSS custom properties (packages/fresh-ui/registry/theme/tokens.css), never JS-side.Already shipping (
packages/fresh-ui/registry.manifest.ts:1102-1116+registry/components/ui/):message.tsx(+TypingIndicator,renderInline,packages/fresh-ui/registry/components/ui/message.tsx:100-108),tool-call-card.tsx,code-block.tsx,chart-block.tsx,citation-chip.tsx,model-selector.tsx,prompt-input.tsx.Gaps to add to the collection (all proven-needed by the live eis-chat build, currently hand-rolled there):
markdownprimitive — the biggest gap. eis-chat addedapps/dashboard/components/ui/markdown.tsx(react-markdown@9 underpreact/compat, remark-gfm/math, rehype-katex/highlight/sanitize, aremarkCitationsmdast plugin[n]→<ns-citation>markdown.tsx:53-82, mid-streamstripIncompleteSyntax:287-303). fresh-ui'smessage.tsx renderInlineis inline-only (bold/code/citation); a real Markdown renderer belongs in the registry. Decision: addmarkdownas a registry item that emits the sanitize-hardened pipeline, with the citations plugin wired to the existingcitation-chip.mcp-ui-widgetprimitive — eis-chat'smcp-widget.tsx+mcp-sandbox.html.ts(the new HEAD feature) renderui://resources in a themed sandboxed iframe. The DS-token theming (?theme=query →mcp-sandbox.html.ts:351-354, guest-token<style>injection +data-themestamp:229-251, CSP header from?csp=:356-376,MCPAppResourcekeyed by theme to force remountmcp-widget.tsx:144) is generic and belongs in the registry as a copy-source widget + a companion sandbox-route recipe. Decision: addmcp-ui-widget(the island component) + ship the sandbox-proxy as the@netscript/fresh/aicreateMcpSandboxHandlerserver helper (§2.2), since it needs the Fresh middleware + CSP-header machinery.paced-revealhooks — eis-chat'slib/paced-reveal.ts(useMinVisible/usePacedReveal, thinking-min-duration + token pacing) is a reusable streaming-UX primitive. Decision: add as a registry lib item (copy-source hook), not a runtime export, since it is presentational polish an app will want to tune.fresh-ui is standalone (no
@netscript/*runtime deps), so these additions carry no dependency-direction risk. Themcp-ui-widget+markdowndo pull heavy npm deps (@mcp-ui/client,react-markdown+ unified stack) — acceptable because copy-source items only add deps to the app that runsui:add, never to the library's own published graph (F-6 safe).5.
packages/plugin-ai-core+plugins/ai— the OPTIONAL AI-gateway serviceThis exists for one topology: centralized BYOK key custody + shared tool/MCP registry + cross-app usage accounting + rate limiting, where multiple apps hit one AI gateway instead of each embedding provider keys. The single-app embedded case (eis-chat today) does NOT need it — it uses
@netscript/ai+@netscript/fresh/aidirectly. Building it emulates auth precisely:packages/plugin-ai-core(archetype 2, thin):aiContract(oRPC) extendsBasePluginContractwith mandatorydescribe, spreads...BASE_PLUGIN_CONTRACT_ROUTES(packages/plugin/src/contract-base/domain/base-contract.ts:76-122), mergesoc.errors({...BASE_PLUGIN_ERRORS, ...AI_ERRORS})— exactly auth (packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:405-429). Routes under/v1/ai:POST /chat(SSE agent-loop producer),GET /models,POST /tools/:name,POST /embed,POST /transcribe,GET /describe. Re-exports message/part types from@netscript/ai/contracts— never redefines.plugins/ai(archetype 5):definePlugin('@netscript/plugin-ai', v).withType('api').withService({name:'ai-api', entrypoint, port}).withContractVersions([{version:'v1', loader:'./contracts/v1/mod.ts'}]).withRuntimeConfigTopics([{name:'ai'}])...build()(auth manifest shape,plugins/auth/src/public/mod.ts:23-57). Service =createPluginService(router, {name:'ai', context: () => ({runtime: getAiRuntime(), telemetry}), ...})(the enforced builder chain,packages/plugin/src/service/presentation/create-plugin-service.ts:125-181; auth usageplugins/auth/services/src/main.ts:70-83). The service is a thin HTTP shell over@netscript/ai— it holds keys server-side and exposes the same ports over oRPC.contracts/v1/mod.ts=export * from '@netscript/plugin-ai-core/contracts/v1'(auth pattern).verify-plugin.tsasserts service + v1 contract +airuntime-config topic (plugins/workers/verify-plugin.ts:21-96).Optional
background-processorcontribution re-declaring embed/transcribe as plugin jobs (workers idiomdefineJobHandler+Object.assign(handler,{id})) if the gateway owns ingestion; otherwise those stay app-side workers jobs.6. Open questions — resolved (my recommendations)
Q1 — Token accounting. eis-chat uses a chars/4 estimate feeding
/usage(apps/dashboard/routes/api/chat.ts:203) because it reads nothing from the stream. DECISION:AgentLoopPort.run()must emit a realusagemetadata chunk extracted from the@tanstack/ai-*finish event (the adapters carry provider usage). chars/4 is a defect, not a design. [PRODUCT SIGN-OFF] only on whether usage is persisted/billed and at what granularity — the seam is my call and is required.Q2 —
outputSchemavs chart-fence. Not mutually exclusive. Fencedchartis a rendering affordance (fresh-uichart-blockalready parses it, model-agnostic); TanStackoutputSchemaonuseChatis a typed data contract. DECISION: support both — keep fenced blocks as the presentational convention in the fresh-ui registry, and exposeoutputSchemapassthrough oncreateNetScriptChatConnection(§2.2) for apps wanting schema-validated structured output. No human decision needed.Q3 — Embeddings/vision one adapter or two. DECISION: one subpath
@netscript/ai/openai-embeddingscovering both (both hit OpenAI-compatible endpoints;services/eischat/src/{embeddings,vision}.tsshareopenaiKey()), but two distinct ports (EmbeddingProviderPort,VisionProviderPort) so a non-OpenAI vision provider can register independently later (A11 — name the axis now, split the package only when a second impl appears). Also fixes eis-chat's inconsistency where vision is hardcodedgpt-4o-mini(services/eischat/src/vision.ts:9) while embeddings are configurable. My call.Q4 —
VectorStorePortin v1. DECISION: OUT of v1. ShipEmbeddingProviderPort(compute) only; leave vector storage/search to the app (eis-chat's tursodbvector32/vector_distance_coshybrid retrieval inpackages/channel/mod.tsis DB-specific). Coupling a core port to a concrete store is exactly the sagas/triggers engine-lock debt to avoid (user memoryplugin-core-depends-on-primitives.md). [PRODUCT SIGN-OFF]: if a first-class KB becomes a framework goal, revisit as a separate@netscript/ai/vectorsubpath (pgvector/libsql/tursodb adapters) or aplugins/ai-kb— I recommend deferring until a second vector backend exists.Q5 — Chat-route authz. eis-chat has zero authz on chat/stream/session routes (no ownership checks;
@workos-inc/nodeimport-mapped but unused). DECISION: the durable-chat server helper + proxy recipe expose anauthorize(req, sessionId)hook (shown in §2.2), and the docs make the secure path the easy path — the proxy already holds the streams secret server-side, so session-ownership verification is a natural extension, composing@netscript/plugin-authsession middleware. The seam is my call (hook provided, secure-by-default docs). [PRODUCT SIGN-OFF]: whether authz is hard-required vs opt-out — some deployments are single-user/local — is a product choice; I recommend hook-provided + documented-required, not enforced-required.Q6 — Promote thinness + base-contract/base-service laws to written doctrine. They currently live only in user memory + partial code (slice 172a-2), not in
docs/architecture/doctrine/. DECISION/recommendation: YES, promote both to a doctrine chapter — a new stack that must be "checked against doctrine" needs them written and enforceable. [PRODUCT SIGN-OFF] — it is the user's doctrine to amend; I recommend it strongly and can draft the chapter.7. Migration path for eis-chat (slices, smallest-first)
Ordering respects Archetype-5 discipline (contracts → runtime → verification → integration) and front-loads highest value / lowest cost.
plugins/streams/services/src/main.ts+ regress test; delete the threeidentityworkarounds. Proves the framework can own a streams-hop concern. No new packages.@netscript/fresh/ai(subpath, not a package/plugin). Wrap the transport + reuse the streams seam; migrate eis-chatChatPane.tsx/chat.ts/chat-stream.ts/stream-loaders.ts. Highest value, lowest structural cost.@netscript/aiengine —ModelProviderPort+@netscript/ai/anthropic+@netscript/ai/openai-compatible, model registry, reasoning/token-cap normalization. Migrate eis-chatbuildChat/models.ts/llm.ts; kill the.envscraper via@netscript/config. Then./agent,./tools,./mcp,./openai-embeddings; migrate thechat()call, KB tool, MCP pool, embeddings/vision.airegistry additions —markdown,mcp-ui-widget(+ the@netscript/fresh/aicreateMcpSandboxHandlerserver helper),paced-reveal. Migrate eis-chat'smarkdown.tsx/mcp-widget.tsx/mcp-sandbox.html.ts/paced-reveal.tsback toui:additems so the app re-derives from the registry.packages/plugin-ai-core+plugins/ai— only if/when a centralized multi-app AI gateway is wanted.verify-plugin.ts, contract re-export,createPluginServiceshell over@netscript/ai.plugin add ai+ui:addbundle recipe (auth adapter pattern).Persistence/analytics stays app-owned (the durable stream is the transcript log; tursodb hybrid-KB + analytics are eis-chat domain data).
8. Doctrine conformance & quality bar
Placement vs doctrine: each home is justified by layering + thinness + the "consumers broader than one plugin ⇒ own package" rule (A9/A11). Dependency directions (all downward, acyclic):
@netscript/ai→@std/*+@tanstack/ai-*only (peer to kv/sdk, zero@netscript/*);@netscript/fresh/ai→@netscript/plugin-streams-core+@netscript/ai/contracts(precedented — fresh already depends on plugin-streams-core,create-stream-db.ts:18);plugin-ai-core→@netscript/ai+@netscript/plugin;plugins/ai→plugin-ai-core+@netscript/ai; fresh-uiairegistry → standalone copy-source (no lib deps).Fitness gates:
@netscript/aiuses role foldersdomain/ports/application/adapters; no up-imports; avoid forbidden folder namesutils/helpers/common/lib; do not copy workers-core's emptyapplication/wart.@param/@returns/@example;@netscript/fresh/ai@moduleleads with the shapes-vs-sessions table (§2.1); split surfaces into subpaths (kv/auth reference, ≤20 exports per mod.ts).isolatedDeclarations:true), noexport defaultin libs, noanyin exported decls. Target publish WITHOUT--allow-slow-types(don't inherit the plugin-core T4 debt). Heavy provider/markdown/mcp deps stay off@netscript/ai's and fresh-ui's published graphs — providers are opt-in subpaths, UI is copy-source. Heed JSR memory traps: relative self-imports, assets viawith {type:'text'}.AgentLoopPortis a state machine with a terminal transition, threadsAbortSignal, exposesstop(). Anyplugins/aiembed/transcribe jobs use the workers.id-keyed registry code-gen discovery, not runtime FS scan; declaration types inplugin-ai-core/runtime.verify-plugin.tsmodeled onplugins/workers/verify-plugin.ts:21-96.Quality bar: emulate workers (multi-axis manifest,
extends BasePluginContract+...BASE_PLUGIN_CONTRACT_ROUTES;createPluginService;denoJson.versionsingle-source; registry code-gen;AspireNSPluginContribution). Emulate auth (contract-in-core re-exported by the plugin; base-contract extension; single-active named selection from env). Emulate kv (adapter-registration-by-subpath-import for the AI providers; runtime-agnostic core). AVOID sagas/triggers store/engine-lock coupling — no core port binds to a concrete store (drives Q4).Delegation record
@durable-streams/tanstack-ai-transport+useChatAPI surfaceTwo prior on-disk research files (
research/eis-chat-ai-stack-map.md,research/netscript-reference-patterns.md) were read directly and corroborated.Synthesis, the five-home placement re-architecture, all six resolved open questions, and this plan: Fable 5.
Sub-issue DAG (rev.4)
Anchor bug #239 (streams gzip-mislabel fix) ships first and standalone. Every AI-stack slice below was filed from the rev.4-E plan. Dependencies are resolved to real issue numbers;
wavelabels partition the release.Widest starting front (no deps): #240 (E1), #249 (FA0), #253 (FB0), #263 (P6).
Cluster: ENGINE —
@netscript/ai(new peer core, adapters by subpath)wave:v1-min· blocked by fix(streams): strip content-encoding on proxy hop — durable-stream gzip-mislabel (#219 pt.A / epic #238 slice 0) #239wave:v1-min· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240wave:v1-min· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240wave:v1-min· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240wave:v1· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240, [AI-stack E4] @netscript/ai: tool system (defineAiTool, createToolRegistry, render_ui built-in) #243wave:v1· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240wave:v1· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240wave:v1· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240wave:defer· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240, [AI-stack E3] @netscript/ai: createAgentLoop() state machine (maxIterations, history truncation, real usage) #242Cluster:
@netscript/fresh/ai(durable-chat client subpath)wave:v1-min· blocked by fix(streams): strip content-encoding on proxy hop — durable-stream gzip-mislabel (#219 pt.A / epic #238 slice 0) #239wave:v1-min· deps [AI-stack FA0] @netscript/fresh/ai subpath skeleton + transport dep wiring #249, fix(streams): strip content-encoding on proxy hop — durable-stream gzip-mislabel (#219 pt.A / epic #238 slice 0) #239, [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240wave:v1-min· deps [AI-stack FA0] @netscript/fresh/ai subpath skeleton + transport dep wiring #249, [AI-stack FA1] @netscript/fresh/ai: createNetScriptChatConnection + toNetScriptChatResponse + resolveChatSnapshot #250, fix(streams): strip content-encoding on proxy hop — durable-stream gzip-mislabel (#219 pt.A / epic #238 slice 0) #239wave:v1· deps [AI-stack FA0] @netscript/fresh/ai subpath skeleton + transport dep wiring #249Cluster: fresh-ui
airegistryaicollection + chat primitives (PREREQUISITE) ·wave:v1-min· no depswave:v1-min· deps [AI-stack FB0] fresh-ui: register the ai collection + chat primitives as manifest items (PREREQUISITE) #253wave:v1-min· deps [AI-stack FB0] fresh-ui: register the ai collection + chat primitives as manifest items (PREREQUISITE) #253, [AI-stack FA1] @netscript/fresh/ai: createNetScriptChatConnection + toNetScriptChatResponse + resolveChatSnapshot #250wave:v1· deps [AI-stack FB0] fresh-ui: register the ai collection + chat primitives as manifest items (PREREQUISITE) #253, [AI-stack FA3] @netscript/fresh/ai: createMcpSandboxHandler (DS-token injection + themed sandbox + CSP) #252wave:defer· deps [AI-stack FB0] fresh-ui: register the ai collection + chat primitives as manifest items (PREREQUISITE) #253wave:defer· deps [AI-stack FB0] fresh-ui: register the ai collection + chat primitives as manifest items (PREREQUISITE) #253, [AI-stack E4] @netscript/ai: tool system (defineAiTool, createToolRegistry, render_ui built-in) #243Cluster:
plugin-aiaccelerator + doctrine@netscript/plugin-ai-corethin contract ·wave:v1-min· deps [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240@netscript/plugin-aimanifest + scaffold + in-process emitters ·wave:v1-min· deps [AI-stack P1] @netscript/plugin-ai-core thin contract package #259, [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240, [AI-stack E2] @netscript/ai: anthropic + openai-compatible providers via self-registering subpaths #241, [AI-stack E3] @netscript/ai: createAgentLoop() state machine (maxIterations, history truncation, real usage) #242, [AI-stack E4] @netscript/ai: tool system (defineAiTool, createToolRegistry, render_ui built-in) #243, [AI-stack E5] @netscript/ai: McpTransportPort (stdio + reconnectable Streamable-HTTP + auth modes) #244, [AI-stack FA0] @netscript/fresh/ai subpath skeleton + transport dep wiring #249, [AI-stack FA1] @netscript/fresh/ai: createNetScriptChatConnection + toNetScriptChatResponse + resolveChatSnapshot #250, [AI-stack FA2] @netscript/fresh/ai: createChatStreamProxyHandler (single impl replacing 3 hand-rolled proxies) #251, [AI-stack FB0] fresh-ui: register the ai collection + chat primitives as manifest items (PREREQUISITE) #253airuntime-registry codegen fornetscript generate·wave:v1-min· deps [AI-stack P2] @netscript/plugin-ai manifest + connector + scaffold + in-process emitters #260wave:v1-min· no deps--gatewaycentralized service ·wave:defer· deps [AI-stack P1] @netscript/plugin-ai-core thin contract package #259, [AI-stack P2] @netscript/plugin-ai manifest + connector + scaffold + in-process emitters #260, [AI-stack E1] Scaffold @netscript/ai archetype-2 core (ports, model registry, createAiRuntime) #240Release waves