KDCube
← Recipes
KDCube Recipes · The Steel Rule

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.

16 July 2026RecipesHands-onThe Steel Rule
settlinglanggraphhosted agentconversationexecute_corestream adapterturn workspacepreserved solution

WHAT LEAVES THE SHOP

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.

YOU WILL NEED
  • 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.

LayerResponsibility
Keepframework, graph, prompts, tools, domain behavior, agent-owned memory
Add for the first settled versioncanonical app package, async turn seam, stream adapter, bound state-scope mapper, accounted model/provider seam
KDCube operates around itauthenticated and serialized turns, bound runtime context, communicator transport, minimal conversation record and reload, app lifecycle
Connect only when neededattachment 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:

  1. One authenticated message runs one agent turn.
  2. The answer and progress stream through the reusable chat.
  3. The conversation reloads after the turn.
  4. A follow-up survives a worker restart through shared state.
  5. 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

the.host.mapMAP
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 in platform/.

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 owningKDCube connects around it
framework, graph, tools, and domain behaviorscale-out serving with ordered turns per conversation and agent
working memory, checkpointer, and domain persistencebound multi-user identity plus durable/shared storage options
model/embedding decisions behind an explicit adapter seamreusable in-house chat and live progress streaming
its standalone deployment and upgrade pathconversation 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 lifecycleaccounted 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.

SETTLING · THE SAME CORE, BEFORE AND AFTERBEFORE · STANDALONEyour solutionframework · graph · prompts · toolsdomain behavior · working memoryUNCHANGEDits own stream loopits own persistence · checkpointerits own model / provider callsits own deployment boundaryVALID AND SUSTAINABLE ON ITS OWNSETTLEAFTER · SETTLED IN KDCUBETHE WRAP · platform/ · THIN · ASYNCyour solutionframework · graph · prompts · toolsdomain behavior · working memoryUNCHANGEDexecute_corestream adapterscope mapperservicesordered turnsreusable chatrecord + reloadworkspace + filescapabilitiesaccountingKDCUBE CONTRACTS AROUND ITCONNECT ONLY WHAT THE PRODUCT NEEDSTHE WRAP IS THE ONLY NEW CODE · AND IT IS THIN
The same core, before and after — the wrap is the only new code, and it is thin.

OP 10OF 120 Locate the seams before writing anything

Read your own solution and write down five findings:

the.five.seamsFINDINGS
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/LAYOUT
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:

platform/entrypoint.pyPYTHON
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:

platform/stream.pyMAPPING
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.

platform/scope.pyKEYS
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.

CHECK · THE INSPECTOR’S STAMP

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:

platform/entrypoint.pyPYTHON
# 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.

bundles.yamlYAML
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.

bundles.yamlYAML
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 its role_models overlay 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-enabled tools for this turn.
  • Code execution binds run_python as 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.inputFRAMING
[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:

  1. Offline smoke — the turn streams and isolates with no DB and no API key.
  2. Contract tests — the app's own tests/ pass offline (dispatch, identity isolation, stream adapter, batch fold, tool/model picking).
  3. Package validation — the shared bundle suite, including the package-relative import contract.
  4. 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.
  5. 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.
  6. Paid tools (live) — after a searching turn, the accounting store shows BOTH meters: a web_search provider event and the llm filtering/segmentation cost, attributed to the conversation and turn.
  7. 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.
FINAL INSPECTION · DONE MEANS
  • solution/ remains independently maintainable; every host-specific source or prompt-instruction addition is small, centralized, and documented.
  • Framework-specific code is confined to solution/ and platform/; 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_hook persists events + stream artifacts; scene_object_action serves 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

From the blog

KDCube Recipe
№ 2026-07-16 · kdcube.tech