@jund/core
v0.1.0
Published
Platform-agnostic headless coding agent.
Maintainers
Readme
jund
Platform-agnostic coding agent primitive inspired by opencode and pi.
jund accepts a FileSystem interface and an exec function instead of
touching the host directly, so the same agent session works against a real
disk, an in-memory FS, or a virtual sandbox like
just-bash. The only runtime
dependency is zod, so it runs anywhere (Node, Bun, Deno, edge workers,
the browser).
Status: Experimental - the public API is not stable and may change without notice.
Install
npm install @jund/coreProvider adapters are optional:
npm install ai # for @jund/core/ai-sdk
npm install @openrouter/sdk # for @jund/core/openrouter-sdk
npm install @tanstack/ai # for @jund/core/tanstack-aiQuick start
import Database from "better-sqlite3";
import { Bash, ReadWriteFs } from "just-bash";
import { anthropic } from "@ai-sdk/anthropic";
import { createAISDKProvider } from "@jund/core/ai-sdk";
import { createSession, getAssistantText, BetterSqlite3Storage } from "@jund/core";
const fs = new ReadWriteFs({ root: "./my-project" });
const shell = new Bash({ fs, cwd: "/" });
const storage = new BetterSqlite3Storage(new Database("sessions.db"));
const llm = createAISDKProvider({
model: anthropic("claude-opus-4-6"),
id: "claude-opus-4-6",
contextLimit: 200_000,
outputLimit: 64_000,
capabilities: { reasoning: true, toolCalls: true, images: true },
});
const session = await createSession({
llm,
workdir: "/",
env: { fs, shell },
storage,
onEvent: (event) => console.log(event.type),
});
const reply = await session.prompt("Add a health-check endpoint to the server.");
console.log(getAssistantText(reply));Entry points
| Import | Description |
| --------------------------- | ---------------------------------------------------- |
| @jund/core | Core session, tools, types, events, context pipeline |
| @jund/core/ai-sdk | Adapter for the Vercel AI SDK (ai) |
| @jund/core/openrouter-sdk | Adapter for @openrouter/sdk |
| @jund/core/tanstack-ai | Adapter for @tanstack/ai |
Core API
createSession(options): Promise<Session>
Creates a stateful agent session. Key options:
| Option | Type | Description |
| --------------- | ------------------------------- | -------------------------------------------------- |
| llm | LLMProviderWithModel | LLM backend + model metadata (use an adapter) |
| workdir | string | Root working directory exposed to tools |
| env | Environment | { fs: FileSystem; shell: ShellOps } |
| tools | ToolDef[] | Additional tools beyond the built-ins |
| systemPrompt | string | Prepended to the default system prompt |
| toolExecution | "parallel" \| "sequential" | How tool calls in a single step run |
| onEvent | EventHandler | Stream of AgentEvents (deltas, tool calls, etc.) |
| compaction | false \| CompactionOptions | Context-window compaction strategy |
| storage | SessionStorageDriver | Persist turns and messages |
| retry | { maxAttempts?, maxDelayMs? } | LLM retry policy |
Lifecycle hooks: beforeInput, beforeToolCall, afterToolCall, beforeLLMCall, beforePrompt, beforeCompaction, beforeBranch, transformContext.
Session
interface Session {
readonly id: string;
readonly parentId: string | undefined;
readonly branchedFromMessageId: string | undefined;
readonly model: ModelInfo;
readonly workdir: string;
readonly isStreaming: boolean;
prompt(input: string | UserPart[]): Promise<AssistantMessage>;
cancel(): void;
messages(): Message[];
branchFrom(messageId: string, options?: BranchOptions): Promise<Session>;
addTool(tool: ToolDef): void;
removeTool(id: string): void;
setTools(tools: ToolDef[]): void;
compact(): Promise<boolean>;
setLLM(llm: LLMProviderWithModel): Promise<void>;
// + setters for every lifecycle hook
}resumeSession(sessionId, options): Promise<Session>
Resumes an existing persisted session. Throws if the session does not exist.
Runtime dependencies (llm, env, storage) are required; workdir and
defaultAgent default from the stored session state.
const session = await resumeSession("session-123", {
llm,
env,
storage,
});Session discovery
Query stored sessions without creating a live Session:
const all = await listSessions(storage);
const branches = await listBranches(storage, parentSessionId);
const info = await getSessionInfo(storage, sessionId);Each returns SessionInfo objects with id, workdir, parentSessionId,
branchedFromMessageId, model, agent, and timestamps.
Built-in tools
| Tool | Description |
| ----------- | -------------------------------------- |
| readTool | Read file contents |
| writeTool | Write / create files |
| editTool | Apply targeted edits to existing files |
| bashTool | Execute shell commands |
| taskTool | Spawn sub-agent sessions |
All tools are registered by default; override with tools or setTools.
LLM provider adapters
Each adapter exports a create*Provider(options): LLMProviderWithModel factory plus lower-level conversion helpers.
Each adapter factory accepts model metadata alongside the provider-specific
model and returns a LLMProviderWithModel — pass it straight to createSession:
// Vercel AI SDK
import { createAISDKProvider } from "@jund/core/ai-sdk";
const llm = createAISDKProvider({
model: anthropic("claude-opus-4-6"),
id: "claude-opus-4-6",
contextLimit: 200_000,
outputLimit: 64_000,
capabilities: { reasoning: true, toolCalls: true, images: true },
});
// OpenRouter
import { createOpenRouterProvider } from "@jund/core/openrouter-sdk";
const llm = createOpenRouterProvider({
client: openRouterClient,
model: "anthropic/claude-opus-4-6",
id: "claude-opus-4-6",
contextLimit: 200_000,
outputLimit: 64_000,
capabilities: { reasoning: true, toolCalls: true, images: true },
});
// TanStack AI
import { createTanStackTextProvider } from "@jund/core/tanstack-ai";
const llm = createTanStackTextProvider({
adapter: myAdapter,
id: "claude-opus-4-6",
contextLimit: 128_000,
outputLimit: 32_000,
capabilities: { reasoning: false, toolCalls: true, images: false },
});Environment
Tools interact with the host through the Environment interface:
interface Environment {
fs: FileSystem; // readFile, writeFile, mkdir, exists, stat, readdir
shell: ShellOps; // exec(command, options?) → { stdout, stderr, exitCode }
}Events
Subscribe via onEvent to receive a stream of typed AgentEvents:
turn.start, turn.end, message.created, text.delta, reasoning.delta, tool.start, tool.update, tool.complete, tool.error, step.finish, compaction, retry, tools.changed, error, done.
Context compaction
When conversation history approaches the model's context limit, jund automatically summarises older messages. Configure the threshold, strategy, or disable entirely:
createSession({
// ...
compaction: { threshold: 0.8, strategy: myStrategy },
// or: compaction: false,
});You can also trigger compaction manually at any time (e.g. from a UI button):
const didCompact = await session.compact();Returns true if history was compacted, false if compaction is disabled or was cancelled by the beforeCompaction hook.
Persistence
Pass any SessionStorageDriver as storage - createSession handles the
rest internally (see the quick start above). Five drivers ship out of the box:
| Driver | Backend |
| ---------------------------- | --------------------------------- |
| MemoryStorage | In-memory (testing / ephemeral) |
| BetterSqlite3Storage | better-sqlite3 (Node) |
| BunSqliteStorage | bun:sqlite (Bun) |
| DurableObjectSqliteStorage | Cloudflare Durable Objects SQLite |
| PgStorage | PostgreSQL via pg |
Implement SessionStorageDriver to bring your own backend.
Resume and branching
Resuming a session
Use resumeSession to pick up where a previous session left off. The session
must already exist in storage:
import { resumeSession } from "@jund/core";
const session = await resumeSession("session-123", {
llm,
env: { fs, shell },
storage,
// workdir and defaultAgent default from stored session
});
// Conversation history is restored automatically
const reply = await session.prompt("Continue where we left off.");createSession with an explicit sessionId will throw if a session with that
ID already exists - use resumeSession when you intend to continue an existing
conversation.
Branching
Create an alternate conversation timeline from any message:
const branch = await session.branchFrom(messageId);
// branch.parentId === session.id
// branch.messages() contains history up to and including messageId
const reply = await branch.prompt("Try a different approach.");Branches are independent sessions - prompting the parent or child does not
affect the other. Branch metadata is accessible via session.parentId and
session.branchedFromMessageId.
Discovering sessions and branches
import { listSessions, listBranches, getSessionInfo } from "@jund/core";
const all = await listSessions(storage);
const branches = await listBranches(storage, session.id);
const info = await getSessionInfo(storage, "session-123");Lower-level primitives
For custom orchestration loops without the full Session:
callLLM(options)- stream a single LLM call with retry.processTurn(options)- one model step with tool execution.buildSystemPrompt(options)- assemble the system prompt.ToolRegistry- manage tool definitions.estimateTokens/shouldCompact/runContextPipeline- context utilities.
License
MIT
