diff --git a/.scripts/copy-shared-files.mjs b/.scripts/copy-shared-files.mjs index 0414125f..c3f1b14c 100644 --- a/.scripts/copy-shared-files.mjs +++ b/.scripts/copy-shared-files.mjs @@ -25,6 +25,7 @@ const TSCONFIG_EXCLUDE = [ 'production', 'hello-world-js', 'food-delivery', + 'google-adk-agents', 'lambda-worker', 'nestjs-exchange-rates', 'empty', @@ -48,6 +49,7 @@ const ESLINTRC_EXCLUDE = [ 'hello-world-js', 'protobufs', 'food-delivery', + 'google-adk-agents', 'nestjs-exchange-rates', ]; const ESLINTIGNORE_EXCLUDE = [ diff --git a/.scripts/list-of-samples.json b/.scripts/list-of-samples.json index 80d55b91..f15cfdee 100644 --- a/.scripts/list-of-samples.json +++ b/.scripts/list-of-samples.json @@ -19,6 +19,7 @@ "expense", "fetch-esm", "food-delivery", + "google-adk-agents", "grpc-calls", "hello-world", "hello-world-js", diff --git a/google-adk-agents/.eslintignore b/google-adk-agents/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/google-adk-agents/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/google-adk-agents/.eslintrc.js b/google-adk-agents/.eslintrc.js new file mode 100644 index 00000000..84cbb0d7 --- /dev/null +++ b/google-adk-agents/.eslintrc.js @@ -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}`]), + ], + }, + }, + ], +}; diff --git a/google-adk-agents/.gitignore b/google-adk-agents/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/google-adk-agents/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/google-adk-agents/.npmrc b/google-adk-agents/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/google-adk-agents/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/google-adk-agents/.nvmrc b/google-adk-agents/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/google-adk-agents/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/google-adk-agents/.post-create b/google-adk-agents/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/google-adk-agents/.post-create @@ -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} diff --git a/google-adk-agents/.prettierignore b/google-adk-agents/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/google-adk-agents/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/google-adk-agents/.prettierrc b/google-adk-agents/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/google-adk-agents/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/google-adk-agents/README.md b/google-adk-agents/README.md new file mode 100644 index 00000000..90efa5ea --- /dev/null +++ b/google-adk-agents/README.md @@ -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. | diff --git a/google-adk-agents/agent-patterns/README.md b/google-adk-agents/agent-patterns/README.md new file mode 100644 index 00000000..74c5ee65 --- /dev/null +++ b/google-adk-agents/agent-patterns/README.md @@ -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. diff --git a/google-adk-agents/agent-patterns/src/client.ts b/google-adk-agents/agent-patterns/src/client.ts new file mode 100644 index 00000000..b7dd93d0 --- /dev/null +++ b/google-adk-agents/agent-patterns/src/client.ts @@ -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); +}); diff --git a/google-adk-agents/agent-patterns/src/mocha/workflows.test.ts b/google-adk-agents/agent-patterns/src/mocha/workflows.test.ts new file mode 100644 index 00000000..43a92c36 --- /dev/null +++ b/google-adk-agents/agent-patterns/src/mocha/workflows.test.ts @@ -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): 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 { + const next = script.shift(); + if (next === undefined) { + throw new Error('scripted model script exhausted'); + } + yield next; + } + + override async connect(_llmRequest: LlmRequest): Promise { + 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'); + }); +}); diff --git a/google-adk-agents/agent-patterns/src/worker.ts b/google-adk-agents/agent-patterns/src/worker.ts new file mode 100644 index 00000000..5edb64c2 --- /dev/null +++ b/google-adk-agents/agent-patterns/src/worker.ts @@ -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); +}); diff --git a/google-adk-agents/agent-patterns/src/workflows.ts b/google-adk-agents/agent-patterns/src/workflows.ts new file mode 100644 index 00000000..cce306ca --- /dev/null +++ b/google-adk-agents/agent-patterns/src/workflows.ts @@ -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 { + 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; +} diff --git a/google-adk-agents/basic/README.md b/google-adk-agents/basic/README.md new file mode 100644 index 00000000..2ae8f1b3 --- /dev/null +++ b/google-adk-agents/basic/README.md @@ -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. diff --git a/google-adk-agents/basic/src/client.ts b/google-adk-agents/basic/src/client.ts new file mode 100644 index 00000000..ec759f2a --- /dev/null +++ b/google-adk-agents/basic/src/client.ts @@ -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); +}); diff --git a/google-adk-agents/basic/src/mocha/workflows.test.ts b/google-adk-agents/basic/src/mocha/workflows.test.ts new file mode 100644 index 00000000..7ea70e65 --- /dev/null +++ b/google-adk-agents/basic/src/mocha/workflows.test.ts @@ -0,0 +1,39 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { Worker } from '@temporalio/worker'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { fakeModelProvider } from '@temporalio/google-adk-agents/testing'; +import { after, before, describe, it } from 'mocha'; +import assert from 'assert'; +import { helloWorld } from '../workflows'; + +describe('google-adk-agents/basic workflow scenarios', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('helloWorld: runs an LlmAgent through the runner with durable model calls', async () => { + const taskQueue = 'test-google-adk-basic'; + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new GoogleAdkPlugin({ modelProvider: fakeModelProvider() })], + }); + const result = await worker.runUntil( + testEnv.client.workflow.execute(helloWorld, { + args: ['Say hello.'], + workflowId: 'test-google-adk-basic-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'fake-response:gemini-2.5-flash'); + }); +}); diff --git a/google-adk-agents/basic/src/worker.ts b/google-adk-agents/basic/src/worker.ts new file mode 100644 index 00000000..20d242f2 --- /dev/null +++ b/google-adk-agents/basic/src/worker.ts @@ -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-basic', + workflowsPath: require.resolve('./workflows'), + plugins: [new GoogleAdkPlugin()], + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/basic/src/workflows.ts b/google-adk-agents/basic/src/workflows.ts new file mode 100644 index 00000000..d4e911de --- /dev/null +++ b/google-adk-agents/basic/src/workflows.ts @@ -0,0 +1,23 @@ +import { InMemoryRunner, LlmAgent, isFinalResponse, stringifyContent } from '@google/adk'; +import { TemporalModel } from '@temporalio/google-adk-agents'; + +export async function helloWorld(prompt: string): Promise { + const agent = new LlmAgent({ + name: 'assistant', + model: new TemporalModel('gemini-2.5-flash'), + instruction: 'You are a helpful assistant. Respond in a single sentence.', + }); + + const runner = new InMemoryRunner({ agent }); + + let finalText = ''; + for await (const event of runner.runEphemeral({ + userId: 'user', + newMessage: { role: 'user', parts: [{ text: prompt }] }, + })) { + if (isFinalResponse(event)) { + finalText = stringifyContent(event); + } + } + return finalText; +} diff --git a/google-adk-agents/human-approval/README.md b/google-adk-agents/human-approval/README.md new file mode 100644 index 00000000..50f07ff1 --- /dev/null +++ b/google-adk-agents/human-approval/README.md @@ -0,0 +1,23 @@ +# Google ADK Agents: Human Approval + +A human-in-the-loop flow. The Workflow body invokes an ADK `LongRunningFunctionTool` whose `execute` blocks on a Temporal `condition` until a human's decision arrives via an `approve` Signal or an `approveUpdate` Update, then returns it — no special shim required. Both the Signal and Update paths drive the same handler. + +## 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): +npx ts-node human-approval/src/worker.ts + +# In another terminal, start the Workflow (the client sends the approval Signal): +npx ts-node human-approval/src/client.ts +``` + +## Test + +```bash +npx mocha --exit --require ts-node/register --require source-map-support/register "human-approval/src/mocha/*.test.ts" +``` + +The test runs a real Worker against `TestWorkflowEnvironment`, driving both the `approve` Signal and the `approveUpdate` Update and asserting the long-running tool resumes with the supplied value. No `GOOGLE_API_KEY` is required. diff --git a/google-adk-agents/human-approval/src/client.ts b/google-adk-agents/human-approval/src/client.ts new file mode 100644 index 00000000..8add76c9 --- /dev/null +++ b/google-adk-agents/human-approval/src/client.ts @@ -0,0 +1,23 @@ +import { Connection, Client } from '@temporalio/client'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { nanoid } from 'nanoid'; +import { humanApproval, approveSignal } from './workflows'; + +async function run() { + const connection = await Connection.connect(); + const client = new Client({ connection, plugins: [new GoogleAdkPlugin()] }); + + const handle = await client.workflow.start(humanApproval, { + taskQueue: 'google-adk-human-approval', + workflowId: 'google-adk-human-approval-' + nanoid(), + }); + console.log(`Started workflow ${handle.workflowId}; sending approval Signal.`); + + await handle.signal(approveSignal, 'approved-by-operator'); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/human-approval/src/mocha/workflows.test.ts b/google-adk-agents/human-approval/src/mocha/workflows.test.ts new file mode 100644 index 00000000..ea27d8e7 --- /dev/null +++ b/google-adk-agents/human-approval/src/mocha/workflows.test.ts @@ -0,0 +1,56 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { Worker } from '@temporalio/worker'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { after, before, describe, it } from 'mocha'; +import assert from 'assert'; +import { humanApproval, approveSignal, approveUpdate } from '../workflows'; + +describe('google-adk-agents/human-approval workflow scenarios', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + async function makeWorker(taskQueue: string) { + return Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new GoogleAdkPlugin()], + }); + } + + it('humanApproval: long-running tool resumes on the approve Signal', async () => { + const taskQueue = 'test-google-adk-human-approval-signal'; + const worker = await makeWorker(taskQueue); + await worker.runUntil(async () => { + const handle = await testEnv.client.workflow.start(humanApproval, { + workflowId: 'test-google-adk-hitl-signal-' + Date.now(), + taskQueue, + }); + await handle.signal(approveSignal, 'approved-via-signal'); + assert.strictEqual(await handle.result(), 'approved-via-signal'); + }); + }); + + it('humanApproval: long-running tool resumes on the approve Update', async () => { + const taskQueue = 'test-google-adk-human-approval-update'; + const worker = await makeWorker(taskQueue); + await worker.runUntil(async () => { + const handle = await testEnv.client.workflow.start(humanApproval, { + workflowId: 'test-google-adk-hitl-update-' + Date.now(), + taskQueue, + }); + const updateResult = await handle.executeUpdate(approveUpdate, { args: ['approved-via-update'] }); + assert.strictEqual(updateResult, 'approved-via-update'); + assert.strictEqual(await handle.result(), 'approved-via-update'); + }); + }); +}); diff --git a/google-adk-agents/human-approval/src/worker.ts b/google-adk-agents/human-approval/src/worker.ts new file mode 100644 index 00000000..98e94d4f --- /dev/null +++ b/google-adk-agents/human-approval/src/worker.ts @@ -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-human-approval', + workflowsPath: require.resolve('./workflows'), + plugins: [new GoogleAdkPlugin()], + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/human-approval/src/workflows.ts b/google-adk-agents/human-approval/src/workflows.ts new file mode 100644 index 00000000..07e4d0e3 --- /dev/null +++ b/google-adk-agents/human-approval/src/workflows.ts @@ -0,0 +1,29 @@ +import { LongRunningFunctionTool } from '@google/adk'; +import { condition, defineSignal, defineUpdate, setHandler } from '@temporalio/workflow'; + +export const approveSignal = defineSignal<[string]>('approve'); +export const approveUpdate = defineUpdate('approveUpdate'); + +export async function humanApproval(): Promise { + let result: string | undefined; + + setHandler(approveSignal, (value) => { + result = value; + }); + setHandler(approveUpdate, (value) => { + result = value; + return value; + }); + + const tool = new LongRunningFunctionTool({ + name: 'humanApproval', + description: 'Wait for a human approval.', + execute: async () => { + await condition(() => result !== undefined); + return result; + }, + }); + + const out = await tool.runAsync({ args: {}, toolContext: {} as never }); + return out as string; +} diff --git a/google-adk-agents/mcp/README.md b/google-adk-agents/mcp/README.md new file mode 100644 index 00000000..e115f060 --- /dev/null +++ b/google-adk-agents/mcp/README.md @@ -0,0 +1,23 @@ +# Google ADK Agents: MCP + +A `TemporalMcpToolSet` backed by a real [Model Context Protocol](https://modelcontextprotocol.io) server. The agent declares `new TemporalMcpToolSet({ name: 'filesystem' })`; the Worker registers the matching `filesystem` factory on the plugin via `mcpToolsets`, which opens a filesystem MCP server over stdio (`@modelcontextprotocol/server-filesystem`). Tool discovery and every tool call route through `filesystem-listTools` / `filesystem-callTool` Activities, so the MCP connection details stay on the Worker and never enter Workflow inputs. + +## Run + +Run these from the `google-adk-agents/` root (run `npm install` there once first). The Worker spawns the filesystem MCP server with `npx`, exposing this sample's `src/sample-files/` directory. + +```bash +# In one terminal, start the Worker (requires a local Temporal server and GOOGLE_API_KEY): +GOOGLE_API_KEY=... npx ts-node mcp/src/worker.ts + +# In another terminal, run the scenario: +npx ts-node mcp/src/client.ts +``` + +## Test + +```bash +npx mocha --exit --require ts-node/register --require source-map-support/register "mcp/src/mocha/*.test.ts" +``` + +The test registers an in-memory `mockMcpToolset` on the plugin and asserts the `TemporalMcpToolSet` discovers its tools through the named factory — no Node, `npx`, or network required, and no `GOOGLE_API_KEY`. diff --git a/google-adk-agents/mcp/src/client.ts b/google-adk-agents/mcp/src/client.ts new file mode 100644 index 00000000..5ef6b23e --- /dev/null +++ b/google-adk-agents/mcp/src/client.ts @@ -0,0 +1,22 @@ +import { Connection, Client } from '@temporalio/client'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { nanoid } from 'nanoid'; +import { filesystemAgent } from './workflows'; + +async function run() { + const connection = await Connection.connect(); + const client = new Client({ connection, plugins: [new GoogleAdkPlugin()] }); + + const result = await client.workflow.execute(filesystemAgent, { + taskQueue: 'google-adk-mcp', + workflowId: 'google-adk-mcp-' + nanoid(), + args: ['List the files you can access, then summarize what is in hello.txt.'], + }); + + console.log(result); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/mcp/src/mocha/workflows.test.ts b/google-adk-agents/mcp/src/mocha/workflows.test.ts new file mode 100644 index 00000000..d8fe7cf8 --- /dev/null +++ b/google-adk-agents/mcp/src/mocha/workflows.test.ts @@ -0,0 +1,57 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { Worker } from '@temporalio/worker'; +import { Type } from '@google/genai'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { mockMcpToolset, type MockMcpToolDefinition } from '@temporalio/google-adk-agents/testing'; +import { after, before, describe, it } from 'mocha'; +import assert from 'assert'; +import { listFilesystemTools } from '../workflows'; + +const readFileDef: MockMcpToolDefinition = { + declaration: { + name: 'read_file', + description: 'Read a file.', + parameters: { type: Type.OBJECT, properties: { path: { type: Type.STRING } }, required: ['path'] }, + }, + handler: (args) => ({ contents: `contents of ${String(args.path)}` }), +}; + +const listDirDef: MockMcpToolDefinition = { + declaration: { + name: 'list_directory', + description: 'List a directory.', + parameters: { type: Type.OBJECT, properties: { path: { type: Type.STRING } } }, + }, + handler: () => ({ entries: ['hello.txt'] }), +}; + +describe('google-adk-agents/mcp workflow scenarios', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('listFilesystemTools: TemporalMcpToolSet discovers tools via the named factory', async () => { + const taskQueue = 'test-google-adk-mcp'; + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new GoogleAdkPlugin({ mcpToolsets: { filesystem: mockMcpToolset([readFileDef, listDirDef]) } })], + }); + const result = await worker.runUntil( + testEnv.client.workflow.execute(listFilesystemTools, { + workflowId: 'test-google-adk-mcp-' + Date.now(), + taskQueue, + }), + ); + assert.deepStrictEqual(result, ['read_file', 'list_directory']); + }); +}); diff --git a/google-adk-agents/mcp/src/sample-files/hello.txt b/google-adk-agents/mcp/src/sample-files/hello.txt new file mode 100644 index 00000000..18f9f10c --- /dev/null +++ b/google-adk-agents/mcp/src/sample-files/hello.txt @@ -0,0 +1,2 @@ +Hello from the Temporal Google ADK filesystem MCP sample. +This file is exposed to the agent through the filesystem MCP server. diff --git a/google-adk-agents/mcp/src/toolsets.ts b/google-adk-agents/mcp/src/toolsets.ts new file mode 100644 index 00000000..57714e7e --- /dev/null +++ b/google-adk-agents/mcp/src/toolsets.ts @@ -0,0 +1,13 @@ +import * as path from 'path'; +import type { McpToolsetFactory } from '@temporalio/google-adk-agents'; + +export function filesystemToolset(): McpToolsetFactory { + const exposedDir = path.resolve(__dirname, 'sample-files'); + return () => ({ + type: 'StdioConnectionParams', + serverParams: { + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', exposedDir], + }, + }); +} diff --git a/google-adk-agents/mcp/src/worker.ts b/google-adk-agents/mcp/src/worker.ts new file mode 100644 index 00000000..f03ea6e9 --- /dev/null +++ b/google-adk-agents/mcp/src/worker.ts @@ -0,0 +1,23 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { filesystemToolset } from './toolsets'; + +async function run() { + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'google-adk-mcp', + workflowsPath: require.resolve('./workflows'), + plugins: [new GoogleAdkPlugin({ mcpToolsets: { filesystem: filesystemToolset() } })], + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/mcp/src/workflows.ts b/google-adk-agents/mcp/src/workflows.ts new file mode 100644 index 00000000..622cd51b --- /dev/null +++ b/google-adk-agents/mcp/src/workflows.ts @@ -0,0 +1,30 @@ +import { InMemoryRunner, LlmAgent, isFinalResponse, stringifyContent } from '@google/adk'; +import { TemporalMcpToolSet, TemporalModel } from '@temporalio/google-adk-agents'; + +export async function filesystemAgent(prompt: string): Promise { + const agent = new LlmAgent({ + name: 'filesystem_agent', + model: new TemporalModel('gemini-2.5-flash'), + instruction: 'Use your tools to answer questions about files.', + tools: [new TemporalMcpToolSet({ name: 'filesystem' })], + }); + + const runner = new InMemoryRunner({ agent }); + + let finalText = ''; + for await (const event of runner.runEphemeral({ + userId: 'user', + newMessage: { role: 'user', parts: [{ text: prompt }] }, + })) { + if (isFinalResponse(event)) { + finalText = stringifyContent(event); + } + } + return finalText; +} + +export async function listFilesystemTools(): Promise { + const toolset = new TemporalMcpToolSet({ name: 'filesystem' }); + const tools = await toolset.getTools(); + return tools.map((tool) => tool.name); +} diff --git a/google-adk-agents/package.json b/google-adk-agents/package.json new file mode 100644 index 00000000..5e730417 --- /dev/null +++ b/google-adk-agents/package.json @@ -0,0 +1,39 @@ +{ + "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/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/google-adk-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@google/adk": "^1.2.0", + "@google/genai": "^2.8.0", + "nanoid": "3.x" + }, + "devDependencies": { + "@temporalio/testing": "^1.18.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" + } +} diff --git a/google-adk-agents/streaming/README.md b/google-adk-agents/streaming/README.md new file mode 100644 index 00000000..1c9c1230 --- /dev/null +++ b/google-adk-agents/streaming/README.md @@ -0,0 +1,25 @@ +# Google ADK Agents: Streaming + +Token streaming over SSE. The Workflow drives a `TemporalModel` configured with a `streamingTopic`, so the model call runs as an `invokeModelStreaming` Activity that publishes each incremental `LlmResponse` chunk to the topic via the Workflow streams API, while still returning the full, ordered transcript to the Workflow (the deterministic, replay-safe channel). The Workflow returns the assembled text and the chunk count. + +## 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 streaming/src/worker.ts + +# In another terminal, run the scenario: +npx ts-node streaming/src/client.ts +``` + +In the Temporal UI the history shows an `invokeModelStreaming` Activity. To consume chunks live as they arrive, subscribe to the `responses` topic with the Workflow streams client. + +## Test + +```bash +npx mocha --exit --require ts-node/register --require source-map-support/register "streaming/src/mocha/*.test.ts" +``` + +The test runs a real Worker against `TestWorkflowEnvironment` with `fakeModelProvider` scripted to yield three chunks, asserting the Workflow receives the full transcript and every chunk. No `GOOGLE_API_KEY` is required. diff --git a/google-adk-agents/streaming/src/client.ts b/google-adk-agents/streaming/src/client.ts new file mode 100644 index 00000000..cecbdd86 --- /dev/null +++ b/google-adk-agents/streaming/src/client.ts @@ -0,0 +1,23 @@ +import { Connection, Client } from '@temporalio/client'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { nanoid } from 'nanoid'; +import { streamingModelCall } from './workflows'; + +async function run() { + const connection = await Connection.connect(); + const client = new Client({ connection, plugins: [new GoogleAdkPlugin()] }); + + const result = await client.workflow.execute(streamingModelCall, { + taskQueue: 'google-adk-streaming', + workflowId: 'google-adk-streaming-' + nanoid(), + args: ['Tell me a short story about a robot learning to paint.'], + }); + + console.log(`Received ${result.chunks} chunk(s):`); + console.log(result.text); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/streaming/src/mocha/workflows.test.ts b/google-adk-agents/streaming/src/mocha/workflows.test.ts new file mode 100644 index 00000000..34321f82 --- /dev/null +++ b/google-adk-agents/streaming/src/mocha/workflows.test.ts @@ -0,0 +1,47 @@ +import type { LlmResponse } from '@google/adk'; +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { Worker } from '@temporalio/worker'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { fakeModelProvider } from '@temporalio/google-adk-agents/testing'; +import { after, before, describe, it } from 'mocha'; +import assert from 'assert'; +import { streamingModelCall } from '../workflows'; + +const chunks: LlmResponse[] = [ + { content: { role: 'model', parts: [{ text: 'Hello ' }] }, partial: true }, + { content: { role: 'model', parts: [{ text: 'streaming ' }] }, partial: true }, + { content: { role: 'model', parts: [{ text: 'world' }] }, turnComplete: true }, +]; + +describe('google-adk-agents/streaming workflow scenarios', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('streamingModelCall: returns the full transcript and chunk count', async () => { + const taskQueue = 'test-google-adk-streaming'; + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new GoogleAdkPlugin({ modelProvider: fakeModelProvider(chunks) })], + }); + const result = await worker.runUntil( + testEnv.client.workflow.execute(streamingModelCall, { + args: ['stream please'], + workflowId: 'test-google-adk-streaming-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result.chunks, 3); + assert.strictEqual(result.text, 'Hello streaming world'); + }); +}); diff --git a/google-adk-agents/streaming/src/worker.ts b/google-adk-agents/streaming/src/worker.ts new file mode 100644 index 00000000..5f8f0df1 --- /dev/null +++ b/google-adk-agents/streaming/src/worker.ts @@ -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-streaming', + workflowsPath: require.resolve('./workflows'), + plugins: [new GoogleAdkPlugin()], + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/streaming/src/workflows.ts b/google-adk-agents/streaming/src/workflows.ts new file mode 100644 index 00000000..08533c36 --- /dev/null +++ b/google-adk-agents/streaming/src/workflows.ts @@ -0,0 +1,34 @@ +import type { LlmRequest } from '@google/adk'; +import type { Duration } from '@temporalio/common'; +import { TemporalModel } from '@temporalio/google-adk-agents'; + +function makeRequest(text: string): LlmRequest { + return { + model: 'gemini-2.5-flash', + contents: [{ role: 'user', parts: [{ text }] }], + config: {}, + toolsDict: {}, + liveConnectConfig: {}, + } as LlmRequest; +} + +export async function streamingModelCall( + prompt: string, + batchInterval: Duration = '50 milliseconds', +): Promise<{ text: string; chunks: number }> { + const model = new TemporalModel('gemini-2.5-flash', { + streamingTopic: 'responses', + streamingBatchInterval: batchInterval, + activity: { heartbeatTimeout: '5 seconds' }, + }); + + let text = ''; + let chunks = 0; + for await (const response of model.generateContentAsync(makeRequest(prompt), true)) { + for (const part of response.content?.parts ?? []) { + if (part.text) text += part.text; + } + chunks++; + } + return { text, chunks }; +} diff --git a/google-adk-agents/tools/README.md b/google-adk-agents/tools/README.md new file mode 100644 index 00000000..3dd2176c --- /dev/null +++ b/google-adk-agents/tools/README.md @@ -0,0 +1,23 @@ +# Google ADK Agents: Tools + +Exposes an existing Temporal Activity to the ADK agent as a tool with `activityAsTool`. When the model decides to call `getWeather`, the tool dispatches the registered `getWeather` Activity — durable and retriable — instead of running the I/O inside the Workflow body. + +## 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 tools/src/worker.ts + +# In another terminal, run the scenario: +npx ts-node tools/src/client.ts +``` + +## Test + +```bash +npx mocha --exit --require ts-node/register --require source-map-support/register "tools/src/mocha/*.test.ts" +``` + +The test runs a real Worker against `TestWorkflowEnvironment` and dispatches the `activityAsTool` tool directly, asserting the named Activity round-trips. No `GOOGLE_API_KEY` is required. diff --git a/google-adk-agents/tools/src/activities.ts b/google-adk-agents/tools/src/activities.ts new file mode 100644 index 00000000..6b4c9fe0 --- /dev/null +++ b/google-adk-agents/tools/src/activities.ts @@ -0,0 +1,3 @@ +export async function getWeather(args: { city: string }): Promise { + return `The weather in ${args.city} is warm and sunny, 17 degrees.`; +} diff --git a/google-adk-agents/tools/src/client.ts b/google-adk-agents/tools/src/client.ts new file mode 100644 index 00000000..612cfe43 --- /dev/null +++ b/google-adk-agents/tools/src/client.ts @@ -0,0 +1,22 @@ +import { Connection, Client } from '@temporalio/client'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { nanoid } from 'nanoid'; +import { weatherAgent } from './workflows'; + +async function run() { + const connection = await Connection.connect(); + const client = new Client({ connection, plugins: [new GoogleAdkPlugin()] }); + + const result = await client.workflow.execute(weatherAgent, { + taskQueue: 'google-adk-tools', + workflowId: 'google-adk-tools-' + nanoid(), + args: ['What is the weather in Tokyo?'], + }); + + console.log(result); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/tools/src/mocha/workflows.test.ts b/google-adk-agents/tools/src/mocha/workflows.test.ts new file mode 100644 index 00000000..14850489 --- /dev/null +++ b/google-adk-agents/tools/src/mocha/workflows.test.ts @@ -0,0 +1,40 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { Worker } from '@temporalio/worker'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import { after, before, describe, it } from 'mocha'; +import assert from 'assert'; +import * as activities from '../activities'; +import { lookupWeather } from '../workflows'; + +describe('google-adk-agents/tools workflow scenarios', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('lookupWeather: activityAsTool dispatches the registered Activity', async () => { + const taskQueue = 'test-google-adk-tools'; + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities, + plugins: [new GoogleAdkPlugin()], + }); + const result = await worker.runUntil( + testEnv.client.workflow.execute(lookupWeather, { + args: ['Tokyo'], + workflowId: 'test-google-adk-tools-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'The weather in Tokyo is warm and sunny, 17 degrees.'); + }); +}); diff --git a/google-adk-agents/tools/src/worker.ts b/google-adk-agents/tools/src/worker.ts new file mode 100644 index 00000000..b05c4fd2 --- /dev/null +++ b/google-adk-agents/tools/src/worker.ts @@ -0,0 +1,24 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { GoogleAdkPlugin } from '@temporalio/google-adk-agents'; +import * as activities from './activities'; + +async function run() { + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'google-adk-tools', + workflowsPath: require.resolve('./workflows'), + activities, + plugins: [new GoogleAdkPlugin()], + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/google-adk-agents/tools/src/workflows.ts b/google-adk-agents/tools/src/workflows.ts new file mode 100644 index 00000000..d266ad22 --- /dev/null +++ b/google-adk-agents/tools/src/workflows.ts @@ -0,0 +1,42 @@ +import { InMemoryRunner, LlmAgent, isFinalResponse, stringifyContent } from '@google/adk'; +import { Type } from '@google/genai'; +import { activityAsTool, TemporalModel } from '@temporalio/google-adk-agents'; + +function weatherTool() { + return activityAsTool({ + name: 'getWeather', + description: 'Get the current weather for a city.', + parameters: { + type: Type.OBJECT, + properties: { city: { type: Type.STRING, description: 'The city name' } }, + required: ['city'], + }, + activity: { startToCloseTimeout: '1 minute' }, + }); +} + +export async function weatherAgent(prompt: string): Promise { + const agent = new LlmAgent({ + name: 'weather_agent', + model: new TemporalModel('gemini-2.5-flash'), + instruction: 'Use the getWeather tool to answer weather questions.', + tools: [weatherTool()], + }); + + const runner = new InMemoryRunner({ agent }); + + let finalText = ''; + for await (const event of runner.runEphemeral({ + userId: 'user', + newMessage: { role: 'user', parts: [{ text: prompt }] }, + })) { + if (isFinalResponse(event)) { + finalText = stringifyContent(event); + } + } + return finalText; +} + +export async function lookupWeather(city: string): Promise { + return weatherTool().runAsync({ args: { city }, toolContext: {} as never }); +} diff --git a/google-adk-agents/tsconfig.json b/google-adk-agents/tsconfig.json new file mode 100644 index 00000000..da8a0e08 --- /dev/null +++ b/google-adk-agents/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": ".", + "outDir": "./lib" + }, + "include": ["*/src/**/*.ts"] +}