zeitlich 0.2.13 → 0.2.14

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 (135) hide show
  1. package/README.md +49 -38
  2. package/dist/adapters/sandbox/daytona/index.cjs +205 -0
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -0
  4. package/dist/adapters/sandbox/daytona/index.d.cts +86 -0
  5. package/dist/adapters/sandbox/daytona/index.d.ts +86 -0
  6. package/dist/adapters/sandbox/daytona/index.js +202 -0
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -0
  8. package/dist/adapters/sandbox/inmemory/index.cjs +174 -0
  9. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -0
  10. package/dist/adapters/sandbox/inmemory/index.d.cts +28 -0
  11. package/dist/adapters/sandbox/inmemory/index.d.ts +28 -0
  12. package/dist/adapters/sandbox/inmemory/index.js +172 -0
  13. package/dist/adapters/sandbox/inmemory/index.js.map +1 -0
  14. package/dist/adapters/sandbox/virtual/index.cjs +405 -0
  15. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -0
  16. package/dist/adapters/sandbox/virtual/index.d.cts +85 -0
  17. package/dist/adapters/sandbox/virtual/index.d.ts +85 -0
  18. package/dist/adapters/sandbox/virtual/index.js +400 -0
  19. package/dist/adapters/sandbox/virtual/index.js.map +1 -0
  20. package/dist/adapters/thread/google-genai/index.cjs +284 -0
  21. package/dist/adapters/thread/google-genai/index.cjs.map +1 -0
  22. package/dist/adapters/thread/google-genai/index.d.cts +145 -0
  23. package/dist/adapters/thread/google-genai/index.d.ts +145 -0
  24. package/dist/adapters/thread/google-genai/index.js +278 -0
  25. package/dist/adapters/thread/google-genai/index.js.map +1 -0
  26. package/dist/adapters/{langchain → thread/langchain}/index.cjs +7 -9
  27. package/dist/adapters/thread/langchain/index.cjs.map +1 -0
  28. package/dist/adapters/{langchain → thread/langchain}/index.d.cts +17 -21
  29. package/dist/adapters/{langchain → thread/langchain}/index.d.ts +17 -21
  30. package/dist/adapters/{langchain → thread/langchain}/index.js +7 -9
  31. package/dist/adapters/thread/langchain/index.js.map +1 -0
  32. package/dist/index.cjs +816 -545
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +235 -74
  35. package/dist/index.d.ts +235 -74
  36. package/dist/index.js +804 -540
  37. package/dist/index.js.map +1 -1
  38. package/dist/types-B4C9txdq.d.ts +389 -0
  39. package/dist/{thread-manager-qc0g5Rvd.d.cts → types-B9ljZewB.d.cts} +1 -6
  40. package/dist/{thread-manager-qc0g5Rvd.d.ts → types-B9ljZewB.d.ts} +1 -6
  41. package/dist/types-BMXzv7TN.d.cts +476 -0
  42. package/dist/types-BMXzv7TN.d.ts +476 -0
  43. package/dist/types-BVP87m_W.d.cts +121 -0
  44. package/dist/types-CDubRtad.d.cts +115 -0
  45. package/dist/types-CDubRtad.d.ts +115 -0
  46. package/dist/types-CwwgQ_9H.d.ts +121 -0
  47. package/dist/types-GpMU4b0w.d.cts +389 -0
  48. package/dist/workflow.cjs +444 -318
  49. package/dist/workflow.cjs.map +1 -1
  50. package/dist/workflow.d.cts +271 -222
  51. package/dist/workflow.d.ts +271 -222
  52. package/dist/workflow.js +440 -316
  53. package/dist/workflow.js.map +1 -1
  54. package/package.json +59 -6
  55. package/src/adapters/sandbox/daytona/filesystem.ts +136 -0
  56. package/src/adapters/sandbox/daytona/index.ts +149 -0
  57. package/src/adapters/sandbox/daytona/types.ts +34 -0
  58. package/src/adapters/sandbox/inmemory/index.ts +213 -0
  59. package/src/adapters/sandbox/virtual/filesystem.ts +345 -0
  60. package/src/adapters/sandbox/virtual/index.ts +88 -0
  61. package/src/adapters/sandbox/virtual/mutations.ts +38 -0
  62. package/src/adapters/sandbox/virtual/provider.ts +101 -0
  63. package/src/adapters/sandbox/virtual/tree.ts +82 -0
  64. package/src/adapters/sandbox/virtual/types.ts +127 -0
  65. package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +523 -0
  66. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +91 -0
  67. package/src/adapters/thread/google-genai/activities.ts +121 -0
  68. package/src/adapters/thread/google-genai/index.ts +41 -0
  69. package/src/adapters/thread/google-genai/model-invoker.ts +154 -0
  70. package/src/adapters/thread/google-genai/thread-manager.ts +169 -0
  71. package/src/adapters/{langchain → thread/langchain}/activities.ts +11 -15
  72. package/src/adapters/{langchain → thread/langchain}/index.ts +1 -1
  73. package/src/adapters/{langchain → thread/langchain}/model-invoker.ts +15 -18
  74. package/src/adapters/{langchain → thread/langchain}/thread-manager.ts +1 -1
  75. package/src/index.ts +32 -24
  76. package/src/lib/activity.ts +87 -0
  77. package/src/lib/hooks/index.ts +11 -0
  78. package/src/lib/hooks/types.ts +98 -0
  79. package/src/lib/model/helpers.ts +6 -0
  80. package/src/lib/model/index.ts +13 -0
  81. package/src/lib/{model-invoker.ts → model/types.ts} +18 -1
  82. package/src/lib/sandbox/index.ts +19 -0
  83. package/src/lib/sandbox/manager.ts +76 -0
  84. package/src/lib/sandbox/sandbox.test.ts +158 -0
  85. package/src/lib/{fs.ts → sandbox/tree.ts} +6 -6
  86. package/src/lib/sandbox/types.ts +164 -0
  87. package/src/lib/session/index.ts +11 -0
  88. package/src/lib/{session.ts → session/session.ts} +76 -48
  89. package/src/lib/session/types.ts +93 -0
  90. package/src/lib/skills/fs-provider.ts +16 -15
  91. package/src/lib/skills/handler.ts +31 -0
  92. package/src/lib/skills/index.ts +5 -1
  93. package/src/lib/skills/register.ts +20 -0
  94. package/src/lib/skills/tool.ts +47 -0
  95. package/src/lib/state/index.ts +9 -0
  96. package/src/lib/{state-manager.ts → state/manager.ts} +10 -147
  97. package/src/lib/state/types.ts +134 -0
  98. package/src/lib/subagent/define.ts +71 -0
  99. package/src/lib/subagent/handler.ts +99 -0
  100. package/src/lib/subagent/index.ts +13 -0
  101. package/src/lib/subagent/register.ts +53 -0
  102. package/src/lib/subagent/tool.ts +80 -0
  103. package/src/lib/subagent/types.ts +92 -0
  104. package/src/lib/thread/index.ts +7 -0
  105. package/src/lib/{thread-manager.ts → thread/manager.ts} +1 -33
  106. package/src/lib/thread/types.ts +33 -0
  107. package/src/lib/tool-router/auto-append.ts +55 -0
  108. package/src/lib/tool-router/index.ts +41 -0
  109. package/src/lib/tool-router/router.ts +462 -0
  110. package/src/lib/tool-router/types.ts +478 -0
  111. package/src/lib/tool-router/with-sandbox.ts +70 -0
  112. package/src/lib/types.ts +5 -382
  113. package/src/tools/bash/bash.test.ts +53 -55
  114. package/src/tools/bash/handler.ts +23 -51
  115. package/src/tools/edit/handler.ts +67 -81
  116. package/src/tools/glob/handler.ts +60 -17
  117. package/src/tools/read-file/handler.ts +67 -0
  118. package/src/tools/read-skill/handler.ts +1 -31
  119. package/src/tools/read-skill/tool.ts +5 -47
  120. package/src/tools/subagent/handler.ts +1 -100
  121. package/src/tools/subagent/tool.ts +5 -93
  122. package/src/tools/task-create/handler.ts +1 -1
  123. package/src/tools/task-get/handler.ts +1 -1
  124. package/src/tools/task-list/handler.ts +1 -1
  125. package/src/tools/task-update/handler.ts +1 -1
  126. package/src/tools/write-file/handler.ts +47 -0
  127. package/src/workflow.ts +88 -47
  128. package/tsup.config.ts +8 -1
  129. package/dist/adapters/langchain/index.cjs.map +0 -1
  130. package/dist/adapters/langchain/index.js.map +0 -1
  131. package/dist/model-invoker-y_zlyMqu.d.cts +0 -892
  132. package/dist/model-invoker-y_zlyMqu.d.ts +0 -892
  133. package/src/lib/tool-router.ts +0 -977
  134. package/src/lib/workflow-helpers.ts +0 -50
  135. /package/src/lib/{thread-id.ts → thread/id.ts} +0 -0
@@ -0,0 +1,91 @@
1
+ import type { WorkflowClient } from "@temporalio/client";
2
+ import { queryParentWorkflowState } from "../../../lib/activity";
3
+ import type { ActivityToolHandler } from "../../../lib/tool-router/types";
4
+ import type {
5
+ FileEntryMetadata,
6
+ TreeMutation,
7
+ VirtualSandboxContext,
8
+ VirtualSandboxState,
9
+ } from "./types";
10
+ import type { VirtualSandboxProvider } from "./provider";
11
+ import { createVirtualSandbox } from "./index";
12
+
13
+ /**
14
+ * Wraps a tool handler that needs a virtual sandbox, automatically querying
15
+ * the parent workflow for the current file tree and resolver context.
16
+ *
17
+ * On each invocation the wrapper:
18
+ * 1. Queries the workflow's `AgentState` for `fileTree` and `resolverContext`
19
+ * 2. Creates an ephemeral {@link VirtualSandbox} from tree + provider's resolver
20
+ * 3. Runs the inner handler
21
+ * 4. Returns the handler's result together with any {@link TreeMutation}s
22
+ *
23
+ * The consumer applies mutations back to workflow state via a post-tool hook.
24
+ *
25
+ * @param client - Temporal `WorkflowClient` for querying the parent workflow
26
+ * @param agentName - Agent name (used to derive the state query name)
27
+ * @param provider - {@link VirtualSandboxProvider} (wraps the resolver)
28
+ * @param handler - Inner handler expecting a {@link VirtualSandboxContext}
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { withVirtualSandbox, type VirtualSandboxContext } from 'zeitlich';
33
+ *
34
+ * const readHandler: ActivityToolHandler<FileReadArgs, ReadResult, VirtualSandboxContext> =
35
+ * async (args, { sandbox }) => {
36
+ * const content = await sandbox.fs.readFile(args.path);
37
+ * return { toolResponse: content, data: { path: args.path, content } };
38
+ * };
39
+ *
40
+ * // At activity registration:
41
+ * const provider = new VirtualSandboxProvider(resolver);
42
+ * const handler = withVirtualSandbox(client, "myAgent", provider, readHandler);
43
+ * ```
44
+ */
45
+ export function withVirtualSandbox<
46
+ TArgs,
47
+ TResult,
48
+ TCtx,
49
+ TMeta = FileEntryMetadata,
50
+ >(
51
+ client: WorkflowClient,
52
+ provider: VirtualSandboxProvider<TCtx, TMeta>,
53
+ handler: ActivityToolHandler<
54
+ TArgs,
55
+ TResult,
56
+ VirtualSandboxContext<TCtx, TMeta>
57
+ >
58
+ ): ActivityToolHandler<
59
+ TArgs,
60
+ (TResult & { treeMutations: TreeMutation<TMeta>[] }) | null
61
+ > {
62
+ return async (args, context) => {
63
+ const state =
64
+ await queryParentWorkflowState<VirtualSandboxState<TCtx, TMeta>>(client);
65
+
66
+ const { sandboxId, fileTree, resolverContext } = state;
67
+ if (!fileTree || !sandboxId) {
68
+ return {
69
+ toolResponse: `Error: No fileTree/sandboxId in agent state. The ${context.toolName} tool requires a virtual sandbox.`,
70
+ data: null,
71
+ };
72
+ }
73
+
74
+ const sandbox = createVirtualSandbox(
75
+ sandboxId,
76
+ fileTree,
77
+ provider.resolver,
78
+ resolverContext
79
+ );
80
+ const response = await handler(args, { ...context, sandbox });
81
+ const mutations = sandbox.fs.getMutations();
82
+
83
+ return {
84
+ toolResponse: response.toolResponse,
85
+ data: {
86
+ ...(response.data ?? {}),
87
+ treeMutations: mutations,
88
+ } as TResult & { treeMutations: TreeMutation<TMeta>[] },
89
+ };
90
+ };
91
+ }
@@ -0,0 +1,121 @@
1
+ import type Redis from "ioredis";
2
+ import type { GoogleGenAI, Content } from "@google/genai";
3
+ import type { ToolResultConfig } from "../../../lib/types";
4
+ import type { MessageContent } from "../../../lib/types";
5
+ import type { ThreadOps } from "../../../lib/session/types";
6
+ import type { ModelInvoker } from "../../../lib/model";
7
+ import { createGoogleGenAIThreadManager } from "./thread-manager";
8
+ import { createGoogleGenAIModelInvoker } from "./model-invoker";
9
+
10
+ export interface GoogleGenAIAdapterConfig {
11
+ redis: Redis;
12
+ client: GoogleGenAI;
13
+ /** Default model name (e.g. 'gemini-2.5-flash'). If omitted, use `createModelInvoker()` */
14
+ model?: string;
15
+ }
16
+
17
+ export interface GoogleGenAIAdapter {
18
+ /** Thread operations (register these as Temporal activities on the worker) */
19
+ threadOps: ThreadOps;
20
+ /** Model invoker using the default model (only available when `model` was provided) */
21
+ invoker: ModelInvoker<Content>;
22
+ /** Create an invoker for a specific model name (for multi-model setups) */
23
+ createModelInvoker(model: string): ModelInvoker<Content>;
24
+ }
25
+
26
+ /**
27
+ * Creates a Google GenAI adapter that bundles thread operations and model
28
+ * invocation using the `@google/genai` SDK.
29
+ *
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.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { createGoogleGenAIAdapter } from 'zeitlich/adapters/thread/google-genai';
37
+ * import { createRunAgentActivity } from 'zeitlich';
38
+ * import { GoogleGenAI } from '@google/genai';
39
+ *
40
+ * const client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
41
+ * const adapter = createGoogleGenAIAdapter({ redis, client, model: 'gemini-2.5-flash' });
42
+ *
43
+ * export function createActivities(temporalClient: WorkflowClient) {
44
+ * return {
45
+ * ...adapter.threadOps,
46
+ * runAgent: createRunAgentActivity(temporalClient, adapter.invoker),
47
+ * };
48
+ * }
49
+ * ```
50
+ *
51
+ * @example Multi-model setup
52
+ * ```typescript
53
+ * const adapter = createGoogleGenAIAdapter({ redis, client });
54
+ *
55
+ * export function createActivities(temporalClient: WorkflowClient) {
56
+ * return {
57
+ * ...adapter.threadOps,
58
+ * runResearchAgent: createRunAgentActivity(
59
+ * temporalClient,
60
+ * adapter.createModelInvoker('gemini-2.5-pro'),
61
+ * ),
62
+ * runFastAgent: createRunAgentActivity(
63
+ * temporalClient,
64
+ * adapter.createModelInvoker('gemini-2.5-flash'),
65
+ * ),
66
+ * };
67
+ * }
68
+ * ```
69
+ */
70
+ export function createGoogleGenAIAdapter(
71
+ config: GoogleGenAIAdapterConfig,
72
+ ): GoogleGenAIAdapter {
73
+ const { redis, client } = config;
74
+
75
+ const threadOps: ThreadOps = {
76
+ async initializeThread(threadId: string): Promise<void> {
77
+ const thread = createGoogleGenAIThreadManager({ redis, threadId });
78
+ await thread.initialize();
79
+ },
80
+
81
+ async appendHumanMessage(
82
+ threadId: string,
83
+ content: string | MessageContent,
84
+ ): Promise<void> {
85
+ const thread = createGoogleGenAIThreadManager({ redis, threadId });
86
+ await thread.appendUserMessage(content);
87
+ },
88
+
89
+ async appendSystemMessage(
90
+ threadId: string,
91
+ content: string,
92
+ ): Promise<void> {
93
+ const thread = createGoogleGenAIThreadManager({ redis, threadId });
94
+ await thread.appendSystemMessage(content);
95
+ },
96
+
97
+ async appendToolResult(cfg: ToolResultConfig): Promise<void> {
98
+ const { threadId, toolCallId, toolName, content } = cfg;
99
+ const thread = createGoogleGenAIThreadManager({ redis, threadId });
100
+ await thread.appendToolResult(toolCallId, toolName, content);
101
+ },
102
+ };
103
+
104
+ const makeInvoker = (model: string): ModelInvoker<Content> =>
105
+ createGoogleGenAIModelInvoker({ redis, client, model });
106
+
107
+ const invoker: ModelInvoker<Content> = config.model
108
+ ? makeInvoker(config.model)
109
+ : ((() => {
110
+ throw new Error(
111
+ "No default model provided to createGoogleGenAIAdapter. " +
112
+ "Either pass `model` in the config or use `createModelInvoker(model)` instead.",
113
+ );
114
+ }) as unknown as ModelInvoker<Content>);
115
+
116
+ return {
117
+ threadOps,
118
+ invoker,
119
+ createModelInvoker: makeInvoker,
120
+ };
121
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Google GenAI adapter for Zeitlich.
3
+ *
4
+ * Provides a unified adapter that bundles thread management and model
5
+ * invocation using the `@google/genai` SDK (Gemini).
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import {
10
+ * createGoogleGenAIAdapter,
11
+ * createGoogleGenAIThreadManager,
12
+ * } from 'zeitlich/adapters/thread/google-genai';
13
+ * import { GoogleGenAI } from '@google/genai';
14
+ *
15
+ * const client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
16
+ * const adapter = createGoogleGenAIAdapter({ redis, client, model: 'gemini-2.5-flash' });
17
+ * ```
18
+ */
19
+
20
+ // Adapter (primary API)
21
+ export {
22
+ createGoogleGenAIAdapter,
23
+ type GoogleGenAIAdapter,
24
+ type GoogleGenAIAdapterConfig,
25
+ } from "./activities";
26
+
27
+ // Thread manager
28
+ export {
29
+ createGoogleGenAIThreadManager,
30
+ messageContentToParts,
31
+ type GoogleGenAIThreadManager,
32
+ type GoogleGenAIThreadManagerConfig,
33
+ type StoredContent,
34
+ } from "./thread-manager";
35
+
36
+ // Model invoker (for advanced use — prefer adapter.createModelInvoker)
37
+ export {
38
+ createGoogleGenAIModelInvoker,
39
+ invokeGoogleGenAIModel,
40
+ type GoogleGenAIModelInvokerConfig,
41
+ } from "./model-invoker";
@@ -0,0 +1,154 @@
1
+ import type Redis from "ioredis";
2
+ import type {
3
+ GoogleGenAI,
4
+ Content,
5
+ FunctionDeclaration,
6
+ } from "@google/genai";
7
+ import type { SerializableToolDefinition } from "../../../lib/types";
8
+ import type { AgentResponse } from "../../../lib/model";
9
+ import type { ModelInvokerConfig } from "../../../lib/model";
10
+ import { createGoogleGenAIThreadManager } from "./thread-manager";
11
+
12
+ export interface GoogleGenAIModelInvokerConfig {
13
+ redis: Redis;
14
+ client: GoogleGenAI;
15
+ model: string;
16
+ }
17
+
18
+ function toFunctionDeclarations(
19
+ tools: SerializableToolDefinition[],
20
+ ): FunctionDeclaration[] {
21
+ return tools.map((t) => ({
22
+ name: t.name,
23
+ description: t.description,
24
+ parametersJsonSchema: t.schema,
25
+ }));
26
+ }
27
+
28
+ /**
29
+ * Merge consecutive Content objects sharing the same role.
30
+ * The Gemini API requires alternating user/model turns; without
31
+ * merging, multiple sequential tool-result messages would violate this.
32
+ */
33
+ function mergeConsecutiveContents(contents: Content[]): Content[] {
34
+ const merged: Content[] = [];
35
+ for (const content of contents) {
36
+ const last = merged[merged.length - 1];
37
+ if (last && last.role === content.role) {
38
+ last.parts = [...(last.parts ?? []), ...(content.parts ?? [])];
39
+ } else {
40
+ merged.push({ ...content, parts: [...(content.parts ?? [])] });
41
+ }
42
+ }
43
+ return merged;
44
+ }
45
+
46
+ /**
47
+ * Creates a Google GenAI model invoker that satisfies the generic
48
+ * `ModelInvoker<Content>` contract.
49
+ *
50
+ * Loads the conversation thread from Redis, invokes the Gemini model via
51
+ * `client.models.generateContent`, appends the AI response, and returns
52
+ * a normalised AgentResponse.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * import { createGoogleGenAIModelInvoker } from 'zeitlich/adapters/thread/google-genai';
57
+ * import { createRunAgentActivity } from 'zeitlich';
58
+ * import { GoogleGenAI } from '@google/genai';
59
+ *
60
+ * const client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
61
+ * const invoker = createGoogleGenAIModelInvoker({
62
+ * redis,
63
+ * client,
64
+ * model: 'gemini-2.5-flash',
65
+ * });
66
+ *
67
+ * return { runAgent: createRunAgentActivity(client, invoker) };
68
+ * ```
69
+ */
70
+ export function createGoogleGenAIModelInvoker({
71
+ redis,
72
+ client,
73
+ model,
74
+ }: GoogleGenAIModelInvokerConfig) {
75
+ return async function invokeGoogleGenAIModel(
76
+ config: ModelInvokerConfig,
77
+ ): Promise<AgentResponse<Content>> {
78
+ const { threadId, state } = config;
79
+
80
+ const thread = createGoogleGenAIThreadManager({ redis, threadId });
81
+ const stored = await thread.load();
82
+
83
+ // Separate system instructions from conversation content.
84
+ // Google GenAI takes system instructions via config, not in the contents array.
85
+ let systemInstruction: string | undefined;
86
+ const conversationContents: Content[] = [];
87
+
88
+ for (const item of stored) {
89
+ if (item.content.role === "system") {
90
+ systemInstruction = item.content.parts?.[0]?.text;
91
+ } else {
92
+ conversationContents.push(item.content);
93
+ }
94
+ }
95
+
96
+ const contents = mergeConsecutiveContents(conversationContents);
97
+
98
+ const functionDeclarations = toFunctionDeclarations(state.tools);
99
+ const tools =
100
+ functionDeclarations.length > 0
101
+ ? [{ functionDeclarations }]
102
+ : undefined;
103
+
104
+ const response = await client.models.generateContent({
105
+ model,
106
+ contents,
107
+ config: {
108
+ ...(systemInstruction ? { systemInstruction } : {}),
109
+ ...(tools ? { tools } : {}),
110
+ },
111
+ });
112
+
113
+ const responseParts = response.candidates?.[0]?.content?.parts ?? [];
114
+ const modelContent: Content = { role: "model", parts: responseParts };
115
+
116
+ await thread.appendModelContent(responseParts);
117
+
118
+ const functionCalls = response.functionCalls ?? [];
119
+
120
+ return {
121
+ message: modelContent,
122
+ rawToolCalls: functionCalls.map((fc) => ({
123
+ id: fc.id,
124
+ name: fc.name ?? "",
125
+ args: fc.args ?? {},
126
+ })),
127
+ usage: {
128
+ inputTokens: response.usageMetadata?.promptTokenCount,
129
+ outputTokens: response.usageMetadata?.candidatesTokenCount,
130
+ cachedReadTokens: response.usageMetadata?.cachedContentTokenCount,
131
+ },
132
+ };
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Standalone function for one-shot Google GenAI model invocation.
138
+ * Convenience wrapper around createGoogleGenAIModelInvoker for cases
139
+ * where you don't need to reuse the invoker.
140
+ */
141
+ export async function invokeGoogleGenAIModel({
142
+ redis,
143
+ client,
144
+ model,
145
+ config,
146
+ }: {
147
+ redis: Redis;
148
+ client: GoogleGenAI;
149
+ model: string;
150
+ config: ModelInvokerConfig;
151
+ }): Promise<AgentResponse<Content>> {
152
+ const invoker = createGoogleGenAIModelInvoker({ redis, client, model });
153
+ return invoker(config);
154
+ }
@@ -0,0 +1,169 @@
1
+ import type Redis from "ioredis";
2
+ import type { Content, Part } from "@google/genai";
3
+ import {
4
+ createThreadManager,
5
+ type BaseThreadManager,
6
+ type ThreadManagerConfig,
7
+ } from "../../../lib/thread";
8
+ import type { MessageContent, ToolMessageContent } from "../../../lib/types";
9
+
10
+ /** A Content with a unique ID for idempotent Redis storage */
11
+ export interface StoredContent {
12
+ id: string;
13
+ content: Content;
14
+ }
15
+
16
+ export interface GoogleGenAIThreadManagerConfig {
17
+ redis: Redis;
18
+ threadId: string;
19
+ /** Thread key, defaults to 'messages' */
20
+ key?: string;
21
+ }
22
+
23
+ /** Thread manager with Google GenAI Content convenience helpers */
24
+ export interface GoogleGenAIThreadManager
25
+ extends BaseThreadManager<StoredContent> {
26
+ createUserContent(content: string | MessageContent): StoredContent;
27
+ createSystemContent(content: string): StoredContent;
28
+ createModelContent(parts: Part[]): StoredContent;
29
+ createToolResponseContent(
30
+ toolCallId: string,
31
+ toolName: string,
32
+ content: ToolMessageContent,
33
+ ): StoredContent;
34
+ appendUserMessage(content: string | MessageContent): Promise<void>;
35
+ appendSystemMessage(content: string): Promise<void>;
36
+ appendModelContent(parts: Part[]): Promise<void>;
37
+ appendToolResult(
38
+ toolCallId: string,
39
+ toolName: string,
40
+ content: ToolMessageContent,
41
+ ): Promise<void>;
42
+ }
43
+
44
+ function storedContentId(msg: StoredContent): string {
45
+ return msg.id;
46
+ }
47
+
48
+ /** Convert zeitlich MessageContent to Google GenAI Part[] */
49
+ export function messageContentToParts(
50
+ content: string | MessageContent,
51
+ ): Part[] {
52
+ if (typeof content === "string") {
53
+ return [{ text: content }];
54
+ }
55
+ if (Array.isArray(content)) {
56
+ return content.map((part) => {
57
+ if (part.type === "text") {
58
+ return { text: part.text as string };
59
+ }
60
+ return part as unknown as Part;
61
+ });
62
+ }
63
+ return [{ text: String(content) }];
64
+ }
65
+
66
+ /** Parse ToolMessageContent into a Record suitable for functionResponse */
67
+ function parseToolResponse(
68
+ content: ToolMessageContent,
69
+ ): Record<string, unknown> {
70
+ if (typeof content === "string") {
71
+ try {
72
+ const parsed: unknown = JSON.parse(content);
73
+ return typeof parsed === "object" && parsed !== null
74
+ ? (parsed as Record<string, unknown>)
75
+ : { result: content };
76
+ } catch {
77
+ return { result: content };
78
+ }
79
+ }
80
+ return { result: content };
81
+ }
82
+
83
+ /**
84
+ * Creates a Google GenAI-specific thread manager that stores StoredContent
85
+ * instances in Redis and provides convenience helpers for creating and
86
+ * appending typed Content messages.
87
+ */
88
+ export function createGoogleGenAIThreadManager(
89
+ config: GoogleGenAIThreadManagerConfig,
90
+ ): GoogleGenAIThreadManager {
91
+ const baseConfig: ThreadManagerConfig<StoredContent> = {
92
+ redis: config.redis,
93
+ threadId: config.threadId,
94
+ key: config.key,
95
+ idOf: storedContentId,
96
+ };
97
+
98
+ const base = createThreadManager(baseConfig);
99
+
100
+ const helpers = {
101
+ createUserContent(content: string | MessageContent): StoredContent {
102
+ return {
103
+ id: crypto.randomUUID(),
104
+ content: { role: "user", parts: messageContentToParts(content) },
105
+ };
106
+ },
107
+
108
+ createSystemContent(content: string): StoredContent {
109
+ return {
110
+ id: crypto.randomUUID(),
111
+ content: { role: "system", parts: [{ text: content }] },
112
+ };
113
+ },
114
+
115
+ createModelContent(parts: Part[]): StoredContent {
116
+ return {
117
+ id: crypto.randomUUID(),
118
+ content: { role: "model", parts },
119
+ };
120
+ },
121
+
122
+ createToolResponseContent(
123
+ toolCallId: string,
124
+ toolName: string,
125
+ content: ToolMessageContent,
126
+ ): StoredContent {
127
+ return {
128
+ id: crypto.randomUUID(),
129
+ content: {
130
+ role: "user",
131
+ parts: [
132
+ {
133
+ functionResponse: {
134
+ id: toolCallId,
135
+ name: toolName,
136
+ response: parseToolResponse(content),
137
+ },
138
+ },
139
+ ],
140
+ },
141
+ };
142
+ },
143
+
144
+ async appendUserMessage(content: string | MessageContent): Promise<void> {
145
+ await base.append([helpers.createUserContent(content)]);
146
+ },
147
+
148
+ async appendSystemMessage(content: string): Promise<void> {
149
+ await base.initialize();
150
+ await base.append([helpers.createSystemContent(content)]);
151
+ },
152
+
153
+ async appendModelContent(parts: Part[]): Promise<void> {
154
+ await base.append([helpers.createModelContent(parts)]);
155
+ },
156
+
157
+ async appendToolResult(
158
+ toolCallId: string,
159
+ toolName: string,
160
+ content: ToolMessageContent,
161
+ ): Promise<void> {
162
+ await base.append([
163
+ helpers.createToolResponseContent(toolCallId, toolName, content),
164
+ ]);
165
+ },
166
+ };
167
+
168
+ return Object.assign(base, helpers);
169
+ }
@@ -1,24 +1,18 @@
1
1
  import type Redis from "ioredis";
2
- import type { ThreadOps, ToolResultConfig } from "../../lib/types";
2
+ import type { ToolResultConfig } from "../../../lib/types";
3
3
  import type { MessageContent } from "@langchain/core/messages";
4
- import type { ModelInvoker } from "../../lib/model-invoker";
4
+ import type { ThreadOps } from "../../../lib/session/types";
5
+ import type { ModelInvoker } from "../../../lib/model";
5
6
  import type { StoredMessage } from "@langchain/core/messages";
6
- import type {
7
- BaseChatModel,
8
- BaseChatModelCallOptions,
9
- BindToolsInput,
10
- } from "@langchain/core/language_models/chat_models";
7
+ import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
11
8
  import { createLangChainThreadManager } from "./thread-manager";
12
9
  import { createLangChainModelInvoker } from "./model-invoker";
13
10
 
14
- type LangChainModel = BaseChatModel<
15
- BaseChatModelCallOptions & { tools?: BindToolsInput }
16
- >;
17
-
18
11
  export interface LangChainAdapterConfig {
19
12
  redis: Redis;
20
13
  /** Optional default model — if omitted, use `createModelInvoker()` to create invokers later */
21
- model?: LangChainModel;
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ model?: BaseChatModel<any>;
22
16
  }
23
17
 
24
18
  export interface LangChainAdapter {
@@ -27,7 +21,8 @@ export interface LangChainAdapter {
27
21
  /** Model invoker using the default model (only available when `model` was provided) */
28
22
  invoker: ModelInvoker<StoredMessage>;
29
23
  /** Create an invoker for a specific model (for multi-model setups) */
30
- createModelInvoker(model: LangChainModel): ModelInvoker<StoredMessage>;
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ createModelInvoker(model: BaseChatModel<any>): ModelInvoker<StoredMessage>;
31
26
  }
32
27
 
33
28
  /**
@@ -40,7 +35,7 @@ export interface LangChainAdapter {
40
35
  *
41
36
  * @example
42
37
  * ```typescript
43
- * import { createLangChainAdapter } from 'zeitlich/adapters/langchain';
38
+ * import { createLangChainAdapter } from 'zeitlich/adapters/thread/langchain';
44
39
  * import { createRunAgentActivity } from 'zeitlich';
45
40
  *
46
41
  * const adapter = createLangChainAdapter({ redis, model });
@@ -100,7 +95,8 @@ export function createLangChainAdapter(
100
95
  },
101
96
  };
102
97
 
103
- const makeInvoker = (model: LangChainModel): ModelInvoker<StoredMessage> =>
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ const makeInvoker = (model: BaseChatModel<any>): ModelInvoker<StoredMessage> =>
104
100
  createLangChainModelInvoker({ redis, model });
105
101
 
106
102
  const invoker: ModelInvoker<StoredMessage> = config.model
@@ -9,7 +9,7 @@
9
9
  * import {
10
10
  * createLangChainAdapter,
11
11
  * createLangChainThreadManager,
12
- * } from 'zeitlich/adapters/langchain';
12
+ * } from 'zeitlich/adapters/thread/langchain';
13
13
  *
14
14
  * const adapter = createLangChainAdapter({ redis, model });
15
15
  * ```