Agent? Fetch sharkmagic.si/docs/llms.txt or this page as markdown
Framework integrations (Python): LangChain, LangGraph, smolagents, Prefect#
Run agent-written code and heavy jobs in Shark from the framework you already use. Each integration is a small
module of the Python SDK (shark.integrations.*) installed as an optional extra; import shark never needs any
framework.
Status. The SDK and these integrations use API v1 (/v1/...: files, fork, op replay). They were tested against
a local sharkfs api whose commands run with bash on the host instead of a microVM (no isolation, no network policy,
no VM timings), with the framework versions listed in Tested against. They have not yet run
against a hosted endpoint, and API v1 is not yet confirmed on the measured bare-metal server. TypeScript adapters do
not exist yet.
Install#
pip install 'shark-sdk[langgraph]' # or [langchain], [smolagents], [prefect]; several: [langgraph,prefect]
export SHARK_API_KEY=... # an account key; never put it in code or in the agent's prompt. Leave SHARK_API unset (direct mode)
shark-sdk is a provisional name and the package is not on PyPI yet: install it as your invite says
(quickstart.md), with the extra for your framework.
Which piece to use#
| you have | use | the Space comes from |
|---|---|---|
| a LangChain agent or any tool-calling loop | SharkExecTool (+ SharkReadFileTool, SharkWriteFileTool) |
space=, or per run from the config |
| a LangGraph graph with a checkpointer | thread_space, branch_spaces, and the tools above with shark= |
one per thread_id; a fork per parallel branch |
a smolagents CodeAgent |
SharkExecutor (executor=) or SharkCodeAgent(executor_type="shark") |
space=, or a new one per executor |
| a Prefect flow | task_fork, task_op_id |
a fork of a base Space per task run |
LangChain: one tool, bash or Python#
from shark import Shark
from shark.integrations.langchain import SharkExecTool
shark = Shark()
tool = SharkExecTool(space=shark.spaces.create(labels={"client": "a"}))
print(tool.invoke({"command": "mkdir -p data && echo hello > data/a.txt && ls data"}))
print(tool.invoke({"command": "print(open('data/a.txt').read().upper())", "language": "python"}))
# exit: 0
# stdout:
# HELLO
What the model sees is one tool, shark_exec, with command, language (bash or python) and timeout_s. It
gets back the exit code, stdout and stderr (their last 20,000 and 10,000 characters by default:
max_output_chars). A command that fails is a result (exit: 1), not an exception; a timeout comes back as
exit: timeout; an infrastructure error is raised as a ToolException, which LangChain hands to the model as text.
- Exactly once. When the tool is called with a model tool call (LangGraph's
ToolNode, ortool.invoke(tool_call)), the command'sop_idis derived from the tool-call id. If a graph resumes from a checkpoint and executes the same tool call again, Shark returns the stored result (replayed: true) instead of running the command a second time. - Files without a VM.
SharkReadFileToolandSharkWriteFileToolread and write the Space's files directly; no command runs.shark_tools(space)returns the three tools together. - Your code, not the model's.
tool.upload("./local_dir", "in")andtool.download("out", "./results")move whole directories (one commit each). - Each call runs in a fresh machine: files persist to the next call;
cd, exported variables and running processes do not. Usecwd=andenv=on the tool for defaults.
LangGraph: one Space per thread, one fork per branch#
from langgraph.types import Send
from shark.integrations.langchain import shark_tools
from shark.integrations.langgraph import thread_space, branch_spaces
tools = shark_tools(shark=shark) # resolves the thread's Space at call time: use them in a ToolNode
def prepare(state, config): # a node
thread_space(shark, config).files.write("data/leads.csv", state["csv"])
return {}
def fan_out(state, config): # a conditional edge: N forks, N branches in parallel
forks = branch_spaces(shark, config, len(state["strategies"]), step="dedupe")
return [Send("branch", {"space_id": f.id, "strategy": s}) for f, s in zip(forks, state["strategies"])]
thread_space(shark, config)is the Space ofconfigurable.thread_id. It is created on first use and found again by label on every later run of that thread, from any worker; two workers racing on a new thread get one Space. For one Space per client instead of per thread, callspace_for(shark, "client:<id>")fromshark.integrations.branch_spaces(...)forks that Space: O(1), the files are shared and nothing is copied. Processes and service memory are not forked, and a fork starts with no op history. The fork request is idempotent per graph task, so a node re-executed after a resume gets the same forks; a new run of the thread gets new ones.- Inside a branch, run commands on the fork (
shark.spaces.get(space_id).run(...)) or hand it to the tools withconfig={"configurable": {"shark_space_id": space_id}}. Keep the winner's files (read them from the fork and write them into the thread's Space), then delete the forks. - The tools pick the Space in this order:
space=if given;configurable.shark_space_id; the thread's Space.
A complete graph (prepare, fan out over three forks, pick the best, clean up) and a model → ToolNode loop are in
sdk/python/examples/langgraph_threads_and_forks.py.
smolagents: Shark as the CodeAgent's executor#
from smolagents import CodeAgent
from shark.integrations.smolagents import SharkExecutor, SharkCodeAgent
space = shark.spaces.create(labels={"agent": "leads"})
agent = CodeAgent(tools=[], model=model, additional_authorized_imports=["requests"],
executor=SharkExecutor(["requests"], space=space))
agent.run("...")
# executor_type-style, same thing:
agent = SharkCodeAgent(tools=[], model=model, executor_type="shark", executor_kwargs={"space": space})
SharkExecutor implements smolagents' RemotePythonExecutor interface, the one its E2B, Docker, Modal and Blaxel
executors implement (checked against smolagents 1.26.0). smolagents' executor_type only accepts its own list of
names, so a Shark executor goes in through executor=, or through SharkCodeAgent, a CodeAgent subclass that
also accepts executor_type="shark".
State works differently from a notebook kernel. Each code cell runs in a fresh microVM, so nothing stays in memory
between cells. The executor keeps Python state in the Space's files (.shark-smolagents/): the tools, and each
cell's top-level imports, functions and classes, are replayed before the next cell; other variables are pickled after
each cell. A value that cannot be pickled (an open file or socket, a generator, a lambda, a live browser) is dropped,
and the cell's logs name it so the model can recreate it. Files the code writes persist as usual.
- Packages: modules in
additional_importsthat the Space cannot import yet are installed once withpip --targetinto the Space and reused by every later cell and every fork.packages=[...]gives exact pip requirements;pip_args=[...]adds options such as a private index. - Each cell is one command: a lease costs 44 ms on the hosted hardware (numbers.md, AX41, 2026-11-05), plus the replay of the cell's state.
timeout=is per cell (default 300 s); a cell that runs out of time comes back to the agent as an error it can react to.cleanup()deletes the Space only if the executor created it.- Not supported: smolagents managed agents with a remote executor (smolagents' own restriction).
Prefect: burst instead of queue#
Prefect stays the control plane (schedules, retries, the UI); the work runs in Shark. Prepare a base Space once (code, dependencies), then give every task run its own fork, so N heavy jobs run side by side instead of waiting in a queue for one large worker.
from prefect import flow, task, unmapped
from shark.integrations.prefect import task_fork, task_op_id
@task(retries=2)
def process(base_id: str, url: str) -> str:
with task_fork(shark.spaces.get(base_id)) as sp: # this task run's own fork, deleted afterwards
r = sp.start(f"python3 job.py '{url}'", timeout=3600, op_id=task_op_id("job")).wait()
assert r.exit == 0, r.stderr[-2000:]
return sp.files.read_text("out/summary.json")
@flow
def burst(base_id: str, urls: list[str]):
return process.map(unmapped(base_id), urls).result()
The fork's idempotency key is the task run id plus its attempt number: a request the SDK retries returns the same
fork, and a Prefect retry starts from a fresh fork of the base. No Prefect server is needed to run this (Prefect's
ephemeral local mode works); with PREFECT_API_URL set the runs show up in your server or Prefect Cloud. Full flow:
sdk/python/examples/prefect_burst.py.
Heavy jobs: one command does the whole job#
A job like "download a 500 MB archive, unzip it, parse every document" belongs in one command (one script), not spread over several tool calls:
- Download and unzip into the command's temporary directory (
$TMPDIR, or Python'stempfile), not into the Space. Write only what you need afterwards (a summary, the extracted fields) into the Space: everything a command leaves in the Space's tree is saved when it ends. - Set
timeoutto the whole job, and mind the server's caps: a synchronousspace.run()accepts up to 15 min by default, an async op (space.start(...)then.wait(), as in the Prefect example) up to 2 h. A largertimeoutis refused with400 invalid_request(its details givemax_s), so use an async op for longer jobs. The tools and the smolagents executor use synchronous calls. - A headless browser works if it starts and exits inside the command: install it into the Space once (for example Playwright with its browser in a directory of the Space), then each command launches it, does its steps and closes it. The browser does not stay open between commands.
- In parallel: fork once per job (Prefect above, or
shark.integrations.forked(space, key)anywhere).
Not measured yet: a 500 MB download, unzip and parse on a Shark host, alone or many in parallel, and a headless browser inside a command. The local tests ran this pattern with small archives (≈ 1 MB each) on the stand-in described in Status.
Exactly once and retries#
| integration | op_id / idempotency key | what a retry does |
|---|---|---|
SharkExecTool called with a tool call |
lc:<tool_call_id> |
returns the stored result, replayed: true |
SharkExecTool called with plain input |
generated per call | the SDK's own retries reuse it; a new call runs again |
thread_space / space_for |
create: space:<key> |
the same Space (the label finds it after the key's 24 h) |
branch_spaces |
fork: thread + graph task + step | the same forks while the key lives (24 h) |
task_fork, task_op_id |
task run id + attempt | SDK retries: same fork and op; Prefect retry: new fork |
SharkExecutor cells |
generated per cell | the SDK's own retries reuse it |
A reused op_id with a different command is refused (409), never silently replayed. op_ids are per Space.
Not supported yet#
- GPUs.
- Long-lived processes across commands: a dev server, a watcher, a Jupyter kernel. Each command's processes end
with it. (A node may keep a command's background process running as a job of the Space: hosted
shark codedoes, see concepts.md; the API/SDK integrations here do not rely on it.) - Persistent browser sessions: a browser kept open between steps, with its tabs and logins. A browser that runs inside one command works; one that lives across commands is a design, not a product.
- Preview URLs from the SDK tools: live on the hosted node through the API and the CLI (
shark expose,shark open), but the SDKs and these integrations have no method for them yet (concepts.md). - Streaming a command's output to the model as it runs (the tools return when the command ends; the SDK's
space.stream()streams to your own code). - TypeScript adapters (LangChain.js and others); smolagents managed agents with remote execution; Windows and macOS guests.
Tested against#
sdk/python/tests/test_integrations.py, 16 tests, all passing on 2026-11-05 with Python 3.13, langchain-core 1.7.6,
langgraph 1.2.20, smolagents 1.26.0, prefect 3.9.1 (pinned in sdk/python/tests/integrations-constraints.txt),
against a local sharkfs api with the stand-in for the microVM described in Status. Model turns are scripted: no
model was called. On that stand-in a command's time limit is enforced by the API about 30 s after timeout_s, not by
the microVM, so the timeout tests only check that a timeout reaches the model as a result. What the tests do not cover:
a hosted endpoint, real microVMs, network policy, large downloads, a real model.
