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.
- package/README.md +49 -38
- package/dist/adapters/sandbox/daytona/index.cjs +205 -0
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -0
- package/dist/adapters/sandbox/daytona/index.d.cts +86 -0
- package/dist/adapters/sandbox/daytona/index.d.ts +86 -0
- package/dist/adapters/sandbox/daytona/index.js +202 -0
- package/dist/adapters/sandbox/daytona/index.js.map +1 -0
- package/dist/adapters/sandbox/inmemory/index.cjs +174 -0
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -0
- package/dist/adapters/sandbox/inmemory/index.d.cts +28 -0
- package/dist/adapters/sandbox/inmemory/index.d.ts +28 -0
- package/dist/adapters/sandbox/inmemory/index.js +172 -0
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -0
- package/dist/adapters/sandbox/virtual/index.cjs +405 -0
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -0
- package/dist/adapters/sandbox/virtual/index.d.cts +85 -0
- package/dist/adapters/sandbox/virtual/index.d.ts +85 -0
- package/dist/adapters/sandbox/virtual/index.js +400 -0
- package/dist/adapters/sandbox/virtual/index.js.map +1 -0
- package/dist/adapters/thread/google-genai/index.cjs +284 -0
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -0
- package/dist/adapters/thread/google-genai/index.d.cts +145 -0
- package/dist/adapters/thread/google-genai/index.d.ts +145 -0
- package/dist/adapters/thread/google-genai/index.js +278 -0
- package/dist/adapters/thread/google-genai/index.js.map +1 -0
- package/dist/adapters/{langchain → thread/langchain}/index.cjs +7 -9
- package/dist/adapters/thread/langchain/index.cjs.map +1 -0
- package/dist/adapters/{langchain → thread/langchain}/index.d.cts +17 -21
- package/dist/adapters/{langchain → thread/langchain}/index.d.ts +17 -21
- package/dist/adapters/{langchain → thread/langchain}/index.js +7 -9
- package/dist/adapters/thread/langchain/index.js.map +1 -0
- package/dist/index.cjs +816 -545
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +235 -74
- package/dist/index.d.ts +235 -74
- package/dist/index.js +804 -540
- package/dist/index.js.map +1 -1
- package/dist/types-B4C9txdq.d.ts +389 -0
- package/dist/{thread-manager-qc0g5Rvd.d.cts → types-B9ljZewB.d.cts} +1 -6
- package/dist/{thread-manager-qc0g5Rvd.d.ts → types-B9ljZewB.d.ts} +1 -6
- package/dist/types-BMXzv7TN.d.cts +476 -0
- package/dist/types-BMXzv7TN.d.ts +476 -0
- package/dist/types-BVP87m_W.d.cts +121 -0
- package/dist/types-CDubRtad.d.cts +115 -0
- package/dist/types-CDubRtad.d.ts +115 -0
- package/dist/types-CwwgQ_9H.d.ts +121 -0
- package/dist/types-GpMU4b0w.d.cts +389 -0
- package/dist/workflow.cjs +444 -318
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +271 -222
- package/dist/workflow.d.ts +271 -222
- package/dist/workflow.js +440 -316
- package/dist/workflow.js.map +1 -1
- package/package.json +59 -6
- package/src/adapters/sandbox/daytona/filesystem.ts +136 -0
- package/src/adapters/sandbox/daytona/index.ts +149 -0
- package/src/adapters/sandbox/daytona/types.ts +34 -0
- package/src/adapters/sandbox/inmemory/index.ts +213 -0
- package/src/adapters/sandbox/virtual/filesystem.ts +345 -0
- package/src/adapters/sandbox/virtual/index.ts +88 -0
- package/src/adapters/sandbox/virtual/mutations.ts +38 -0
- package/src/adapters/sandbox/virtual/provider.ts +101 -0
- package/src/adapters/sandbox/virtual/tree.ts +82 -0
- package/src/adapters/sandbox/virtual/types.ts +127 -0
- package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +523 -0
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +91 -0
- package/src/adapters/thread/google-genai/activities.ts +121 -0
- package/src/adapters/thread/google-genai/index.ts +41 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +154 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +169 -0
- package/src/adapters/{langchain → thread/langchain}/activities.ts +11 -15
- package/src/adapters/{langchain → thread/langchain}/index.ts +1 -1
- package/src/adapters/{langchain → thread/langchain}/model-invoker.ts +15 -18
- package/src/adapters/{langchain → thread/langchain}/thread-manager.ts +1 -1
- package/src/index.ts +32 -24
- package/src/lib/activity.ts +87 -0
- package/src/lib/hooks/index.ts +11 -0
- package/src/lib/hooks/types.ts +98 -0
- package/src/lib/model/helpers.ts +6 -0
- package/src/lib/model/index.ts +13 -0
- package/src/lib/{model-invoker.ts → model/types.ts} +18 -1
- package/src/lib/sandbox/index.ts +19 -0
- package/src/lib/sandbox/manager.ts +76 -0
- package/src/lib/sandbox/sandbox.test.ts +158 -0
- package/src/lib/{fs.ts → sandbox/tree.ts} +6 -6
- package/src/lib/sandbox/types.ts +164 -0
- package/src/lib/session/index.ts +11 -0
- package/src/lib/{session.ts → session/session.ts} +76 -48
- package/src/lib/session/types.ts +93 -0
- package/src/lib/skills/fs-provider.ts +16 -15
- package/src/lib/skills/handler.ts +31 -0
- package/src/lib/skills/index.ts +5 -1
- package/src/lib/skills/register.ts +20 -0
- package/src/lib/skills/tool.ts +47 -0
- package/src/lib/state/index.ts +9 -0
- package/src/lib/{state-manager.ts → state/manager.ts} +10 -147
- package/src/lib/state/types.ts +134 -0
- package/src/lib/subagent/define.ts +71 -0
- package/src/lib/subagent/handler.ts +99 -0
- package/src/lib/subagent/index.ts +13 -0
- package/src/lib/subagent/register.ts +53 -0
- package/src/lib/subagent/tool.ts +80 -0
- package/src/lib/subagent/types.ts +92 -0
- package/src/lib/thread/index.ts +7 -0
- package/src/lib/{thread-manager.ts → thread/manager.ts} +1 -33
- package/src/lib/thread/types.ts +33 -0
- package/src/lib/tool-router/auto-append.ts +55 -0
- package/src/lib/tool-router/index.ts +41 -0
- package/src/lib/tool-router/router.ts +462 -0
- package/src/lib/tool-router/types.ts +478 -0
- package/src/lib/tool-router/with-sandbox.ts +70 -0
- package/src/lib/types.ts +5 -382
- package/src/tools/bash/bash.test.ts +53 -55
- package/src/tools/bash/handler.ts +23 -51
- package/src/tools/edit/handler.ts +67 -81
- package/src/tools/glob/handler.ts +60 -17
- package/src/tools/read-file/handler.ts +67 -0
- package/src/tools/read-skill/handler.ts +1 -31
- package/src/tools/read-skill/tool.ts +5 -47
- package/src/tools/subagent/handler.ts +1 -100
- package/src/tools/subagent/tool.ts +5 -93
- package/src/tools/task-create/handler.ts +1 -1
- package/src/tools/task-get/handler.ts +1 -1
- package/src/tools/task-list/handler.ts +1 -1
- package/src/tools/task-update/handler.ts +1 -1
- package/src/tools/write-file/handler.ts +47 -0
- package/src/workflow.ts +88 -47
- package/tsup.config.ts +8 -1
- package/dist/adapters/langchain/index.cjs.map +0 -1
- package/dist/adapters/langchain/index.js.map +0 -1
- package/dist/model-invoker-y_zlyMqu.d.cts +0 -892
- package/dist/model-invoker-y_zlyMqu.d.ts +0 -892
- package/src/lib/tool-router.ts +0 -977
- package/src/lib/workflow-helpers.ts +0 -50
- /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 {
|
|
2
|
+
import type { ToolResultConfig } from "../../../lib/types";
|
|
3
3
|
import type { MessageContent } from "@langchain/core/messages";
|
|
4
|
-
import type {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|