Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .scripts/copy-shared-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const TSCONFIG_EXCLUDE = [
'production',
'hello-world-js',
'food-delivery',
'google-adk-agents',
'lambda-worker',
'nestjs-exchange-rates',
'empty',
Expand All @@ -48,6 +49,7 @@ const ESLINTRC_EXCLUDE = [
'hello-world-js',
'protobufs',
'food-delivery',
'google-adk-agents',
'nestjs-exchange-rates',
];
const ESLINTIGNORE_EXCLUDE = [
Expand Down
1 change: 1 addition & 0 deletions .scripts/list-of-samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"expense",
"fetch-esm",
"food-delivery",
"google-adk-agents",
"grpc-calls",
"hello-world",
"hello-world-js",
Expand Down
3 changes: 3 additions & 0 deletions google-adk-agents/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
lib
.eslintrc.js
48 changes: 48 additions & 0 deletions google-adk-agents/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { builtinModules } = require('module');

const ALLOWED_NODE_BUILTINS = new Set(['assert']);

module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'deprecation'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
rules: {
// recommended for safety
'@typescript-eslint/no-floating-promises': 'error', // forgetting to await Activities and Workflow APIs is bad
'deprecation/deprecation': 'warn',

// code style preference
'object-shorthand': ['error', 'always'],

// relaxed rules, for convenience
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'off',
},
overrides: [
{
files: ['*/src/workflows.ts', '*/src/workflows-*.ts', '*/src/workflows/*.ts'],
rules: {
'no-restricted-imports': [
'error',
...builtinModules.filter((m) => !ALLOWED_NODE_BUILTINS.has(m)).flatMap((m) => [m, `node:${m}`]),
],
},
},
],
};
2 changes: 2 additions & 0 deletions google-adk-agents/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib
node_modules
1 change: 1 addition & 0 deletions google-adk-agents/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
1 change: 1 addition & 0 deletions google-adk-agents/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
18 changes: 18 additions & 0 deletions google-adk-agents/.post-create
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
To begin development, install the Temporal CLI:

Mac: {cyan brew install temporal}
Other: Download and extract the latest release from https://github.com/temporalio/cli/releases/latest

Start Temporal Server:

{cyan temporal server start-dev}

Use Node version 18+ (v22.x is recommended):

Mac: {cyan brew install node@22}
Other: https://nodejs.org/en/download/

Then, in the project directory, using two other shells, run these commands:

{cyan npm run start.watch}
{cyan npm run workflow}
1 change: 1 addition & 0 deletions google-adk-agents/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
2 changes: 2 additions & 0 deletions google-adk-agents/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
printWidth: 120
singleQuote: true
27 changes: 27 additions & 0 deletions google-adk-agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Google ADK Agents

These samples use the `@temporalio/google-adk-agents` integration to run [Google Agent Development Kit](https://github.com/google/adk-js) (`@google/adk`) agents as durable Temporal Workflows. The ADK agent graph — the `Runner` loop, `LlmAgent`s, tools, and MCP toolsets — runs inside the Workflow and replays deterministically, while the non-deterministic I/O boundaries (every model call and every MCP tool call) run as durable Activities, so they retry on failure and are not repeated during Workflow replay.

This is a single project: one `package.json` and one set of configs at the `google-adk-agents/` root, with each scenario in its own subdirectory. Run `npm install` once here, then run any scenario by path (see each scenario's README). The integration package itself is documented in the [`@temporalio/google-adk-agents` README](https://github.com/temporalio/sdk-typescript/tree/main/contrib/google-adk-agents).

## Prerequisites

These apply to every sample in this directory:

- A running Temporal dev server: `temporal server start-dev`.
- Node 22 or later.
- A Gemini API key for live runs: `export GOOGLE_API_KEY=...`.
- Dependencies installed once at the `google-adk-agents/` root: `npm install`.

Each scenario's README describes how to start its Worker and run its scenarios by path.

## Samples

| Sample | Demonstrates |
| :----------------------------------- | :------------------------------------------------------------------------------------------------------------ |
| [`basic`](./basic) | A single `LlmAgent` whose model is a `TemporalModel`, driven by `InMemoryRunner` for one durable model call. |
| [`tools`](./tools) | An existing Temporal Activity exposed to the agent as an ADK tool via `activityAsTool`. |
| [`agent-patterns`](./agent-patterns) | A coordinator `LlmAgent` that delegates to sub-agents, each with its own `TemporalModel`. |
| [`mcp`](./mcp) | A `TemporalMcpToolSet` backed by an `mcpToolsets` factory on the plugin (a filesystem MCP server over stdio). |
| [`streaming`](./streaming) | Token streaming via `TemporalModel` `streamingTopic` and the Workflow streams API. |
| [`human-approval`](./human-approval) | A `LongRunningFunctionTool` whose completion is gated by a Temporal Signal or Update. |
23 changes: 23 additions & 0 deletions google-adk-agents/agent-patterns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Google ADK Agents: Agent Patterns

A coordinator `LlmAgent` that delegates to sub-agents (a researcher and a writer) via ADK's built-in `transfer_to_agent` handoff. Each agent has its own `TemporalModel` with a `summary`, so every model turn shows up as a named Activity in Workflow history. The whole multi-agent handoff runs durably inside the Workflow.

## Run

Run these from the `google-adk-agents/` root (run `npm install` there once first).

```bash
# In one terminal, start the Worker (requires a local Temporal server and GOOGLE_API_KEY):
GOOGLE_API_KEY=... npx ts-node agent-patterns/src/worker.ts

# In another terminal, run the scenario:
npx ts-node agent-patterns/src/client.ts
```

## Test

```bash
npx mocha --exit --require ts-node/register --require source-map-support/register "agent-patterns/src/mocha/*.test.ts"
```

The test runs a real Worker against `TestWorkflowEnvironment` with `fakeModelProvider`, so no `GOOGLE_API_KEY` is required.
22 changes: 22 additions & 0 deletions google-adk-agents/agent-patterns/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Connection, Client } from '@temporalio/client';
import { GoogleAdkPlugin } from '@temporalio/google-adk-agents';
import { nanoid } from 'nanoid';
import { multiAgent } from './workflows';

async function run() {
const connection = await Connection.connect();
const client = new Client({ connection, plugins: [new GoogleAdkPlugin()] });

const result = await client.workflow.execute(multiAgent, {
taskQueue: 'google-adk-agent-patterns',
workflowId: 'google-adk-agent-patterns-' + nanoid(),
args: ['durable execution'],
});

console.log(result);
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
80 changes: 80 additions & 0 deletions google-adk-agents/agent-patterns/src/mocha/workflows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import { GoogleAdkPlugin } from '@temporalio/google-adk-agents';
import { BaseLlm } from '@google/adk';
import type { BaseLlmConnection, LlmRequest, LlmResponse } from '@google/adk';
import { after, before, describe, it } from 'mocha';
import assert from 'assert';
import { multiAgent } from '../workflows';

// A plain-text model turn.
function text(s: string): LlmResponse {
return { content: { role: 'model', parts: [{ text: s }] }, turnComplete: true };
}

// A single function-call turn. ADK's JS `transfer_to_agent` tool reads
// `args.agentName` (camelCase), so delegation is driven via that key.
function toolCall(name: string, args: Record<string, unknown>): LlmResponse {
return { content: { role: 'model', parts: [{ functionCall: { name, args } }] }, turnComplete: true };
}

// BaseLlm test double: each invokeModel Activity call shifts one response off the shared script, so a delegation chain sees successive scripted turns.
function scriptedModelProvider(script: LlmResponse[]): (model: string) => BaseLlm {
class ScriptedLlm extends BaseLlm {
override async *generateContentAsync(
_llmRequest: LlmRequest,
_stream = false,
_abortSignal?: AbortSignal,
): AsyncGenerator<LlmResponse, void> {
const next = script.shift();
if (next === undefined) {
throw new Error('scripted model script exhausted');
}
yield next;
}

override async connect(_llmRequest: LlmRequest): Promise<BaseLlmConnection> {
throw new Error('ScriptedLlm does not support connect().');
}
}
return (model: string) => new ScriptedLlm({ model });
}

describe('google-adk-agents/agent-patterns workflow scenarios', function () {
this.timeout(30_000);

let testEnv: TestWorkflowEnvironment;

before(async () => {
testEnv = await TestWorkflowEnvironment.createLocal();
});

after(async () => {
await testEnv?.teardown();
});

it('multiAgent: coordinator delegates to researcher then writer', async () => {
// Reaches the writer's text only if subAgents delegation actually fires: coordinator -> researcher -> writer -> final text.
const modelProvider = scriptedModelProvider([
toolCall('transfer_to_agent', { agentName: 'researcher' }),
toolCall('transfer_to_agent', { agentName: 'writer' }),
text('snow on the mountain'),
]);

const taskQueue = 'test-google-adk-agent-patterns';
const worker = await Worker.create({
connection: testEnv.nativeConnection,
taskQueue,
workflowsPath: require.resolve('../workflows'),
plugins: [new GoogleAdkPlugin({ modelProvider })],
});
const result = await worker.runUntil(
testEnv.client.workflow.execute(multiAgent, {
args: ['mountains'],
workflowId: 'test-google-adk-agent-patterns-' + Date.now(),
taskQueue,
}),
);
assert.strictEqual(result, 'snow on the mountain');
});
});
22 changes: 22 additions & 0 deletions google-adk-agents/agent-patterns/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NativeConnection, Worker } from '@temporalio/worker';
import { GoogleAdkPlugin } from '@temporalio/google-adk-agents';

async function run() {
const connection = await NativeConnection.connect({ address: 'localhost:7233' });
try {
const worker = await Worker.create({
connection,
taskQueue: 'google-adk-agent-patterns',
workflowsPath: require.resolve('./workflows'),
plugins: [new GoogleAdkPlugin()],
});
await worker.run();
} finally {
await connection.close();
}
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
36 changes: 36 additions & 0 deletions google-adk-agents/agent-patterns/src/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { InMemoryRunner, LlmAgent, isFinalResponse, stringifyContent } from '@google/adk';
import { TemporalModel } from '@temporalio/google-adk-agents';

export async function multiAgent(topic: string): Promise<string> {
const researcher = new LlmAgent({
name: 'researcher',
model: new TemporalModel('gemini-2.5-flash', { summary: 'Researcher Agent' }),
instruction: 'You are a researcher. Find information about the topic.',
});

const writer = new LlmAgent({
name: 'writer',
model: new TemporalModel('gemini-2.5-flash', { summary: 'Writer Agent' }),
instruction: 'You are a poet. Write a haiku based on the research.',
});

const coordinator = new LlmAgent({
name: 'coordinator',
model: new TemporalModel('gemini-2.5-flash', { summary: 'Coordinator Agent' }),
instruction: 'You are a coordinator. Delegate to the researcher, then the writer.',
subAgents: [researcher, writer],
});

const runner = new InMemoryRunner({ agent: coordinator });

let finalText = '';
for await (const event of runner.runEphemeral({
userId: 'user',
newMessage: { role: 'user', parts: [{ text: `Write a haiku about ${topic}. First research it, then write it.` }] },
})) {
if (isFinalResponse(event)) {
finalText = stringifyContent(event);
}
}
return finalText;
}
23 changes: 23 additions & 0 deletions google-adk-agents/basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Google ADK Agents: Basic

A single ADK `LlmAgent` whose model is a `TemporalModel`, driven by `InMemoryRunner` for one durable model call. The only change from a vanilla ADK agent is wrapping the model in `TemporalModel`; the agent loop runs inside the Workflow while the model call runs as an Activity.

## Run

Run these from the `google-adk-agents/` root (run `npm install` there once first).

```bash
# In one terminal, start the Worker (requires a local Temporal server and GOOGLE_API_KEY):
GOOGLE_API_KEY=... npx ts-node basic/src/worker.ts

# In another terminal, run the scenario:
npx ts-node basic/src/client.ts
```

## Test

```bash
npx mocha --exit --require ts-node/register --require source-map-support/register "basic/src/mocha/*.test.ts"
```

The test runs a real Worker against `TestWorkflowEnvironment` with `fakeModelProvider`, so no `GOOGLE_API_KEY` is required.
22 changes: 22 additions & 0 deletions google-adk-agents/basic/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Connection, Client } from '@temporalio/client';
import { GoogleAdkPlugin } from '@temporalio/google-adk-agents';
import { nanoid } from 'nanoid';
import { helloWorld } from './workflows';

async function run() {
const connection = await Connection.connect();
const client = new Client({ connection, plugins: [new GoogleAdkPlugin()] });

const result = await client.workflow.execute(helloWorld, {
taskQueue: 'google-adk-basic',
workflowId: 'google-adk-basic-' + nanoid(),
args: ['Write a haiku about durable execution.'],
});

console.log(result);
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading