npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zeitlich

v0.2.28

Published

[EXPERIMENTAL] An opinionated AI agent implementation for Temporal

Readme

npm version npm downloads Ask DeepWiki

Zeitlich

⚠️ Experimental Beta: This library is under active development. APIs and interfaces may change between versions. Use in production at your own risk.

Durable AI Agents for Temporal

Zeitlich is an opinionated framework for building reliable, stateful AI agents using Temporal. It provides the building blocks for creating agents that can survive crashes, handle long-running tasks, and coordinate with other agents—all with full type safety.

Why Zeitlich?

Building production AI agents is hard. Agents need to:

  • Survive failures — What happens when your agent crashes mid-task?
  • Handle long-running work — Some tasks take hours or days
  • Coordinate — Multiple agents often need to work together
  • Maintain state — Conversation history, tool results, workflow state

Temporal solves these problems for workflows. Zeitlich brings these guarantees to AI agents.

Features

  • Durable execution — Agent state survives process restarts and failures
  • Thread management — Redis-backed conversation storage with automatic persistence
  • Type-safe tools — Define tools with Zod schemas, get full TypeScript inference
  • Lifecycle hooks — Pre/post tool execution, session start/end
  • Subagent support — Spawn child agents as Temporal child workflows
  • Skills — First-class agentskills.io support with progressive disclosure
  • Filesystem utilities — In-memory or custom providers for file operations
  • Model flexibility — Framework-agnostic model invocation with adapters for LangChain, Vercel AI SDK, or provider-specific SDKs

LLM Integration

Zeitlich's core is framework-agnostic — it defines generic interfaces (ModelInvoker, ThreadOps, MessageContent) that work with any LLM SDK. You choose a thread adapter (for conversation storage and model invocation) and a sandbox adapter (for filesystem operations), then wire them together.

Thread Adapters

A thread adapter bundles two concerns:

  1. Thread management — Storing and retrieving conversation messages in Redis
  2. Model invocation — Calling the LLM with the conversation history and tools

Each adapter exposes the same shape: createActivities(scope) for Temporal worker registration, and an invoker for model calls. Pick the one matching your preferred SDK:

| Adapter | Import | SDK | |---------|--------|-----| | LangChain | zeitlich/adapters/thread/langchain | @langchain/core + any provider package | | Google GenAI | zeitlich/adapters/thread/google-genai | @google/genai |

Vercel AI SDK and other provider-specific adapters can be built by implementing the ThreadOps and ModelInvoker interfaces.

Sandbox Adapters

A sandbox adapter provides filesystem access for tools like Bash, Read, Write, and Edit:

| Adapter | Import | Use case | |---------|--------|----------| | In-memory | zeitlich/adapters/sandbox/inmemory | Tests and lightweight agents | | Virtual | zeitlich/adapters/sandbox/virtual | Custom resolvers with path-only ops | | Daytona | zeitlich/adapters/sandbox/daytona | Remote Daytona workspaces | | E2B | zeitlich/adapters/sandbox/e2b | E2B cloud sandboxes | | Bedrock | zeitlich/adapters/sandbox/bedrock | AWS Bedrock AgentCore Code Interpreter |

Example: LangChain Adapter

import { ChatAnthropic } from "@langchain/anthropic";
import { createLangChainAdapter } from "zeitlich/adapters/thread/langchain";
import { createRunAgentActivity } from "zeitlich";

const adapter = createLangChainAdapter({
  redis,
  model: new ChatAnthropic({ model: "claude-sonnet-4-20250514" }),
});

export function createActivities(client: WorkflowClient) {
  return {
    ...adapter.createActivities("myAgentWorkflow"),
    runAgent: createRunAgentActivity(client, adapter.invoker),
  };
}

All adapters follow the same pattern — createActivities(scope) for worker registration and invoker for model calls.

Installation

npm install zeitlich ioredis

Peer dependencies:

  • ioredis >= 5.0.0
  • @langchain/core >= 1.0.0 (optional — only when using the LangChain adapter)
  • @google/genai >= 1.0.0 (optional — only when using the Google GenAI adapter)
  • @aws-sdk/client-bedrock-agentcore >= 3.900.0 (optional — only when using the Bedrock adapter)

Required infrastructure:

  • Temporal server (local dev: temporal server start-dev)
  • Redis instance

Import Paths

Zeitlich uses separate entry points for workflow-side and activity-side code:

// In workflow files — no external dependencies (Redis, LLM SDKs, etc.)
import {
  createSession,
  createAgentStateManager,
  defineTool,
  bashTool,
} from "zeitlich/workflow";

// Adapter workflow proxies (auto-scoped to current workflow)
import { proxyLangChainThreadOps } from "zeitlich/adapters/thread/langchain/workflow";
import { proxyInMemorySandboxOps } from "zeitlich/adapters/sandbox/inmemory/workflow";

// In activity files and worker setup — framework-agnostic core
import {
  createRunAgentActivity,
  SandboxManager,
  withSandbox,
  bashHandler,
} from "zeitlich";

// Thread adapter — activity-side
import { createLangChainAdapter } from "zeitlich/adapters/thread/langchain";

Entry points:

  • zeitlich/workflow — Pure TypeScript, safe for Temporal's V8 sandbox
  • zeitlich/adapters/*/workflow — Workflow-side proxies that auto-scope activities to the current workflow
  • zeitlich — Activity-side utilities (Redis, filesystem), framework-agnostic
  • zeitlich/adapters/thread/* — Activity-side adapters (thread management + model invocation)
  • zeitlich/adapters/sandbox/* — Activity-side sandbox providers

Examples

Runnable examples (worker, client, workflows) are in a separate repo: zeitlich-examples.

Quick Start

1. Define Your Tools

import { z } from "zod";
import type { ToolDefinition } from "zeitlich/workflow";

export const searchTool: ToolDefinition<"Search", typeof searchSchema> = {
  name: "Search",
  description: "Search for information",
  schema: z.object({
    query: z.string().describe("The search query"),
  }),
};

2. Create the Workflow

The workflow wires together a thread adapter (for conversation storage / model calls) and a sandbox adapter (for filesystem tools). Both are pluggable — swap the proxy import to switch providers.

import { proxyActivities, workflowInfo } from "@temporalio/workflow";
import {
  createAgentStateManager,
  createSession,
  defineWorkflow,
  askUserQuestionTool,
  bashTool,
  defineTool,
} from "zeitlich/workflow";
import { searchTool } from "./tools";
import type { MyActivities } from "./activities";

import { proxyLangChainThreadOps } from "zeitlich/adapters/thread/langchain/workflow";
import { proxyInMemorySandboxOps } from "zeitlich/adapters/sandbox/inmemory/workflow";

const {
  runAgentActivity,
  searchHandlerActivity,
  bashHandlerActivity,
  askUserQuestionHandlerActivity,
} = proxyActivities<MyActivities>({
  startToCloseTimeout: "30m",
  retry: {
    maximumAttempts: 6,
    initialInterval: "5s",
    maximumInterval: "15m",
    backoffCoefficient: 4,
  },
  heartbeatTimeout: "5m",
});

export const myAgentWorkflow = defineWorkflow(
  { name: "myAgentWorkflow" },
  async ({ prompt }: { prompt: string }, sessionInput) => {
    const { runId } = workflowInfo();

    const stateManager = createAgentStateManager({
      initialState: {
        systemPrompt: "You are a helpful assistant.",
      },
      agentName: "my-agent",
    });

    const session = await createSession({
      agentName: "my-agent",
      maxTurns: 20,
      thread: { mode: "new", threadId: runId },
      threadOps: proxyLangChainThreadOps(),
      sandboxOps: proxyInMemorySandboxOps(),
      runAgent: runAgentActivity,
      buildContextMessage: () => [{ type: "text", text: prompt }],
      tools: {
        Search: defineTool({
          ...searchTool,
          handler: searchHandlerActivity,
        }),
        AskUserQuestion: defineTool({
          ...askUserQuestionTool,
          handler: askUserQuestionHandlerActivity,
          hooks: {
            onPostToolUse: () => {
              stateManager.waitForInput();
            },
          },
        }),
        Bash: defineTool({
          ...bashTool,
          handler: bashHandlerActivity,
        }),
      },
      ...sessionInput,
    });

    const result = await session.runSession({ stateManager });
    return result;
  }
);

3. Create Activities

Activities are factory functions that receive infrastructure dependencies (redis, client). The thread adapter and sandbox provider are configured here — swap imports to change LLM or sandbox backend.

import type Redis from "ioredis";
import type { WorkflowClient } from "@temporalio/client";
import { ChatAnthropic } from "@langchain/anthropic";
import {
  SandboxManager,
  withSandbox,
  bashHandler,
  createAskUserQuestionHandler,
  createRunAgentActivity,
} from "zeitlich";
import { InMemorySandboxProvider } from "zeitlich/adapters/sandbox/inmemory";

import { createLangChainAdapter } from "zeitlich/adapters/thread/langchain";

const sandboxProvider = new InMemorySandboxProvider();
const sandboxManager = new SandboxManager(sandboxProvider);

export const createActivities = ({
  redis,
  client,
}: {
  redis: Redis;
  client: WorkflowClient;
}) => {
  const adapter = createLangChainAdapter({
    redis,
    model: new ChatAnthropic({
      model: "claude-sonnet-4-20250514",
      maxTokens: 4096,
    }),
  });

  return {
    ...adapter.createActivities("myAgentWorkflow"),
    ...sandboxManager.createActivities("myAgentWorkflow"),
    runAgentActivity: createRunAgentActivity(client, adapter.invoker),
    searchHandlerActivity: async (args: { query: string }) => ({
      toolResponse: JSON.stringify(await performSearch(args.query)),
      data: null,
    }),
    bashHandlerActivity: withSandbox(sandboxManager, bashHandler),
    askUserQuestionHandlerActivity: createAskUserQuestionHandler(),
  };
};

export type MyActivities = ReturnType<typeof createActivities>;

4. Set Up the Worker

import { Worker, NativeConnection } from "@temporalio/worker";
import Redis from "ioredis";
import { fileURLToPath } from "node:url";
import { createActivities } from "./activities";

async function run() {
  const connection = await NativeConnection.connect({
    address: "localhost:7233",
  });
  const redis = new Redis({ host: "localhost", port: 6379 });

  const worker = await Worker.create({
    connection,
    taskQueue: "my-agent",
    workflowsPath: fileURLToPath(new URL("./workflows.ts", import.meta.url)),
    activities: createActivities({ redis, client }),
  });

  await worker.run();
}

Core Concepts

Agent State Manager

Manages workflow state with automatic versioning and status tracking. Requires agentName to register Temporal query/update handlers, and accepts an optional initialState for system prompt and custom fields:

import { createAgentStateManager } from "zeitlich/workflow";

const stateManager = createAgentStateManager({
  initialState: {
    systemPrompt: "You are a helpful assistant.",
    customField: "value",
  },
  agentName: "my-agent",
});

// State operations
stateManager.set("customField", "new value");
stateManager.get("customField"); // Get current value
stateManager.complete(); // Mark as COMPLETED
stateManager.waitForInput(); // Mark as WAITING_FOR_INPUT
stateManager.isRunning(); // Check if RUNNING
stateManager.isTerminal(); // Check if COMPLETED/FAILED/CANCELLED

Tools with Handlers

Define tools with their handlers inline in createSession:

import { z } from "zod";
import type { ToolDefinition } from "zeitlich/workflow";

// Define tool schema
const searchTool: ToolDefinition<"Search", typeof searchSchema> = {
  name: "Search",
  description: "Search for information",
  schema: z.object({ query: z.string() }),
};

// In workflow - combine tool definition with handler using defineTool()
const session = await createSession({
  // ... other config
  tools: {
    Search: defineTool({
      ...searchTool,
      handler: handleSearchResult, // Activity that implements the tool
    }),
  },
});

Lifecycle Hooks

Add hooks for tool execution and session lifecycle:

const session = await createSession({
  // ... other config
  hooks: {
    onPreToolUse: ({ toolCall }) => {
      console.log(`Executing ${toolCall.name}`);
      return {}; // Can return { skip: true } or { modifiedArgs: {...} }
    },
    onPostToolUse: ({ toolCall, result, durationMs }) => {
      console.log(`${toolCall.name} completed in ${durationMs}ms`);
      // Access stateManager here to update state based on results
    },
    onPostToolUseFailure: ({ toolCall, error }) => {
      return { fallbackContent: "Tool failed, please try again" };
    },
    onSessionStart: ({ threadId, agentName }) => {
      console.log(`Session started: ${agentName}`);
    },
    onSessionEnd: ({ exitReason, turns }) => {
      console.log(`Session ended: ${exitReason} after ${turns} turns`);
    },
  },
});

Subagents

Spawn child agents as Temporal child workflows. Use defineSubagentWorkflow to define the workflow with its metadata once, then defineSubagent to register it in the parent:

// researcher.workflow.ts
import { proxyActivities } from "@temporalio/workflow";
import {
  createAgentStateManager,
  createSession,
  defineSubagentWorkflow,
} from "zeitlich/workflow";
import { proxyLangChainThreadOps } from "zeitlich/adapters/thread/langchain/workflow";
import type { createResearcherActivities } from "./activities";

const { runResearcherActivity } = proxyActivities<
  ReturnType<typeof createResearcherActivities>
>({ startToCloseTimeout: "30m", heartbeatTimeout: "5m" });

// Define the workflow — name, description (and optional resultSchema) live here
export const researcherWorkflow = defineSubagentWorkflow(
  {
    name: "Researcher",
    description: "Researches topics and gathers information",
  },
  async (prompt, sessionInput) => {
    const stateManager = createAgentStateManager({
      initialState: { systemPrompt: "You are a researcher." },
    });

    const session = await createSession({
      ...sessionInput, // spreads agentName, thread, sandbox, sandboxShutdown
      threadOps: proxyLangChainThreadOps(), // auto-scoped to "Researcher"
      runAgent: runResearcherActivity,
      buildContextMessage: () => [{ type: "text", text: prompt }],
    });

    const { finalMessage, threadId } = await session.runSession({ stateManager });
    return {
      toolResponse: finalMessage ? extractText(finalMessage) : "No response",
      data: null,
      threadId,
    };
  },
);

In the parent workflow, register it with defineSubagent and pass it to createSession:

// parent.workflow.ts
import { defineSubagent } from "zeitlich/workflow";
import { researcherWorkflow } from "./researcher.workflow";

// Metadata (name, description) comes from the workflow definition
export const researcherSubagent = defineSubagent(researcherWorkflow);

// Optionally override parent-specific config
export const researcherSubagent = defineSubagent(researcherWorkflow, {
  thread: "fork",
  sandbox: "own",
  hooks: {
    onPostExecution: ({ result }) => console.log("researcher done", result),
  },
});

const session = await createSession({
  // ... other config
  subagents: [researcherSubagent, codeReviewerSubagent],
});

The Subagent tool is automatically added when subagents are configured, allowing the LLM to spawn child workflows.

Skills

Zeitlich has first-class support for the agentskills.io specification. Skills are reusable instruction sets that an agent can load on-demand via the built-in ReadSkill tool — progressive disclosure keeps token usage low while giving agents access to rich, domain-specific guidance.

Defining a Skill

Each skill lives in its own directory as a SKILL.md file with YAML frontmatter. A skill directory can also contain resource files — supporting documents, templates, or data that the agent can read from the sandbox filesystem:

skills/
├── code-review/
│   ├── SKILL.md
│   └── resources/
│       └── checklist.md
├── pdf-processing/
│   ├── SKILL.md
│   └── templates/
│       └── extraction-prompt.txt
---
name: code-review
description: Review pull requests for correctness, style, and security issues
allowed-tools: Bash Grep Read
license: MIT
---

## Instructions

When reviewing code, follow these steps:
1. Read the diff with `Bash`
2. Search for related tests with `Grep`
3. Read the checklist from `resources/checklist.md`
4. ...

Required fields: name and description. Optional: license, compatibility, allowed-tools (space-delimited), metadata (key-value map).

Resource files are any non-SKILL.md files inside the skill directory (discovered recursively). When loaded via FileSystemSkillProvider, their contents are stored in skill.resourceContents — a Record<string, string> keyed by relative path (e.g. "resources/checklist.md").

Loading Skills

Use FileSystemSkillProvider to load skills from a directory. It accepts any SandboxFileSystem implementation. loadAll() eagerly reads SKILL.md instructions and all resource file contents into each Skill object:

import { FileSystemSkillProvider } from "zeitlich";
import { InMemorySandboxProvider } from "zeitlich/adapters/sandbox/inmemory";

const provider = new InMemorySandboxProvider();
const { sandbox } = await provider.create({});

const skillProvider = new FileSystemSkillProvider(sandbox.fs, "/skills");
const skills = await skillProvider.loadAll();
// Each skill has: { name, description, instructions, resourceContents }
// resourceContents: { "resources/checklist.md": "...", ... }

Loading from the local filesystem (activity-side): Use NodeFsSandboxFileSystem to read skills from the worker's disk. This is the simplest option when skill files are bundled alongside your application code:

import { NodeFsSandboxFileSystem, FileSystemSkillProvider } from "zeitlich";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const fs = new NodeFsSandboxFileSystem(join(__dirname, "skills"));
const skillProvider = new FileSystemSkillProvider(fs, "/");
const skills = await skillProvider.loadAll();

For lightweight discovery without reading file contents, use listSkills():

const metadata = await skillProvider.listSkills();
// SkillMetadata[] — name, description, location only

Or parse a single file directly:

import { parseSkillFile } from "zeitlich/workflow";

const { frontmatter, body } = parseSkillFile(rawMarkdown);
// frontmatter: SkillMetadata, body: instruction text

Passing Skills to a Session

Pass loaded skills to createSession. Zeitlich automatically:

  1. Registers a ReadSkill tool whose description lists all available skills — the agent discovers them through the tool definition and loads instructions on demand.
  2. Seeds resourceContents into the sandbox as initialFiles (when sandboxOps is configured), so the agent can read resource files with its Read tool without any extra setup.
import { createSession } from "zeitlich/workflow";

const session = await createSession({
  // ... other config
  skills, // Skill[] — loaded via FileSystemSkillProvider or manually
});

The ReadSkill tool accepts a skill_name parameter (constrained to an enum of available names) and returns the full instruction body plus a list of available resource file paths. The handler runs directly in the workflow — no activity needed. Resource file contents are not included in the ReadSkill response (progressive disclosure); the agent reads them from the sandbox filesystem on demand.

Building Skills Manually

For advanced use cases, you can construct the tool and handler independently:

import { createReadSkillTool, createReadSkillHandler } from "zeitlich/workflow";

const tool = createReadSkillTool(skills);    // ToolDefinition with enum schema
const handler = createReadSkillHandler(skills); // Returns skill instructions

Thread & Sandbox Lifecycle

Every session has a thread (conversation history) and an optional sandbox (filesystem environment). Both are configured with explicit lifecycle types that control how they are initialized and torn down.

Thread Initialization (ThreadInit)

The thread field on SessionConfig (and WorkflowInput) accepts one of three modes:

| Mode | Description | |------|-------------| | { mode: "new" } | Start a fresh thread (default). Optionally pass threadId to choose the ID. | | { mode: "fork", threadId } | Copy all messages from an existing thread into a new one and continue there. The original is never mutated. | | { mode: "continue", threadId } | Append directly to an existing thread in-place. |

import { createSession } from "zeitlich/workflow";

// First run — fresh thread
const session = await createSession({
  thread: { mode: "new" },
  // ... other config
});

// Later — fork the previous conversation
const resumedSession = await createSession({
  thread: { mode: "fork", threadId: savedThreadId },
  // ... other config
});

// Or append directly to the existing thread
const continuedSession = await createSession({
  thread: { mode: "continue", threadId: savedThreadId },
  // ... other config
});

getShortId() produces compact, workflow-deterministic IDs (~12 base-62 chars) that are more token-efficient than UUIDs.

Sandbox Initialization (SandboxInit)

The sandbox field controls how a sandbox is created or reused:

| Mode | Description | |------|-------------| | { mode: "new" } | Create a fresh sandbox (default when sandboxOps is provided). | | { mode: "continue", sandboxId } | Resume a previously-paused sandbox. This session takes ownership. | | { mode: "fork", sandboxId } | Fork from an existing sandbox. A new sandbox is created and owned by this session. | | { mode: "inherit", sandboxId } | Use a sandbox owned by someone else (e.g. a parent agent). Shutdown policy is ignored. |

Sandbox Shutdown (SandboxShutdown)

The sandboxShutdown field controls what happens to the sandbox when the session exits:

| Value | Description | |-------|-------------| | "destroy" | Tear down the sandbox entirely (default). | | "pause" | Pause the sandbox so it can be resumed later. | | "keep" | Leave the sandbox running (no-op on exit). |

Subagents also support "pause-until-parent-close" — pause on exit, then wait for the parent workflow to signal when to destroy it.

Subagent Thread & Sandbox Config

Subagents configure thread and sandbox strategies via defineSubagent:

import { defineSubagent } from "zeitlich/workflow";
import { researcherWorkflow } from "./researcher.workflow";

// Fresh thread each time, no sandbox (defaults)
export const researcherSubagent = defineSubagent(researcherWorkflow);

// Allow the parent to continue a previous conversation via fork
export const researcherSubagent = defineSubagent(researcherWorkflow, {
  thread: "fork",
});

// Own sandbox with pause-on-exit
export const researcherSubagent = defineSubagent(researcherWorkflow, {
  thread: "fork",
  sandbox: { source: "own", shutdown: "pause" },
});

// Inherit the parent's sandbox
export const researcherSubagent = defineSubagent(researcherWorkflow, {
  sandbox: "inherit",
});

The thread field accepts "new" (default), "fork", or "continue". When set to "fork" or "continue", the parent agent can pass a threadId in a subsequent Task tool call to resume the conversation. The subagent returns its threadId in the response (surfaced as [Thread ID: ...]), which the parent can use for continuation.

The sandbox field accepts "none" (default), "inherit", "own", or { source: "own", shutdown } for explicit shutdown policy.

The subagent workflow receives lifecycle fields via sessionInput:

export const researcherWorkflow = defineSubagentWorkflow(
  {
    name: "Researcher",
    description: "Researches topics and gathers information",
  },
  async (prompt, sessionInput) => {
    const session = await createSession({
      ...sessionInput, // spreads agentName, thread, sandbox, sandboxShutdown
      threadOps: proxyLangChainThreadOps(),
      // ... other config
    });

    const { threadId, finalMessage } = await session.runSession({ stateManager });
    return { toolResponse: extractText(finalMessage), data: null, threadId };
  },
);

Filesystem Utilities

Built-in support for file operations with in-memory or custom filesystem providers (e.g. from just-bash).

toTree generates a file tree string from an IFileSystem instance:

import { toTree } from "zeitlich";

// In activities - generate a file tree string for agent context
export const createActivities = ({ redis, client }) => ({
  generateFileTreeActivity: async () => toTree(inMemoryFileSystem),
  // ...
});

Use the tree in buildContextMessage to give the agent filesystem awareness:

// In workflow
const fileTree = await generateFileTreeActivity();

const session = await createSession({
  // ... other config
  buildContextMessage: () => [
    { type: "text", text: `Files in the filesystem: ${fileTree}` },
    { type: "text", text: prompt },
  ],
});

For file operations, use the built-in tool handlers wrapped with withSandbox:

import {
  SandboxManager,
  withSandbox,
  globHandler,
  editHandler,
  bashHandler,
} from "zeitlich";

const sandboxManager = new SandboxManager(provider);

export const createActivities = ({ redis, client }) => ({
  // scope auto-prepends the provider id (e.g. "inMemory", "virtual")
  ...sandboxManager.createActivities("MyAgentWorkflow"),
  globHandlerActivity: withSandbox(sandboxManager, globHandler),
  editHandlerActivity: withSandbox(sandboxManager, editHandler),
  bashHandlerActivity: withSandbox(sandboxManager, bashHandler),
});

Sandbox Path Semantics (Virtual + Daytona)

Filesystem adapters now apply the same path rules:

  • Absolute paths are used as-is (canonicalized).
  • Relative paths are resolved from /.
  • Paths are normalized (duplicate slashes removed, ./.. collapsed).

This means readFile("a/b.txt") is treated as /a/b.txt across adapters.

Each fs instance also exposes workspaceBase, which is the base used for relative paths.

Virtual sandbox example (path-only calls):

import { createVirtualSandbox, VirtualSandboxProvider } from "zeitlich";

const provider = new VirtualSandboxProvider(resolver);
const { sandbox } = await provider.create({
  resolverContext: { projectId: "p1" },
  workspaceBase: "/repo",
});

const fs = sandbox.fs;
console.log(fs.workspaceBase); // "/repo"

await fs.writeFile("src/index.ts", 'export const ok = true;\n');
const content = await fs.readFile("src/index.ts"); // reads /repo/src/index.ts

Daytona sandbox example (base /home/daytona):

import { DaytonaSandboxProvider } from "zeitlich";

const provider = new DaytonaSandboxProvider();
const { sandbox } = await provider.create({
  workspaceBase: "/home/daytona",
});

const fs = sandbox.fs;
console.log(fs.workspaceBase); // "/home/daytona"

await fs.mkdir("project", { recursive: true });
await fs.writeFile("project/README.md", "# Hello from Daytona\n");
const content = await fs.readFile("project/README.md");

For Daytona, use workspaceBase: "/home/daytona" (or your own working dir) so relative paths stay in the expected workspace.

Built-in Tools

Zeitlich provides ready-to-use tool definitions and handlers for common agent operations.

| Tool | Description | | ----------------- | ----------------------------------------------------------------- | | Read | Read file contents with optional pagination | | Write | Create or overwrite files with new content | | Edit | Edit specific sections of a file by find/replace | | Glob | Search for files matching a glob pattern | | Grep | Search file contents with regex patterns | | Bash | Execute shell commands | | AskUserQuestion | Ask the user questions during execution with structured options | | ReadSkill | Load skill instructions on demand (see Skills) | | Task | Launch subagents as child workflows (see Subagents) |

// Import tool definitions in workflows
import {
  readTool,
  writeTool,
  editTool,
  globTool,
  grepTool,
  bashTool,
  askUserQuestionTool,
} from "zeitlich/workflow";

// Import handlers + wrapper in activities
import {
  withSandbox,
  editHandler,
  globHandler,
  bashHandler,
  createAskUserQuestionHandler,
} from "zeitlich";

All tools are passed via tools. The Bash tool's description is automatically enhanced with the file tree when provided:

const session = await createSession({
  // ... other config
  tools: {
    AskUserQuestion: defineTool({
      ...askUserQuestionTool,
      handler: askUserQuestionHandlerActivity,
    }),
    Bash: defineTool({
      ...bashTool,
      handler: bashHandlerActivity,
    }),
  },
});

API Reference

Workflow Entry Point (zeitlich/workflow)

Safe for use in Temporal workflow files:

| Export | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------ | | createSession | Creates an agent session with tools, prompts, subagents, and hooks | | createAgentStateManager | Creates a state manager for workflow state with query/update handlers | | createToolRouter | Creates a tool router (used internally by session, or for advanced use) | | defineTool | Identity function for type-safe tool definition with handler and hooks | | defineSubagentWorkflow | Defines a subagent workflow with embedded name, description, and optional resultSchema | | defineSubagent | Creates a SubagentConfig from a SubagentDefinition with optional parent-specific overrides | | getShortId | Generate a compact, workflow-deterministic identifier (base-62, 12 chars) | | Tool definitions | askUserQuestionTool, globTool, grepTool, readFileTool, writeFileTool, editTool, bashTool | | Task tools | taskCreateTool, taskGetTool, taskListTool, taskUpdateTool for workflow task management | | Skill utilities | parseSkillFile, createReadSkillTool, createReadSkillHandler | | defineWorkflow | Wraps a main workflow function, translating WorkflowInput into session-compatible fields | | Lifecycle types | ThreadInit, SandboxInit, SandboxShutdown, SubagentSandboxShutdown, SubagentSandboxConfig | | Types | Skill, SkillMetadata, SkillProvider, SubagentDefinition, SubagentConfig, ToolDefinition, ToolWithHandler, RouterContext, SessionConfig, WorkflowConfig, WorkflowInput, etc. |

Activity Entry Point (zeitlich)

Framework-agnostic utilities for activities, worker setup, and Node.js code:

| Export | Description | | ------------------------- | --------------------------------------------------------------------------------------------- | | createRunAgentActivity | Wraps a handler into a RunAgentActivity with auto-fetched parent workflow state | | withParentWorkflowState | Wraps a tool handler into an ActivityToolHandler with auto-fetched parent workflow state | | createThreadManager | Generic Redis-backed thread manager factory | | toTree | Generate file tree string from an IFileSystem instance | | withSandbox | Wraps a handler to auto-resolve sandbox from context (pairs with withAutoAppend) | | NodeFsSandboxFileSystem | node:fs adapter for SandboxFileSystem — read skills from the worker's local disk | | FileSystemSkillProvider | Load skills from a directory following the agentskills.io layout | | Tool handlers | bashHandler, editHandler, globHandler, readFileHandler, writeFileHandler, createAskUserQuestionHandler |

Thread Adapter Entry Points

LangChain (zeitlich/adapters/thread/langchain):

| Export | Description | | ----------------------------------- | ---------------------------------------------------------------------- | | createLangChainAdapter | Unified adapter returning createActivities, invoker, createModelInvoker | | createLangChainModelInvoker | Factory that returns a ModelInvoker backed by a LangChain chat model | | invokeLangChainModel | One-shot model invocation convenience function | | createLangChainThreadManager | Thread manager with LangChain StoredMessage helpers |

Google GenAI (zeitlich/adapters/thread/google-genai):

| Export | Description | | ----------------------------------- | ---------------------------------------------------------------------- | | createGoogleGenAIAdapter | Unified adapter returning createActivities, invoker, createModelInvoker | | createGoogleGenAIModelInvoker | Factory that returns a ModelInvoker backed by the @google/genai SDK | | invokeGoogleGenAIModel | One-shot model invocation convenience function | | createGoogleGenAIThreadManager | Thread manager with Google GenAI Content helpers |

Types

| Export | Description | | ----------------------- | ---------------------------------------------------------------------------- | | AgentStatus | "RUNNING" \| "WAITING_FOR_INPUT" \| "COMPLETED" \| "FAILED" \| "CANCELLED" | | MessageContent | Framework-agnostic message content (string \| ContentPart[]) | | ToolMessageContent | Content returned by a tool handler (string) | | ModelInvoker | Generic model invocation contract | | ModelInvokerConfig | Configuration passed to a model invoker | | ToolDefinition | Tool definition with name, description, and Zod schema | | ToolWithHandler | Tool definition combined with its handler | | RouterContext | Base context every tool handler receives (threadId, toolCallId, toolName, sandboxId?) | | Hooks | Combined session lifecycle + tool execution hooks | | ToolRouterHooks | Narrowed hook interface for tool execution only (pre/post/failure) | | ThreadInit | Thread initialization strategy: "new", "continue", or "fork" | | SandboxInit | Sandbox initialization strategy: "new", "continue", "fork", or "inherit" | | SandboxShutdown | Sandbox exit policy: "destroy" \| "pause" \| "keep" | | SubagentSandboxShutdown | Extended shutdown with "pause-until-parent-close" | | SubagentSandboxConfig | Subagent sandbox strategy: "none" \| "inherit" \| "own" \| { source, shutdown } | | SubagentDefinition | Callable subagent workflow with embedded metadata (from defineSubagentWorkflow) | | SubagentConfig | Resolved subagent configuration consumed by createSession | | AgentState | Generic agent state type |

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Temporal Worker                          │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │              Workflow (zeitlich/workflow)                  │  │
│  │  ┌────────────────┐  ┌───────────────────────────────┐   │  │
│  │  │ State Manager  │  │           Session             │   │  │
│  │  │ • Status       │  │  • Agent loop                 │   │  │
│  │  │ • Turns        │  │  • Tool routing & hooks       │   │  │
│  │  │ • Custom state │  │  • Prompts (system, context)  │   │  │
│  │  └────────────────┘  │  • Subagent coordination      │   │  │
│  │                      │  • Skills (progressive load)   │   │  │
│  │                      └───────────────────────────────┘   │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                Activities (zeitlich)                       │  │
│  │  • Tool handlers (search, file ops, bash, etc.)           │  │
│  │  • Generic thread manager (BaseThreadManager<T>)          │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │          Thread Adapter (zeitlich/adapters/thread/*)       │  │
│  │  • LangChain, Google GenAI, or custom                     │  │
│  │  • Thread ops (message storage) + model invoker            │  │
│  └──────────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │         Sandbox Adapter (zeitlich/adapters/sandbox/*)      │  │
│  │  • In-memory, Virtual, Daytona, E2B, Bedrock, or custom   │  │
│  │  • Filesystem ops for agent tools                          │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │      Redis      │
                    │ • Thread state  │
                    │ • Messages      │
                    └─────────────────┘

Requirements

  • Node.js >= 18
  • Temporal server
  • Redis

Contributing

Contributions are welcome! Please open an issue or submit a PR.

For maintainers: see RELEASING.md for the release process.

License

MIT © Bead Technologies Inc.