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.
| Bundle | Description |
|---|---|
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.
Big Picture: How It All Fits Together
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:
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 / Property | When Called | Typical Use |
|---|---|---|
configuration (property) | On bundle load | Declare defaults: role_models, embedding, knowledge, economics, execution |
on_bundle_load(**kwargs) | Once per process, per tenant/project | Build knowledge index, connect external services, warm caches |
pre_run_hook(state) | Before each turn | Re-reconcile knowledge space if config changed |
execute_core(state, thread_id, params) | Every turn | Build + invoke your LangGraph workflow |
rebind_request_context(...) | On cached singleton reuse | Refresh 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.
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
| Hook | Frequency | Typical Use |
|---|---|---|
on_bundle_load(**kwargs) | Once per process, per tenant/project | Build indexes, warm caches, clone repos, prepare local read-only assets, trigger UI build |
pre_run_hook(state) | Every invocation | Last-minute validation or reconciliation before execution |
execute_core(state, thread_id, params) | Every invocation | Main bundle logic (chat turn or operation handling) |
post_run_hook(state, result) | Every invocation | Final bookkeeping after execution completes |
rebind_request_context(...) | Singleton reuse only | Refresh 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
| Surface | Access | Isolation | Use It For |
|---|---|---|---|
bundle_props | Read | Tenant + project + bundle | Effective non-secret configuration |
get_secret(...) | Read | Secret key namespace | API keys, tokens, credentials |
| Redis KV cache | Read/write | Developer-chosen keys | Lightweight distributed state, flags, small caches |
| Bundle storage backend | Read/write | Tenant + project + bundle | Persistent data on S3/file storage |
Shared local storage (BUNDLE_STORAGE_ROOT) | Read/write | Tenant + project + bundle | Large local caches, cloned repos, indexes |
OUT_DIR / workdir | Read/write | Current invocation | Transient turn files, generated artifacts |
Interface: In & Out
📥 Inbound: ChatTaskPayload
Every turn arrives as a ChatTaskPayload (Pydantic). Key fields:
request.message— user's textrequest.chat_history— prior messagesrequest.payload— arbitrary JSON (for REST ops)actor— tenant_id, project_idrouting— conversation_id, turn_id, bundle_iduser— user_id, user_type, roles, timezonecontinuation— 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
| Type | Description |
|---|---|
regular | Normal new message |
followup | User clicked a suggested follow-up |
steer | User is redirecting the ongoing turn |
Classic vs. Interactive Turn Execution
| Mode | Behavior |
|---|---|
| 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. |
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:
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
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
| Parameter | Type | Description |
|---|---|---|
conv_idx | ConvIndex | Conversation vector index for semantic search |
store | ConversationStore | File/S3-backed conversation storage |
comm | ChatCommunicator | SSE streaming channel |
model_service | ModelServiceBase | LLM registry / router |
ctx_client | ContextRAGClient | Context retrieval and RAG |
bundle_props | Dict | Bundle runtime configuration |
graph | GraphCtx | Optional 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:
Tools System
| Tool | Namespace | Description |
|---|---|---|
web_search | web_tools | Neural web search with ranking (Brave / DuckDuckGo) |
web_fetch | web_tools | Fetch + parse web pages (readability-enabled) |
execute_code_python | exec_tools | Isolated Python code execution — Docker (default), Fargate, or in-process per bundle config |
write_pdf | rendering_tools | Generate PDF from Markdown + table of contents |
write_png | rendering_tools | Render HTML/SVG to PNG image |
write_docx | rendering_tools | Generate DOCX from Markdown |
write_pptx | rendering_tools | Generate PPTX slide deck |
write_html | rendering_tools | Generate standalone HTML artifact |
fetch_ctx | ctx_tools | Fetch artifacts by logical path (ar:/fi:/ks: addresses) |
read | react → react.read | Load artifact into timeline (fi:/ar:/ks:/so: paths) |
write | react → react.write | Author 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 |
pull | react → react.pull | Explicitly 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. |
plan | react → react.plan | Create / update / close the current turn plan (shown in ANNOUNCE) |
patch | react → react.patch | Patch an existing artifact (unified diff or full replacement) |
checkout | react → react.checkout | Construct 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. |
memsearch | react → react.memsearch | Semantic search in past conversation turns |
search_files | react → react.search_files | Search files in execution workspace |
hide | react → react.hide | Replace 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
| Prefix | Resolves To | Example |
|---|---|---|
fi: | Turn-scoped file artifact namespace for workspace files, non-workspace outputs, and attachments | fi:turn_123.outputs/export/report.pdf |
ar: | Artifact from timeline | ar: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 pool | so:sources_pool[1-5] |
tc: | Tool call block | tc: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 ID | Namespace | Description |
|---|---|---|
url-gen | public | Generate hosted URLs for file artifacts |
pdf-press | public | PDF generation and manipulation |
docx-press | public | DOCX document generation |
pptx-press | public | PPTX presentation generation |
png-press | public | PNG image rendering from HTML/SVG |
mermaid | public | Mermaid diagram generation |
link-evidence | internal | Citation and evidence linking |
sources-section | internal | Automatic 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 agentssources.yaml— sources injected intosources_poolwhen the skill loads (referenceable asso: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.*"]
},
}
| Rule | Behavior |
|---|---|
enabled: [...] | Allow-list: only listed skills are visible to that consumer |
disabled: [...] | Deny-list: listed skills are hidden from that consumer |
| Wildcards | Supported in both lists ("public.*", "public.docx-*", "*") |
| Missing consumer entry | No 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.
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.mdinstructions focused — one skill per capability domain - Use
when_to_usefront-matter to help the agent decide when to activate the skill - Include
tools.yamlto pair skills with the tools they need - Use
AGENTS_CONFIGto 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.v2is 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.
Bundle Interface Decorators
| Decorator | Attaches To | Purpose | Key args |
|---|---|---|---|
@agentic_workflow(...) | class | Marks the bundle entrypoint for loader discovery | name, version, priority |
@bundle_id(...) | class | Declares the canonical bundle id from code | id |
@ui_widget(...) | method | Declares a UI widget discoverable under the widgets endpoints | icon, alias, roles |
@api(...) | method | Declares a bundle API method under /operations/{operation} or /public/{operation} | method=POST|GET, alias, route, roles, public_auth |
@ui_main | method | Marks the bundle’s main UI entrypoint returned in the interface manifest | no args |
@on_message | method | Marks the message-entry method the proc/web layer can route to for live message handling | no 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
| Endpoint | Purpose | Notes |
|---|---|---|
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} | Return the bundle interface manifest | Includes ui_widgets, api_endpoints, ui_main, on_message |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgets | List visible widgets for the current user | Returns alias, icon, roles |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgets/{widget} | Render or fetch one widget | Resolved via @ui_widget |
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation} | Call a bundle API operation | Resolved via @api; preferred explicit form |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation} | GET variant for idempotent operations and static-like API reads | Resolved via the same interface manifest |
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation} | Call a public bundle API operation | Resolved 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 APIs | Supports the same route-scoped manifest contract |
POST /api/integrations/bundles/{tenant}/{project}/operations/{operation} | Default-bundle shortcut | Compatibility path when bundle_id is omitted |
GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path} | Serve bundle-scoped static assets | Useful for bundle-shipped frontend assets and media |
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.
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
| Operation | Description |
|---|---|
ai_bundle | Bundle admin dashboard (props editor, status) — all bundles inherit this |
control_plane | Economics dashboard (usage, billing) — via BaseEntrypointWithEconomics |
suggestions | Suggested prompts for new conversations |
news | News/updates from the bundle |
Deploying Your Bundle
Option A: With the KDCube Platform
-
1
Push your bundle to Git
git push origin v1.0.0 -
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
Inject secrets and apply
kdcube-setup --secrets-set GIT_HTTP_TOKEN=ghp_... kdcube-setup --secrets-prompt # for LLM keysThe bundle config is applied immediately via Redis — no restart needed. Change the
refand re-run to switch bundle versions on the fly. -
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 thedefault_bundle_idfor 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
| Mode | bundles.yaml | Secret |
|---|---|---|
| SSH key | git@github.com:org/repo.git | SSH key mounted in container |
| HTTPS token | https://github.com/org/repo.git | GIT_HTTP_TOKEN secret |
Example Bundles
| Bundle ID | What It Shows | Key Features |
|---|---|---|
versatile@2026-03-31-13-36 | Reference bundle for SDK and integrations docs | ReAct 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.
Documentation Reference
SDK & Bundle
Deployment Descriptors
ReAct Agent [full folder →]
- flow-README.md
- react-context-README.md
- timeline-README.md
- react-turn-workspace-README.md
- artifact-discovery-README.md
- artifact-storage-README.md
- runtime-configuration-README.md
- source-pool-README.md
- turn-log-README.md
- turn-data-README.md
- external-exec-README.md
- event-blocks-README.md
- tool-call-blocks-README.md
- conversation-artifacts-README.md
- agent-workspace-collaboration-README.md
Coming Soon & Current Status
Here's the current state of platform capabilities and what's next on the roadmap:
| Feature | Status | Notes |
|---|---|---|
| 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. |