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
11 changes: 6 additions & 5 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
# @temporalio/sdk will be requested for review when
# someone opens a pull request.
* @temporalio/sdk
/ai-sdk/ @temporalio/sdk @temporalio/ai-sdk
/langsmith/ @temporalio/sdk @temporalio/ai-sdk
/openai-agents/ @temporalio/sdk @temporalio/ai-sdk
/strands-agents/ @temporalio/sdk @temporalio/ai-sdk
/workflow-streams/ @temporalio/sdk @temporalio/ai-sdk
/ai-sdk/ @temporalio/sdk @temporalio/ai-sdk
/google-adk-agents/ @temporalio/sdk @temporalio/ai-sdk
/langsmith/ @temporalio/sdk @temporalio/ai-sdk
/openai-agents/ @temporalio/sdk @temporalio/ai-sdk
/strands-agents/ @temporalio/sdk @temporalio/ai-sdk
/workflow-streams/ @temporalio/sdk @temporalio/ai-sdk
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ jobs:
eager-workflow-start
early-return
empty
google-adk-agents
hello-world
langsmith
mutex
Expand Down
1 change: 1 addition & 0 deletions .scripts/copy-shared-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const ESLINTIGNORE_EXCLUDE = [

const POST_CREATE_EXCLUDE = [
'openai-agents',
'google-adk-agents',
'env-config',
'dsl-interpreter',
'eager-workflow-start',
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ and you'll be given the list of sample options.
- [**Customer Service**](./openai-agents/customer-service): A long-running, multi-turn Workflow driven by Updates and Queries, with triage handoffs and `continueAsNew` to bound history.
- [**Nexus Tools**](./openai-agents/nexus-tools): Expose a Nexus Operation as an agent tool with `nexusOperationAsTool`.
- [**Streaming**](./openai-agents/src/streaming): Run an agent in streaming mode over a Workflow Stream, with an external client subscribing to the model's deltas live.
- [**Google ADK Agents**](./google-adk-agents): Run [Google Agent Development Kit](https://github.com/google/adk-js) (`@google/adk`) agents as Temporal Workflows with the `@temporalio/google-adk-agents` integration. The [`google-adk-agents/`](./google-adk-agents) directory contains seven samples:
- [**Basic**](./google-adk-agents/src/basic): A single `LlmAgent` whose model is a `TemporalModel`, driven by `InMemoryRunner` for one durable model call.
- [**Tools**](./google-adk-agents/src/tools): An existing Temporal Activity exposed to the agent as an ADK tool with `activityAsTool`.
- [**Agent Patterns**](./google-adk-agents/src/agent-patterns): A coordinator `LlmAgent` starts an ADK `transfer_to_agent` relay through a researcher and a writer, each with its own `TemporalModel`.
- [**MCP**](./google-adk-agents/src/mcp): A `TemporalMCPToolset` backed by a filesystem MCP server the Worker opens over stdio.
- [**Streaming**](./google-adk-agents/src/streaming): Token streaming from a direct `TemporalModel` call — no agent loop — over a Workflow Stream, with an external client printing the deltas as they arrive.
- [**Human Approval**](./google-adk-agents/src/human-approval): A `LongRunningFunctionTool` whose completion is gated by a Temporal Signal or Update.
- [**Observability**](./google-adk-agents/src/observability): Token usage, latency, and call counts from the agent loop's OpenTelemetry spans, by composing `OpenTelemetryPlugin` onto the Worker alongside `GoogleAdkPlugin`.

### Full-stack apps

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
20 changes: 20 additions & 0 deletions google-adk-agents/.post-create
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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 22 or later:

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

This sample has several scenarios under {cyan src/}. Using two other shells, start a Worker for one scenario and run its client (example: {cyan basic}):

{cyan GEMINI_API_KEY=<your-key> npx ts-node src/basic/worker.ts}
{cyan npx ts-node src/basic/client.ts}

See README.md for the full list of scenarios.
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
28 changes: 28 additions & 0 deletions google-adk-agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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 its non-deterministic I/O — model calls, MCP tool calls, and Activities exposed as tools — runs 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 under `src/`. 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 GEMINI_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`](./src/basic) | A single `LlmAgent` whose model is a `TemporalModel`, driven by `InMemoryRunner` for one durable model call. |
| [`tools`](./src/tools) | An existing Temporal Activity exposed to the agent as an ADK tool via `activityAsTool`. |
| [`agent-patterns`](./src/agent-patterns) | A `transfer_to_agent` relay from a coordinator `LlmAgent` through a researcher and a writer, each with its own `TemporalModel`. |
| [`mcp`](./src/mcp) | A `TemporalMCPToolset` backed by an `mcpToolsets` factory on the plugin (a filesystem MCP server over stdio). |
| [`streaming`](./src/streaming) | Token streaming from a direct `TemporalModel` call — no agent loop — over the Workflow streams API. |
| [`human-approval`](./src/human-approval) | A `LongRunningFunctionTool` whose completion is gated by a Temporal Signal or Update. |
| [`observability`](./src/observability) | Token usage, latency, and call counts, by composing `OpenTelemetryPlugin` onto the Worker alongside `GoogleAdkPlugin`. |
44 changes: 44 additions & 0 deletions google-adk-agents/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "temporal-google-adk-agents",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc --build",
"build.watch": "tsc --build --watch",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"test": "mocha --exit --require ts-node/register --require source-map-support/register \"src/*/mocha/*.test.ts\""
},
"dependencies": {
"@temporalio/client": "^1.22.0",
"@temporalio/common": "^1.22.0",
"@temporalio/google-adk-agents": "^1.22.0",
"@temporalio/interceptors-opentelemetry": "^1.22.0",
"@temporalio/worker": "^1.22.0",
"@temporalio/workflow": "^1.22.0",
"@temporalio/workflow-streams": "^1.22.0",
"@google/adk": ">=1.5.0 <1.6.0",
"@google/genai": "^2.9.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-trace-base": "^1.25.1",
"nanoid": "3.x"
},
"devDependencies": {
"@temporalio/testing": "^1.22.0",
"@tsconfig/node22": "^22.0.0",
"@types/mocha": "8.x",
"@types/node": "^22.9.1",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0",
"mocha": "8.x",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"typescript": "^5.6.3",
"source-map-support": "^0.5.21"
}
}
25 changes: 25 additions & 0 deletions google-adk-agents/src/agent-patterns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Google ADK Agents: Agent Patterns

A relay of three `LlmAgent`s over ADK's built-in `transfer_to_agent` tool, all of it running durably inside the Workflow: a coordinator transfers to a researcher, and the researcher transfers on to a writer, which produces the final answer.

Each agent's `TemporalModel` sets a `summary` — the label the Temporal UI puts on that turn's `adk-invokeModel` 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 GEMINI_API_KEY):
GEMINI_API_KEY=... npx ts-node src/agent-patterns/worker.ts

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

## Test

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

The test runs a real Worker against `TestWorkflowEnvironment` with a scripted `BaseLlm` double of its own, which answers each turn according to the agent ADK names as the asker: a transfer for the coordinator and the researcher, the haiku for the writer. No `GEMINI_API_KEY` is required.
21 changes: 21 additions & 0 deletions google-adk-agents/src/agent-patterns/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Connection, Client } from '@temporalio/client';
import { nanoid } from 'nanoid';
import { multiAgent } from './workflows';

async function run() {
const connection = await Connection.connect();
const client = new Client({ connection });

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);
});
81 changes: 81 additions & 0 deletions google-adk-agents/src/agent-patterns/mocha/workflows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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';

function text(s: string): LlmResponse {
return { content: { role: 'model', parts: [{ text: s }] }, turnComplete: true };
}

// ADK JS's `transfer_to_agent` tool reads `args.agentName` (camelCase).
function transferTo(agentName: string): LlmResponse {
return {
content: { role: 'model', parts: [{ functionCall: { name: 'transfer_to_agent', args: { agentName } } }] },
turnComplete: true,
};
}

// Keyed by the asking agent rather than by call order, so an Activity retry re-serves the same turn.
function scriptedModelProvider(script: Record<string, LlmResponse>): (model: string) => BaseLlm {
class ScriptedLlm extends BaseLlm {
override async *generateContentAsync(
llmRequest: LlmRequest,
_stream = false,
_abortSignal?: AbortSignal,
): AsyncGenerator<LlmResponse, void> {
const asking = llmRequest.config?.labels?.['adk_agent_name'];
const next = asking === undefined ? undefined : script[asking];
if (next === undefined) {
throw new Error(`scripted model has no turn for agent '${asking}'`);
}
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: the relay reaches the writer, and only the writer produces the final text', async () => {
const modelProvider = scriptedModelProvider({
coordinator: transferTo('researcher'),
researcher: transferTo('writer'),
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/src/agent-patterns/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);
});
Loading
Loading