zeitlich 0.2.21 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +303 -105
  2. package/dist/adapters/sandbox/daytona/index.cjs +7 -1
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/daytona/index.d.cts +3 -1
  5. package/dist/adapters/sandbox/daytona/index.d.ts +3 -1
  6. package/dist/adapters/sandbox/daytona/index.js +7 -1
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  8. package/dist/adapters/sandbox/daytona/workflow.cjs +33 -0
  9. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -0
  10. package/dist/adapters/sandbox/daytona/workflow.d.cts +27 -0
  11. package/dist/adapters/sandbox/daytona/workflow.d.ts +27 -0
  12. package/dist/adapters/sandbox/daytona/workflow.js +31 -0
  13. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -0
  14. package/dist/adapters/sandbox/inmemory/index.cjs +18 -1
  15. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/inmemory/index.d.cts +4 -2
  17. package/dist/adapters/sandbox/inmemory/index.d.ts +4 -2
  18. package/dist/adapters/sandbox/inmemory/index.js +18 -1
  19. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  20. package/dist/adapters/sandbox/inmemory/workflow.cjs +33 -0
  21. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -0
  22. package/dist/adapters/sandbox/inmemory/workflow.d.cts +25 -0
  23. package/dist/adapters/sandbox/inmemory/workflow.d.ts +25 -0
  24. package/dist/adapters/sandbox/inmemory/workflow.js +31 -0
  25. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -0
  26. package/dist/adapters/sandbox/virtual/index.cjs +36 -9
  27. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/virtual/index.d.cts +8 -5
  29. package/dist/adapters/sandbox/virtual/index.d.ts +8 -5
  30. package/dist/adapters/sandbox/virtual/index.js +36 -9
  31. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  32. package/dist/adapters/sandbox/virtual/workflow.cjs +33 -0
  33. package/dist/adapters/sandbox/virtual/workflow.cjs.map +1 -0
  34. package/dist/adapters/sandbox/virtual/workflow.d.cts +27 -0
  35. package/dist/adapters/sandbox/virtual/workflow.d.ts +27 -0
  36. package/dist/adapters/sandbox/virtual/workflow.js +31 -0
  37. package/dist/adapters/sandbox/virtual/workflow.js.map +1 -0
  38. package/dist/adapters/thread/google-genai/index.cjs +9 -1
  39. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  40. package/dist/adapters/thread/google-genai/index.d.cts +31 -19
  41. package/dist/adapters/thread/google-genai/index.d.ts +31 -19
  42. package/dist/adapters/thread/google-genai/index.js +9 -1
  43. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  44. package/dist/adapters/thread/google-genai/workflow.cjs +33 -0
  45. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -0
  46. package/dist/adapters/thread/google-genai/workflow.d.cts +32 -0
  47. package/dist/adapters/thread/google-genai/workflow.d.ts +32 -0
  48. package/dist/adapters/thread/google-genai/workflow.js +31 -0
  49. package/dist/adapters/thread/google-genai/workflow.js.map +1 -0
  50. package/dist/adapters/thread/langchain/index.cjs +9 -1
  51. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  52. package/dist/adapters/thread/langchain/index.d.cts +27 -16
  53. package/dist/adapters/thread/langchain/index.d.ts +27 -16
  54. package/dist/adapters/thread/langchain/index.js +9 -1
  55. package/dist/adapters/thread/langchain/index.js.map +1 -1
  56. package/dist/adapters/thread/langchain/workflow.cjs +33 -0
  57. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -0
  58. package/dist/adapters/thread/langchain/workflow.d.cts +32 -0
  59. package/dist/adapters/thread/langchain/workflow.d.ts +32 -0
  60. package/dist/adapters/thread/langchain/workflow.js +31 -0
  61. package/dist/adapters/thread/langchain/workflow.js.map +1 -0
  62. package/dist/index.cjs +282 -90
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +38 -16
  65. package/dist/index.d.ts +38 -16
  66. package/dist/index.js +281 -87
  67. package/dist/index.js.map +1 -1
  68. package/dist/queries-DModcWRy.d.cts +44 -0
  69. package/dist/queries-byD0jr1Y.d.ts +44 -0
  70. package/dist/{types-BkAYmc96.d.ts → types-B50pBPEV.d.ts} +190 -38
  71. package/dist/{types-YbL7JpEA.d.cts → types-Bll19FZJ.d.cts} +7 -0
  72. package/dist/{types-YbL7JpEA.d.ts → types-Bll19FZJ.d.ts} +7 -0
  73. package/dist/{queries-6Avfh74U.d.ts → types-BuXdFhaZ.d.cts} +7 -48
  74. package/dist/{types-BMRzfELQ.d.cts → types-ChAMwU3q.d.cts} +17 -1
  75. package/dist/{types-BMRzfELQ.d.ts → types-ChAMwU3q.d.ts} +17 -1
  76. package/dist/{types-CES_30qx.d.cts → types-DQW8l7pY.d.cts} +190 -38
  77. package/dist/{queries-CHa2iv_I.d.cts → types-GZ76HZSj.d.ts} +7 -48
  78. package/dist/workflow.cjs +244 -86
  79. package/dist/workflow.cjs.map +1 -1
  80. package/dist/workflow.d.cts +54 -65
  81. package/dist/workflow.d.ts +54 -65
  82. package/dist/workflow.js +243 -83
  83. package/dist/workflow.js.map +1 -1
  84. package/package.json +54 -2
  85. package/src/adapters/sandbox/daytona/filesystem.ts +1 -1
  86. package/src/adapters/sandbox/daytona/index.ts +8 -0
  87. package/src/adapters/sandbox/daytona/proxy.ts +56 -0
  88. package/src/adapters/sandbox/e2b/filesystem.ts +147 -0
  89. package/src/adapters/sandbox/e2b/index.ts +164 -0
  90. package/src/adapters/sandbox/e2b/types.ts +23 -0
  91. package/src/adapters/sandbox/inmemory/index.ts +27 -3
  92. package/src/adapters/sandbox/inmemory/proxy.ts +53 -0
  93. package/src/adapters/sandbox/virtual/filesystem.ts +41 -17
  94. package/src/adapters/sandbox/virtual/provider.ts +9 -1
  95. package/src/adapters/sandbox/virtual/proxy.ts +53 -0
  96. package/src/adapters/sandbox/virtual/types.ts +9 -4
  97. package/src/adapters/thread/google-genai/activities.ts +51 -17
  98. package/src/adapters/thread/google-genai/index.ts +1 -0
  99. package/src/adapters/thread/google-genai/proxy.ts +61 -0
  100. package/src/adapters/thread/langchain/activities.ts +47 -14
  101. package/src/adapters/thread/langchain/index.ts +1 -0
  102. package/src/adapters/thread/langchain/proxy.ts +61 -0
  103. package/src/lib/lifecycle.ts +57 -0
  104. package/src/lib/sandbox/manager.ts +52 -6
  105. package/src/lib/sandbox/sandbox.test.ts +12 -11
  106. package/src/lib/sandbox/types.ts +31 -4
  107. package/src/lib/session/index.ts +4 -5
  108. package/src/lib/session/session-edge-cases.integration.test.ts +491 -66
  109. package/src/lib/session/session.integration.test.ts +92 -80
  110. package/src/lib/session/session.ts +108 -96
  111. package/src/lib/session/types.ts +87 -17
  112. package/src/lib/subagent/define.ts +6 -5
  113. package/src/lib/subagent/handler.ts +148 -16
  114. package/src/lib/subagent/index.ts +4 -0
  115. package/src/lib/subagent/register.ts +10 -3
  116. package/src/lib/subagent/signals.ts +8 -0
  117. package/src/lib/subagent/subagent.integration.test.ts +893 -128
  118. package/src/lib/subagent/tool.ts +2 -2
  119. package/src/lib/subagent/types.ts +84 -21
  120. package/src/lib/subagent/workflow.ts +83 -12
  121. package/src/lib/tool-router/router-edge-cases.integration.test.ts +4 -1
  122. package/src/lib/tool-router/router.integration.test.ts +141 -5
  123. package/src/lib/tool-router/router.ts +13 -3
  124. package/src/lib/tool-router/types.ts +7 -0
  125. package/src/lib/workflow.test.ts +104 -27
  126. package/src/lib/workflow.ts +37 -19
  127. package/src/tools/bash/bash.test.ts +16 -7
  128. package/src/workflow.ts +11 -14
  129. package/tsup.config.ts +6 -0
@@ -29,7 +29,7 @@ function parentDir(p: string): string {
29
29
  */
30
30
  function inferDirectories(
31
31
  entries: { path: string }[],
32
- workspaceBase: string,
32
+ workspaceBase: string
33
33
  ): Set<string> {
34
34
  const dirs = new Set<string>();
35
35
  dirs.add("/");
@@ -54,8 +54,7 @@ function inferDirectories(
54
54
  export class VirtualSandboxFileSystem<
55
55
  TCtx = unknown,
56
56
  TMeta = FileEntryMetadata,
57
- > implements SandboxFileSystem
58
- {
57
+ > implements SandboxFileSystem {
59
58
  readonly workspaceBase: string;
60
59
  private entries: Map<string, FileEntry<TMeta>>;
61
60
  private directories: Set<string>;
@@ -65,11 +64,11 @@ export class VirtualSandboxFileSystem<
65
64
  tree: FileEntry<TMeta>[],
66
65
  private resolver: FileResolver<TCtx, TMeta>,
67
66
  private ctx: TCtx,
68
- workspaceBase = "/",
67
+ workspaceBase = "/"
69
68
  ) {
70
69
  this.workspaceBase = normalisePath(workspaceBase);
71
70
  this.entries = new Map(
72
- tree.map((e) => [normalisePath(e.path, this.workspaceBase), e]),
71
+ tree.map((e) => [normalisePath(e.path, this.workspaceBase), e])
73
72
  );
74
73
  this.directories = inferDirectories(tree, this.workspaceBase);
75
74
  }
@@ -91,13 +90,13 @@ export class VirtualSandboxFileSystem<
91
90
  async readFile(path: string): Promise<string> {
92
91
  const entry = this.entries.get(normalisePath(path, this.workspaceBase));
93
92
  if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
94
- return this.resolver.readFile(entry.id, this.ctx);
93
+ return this.resolver.readFile(entry.id, this.ctx, entry.metadata);
95
94
  }
96
95
 
97
96
  async readFileBuffer(path: string): Promise<Uint8Array> {
98
97
  const entry = this.entries.get(normalisePath(path, this.workspaceBase));
99
98
  if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
100
- return this.resolver.readFileBuffer(entry.id, this.ctx);
99
+ return this.resolver.readFileBuffer(entry.id, this.ctx, entry.metadata);
101
100
  }
102
101
 
103
102
  // --------------------------------------------------------------------------
@@ -180,7 +179,12 @@ export class VirtualSandboxFileSystem<
180
179
  const existing = this.entries.get(norm);
181
180
 
182
181
  if (existing) {
183
- await this.resolver.writeFile(existing.id, content, this.ctx);
182
+ await this.resolver.writeFile(
183
+ existing.id,
184
+ content,
185
+ this.ctx,
186
+ existing.metadata
187
+ );
184
188
  const size =
185
189
  typeof content === "string"
186
190
  ? new TextEncoder().encode(content).byteLength
@@ -209,12 +213,21 @@ export class VirtualSandboxFileSystem<
209
213
  return this.writeFile(path, content);
210
214
  }
211
215
 
212
- const current = await this.resolver.readFile(existing.id, this.ctx);
216
+ const current = await this.resolver.readFile(
217
+ existing.id,
218
+ this.ctx,
219
+ existing.metadata
220
+ );
213
221
  const appended =
214
222
  typeof content === "string"
215
223
  ? current + content
216
224
  : current + new TextDecoder().decode(content);
217
- await this.resolver.writeFile(existing.id, appended, this.ctx);
225
+ await this.resolver.writeFile(
226
+ existing.id,
227
+ appended,
228
+ this.ctx,
229
+ existing.metadata
230
+ );
218
231
 
219
232
  const size = new TextEncoder().encode(appended).byteLength;
220
233
  const updated: FileEntry<TMeta> = {
@@ -226,7 +239,10 @@ export class VirtualSandboxFileSystem<
226
239
  this.mutations.push({ type: "update", path: norm, entry: updated });
227
240
  }
228
241
 
229
- async mkdir(_path: string, _options?: { recursive?: boolean }): Promise<void> {
242
+ async mkdir(
243
+ _path: string,
244
+ _options?: { recursive?: boolean }
245
+ ): Promise<void> {
230
246
  const norm = normalisePath(_path, this.workspaceBase);
231
247
  if (this.directories.has(norm)) return;
232
248
 
@@ -244,13 +260,13 @@ export class VirtualSandboxFileSystem<
244
260
 
245
261
  async rm(
246
262
  path: string,
247
- options?: { recursive?: boolean; force?: boolean },
263
+ options?: { recursive?: boolean; force?: boolean }
248
264
  ): Promise<void> {
249
265
  const norm = normalisePath(path, this.workspaceBase);
250
266
  const entry = this.entries.get(norm);
251
267
 
252
268
  if (entry) {
253
- await this.resolver.deleteFile(entry.id, this.ctx);
269
+ await this.resolver.deleteFile(entry.id, this.ctx, entry.metadata);
254
270
  this.entries.delete(norm);
255
271
  this.mutations.push({ type: "remove", path: norm });
256
272
  return;
@@ -263,7 +279,7 @@ export class VirtualSandboxFileSystem<
263
279
  const prefix = norm === "/" ? "/" : norm + "/";
264
280
  for (const [p, e] of this.entries) {
265
281
  if (p.startsWith(prefix)) {
266
- await this.resolver.deleteFile(e.id, this.ctx);
282
+ await this.resolver.deleteFile(e.id, this.ctx, e.metadata);
267
283
  this.entries.delete(p);
268
284
  this.mutations.push({ type: "remove", path: p });
269
285
  }
@@ -283,14 +299,18 @@ export class VirtualSandboxFileSystem<
283
299
  async cp(
284
300
  src: string,
285
301
  dest: string,
286
- _options?: { recursive?: boolean },
302
+ _options?: { recursive?: boolean }
287
303
  ): Promise<void> {
288
304
  const normSrc = normalisePath(src, this.workspaceBase);
289
305
  const normDest = normalisePath(dest, this.workspaceBase);
290
306
 
291
307
  const entry = this.entries.get(normSrc);
292
308
  if (entry) {
293
- const content = await this.resolver.readFile(entry.id, this.ctx);
309
+ const content = await this.resolver.readFile(
310
+ entry.id,
311
+ this.ctx,
312
+ entry.metadata
313
+ );
294
314
  await this.writeFile(normDest, content);
295
315
  return;
296
316
  }
@@ -306,7 +326,11 @@ export class VirtualSandboxFileSystem<
306
326
  for (const [p, e] of this.entries) {
307
327
  if (p.startsWith(prefix)) {
308
328
  const relative = p.slice(normSrc.length);
309
- const content = await this.resolver.readFile(e.id, this.ctx);
329
+ const content = await this.resolver.readFile(
330
+ e.id,
331
+ this.ctx,
332
+ e.metadata
333
+ );
310
334
  await this.writeFile(normDest + relative, content);
311
335
  }
312
336
  }
@@ -26,7 +26,7 @@ import type {
26
26
  * const manager = new SandboxManager(provider);
27
27
  *
28
28
  * export const activities = {
29
- * ...manager.createActivities(),
29
+ * ...manager.createActivities("CodingAgent"),
30
30
  * readFile: withVirtualSandbox(client, provider, readHandler),
31
31
  * };
32
32
  * ```
@@ -90,6 +90,14 @@ export class VirtualSandboxProvider<
90
90
  // No-op — no internal state to clean up
91
91
  }
92
92
 
93
+ async pause(): Promise<void> {
94
+ // No-op — virtual sandbox state lives in workflow AgentState
95
+ }
96
+
97
+ async fork(_sandboxId: string): Promise<never> {
98
+ throw new Error("Not implemented");
99
+ }
100
+
93
101
  async snapshot(): Promise<never> {
94
102
  throw new SandboxNotSupportedError(
95
103
  "snapshot (virtual sandbox state lives in workflow AgentState)"
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Workflow-safe proxy for virtual sandbox operations.
3
+ *
4
+ * Import this from `zeitlich/adapters/sandbox/virtual/workflow`
5
+ * in your Temporal workflow files.
6
+ *
7
+ * By default the scope is derived from `workflowInfo().workflowType`,
8
+ * so activities are automatically namespaced per workflow.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { proxyVirtualSandboxOps } from 'zeitlich/adapters/sandbox/virtual/workflow';
13
+ *
14
+ * const sandbox = proxyVirtualSandboxOps();
15
+ * ```
16
+ */
17
+ import { proxyActivities, workflowInfo } from "@temporalio/workflow";
18
+ import type { SandboxOps } from "../../../lib/sandbox/types";
19
+ import type { VirtualSandboxCreateOptions } from "./types";
20
+
21
+ const ADAPTER_PREFIX = "virtual";
22
+
23
+ export function proxyVirtualSandboxOps(
24
+ scope?: string,
25
+ options?: Parameters<typeof proxyActivities>[0]
26
+ ): SandboxOps<VirtualSandboxCreateOptions<unknown>> {
27
+ const resolvedScope = scope ?? workflowInfo().workflowType;
28
+
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
31
+ options ?? {
32
+ startToCloseTimeout: "30s",
33
+ retry: {
34
+ maximumAttempts: 3,
35
+ initialInterval: "2s",
36
+ maximumInterval: "30s",
37
+ backoffCoefficient: 2,
38
+ },
39
+ }
40
+ );
41
+
42
+ const prefix = `${ADAPTER_PREFIX}${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
43
+ const p = (key: string): string =>
44
+ `${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
45
+
46
+ return {
47
+ createSandbox: acts[p("createSandbox")],
48
+ destroySandbox: acts[p("destroySandbox")],
49
+ pauseSandbox: acts[p("pauseSandbox")],
50
+ snapshotSandbox: acts[p("snapshotSandbox")],
51
+ forkSandbox: acts[p("forkSandbox")],
52
+ } as SandboxOps<VirtualSandboxCreateOptions<unknown>>;
53
+ }
@@ -56,15 +56,20 @@ export type TreeMutation<TMeta = FileEntryMetadata> =
56
56
  */
57
57
  export interface FileResolver<TCtx = unknown, TMeta = FileEntryMetadata> {
58
58
  resolveEntries(ctx: TCtx): Promise<FileEntry<TMeta>[]>;
59
- readFile(id: string, ctx: TCtx): Promise<string>;
60
- readFileBuffer(id: string, ctx: TCtx): Promise<Uint8Array>;
61
- writeFile(id: string, content: string | Uint8Array, ctx: TCtx): Promise<void>;
59
+ readFile(id: string, ctx: TCtx, metadata: TMeta): Promise<string>;
60
+ readFileBuffer(id: string, ctx: TCtx, metadata: TMeta): Promise<Uint8Array>;
61
+ writeFile(
62
+ id: string,
63
+ content: string | Uint8Array,
64
+ ctx: TCtx,
65
+ metadata: TMeta
66
+ ): Promise<void>;
62
67
  createFile(
63
68
  path: string,
64
69
  content: string | Uint8Array,
65
70
  ctx: TCtx
66
71
  ): Promise<FileEntry<TMeta>>;
67
- deleteFile(id: string, ctx: TCtx): Promise<void>;
72
+ deleteFile(id: string, ctx: TCtx, metadata: TMeta): Promise<void>;
68
73
  }
69
74
 
70
75
  // ============================================================================
@@ -2,11 +2,20 @@ import type Redis from "ioredis";
2
2
  import type { GoogleGenAI, Content } from "@google/genai";
3
3
  import type { ToolResultConfig } from "../../../lib/types";
4
4
  import type { MessageContent } from "../../../lib/types";
5
- import type { ThreadOps } from "../../../lib/session/types";
5
+ import type {
6
+ ThreadOps,
7
+ PrefixedThreadOps,
8
+ ScopedPrefix,
9
+ } from "../../../lib/session/types";
6
10
  import type { ModelInvoker } from "../../../lib/model";
7
11
  import { createGoogleGenAIThreadManager } from "./thread-manager";
8
12
  import { createGoogleGenAIModelInvoker } from "./model-invoker";
9
13
 
14
+ const ADAPTER_PREFIX = "googleGenAI" as const;
15
+
16
+ export type GoogleGenAIThreadOps<TScope extends string = ""> =
17
+ PrefixedThreadOps<ScopedPrefix<TScope, typeof ADAPTER_PREFIX>>;
18
+
10
19
  export interface GoogleGenAIAdapterConfig {
11
20
  redis: Redis;
12
21
  client: GoogleGenAI;
@@ -15,21 +24,37 @@ export interface GoogleGenAIAdapterConfig {
15
24
  }
16
25
 
17
26
  export interface GoogleGenAIAdapter {
18
- /** Thread operations (register these as Temporal activities on the worker) */
19
- threadOps: ThreadOps;
20
27
  /** Model invoker using the default model (only available when `model` was provided) */
21
28
  invoker: ModelInvoker<Content>;
22
29
  /** Create an invoker for a specific model name (for multi-model setups) */
23
30
  createModelInvoker(model: string): ModelInvoker<Content>;
31
+ /**
32
+ * Create prefixed thread activities for registration on the worker.
33
+ *
34
+ * @param scope - Workflow name appended to the adapter prefix.
35
+ * Use different scopes for the main agent vs subagents to avoid collisions.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * adapter.createActivities("codingAgent")
40
+ * // → { googleGenAICodingAgentInitializeThread, googleGenAICodingAgentAppendHumanMessage, … }
41
+ *
42
+ * adapter.createActivities("researchAgent")
43
+ * // → { googleGenAIResearchAgentInitializeThread, … }
44
+ * ```
45
+ */
46
+ createActivities<S extends string = "">(
47
+ scope?: S
48
+ ): GoogleGenAIThreadOps<S>;
24
49
  }
25
50
 
26
51
  /**
27
52
  * Creates a Google GenAI adapter that bundles thread operations and model
28
53
  * invocation using the `@google/genai` SDK.
29
54
  *
30
- * The returned `threadOps` should be registered as Temporal activities on
31
- * the worker. The `invoker` (or invokers created via `createModelInvoker`)
32
- * should be wrapped with `createRunAgentActivity` for per-agent activities.
55
+ * Use `createActivities(scope)` to register scoped thread operations as
56
+ * Temporal activities on the worker. The `invoker` (or invokers created via
57
+ * `createModelInvoker`) should be wrapped with `createRunAgentActivity`.
33
58
  *
34
59
  * @example
35
60
  * ```typescript
@@ -42,27 +67,23 @@ export interface GoogleGenAIAdapter {
42
67
  *
43
68
  * export function createActivities(temporalClient: WorkflowClient) {
44
69
  * return {
45
- * ...adapter.threadOps,
46
- * runAgent: createRunAgentActivity(temporalClient, adapter.invoker),
70
+ * ...adapter.createActivities("codingAgent"),
71
+ * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
47
72
  * };
48
73
  * }
49
74
  * ```
50
75
  *
51
- * @example Multi-model setup
76
+ * @example Multi-agent worker (main + subagent share the adapter)
52
77
  * ```typescript
53
- * const adapter = createGoogleGenAIAdapter({ redis, client });
54
- *
55
78
  * export function createActivities(temporalClient: WorkflowClient) {
56
79
  * return {
57
- * ...adapter.threadOps,
80
+ * ...adapter.createActivities("codingAgent"),
81
+ * ...adapter.createActivities("researchAgent"),
82
+ * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
58
83
  * runResearchAgent: createRunAgentActivity(
59
84
  * temporalClient,
60
85
  * adapter.createModelInvoker('gemini-2.5-pro'),
61
86
  * ),
62
- * runFastAgent: createRunAgentActivity(
63
- * temporalClient,
64
- * adapter.createModelInvoker('gemini-2.5-flash'),
65
- * ),
66
87
  * };
67
88
  * }
68
89
  * ```
@@ -114,6 +135,19 @@ export function createGoogleGenAIAdapter(
114
135
  },
115
136
  };
116
137
 
138
+ function createActivities<S extends string = "">(
139
+ scope?: S
140
+ ): GoogleGenAIThreadOps<S> {
141
+ const prefix = scope
142
+ ? `${ADAPTER_PREFIX}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
143
+ : ADAPTER_PREFIX;
144
+ const cap = (s: string): string =>
145
+ s.charAt(0).toUpperCase() + s.slice(1);
146
+ return Object.fromEntries(
147
+ Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
148
+ ) as GoogleGenAIThreadOps<S>;
149
+ }
150
+
117
151
  const makeInvoker = (model: string): ModelInvoker<Content> =>
118
152
  createGoogleGenAIModelInvoker({ redis, client, model });
119
153
 
@@ -127,7 +161,7 @@ export function createGoogleGenAIAdapter(
127
161
  }) as unknown as ModelInvoker<Content>);
128
162
 
129
163
  return {
130
- threadOps,
164
+ createActivities,
131
165
  invoker,
132
166
  createModelInvoker: makeInvoker,
133
167
  };
@@ -22,6 +22,7 @@ export {
22
22
  createGoogleGenAIAdapter,
23
23
  type GoogleGenAIAdapter,
24
24
  type GoogleGenAIAdapterConfig,
25
+ type GoogleGenAIThreadOps,
25
26
  } from "./activities";
26
27
 
27
28
  // Thread manager
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Workflow-safe proxy for Google GenAI thread operations.
3
+ *
4
+ * Import this from `zeitlich/adapters/thread/google-genai/workflow`
5
+ * in your Temporal workflow files.
6
+ *
7
+ * By default the scope is derived from `workflowInfo().workflowType`,
8
+ * so activities are automatically namespaced per workflow.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { proxyGoogleGenAIThreadOps } from 'zeitlich/adapters/thread/google-genai/workflow';
13
+ *
14
+ * // Auto-scoped to the current workflow name
15
+ * const threadOps = proxyGoogleGenAIThreadOps();
16
+ *
17
+ * // Explicit scope override
18
+ * const threadOps = proxyGoogleGenAIThreadOps("customScope");
19
+ * ```
20
+ */
21
+ import {
22
+ proxyActivities,
23
+ workflowInfo,
24
+ type ActivityInterfaceFor,
25
+ } from "@temporalio/workflow";
26
+ import type { ThreadOps } from "../../../lib/session/types";
27
+
28
+ const ADAPTER_PREFIX = "googleGenAI";
29
+
30
+ export function proxyGoogleGenAIThreadOps(
31
+ scope?: string,
32
+ options?: Parameters<typeof proxyActivities>[0]
33
+ ): ActivityInterfaceFor<ThreadOps> {
34
+ const resolvedScope = scope ?? workflowInfo().workflowType;
35
+
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
38
+ options ?? {
39
+ startToCloseTimeout: "10s",
40
+ retry: {
41
+ maximumAttempts: 6,
42
+ initialInterval: "5s",
43
+ maximumInterval: "15m",
44
+ backoffCoefficient: 4,
45
+ },
46
+ }
47
+ );
48
+
49
+ const prefix =
50
+ `${ADAPTER_PREFIX}${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
51
+ const p = (key: string): string =>
52
+ `${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
53
+
54
+ return {
55
+ initializeThread: acts[p("initializeThread")],
56
+ appendHumanMessage: acts[p("appendHumanMessage")],
57
+ appendToolResult: acts[p("appendToolResult")],
58
+ appendSystemMessage: acts[p("appendSystemMessage")],
59
+ forkThread: acts[p("forkThread")],
60
+ } as ActivityInterfaceFor<ThreadOps>;
61
+ }
@@ -1,13 +1,22 @@
1
1
  import type Redis from "ioredis";
2
2
  import type { ToolResultConfig } from "../../../lib/types";
3
3
  import type { MessageContent } from "@langchain/core/messages";
4
- import type { ThreadOps } from "../../../lib/session/types";
4
+ import type {
5
+ ThreadOps,
6
+ PrefixedThreadOps,
7
+ ScopedPrefix,
8
+ } from "../../../lib/session/types";
5
9
  import type { ModelInvoker } from "../../../lib/model";
6
10
  import type { StoredMessage } from "@langchain/core/messages";
7
11
  import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
8
12
  import { createLangChainThreadManager } from "./thread-manager";
9
13
  import { createLangChainModelInvoker } from "./model-invoker";
10
14
 
15
+ const ADAPTER_PREFIX = "langChain" as const;
16
+
17
+ export type LangChainThreadOps<TScope extends string = ""> =
18
+ PrefixedThreadOps<ScopedPrefix<TScope, typeof ADAPTER_PREFIX>>;
19
+
11
20
  export interface LangChainAdapterConfig {
12
21
  redis: Redis;
13
22
  /** Optional default model — if omitted, use `createModelInvoker()` to create invokers later */
@@ -16,22 +25,34 @@ export interface LangChainAdapterConfig {
16
25
  }
17
26
 
18
27
  export interface LangChainAdapter {
19
- /** Thread operations (register these as Temporal activities on the worker) */
20
- threadOps: ThreadOps;
21
28
  /** Model invoker using the default model (only available when `model` was provided) */
22
29
  invoker: ModelInvoker<StoredMessage>;
23
30
  /** Create an invoker for a specific model (for multi-model setups) */
24
31
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
32
  createModelInvoker(model: BaseChatModel<any>): ModelInvoker<StoredMessage>;
33
+ /**
34
+ * Create prefixed thread activities for registration on the worker.
35
+ *
36
+ * @param scope - Workflow name appended to the adapter prefix.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * adapter.createActivities("codingAgent")
41
+ * // → { langChainCodingAgentInitializeThread, langChainCodingAgentAppendHumanMessage, … }
42
+ * ```
43
+ */
44
+ createActivities<S extends string = "">(
45
+ scope?: S
46
+ ): LangChainThreadOps<S>;
26
47
  }
27
48
 
28
49
  /**
29
50
  * Creates a LangChain adapter that bundles thread operations and model
30
51
  * invocation using a consistent message format (StoredMessage).
31
52
  *
32
- * The returned `threadOps` should be registered as Temporal activities on
33
- * the worker. The `invoker` (or invokers created via `createModelInvoker`)
34
- * should be wrapped with `createRunAgentActivity` for per-agent activities.
53
+ * Use `createActivities(scope)` to register scoped thread operations as
54
+ * Temporal activities on the worker. The `invoker` (or invokers created via
55
+ * `createModelInvoker`) should be wrapped with `createRunAgentActivity`.
35
56
  *
36
57
  * @example
37
58
  * ```typescript
@@ -42,21 +63,20 @@ export interface LangChainAdapter {
42
63
  *
43
64
  * export function createActivities(client: WorkflowClient) {
44
65
  * return {
45
- * ...adapter.threadOps,
46
- * runAgent: createRunAgentActivity(client, adapter.invoker),
66
+ * ...adapter.createActivities("codingAgent"),
67
+ * runCodingAgent: createRunAgentActivity(client, adapter.invoker),
47
68
  * };
48
69
  * }
49
70
  * ```
50
71
  *
51
- * @example Multi-model setup
72
+ * @example Multi-agent worker
52
73
  * ```typescript
53
- * const adapter = createLangChainAdapter({ redis });
54
- *
55
74
  * export function createActivities(client: WorkflowClient) {
56
75
  * return {
57
- * ...adapter.threadOps,
76
+ * ...adapter.createActivities("codingAgent"),
77
+ * ...adapter.createActivities("researchAgent"),
78
+ * runCodingAgent: createRunAgentActivity(client, adapter.invoker),
58
79
  * runResearchAgent: createRunAgentActivity(client, adapter.createModelInvoker(claude)),
59
- * runWriterAgent: createRunAgentActivity(client, adapter.createModelInvoker(gpt4)),
60
80
  * };
61
81
  * }
62
82
  * ```
@@ -108,6 +128,19 @@ export function createLangChainAdapter(
108
128
  },
109
129
  };
110
130
 
131
+ function createActivities<S extends string = "">(
132
+ scope?: S
133
+ ): LangChainThreadOps<S> {
134
+ const prefix = scope
135
+ ? `${ADAPTER_PREFIX}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
136
+ : ADAPTER_PREFIX;
137
+ const cap = (s: string): string =>
138
+ s.charAt(0).toUpperCase() + s.slice(1);
139
+ return Object.fromEntries(
140
+ Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
141
+ ) as LangChainThreadOps<S>;
142
+ }
143
+
111
144
  const makeInvoker = (
112
145
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
146
  model: BaseChatModel<any>
@@ -124,7 +157,7 @@ export function createLangChainAdapter(
124
157
  };
125
158
 
126
159
  return {
127
- threadOps,
160
+ createActivities,
128
161
  invoker,
129
162
  createModelInvoker: makeInvoker,
130
163
  };
@@ -20,6 +20,7 @@ export {
20
20
  createLangChainAdapter,
21
21
  type LangChainAdapter,
22
22
  type LangChainAdapterConfig,
23
+ type LangChainThreadOps,
23
24
  } from "./activities";
24
25
 
25
26
  // Thread manager
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Workflow-safe proxy for LangChain thread operations.
3
+ *
4
+ * Import this from `zeitlich/adapters/thread/langchain/workflow`
5
+ * in your Temporal workflow files.
6
+ *
7
+ * By default the scope is derived from `workflowInfo().workflowType`,
8
+ * so activities are automatically namespaced per workflow.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { proxyLangChainThreadOps } from 'zeitlich/adapters/thread/langchain/workflow';
13
+ *
14
+ * // Auto-scoped to the current workflow name
15
+ * const threadOps = proxyLangChainThreadOps();
16
+ *
17
+ * // Explicit scope override
18
+ * const threadOps = proxyLangChainThreadOps("customScope");
19
+ * ```
20
+ */
21
+ import {
22
+ proxyActivities,
23
+ workflowInfo,
24
+ type ActivityInterfaceFor,
25
+ } from "@temporalio/workflow";
26
+ import type { ThreadOps } from "../../../lib/session/types";
27
+
28
+ const ADAPTER_PREFIX = "langChain";
29
+
30
+ export function proxyLangChainThreadOps(
31
+ scope?: string,
32
+ options?: Parameters<typeof proxyActivities>[0]
33
+ ): ActivityInterfaceFor<ThreadOps> {
34
+ const resolvedScope = scope ?? workflowInfo().workflowType;
35
+
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
38
+ options ?? {
39
+ startToCloseTimeout: "10s",
40
+ retry: {
41
+ maximumAttempts: 6,
42
+ initialInterval: "5s",
43
+ maximumInterval: "15m",
44
+ backoffCoefficient: 4,
45
+ },
46
+ }
47
+ );
48
+
49
+ const prefix =
50
+ `${ADAPTER_PREFIX}${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
51
+ const p = (key: string): string =>
52
+ `${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
53
+
54
+ return {
55
+ initializeThread: acts[p("initializeThread")],
56
+ appendHumanMessage: acts[p("appendHumanMessage")],
57
+ appendToolResult: acts[p("appendToolResult")],
58
+ appendSystemMessage: acts[p("appendSystemMessage")],
59
+ forkThread: acts[p("forkThread")],
60
+ } as ActivityInterfaceFor<ThreadOps>;
61
+ }