What Is a Bundle?

A Bundle is your application package. It combines Python backend code with optional TypeScript UI widgets and views into a single deployable unit. The package becomes a bundle by subclassing the platform's base class (BaseEntrypoint or a derived variant) and registering with @agentic_workflow.

App sources live in git. The platform's assembly descriptor points to a git repo, ref, and subdirectory. On deploy, the CLI or CI pipeline pulls the ref and registers the bundle. You can update a running deployment by pushing a new ref and re-running the deploy step — no service restart required.

Most example bundles use the ReAct agent (ReactStateV2), but a bundle does not have to. You can use any LangGraph graph, a simple request→response pipeline, or no LLM at all.

Example Bundles

Start from the examples folder in sdk/examples/bundles/. The concrete reference bundle used on this page is versatile@2026-03-31-13-36 — a production-style sample that combines ReAct orchestration, bundle-local tools, widget decorators, REST operations, and a custom main UI.

BundleDescription
versatile@2026-03-31-13-36 Reference bundle for this page. Demonstrates ReAct orchestration, bundle-local and MCP tools, decorators for widgets and APIs, economics-aware entrypoint patterns, a custom main view in ui-src/, and integrations operations that a real UI can call.

Platform Architecture for Bundle Developers

See also: Platform Overview

KDCube is a multi-tenant AI application platform. It provides the infrastructure for deploying conversational AI agents at scale — with streaming, storage, economics, tooling, and extensibility all built in.

A Bundle is your unit of deployment. It's a Python package that plugs into the platform and defines how your AI agent thinks, what tools it uses, how it stores data, what UI widgets it exposes, and how it bills users.

💡
Think of a Bundle as a self-contained AI product — it can include its own agent logic, knowledge base, custom tools, UI dashboards, billing configuration, and more. The platform provides everything else (auth, routing, scaling, storage backends, rate limiting).

Big Picture: How It All Fits Together

Big picture platform overview diagram
Big Picture: How It All Fits Together Complete platform overview showing all components from client through ingress, processor, bundles, storage, and integrations User Browser / App SSE + REST SSE/WS REST /bundles/{t}/{p}/{b}/operations/{op} Ingress Auth & Rate Limit Bundle Routing SSE Emitter Task Enqueueing enqueue Redis Queue Task Buffer dequeue Processor (proc) Bundle Loader LangGraph Exec Communicator (async) Operations API (REST) GET/POST /bundles/{t}/{p}/{b}/operations/{op} invoke chat·steer·ops YOUR BUNDLE Entrypoint Workflow Tools Skills ReAct Agent Widgets / UI Storage · Config · Economics Communicator Firewall Settings SSE stream Communicator (async streaming → client) PostgreSQL Conversations Redis Props / Cache S3 / Local FS Artifacts / Files Platform-managed persistence layer

Two Services, One Platform

Ingress (chat-ingress)

Handles all inbound traffic. Authenticates users, enforces rate limits, validates the bundle, enqueues the task, and opens an SSE stream back to the client. Your bundle rarely needs to know about this — the platform wires it up automatically.

Processor (chat-proc)

Dequeues tasks, loads your bundle singleton, and calls execute_core(). It also hosts the Operations API — the REST endpoint that your UI widgets call directly (no SSE needed for widget interactions).

Bundle Anatomy

A bundle is a Python package with a required entrypoint.py and optional supporting files:

Bundle anatomy file structure diagram
Bundle Anatomy File structure of a KDCube bundle package showing entrypoint, tools, skills, and config files my-bundle@1-0/ entrypoint.py REQUIRED — @agentic_workflow orchestrator/workflow.py BaseWorkflow subclass agents/gate.py Optional: custom intent gate (demo pattern) tools/my_tools.py Custom tool implementations tools_descriptor.py Register which tools are enabled skills/my_skill/SKILL.md Agent instruction sets skills_descriptor.py Which skills are visible to which agents knowledge/resolver.py Optional: docs/code knowledge space resources.py User-facing error messages event_filter.py Optional: filter/transform events

Minimal Entrypoint

from kdcube_ai_app.infra.plugin.agentic_loader import agentic_workflow
from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.entrypoint import BaseEntrypoint
from langgraph.graph import StateGraph, START, END
from typing import Dict, Any

BUNDLE_ID = "my-bundle"

@agentic_workflow(bundle_id=BUNDLE_ID)
class MyBundle(BaseEntrypoint):

    @property
    def configuration(self) -> Dict[str, Any]:
        return {
            "role_models": {
                "solver.react.v2.decision.v2.strong": {
                    "provider": "anthropic",
                    "model": "claude-sonnet-4-6"
                }
            }
        }

    def on_bundle_load(self, **kwargs):
        # Called ONCE per process when bundle first loads.
        self.logger.info("Bundle loaded!")

    async def execute_core(self, state, thread_id, params):
        graph = StateGraph(dict)
        graph.add_node("run", self._run_node)
        graph.add_edge(START, "run"); graph.add_edge("run", END)
        return await graph.compile().ainvoke(state)

    async def _run_node(self, state):
        await self._comm.delta(text="Hello from my bundle!", index=0, marker="answer")
        return {"final_answer": "Done", "followups": []}

BaseEntrypoint Lifecycle

Method / PropertyWhen CalledTypical Use
configuration (property)On bundle loadDeclare defaults: role_models, embedding, knowledge, economics, execution
on_bundle_load(**kwargs)Once per process, per tenant/projectBuild knowledge index, connect external services, warm caches
pre_run_hook(state)Before each turnRe-reconcile knowledge space if config changed
execute_core(state, thread_id, params)Every turnBuild + invoke your LangGraph workflow
rebind_request_context(...)On cached singleton reuseRefresh request-bound state (comm, user, etc.)

Bundle Lifecycle

A bundle goes through a well-defined lifecycle from discovery to shutdown. Understanding these phases helps you place initialization logic, manage state, and handle configuration changes correctly.

Discovery and Loading

Bundle source lives in git. On deployment the platform clones the configured repo and ref, imports the module, and finds the class decorated with @agentic_workflow. The loader extracts interface metadata (decorators for widgets, APIs, message handler) and builds a BundleInterfaceManifest that the REST layer uses for routing.

Singleton Instance Model

By default each incoming turn or operation creates a fresh entrypoint instance. When the registry sets singleton=true, the platform caches and reuses one instance per loaded bundle spec in the current process.

i
Even with singleton reuse, treat runtime memory as ephemeral. Request-bound surfaces (self.comm, current actor, conversation/turn ids) are rebound per invocation via rebind_request_context() and must never be cached across requests. Durable state belongs in bundle storage, Redis, or bundle props — not on self.

Initialization Hooks

HookFrequencyTypical Use
on_bundle_load(**kwargs)Once per process, per tenant/projectBuild indexes, warm caches, clone repos, prepare local read-only assets, trigger UI build
pre_run_hook(state)Every invocationLast-minute validation or reconciliation before execution
execute_core(state, thread_id, params)Every invocationMain bundle logic (chat turn or operation handling)
post_run_hook(state, result)Every invocationFinal bookkeeping after execution completes
rebind_request_context(...)Singleton reuse onlyRefresh request-local handles on cached instance before the current call

on_bundle_load() must be deterministic and idempotent. It runs before any request depends on the bundle and is the right place for heavy preparation work. Do not store request-local state there.

Hot-Reload on Config Change

When you update a bundle's ref in bundles.yaml and re-apply, the platform detects the configuration change via a content hash of the bundle directory. The updated bundle is loaded in the current process without a service restart. on_bundle_load() runs again for the new version, and subsequent requests are served by the refreshed instance.

Shutdown and Cleanup

The platform does not expose a dedicated shutdown hook. Because bundle state should live in external storage (Redis, S3, local shared storage), process termination is safe by design. Transient per-invocation state in OUT_DIR / workdir is scoped to the request and cleaned up automatically.

Storage Surfaces Across Phases

SurfaceAccessIsolationUse It For
bundle_propsReadTenant + project + bundleEffective non-secret configuration
get_secret(...)ReadSecret key namespaceAPI keys, tokens, credentials
Redis KV cacheRead/writeDeveloper-chosen keysLightweight distributed state, flags, small caches
Bundle storage backendRead/writeTenant + project + bundlePersistent data on S3/file storage
Shared local storage (BUNDLE_STORAGE_ROOT)Read/writeTenant + project + bundleLarge local caches, cloned repos, indexes
OUT_DIR / workdirRead/writeCurrent invocationTransient turn files, generated artifacts

Interface: In & Out

📥 Inbound: ChatTaskPayload

Every turn arrives as a ChatTaskPayload (Pydantic). Key fields:

  • request.message — user's text
  • request.chat_history — prior messages
  • request.payload — arbitrary JSON (for REST ops)
  • actor — tenant_id, project_id
  • routing — conversation_id, turn_id, bundle_id
  • user — user_id, user_type, roles, timezone
  • continuation — follow-up or steer type

📤 Outbound: SSE Streaming

Your bundle streams back via the Communicator. The client receives ChatEnvelope events in real time.

  • delta — streaming text chunks (thinking / answer)
  • step — tool calls, status updates, timeline events
  • complete — turn finished with final data
  • error — propagate errors cleanly
  • event — custom events (artifacts, reactions, etc.)

Using the Communicator

# Stream answer text
await self._comm.delta(text="chunk...", index=0, marker="answer")

# Announce a step
await self._comm.step(step="web_search", status="started", title="Searching the web...")
await self._comm.step(step="web_search", status="completed")

# Emit follow-up suggestions
await self._comm.followups(["Tell me more", "Show examples"])

# Final complete signal
await self._comm.complete(data={"answer": "..."})

REST Operations (for UI Widgets)

Your bundle exposes additional REST operations via the Operations API hosted by the Processor:

POST /bundles/{tenant}/{project}/{bundle_id}/operations/{operation}
GET  /bundles/{tenant}/{project}/{bundle_id}/operations/{operation}

The explicit bundle_id form is now the preferred route. The processor resolves the target method from the bundle’s decorator-discovered interface surface rather than assuming every operation is just workflow.run(). A default-bundle compatibility shortcut still exists at POST /bundles/{tenant}/{project}/operations/{operation}.

Continuation Types

TypeDescription
regularNormal new message
followupUser clicked a suggested follow-up
steerUser is redirecting the ongoing turn

Classic vs. Interactive Turn Execution

ModeBehavior
Classic turn execution The turn runs to completion before the user can affect the next agent step. From the user perspective, input is effectively blocked until the turn finishes and emits its final result.
Interactive turn execution While a turn is running, the user can still contribute to the same conversation. The platform stores these contributions as conversation continuations and promotes them after the active turn completes.
↪️
Interactive execution uses the existing continuation kinds: followup and steer. A busy conversation accepts these into the continuation mailbox rather than dropping them. followup preserves the user's next prompt in order, while steer is the explicit “redirect the currently running work” signal. After the current turn finishes, the processor promotes the next stored continuation and starts the next turn with that intent already attached.

Storage

KDCube is a distributed, multi-tenant system. Three storage tiers are available to your bundle:

Storage tiers diagram
Storage Tiers Three storage tiers available to bundles: cloud object storage, local filesystem, and Redis cache ☁️ Cloud Storage AIBundleStorage file:// or s3:// Survives restarts & scaling Per tenant / project / bundle Read/write arbitrary keys CB_BUNDLE_STORAGE_URL 📁 Local FS (Shared) bundle_storage_root() BUNDLE_STORAGE_ROOT env Per bundle version namespace Knowledge indexes, caches EFS in production pathlib.Path (filesystem) ⚡ Redis Cache self.kv_cache / self.redis In-memory, fast Bundle props & config Session state, rate limits Pub/sub for bundle updates aioredis client

Cloud Storage (AIBundleStorage)

from kdcube_ai_app.apps.chat.sdk.storage.ai_bundle_storage import AIBundleStorage

storage = AIBundleStorage(
    tenant="my-tenant", project="my-project",
    ai_bundle_id="my-bundle@1-0",
    storage_uri="s3://my-bucket"  # or file:///data/bundle-storage
)

storage.write("reports/latest.json", data='{"count": 42}')
content = storage.read("reports/latest.json", as_text=True)
keys = storage.list("reports/")

Local FS (Shared Bundle Storage)

# In entrypoint or workflow
root = self.bundle_storage_root()  # pathlib.Path
index_path = root / "knowledge_index"
index_path.mkdir(exist_ok=True)

Path namespaced: {BUNDLE_STORAGE_ROOT}/{tenant}/{project}/{bundle_id}/. Ideal for knowledge indexes, model weights, large caches. In production, mount an EFS volume.

Redis Cache

# Low-level Redis client (aioredis)
await self.redis.set("my:key", "value", ex=3600)
val = await self.redis.get("my:key")

# KVCache wrapper
await self.kv_cache.set("user_prefs", {"theme": "dark"}, ttl=86400)

Workflow Orchestration

The platform uses LangGraph for workflow orchestration. Your execute_core builds and invokes a StateGraph. For common patterns, use BaseWorkflow:

from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.base_workflow import BaseWorkflow

class MyWorkflow(BaseWorkflow):
    def __init__(self, *args, bundle_props=None, **kwargs):
        super().__init__(*args, bundle_props=bundle_props, **kwargs)

    async def process(self, payload):
        scratchpad = self.start_turn(payload)
        try:
            react = await self.build_react(
                scratchpad=scratchpad,
                tools_module="my_bundle.tools_descriptor",
                skills_module="my_bundle.skills_descriptor",
            )
            result = await react.run(payload)
            self.finish_turn(scratchpad, ok=True)
            return result
        except Exception as e:
            self.finish_turn(scratchpad, ok=False); raise
🚀
Use BaseWorkflow for the quickest path. It wires up ConvMemories, TurnStatus, ContextRAG, ApplicationHosting, and gives you build_react() which assembles the full ReAct agent with all tools and skills resolved.

BaseWorkflow Key Parameters

ParameterTypeDescription
conv_idxConvIndexConversation vector index for semantic search
storeConversationStoreFile/S3-backed conversation storage
commChatCommunicatorSSE streaming channel
model_serviceModelServiceBaseLLM registry / router
ctx_clientContextRAGClientContext retrieval and RAG
bundle_propsDictBundle runtime configuration
graphGraphCtxOptional knowledge graph context

Simplest Agentic Workflow Pattern

A bundle can use any orchestration pattern — it is not required to use the ReAct agent. This is one common pattern that combines a Gate agent with the ReAct agent:

Agentic workflow pattern diagram
Simplest Agentic Workflow Pattern Workflow diagram combining a Gate agent with the ReAct agent for agentic task routing Simplest Agentic Workflow Pattern (optional gate + react layers) User Message Gate Agent (Optional layer) Intent classify Route decision ReAct Agent Decision loop Tool calls Self-planning Answer Gen (Optional layer) Final answer Follow-ups 📜 Timeline — streamed to client in real time tool_calls · answer · thinking · artifacts · plan blocks — all streaming

Tools System

ToolNamespaceDescription
web_searchweb_toolsNeural web search with ranking (Brave / DuckDuckGo)
web_fetchweb_toolsFetch + parse web pages (readability-enabled)
execute_code_pythonexec_toolsIsolated Python code execution — Docker (default), Fargate, or in-process per bundle config
write_pdfrendering_toolsGenerate PDF from Markdown + table of contents
write_pngrendering_toolsRender HTML/SVG to PNG image
write_docxrendering_toolsGenerate DOCX from Markdown
write_pptxrendering_toolsGenerate PPTX slide deck
write_htmlrendering_toolsGenerate standalone HTML artifact
fetch_ctxctx_toolsFetch artifacts by logical path (ar:/fi:/ks: addresses)
readreactreact.readLoad artifact into timeline (fi:/ar:/ks:/so: paths)
writereactreact.writeAuthor current-turn content into files/<scope>/... for durable workspace state or outputs/<scope>/... for non-workspace artifacts; stream to canvas/timeline_text/internal channel and optionally share as file
pullreactreact.pullExplicitly materialize historical artifacts from fi: refs as readonly local reference material; subtree pulls for .files/..., exact-file pulls for .outputs/... and attachments. Pulled refs do not modify the active current-turn workspace.
planreactreact.planCreate / update / close the current turn plan (shown in ANNOUNCE)
patchreactreact.patchPatch an existing artifact (unified diff or full replacement)
checkoutreactreact.checkoutConstruct or update the active current-turn workspace from ordered fi:<turn>.files/... refs. mode="replace" seeds the workspace from scratch; mode="overlay" imports selected historical files into the existing workspace.
memsearchreactreact.memsearchSemantic search in past conversation turns
search_filesreactreact.search_filesSearch files in execution workspace
hidereactreact.hideReplace timeline snippet with placeholder

Bundle-local tools use @kernel_function from Semantic Kernel and are registered in tools_descriptor.py.

# tools/my_tools.py
from typing import Annotated
import semantic_kernel as sk
from semantic_kernel.functions import kernel_function

class MyTools:
    @kernel_function(name="search", description="Search product catalog")
    async def search(self,
        query: Annotated[str, "Search query"],
        limit: Annotated[int, "Max results"] = 5
    ) -> str:
        # your logic here
        return "results..."
# tools_descriptor.py
TOOLS_SPECS = [
    # SDK built-in tool modules (installed package)
    {"module": "kdcube_ai_app.apps.chat.sdk.tools.web_tools", "alias": "web_tools", "use_sk": True},
    {"module": "kdcube_ai_app.apps.chat.sdk.tools.exec_tools", "alias": "exec_tools", "use_sk": True},
    # Bundle-local tools ("ref" = path relative to bundle root, works in Docker too)
    {"ref": "tools/my_tools.py", "alias": "my_tools", "use_sk": True},
]
# Tool IDs: web_tools.web_search, exec_tools.execute_code_python, my_tools.search

# Optional: per-tool runtime overrides
TOOL_RUNTIME = {
    "web_tools.web_search": "local",       # subprocess sandbox
    "exec_tools.execute_code_python": "docker",  # Docker container
}

See custom-tools-README.md and the reference bundle’s versatile tools_descriptor.py.

MCP (Model Context Protocol) servers are declared in MCP_TOOL_SPECS and configured in bundle props.

# tools_descriptor.py
MCP_TOOL_SPECS = [
    {"server_id": "web_search", "alias": "web_search", "tools": ["web_search"]},
    {"server_id": "docs", "alias": "docs", "tools": ["*"]},  # all tools
    {"server_id": "stack", "alias": "stack", "tools": ["*"]},
]
# Tool IDs: mcp.docs.some_tool, mcp.stack.some_tool
# bundles.yaml config section — how to connect
config:
  mcp:
    services:
      mcpServers:
        docs:
          transport: http
          url: https://mcp.example.com
          auth:
            type: bearer
            secret: bundles.my-bundle.secrets.docs.token
        stack:
          transport: stdio
          command: npx
          args: ["mcp-remote", "mcp.stackoverflow.com"]

See mcp-README.md for all transports (stdio, http, streamable-http, sse) and auth modes.

Artifact Path Families

PrefixResolves ToExample
fi:Turn-scoped file artifact namespace for workspace files, non-workspace outputs, and attachmentsfi:turn_123.outputs/export/report.pdf
ar:Artifact from timelinear:turn_123.artifacts.summary
ks:Knowledge space (read-only, docs/src)ks:docs/architecture.md
sk:Skills space (skill instruction files)sk:public.pdf-press/SKILL.md
so:Sources from context poolso:sources_pool[1-5]
tc:Tool call blocktc:turn_123.abc.call

Skills System

Skills are reusable instruction sets that give agents specialized capabilities. A skill bundles a natural-language instruction (SKILL.md), tool references, and source references.

Built-in Platform Skills

Skill IDNamespaceDescription
url-genpublicGenerate hosted URLs for file artifacts
pdf-presspublicPDF generation and manipulation
docx-presspublicDOCX document generation
pptx-presspublicPPTX presentation generation
png-presspublicPNG image rendering from HTML/SVG
mermaidpublicMermaid diagram generation
link-evidenceinternalCitation and evidence linking
sources-sectioninternalAutomatic sources section generation

Custom Bundle Skill

# skills/my_skill/SKILL.md
You are an expert in our product catalog.
When asked about products, use the `product_search` tool to find relevant items.
Always include pricing and availability.
# skills/my_skill/tools.yaml
tools:
  - id: product_search
    role: search
    why: Search the product catalog
# skills_descriptor.py
AGENTS_CONFIG = {
    "solver": {
        "enabled_skills": ["my_skill", "pdf-press", "url-gen"],
        "disabled_skills": []
    }
}

Creating a SKILL.md File

Each skill lives in its own folder under the bundle's skills/ directory. The required file is SKILL.md (or skill.yml), which contains YAML front-matter and a natural-language instruction body.

# skills/product/SKILL.md
---
name: "Product Catalog Expert"
id: "kdcube"
namespace: "product"
description: "Search and present product information"
version: "1.0"
tags: ["catalog", "products"]
when_to_use: "When the user asks about products, pricing, or availability"
imports: []
---

You are an expert in the product catalog.
When asked about products, use the `product_search` tool.
Always include pricing and availability in your response.

Optional companion files in the same folder:

  • compact.md — a shorter instruction variant for context-constrained agents
  • sources.yaml — sources injected into sources_pool when the skill loads (referenceable as so:sources_pool[...])
  • tools.yaml — recommended tools for the skill, used by planners and UX

Skill Visibility Configuration

The skills_descriptor.py file controls which skills are visible to which agent consumers. It exposes two key variables:

# skills_descriptor.py
import pathlib

BUNDLE_ROOT = pathlib.Path(__file__).resolve().parent

# Points to the skills folder layout: <root>/<namespace>/<skill_id>/SKILL.md
CUSTOM_SKILLS_ROOT = BUNDLE_ROOT / "skills"

AGENTS_CONFIG = {
    # Allow-list for the ReAct decision agent
    "solver.react.decision.v2": {
        "enabled": ["product.kdcube"]
    },
    # Deny-list for a generator agent
    "answer.generator.strong": {
        "disabled": ["public.*"]
    },
}
RuleBehavior
enabled: [...]Allow-list: only listed skills are visible to that consumer
disabled: [...]Deny-list: listed skills are hidden from that consumer
WildcardsSupported in both lists ("public.*", "public.docx-*", "*")
Missing consumer entryNo filtering — all registered skills are visible

Skill Loading and Resolution

The runtime auto-detects <bundle_root>/skills as the custom skills root when CUSTOM_SKILLS_ROOT is not set. Skills are loaded into the skill registry and resolved by fully qualified id (namespace.skill_id). Consumer agents reference skills via short-id tokens (SKx) or explicit sk:<id> references. When a skill is loaded with react.read("sk:<skill>"), its sources are merged into sources_pool and citation tokens are rewritten to match.

i
Setting CUSTOM_SKILLS_ROOT = None does not reliably disable bundle-local skills — the runtime falls back to auto-detection. To truly disable them, remove the skills/ folder or set CUSTOM_SKILLS_ROOT to a non-existent path.

Best Practices for Skill Authoring

  • Keep SKILL.md instructions focused — one skill per capability domain
  • Use when_to_use front-matter to help the agent decide when to activate the skill
  • Include tools.yaml to pair skills with the tools they need
  • Use AGENTS_CONFIG to restrict skill visibility rather than deleting skill files — this keeps skills available for future consumers
  • Test visibility filtering per consumer: a skill hidden from solver.react.decision.v2 is still visible to unfiltered consumers

Widgets, APIs & Custom UI

Bundles now expose a decorator-discovered interface. Widgets and APIs are separate surfaces: widgets are UI entrypoints intended for the platform shell, while APIs are programmatic operations reachable under the integrations operations routes. A bundle may also define a custom main view and a dedicated message entrypoint.

💡
Think in four interface surfaces: widgets, APIs, ui_main, and on_message. The loader scans them from decorators on the bundle entrypoint/workflow class and exposes them as a bundle interface manifest.
Widget and custom UI integration diagram
Widgets and Custom UI Widget integration flow showing decorator-discovered widgets and bundle APIs UI Panel Loads widget GET /widgets/{alias} Integrations API /bundles/{t}/{p}/{b}/ widgets / operations Your Bundle @ui_widget / @ui_main @api / @on_message Widget / Main View Rendered in UI panel Calls GET/POST /operations UI can call bundle APIs on the same explicit bundle_id path

Bundle Interface Decorators

DecoratorAttaches ToPurposeKey args
@agentic_workflow(...)classMarks the bundle entrypoint for loader discoveryname, version, priority
@bundle_id(...)classDeclares the canonical bundle id from codeid
@ui_widget(...)methodDeclares a UI widget discoverable under the widgets endpointsicon, alias, roles
@api(...)methodDeclares a bundle API method under /operations/{operation} or /public/{operation}method=POST|GET, alias, route, roles, public_auth
@ui_mainmethodMarks the bundle’s main UI entrypoint returned in the interface manifestno args
@on_messagemethodMarks the message-entry method the proc/web layer can route to for live message handlingno args

Bundle identity can come from the registry entry or be declared explicitly with @bundle_id("my.bundle@version"). The loader uses the discovered bundle interface decorators as the HTTP and UI contract.

Example: Declare widget, API, and UI entrypoints

from kdcube_ai_app.infra.plugin.agentic_loader import agentic_workflow, bundle_id, api, ui_widget, ui_main, on_message

@agentic_workflow(name="my-bundle", version="1.0.0", priority=100)
@bundle_id("my.bundle@1.0.0")
class MyBundle(BaseEntrypointWithEconomics):

    @ui_widget(icon={"tailwind": "heroicons-outline:swatch"}, alias="dashboard", roles=("registered", "admin"))
    def dashboard_widget(self, **kwargs):
        return ["<div id='root'></div>"]

    @api(alias="refresh_dashboard", method="POST", roles=("registered", "admin"))
    def refresh_dashboard(self, **kwargs):
        return {"ok": True}

    @api(alias="telegram_webhook", method="POST", route="public", public_auth={"mode": "header_secret", "header": "X-Telegram-Bot-Api-Secret-Token", "secret_key": "telegram.webhook_secret"})
    async def telegram_webhook(self, **kwargs):
        return {"ok": True}

    @ui_main
    def main_view(self, **kwargs):
        return ["<div id='app'></div>"]

    @on_message
    async def run_message(self, **kwargs):
        return await self.run(**kwargs)

Integrations REST Surface

EndpointPurposeNotes
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}Return the bundle interface manifestIncludes ui_widgets, api_endpoints, ui_main, on_message
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgetsList visible widgets for the current userReturns alias, icon, roles
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgets/{widget}Render or fetch one widgetResolved via @ui_widget
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}Call a bundle API operationResolved via @api; preferred explicit form
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}GET variant for idempotent operations and static-like API readsResolved via the same interface manifest
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation}Call a public bundle API operationResolved only via @api(route="public", ...); current public methods must declare public_auth
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation}GET variant for public idempotent bundle APIsSupports the same route-scoped manifest contract
POST /api/integrations/bundles/{tenant}/{project}/operations/{operation}Default-bundle shortcutCompatibility path when bundle_id is omitted
GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}Serve bundle-scoped static assetsUseful for bundle-shipped frontend assets and media
i
Public bundle APIs are now a real platform surface. Use route="public" only for intentionally public endpoints, and declare public_auth explicitly. Current built-in modes are "none" for intentionally open routes and header_secret for webhook headers such as Telegram’s X-Telegram-Bot-Api-Secret-Token.

Interface Manifest Discovery

Decorators on the bundle class are the source of truth for the bundle's HTTP and UI contract. At load time the loader scans the entrypoint class for @api, @ui_widget, @ui_main, and @on_message decorators and builds a typed BundleInterfaceManifest:

# Internal manifest shape (built automatically by the loader)
BundleInterfaceManifest(
    bundle_id="versatile@2026-03-31-13-36",
    ui_widgets=(UIWidgetSpec(alias="preferences", icon={...}, roles=(...)),),
    api_endpoints=(APIEndpointSpec(alias="preferences_exec_report", http_method="POST", route="operations", roles=(...)),),
    ui_main=UIMainSpec(method_name="main_ui"),
    on_message=OnMessageSpec(method_name="run"),
)

The manifest is returned by GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} and drives all route resolution. Only decorated methods are remotely callable — there is no same-name fallback for undecorated methods.

Iframe Embedding Model for Widgets

Widget methods (@ui_widget) return HTML fragments that the platform shell renders inside an iframe panel. The widget fetch endpoint resolves the alias, checks role visibility, and invokes the method. If the same widget must also be callable through the operations route (for legacy clients), decorate it with both @ui_widget and @api:

@api(alias="preferences_widget", route="operations", roles=("registered",))
@ui_widget(alias="preferences", icon={"tailwind": "heroicons-outline:adjustments-horizontal"}, roles=("registered",))
def preferences_widget(self, **kwargs):
    return ["<div id='root'></div>"]

Static File Serving for Custom UIs

Bundles that ship a custom frontend (a Vite/React SPA in ui-src/) have their built assets served from a dedicated static route:

GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}

The endpoint locates the correct bundle_storage_root via a content hash of the bundle directory, then returns files from its ui/ subdirectory. Missing paths fall back to index.html for client-side routing. A <base> tag is injected into index.html so relative assets resolve correctly.

i
The UI build runs during on_bundle_load() via _ensure_ui_build(). It resolves src_folder, runs build_command, and stores output under <bundle_storage_root>/ui/. A .ui.signature file skips rebuilds when nothing changed. Build-on-first-request is also supported if the UI was not yet built in the current process.

Main View Configuration

To define a custom main view, declare ui.main_view in your bundle's configuration property and mark a method with @ui_main:

@property
def configuration(self):
    return {
        "ui": {
            "main_view": {
                "src_folder": "ui-src",
                "build_command": "npm install && OUTDIR=<VI_BUILD_DEST_ABSOLUTE_PATH> npm run build",
            }
        }
    }

The built SPA communicates with the backend through the bundle operations endpoint and receives runtime config (base URL, auth tokens, tenant/project) via postMessage from the host frame. The UI is a normal platform client — if it needs bundle-originated events targeting one exact connected peer, it must propagate the connected peer id on REST requests per the client communication contract.

Registry vs. Interface Discovery

GET /api/admin/integrations/bundles returns the configured bundle registry entries — id, repo, ref, module, path, version, and related deployment metadata. The richer interface discovery surface lives on the per-bundle integrations endpoint above, where the runtime scans decorators and returns the current manifest.

Platform Built-in Operations

OperationDescription
ai_bundleBundle admin dashboard (props editor, status) — all bundles inherit this
control_planeEconomics dashboard (usage, billing) — via BaseEntrypointWithEconomics
suggestionsSuggested prompts for new conversations
newsNews/updates from the bundle

Deploying Your Bundle

Option A: With the KDCube Platform

  1. 1

    Push your bundle to Git

    git push origin v1.0.0
  2. 2

    Add to bundles.yaml

    - id: "my-bundle@1-0"
      repo: "git@github.com:org/my-bundle.git"
      ref: "v1.0.0"
      module: "my_bundle.entrypoint"
  3. 3

    Inject secrets and apply

    kdcube-setup --secrets-set GIT_HTTP_TOKEN=ghp_...
    kdcube-setup --secrets-prompt  # for LLM keys

    The bundle config is applied immediately via Redis — no restart needed. Change the ref and re-run to switch bundle versions on the fly.

  4. 4

    Set as default bundle via the Admin Dashboard

    Open the AI Bundle Dashboard (/api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/ai_bundle). Your registered bundle appears in the list. Set it as the default_bundle_id for the tenant/project. The change is applied immediately via Redis — no restart needed.

Option B: Standalone (Without Platform)

Bundles can run outside the platform. The SDK is a plain Python package — build your own FastAPI app that imports and invokes it directly. Or build a custom Docker image that runs just your bundle with its own server. The platform's value is in hosting, auth, SSE, storage, economics, and UI — none of that is required for the core agent logic.

Bundle Git Auth

Modebundles.yamlSecret
SSH keygit@github.com:org/repo.gitSSH key mounted in container
HTTPS tokenhttps://github.com/org/repo.gitGIT_HTTP_TOKEN secret

Example Bundles

Bundle IDWhat It ShowsKey Features
versatile@2026-03-31-13-36Reference bundle for SDK and integrations docsReAct agent, bundle-local tools, MCP tools, widget decorators, integrations operations, custom main UI under ui-src/, and economics-aware entrypoint patterns
versatile — widgets, operations, and custom main-view pattern

The versatile sample shows how one bundle can combine a standard ReAct conversation workflow with decorated widget endpoints, bundle APIs, and a custom SPA main view. It is the best current reference for how entrypoint methods, tools_descriptor.py, and ui-src/ fit together.

View entrypoint.py on GitHub →

Coming Soon & Current Status

Here's the current state of platform capabilities and what's next on the roadmap:

FeatureStatusNotes
Bundle-declared widgets Available Bundles now mark their UI widgets explicitly with @ui_widget. The platform exposes that widget manifest via GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} and /widgets, so frontends can render bundle-defined widget launchers directly from discovered metadata instead of hardcoded platform lists.
Custom main view Available Bundles can override the default chat surface with a custom main UI via the reserved ui.main_view config and a discovered @ui_main entrypoint. This is not limited to static dashboards: a bundle main view can be a full SPA using REST, SSE streaming, widgets, downloads, and any richer client behavior the bundle wants to implement.
Static asset serving from bundle Available Bundle-scoped static files can now be served directly via GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}. GET operations are also available for idempotent bundle APIs under /operations/{operation}.
Anonymous public bundle APIs Available Bundle APIs can now be exposed under /public/{operation} through @api(..., route="public"). Public methods must declare public_auth; current built-in modes are explicit open access ("none") and shared-secret header verification for webhook providers such as Telegram.
Admin bundle interface enrichment Soon /api/admin/integrations/bundles already returns registry metadata. Joining it with scanned widgets, apis, and on_message manifest data is the next natural admin-facing refinement.
Bundle marketplace Soon Browse and install community bundles from a registry.
Live bundle reload Available Bundle code is loaded per-process and cached as a singleton. Config updates (bundle props, role_models, etc.) propagate immediately via Redis pub/sub. Restarting the processor picks up any code changes.
Bundle versioning Available Update the ref (branch/tag/commit) in bundles.yaml or via the Admin API and the change applies immediately to new requests — no downtime. The bundle registry is stored in Redis and updated atomically.
Git-backed React workspace Available React now supports a git workspace backend with sparse current-turn repos, lineage-isolated history, explicit historical materialization via react.pull(...), and active workspace construction via react.checkout(mode="replace"|"overlay", ...). Turn-end publish writes current-turn text workspace state back into the lineage without exposing other users' branches.
Interactive steer / followup execution Soon The continuation protocol and some processor-side groundwork exist, but steer / followup is not yet integrated as a complete end-to-end feature. It still needs explicit integration across ingress, processor, SSE transport, and the React runtime before it should be considered ready.
KDCube bundle-copilot Partial The bundle-copilot foundation already exists as a React-based coding bundle and can help scaffold or evolve bundle code. The remaining work is to complete the product experience for bundle builders, including smoother workspace continuity, stronger guidance, and tighter bundle-development flows.
Multi-bundle conversations Partial Technically possible today via routing, but both bundles must understand and agree on the shared conversation format (timeline structure, artifact paths, turn state). Requires alignment between bundle developers on the protocol.
Streaming SSE for operations Soon Today, widget operations are synchronous REST POST. Adding an SSE channel for operations would allow widgets to stream responses back — enabling live-updating dashboards and progress indicators from widget calls.
Browser navigation toolset for agents Soon A richer browser-navigation tool family for agentic use is the next natural step beyond search and fetch. The goal is to let bundles navigate, inspect, and operate on web flows more deliberately while still staying inside the platform’s controlled tool surface.
Policy DSL Roadmap Relevant for regulated or security-sensitive deployments where operators want to express bundle access restrictions, tenant rules, and data-handling constraints declaratively instead of encoding them in Python hooks. Important governance work, but not on the critical path for the current bundle interface rollout.
Deterministic Enforcement Engine Roadmap Highly complementary to policy work: enforcement decisions should stay deterministic, auditable, and separate from LLM judgment. This is a strong control-plane capability for the platform, especially once policies, gates, and richer workflow contracts grow.
Workflow Invariants Roadmap One of the most immediately relevant roadmap items for agent reliability. Declarative assertions such as “artifact Z must exist before LLM call” or “tool X may only run after step Y” would let the runtime halt bad states before they propagate.
Cross-Agent Approval Gates Roadmap Relevant when multi-agent or supervisor-driven workflows become a primary product surface. The idea is to let a worker or sub-agent pause on a high-impact action and require approval from a coordinator or human operator before continuing.

🎯 Priority: Steer / Followup + KDCube Bundle Copilot

The next high-value product work is to polish interactive execution around steer and followup so running turns can be redirected and continued more naturally, and to finish the KDCube bundle-copilot experience that helps developers create and evolve bundles directly inside the platform.

💬
Have feedback or want to prioritize something? Open an issue on the GitHub repo.