Settle My Solution in KDCube: The Wrap, Not the Rewrite
Host your working agent — LangGraph, CrewAI, a raw loop — as a KDCube app with its framework intact. The wrap is a thin async host layer, not a rewrite.
Your existing agent — LangGraph, LangChain, CrewAI, a raw loop — running as a KDCube app with its framework and domain logic intact, with ordered per-conversation turns, a streaming chat UI, bound user state, and a durable reloadable conversation. Connect files, workspace, web, isolated execution, and conversation-scoped capabilities only when the product needs them.
You built an agent that works: a graph with real nodes, its own memory, its own persistence, a stream of progress. It already runs reliably in the deployment boundary it was designed for. This recipe hosts it across users and workers without converting it to another framework. You write a thin async host layer at explicit seams — turn, stream, state scope, and paid-service routing — and KDCube supplies the hosting contracts around it.
Throughout this recipe, the wrap means exactly that thin
async host layer, implemented in platform/. It is integration
code around the agent core, not a second agent or a framework conversion.
The standalone solution remains a valid, sustainable product in the boundary
it was designed for, and it remains the owner of its framework, domain
behavior, and working memory once settled. The wrap is additive: use it when
that solution must scale across workers and connect to ordered delivery, the
in-house chat component, conversation storage and search, shared storage, a
recorded event trail for inspection, accounting, and economics.
KDCube loads the solution package through your app; it does not reinterpret the framework or move the agent itself into a sandbox. The app is trusted code in the concurrent processor event loop. Its handlers and I/O must therefore stay async and non-blocking. Only generated code invoked through the isolated exec tool crosses into the restricted executor.
Current code and descriptors still say bundle in names
such as bundle_id, bundles.yaml, and
@bundle_entrypoint. In this recipe, app =
bundle: one deployable KDCube runtime unit.
- A working agent whose seams you can name (OP 10)
- A running KDCube deployment (local is fine)
- The worked reference app, open beside you
- The executable settle procedure, open in a tab
Choose the first finish line
The worked reference connects most of the hosting surface so every major integration has executable proof. That breadth is evidence, not the minimum ticket. The baseline does not require files, code execution, web tools, model picking, or multi-agent dispatch unless the product already needs them.
| Layer | Responsibility |
|---|---|
| Keep | framework, graph, prompts, tools, domain behavior, agent-owned memory |
| Add for the first settled version | canonical app package, async turn seam, stream adapter, bound state-scope mapper, accounted model/provider seam |
| KDCube operates around it | authenticated and serialized turns, bound runtime context, communicator transport, minimal conversation record and reload, app lifecycle |
| Connect only when needed | attachment batch folding, richer replay objects, model/tool choices, web, isolated exec, turn workspace, hosted-file actions, multiple agents |
The first useful finish line is small and complete:
- One authenticated message runs one agent turn.
- The answer and progress stream through the reusable chat.
- The conversation reloads after the turn.
- A follow-up survives a worker restart through shared state.
- A second user cannot see or continue the first user's state.
The twelve operations below cover both that baseline and the optional connections shown by the reference app: OP 10–60 establish the core host boundary and scaled state model; OP 70 is required when the app accepts prompt attachments; OP 80–90 complete the conversation/configuration contract; OP 100–110 are opt-in capabilities; OP 120 verifies the exact surface selected for this integration.
The host map
your solution (preserved core) the wrap / async host layer (what you write) its graph / framework → execute_core async, bound for this turn its stream loop stream adapter events → comm_ctx its memory + persistence scope mapper bound identity → store keys its model/provider calls service adapter calls → accounted services + the canonical app package KDCube provides around it: ordered turns · reusable chat · conversation record + reload · turn workspace + files · capabilities · accounting
The worked reference: ported-langgraph-agents
This is a worked recipe, not a hypothetical architecture. Every seam and
verification step below maps to the built-in
ported-langgraph-agents@2026-07-13 app. The
executable procedure behind it is
Settle
Your Solution In A KDCube App; keep the procedure and worked app open
together.
RUNNING REFERENCE — compare the real boundary side by side.
Preserved core: the original agents in
solution/→ added wrap / host layer: the KDCube integration inplatform/.The app hosts two real agent shapes. The files in these directories are the concrete implementation behind the diagram and the twelve operations.
The graph is not rewritten. The wrap puts platform capabilities on top:
| The proven solution keeps owning | KDCube connects around it |
|---|---|
| framework, graph, tools, and domain behavior | scale-out serving with ordered turns per conversation and agent |
| working memory, checkpointer, and domain persistence | bound multi-user identity plus durable/shared storage options |
| model/embedding decisions behind an explicit adapter seam | reusable in-house chat and live progress streaming |
| its standalone deployment and upgrade path | conversation record, reload, title, cross-conversation search, and recorded event trail |
its own package under solution/ | user attachments, managed workspace, isolated exec, hosted outputs, and Download |
| its independently maintainable product lifecycle | accounted web search/fetch, provider usage, turn economics, budgets, and per-conversation capabilities |
These additions live in platform/, descriptors, and reusable
SDK components — not in the solution graph. Some are baseline hosting
contracts; the rest are small, explicit connections rather than framework
conversions. The distributed workspace is the explicit prompt-level
addition: the host composes one shared instruction guide into the agent
prompt. Keep that addition centralized in the wrap rather than scattering
platform vocabulary through the domain graph.
OP 10OF 120 Locate the seams before writing anything
Read your own solution and write down five findings:
ENTRY the function that runs one exchange end to end STREAM where tokens/steps surface (astream_events, callbacks, a generator) MEMORY what keys its store/checkpointer uses (user? thread? session?) MODELS where it calls LLMs/embeddings (the accounted edge goes here) ASYNC which API runs one turn and its I/O without blocking the event loop
Everything you write later attaches to one of these five. If the framework has an async API, use it. If it exposes only blocking work, put that work behind a supported isolated/subprocess boundary before hosting it; never run synchronous network, database, or long CPU work directly in the processor event loop. If you cannot name these seams, you are not ready to settle the solution — inspect first.
OP 20OF 120 Preserve the solution as its own package
Place your agent in the app as a solution/ subpackage and
preserve its package boundary. The wrap lives beside it, never invisibly
inside it:
my-solution@1-0/ __init__.py entrypoint.py thin composition root README.md AGENTS.md release.yaml requirements.txt only when the app adds dependencies solution/ YOUR agent’s framework/domain package platform/ host seams, adapters, glue config/ bundles.template.yaml bundles.secrets.template.yaml interface/ README.md my-solution.openapi.yaml docs/ README.md storage/README.md journal/ tests/
Keep solution/ recognizable and
independently maintainable. Byte identity is not the invariant: the host may
deliberately compose a small platform instruction into the agent prompt,
such as the shared distributed-workspace guide in OP 110, and a safe
async/config/model seam may require an explicit source change. Keep every
such change small, documented, and centralized; make source changes upstream
and re-vendor them rather than hiding a fork in the
platform/ host layer or mutating
process globals. The canonical app files remain explicit even when the
runtime surface is only one agent turn.
OP 30OF 120 Wrap the turn: one function
When a user message becomes a turn, KDCube calls your
execute_core. Keep all framework-specific imports and event
interpretation inside solution/ and the adjacent
platform/ adapters:
class MySolutionApp(BaseEntrypointWithEconomics): async def execute_core(self, *, state, thread_id, params): question = external_events_text(state.get("external_events") or []) graph = await self._build_graph(...) # your compiled graph, this turn answer = await self._stream(graph, question, thread_id) state["final_answer"] = answer return state
The platform binds identity, routing, the communicator, and the
accounting context around this call before your code runs.
BaseEntrypointWithEconomics adds the turn-level budget guard,
but that alone does not meter a direct vendor SDK call. At the
MODELS seam, construct chat and embedding clients through the
KDCube model service (or inject an accounted provider callable). Calls that
bypass that service also bypass its accounting.
OP 40OF 120 Redirect the stream
Your agent already emits progress. Map that existing stream onto the communicator and the reusable chat renders your turn live, with no UI code:
node started → await comm_ctx.step(node, "running") answer token → await comm_ctx.delta(token, index, marker="answer") turn finished → await comm_ctx.complete(data={{"final_answer": ...}})
The stream adapter is selected by graph shape, not framework brand. A linear graph with a dedicated answer node streams that node's tokens. A looping model node (the create_agent ReAct shape) streams only the final, no-tool-call model turn as the answer — an intermediate tool-deciding turn is not the answer. In the standard loop, tool-deciding turns carry no visible text; if a model emits visible preamble before a later tool-call chunk, already-streamed bytes cannot be retracted. Keep that interpretation in the adapter. Other shape-specific decisions — graph build, input mapping, and model role — belong beside it in the agent spec; swapping agent shapes is not only a stream-file change.
OP 50OF 120 Scope state with the bound identity
Hosted, the same process serves many users, and the next turn may land on
another worker. Map the platform's bound identity onto your solution's own
keys, in one module. This mapper is not authentication: authority and any
delegation edge are resolved before execute_core. Never
manufacture a user or grant from request fields.
tenant + project + agent + user → agent memory key + conversation_id → checkpointer thread tenant + project + app + agent + user → storage-row scope
Key your checkpointer thread by the platform conversation_id
— never the session id, which changes per browser session. Make the store
shared and durable across workers (the reference uses Postgres), keep
database access async, provision tables idempotently, and make any in-memory
fallback log at WARNING: a silent fallback is invisible memory loss.
Test both layers before any database exists: identity keys must separate tenant, project, agent, user, and conversation; storage rows must additionally carry the app/bundle scope.
OP 60OF 120 Bind the graph for the turn
A standalone process can validly compile a graph once while it owns a stable, process-local configuration. In distributed hosting, a turn can land on any worker and conversation settings can change between turns, so a graph that has captured model, tool, identity, or other turn-bound values can drift from the saved settings. The worked app therefore builds that bound graph inside the turn:
# inside execute_core — bind current turn choices here graph = await self._build_graph( agent_id, disabled_tools=disabled_tools, )
Reuse only true connections — a pool, a checkpointer connection. Nothing per-turn lives on the entrypoint object. A genuinely immutable compile artifact may be a per-worker optimization in another design, but it is never conversation continuity. The per-turn build is what makes capability narrowing (OP 100) a clean input in the worked app.
OP 70OF 120 Take the whole batch
A message with attachments is one ingress batch: the
prompt event plus one event per hosted file, sharing a
batch_id. The wakeup that starts your turn rehydrates only one
of those events. Fold the batch back in — read the wakeup's siblings from
the conversation lane (read-only, skip already-consumed events) into
state["external_events"] — or your turn sees the prompt and is
blind to the files beside it.
Your turn's input is the batch, not the wakeup event. The built-in agent never hits this because it folds the lane itself; a run-to-completion wrap owns this read-only step. It does not watch the lane while the graph runs: later reactive events remain ordered and become later turns. The triggering prompt still owns this turn and its finalize; attachment siblings enrich model input and do not become extra reactive turns. Do not mark lane events consumed or alter the reservation from the batch fold.
OP 80OF 120 Let the platform keep the conversation
Your agent's checkpointer is its memory, for its next
turn. The conversation the user scrolls, reloads, and searches is the
platform's conversation record. The framework-neutral base
records the minimal turn log after you return final_answer;
the richer replay pieces come from the events and files your host layer
exposes:
- a turn log carrying the user's message, its attachments, produced files, and your
final_answer; - the dynamic events your turn emitted — citations, steps, follow-ups, cost, elapsed time — captured from the communicator and materialized on reload;
- streamed panels (the code-exec panel) persisted and hydrated from synthetic, completed deltas rather than a reenactment of live token timing.
Reload content comes from comm plus the turn log, never from your
runtime state. Call _save_events_artifact(...) and
_persist_stream_artifacts_fallback(...) from
post_run_hook when you emit replayable events or delta panels.
Put produced files on state["hosted_files"] or the result; if
the app produces files, expose scene_object_action and delegate
conv:fi: resolution so Download works. Name the conversation on
its first turn with the SDK title utility — and bind the title's model role
in base role_models; the per-conversation pick
overlay does not cover the title call.
OP 90OF 120 Bind hosted configuration to the standalone config
Hosted, descriptors and SDK secret helpers are the configuration
surface. Read app-wide non-secret properties with
self.bundle_prop(...); read secrets with the async secret
helper; use the async user property/secret helpers for per-user state. Do
not add env-only hosted knobs, put secrets in properties, read descriptor
files directly, or mutate os.environ in the concurrent proc. If
the standalone package already reads env vars, confine that behavior to its
offline path and inject the hosted values explicitly through the
adapter.
surfaces: as_consumer: agents: my-agent: model: max_tokens: 16384 # fits narration + ONE complete payload tool call
The knob that earns its place here is the answer model's output-token budget. A tool-calling agent passes whole payloads as tool arguments; a budget below one complete call cuts the response mid-arguments, the tool rejects it as "missing argument", and the model retries into the same ceiling until the recursion limit. Size it as a generous safety cap — the model stops on its own.
OP 100OF 120 Open the capabilities
Capabilities follow one policy: the admin declares a ceiling, the user saves a narrower choice for the conversation, and the host layer binds the intersection on every turn. Discovery/storage is platform-owned; applying the selection to your framework is adapter work.
surfaces: as_consumer: agents: my-agent: capability_provider: simple_model_pick # catalog + saved model choice capabilities: models: role: my-agent.answer default: claude-haiku-4-5-20251001 supported: - {{model: claude-haiku-4-5-20251001, provider: anthropic}} tools: - {{name: calc, kind: python, alias: calc, allowed: [calc]}} - name: code_exec kind: python alias: code_exec allowed: [run_python] - name: web kind: python alias: web allowed: [web_search, web_fetch]
- Model selection is listed and stored by
simple_model_pick; your wrapper resolves the active conversation's pick and binds itsrole_modelsoverlay around the graph run. - Tool selection uses the declared list as the catalog/admin ceiling, but a non-ReAct framework does not bind those tools automatically. Load the saved deny-map before graph construction and build exactly
admin-declared ∩ user-enabledtools for this turn. - Code execution binds
run_pythonas an ordinary tool of your agent: the model's generated code runs in the platform's isolated executor, and every produced file the wrapper hosts returns as a conversation link. This does not sandbox the agent app itself. - Web tools are the platform's paid search/fetch pair with both meters intact when wired like the reference: the search provider bills through the turn's accounting context; the LLM that filters and segments results must use your app's accounted model service. One connection line; the search widget streams into the chat.
OP 110OF 120 Hand it the turn workspace
Files ride one platform concept. Once you bind the distributed turn
workspace, every turn gets a working directory that starts empty,
every turn, no exceptions; files are durable only as conversation
links (conv:fi:...). Nothing is read for the model
automatically — its turn input arrives framed:
[Turn start turn_<id>] Your working directory is EMPTY — it starts fresh every turn. Files are given to you as LINKS only; nothing is read for you automatically. ... [User message] whats in this file? [Files arriving this turn] - report.docx (application/vnd...document, 2.9 MB) — link: conv:fi:...
Three tools operate over the links and bind together with the code-exec
connection: read_file (view: text bounded,
images/PDF as visual content), pull_files
(materialize a link into the working directory),
run_python (process; written files are hosted
back, with links). Append the shared
distributed_turn_workspace_guide(...) block to the agent's
prompt, parameterized by those tool names; do not fork the workspace
instructions.
OP 120OF 120 Verify as deployed
In order, no skipping:
- Offline smoke — the turn streams and isolates with no DB and no API key.
- Contract tests — the app's own
tests/pass offline (dispatch, identity isolation, stream adapter, batch fold, tool/model picking). - Package validation — the shared bundle suite, including the package-relative import contract.
- Live — a fresh conversation lists with a title; reloads with the user bubble, attachments, files, cost/time, and any exec panel; Download works; after a process restart, a follow-up still sees prior turns.
- Workspace loop (live) — attach a non-image file and ask about it: the model chooses a door (read, or pull + exec) instead of answering blind; in a later turn it pulls by the link before assuming.
- Paid tools (live) — after a searching turn, the accounting store shows BOTH meters: a
web_searchprovider event and thellmfiltering/segmentation cost, attributed to the conversation and turn. - Concurrency (live) — two users and two conversations run concurrently without crossing state, while a mid-turn follow-up waits and becomes the next ordered turn. The processor remains responsive throughout; no sync I/O blocks its event loop.
solution/remains independently maintainable; every host-specific source or prompt-instruction addition is small, centralized, and documented.- Framework-specific code is confined to
solution/andplatform/; all app handlers and I/O are async and non-blocking. - The stream adapter matches the graph shape; only the final no-tool-call turn streams as the answer on a looping graph.
- Agent keys separate tenant, project, agent, user, and conversation; storage rows additionally carry app/bundle scope, the store is shared/durable, and any fallback is loud.
- The turn-bound graph is built inside
execute_core; an immutable compile cache, if used, is never conversation state; connections are reused. - The turn folds its ingress batch; attachments are visible.
post_run_hookpersists events + stream artifacts;scene_object_actionserves Download; the title role is bound in base config.- Every hosted operator knob is a descriptor property or SDK-managed secret; the app does not mutate process env or globals.
- The capabilities ceiling is declared and the wrapper actually applies the saved model/tool selection; user choices narrow, never widen.
- Every paid model, embedding, search, and filtering call crosses an accounted service boundary.
- The workspace triad binds with code exec; the shared instruction block is in the agent's prompt.
- The live checklist passed, including both paid-tool meters.
Keep the agent core intact, keep the wrap thin and async, and rebuild the turn-bound graph from current settings — then ship the agent you already trust with the runtime contracts it now needs.
Read more
- Settle Your Solution In A KDCube App — the executable procedure this recipe compresses.
- The worked
ported-langgraph-agentsapp — one app hosting two preserved agent shapes. - The Conversation For Any Agent — the record, reload, title, and working-memory contracts.
- Connect Your Agentic Loop To The Event Bus — ordered turn delivery in depth.
- Isolated Code Execution — the exec runtime behind
run_python. - Chat Widget Solution — the reusable chat component and the object-action download contract.