LaufwerkLaufwerk
API reference

Session

All options for Session.open, Session.run, ask, exec and close, including prompts and structured results.

A session combines an agent's conversation state with its execution environment. A workspace supplies files; multiple sessions can share those files without sharing conversation history. Import Session and SessionError from @laufwerk/sdk.

Session.open(options)

Creates a handle for a continuous session. This call itself is lazy: the underlying Harness session is created or resumed by ask or exec.

PropertyTypeRequiredMeaning
keystringYesStable session key within the run; use a nonempty distinct key
agentHarnessAgentYesConfigured agent
workspaceWorkspaceReferenceWith accessReference returned by Workspace.open
access"read-only" | "read-write"With workspaceAccess for every turn and command in this session

Supply both workspace and access, or neither. Returns an Effect containing SessionHandle<Agent> without a workspace, or WorkspaceSessionHandle<Agent> with one. Both expose key, ask and close; only the latter exposes exec. Model, tools, instructions, environment and timeouts are not Session.open options.

session.ask(options)

Runs the next model turn in the same conversation, resuming the prior Harness state.

PropertyTypeRequiredMeaning
keystringYesStable, nonempty turn key within this session
promptstring | UserModelMessageYesText or a user message; see prompt types below
outputSchema.Schema<A, I, never>NoEffect Schema for JSON output; returns decoded A

Returns an Effect of string, or A with output. Failures use SessionError. Concurrent calls on the same continuous session are serialized. A new key means a new turn; repeating a completed key reuses its recorded result.

Prompt types

HarnessAgentPrompt (also HarnessV1Prompt) is a string or AI SDK UserModelMessage. The object form has required role: "user" and content: string | Array<TextPart | ImagePart | FilePart>, plus optional providerOptions (provider-name keys mapped to provider-specific options).

Content partProperties
Texttype: "text", text: string, optional providerOptions
Imagetype: "image", image: DataContent | URL, optional mediaType: string, optional providerOptions
Filetype: "file", data: DataContent | URL, mediaType: string, optional filename: string, optional providerOptions

DataContent accepts a base64 string, Uint8Array, ArrayBuffer or Buffer. The table describes the upstream message type. Both the pinned Codex and Claude Code adapters accept text only: image and file parts throw HarnessCapabilityUnsupportedError, surfaced through SessionError. They are not supported attachment inputs in this release. Text parts are joined with blank lines; providerOptions on these messages are not forwarded by their text extractors. You cannot pass a whole chat-message array or a system message as prompt. Put persistent instructions on the agent.

text-prompt.ts
import type { HarnessAgentPrompt } from "@laufwerk/sdk/harness";

export const prompt = {
  role: "user",
  content: [
    { type: "text", text: "Review the proposed change." },
    { type: "text", text: "Focus on incorrect behavior and missing tests." },
  ],
} satisfies HarnessAgentPrompt;

Structured output

Laufwerk converts output to JSON Schema prompt guidance, then parses JSON candidates from the response and validates them with the Effect Schema. The schema must be describable as JSON Schema and require no additional services. The return is the decoded type A, which can differ from encoded type I.

This is prompt guidance plus validation, not constrained generation. Undescribable schemas fail before inference; missing/invalid JSON or schema mismatches fail after the turn. Laufwerk does not automatically repeat that turn to repair JSON. See Types and schemas for optional fields, literals and the difference between encoded and decoded values.

session.exec(options)

Runs an exact shell command through this session's execution environment with the workspace path as its working directory. It does not call the model or append the command output to the conversation. Pass the output to a later ask if needed.

PropertyTypeRequiredMeaning
keystringYesNonempty command identity within this session
commandstringYesNonempty shell command

Returns an Effect of SessionExecResult:

Result propertyTypeMeaning
exitCodenumberProcess exit status; check it explicitly
stdoutstringCaptured standard output
stderrstringCaptured standard error

A nonzero exit code is a returned command result, not automatically a failed Effect. Setup, transport, execution or detach failures can produce SessionError. There are no cwd, env, timeout, shell or abortSignal options here. Separate commands do not promise a persistent shell: use cd subdir && command within one command, or an executable script, rather than relying on an earlier shell's cd.

Completed results are recorded. An interruption between an external side effect and its recording can leave an uncertain operation requiring recovery; changing the key to rerun it is not proof the first command did nothing.

session.close()

Takes no arguments. Returns an Effect of void or fails with SessionError. Closes the continuous session and destroys its Harness execution resources. It does not write the workspace back. Closing an unused handle needs no environment allocation; closing an already closed session is harmless.

Close explicitly after the intended work, including handled failure paths. Do not put close in a generic scope finalizer: workflow suspension can run finalizers even though the conversation must resume later. A closed session cannot accept new work under the same key.

Cleanup can also fail. In particular, a failed first turn may leave no saved resume state for close to use; a still-active turn cannot be closed. Preserve the original failure when reporting cleanup failure, and inspect the run's resources before assuming they were released.

Session.run(options)

Runs a single turn and manages its session lifecycle. Returns text or a decoded structured result, rather than a reusable handle.

PropertyTypeRequired
keystringYes
agentHarnessAgentYes
promptstring | UserModelMessageYes
workspaceWorkspaceReferenceWith access
access"read-only" | "read-write"With workspace
outputSchema.Schema<A, I, never>No

The prompt, access and output rules are the same as above. Use open when a later question needs the previous conversation or a command must use that session's environment. Use distinct keys for distinct single-turn tasks.

Example: implement, test, return a report

Save this complete workflow as laufwerk/workflows/implement/workflow.ts in an initialized consumer. It assumes a Bun project with a committed bun.lock, a working Docker installation and configured Codex subscription credentials. The dependency installation is explicit because copied workspaces omit node_modules. Its file changes stay in the working copy; this example does not write them back.

session-example.ts
import { Effect, Schema } from "effect";
import { Workflow } from "@effect/workflow";
import { Session, Workspace } from "@laufwerk/sdk";
import { createDockerExecution } from "@laufwerk/execution";
import { createCodex } from "@laufwerk/sdk/harness/codex";

const execution = createDockerExecution({ credentials: "codex-subscription" });
const agent = execution.agent({
  harness: createCodex(),
  instructions: "Make small changes and verify them.",
});

const Report = Schema.Struct({ summary: Schema.String });
export const workflow = Workflow.make({
  name: "implement",
  payload: { requestId: Schema.String, request: Schema.String },
  success: Report,
  error: Schema.String,
  idempotencyKey: input => input.requestId,
});

export const layer = workflow.toLayer(input => Effect.gen(function* () {
  const workspace = yield* Workspace.open({ source: ".", execution });
  const session = yield* Session.open({
    key: "implementer", agent, workspace, access: "read-write",
  });
  const result = yield* Effect.gen(function* () {
    const install = yield* session.exec({ key: "install", command: "bun install --frozen-lockfile" });
    if (install.exitCode !== 0) return yield* Effect.fail(`Dependency installation failed:\n${install.stdout}\n${install.stderr}`);
    yield* session.ask({ key: "implement", prompt: input.request });
    const tests = yield* session.exec({ key: "test", command: "bun test" });
    if (tests.exitCode !== 0) return yield* Effect.fail(`Tests failed:\n${tests.stdout}\n${tests.stderr}`);
    return yield* session.ask({
      key: "report",
      prompt: `Tests passed. Summarize the changes. Test output:\n${tests.stdout}`,
      output: Report,
    });
  }).pipe(Effect.either);
  const closed = yield* session.close().pipe(Effect.either);
  if (result._tag === "Left") {
    const cleanup = closed._tag === "Left" ? `\nCleanup also failed: ${closed.left.message}` : "";
    return yield* Effect.fail(`${String(result.left)}${cleanup}`);
  }
  if (closed._tag === "Left") return yield* Effect.fail(closed.left.message);
  return result.right;
}).pipe(Effect.mapError(String)));
bunx laufwerk@0.0.1-alpha.13 run implement --input '{"requestId":"fix-001","request":"Fix the failing tests and explain the change."}'

This handles typed failures and closes at the explicit boundary. Defects and process crashes still require the runtime's inspection/recovery path.

Shared workspace rules

Sessions share files, not conversation or all environment state. Read-only operations may overlap; read-write turns and commands hold exclusive workspace access, blocking readers as well as writers. The lock is for the operation, not the entire lifetime of an open handle. Local execution rejects enforced read-only access. See Workspace and Execution.

On this page