{"id":43932706,"url":"https://github.com/bead-ai/zeitlich","last_synced_at":"2026-04-01T19:51:13.928Z","repository":{"id":336455773,"uuid":"1146566916","full_name":"bead-ai/zeitlich","owner":"bead-ai","description":"An opinionated AI agent implementation for Temporal","archived":false,"fork":false,"pushed_at":"2026-03-03T15:48:29.000Z","size":902,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-03T20:05:10.202Z","etag":null,"topics":["agents","ai","temporal"],"latest_commit_sha":null,"homepage":"https://github.com/bead-ai/zeitlich","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bead-ai.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-31T10:00:57.000Z","updated_at":"2026-03-03T15:53:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bead-ai/zeitlich","commit_stats":null,"previous_names":["bead-ai/zeitlich"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/bead-ai/zeitlich","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bead-ai%2Fzeitlich","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bead-ai%2Fzeitlich/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bead-ai%2Fzeitlich/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bead-ai%2Fzeitlich/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bead-ai","download_url":"https://codeload.github.com/bead-ai/zeitlich/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bead-ai%2Fzeitlich/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30084685,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T13:22:36.021Z","status":"ssl_error","status_checked_at":"2026-03-04T13:20:45.750Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agents","ai","temporal"],"created_at":"2026-02-07T00:18:50.732Z","updated_at":"2026-04-01T19:51:13.908Z","avatar_url":"https://github.com/bead-ai.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![npm version](https://img.shields.io/npm/v/zeitlich.svg?style=flat-square)](https://www.npmjs.org/package/zeitlich)\n[![npm downloads](https://img.shields.io/npm/dm/zeitlich.svg?style=flat-square)](https://npm-stat.com/charts.html?package=zeitlich)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bead-ai/zeitlich)\n\n# Zeitlich\n\n\u003e **⚠️ Experimental Beta**: This library is under active development. APIs and interfaces may change between versions. Use in production at your own risk.\n\n**Durable AI Agents for Temporal**\n\nZeitlich is an opinionated framework for building reliable, stateful AI agents using [Temporal](https://temporal.io). 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.\n\n## Why Zeitlich?\n\nBuilding production AI agents is hard. Agents need to:\n\n- **Survive failures** — What happens when your agent crashes mid-task?\n- **Handle long-running work** — Some tasks take hours or days\n- **Coordinate** — Multiple agents often need to work together\n- **Maintain state** — Conversation history, tool results, workflow state\n\nTemporal solves these problems for workflows. Zeitlich brings these guarantees to AI agents.\n\n## Features\n\n- **Durable execution** — Agent state survives process restarts and failures\n- **Thread management** — Redis-backed conversation storage with automatic persistence\n- **Type-safe tools** — Define tools with Zod schemas, get full TypeScript inference\n- **Lifecycle hooks** — Pre/post tool execution, session start/end\n- **Subagent support** — Spawn child agents as Temporal child workflows\n- **Skills** — First-class [agentskills.io](https://agentskills.io) support with progressive disclosure\n- **Filesystem utilities** — In-memory or custom providers for file operations\n- **Model flexibility** — Framework-agnostic model invocation with adapters for LangChain, Vercel AI SDK, or provider-specific SDKs\n\n## LLM Integration\n\nZeitlich'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.\n\n### Thread Adapters\n\nA thread adapter bundles two concerns:\n1. **Thread management** — Storing and retrieving conversation messages in Redis\n2. **Model invocation** — Calling the LLM with the conversation history and tools\n\nEach adapter exposes the same shape: `createActivities(scope)` for Temporal worker registration, and an `invoker` for model calls. Pick the one matching your preferred SDK:\n\n| Adapter | Import | SDK |\n|---------|--------|-----|\n| LangChain | `zeitlich/adapters/thread/langchain` | `@langchain/core` + any provider package |\n| Google GenAI | `zeitlich/adapters/thread/google-genai` | `@google/genai` |\n\nVercel AI SDK and other provider-specific adapters can be built by implementing the `ThreadOps` and `ModelInvoker` interfaces.\n\n### Sandbox Adapters\n\nA sandbox adapter provides filesystem access for tools like `Bash`, `Read`, `Write`, and `Edit`:\n\n| Adapter | Import | Use case |\n|---------|--------|----------|\n| In-memory | `zeitlich/adapters/sandbox/inmemory` | Tests and lightweight agents |\n| Virtual FS | `zeitlich` / `zeitlich/workflow` | Built-in virtual filesystem with custom resolvers |\n| Daytona | `zeitlich/adapters/sandbox/daytona` | Remote Daytona workspaces |\n| E2B | `zeitlich/adapters/sandbox/e2b` | E2B cloud sandboxes |\n| Bedrock | `zeitlich/adapters/sandbox/bedrock` | AWS Bedrock AgentCore Code Interpreter |\n\n### Example: LangChain Adapter\n\n```typescript\nimport { ChatAnthropic } from \"@langchain/anthropic\";\nimport { createLangChainAdapter } from \"zeitlich/adapters/thread/langchain\";\nimport { createRunAgentActivity } from \"zeitlich\";\n\nconst adapter = createLangChainAdapter({\n  redis,\n  model: new ChatAnthropic({ model: \"claude-sonnet-4-20250514\" }),\n});\n\nexport function createActivities(client: WorkflowClient) {\n  return {\n    ...adapter.createActivities(\"myAgentWorkflow\"),\n    runAgent: createRunAgentActivity(client, adapter.invoker),\n  };\n}\n```\n\nAll adapters follow the same pattern — `createActivities(scope)` for worker registration and `invoker` for model calls.\n\n## Installation\n\n```bash\nnpm install zeitlich ioredis\n```\n\n**Peer dependencies:**\n\n- `ioredis` \u003e= 5.0.0\n- `@langchain/core` \u003e= 1.0.0 (optional — only when using the LangChain adapter)\n- `@google/genai` \u003e= 1.0.0 (optional — only when using the Google GenAI adapter)\n- `@aws-sdk/client-bedrock-agentcore` \u003e= 3.900.0 (optional — only when using the Bedrock adapter)\n\n**Required infrastructure:**\n\n- Temporal server (local dev: `temporal server start-dev`)\n- Redis instance\n\n## Import Paths\n\nZeitlich uses separate entry points for workflow-side and activity-side code:\n\n```typescript\n// In workflow files — no external dependencies (Redis, LLM SDKs, etc.)\nimport {\n  createSession,\n  createAgentStateManager,\n  defineTool,\n  bashTool,\n} from \"zeitlich/workflow\";\n\n// Adapter workflow proxies (auto-scoped to current workflow)\nimport { proxyLangChainThreadOps } from \"zeitlich/adapters/thread/langchain/workflow\";\nimport { proxyInMemorySandboxOps } from \"zeitlich/adapters/sandbox/inmemory/workflow\";\n\n// In activity files and worker setup — framework-agnostic core\nimport {\n  createRunAgentActivity,\n  SandboxManager,\n  withSandbox,\n  bashHandler,\n} from \"zeitlich\";\n\n// Thread adapter — activity-side\nimport { createLangChainAdapter } from \"zeitlich/adapters/thread/langchain\";\n```\n\n**Entry points:**\n\n- `zeitlich/workflow` — Pure TypeScript, safe for Temporal's V8 sandbox\n- `zeitlich/adapters/*/workflow` — Workflow-side proxies that auto-scope activities to the current workflow\n- `zeitlich` — Activity-side utilities (Redis, filesystem), framework-agnostic\n- `zeitlich/adapters/thread/*` — Activity-side adapters (thread management + model invocation)\n- `zeitlich/adapters/sandbox/*` — Activity-side sandbox providers\n\n## Examples\n\nRunnable examples (worker, client, workflows) are in a separate repo: [zeitlich-examples](https://github.com/bead-ai/zeitlich-examples).\n\n## Quick Start\n\n### 1. Define Your Tools\n\n```typescript\nimport { z } from \"zod\";\nimport type { ToolDefinition } from \"zeitlich/workflow\";\n\nexport const searchTool: ToolDefinition\u003c\"Search\", typeof searchSchema\u003e = {\n  name: \"Search\",\n  description: \"Search for information\",\n  schema: z.object({\n    query: z.string().describe(\"The search query\"),\n  }),\n};\n```\n\n### 2. Create the Workflow\n\nThe 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.\n\n```typescript\nimport { proxyActivities, workflowInfo } from \"@temporalio/workflow\";\nimport {\n  createAgentStateManager,\n  createSession,\n  defineWorkflow,\n  askUserQuestionTool,\n  bashTool,\n  defineTool,\n} from \"zeitlich/workflow\";\nimport { searchTool } from \"./tools\";\nimport type { MyActivities } from \"./activities\";\n\nimport { proxyLangChainThreadOps } from \"zeitlich/adapters/thread/langchain/workflow\";\nimport { proxyInMemorySandboxOps } from \"zeitlich/adapters/sandbox/inmemory/workflow\";\n\nconst {\n  runAgentActivity,\n  searchHandlerActivity,\n  bashHandlerActivity,\n  askUserQuestionHandlerActivity,\n} = proxyActivities\u003cMyActivities\u003e({\n  startToCloseTimeout: \"30m\",\n  retry: {\n    maximumAttempts: 6,\n    initialInterval: \"5s\",\n    maximumInterval: \"15m\",\n    backoffCoefficient: 4,\n  },\n  heartbeatTimeout: \"5m\",\n});\n\nexport const myAgentWorkflow = defineWorkflow(\n  { name: \"myAgentWorkflow\" },\n  async ({ prompt }: { prompt: string }, sessionInput) =\u003e {\n    const { runId } = workflowInfo();\n\n    const stateManager = createAgentStateManager({\n      initialState: {\n        systemPrompt: \"You are a helpful assistant.\",\n      },\n      agentName: \"my-agent\",\n    });\n\n    const session = await createSession({\n      agentName: \"my-agent\",\n      maxTurns: 20,\n      thread: { mode: \"new\", threadId: runId },\n      threadOps: proxyLangChainThreadOps(),\n      sandboxOps: proxyInMemorySandboxOps(),\n      runAgent: runAgentActivity,\n      buildContextMessage: () =\u003e [{ type: \"text\", text: prompt }],\n      tools: {\n        Search: defineTool({\n          ...searchTool,\n          handler: searchHandlerActivity,\n        }),\n        AskUserQuestion: defineTool({\n          ...askUserQuestionTool,\n          handler: askUserQuestionHandlerActivity,\n          hooks: {\n            onPostToolUse: () =\u003e {\n              stateManager.waitForInput();\n            },\n          },\n        }),\n        Bash: defineTool({\n          ...bashTool,\n          handler: bashHandlerActivity,\n        }),\n      },\n      ...sessionInput,\n    });\n\n    const result = await session.runSession({ stateManager });\n    return result;\n  }\n);\n```\n\n### 3. Create Activities\n\nActivities 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.\n\n```typescript\nimport type Redis from \"ioredis\";\nimport type { WorkflowClient } from \"@temporalio/client\";\nimport { ChatAnthropic } from \"@langchain/anthropic\";\nimport {\n  SandboxManager,\n  withSandbox,\n  bashHandler,\n  createAskUserQuestionHandler,\n  createRunAgentActivity,\n} from \"zeitlich\";\nimport { InMemorySandboxProvider } from \"zeitlich/adapters/sandbox/inmemory\";\n\nimport { createLangChainAdapter } from \"zeitlich/adapters/thread/langchain\";\n\nconst sandboxProvider = new InMemorySandboxProvider();\nconst sandboxManager = new SandboxManager(sandboxProvider);\n\nexport const createActivities = ({\n  redis,\n  client,\n}: {\n  redis: Redis;\n  client: WorkflowClient;\n}) =\u003e {\n  const adapter = createLangChainAdapter({\n    redis,\n    model: new ChatAnthropic({\n      model: \"claude-sonnet-4-20250514\",\n      maxTokens: 4096,\n    }),\n  });\n\n  return {\n    ...adapter.createActivities(\"myAgentWorkflow\"),\n    ...sandboxManager.createActivities(\"myAgentWorkflow\"),\n    runAgentActivity: createRunAgentActivity(client, adapter.invoker),\n    searchHandlerActivity: async (args: { query: string }) =\u003e ({\n      toolResponse: JSON.stringify(await performSearch(args.query)),\n      data: null,\n    }),\n    bashHandlerActivity: withSandbox(sandboxManager, bashHandler),\n    askUserQuestionHandlerActivity: createAskUserQuestionHandler(),\n  };\n};\n\nexport type MyActivities = ReturnType\u003ctypeof createActivities\u003e;\n```\n\n### 4. Set Up the Worker\n\n```typescript\nimport { Worker, NativeConnection } from \"@temporalio/worker\";\nimport Redis from \"ioredis\";\nimport { fileURLToPath } from \"node:url\";\nimport { createActivities } from \"./activities\";\n\nasync function run() {\n  const connection = await NativeConnection.connect({\n    address: \"localhost:7233\",\n  });\n  const redis = new Redis({ host: \"localhost\", port: 6379 });\n\n  const worker = await Worker.create({\n    connection,\n    taskQueue: \"my-agent\",\n    workflowsPath: fileURLToPath(new URL(\"./workflows.ts\", import.meta.url)),\n    activities: createActivities({ redis, client }),\n  });\n\n  await worker.run();\n}\n```\n\n## Core Concepts\n\n### Agent State Manager\n\nManages 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:\n\n```typescript\nimport { createAgentStateManager } from \"zeitlich/workflow\";\n\nconst stateManager = createAgentStateManager({\n  initialState: {\n    systemPrompt: \"You are a helpful assistant.\",\n    customField: \"value\",\n  },\n  agentName: \"my-agent\",\n});\n\n// State operations\nstateManager.set(\"customField\", \"new value\");\nstateManager.get(\"customField\"); // Get current value\nstateManager.complete(); // Mark as COMPLETED\nstateManager.waitForInput(); // Mark as WAITING_FOR_INPUT\nstateManager.isRunning(); // Check if RUNNING\nstateManager.isTerminal(); // Check if COMPLETED/FAILED/CANCELLED\n```\n\n### Tools with Handlers\n\nDefine tools with their handlers inline in `createSession`:\n\n```typescript\nimport { z } from \"zod\";\nimport type { ToolDefinition } from \"zeitlich/workflow\";\n\n// Define tool schema\nconst searchTool: ToolDefinition\u003c\"Search\", typeof searchSchema\u003e = {\n  name: \"Search\",\n  description: \"Search for information\",\n  schema: z.object({ query: z.string() }),\n};\n\n// In workflow - combine tool definition with handler using defineTool()\nconst session = await createSession({\n  // ... other config\n  tools: {\n    Search: defineTool({\n      ...searchTool,\n      handler: handleSearchResult, // Activity that implements the tool\n    }),\n  },\n});\n```\n\n### Lifecycle Hooks\n\nAdd hooks for tool execution and session lifecycle:\n\n```typescript\nconst session = await createSession({\n  // ... other config\n  hooks: {\n    onPreToolUse: ({ toolCall }) =\u003e {\n      console.log(`Executing ${toolCall.name}`);\n      return {}; // Can return { skip: true } or { modifiedArgs: {...} }\n    },\n    onPostToolUse: ({ toolCall, result, durationMs }) =\u003e {\n      console.log(`${toolCall.name} completed in ${durationMs}ms`);\n      // Access stateManager here to update state based on results\n    },\n    onPostToolUseFailure: ({ toolCall, error }) =\u003e {\n      return { fallbackContent: \"Tool failed, please try again\" };\n    },\n    onSessionStart: ({ threadId, agentName }) =\u003e {\n      console.log(`Session started: ${agentName}`);\n    },\n    onSessionEnd: ({ exitReason, turns }) =\u003e {\n      console.log(`Session ended: ${exitReason} after ${turns} turns`);\n    },\n  },\n});\n```\n\n### Subagents\n\nSpawn child agents as Temporal child workflows. Use `defineSubagentWorkflow` to define the workflow with its metadata once, then `defineSubagent` to register it in the parent:\n\n```typescript\n// researcher.workflow.ts\nimport { proxyActivities } from \"@temporalio/workflow\";\nimport {\n  createAgentStateManager,\n  createSession,\n  defineSubagentWorkflow,\n} from \"zeitlich/workflow\";\nimport { proxyLangChainThreadOps } from \"zeitlich/adapters/thread/langchain/workflow\";\nimport type { createResearcherActivities } from \"./activities\";\n\nconst { runResearcherActivity } = proxyActivities\u003c\n  ReturnType\u003ctypeof createResearcherActivities\u003e\n\u003e({ startToCloseTimeout: \"30m\", heartbeatTimeout: \"5m\" });\n\n// Define the workflow — name, description (and optional resultSchema) live here\nexport const researcherWorkflow = defineSubagentWorkflow(\n  {\n    name: \"Researcher\",\n    description: \"Researches topics and gathers information\",\n  },\n  async (prompt, sessionInput) =\u003e {\n    const stateManager = createAgentStateManager({\n      initialState: { systemPrompt: \"You are a researcher.\" },\n    });\n\n    const session = await createSession({\n      ...sessionInput, // spreads agentName, thread, sandbox, sandboxShutdown\n      threadOps: proxyLangChainThreadOps(), // auto-scoped to \"Researcher\"\n      runAgent: runResearcherActivity,\n      buildContextMessage: () =\u003e [{ type: \"text\", text: prompt }],\n    });\n\n    const { finalMessage, threadId } = await session.runSession({ stateManager });\n    return {\n      toolResponse: finalMessage ? extractText(finalMessage) : \"No response\",\n      data: null,\n      threadId,\n    };\n  },\n);\n```\n\nIn the parent workflow, register it with `defineSubagent` and pass it to `createSession`:\n\n```typescript\n// parent.workflow.ts\nimport { defineSubagent } from \"zeitlich/workflow\";\nimport { researcherWorkflow } from \"./researcher.workflow\";\n\n// Metadata (name, description) comes from the workflow definition\nexport const researcherSubagent = defineSubagent(researcherWorkflow);\n\n// Optionally override parent-specific config\nexport const researcherSubagent = defineSubagent(researcherWorkflow, {\n  thread: \"fork\",\n  sandbox: \"own\",\n  hooks: {\n    onPostExecution: ({ result }) =\u003e console.log(\"researcher done\", result),\n  },\n});\n\nconst session = await createSession({\n  // ... other config\n  subagents: [researcherSubagent, codeReviewerSubagent],\n});\n```\n\nThe `Subagent` tool is automatically added when subagents are configured, allowing the LLM to spawn child workflows.\n\n### Skills\n\nZeitlich has first-class support for the [agentskills.io](https://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.\n\n#### Defining a Skill\n\nEach 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:\n\n```\nskills/\n├── code-review/\n│   ├── SKILL.md\n│   └── resources/\n│       └── checklist.md\n├── pdf-processing/\n│   ├── SKILL.md\n│   └── templates/\n│       └── extraction-prompt.txt\n```\n\n```markdown\n---\nname: code-review\ndescription: Review pull requests for correctness, style, and security issues\nallowed-tools: Bash Grep Read\nlicense: MIT\n---\n\n## Instructions\n\nWhen reviewing code, follow these steps:\n1. Read the diff with `Bash`\n2. Search for related tests with `Grep`\n3. Read the checklist from `resources/checklist.md`\n4. ...\n```\n\nRequired fields: `name` and `description`. Optional: `license`, `compatibility`, `allowed-tools` (space-delimited), `metadata` (key-value map).\n\nResource 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\u003cstring, string\u003e` keyed by relative path (e.g. `\"resources/checklist.md\"`).\n\n#### Loading Skills\n\nUse `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:\n\n```typescript\nimport { FileSystemSkillProvider } from \"zeitlich\";\nimport { InMemorySandboxProvider } from \"zeitlich/adapters/sandbox/inmemory\";\n\nconst provider = new InMemorySandboxProvider();\nconst { sandbox } = await provider.create({});\n\nconst skillProvider = new FileSystemSkillProvider(sandbox.fs, \"/skills\");\nconst skills = await skillProvider.loadAll();\n// Each skill has: { name, description, instructions, resourceContents }\n// resourceContents: { \"resources/checklist.md\": \"...\", ... }\n```\n\n**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:\n\n```typescript\nimport { NodeFsSandboxFileSystem, FileSystemSkillProvider } from \"zeitlich\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst fs = new NodeFsSandboxFileSystem(join(__dirname, \"skills\"));\nconst skillProvider = new FileSystemSkillProvider(fs, \"/\");\nconst skills = await skillProvider.loadAll();\n```\n\nFor lightweight discovery without reading file contents, use `listSkills()`:\n\n```typescript\nconst metadata = await skillProvider.listSkills();\n// SkillMetadata[] — name, description, location only\n```\n\nOr parse a single file directly:\n\n```typescript\nimport { parseSkillFile } from \"zeitlich/workflow\";\n\nconst { frontmatter, body } = parseSkillFile(rawMarkdown);\n// frontmatter: SkillMetadata, body: instruction text\n```\n\n#### Passing Skills to a Session\n\nPass loaded skills to `createSession`. Zeitlich automatically:\n\n1. Registers a `ReadSkill` tool whose description lists all available skills — the agent discovers them through the tool definition and loads instructions on demand.\n2. 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.\n\n```typescript\nimport { createSession } from \"zeitlich/workflow\";\n\nconst session = await createSession({\n  // ... other config\n  skills, // Skill[] — loaded via FileSystemSkillProvider or manually\n});\n```\n\nThe `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.\n\n#### Building Skills Manually\n\nFor advanced use cases, you can construct the tool and handler independently:\n\n```typescript\nimport { createReadSkillTool, createReadSkillHandler } from \"zeitlich/workflow\";\n\nconst tool = createReadSkillTool(skills);    // ToolDefinition with enum schema\nconst handler = createReadSkillHandler(skills); // Returns skill instructions\n```\n\n### Thread \u0026 Sandbox Lifecycle\n\nEvery 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.\n\n#### Thread Initialization (`ThreadInit`)\n\nThe `thread` field on `SessionConfig` (and `WorkflowInput`) accepts one of three modes:\n\n| Mode | Description |\n|------|-------------|\n| `{ mode: \"new\" }` | Start a fresh thread (default). Optionally pass `threadId` to choose the ID. |\n| `{ mode: \"fork\", threadId }` | Copy all messages from an existing thread into a new one and continue there. The original is never mutated. |\n| `{ mode: \"continue\", threadId }` | Append directly to an existing thread in-place. |\n\n```typescript\nimport { createSession } from \"zeitlich/workflow\";\n\n// First run — fresh thread\nconst session = await createSession({\n  thread: { mode: \"new\" },\n  // ... other config\n});\n\n// Later — fork the previous conversation\nconst resumedSession = await createSession({\n  thread: { mode: \"fork\", threadId: savedThreadId },\n  // ... other config\n});\n\n// Or append directly to the existing thread\nconst continuedSession = await createSession({\n  thread: { mode: \"continue\", threadId: savedThreadId },\n  // ... other config\n});\n```\n\n`getShortId()` produces compact, workflow-deterministic IDs (~12 base-62 chars) that are more token-efficient than UUIDs.\n\n#### Sandbox Initialization (`SandboxInit`)\n\nThe `sandbox` field controls how a sandbox is created or reused:\n\n| Mode | Description |\n|------|-------------|\n| `{ mode: \"new\" }` | Create a fresh sandbox (default when `sandboxOps` is provided). |\n| `{ mode: \"continue\", sandboxId }` | Resume a previously-paused sandbox. This session takes ownership. |\n| `{ mode: \"fork\", sandboxId }` | Fork from an existing sandbox. A new sandbox is created and owned by this session. |\n| `{ mode: \"inherit\", sandboxId }` | Use a sandbox owned by someone else (e.g. a parent agent). Shutdown policy is ignored. |\n\n#### Sandbox Shutdown (`SandboxShutdown`)\n\nThe `sandboxShutdown` field controls what happens to the sandbox when the session exits:\n\n| Value | Description |\n|-------|-------------|\n| `\"destroy\"` | Tear down the sandbox entirely (default). |\n| `\"pause\"` | Pause the sandbox so it can be resumed later. |\n| `\"keep\"` | Leave the sandbox running (no-op on exit). |\n\nSubagents also support `\"pause-until-parent-close\"` — pause on exit, then wait for the parent workflow to signal when to destroy it.\n\n#### Subagent Thread \u0026 Sandbox Config\n\nSubagents configure thread and sandbox strategies via `defineSubagent`:\n\n```typescript\nimport { defineSubagent } from \"zeitlich/workflow\";\nimport { researcherWorkflow } from \"./researcher.workflow\";\n\n// Fresh thread each time, no sandbox (defaults)\nexport const researcherSubagent = defineSubagent(researcherWorkflow);\n\n// Allow the parent to continue a previous conversation via fork\nexport const researcherSubagent = defineSubagent(researcherWorkflow, {\n  thread: \"fork\",\n});\n\n// Own sandbox with pause-on-exit\nexport const researcherSubagent = defineSubagent(researcherWorkflow, {\n  thread: \"fork\",\n  sandbox: { source: \"own\", shutdown: \"pause\" },\n});\n\n// Inherit the parent's sandbox\nexport const researcherSubagent = defineSubagent(researcherWorkflow, {\n  sandbox: \"inherit\",\n});\n```\n\nThe `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.\n\nThe `sandbox` field accepts `\"none\"` (default), `\"inherit\"`, `\"own\"`, or `{ source: \"own\", shutdown }` for explicit shutdown policy.\n\nThe subagent workflow receives lifecycle fields via `sessionInput`:\n\n```typescript\nexport const researcherWorkflow = defineSubagentWorkflow(\n  {\n    name: \"Researcher\",\n    description: \"Researches topics and gathers information\",\n  },\n  async (prompt, sessionInput) =\u003e {\n    const session = await createSession({\n      ...sessionInput, // spreads agentName, thread, sandbox, sandboxShutdown\n      threadOps: proxyLangChainThreadOps(),\n      // ... other config\n    });\n\n    const { threadId, finalMessage } = await session.runSession({ stateManager });\n    return { toolResponse: extractText(finalMessage), data: null, threadId };\n  },\n);\n```\n\n### Filesystem Utilities\n\nBuilt-in support for file operations with in-memory or custom filesystem providers (e.g. from [`just-bash`](https://github.com/nicholasgasior/just-bash)).\n\n`toTree` generates a file tree string from an `IFileSystem` instance:\n\n```typescript\nimport { toTree } from \"zeitlich\";\n\n// In activities - generate a file tree string for agent context\nexport const createActivities = ({ redis, client }) =\u003e ({\n  generateFileTreeActivity: async () =\u003e toTree(inMemoryFileSystem),\n  // ...\n});\n```\n\nUse the tree in `buildContextMessage` to give the agent filesystem awareness:\n\n```typescript\n// In workflow\nconst fileTree = await generateFileTreeActivity();\n\nconst session = await createSession({\n  // ... other config\n  buildContextMessage: () =\u003e [\n    { type: \"text\", text: `Files in the filesystem: ${fileTree}` },\n    { type: \"text\", text: prompt },\n  ],\n});\n```\n\nFor file operations, use the built-in tool handlers wrapped with `withSandbox`:\n\n```typescript\nimport {\n  SandboxManager,\n  withSandbox,\n  globHandler,\n  editHandler,\n  bashHandler,\n} from \"zeitlich\";\n\nconst sandboxManager = new SandboxManager(provider);\n\nexport const createActivities = ({ redis, client }) =\u003e ({\n  // scope auto-prepends the provider id (e.g. \"inMemory\", \"virtual\")\n  ...sandboxManager.createActivities(\"MyAgentWorkflow\"),\n  globHandlerActivity: withSandbox(sandboxManager, globHandler),\n  editHandlerActivity: withSandbox(sandboxManager, editHandler),\n  bashHandlerActivity: withSandbox(sandboxManager, bashHandler),\n});\n```\n\n#### Sandbox Path Semantics (Virtual + Daytona)\n\nFilesystem adapters now apply the same path rules:\n\n- Absolute paths are used as-is (canonicalized).\n- Relative paths are resolved from `/`.\n- Paths are normalized (duplicate slashes removed, `.`/`..` collapsed).\n\nThis means `readFile(\"a/b.txt\")` is treated as `/a/b.txt` across adapters.\n\nEach `fs` instance also exposes `workspaceBase`, which is the base used for relative paths.\n\n**Virtual filesystem example (path-only calls):**\n\n```typescript\nimport { VirtualFileSystem } from \"zeitlich\";\n\nconst virtualFs = new VirtualFileSystem(fileTree, resolver, { projectId: \"p1\" }, \"/repo\");\nconsole.log(virtualFs.workspaceBase); // \"/repo\"\n\nawait virtualFs.writeFile(\"src/index.ts\", 'export const ok = true;\\n');\nconst content = await virtualFs.readFile(\"src/index.ts\"); // reads /repo/src/index.ts\n```\n\n**Daytona sandbox example (base `/home/daytona`):**\n\n```typescript\nimport { DaytonaSandboxProvider } from \"zeitlich\";\n\nconst provider = new DaytonaSandboxProvider();\nconst { sandbox } = await provider.create({\n  workspaceBase: \"/home/daytona\",\n});\n\nconst fs = sandbox.fs;\nconsole.log(fs.workspaceBase); // \"/home/daytona\"\n\nawait fs.mkdir(\"project\", { recursive: true });\nawait fs.writeFile(\"project/README.md\", \"# Hello from Daytona\\n\");\nconst content = await fs.readFile(\"project/README.md\");\n```\n\nFor Daytona, use `workspaceBase: \"/home/daytona\"` (or your own working dir) so relative paths stay in the expected workspace.\n\n### Built-in Tools\n\nZeitlich provides ready-to-use tool definitions and handlers for common agent operations.\n\n| Tool              | Description                                                       |\n| ----------------- | ----------------------------------------------------------------- |\n| `Read`            | Read file contents with optional pagination                       |\n| `Write`           | Create or overwrite files with new content                        |\n| `Edit`            | Edit specific sections of a file by find/replace                  |\n| `Glob`            | Search for files matching a glob pattern                          |\n| `Grep`            | Search file contents with regex patterns                          |\n| `Bash`            | Execute shell commands                                            |\n| `AskUserQuestion` | Ask the user questions during execution with structured options   |\n| `ReadSkill`       | Load skill instructions on demand (see [Skills](#skills))         |\n| `Task`            | Launch subagents as child workflows (see [Subagents](#subagents)) |\n\n```typescript\n// Import tool definitions in workflows\nimport {\n  readTool,\n  writeTool,\n  editTool,\n  globTool,\n  grepTool,\n  bashTool,\n  askUserQuestionTool,\n} from \"zeitlich/workflow\";\n\n// Import handlers + wrapper in activities\nimport {\n  withSandbox,\n  editHandler,\n  globHandler,\n  bashHandler,\n  createAskUserQuestionHandler,\n} from \"zeitlich\";\n```\n\nAll tools are passed via `tools`. The Bash tool's description is automatically enhanced with the file tree when provided:\n\n```typescript\nconst session = await createSession({\n  // ... other config\n  tools: {\n    AskUserQuestion: defineTool({\n      ...askUserQuestionTool,\n      handler: askUserQuestionHandlerActivity,\n    }),\n    Bash: defineTool({\n      ...bashTool,\n      handler: bashHandlerActivity,\n    }),\n  },\n});\n```\n\n## API Reference\n\n### Workflow Entry Point (`zeitlich/workflow`)\n\nSafe for use in Temporal workflow files:\n\n| Export                      | Description                                                                                            |\n| --------------------------- | ------------------------------------------------------------------------------------------------------ |\n| `createSession`             | Creates an agent session with tools, prompts, subagents, and hooks                                     |\n| `createAgentStateManager`   | Creates a state manager for workflow state with query/update handlers                                  |\n| `createToolRouter`          | Creates a tool router (used internally by session, or for advanced use)                                |\n| `defineTool`                | Identity function for type-safe tool definition with handler and hooks                                 |\n| `defineSubagentWorkflow`    | Defines a subagent workflow with embedded name, description, and optional resultSchema                 |\n| `defineSubagent`            | Creates a `SubagentConfig` from a `SubagentDefinition` with optional parent-specific overrides         |\n| `getShortId`                | Generate a compact, workflow-deterministic identifier (base-62, 12 chars)                              |\n| Tool definitions            | `askUserQuestionTool`, `globTool`, `grepTool`, `readFileTool`, `writeFileTool`, `editTool`, `bashTool` |\n| Task tools                  | `taskCreateTool`, `taskGetTool`, `taskListTool`, `taskUpdateTool` for workflow task management         |\n| Skill utilities             | `parseSkillFile`, `createReadSkillTool`, `createReadSkillHandler`                                      |\n| `defineWorkflow`            | Wraps a main workflow function, translating `WorkflowInput` into session-compatible fields             |\n| Lifecycle types             | `ThreadInit`, `SandboxInit`, `SandboxShutdown`, `SubagentSandboxShutdown`, `SubagentSandboxConfig`     |\n| Types                       | `Skill`, `SkillMetadata`, `SkillProvider`, `SubagentDefinition`, `SubagentConfig`, `ToolDefinition`, `ToolWithHandler`, `RouterContext`, `SessionConfig`, `WorkflowConfig`, `WorkflowInput`, etc. |\n\n### Activity Entry Point (`zeitlich`)\n\nFramework-agnostic utilities for activities, worker setup, and Node.js code:\n\n| Export                    | Description                                                                                   |\n| ------------------------- | --------------------------------------------------------------------------------------------- |\n| `createRunAgentActivity`  | Wraps a handler into a `RunAgentActivity` with auto-fetched parent workflow state             |\n| `withParentWorkflowState`  | Wraps a tool handler into an `ActivityToolHandler` with auto-fetched parent workflow state    |\n| `createThreadManager`     | Generic Redis-backed thread manager factory                                                   |\n| `toTree`                  | Generate file tree string from an `IFileSystem` instance                                      |\n| `withSandbox`             | Wraps a handler to auto-resolve sandbox from context (pairs with `withAutoAppend`)            |\n| `NodeFsSandboxFileSystem`   | `node:fs` adapter for `SandboxFileSystem` — read skills from the worker's local disk              |\n| `FileSystemSkillProvider`   | Load skills from a directory following the agentskills.io layout                                  |\n| Tool handlers             | `bashHandler`, `editHandler`, `globHandler`, `readFileHandler`, `writeFileHandler`, `createAskUserQuestionHandler` |\n\n### Thread Adapter Entry Points\n\n**LangChain** (`zeitlich/adapters/thread/langchain`):\n\n| Export                              | Description                                                            |\n| ----------------------------------- | ---------------------------------------------------------------------- |\n| `createLangChainAdapter`            | Unified adapter returning `createActivities`, `invoker`, `createModelInvoker` |\n| `createLangChainModelInvoker`       | Factory that returns a `ModelInvoker` backed by a LangChain chat model |\n| `invokeLangChainModel`              | One-shot model invocation convenience function                         |\n| `createLangChainThreadManager`      | Thread manager with LangChain `StoredMessage` helpers                  |\n\n**Google GenAI** (`zeitlich/adapters/thread/google-genai`):\n\n| Export                              | Description                                                            |\n| ----------------------------------- | ---------------------------------------------------------------------- |\n| `createGoogleGenAIAdapter`          | Unified adapter returning `createActivities`, `invoker`, `createModelInvoker` |\n| `createGoogleGenAIModelInvoker`     | Factory that returns a `ModelInvoker` backed by the `@google/genai` SDK |\n| `invokeGoogleGenAIModel`            | One-shot model invocation convenience function                         |\n| `createGoogleGenAIThreadManager`    | Thread manager with Google GenAI `Content` helpers                     |\n\n### Types\n\n| Export                  | Description                                                                  |\n| ----------------------- | ---------------------------------------------------------------------------- |\n| `AgentStatus`           | `\"RUNNING\" \\| \"WAITING_FOR_INPUT\" \\| \"COMPLETED\" \\| \"FAILED\" \\| \"CANCELLED\"` |\n| `MessageContent`        | Framework-agnostic message content (`string \\| ContentPart[]`)               |\n| `ToolMessageContent`    | Content returned by a tool handler (`string`)                                |\n| `ModelInvoker`          | Generic model invocation contract                                            |\n| `ModelInvokerConfig`    | Configuration passed to a model invoker                                      |\n| `ToolDefinition`        | Tool definition with name, description, and Zod schema                       |\n| `ToolWithHandler`       | Tool definition combined with its handler                                    |\n| `RouterContext`          | Base context every tool handler receives (`threadId`, `toolCallId`, `toolName`, `sandboxId?`) |\n| `Hooks`                 | Combined session lifecycle + tool execution hooks                            |\n| `ToolRouterHooks`       | Narrowed hook interface for tool execution only (pre/post/failure)            |\n| `ThreadInit`            | Thread initialization strategy: `\"new\"`, `\"continue\"`, or `\"fork\"`               |\n| `SandboxInit`           | Sandbox initialization strategy: `\"new\"`, `\"continue\"`, `\"fork\"`, or `\"inherit\"` |\n| `SandboxShutdown`       | Sandbox exit policy: `\"destroy\" \\| \"pause\" \\| \"keep\"`                            |\n| `SubagentSandboxShutdown` | Extended shutdown with `\"pause-until-parent-close\"`                             |\n| `SubagentSandboxConfig` | Subagent sandbox strategy: `\"none\" \\| \"inherit\" \\| \"own\" \\| { source, shutdown }` |\n| `SubagentDefinition`    | Callable subagent workflow with embedded metadata (from `defineSubagentWorkflow`) |\n| `SubagentConfig`        | Resolved subagent configuration consumed by `createSession`                  |\n| `AgentState`            | Generic agent state type                                                     |\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                        Temporal Worker                          │\n│  ┌──────────────────────────────────────────────────────────┐  │\n│  │              Workflow (zeitlich/workflow)                  │  │\n│  │  ┌────────────────┐  ┌───────────────────────────────┐   │  │\n│  │  │ State Manager  │  │           Session             │   │  │\n│  │  │ • Status       │  │  • Agent loop                 │   │  │\n│  │  │ • Turns        │  │  • Tool routing \u0026 hooks       │   │  │\n│  │  │ • Custom state │  │  • Prompts (system, context)  │   │  │\n│  │  └────────────────┘  │  • Subagent coordination      │   │  │\n│  │                      │  • Skills (progressive load)   │   │  │\n│  │                      └───────────────────────────────┘   │  │\n│  └──────────────────────────────────────────────────────────┘  │\n│                              │                                  │\n│  ┌──────────────────────────────────────────────────────────┐  │\n│  │                Activities (zeitlich)                       │  │\n│  │  • Tool handlers (search, file ops, bash, etc.)           │  │\n│  │  • Generic thread manager (BaseThreadManager\u003cT\u003e)          │  │\n│  └──────────────────────────────────────────────────────────┘  │\n│                              │                                  │\n│  ┌──────────────────────────────────────────────────────────┐  │\n│  │          Thread Adapter (zeitlich/adapters/thread/*)       │  │\n│  │  • LangChain, Google GenAI, or custom                     │  │\n│  │  • Thread ops (message storage) + model invoker            │  │\n│  └──────────────────────────────────────────────────────────┘  │\n│  ┌──────────────────────────────────────────────────────────┐  │\n│  │         Sandbox Adapter (zeitlich/adapters/sandbox/*)      │  │\n│  │  • In-memory, Virtual, Daytona, E2B, Bedrock, or custom   │  │\n│  │  • Filesystem ops for agent tools                          │  │\n│  └──────────────────────────────────────────────────────────┘  │\n└─────────────────────────────────────────────────────────────────┘\n                              │\n                              ▼\n                    ┌─────────────────┐\n                    │      Redis      │\n                    │ • Thread state  │\n                    │ • Messages      │\n                    └─────────────────┘\n```\n\n## Observability\n\nZeitlich emits structured, replay-safe logs at key lifecycle points (session start/end, each turn, tool execution, subagent spawn/completion). These flow through Temporal's built-in workflow logger with zero configuration.\n\n### Logging\n\nAll log messages are emitted via `@temporalio/workflow`'s `log` and automatically routed to whatever logger you configure on the Temporal Runtime. By default they go to `STDERR` via `console.error`.\n\n**Custom logger (e.g. winston):**\n\n```typescript\nimport { Runtime, makeTelemetryFilterString } from \"@temporalio/worker\";\nimport winston from \"winston\";\n\nconst logger = winston.createLogger({\n  level: \"info\",\n  format: winston.format.json(),\n  transports: [new winston.transports.File({ filename: \"worker.log\" })],\n});\n\nRuntime.install({\n  logger,\n  telemetryOptions: {\n    logging: {\n      filter: makeTelemetryFilterString({ core: \"INFO\", other: \"INFO\" }),\n      forward: {},\n    },\n  },\n});\n```\n\n### Metrics via Sinks\n\nFor custom metrics (Prometheus, Datadog, OpenTelemetry, etc.), zeitlich provides `ZeitlichObservabilitySinks` — a typed Temporal Sinks interface that bridges agent events from the workflow sandbox to your Node.js metrics backend.\n\n**1. Register sinks on the Worker:**\n\n```typescript\nimport { Worker, InjectedSinks } from \"@temporalio/worker\";\nimport type { ZeitlichObservabilitySinks } from \"zeitlich/workflow\";\n\nconst sinks: InjectedSinks\u003cZeitlichObservabilitySinks\u003e = {\n  zeitlichMetrics: {\n    sessionStarted: {\n      fn(_workflowInfo, event) {\n        sessionCounter.inc({ agent: event.agentName });\n      },\n      callDuringReplay: false,\n    },\n    sessionEnded: {\n      fn(_workflowInfo, event) {\n        sessionDuration.observe(event.durationMs);\n        tokenCounter.inc({ type: \"input\" }, event.usage.inputTokens ?? 0);\n      },\n      callDuringReplay: false,\n    },\n    turnCompleted: {\n      fn(_workflowInfo, event) {\n        turnGauge.set({ agent: event.agentName }, event.turn);\n      },\n      callDuringReplay: false,\n    },\n    toolExecuted: {\n      fn(_workflowInfo, event) {\n        toolDuration.observe({ tool: event.toolName }, event.durationMs);\n        if (!event.success) toolErrors.inc({ tool: event.toolName });\n      },\n      callDuringReplay: false,\n    },\n  },\n};\n\nconst worker = await Worker.create({ sinks, /* ... */ });\n```\n\n**2. Wire hooks in your workflow:**\n\n```typescript\nimport { createSession, createObservabilityHooks } from \"zeitlich/workflow\";\n\nconst session = await createSession({\n  agentName: \"myAgent\",\n  hooks: createObservabilityHooks(\"myAgent\"),\n  // ...\n});\n```\n\nUse `composeHooks()` to combine observability hooks with your own:\n\n```typescript\nimport { createObservabilityHooks, composeHooks } from \"zeitlich/workflow\";\n\nconst obs = createObservabilityHooks(\"myAgent\");\n\nconst session = await createSession({\n  hooks: {\n    ...obs,\n    onSessionEnd: composeHooks(obs.onSessionEnd, (ctx) =\u003e {\n      // your custom session-end logic\n    }),\n  },\n});\n```\n\n### Tracing with OpenTelemetry\n\nFor distributed tracing across client, workflow, and activities, use Temporal's OpenTelemetry interceptor package:\n\n```bash\nnpm install @temporalio/interceptors-opentelemetry @opentelemetry/sdk-node\n```\n\nSee [Temporal's tracing docs](https://docs.temporal.io/develop/typescript/observability#set-up-tracing) and the [`interceptors-opentelemetry` sample](https://github.com/temporalio/samples-typescript/tree/main/interceptors-opentelemetry) for setup.\n\n## Requirements\n\n- Node.js \u003e= 18\n- Temporal server\n- Redis\n\n## Contributing\n\nContributions are welcome! Please open an issue or submit a PR.\n\nFor maintainers: see [RELEASING.md](./RELEASING.md) for the release process.\n\n## License\n\nMIT © [Bead Technologies Inc.](https://usebead.ai)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbead-ai%2Fzeitlich","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbead-ai%2Fzeitlich","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbead-ai%2Fzeitlich/lists"}