-
Notifications
You must be signed in to change notification settings - Fork 197
feat(google-adk-agents): add @temporalio/google-adk-agents package #2120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
869b5ed
217623f
d3c4928
af3b272
9cc8634
6c14301
325a2c9
1238e34
a5c365f
bf245c3
a21bb40
b9c733a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| # @temporalio/google-adk-agents | ||
|
|
||
| Run [Google Agent Development Kit](https://github.com/google/adk-js) (`@google/adk`) | ||
| agents as durable [Temporal](https://temporal.io) Workflows. | ||
|
|
||
| Your ADK agent graph — `LlmAgent`, `SequentialAgent`/`ParallelAgent`/`LoopAgent`, | ||
| `FunctionTool`s, `MCPToolset`s, the `Runner` loop — runs **inside the Workflow** | ||
| and replays deterministically. Only the non-deterministic I/O boundaries are | ||
| routed out to Activities: | ||
|
|
||
| - every **model call** (`generateContentAsync`) becomes a retryable, observable | ||
| Activity, and | ||
| - every **MCP tool call** (list-tools / call-tool) becomes an Activity. | ||
|
|
||
| Regular `FunctionTool`s run in the Workflow; to have a tool run as an Activity | ||
| instead, expose an existing Temporal Activity to the agent with `activityAsTool`. | ||
|
|
||
| Temporal then gives you automatic retries, timeouts, heartbeating, and | ||
| crash-safe replay for the whole run. | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| npm install @temporalio/google-adk-agents | ||
| ``` | ||
|
|
||
| Peer dependency: `@google/adk` `^1.2.0` (and its `@google/genai`). Provide your | ||
| Gemini credentials to the **worker** as usual (e.g. `GOOGLE_API_KEY` / | ||
| `GEMINI_API_KEY`) — credentials are never placed in workflow or activity inputs. | ||
|
|
||
| ## Hello world | ||
|
|
||
| Take an agent you already have and change **one line** — wrap its model in | ||
| `TemporalModel` — then register the plugin. | ||
|
|
||
| ### `workflows.ts` | ||
|
|
||
| ```typescript | ||
| import { InMemoryRunner, LlmAgent, isFinalResponse, stringifyContent } from '@google/adk'; | ||
| import { TemporalModel } from '@temporalio/google-adk-agents/workflow'; | ||
|
|
||
| export async function askAgent(prompt: string): Promise<string> { | ||
| const agent = new LlmAgent({ | ||
| name: 'assistant', | ||
| // The only change from a vanilla ADK agent: | ||
| model: new TemporalModel('gemini-2.5-flash'), | ||
| instruction: 'You are a helpful assistant.', | ||
| }); | ||
|
|
||
| const runner = new InMemoryRunner({ agent }); | ||
|
|
||
| let text = ''; | ||
| for await (const event of runner.runEphemeral({ | ||
| userId: 'user', | ||
| newMessage: { role: 'user', parts: [{ text: prompt }] }, | ||
| })) { | ||
| if (isFinalResponse(event)) text = stringifyContent(event); | ||
| } | ||
| return text; | ||
| } | ||
| ``` | ||
|
|
||
| ### `worker.ts` | ||
|
|
||
| ```typescript | ||
| import { Worker } from '@temporalio/worker'; | ||
| import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; | ||
|
|
||
| const worker = await Worker.create({ | ||
| taskQueue: 'adk', | ||
| workflowsPath: require.resolve('./workflows'), | ||
| // Registers invokeModel / invokeModelStreaming for you. | ||
| plugins: [new GoogleAdkPlugin()], | ||
| }); | ||
| await worker.run(); | ||
| ``` | ||
|
|
||
| ### `client.ts` | ||
|
|
||
| ```typescript | ||
| import { Client } from '@temporalio/client'; | ||
| import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; | ||
| import { askAgent } from './workflows'; | ||
|
|
||
| // Passing the plugin to the Client auto-propagates it to Workers created from | ||
| // this Client — register it on EITHER the Client OR the Worker, not both. | ||
| const client = new Client({ plugins: [new GoogleAdkPlugin()] }); | ||
|
|
||
| const result = await client.workflow.execute(askAgent, { | ||
| taskQueue: 'adk', | ||
| workflowId: 'adk-hello', | ||
| args: ['Write a haiku about durable execution.'], | ||
| }); | ||
| console.log(result); | ||
| ``` | ||
|
|
||
| ## What this plugin gives you | ||
|
|
||
| - **Durable model calls.** Swap `model: 'gemini-2.5-flash'` for | ||
| `model: new TemporalModel('gemini-2.5-flash')` and each inference runs as a | ||
| Temporal Activity with a per-model `RetryPolicy`, `startToCloseTimeout`, and | ||
| auto-heartbeat for slow / thinking-mode calls. Upstream `429`/`5xx` and | ||
| `retry-after` headers are honored; non-retryable `4xx` fail fast. | ||
| - **Durable MCP tools.** `new TemporalMCPToolset({ name })` routes tool | ||
| discovery and tool calls through Activities. The full tool schema (name, | ||
| description, **parameters**) round-trips, so the model still sees argument | ||
| schemas. MCP connection params stay on the worker: | ||
|
|
||
| ```typescript | ||
| // worker | ||
| new GoogleAdkPlugin({ | ||
| mcpToolsets: { | ||
| filesystem: () => ({ | ||
| type: 'StdioConnectionParams', | ||
| serverParams: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/data'] }, | ||
| }), | ||
| }, | ||
| }); | ||
|
|
||
| // workflow / agent | ||
| const agent = new LlmAgent({ | ||
| name: 'fs', | ||
| model: new TemporalModel('gemini-2.5-flash'), | ||
| tools: [new TemporalMCPToolset({ name: 'filesystem' })], | ||
| }); | ||
| ``` | ||
|
|
||
| - **Existing Activities as tools.** Already have a Temporal Activity? Expose it | ||
| to the agent with `activityAsTool` instead of re-declaring it: | ||
|
|
||
| ```typescript | ||
| import { activityAsTool } from '@temporalio/google-adk-agents/workflow'; | ||
| import { Type } from '@google/genai'; | ||
|
|
||
| const lookupTool = activityAsTool({ | ||
| name: 'lookupOrder', // a registered Activity on your worker | ||
| description: 'Look up an order by id.', | ||
| parameters: { type: Type.OBJECT, properties: { orderId: { type: Type.STRING } } }, | ||
| }); | ||
| ``` | ||
|
|
||
| - **Streaming (SSE).** Streaming requires `streamingTopic` on `TemporalModel`: | ||
| set it to publish incremental `LlmResponse` chunks via | ||
| `@temporalio/workflow-streams` while the Workflow still receives the full | ||
| transcript. Requesting streaming without a `streamingTopic` throws a | ||
| non-retryable `GoogleAdkStreamingTopicRequired` error. | ||
| - **Human-in-the-loop.** Because the agent loop runs in the Workflow body, a | ||
| `LongRunningFunctionTool` can `await` a Temporal Signal or Update carrying a | ||
| human's result — no special shim required. | ||
| - **Deterministic replay.** ADK's event IDs and timestamps funnel through | ||
| `Math.random()` / `Date.now()`, which the Temporal Workflow sandbox makes | ||
| deterministic — so the agent loop replays without a custom determinism hook. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would a custom determinism hook entail? Since we don't have one, maybe we can just leave that part out so we don't confuse anyone. |
||
|
|
||
| ### Testing your workflows | ||
|
|
||
| Import test doubles from the `./testing` entry point to unit-test agents | ||
| without a live model or MCP server: | ||
|
|
||
| ```typescript | ||
| import { fakeModelProvider, mockMCPToolset } from '@temporalio/google-adk-agents/testing'; | ||
|
|
||
| const plugin = new GoogleAdkPlugin({ | ||
| modelProvider: fakeModelProvider(), | ||
| mcpToolsets: { | ||
| weather: mockMCPToolset([ | ||
| /* tool defs */ | ||
| ]), | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## Under the hood | ||
|
|
||
| ### Retries | ||
|
|
||
| Temporal's `RetryPolicy` is the **sole** retry authority for model calls. Inside | ||
| `invokeModel`, the plugin pins the reconstructed `@google/genai` client's | ||
| `httpOptions.retryOptions.attempts = 1` so the SDK does not run a second retry | ||
| loop _inside_ each Activity attempt (which would multiply latency/request volume | ||
| and hide the real failure from Temporal). One Activity attempt is exactly one | ||
| model request; Temporal owns backoff, `retry-after` handling, and the retry | ||
| budget. Tune it per model via `TemporalModelOptions.activity.retry`. | ||
|
|
||
| ## Composing with other plugins | ||
|
|
||
| Temporal applies `plugins: [...]` in order. Place observability and governance | ||
| plugins **before** this one so model/tool Activities are wrapped by them: | ||
|
|
||
| ```typescript | ||
| new Client({ | ||
| plugins: [ | ||
| new OpenTelemetryPlugin(), // 1. observability (outermost) | ||
| new GovernancePlugin(), // 2. governance | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have an example plugin for this? If not, rename OpenTelemetryPlugin() to ObservabilityPlugin() so users aren't looking for this. |
||
| new GoogleAdkPlugin(), // 3. this plugin | ||
| ], | ||
| }); | ||
| ``` | ||
|
|
||
| This plugin carries no trace context of its own — compose it with | ||
| `@temporalio/interceptors-opentelemetry` for distributed tracing across the | ||
| model/tool Activity boundary. | ||
|
|
||
| ## License | ||
|
|
||
| MIT | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| { | ||
| "name": "@temporalio/google-adk-agents", | ||
| "version": "0.1.0", | ||
| "description": "Temporal plugin for the Google Agent Development Kit (@google/adk): run ADK agents as durable Temporal Workflows.", | ||
| "main": "./lib/index.js", | ||
| "types": "./lib/index.d.ts", | ||
| "typesVersions": { | ||
| "*": { | ||
| "workflow": [ | ||
| "./lib/workflow.d.ts" | ||
| ], | ||
| "testing": [ | ||
| "./lib/testing.d.ts" | ||
| ] | ||
| } | ||
| }, | ||
| "exports": { | ||
| ".": { | ||
| "types": "./lib/index.d.ts", | ||
| "import": "./lib/index.js", | ||
| "require": "./lib/index.js", | ||
| "default": "./lib/index.js" | ||
| }, | ||
| "./workflow": { | ||
| "types": "./lib/workflow.d.ts", | ||
| "import": "./lib/workflow.js", | ||
| "require": "./lib/workflow.js", | ||
| "default": "./lib/workflow.js" | ||
| }, | ||
| "./testing": { | ||
| "types": "./lib/testing.d.ts", | ||
| "import": "./lib/testing.js", | ||
| "require": "./lib/testing.js", | ||
| "default": "./lib/testing.js" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc --build", | ||
| "build.watch": "tsc --build --watch", | ||
| "test": "ava ./lib/__tests__/*.test.js" | ||
| }, | ||
| "ava": { | ||
| "timeout": "120s", | ||
| "concurrency": 1, | ||
| "workerThreads": false | ||
| }, | ||
| "keywords": [ | ||
| "temporal", | ||
| "google", | ||
| "adk", | ||
| "agents", | ||
| "ai", | ||
| "durable", | ||
| "workflow" | ||
| ], | ||
| "author": "Temporal Technologies Inc. <sdk@temporal.io>", | ||
| "license": "MIT", | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "dependencies": { | ||
| "@temporalio/activity": "workspace:*", | ||
| "@temporalio/common": "workspace:*", | ||
| "@temporalio/plugin": "workspace:*", | ||
| "@temporalio/workflow": "workspace:*", | ||
| "@temporalio/workflow-streams": "workspace:*", | ||
| "@ungap/structured-clone": "^1.2.0", | ||
| "headers-polyfill": "^4.0.0", | ||
| "web-streams-polyfill": "^4.0.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "@google/adk": "^1.2.0", | ||
| "@google/genai": "^2.8.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@google/adk": "^1.2.0", | ||
| "@google/genai": "^2.8.0", | ||
| "@temporalio/client": "workspace:*", | ||
| "@temporalio/testing": "workspace:*", | ||
| "@temporalio/worker": "workspace:*", | ||
| "@types/node": "^20.0.0", | ||
| "ava": "^5.3.1", | ||
| "typescript": "^5.4.0" | ||
| }, | ||
| "engines": { | ||
| "node": ">= 20.0.0" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/temporalio/sdk-typescript/issues" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/temporalio/sdk-typescript.git", | ||
| "directory": "contrib/google-adk-agents" | ||
| }, | ||
| "homepage": "https://github.com/temporalio/sdk-typescript/tree/main/contrib/google-adk-agents", | ||
| "files": [ | ||
| "src", | ||
| "lib", | ||
| "!src/__tests__", | ||
| "!lib/__tests__" | ||
| ] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this have its own section with a code snippet?