From d33a6dc34c6a42ad313c43fa9a93d7566f345198 Mon Sep 17 00:00:00 2001 From: maplexu Date: Mon, 15 Jun 2026 13:54:03 -0400 Subject: [PATCH 1/5] AI-266: Add @temporalio/openai-agents sample suite Fourteen self-contained samples demonstrating how to run OpenAI Agents SDK agents as Temporal Workflows with the @temporalio/openai-agents integration: the basic building blocks, handoffs, agent patterns, sessions, human approval, hosted/Nexus/MCP tools, tracing, model providers, reasoning content, a research bot, and a customer-service chat. Each sample is a standalone package with a fake-model Worker test that runs without an API key, and the suite is wired into the pnpm workspace, CI, and the samples list. --- .github/workflows/ci.yml | 14 + .scripts/copy-shared-files.mjs | 1 + .scripts/list-of-samples.json | 14 + README.md | 56 +- openai-agents/README.md | 62 + openai-agents/agent-patterns/.eslintignore | 3 + openai-agents/agent-patterns/.eslintrc.js | 32 + openai-agents/agent-patterns/.gitignore | 2 + openai-agents/agent-patterns/.npmrc | 1 + openai-agents/agent-patterns/.nvmrc | 1 + openai-agents/agent-patterns/.post-create | 18 + openai-agents/agent-patterns/.prettierignore | 1 + openai-agents/agent-patterns/.prettierrc | 2 + openai-agents/agent-patterns/README.md | 41 + openai-agents/agent-patterns/package.json | 54 + openai-agents/agent-patterns/src/client.ts | 87 + .../agent-patterns/src/mocha/fake-model.ts | 69 + .../src/mocha/workflows.test.ts | 190 +++ openai-agents/agent-patterns/src/worker.ts | 42 + openai-agents/agent-patterns/src/workflows.ts | 197 +++ openai-agents/agent-patterns/tsconfig.json | 12 + openai-agents/basic/.eslintignore | 3 + openai-agents/basic/.eslintrc.js | 32 + openai-agents/basic/.gitignore | 2 + openai-agents/basic/.npmrc | 1 + openai-agents/basic/.nvmrc | 1 + openai-agents/basic/.post-create | 18 + openai-agents/basic/.prettierignore | 1 + openai-agents/basic/.prettierrc | 2 + openai-agents/basic/README.md | 42 + openai-agents/basic/package.json | 54 + openai-agents/basic/src/activities.ts | 10 + openai-agents/basic/src/client.ts | 83 + openai-agents/basic/src/mocha/fake-model.ts | 69 + .../basic/src/mocha/workflows.test.ts | 190 +++ openai-agents/basic/src/worker.ts | 44 + openai-agents/basic/src/workflows.ts | 135 ++ openai-agents/basic/tsconfig.json | 12 + openai-agents/customer-service/.eslintignore | 3 + openai-agents/customer-service/.eslintrc.js | 32 + openai-agents/customer-service/.gitignore | 2 + openai-agents/customer-service/.npmrc | 1 + openai-agents/customer-service/.nvmrc | 1 + openai-agents/customer-service/.post-create | 18 + .../customer-service/.prettierignore | 1 + openai-agents/customer-service/.prettierrc | 2 + openai-agents/customer-service/README.md | 44 + openai-agents/customer-service/package.json | 54 + openai-agents/customer-service/src/agents.ts | 98 ++ openai-agents/customer-service/src/client.ts | 53 + .../customer-service/src/mocha/fake-model.ts | 69 + .../src/mocha/workflows.test.ts | 72 + openai-agents/customer-service/src/worker.ts | 42 + .../customer-service/src/workflows.ts | 65 + openai-agents/customer-service/tsconfig.json | 12 + openai-agents/handoffs/.eslintignore | 3 + openai-agents/handoffs/.eslintrc.js | 32 + openai-agents/handoffs/.gitignore | 2 + openai-agents/handoffs/.npmrc | 1 + openai-agents/handoffs/.nvmrc | 1 + openai-agents/handoffs/.post-create | 18 + openai-agents/handoffs/.prettierignore | 1 + openai-agents/handoffs/.prettierrc | 2 + openai-agents/handoffs/README.md | 37 + openai-agents/handoffs/package.json | 54 + openai-agents/handoffs/src/activities.ts | 3 + openai-agents/handoffs/src/client.ts | 59 + .../handoffs/src/mocha/fake-model.ts | 69 + .../handoffs/src/mocha/workflows.test.ts | 120 ++ openai-agents/handoffs/src/worker.ts | 44 + openai-agents/handoffs/src/workflows.ts | 79 + openai-agents/handoffs/tsconfig.json | 12 + openai-agents/hosted-mcp/.eslintignore | 3 + openai-agents/hosted-mcp/.eslintrc.js | 32 + openai-agents/hosted-mcp/.gitignore | 2 + openai-agents/hosted-mcp/.npmrc | 1 + openai-agents/hosted-mcp/.nvmrc | 1 + openai-agents/hosted-mcp/.post-create | 18 + openai-agents/hosted-mcp/.prettierignore | 1 + openai-agents/hosted-mcp/.prettierrc | 2 + openai-agents/hosted-mcp/README.md | 56 + openai-agents/hosted-mcp/package.json | 54 + openai-agents/hosted-mcp/src/client.ts | 53 + .../hosted-mcp/src/mocha/fake-model.ts | 69 + .../hosted-mcp/src/mocha/workflows.test.ts | 83 + openai-agents/hosted-mcp/src/worker.ts | 42 + openai-agents/hosted-mcp/src/workflows.ts | 53 + openai-agents/hosted-mcp/tsconfig.json | 12 + openai-agents/human-approval/.eslintignore | 3 + openai-agents/human-approval/.eslintrc.js | 32 + openai-agents/human-approval/.gitignore | 2 + openai-agents/human-approval/.npmrc | 1 + openai-agents/human-approval/.nvmrc | 1 + openai-agents/human-approval/.post-create | 18 + openai-agents/human-approval/.prettierignore | 1 + openai-agents/human-approval/.prettierrc | 2 + openai-agents/human-approval/README.md | 37 + openai-agents/human-approval/package.json | 54 + .../human-approval/src/activities.ts | 1 + openai-agents/human-approval/src/client.ts | 40 + .../human-approval/src/mocha/fake-model.ts | 69 + .../src/mocha/workflows.test.ts | 68 + openai-agents/human-approval/src/worker.ts | 44 + openai-agents/human-approval/src/workflows.ts | 59 + openai-agents/human-approval/tsconfig.json | 12 + openai-agents/mcp/.eslintignore | 3 + openai-agents/mcp/.eslintrc.js | 32 + openai-agents/mcp/.gitignore | 2 + openai-agents/mcp/.npmrc | 1 + openai-agents/mcp/.nvmrc | 1 + openai-agents/mcp/.post-create | 18 + openai-agents/mcp/.prettierignore | 1 + openai-agents/mcp/.prettierrc | 2 + openai-agents/mcp/README.md | 47 + openai-agents/mcp/package.json | 55 + openai-agents/mcp/src/activities.ts | 24 + openai-agents/mcp/src/client.ts | 73 + openai-agents/mcp/src/mocha/fake-model.ts | 69 + openai-agents/mcp/src/mocha/workflows.test.ts | 269 ++++ .../mcp/src/servers/filesystem-server.ts | 74 + openai-agents/mcp/src/servers/notes-server.ts | 72 + .../mcp/src/servers/prompt-server.ts | 103 ++ .../mcp/src/servers/sample-files/hello.txt | 3 + .../mcp/src/servers/sample-files/notes.txt | 3 + openai-agents/mcp/src/servers/sse-server.ts | 125 ++ openai-agents/mcp/src/servers/tools-server.ts | 104 ++ openai-agents/mcp/src/worker.ts | 78 + openai-agents/mcp/src/workflows.ts | 62 + openai-agents/mcp/tsconfig.json | 12 + openai-agents/model-providers/.eslintignore | 3 + openai-agents/model-providers/.eslintrc.js | 32 + openai-agents/model-providers/.gitignore | 2 + openai-agents/model-providers/.npmrc | 1 + openai-agents/model-providers/.nvmrc | 1 + openai-agents/model-providers/.post-create | 18 + openai-agents/model-providers/.prettierignore | 1 + openai-agents/model-providers/.prettierrc | 2 + openai-agents/model-providers/README.md | 55 + openai-agents/model-providers/package.json | 54 + openai-agents/model-providers/src/client.ts | 38 + .../model-providers/src/mocha/fake-model.ts | 69 + .../src/mocha/workflows.test.ts | 68 + openai-agents/model-providers/src/worker.ts | 47 + .../model-providers/src/workflows.ts | 8 + openai-agents/model-providers/tsconfig.json | 12 + openai-agents/nexus-tools/.eslintignore | 3 + openai-agents/nexus-tools/.eslintrc.js | 32 + openai-agents/nexus-tools/.gitignore | 2 + openai-agents/nexus-tools/.npmrc | 1 + openai-agents/nexus-tools/.nvmrc | 1 + openai-agents/nexus-tools/.post-create | 18 + openai-agents/nexus-tools/.prettierignore | 1 + openai-agents/nexus-tools/.prettierrc | 2 + openai-agents/nexus-tools/README.md | 44 + openai-agents/nexus-tools/package.json | 56 + openai-agents/nexus-tools/src/api.ts | 15 + openai-agents/nexus-tools/src/client.ts | 52 + openai-agents/nexus-tools/src/handler.ts | 15 + .../nexus-tools/src/mocha/fake-model.ts | 69 + .../nexus-tools/src/mocha/workflows.test.ts | 72 + openai-agents/nexus-tools/src/worker.ts | 44 + openai-agents/nexus-tools/src/workflows.ts | 31 + openai-agents/nexus-tools/tsconfig.json | 12 + openai-agents/reasoning-content/.eslintignore | 3 + openai-agents/reasoning-content/.eslintrc.js | 32 + openai-agents/reasoning-content/.gitignore | 2 + openai-agents/reasoning-content/.npmrc | 1 + openai-agents/reasoning-content/.nvmrc | 1 + openai-agents/reasoning-content/.post-create | 18 + .../reasoning-content/.prettierignore | 1 + openai-agents/reasoning-content/.prettierrc | 2 + openai-agents/reasoning-content/README.md | 48 + openai-agents/reasoning-content/package.json | 53 + .../reasoning-content/src/activities.ts | 39 + openai-agents/reasoning-content/src/client.ts | 32 + .../src/mocha/workflows.test.ts | 66 + openai-agents/reasoning-content/src/worker.ts | 26 + .../reasoning-content/src/workflows.ts | 16 + openai-agents/reasoning-content/tsconfig.json | 12 + openai-agents/research-bot/.eslintignore | 3 + openai-agents/research-bot/.eslintrc.js | 32 + openai-agents/research-bot/.gitignore | 2 + openai-agents/research-bot/.npmrc | 1 + openai-agents/research-bot/.nvmrc | 1 + openai-agents/research-bot/.post-create | 18 + openai-agents/research-bot/.prettierignore | 1 + openai-agents/research-bot/.prettierrc | 2 + openai-agents/research-bot/README.md | 39 + openai-agents/research-bot/package.json | 54 + openai-agents/research-bot/src/client.ts | 34 + .../research-bot/src/mocha/fake-model.ts | 69 + .../research-bot/src/mocha/workflows.test.ts | 77 + openai-agents/research-bot/src/worker.ts | 42 + openai-agents/research-bot/src/workflows.ts | 76 + openai-agents/research-bot/tsconfig.json | 12 + openai-agents/sessions/.eslintignore | 3 + openai-agents/sessions/.eslintrc.js | 32 + openai-agents/sessions/.gitignore | 2 + openai-agents/sessions/.npmrc | 1 + openai-agents/sessions/.nvmrc | 1 + openai-agents/sessions/.post-create | 18 + openai-agents/sessions/.prettierignore | 1 + openai-agents/sessions/.prettierrc | 2 + openai-agents/sessions/README.md | 34 + openai-agents/sessions/package.json | 54 + openai-agents/sessions/src/activities.ts | 3 + openai-agents/sessions/src/client.ts | 52 + .../sessions/src/mocha/fake-model.ts | 69 + .../sessions/src/mocha/workflows.test.ts | 153 ++ openai-agents/sessions/src/worker.ts | 44 + openai-agents/sessions/src/workflows.ts | 48 + openai-agents/sessions/tsconfig.json | 12 + openai-agents/tools/.eslintignore | 3 + openai-agents/tools/.eslintrc.js | 32 + openai-agents/tools/.gitignore | 2 + openai-agents/tools/.npmrc | 1 + openai-agents/tools/.nvmrc | 1 + openai-agents/tools/.post-create | 18 + openai-agents/tools/.prettierignore | 1 + openai-agents/tools/.prettierrc | 2 + openai-agents/tools/README.md | 48 + openai-agents/tools/package.json | 54 + openai-agents/tools/src/client.ts | 59 + openai-agents/tools/src/mocha/fake-model.ts | 69 + .../tools/src/mocha/workflows.test.ts | 100 ++ openai-agents/tools/src/worker.ts | 42 + openai-agents/tools/src/workflows.ts | 34 + openai-agents/tools/tsconfig.json | 12 + openai-agents/tracing/.eslintignore | 3 + openai-agents/tracing/.eslintrc.js | 32 + openai-agents/tracing/.gitignore | 2 + openai-agents/tracing/.npmrc | 1 + openai-agents/tracing/.nvmrc | 1 + openai-agents/tracing/.post-create | 18 + openai-agents/tracing/.prettierignore | 1 + openai-agents/tracing/.prettierrc | 2 + openai-agents/tracing/README.md | 70 + openai-agents/tracing/package.json | 56 + openai-agents/tracing/src/client.ts | 34 + openai-agents/tracing/src/mocha/fake-model.ts | 69 + .../tracing/src/mocha/workflows.test.ts | 62 + .../tracing/src/recording-processor.ts | 22 + openai-agents/tracing/src/worker.ts | 69 + openai-agents/tracing/src/workflows.ts | 20 + openai-agents/tracing/tsconfig.json | 12 + pnpm-lock.yaml | 1399 ++++++++++++++++- pnpm-workspace.yaml | 1 + 247 files changed, 9339 insertions(+), 101 deletions(-) create mode 100644 openai-agents/README.md create mode 100644 openai-agents/agent-patterns/.eslintignore create mode 100644 openai-agents/agent-patterns/.eslintrc.js create mode 100644 openai-agents/agent-patterns/.gitignore create mode 100644 openai-agents/agent-patterns/.npmrc create mode 100644 openai-agents/agent-patterns/.nvmrc create mode 100644 openai-agents/agent-patterns/.post-create create mode 100644 openai-agents/agent-patterns/.prettierignore create mode 100644 openai-agents/agent-patterns/.prettierrc create mode 100644 openai-agents/agent-patterns/README.md create mode 100644 openai-agents/agent-patterns/package.json create mode 100644 openai-agents/agent-patterns/src/client.ts create mode 100644 openai-agents/agent-patterns/src/mocha/fake-model.ts create mode 100644 openai-agents/agent-patterns/src/mocha/workflows.test.ts create mode 100644 openai-agents/agent-patterns/src/worker.ts create mode 100644 openai-agents/agent-patterns/src/workflows.ts create mode 100644 openai-agents/agent-patterns/tsconfig.json create mode 100644 openai-agents/basic/.eslintignore create mode 100644 openai-agents/basic/.eslintrc.js create mode 100644 openai-agents/basic/.gitignore create mode 100644 openai-agents/basic/.npmrc create mode 100644 openai-agents/basic/.nvmrc create mode 100644 openai-agents/basic/.post-create create mode 100644 openai-agents/basic/.prettierignore create mode 100644 openai-agents/basic/.prettierrc create mode 100644 openai-agents/basic/README.md create mode 100644 openai-agents/basic/package.json create mode 100644 openai-agents/basic/src/activities.ts create mode 100644 openai-agents/basic/src/client.ts create mode 100644 openai-agents/basic/src/mocha/fake-model.ts create mode 100644 openai-agents/basic/src/mocha/workflows.test.ts create mode 100644 openai-agents/basic/src/worker.ts create mode 100644 openai-agents/basic/src/workflows.ts create mode 100644 openai-agents/basic/tsconfig.json create mode 100644 openai-agents/customer-service/.eslintignore create mode 100644 openai-agents/customer-service/.eslintrc.js create mode 100644 openai-agents/customer-service/.gitignore create mode 100644 openai-agents/customer-service/.npmrc create mode 100644 openai-agents/customer-service/.nvmrc create mode 100644 openai-agents/customer-service/.post-create create mode 100644 openai-agents/customer-service/.prettierignore create mode 100644 openai-agents/customer-service/.prettierrc create mode 100644 openai-agents/customer-service/README.md create mode 100644 openai-agents/customer-service/package.json create mode 100644 openai-agents/customer-service/src/agents.ts create mode 100644 openai-agents/customer-service/src/client.ts create mode 100644 openai-agents/customer-service/src/mocha/fake-model.ts create mode 100644 openai-agents/customer-service/src/mocha/workflows.test.ts create mode 100644 openai-agents/customer-service/src/worker.ts create mode 100644 openai-agents/customer-service/src/workflows.ts create mode 100644 openai-agents/customer-service/tsconfig.json create mode 100644 openai-agents/handoffs/.eslintignore create mode 100644 openai-agents/handoffs/.eslintrc.js create mode 100644 openai-agents/handoffs/.gitignore create mode 100644 openai-agents/handoffs/.npmrc create mode 100644 openai-agents/handoffs/.nvmrc create mode 100644 openai-agents/handoffs/.post-create create mode 100644 openai-agents/handoffs/.prettierignore create mode 100644 openai-agents/handoffs/.prettierrc create mode 100644 openai-agents/handoffs/README.md create mode 100644 openai-agents/handoffs/package.json create mode 100644 openai-agents/handoffs/src/activities.ts create mode 100644 openai-agents/handoffs/src/client.ts create mode 100644 openai-agents/handoffs/src/mocha/fake-model.ts create mode 100644 openai-agents/handoffs/src/mocha/workflows.test.ts create mode 100644 openai-agents/handoffs/src/worker.ts create mode 100644 openai-agents/handoffs/src/workflows.ts create mode 100644 openai-agents/handoffs/tsconfig.json create mode 100644 openai-agents/hosted-mcp/.eslintignore create mode 100644 openai-agents/hosted-mcp/.eslintrc.js create mode 100644 openai-agents/hosted-mcp/.gitignore create mode 100644 openai-agents/hosted-mcp/.npmrc create mode 100644 openai-agents/hosted-mcp/.nvmrc create mode 100644 openai-agents/hosted-mcp/.post-create create mode 100644 openai-agents/hosted-mcp/.prettierignore create mode 100644 openai-agents/hosted-mcp/.prettierrc create mode 100644 openai-agents/hosted-mcp/README.md create mode 100644 openai-agents/hosted-mcp/package.json create mode 100644 openai-agents/hosted-mcp/src/client.ts create mode 100644 openai-agents/hosted-mcp/src/mocha/fake-model.ts create mode 100644 openai-agents/hosted-mcp/src/mocha/workflows.test.ts create mode 100644 openai-agents/hosted-mcp/src/worker.ts create mode 100644 openai-agents/hosted-mcp/src/workflows.ts create mode 100644 openai-agents/hosted-mcp/tsconfig.json create mode 100644 openai-agents/human-approval/.eslintignore create mode 100644 openai-agents/human-approval/.eslintrc.js create mode 100644 openai-agents/human-approval/.gitignore create mode 100644 openai-agents/human-approval/.npmrc create mode 100644 openai-agents/human-approval/.nvmrc create mode 100644 openai-agents/human-approval/.post-create create mode 100644 openai-agents/human-approval/.prettierignore create mode 100644 openai-agents/human-approval/.prettierrc create mode 100644 openai-agents/human-approval/README.md create mode 100644 openai-agents/human-approval/package.json create mode 100644 openai-agents/human-approval/src/activities.ts create mode 100644 openai-agents/human-approval/src/client.ts create mode 100644 openai-agents/human-approval/src/mocha/fake-model.ts create mode 100644 openai-agents/human-approval/src/mocha/workflows.test.ts create mode 100644 openai-agents/human-approval/src/worker.ts create mode 100644 openai-agents/human-approval/src/workflows.ts create mode 100644 openai-agents/human-approval/tsconfig.json create mode 100644 openai-agents/mcp/.eslintignore create mode 100644 openai-agents/mcp/.eslintrc.js create mode 100644 openai-agents/mcp/.gitignore create mode 100644 openai-agents/mcp/.npmrc create mode 100644 openai-agents/mcp/.nvmrc create mode 100644 openai-agents/mcp/.post-create create mode 100644 openai-agents/mcp/.prettierignore create mode 100644 openai-agents/mcp/.prettierrc create mode 100644 openai-agents/mcp/README.md create mode 100644 openai-agents/mcp/package.json create mode 100644 openai-agents/mcp/src/activities.ts create mode 100644 openai-agents/mcp/src/client.ts create mode 100644 openai-agents/mcp/src/mocha/fake-model.ts create mode 100644 openai-agents/mcp/src/mocha/workflows.test.ts create mode 100644 openai-agents/mcp/src/servers/filesystem-server.ts create mode 100644 openai-agents/mcp/src/servers/notes-server.ts create mode 100644 openai-agents/mcp/src/servers/prompt-server.ts create mode 100644 openai-agents/mcp/src/servers/sample-files/hello.txt create mode 100644 openai-agents/mcp/src/servers/sample-files/notes.txt create mode 100644 openai-agents/mcp/src/servers/sse-server.ts create mode 100644 openai-agents/mcp/src/servers/tools-server.ts create mode 100644 openai-agents/mcp/src/worker.ts create mode 100644 openai-agents/mcp/src/workflows.ts create mode 100644 openai-agents/mcp/tsconfig.json create mode 100644 openai-agents/model-providers/.eslintignore create mode 100644 openai-agents/model-providers/.eslintrc.js create mode 100644 openai-agents/model-providers/.gitignore create mode 100644 openai-agents/model-providers/.npmrc create mode 100644 openai-agents/model-providers/.nvmrc create mode 100644 openai-agents/model-providers/.post-create create mode 100644 openai-agents/model-providers/.prettierignore create mode 100644 openai-agents/model-providers/.prettierrc create mode 100644 openai-agents/model-providers/README.md create mode 100644 openai-agents/model-providers/package.json create mode 100644 openai-agents/model-providers/src/client.ts create mode 100644 openai-agents/model-providers/src/mocha/fake-model.ts create mode 100644 openai-agents/model-providers/src/mocha/workflows.test.ts create mode 100644 openai-agents/model-providers/src/worker.ts create mode 100644 openai-agents/model-providers/src/workflows.ts create mode 100644 openai-agents/model-providers/tsconfig.json create mode 100644 openai-agents/nexus-tools/.eslintignore create mode 100644 openai-agents/nexus-tools/.eslintrc.js create mode 100644 openai-agents/nexus-tools/.gitignore create mode 100644 openai-agents/nexus-tools/.npmrc create mode 100644 openai-agents/nexus-tools/.nvmrc create mode 100644 openai-agents/nexus-tools/.post-create create mode 100644 openai-agents/nexus-tools/.prettierignore create mode 100644 openai-agents/nexus-tools/.prettierrc create mode 100644 openai-agents/nexus-tools/README.md create mode 100644 openai-agents/nexus-tools/package.json create mode 100644 openai-agents/nexus-tools/src/api.ts create mode 100644 openai-agents/nexus-tools/src/client.ts create mode 100644 openai-agents/nexus-tools/src/handler.ts create mode 100644 openai-agents/nexus-tools/src/mocha/fake-model.ts create mode 100644 openai-agents/nexus-tools/src/mocha/workflows.test.ts create mode 100644 openai-agents/nexus-tools/src/worker.ts create mode 100644 openai-agents/nexus-tools/src/workflows.ts create mode 100644 openai-agents/nexus-tools/tsconfig.json create mode 100644 openai-agents/reasoning-content/.eslintignore create mode 100644 openai-agents/reasoning-content/.eslintrc.js create mode 100644 openai-agents/reasoning-content/.gitignore create mode 100644 openai-agents/reasoning-content/.npmrc create mode 100644 openai-agents/reasoning-content/.nvmrc create mode 100644 openai-agents/reasoning-content/.post-create create mode 100644 openai-agents/reasoning-content/.prettierignore create mode 100644 openai-agents/reasoning-content/.prettierrc create mode 100644 openai-agents/reasoning-content/README.md create mode 100644 openai-agents/reasoning-content/package.json create mode 100644 openai-agents/reasoning-content/src/activities.ts create mode 100644 openai-agents/reasoning-content/src/client.ts create mode 100644 openai-agents/reasoning-content/src/mocha/workflows.test.ts create mode 100644 openai-agents/reasoning-content/src/worker.ts create mode 100644 openai-agents/reasoning-content/src/workflows.ts create mode 100644 openai-agents/reasoning-content/tsconfig.json create mode 100644 openai-agents/research-bot/.eslintignore create mode 100644 openai-agents/research-bot/.eslintrc.js create mode 100644 openai-agents/research-bot/.gitignore create mode 100644 openai-agents/research-bot/.npmrc create mode 100644 openai-agents/research-bot/.nvmrc create mode 100644 openai-agents/research-bot/.post-create create mode 100644 openai-agents/research-bot/.prettierignore create mode 100644 openai-agents/research-bot/.prettierrc create mode 100644 openai-agents/research-bot/README.md create mode 100644 openai-agents/research-bot/package.json create mode 100644 openai-agents/research-bot/src/client.ts create mode 100644 openai-agents/research-bot/src/mocha/fake-model.ts create mode 100644 openai-agents/research-bot/src/mocha/workflows.test.ts create mode 100644 openai-agents/research-bot/src/worker.ts create mode 100644 openai-agents/research-bot/src/workflows.ts create mode 100644 openai-agents/research-bot/tsconfig.json create mode 100644 openai-agents/sessions/.eslintignore create mode 100644 openai-agents/sessions/.eslintrc.js create mode 100644 openai-agents/sessions/.gitignore create mode 100644 openai-agents/sessions/.npmrc create mode 100644 openai-agents/sessions/.nvmrc create mode 100644 openai-agents/sessions/.post-create create mode 100644 openai-agents/sessions/.prettierignore create mode 100644 openai-agents/sessions/.prettierrc create mode 100644 openai-agents/sessions/README.md create mode 100644 openai-agents/sessions/package.json create mode 100644 openai-agents/sessions/src/activities.ts create mode 100644 openai-agents/sessions/src/client.ts create mode 100644 openai-agents/sessions/src/mocha/fake-model.ts create mode 100644 openai-agents/sessions/src/mocha/workflows.test.ts create mode 100644 openai-agents/sessions/src/worker.ts create mode 100644 openai-agents/sessions/src/workflows.ts create mode 100644 openai-agents/sessions/tsconfig.json create mode 100644 openai-agents/tools/.eslintignore create mode 100644 openai-agents/tools/.eslintrc.js create mode 100644 openai-agents/tools/.gitignore create mode 100644 openai-agents/tools/.npmrc create mode 100644 openai-agents/tools/.nvmrc create mode 100644 openai-agents/tools/.post-create create mode 100644 openai-agents/tools/.prettierignore create mode 100644 openai-agents/tools/.prettierrc create mode 100644 openai-agents/tools/README.md create mode 100644 openai-agents/tools/package.json create mode 100644 openai-agents/tools/src/client.ts create mode 100644 openai-agents/tools/src/mocha/fake-model.ts create mode 100644 openai-agents/tools/src/mocha/workflows.test.ts create mode 100644 openai-agents/tools/src/worker.ts create mode 100644 openai-agents/tools/src/workflows.ts create mode 100644 openai-agents/tools/tsconfig.json create mode 100644 openai-agents/tracing/.eslintignore create mode 100644 openai-agents/tracing/.eslintrc.js create mode 100644 openai-agents/tracing/.gitignore create mode 100644 openai-agents/tracing/.npmrc create mode 100644 openai-agents/tracing/.nvmrc create mode 100644 openai-agents/tracing/.post-create create mode 100644 openai-agents/tracing/.prettierignore create mode 100644 openai-agents/tracing/.prettierrc create mode 100644 openai-agents/tracing/README.md create mode 100644 openai-agents/tracing/package.json create mode 100644 openai-agents/tracing/src/client.ts create mode 100644 openai-agents/tracing/src/mocha/fake-model.ts create mode 100644 openai-agents/tracing/src/mocha/workflows.test.ts create mode 100644 openai-agents/tracing/src/recording-processor.ts create mode 100644 openai-agents/tracing/src/worker.ts create mode 100644 openai-agents/tracing/src/workflows.ts create mode 100644 openai-agents/tracing/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a9e1bfa..1a152f39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,20 @@ jobs: timer-examples message-passing/introduction message-passing/safe-message-handlers + openai-agents/agent-patterns + openai-agents/basic + openai-agents/customer-service + openai-agents/handoffs + openai-agents/hosted-mcp + openai-agents/human-approval + openai-agents/mcp + openai-agents/model-providers + openai-agents/nexus-tools + openai-agents/reasoning-content + openai-agents/research-bot + openai-agents/sessions + openai-agents/tools + openai-agents/tracing polling/infrequent ) for project in "${projects[@]}"; do diff --git a/.scripts/copy-shared-files.mjs b/.scripts/copy-shared-files.mjs index 0414125f..8e10d2fa 100644 --- a/.scripts/copy-shared-files.mjs +++ b/.scripts/copy-shared-files.mjs @@ -13,6 +13,7 @@ const ADDITIONAL_SAMPLES = []; // as samples. const HAS_CHILD_SAMPLES = [ 'message-passing', + 'openai-agents', 'polling', ]; diff --git a/.scripts/list-of-samples.json b/.scripts/list-of-samples.json index 80d55b91..1567f751 100644 --- a/.scripts/list-of-samples.json +++ b/.scripts/list-of-samples.json @@ -54,6 +54,20 @@ "message-passing/execute-update", "message-passing/introduction", "message-passing/safe-message-handlers", + "openai-agents/agent-patterns", + "openai-agents/basic", + "openai-agents/customer-service", + "openai-agents/handoffs", + "openai-agents/hosted-mcp", + "openai-agents/human-approval", + "openai-agents/mcp", + "openai-agents/model-providers", + "openai-agents/nexus-tools", + "openai-agents/reasoning-content", + "openai-agents/research-bot", + "openai-agents/sessions", + "openai-agents/tools", + "openai-agents/tracing", "polling/infrequent" ] } \ No newline at end of file diff --git a/README.md b/README.md index 571103cc..37ede1c8 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,25 @@ -- [samples-typescript](#samples-typescript) - - [Running](#running) - - [Locally](#locally) - - [Scaffold](#scaffold) - - [Samples](#samples) - - [Basic](#basic) - - [API demos](#api-demos) - - [Activity APIs and design patterns](#activity-apis-and-design-patterns) - - [Nexus APIs](#nexus-apis) - - [Workflow APIs](#workflow-apis) - - [Production APIs](#production-apis) - - [Advanced APIs](#advanced-apis) - - [Test APIs](#test-apis) - - [Full-stack apps](#full-stack-apps) - - [External apps \& libraries](#external-apps--libraries) - - [Contributing](#contributing) - - [Dependencies](#dependencies) - - [Upgrading the SDK version in `package.json`s](#upgrading-the-sdk-version-in-packagejsons) - - [Config files](#config-files) +- [Running](#running) + - [Locally](#locally) + - [Scaffold](#scaffold) +- [Samples](#samples) + - [Basic](#basic) + - [API demos](#api-demos) + - [Activity APIs and design patterns](#activity-apis-and-design-patterns) + - [Nexus APIs](#nexus-apis) + - [Workflow APIs](#workflow-apis) + - [Production APIs](#production-apis) + - [Advanced APIs](#advanced-apis) + - [Test APIs](#test-apis) + - [AI / LLM](#ai--llm) + - [Full-stack apps](#full-stack-apps) +- [External apps & libraries](#external-apps--libraries) +- [Contributing](#contributing) + - [Dependencies](#dependencies) + - [Upgrading the SDK version in `package.json`s](#upgrading-the-sdk-version-in-packagejsons) + - [Config files](#config-files) @@ -161,6 +161,24 @@ and you'll be given the list of sample options. - [**Mocha with code coverage or Jest**](https://github.com/temporalio/samples-typescript/tree/main/activities-examples#testing) - [**Time skipping**](https://github.com/temporalio/samples-typescript/tree/main/timer-examples#testing) +#### AI / LLM + +- [**OpenAI Agents**](./openai-agents): Run [OpenAI Agents SDK](https://github.com/openai/openai-agents-js) agents as Temporal Workflows with the `@temporalio/openai-agents` integration. The [`openai-agents/`](./openai-agents) directory contains fourteen samples: + - [**Basic**](./openai-agents/basic): A single agent plus the building blocks — Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. + - [**Handoffs**](./openai-agents/handoffs): A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. + - [**Agent Patterns**](./openai-agents/agent-patterns): Multi-agent orchestration patterns — deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. + - [**Sessions**](./openai-agents/sessions): Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. + - [**Human Approval**](./openai-agents/human-approval): A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. + - [**Tools**](./openai-agents/tools): Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider in the model Activity. + - [**Tracing**](./openai-agents/tracing): The three supported tracing paths — a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry — plus `temporal:*` orchestration spans. + - [**Model Providers**](./openai-agents/model-providers): Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. + - [**Reasoning Content**](./openai-agents/reasoning-content): Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. + - [**MCP**](./openai-agents/mcp): Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. + - [**Hosted MCP**](./openai-agents/hosted-mcp): A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. + - [**Research Bot**](./openai-agents/research-bot): A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. + - [**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`. + ### Full-stack apps - **Next.js**: diff --git a/openai-agents/README.md b/openai-agents/README.md new file mode 100644 index 00000000..b0afc0f6 --- /dev/null +++ b/openai-agents/README.md @@ -0,0 +1,62 @@ +# OpenAI Agents + +These samples use the `@temporalio/openai-agents` integration to run [OpenAI Agents SDK](https://github.com/openai/openai-agents-js) agents as Temporal Workflows. Agent orchestration — the agent loop, handoffs, tool calls, and guardrails — runs inside the Workflow, while model calls run as durable Activities, so they retry on failure and are not repeated during Workflow replay. + +Each subdirectory is a standalone sample with its own `package.json` and README. The integration package itself is documented in the [`@temporalio/openai-agents` README](https://github.com/temporalio/sdk-typescript/tree/main/packages/openai-agents). + +## Prerequisites + +These apply to every sample in this directory: + +- A running Temporal dev server: `temporal server start-dev`. +- Node 22 or later. +- An OpenAI API key: `export OPENAI_API_KEY=...`. +- Dependencies installed in the sample directory: `npm install`. + +Each sample's README describes how to start its Worker and run its scenarios. + +## Samples + +| Sample | Demonstrates | +| :------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- | +| [`basic`](./basic) | A single agent plus the core building blocks: Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. | +| [`handoffs`](./handoffs) | A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. | +| [`agent-patterns`](./agent-patterns) | Multi-agent orchestration patterns: deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. | +| [`sessions`](./sessions) | Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. | +| [`human-approval`](./human-approval) | A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. | +| [`tools`](./tools) | Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider during the model Activity. | +| [`tracing`](./tracing) | The three supported tracing paths: a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry, plus `temporal:*` orchestration spans. | +| [`model-providers`](./model-providers) | Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. | +| [`reasoning-content`](./reasoning-content) | Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. | +| [`mcp`](./mcp) | Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. | +| [`hosted-mcp`](./hosted-mcp) | A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. | +| [`research-bot`](./research-bot) | A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. | +| [`customer-service`](./customer-service) | A long-running, multi-turn Workflow driven by Updates and Queries, with triage handoffs and `continueAsNew` to bound history. | +| [`nexus-tools`](./nexus-tools) | Expose a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with `nexusOperationAsTool`. | + +## Feature support + +Any OpenAI Agents SDK `ModelProvider` can drive the model Activity. The provider runs in the Activity, never inside the Workflow sandbox. + +| Feature | Status | Notes | +| :---------------------- | :------------ | :-------------------------------------------------------------------------------------- | +| Multi-turn agents | Supported | Agent loop runs durably in the Workflow | +| Handoffs | Supported | `Agent` and `handoff()` forms | +| Inline function tools | Supported | Must be deterministic | +| Activity-backed tools | Supported | Via `activityAsTool()` | +| Nexus operation tools | Supported | Via `nexusOperationAsTool()` | +| Nested agent tools | Supported | Via `agentAsTool()` | +| Hosted tools | Supported | Executed server-side by the model provider | +| Stateless MCP servers | Supported | Via `StatelessMCPServerProvider` and `statelessMcpServer()` | +| Stateful MCP servers | Supported | Via `StatefulMCPServerProvider` and `statefulMcpServer()` | +| Sessions | Supported | Via `WorkflowSafeMemorySession`; upstream `MemorySession` is rejected | +| Run state and approvals | Supported | Serialize with `result.state.toString()` and rehydrate with `RunState.fromString` | +| Guardrails | Supported | Guardrail callbacks must be deterministic | +| Tracing | Supported | OpenAI hosted traces, custom `TracingProcessor`s, OTel, and optional `temporal:*` spans | +| Agent context | Supported | Activity tools receive a copy | +| `continueAsNew` | Supported | Plugin config propagates to the continuation | +| Child Workflows | Supported | Plugin config propagates to children | +| Local Activities | Supported | Set `useLocalActivity: true` in `modelParams` | +| Model override per run | Supported | `runConfig.model` accepts a string model name | +| Streaming | Not supported | Use `runner.run()` | +| Voice agents | Not supported | | diff --git a/openai-agents/agent-patterns/.eslintignore b/openai-agents/agent-patterns/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/agent-patterns/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/agent-patterns/.eslintrc.js b/openai-agents/agent-patterns/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/agent-patterns/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/agent-patterns/.gitignore b/openai-agents/agent-patterns/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/agent-patterns/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/agent-patterns/.npmrc b/openai-agents/agent-patterns/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/agent-patterns/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/agent-patterns/.nvmrc b/openai-agents/agent-patterns/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/agent-patterns/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/agent-patterns/.post-create b/openai-agents/agent-patterns/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/agent-patterns/.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/openai-agents/agent-patterns/.prettierignore b/openai-agents/agent-patterns/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/agent-patterns/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/agent-patterns/.prettierrc b/openai-agents/agent-patterns/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/agent-patterns/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/agent-patterns/README.md b/openai-agents/agent-patterns/README.md new file mode 100644 index 00000000..bc05eea0 --- /dev/null +++ b/openai-agents/agent-patterns/README.md @@ -0,0 +1,41 @@ +# OpenAI Agents: Agent Patterns + +Demonstrates common multi-agent orchestration patterns with the Temporal OpenAI Agents +integration. Each pattern is its own Workflow in `src/workflows.ts`. + +Scenarios: + +- **deterministic** — three agents run in sequence, each gating the next (outline → draft → polish). +- **parallelization** — `Promise.all` fans out to three agents, then a judge agent picks the best answer. +- **llm-as-judge** — a generate→judge loop that retries until the judge approves. +- **agents-as-tools** — an orchestrator uses `agentAsTool` to call a specialist agent as a tool. +- **input-guardrails** — `runConfig.inputGuardrails` blocks forbidden input before the model runs. +- **output-guardrails** — `runConfig.outputGuardrails` blocks unsafe model output after the model runs. + +## Run + +```bash +npm install +npm run build + +# In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): +OPENAI_API_KEY=sk-... npm start + +# In another terminal, start a scenario: +npm run workflow deterministic +npm run workflow parallelization +npm run workflow llm-as-judge +npm run workflow agents-as-tools +npm run workflow input-guardrails +npm run workflow output-guardrails +``` + +## Test + +```bash +npm test +``` + +Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no +`OPENAI_API_KEY` is required. Each pattern has a test asserting its mechanism (call counts, +history threading, tool round-trips, and guardrail tripwires). diff --git a/openai-agents/agent-patterns/package.json b/openai-agents/agent-patterns/package.json new file mode 100644 index 00000000..b4c0d7e8 --- /dev/null +++ b/openai-agents/agent-patterns/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-agent-patterns", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/agent-patterns/src/client.ts b/openai-agents/agent-patterns/src/client.ts new file mode 100644 index 00000000..7a990146 --- /dev/null +++ b/openai-agents/agent-patterns/src/client.ts @@ -0,0 +1,87 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { + deterministic, + parallelization, + llmAsJudge, + agentsAsTools, + inputGuardrail, + outputGuardrail, +} from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'deterministic'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-agent-patterns'; + const workflowId = 'openai-agents-' + nanoid(); + + let handle; + switch (scenario) { + case 'deterministic': + handle = await client.workflow.start(deterministic, { + taskQueue, + workflowId, + args: ['Write a short piece about the importance of software testing.'], + }); + break; + case 'parallelization': + handle = await client.workflow.start(parallelization, { + taskQueue, + workflowId, + args: ['What is the most important skill for a software engineer?'], + }); + break; + case 'llm-as-judge': + handle = await client.workflow.start(llmAsJudge, { + taskQueue, + workflowId, + args: ['Explain what Temporal is in two sentences.'], + }); + break; + case 'agents-as-tools': + handle = await client.workflow.start(agentsAsTools, { + taskQueue, + workflowId, + args: ['What are the benefits of durable execution?'], + }); + break; + case 'input-guardrails': + handle = await client.workflow.start(inputGuardrail, { + taskQueue, + workflowId, + args: ['Tell me something interesting about Temporal.'], + }); + break; + case 'output-guardrails': + handle = await client.workflow.start(outputGuardrail, { + taskQueue, + workflowId, + args: ['Describe a safe software deployment strategy.'], + }); + break; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/agent-patterns/src/mocha/fake-model.ts b/openai-agents/agent-patterns/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/agent-patterns/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/agent-patterns/src/mocha/workflows.test.ts b/openai-agents/agent-patterns/src/mocha/workflows.test.ts new file mode 100644 index 00000000..d81f8234 --- /dev/null +++ b/openai-agents/agent-patterns/src/mocha/workflows.test.ts @@ -0,0 +1,190 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import assert from 'assert'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { + deterministic, + parallelization, + llmAsJudge, + agentsAsTools, + inputGuardrail, + outputGuardrail, +} from '../workflows'; + +describe('openai-agents/agent-patterns workflow scenarios', function () { + this.timeout(60_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + async function makeWorker(taskQueue: string, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('deterministic: runs three agents in sequence, each gating the next', async () => { + const taskQueue = 'test-deterministic'; + const outlineText = 'Outline: Introduction, Body, Conclusion'; + const draftText = 'Draft: Here is the full draft.'; + const polishedText = 'Polished: Here is the final polished text.'; + const { worker, provider } = await makeWorker(taskQueue, [ + textResponse(outlineText), + textResponse(draftText), + textResponse(polishedText), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(deterministic, { + args: ['Write about testing.'], + workflowId: 'test-deterministic-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, polishedText); + assert.strictEqual(provider.model.requests.length, 3, 'should have made exactly 3 model calls'); + const secondTurnInput = provider.model.requests[1]?.input; + const secondTurnText = Array.isArray(secondTurnInput) ? JSON.stringify(secondTurnInput) : String(secondTurnInput); + assert.ok( + secondTurnText.includes(outlineText), + 'second agent input should contain the first agent output (outline)', + ); + }); + + it('parallelization: fans out to 3 agents concurrently then judge picks best', async () => { + const taskQueue = 'test-parallelization'; + const answer1 = 'Technical answer: reliability.'; + const answer2 = 'Business answer: value delivery.'; + const answer3 = 'Creative answer: problem-solving mindset.'; + const judgeAnswer = answer1; + const { worker, provider } = await makeWorker(taskQueue, [ + textResponse(answer1), + textResponse(answer2), + textResponse(answer3), + textResponse(judgeAnswer), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(parallelization, { + args: ['What is the most important skill?'], + workflowId: 'test-parallelization-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, judgeAnswer); + assert.strictEqual(provider.model.requests.length, 4, 'should have called model 3 times for agents + 1 for judge'); + }); + + it('llmAsJudge: loops generate→judge until approved, returns approved output', async () => { + const taskQueue = 'test-llm-as-judge'; + const firstDraft = 'First draft about Temporal.'; + const secondDraft = 'Improved draft about Temporal.'; + const { worker, provider } = await makeWorker(taskQueue, [ + textResponse(firstDraft), + textResponse('REJECTED: needs more detail'), + textResponse(secondDraft), + textResponse('APPROVED'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(llmAsJudge, { + args: ['Explain what Temporal is.'], + workflowId: 'test-llm-as-judge-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, secondDraft); + assert.strictEqual( + provider.model.requests.length, + 4, + 'should have made 4 model calls: generate, judge-reject, generate, judge-approve', + ); + }); + + it('agentsAsTools: orchestrator calls specialist tool and returns final output', async () => { + const taskQueue = 'test-agents-as-tools'; + const specialistAnswer = 'Specialist answer: durable execution ensures reliability.'; + const finalAnswer = 'Orchestrator synthesized: ' + specialistAnswer; + const { worker, provider } = await makeWorker(taskQueue, [ + toolCallResponse('ask_specialist', { input: 'What are the benefits of durable execution?' }), + textResponse(specialistAnswer), + textResponse(finalAnswer), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(agentsAsTools, { + args: ['What are the benefits of durable execution?'], + workflowId: 'test-agents-as-tools-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, finalAnswer); + const toolResults = provider.model.requests + .flatMap((req) => (Array.isArray(req.input) ? req.input : [])) + .filter((item) => item.type === 'function_call_result' && item.name === 'ask_specialist'); + assert.strictEqual(toolResults.length, 1, 'ask_specialist tool result should reach the orchestrator exactly once'); + }); + + it('inputGuardrail: blocks forbidden input before model is called', async () => { + const taskQueue = 'test-input-guardrail-blocked'; + const { worker, provider } = await makeWorker(taskQueue, []); + const result = await worker.runUntil( + testEnv.client.workflow.execute(inputGuardrail, { + args: ['This is a blocked request.'], + workflowId: 'test-input-guardrail-blocked-' + Date.now(), + taskQueue, + }), + ); + assert.ok(result.startsWith('BLOCKED:'), `expected BLOCKED sentinel, got: ${result}`); + assert.ok(result.includes('keyword-guardrail'), 'sentinel should name the guardrail'); + assert.strictEqual(provider.model.requests.length, 0, 'model should not be called when input guardrail fires'); + }); + + it('inputGuardrail: passes clean input through to model', async () => { + const taskQueue = 'test-input-guardrail-pass'; + const modelReply = 'Here is something interesting about Temporal.'; + const { worker } = await makeWorker(taskQueue, [textResponse(modelReply)]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(inputGuardrail, { + args: ['Tell me something interesting about Temporal.'], + workflowId: 'test-input-guardrail-pass-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, modelReply); + }); + + it('outputGuardrail: blocks unsafe model output after model call', async () => { + const taskQueue = 'test-output-guardrail'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('This response contains UNSAFE content.')]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(outputGuardrail, { + args: ['Describe something.'], + workflowId: 'test-output-guardrail-' + Date.now(), + taskQueue, + }), + ); + assert.ok(result.startsWith('BLOCKED:'), `expected BLOCKED sentinel, got: ${result}`); + assert.ok(result.includes('output-safety-guardrail'), 'sentinel should name the guardrail'); + assert.strictEqual(provider.model.requests.length, 1, 'model should be called once before output guardrail fires'); + }); +}); diff --git a/openai-agents/agent-patterns/src/worker.ts b/openai-agents/agent-patterns/src/worker.ts new file mode 100644 index 00000000..88e7d864 --- /dev/null +++ b/openai-agents/agent-patterns/src/worker.ts @@ -0,0 +1,42 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-agent-patterns', + workflowsPath: require.resolve('./workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/agent-patterns/src/workflows.ts b/openai-agents/agent-patterns/src/workflows.ts new file mode 100644 index 00000000..609cc0a5 --- /dev/null +++ b/openai-agents/agent-patterns/src/workflows.ts @@ -0,0 +1,197 @@ +import { Agent } from '@openai/agents-core'; +import { InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered } from '@openai/agents-core'; +import { ApplicationFailure } from '@temporalio/workflow'; +import type { + InputGuardrail, + InputGuardrailFunctionArgs, + GuardrailFunctionOutput, + OutputGuardrail, + OutputGuardrailFunctionArgs, +} from '@openai/agents-core'; +import { TemporalOpenAIRunner, agentAsTool } from '@temporalio/openai-agents/workflow'; + +export async function deterministic(prompt: string): Promise { + const outlineAgent = new Agent({ + name: 'OutlineAgent', + instructions: 'Create a brief outline for the given topic.', + }); + const draftAgent = new Agent({ + name: 'DraftAgent', + instructions: 'Write a short draft based on the outline provided.', + }); + const polishAgent = new Agent({ + name: 'PolishAgent', + instructions: 'Polish and improve the draft provided.', + }); + + const runner = new TemporalOpenAIRunner(); + + const outlineResult = await runner.run(outlineAgent, prompt); + const outline = outlineResult.finalOutput ?? ''; + + const draftResult = await runner.run(draftAgent, outline); + const draft = draftResult.finalOutput ?? ''; + + const polishResult = await runner.run(polishAgent, draft); + return polishResult.finalOutput ?? ''; +} + +export async function parallelization(prompt: string): Promise { + const agent1 = new Agent({ + name: 'Perspective1Agent', + instructions: 'Answer the question from a technical perspective.', + }); + const agent2 = new Agent({ + name: 'Perspective2Agent', + instructions: 'Answer the question from a business perspective.', + }); + const agent3 = new Agent({ + name: 'Perspective3Agent', + instructions: 'Answer the question from a creative perspective.', + }); + const judgeAgent = new Agent({ + name: 'JudgeAgent', + instructions: 'You are given multiple candidate answers. Select the best one and return it verbatim.', + }); + + const runner = new TemporalOpenAIRunner(); + + const [r1, r2, r3] = await Promise.all([ + runner.run(agent1, prompt), + runner.run(agent2, prompt), + runner.run(agent3, prompt), + ]); + + const candidates = [r1.finalOutput ?? '', r2.finalOutput ?? '', r3.finalOutput ?? '']; + const judgeInput = candidates.map((c, i) => `Candidate ${i + 1}:\n${c}`).join('\n\n'); + + const judgeResult = await runner.run(judgeAgent, judgeInput); + return judgeResult.finalOutput ?? ''; +} + +export async function llmAsJudge(prompt: string): Promise { + const generatorAgent = new Agent({ + name: 'GeneratorAgent', + instructions: 'Generate a response to the given prompt.', + }); + const judgeAgent = new Agent({ + name: 'JudgeAgent', + instructions: + 'Evaluate the response. Reply with exactly "APPROVED" if it is good, or "REJECTED: " if it needs improvement.', + }); + + const runner = new TemporalOpenAIRunner(); + const maxIterations = 3; + let output = ''; + + for (let i = 0; i < maxIterations; i++) { + const genResult = await runner.run(generatorAgent, i === 0 ? prompt : `Improve: ${output}`); + output = genResult.finalOutput ?? ''; + + const judgeResult = await runner.run(judgeAgent, output); + const judgment = judgeResult.finalOutput ?? ''; + + if (judgment.startsWith('APPROVED')) { + return output; + } + } + + return output; +} + +export async function agentsAsTools(prompt: string): Promise { + const specialistAgent = new Agent({ + name: 'SpecialistAgent', + instructions: 'You are a specialist. Answer questions concisely.', + }); + + const specialistTool = agentAsTool(specialistAgent, { + toolName: 'ask_specialist', + toolDescription: 'Ask the specialist agent a question and get a concise answer.', + }); + + const orchestratorAgent = new Agent({ + name: 'OrchestratorAgent', + instructions: + 'You orchestrate tasks. Use the ask_specialist tool to get answers, then synthesize a final response.', + tools: [specialistTool], + }); + + const runner = new TemporalOpenAIRunner(); + const result = await runner.run(orchestratorAgent, prompt); + return result.finalOutput ?? ''; +} + +export async function inputGuardrail(prompt: string): Promise { + const blockedKeywords = ['blocked', 'BLOCK', 'forbidden']; + + const guardrail: InputGuardrail = { + name: 'keyword-guardrail', + execute: async (args: InputGuardrailFunctionArgs): Promise => { + const inputText = typeof args.input === 'string' ? args.input : JSON.stringify(args.input); + const tripped = blockedKeywords.some((kw) => inputText.includes(kw)); + return { tripwireTriggered: tripped, outputInfo: { checked: inputText } }; + }, + }; + + const agent = new Agent({ + name: 'GuardedAgent', + instructions: 'You are a helpful assistant.', + }); + + const runner = new TemporalOpenAIRunner(); + try { + const result = await runner.run(agent, prompt, { + runConfig: { inputGuardrails: [guardrail] }, + }); + return result.finalOutput ?? ''; + } catch (err) { + const tripwire = + err instanceof InputGuardrailTripwireTriggered + ? err + : err instanceof ApplicationFailure && err.cause instanceof InputGuardrailTripwireTriggered + ? err.cause + : undefined; + if (tripwire) { + return `BLOCKED: input failed guardrail "${tripwire.result.guardrail.name}"`; + } + throw err; + } +} + +export async function outputGuardrail(prompt: string): Promise { + const bannedPhrase = 'UNSAFE'; + + const guardrail: OutputGuardrail = { + name: 'output-safety-guardrail', + execute: async (args: OutputGuardrailFunctionArgs): Promise => { + const outputText = typeof args.agentOutput === 'string' ? args.agentOutput : JSON.stringify(args.agentOutput); + const tripped = outputText.includes(bannedPhrase); + return { tripwireTriggered: tripped, outputInfo: { checked: outputText } }; + }, + }; + + const agent = new Agent({ + name: 'GuardedOutputAgent', + instructions: 'You are a helpful assistant.', + }); + + const runner = new TemporalOpenAIRunner(); + try { + const result = await runner.run(agent, prompt, { + runConfig: { outputGuardrails: [guardrail] }, + }); + return result.finalOutput ?? ''; + } catch (err) { + const tripwire = + err instanceof OutputGuardrailTripwireTriggered + ? err + : err instanceof ApplicationFailure && err.cause instanceof OutputGuardrailTripwireTriggered + ? err.cause + : undefined; + if (tripwire) { + return `BLOCKED: output failed guardrail "${tripwire.result.guardrail.name}"`; + } + throw err; + } +} diff --git a/openai-agents/agent-patterns/tsconfig.json b/openai-agents/agent-patterns/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/agent-patterns/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/basic/.eslintignore b/openai-agents/basic/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/basic/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/basic/.eslintrc.js b/openai-agents/basic/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/basic/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/basic/.gitignore b/openai-agents/basic/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/basic/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/basic/.npmrc b/openai-agents/basic/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/basic/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/basic/.nvmrc b/openai-agents/basic/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/basic/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/basic/.post-create b/openai-agents/basic/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/basic/.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/openai-agents/basic/.prettierignore b/openai-agents/basic/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/basic/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/basic/.prettierrc b/openai-agents/basic/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/basic/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/basic/README.md b/openai-agents/basic/README.md new file mode 100644 index 00000000..cbfd4e6e --- /dev/null +++ b/openai-agents/basic/README.md @@ -0,0 +1,42 @@ +# OpenAI Agents: Basic + +Demonstrates the building blocks of the Temporal OpenAI Agents integration: a single agent, the +different ways to give it tools, run context, structured output, per-run model overrides, and +dynamic instructions — each running durably as a Temporal Workflow. + +Scenarios (`src/workflows.ts`): + +- **hello-world** — a single agent that returns model text, with no tools. +- **tools** — an Activity-backed tool wired in with `activityAsTool` (getWeather). +- **inline-tool** — an inline deterministic `tool()` that runs inside the Workflow (adds two numbers). +- **local-activity-tool** — a tool backed by a local Activity via `proxyLocalActivities` (getHeadlines). +- **agent-context** — a tool reads `runContext.context` (the userId) and that value reaches the model. +- **structured-output** — an agent with a zod `outputType` that returns a typed object. +- **model-override** — `runConfig.model` overrides the model for a single run. +- **dynamic-instructions** — instructions computed from `runContext.context` (userName and style). + +## Run + +```bash +npm install +npm run build + +# In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): +OPENAI_API_KEY=sk-... npm start + +# In another terminal, start a scenario: +npm run workflow hello-world +npm run workflow tools +npm run workflow structured-output +``` + +## Test + +```bash +npm test +``` + +Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no +`OPENAI_API_KEY` is required. They assert each scenario's mechanism — that tools are invoked, that +context reaches the model, that structured output is typed, and that the model override and dynamic +instructions take effect. diff --git a/openai-agents/basic/package.json b/openai-agents/basic/package.json new file mode 100644 index 00000000..6edd5cb4 --- /dev/null +++ b/openai-agents/basic/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-basic", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/basic/src/activities.ts b/openai-agents/basic/src/activities.ts new file mode 100644 index 00000000..ed220cd0 --- /dev/null +++ b/openai-agents/basic/src/activities.ts @@ -0,0 +1,10 @@ +export async function getWeather(input: { city: string }): Promise { + return JSON.stringify({ city: input.city, temperature: '22C', conditions: 'Sunny' }); +} + +export async function getHeadlines(input: { topic: string }): Promise { + return JSON.stringify({ + topic: input.topic, + headlines: [`Breaking: ${input.topic} makes news`, `Update on ${input.topic}`], + }); +} diff --git a/openai-agents/basic/src/client.ts b/openai-agents/basic/src/client.ts new file mode 100644 index 00000000..03f8e640 --- /dev/null +++ b/openai-agents/basic/src/client.ts @@ -0,0 +1,83 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { + helloWorld, + tools, + inlineTool, + localActivityTool, + agentContext, + structuredOutput, + modelOverride, + dynamicInstructions, +} from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'hello-world'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-basic'; + const workflowId = 'openai-agents-' + nanoid(); + + let handle; + switch (scenario) { + case 'hello-world': + handle = await client.workflow.start(helloWorld, { taskQueue, workflowId, args: ['Say hello in one sentence.'] }); + break; + case 'tools': + handle = await client.workflow.start(tools, { taskQueue, workflowId, args: ['What is the weather in Tokyo?'] }); + break; + case 'inline-tool': + handle = await client.workflow.start(inlineTool, { taskQueue, workflowId, args: ['What is 42 plus 58?'] }); + break; + case 'local-activity-tool': + handle = await client.workflow.start(localActivityTool, { + taskQueue, + workflowId, + args: ['Get headlines about AI.'], + }); + break; + case 'agent-context': + handle = await client.workflow.start(agentContext, { taskQueue, workflowId, args: ['Who am I?'] }); + break; + case 'structured-output': + handle = await client.workflow.start(structuredOutput, { + taskQueue, + workflowId, + args: ['Temporal is a durable execution platform for building reliable distributed systems.'], + }); + break; + case 'model-override': + handle = await client.workflow.start(modelOverride, { + taskQueue, + workflowId, + args: ['Briefly explain what Temporal is.'], + }); + break; + case 'dynamic-instructions': + handle = await client.workflow.start(dynamicInstructions, { taskQueue, workflowId, args: ['Hello!'] }); + break; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/basic/src/mocha/fake-model.ts b/openai-agents/basic/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/basic/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/basic/src/mocha/workflows.test.ts b/openai-agents/basic/src/mocha/workflows.test.ts new file mode 100644 index 00000000..77021024 --- /dev/null +++ b/openai-agents/basic/src/mocha/workflows.test.ts @@ -0,0 +1,190 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import * as activities from '../activities'; +import assert from 'assert'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { + helloWorld, + tools, + inlineTool, + localActivityTool, + agentContext, + structuredOutput, + modelOverride, + dynamicInstructions, +} from '../workflows'; + +describe('openai-agents/basic 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, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities, + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('helloWorld: returns model text output', async () => { + const taskQueue = 'test-hello-world'; + const { worker } = await makeWorker(taskQueue, [textResponse('Hello from the fake model!')]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(helloWorld, { + args: ['Say hello.'], + workflowId: 'test-hello-world-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Hello from the fake model!'); + }); + + it('tools: activityAsTool round-trip', async () => { + const taskQueue = 'test-tools'; + const { worker } = await makeWorker(taskQueue, [ + toolCallResponse('getWeather', { city: 'Tokyo' }), + textResponse('It is sunny in Tokyo.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(tools, { + args: ['What is the weather in Tokyo?'], + workflowId: 'test-tools-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'It is sunny in Tokyo.'); + }); + + it('inlineTool: inline tool round-trip', async () => { + const taskQueue = 'test-inline-tool'; + const { worker } = await makeWorker(taskQueue, [ + toolCallResponse('add', { a: 3, b: 4 }), + textResponse('3 plus 4 equals 7.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(inlineTool, { + args: ['What is 3 + 4?'], + workflowId: 'test-inline-tool-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, '3 plus 4 equals 7.'); + }); + + it('localActivityTool: local activity tool round-trip', async () => { + const taskQueue = 'test-local-activity-tool'; + const { worker } = await makeWorker(taskQueue, [ + toolCallResponse('getHeadlines', { topic: 'AI' }), + textResponse('Here are the latest AI headlines.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(localActivityTool, { + args: ['Get headlines about AI.'], + workflowId: 'test-local-activity-tool-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Here are the latest AI headlines.'); + }); + + it('agentContext: tool reads runContext.context and value reaches the model', async () => { + const taskQueue = 'test-agent-context'; + const { worker, provider } = await makeWorker(taskQueue, [ + toolCallResponse('whoAmI', {}), + textResponse('You are user-42.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(agentContext, { + args: ['Who am I?'], + workflowId: 'test-agent-context-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'You are user-42.'); + + // The whoAmI tool returns runContext.context.userId; its result is sent back + // to the model on the second turn as a function_call_result item, proving the + // context-derived value flowed through the tool to the model. + const toolResults = provider.model.requests + .flatMap((req) => (Array.isArray(req.input) ? req.input : [])) + .filter((item) => item.type === 'function_call_result' && item.name === 'whoAmI'); + assert.strictEqual(toolResults.length, 1, 'whoAmI tool result should reach the model exactly once'); + assert.deepStrictEqual((toolResults[0] as { output: unknown }).output, { type: 'text', text: 'user-42' }); + }); + + it('structuredOutput: returns typed object as JSON', async () => { + const taskQueue = 'test-structured-output'; + const payload = { title: 'Temporal', summary: 'A durable execution platform.', keywords: ['temporal', 'workflow'] }; + const { worker } = await makeWorker(taskQueue, [textResponse(JSON.stringify(payload))]); + const raw = await worker.runUntil( + testEnv.client.workflow.execute(structuredOutput, { + args: ['Temporal is a durable execution platform for building reliable distributed systems.'], + workflowId: 'test-structured-output-' + Date.now(), + taskQueue, + }), + ); + const parsed = JSON.parse(raw); + assert.strictEqual(parsed.title, 'Temporal'); + assert.ok(typeof parsed.summary === 'string'); + assert.ok(Array.isArray(parsed.keywords)); + }); + + it('modelOverride: runConfig.model is the model resolved by the provider', async () => { + const taskQueue = 'test-model-override'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('Temporal is a workflow engine.')]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(modelOverride, { + args: ['Briefly explain what Temporal is.'], + workflowId: 'test-model-override-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Temporal is a workflow engine.'); + assert.deepStrictEqual(provider.requestedModelNames, ['gpt-4o-mini']); + }); + + it('dynamicInstructions: context-derived instruction reaches the model', async () => { + const taskQueue = 'test-dynamic-instructions'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('Hello back!')]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(dynamicInstructions, { + args: ['Hello!'], + workflowId: 'test-dynamic-instructions-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Hello back!'); + + // The instructions function computes the system prompt from runContext.context + // ({ userName: 'Ada', style: 'concise' }); assert that resolved prompt reached the model. + const systemInstructions = provider.model.requests[0]?.systemInstructions; + assert.strictEqual( + systemInstructions, + 'You are a helpful assistant. Address the user as Ada and respond in a concise style.', + ); + }); +}); diff --git a/openai-agents/basic/src/worker.ts b/openai-agents/basic/src/worker.ts new file mode 100644 index 00000000..d90f5327 --- /dev/null +++ b/openai-agents/basic/src/worker.ts @@ -0,0 +1,44 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import * as activities from './activities'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-basic', + workflowsPath: require.resolve('./workflows'), + activities, + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/basic/src/workflows.ts b/openai-agents/basic/src/workflows.ts new file mode 100644 index 00000000..45f6aef0 --- /dev/null +++ b/openai-agents/basic/src/workflows.ts @@ -0,0 +1,135 @@ +import { Agent, RunContext, tool } from '@openai/agents-core'; +import { TemporalOpenAIRunner, activityAsTool } from '@temporalio/openai-agents/workflow'; +import { proxyLocalActivities } from '@temporalio/workflow'; +import z from 'zod'; +import type * as activities from './activities'; + +const localActivities = proxyLocalActivities({ startToCloseTimeout: '10 seconds' }); + +export async function helloWorld(prompt: string): Promise { + const agent = new Agent({ name: 'HelloAgent', instructions: 'You are a helpful assistant.' }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function tools(prompt: string): Promise { + const weatherTool = activityAsTool( + { + name: 'getWeather', + description: 'Get the current weather for a city.', + parameters: { + type: 'object', + properties: { city: { type: 'string', description: 'The city name' } }, + required: ['city'], + additionalProperties: false, + }, + }, + { startToCloseTimeout: '1 minute' }, + ); + + const agent = new Agent({ + name: 'WeatherAgent', + instructions: 'You are a helpful weather assistant. Always use the getWeather tool to answer weather questions.', + tools: [weatherTool], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function inlineTool(prompt: string): Promise { + const addTool = tool({ + name: 'add', + description: 'Add two numbers together.', + parameters: z.object({ a: z.number().describe('First number'), b: z.number().describe('Second number') }), + execute: async ({ a, b }) => String(a + b), + }); + + const agent = new Agent({ + name: 'MathAgent', + instructions: 'You are a math assistant. Use the add tool to compute sums.', + tools: [addTool], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function localActivityTool(prompt: string): Promise { + const headlinesTool = tool({ + name: 'getHeadlines', + description: 'Get the latest headlines for a topic.', + parameters: z.object({ topic: z.string().describe('The topic to get headlines for') }), + execute: async ({ topic }) => localActivities.getHeadlines({ topic }), + }); + + const agent = new Agent({ + name: 'NewsAgent', + instructions: 'You are a news assistant. Use the getHeadlines tool to fetch headlines.', + tools: [headlinesTool], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function agentContext(prompt: string): Promise { + interface AppContext { + userId: string; + } + + const whoAmITool = tool({ + name: 'whoAmI', + description: 'Returns the current user ID from context.', + parameters: z.object({}), + execute: async (_args, runCtx?: RunContext) => runCtx?.context.userId ?? 'unknown', + }); + + const agent = new Agent({ + name: 'ContextAgent', + instructions: 'You are a helpful assistant. Use the whoAmI tool to identify the user.', + tools: [whoAmITool], + }); + + const result = await new TemporalOpenAIRunner().run(agent, prompt, { context: { userId: 'user-42' } }); + return result.finalOutput ?? ''; +} + +const SummarySchema = z.object({ + title: z.string().describe('Short title'), + summary: z.string().describe('One sentence summary'), + keywords: z.array(z.string()).describe('Key terms'), +}); + +export async function structuredOutput(prompt: string): Promise { + const agent = new Agent({ + name: 'SummaryAgent', + instructions: 'Summarize the provided text as structured JSON matching the output schema.', + outputType: SummarySchema, + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return JSON.stringify(result.finalOutput); +} + +export async function modelOverride(prompt: string): Promise { + const agent = new Agent({ name: 'OverrideAgent', instructions: 'You are a helpful assistant.' }); + const result = await new TemporalOpenAIRunner().run(agent, prompt, { + runConfig: { model: 'gpt-4o-mini' }, + }); + return result.finalOutput ?? ''; +} + +export async function dynamicInstructions(prompt: string): Promise { + interface UserContext { + userName: string; + style: string; + } + + const agent = new Agent({ + name: 'DynamicAgent', + instructions: (runContext: RunContext, _agent: Agent) => + `You are a helpful assistant. Address the user as ${runContext.context.userName} and respond in a ${runContext.context.style} style.`, + }); + + const result = await new TemporalOpenAIRunner().run(agent, prompt, { + context: { userName: 'Ada', style: 'concise' }, + }); + return result.finalOutput ?? ''; +} diff --git a/openai-agents/basic/tsconfig.json b/openai-agents/basic/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/basic/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/customer-service/.eslintignore b/openai-agents/customer-service/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/customer-service/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/customer-service/.eslintrc.js b/openai-agents/customer-service/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/customer-service/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/customer-service/.gitignore b/openai-agents/customer-service/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/customer-service/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/customer-service/.npmrc b/openai-agents/customer-service/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/customer-service/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/customer-service/.nvmrc b/openai-agents/customer-service/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/customer-service/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/customer-service/.post-create b/openai-agents/customer-service/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/customer-service/.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/openai-agents/customer-service/.prettierignore b/openai-agents/customer-service/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/customer-service/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/customer-service/.prettierrc b/openai-agents/customer-service/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/customer-service/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/customer-service/README.md b/openai-agents/customer-service/README.md new file mode 100644 index 00000000..25629c09 --- /dev/null +++ b/openai-agents/customer-service/README.md @@ -0,0 +1,44 @@ +# OpenAI Agents: Customer Service + +A stateful, multi-turn customer-service Workflow built with `@temporalio/openai-agents`. It mirrors +the OpenAI Agents SDK airline customer-service sample: + +- The Workflow stays running and accepts user messages via an **Update** (`processUserMessage`), each + returning the agent's reply. +- A **Query** (`getHistory`) returns the running conversation transcript. +- A **Triage** agent hands off to specialist **FAQ** and **SeatBooking** agents; the seat-booking + handoff seeds a shared `RunContext` with a flight number, and the seat tool updates that context. +- The Workflow calls `continueAsNew` once Temporal suggests it, carrying the conversation state across + the boundary to bound history growth. + +## Run + +Start the Temporal dev server: + +``` +temporal server start-dev +``` + +Set your OpenAI key and start the Worker: + +``` +export OPENAI_API_KEY=sk-... +npm run start +``` + +In another shell, start the interactive chat client: + +``` +npm run workflow +``` + +Type messages to chat. Type `history` to print the transcript, or `exit` to quit. + +## Test + +``` +npm test +``` + +The test uses `TestWorkflowEnvironment`, a real Worker, and a `FakeModelProvider`, so it runs without +an API key. It drives two Updates, asserts a handoff occurred, and reads the transcript via Query. diff --git a/openai-agents/customer-service/package.json b/openai-agents/customer-service/package.json new file mode 100644 index 00000000..a3f60286 --- /dev/null +++ b/openai-agents/customer-service/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-customer-service", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/customer-service/src/agents.ts b/openai-agents/customer-service/src/agents.ts new file mode 100644 index 00000000..b15d9265 --- /dev/null +++ b/openai-agents/customer-service/src/agents.ts @@ -0,0 +1,98 @@ +import { Agent, handoff, tool } from '@openai/agents-core'; +import type { RunContext } from '@openai/agents-core'; +import z from 'zod'; + +export interface AirlineAgentContext { + passengerName?: string; + confirmationNumber?: string; + seatNumber?: string; + flightNumber?: string; +} + +const faqLookupTool = tool({ + name: 'faqLookupTool', + description: 'Lookup frequently asked questions.', + parameters: z.object({ question: z.string() }), + execute: async ({ question }) => { + const q = question.toLowerCase(); + if (q.includes('bag') || q.includes('baggage')) { + return 'You are allowed to bring one bag. It must be under 50 pounds and 22 x 14 x 9 inches.'; + } + if (q.includes('seat') || q.includes('plane')) { + return 'There are 120 seats: 22 business and 98 economy. Exit rows are 4 and 16.'; + } + if (q.includes('wifi')) { + return 'We have free wifi on the plane; join Airline-Wifi.'; + } + return "I'm sorry, I don't know the answer to that question."; + }, +}); + +const updateSeatTool = tool({ + name: 'updateSeat', + description: 'Update the seat for a given confirmation number.', + parameters: z.object({ + confirmationNumber: z.string().describe('The confirmation number for the flight.'), + newSeat: z.string().describe('The new seat to update to.'), + }), + execute: async ({ confirmationNumber, newSeat }, runCtx?: RunContext) => { + const ctx = runCtx?.context; + if (ctx) { + ctx.confirmationNumber = confirmationNumber; + ctx.seatNumber = newSeat; + } + return `Updated seat to ${newSeat} for confirmation number ${confirmationNumber}`; + }, +}); + +export function initAgents(): { + startingAgent: Agent; + agentsByName: Map>; +} { + const faqAgent = new Agent({ + name: 'FAQ', + model: 'gpt-4o-mini', + handoffDescription: 'A helpful agent that can answer questions about the airline.', + instructions: + 'You are an FAQ agent. Use the faqLookupTool to answer the question; do not rely on your own ' + + 'knowledge. If you cannot answer, hand off back to the Triage agent.', + tools: [faqLookupTool], + }); + + const seatBookingAgent = new Agent({ + name: 'SeatBooking', + model: 'gpt-4o-mini', + handoffDescription: 'A helpful agent that can update a seat on a flight.', + instructions: + 'You are a seat booking agent. Ask for the confirmation number and desired seat, then use the ' + + 'updateSeat tool. If the question is unrelated, hand off back to the Triage agent.', + tools: [updateSeatTool], + }); + + const triageAgent = new Agent({ + name: 'Triage', + model: 'gpt-4o-mini', + handoffDescription: 'A triage agent that delegates a request to the appropriate agent.', + instructions: + 'You are a triage agent. Delegate FAQ questions to the FAQ agent and seat changes to the ' + 'SeatBooking agent.', + handoffs: [ + faqAgent, + handoff(seatBookingAgent, { + onHandoff: (runCtx: RunContext) => { + runCtx.context.flightNumber = `FLT-${100 + Math.floor(Math.random() * 900)}`; + }, + }), + ], + }); + + faqAgent.handoffs.push(triageAgent); + seatBookingAgent.handoffs.push(triageAgent); + + const agentsByName = new Map>([ + [faqAgent.name, faqAgent], + [seatBookingAgent.name, seatBookingAgent], + [triageAgent.name, triageAgent], + ]); + + return { startingAgent: triageAgent, agentsByName }; +} diff --git a/openai-agents/customer-service/src/client.ts b/openai-agents/customer-service/src/client.ts new file mode 100644 index 00000000..dfd1fdc9 --- /dev/null +++ b/openai-agents/customer-service/src/client.ts @@ -0,0 +1,53 @@ +import * as readline from 'node:readline'; +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { customerServiceWorkflow, processUserMessage, getHistory } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const workflowId = 'openai-agents-customer-service-' + nanoid(); + const handle = await client.workflow.start(customerServiceWorkflow, { + taskQueue: 'openai-agents-customer-service', + workflowId, + }); + console.log(`Started chat workflow ${workflowId}`); + console.log('Type a message and press Enter. Type "history" to print the transcript, "exit" to quit.\n'); + + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const ask = (q: string) => new Promise((resolve) => rl.question(q, resolve)); + + try { + for (;;) { + const line = (await ask('you> ')).trim(); + if (!line) continue; + if (line === 'exit') break; + if (line === 'history') { + const history = await handle.query(getHistory); + console.log(history.join('\n')); + continue; + } + const reply = await handle.executeUpdate(processUserMessage, { args: [line] }); + console.log(`agent> ${reply}\n`); + } + } finally { + rl.close(); + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/customer-service/src/mocha/fake-model.ts b/openai-agents/customer-service/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/customer-service/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/customer-service/src/mocha/workflows.test.ts b/openai-agents/customer-service/src/mocha/workflows.test.ts new file mode 100644 index 00000000..5c456e3d --- /dev/null +++ b/openai-agents/customer-service/src/mocha/workflows.test.ts @@ -0,0 +1,72 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import assert from 'assert'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { customerServiceWorkflow, processUserMessage, getHistory } from '../workflows'; + +describe('openai-agents/customer-service workflow', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('drives two updates, hands off to a specialist, and exposes history via query', async () => { + const taskQueue = 'test-customer-service'; + + // Turn 1: Triage hands off to the FAQ agent, which then answers. + // Turn 2: the current agent is now FAQ, which answers directly. + const provider = new FakeModelProvider([ + toolCallResponse('transfer_to_FAQ', {}), + textResponse('You may bring one carry-on bag.'), + textResponse('We offer free wifi; join Airline-Wifi.'), + ]); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + + await worker.runUntil(async () => { + const handle = await testEnv.client.workflow.start(customerServiceWorkflow, { + workflowId: 'test-customer-service-' + Date.now(), + taskQueue, + }); + + const reply1 = await handle.executeUpdate(processUserMessage, { args: ['What is the baggage allowance?'] }); + assert.strictEqual(reply1, 'You may bring one carry-on bag.'); + + const reply2 = await handle.executeUpdate(processUserMessage, { args: ['Do you have wifi?'] }); + assert.strictEqual(reply2, 'We offer free wifi; join Airline-Wifi.'); + + const history = await handle.query(getHistory); + assert.ok( + history.some((line) => line === 'Handed off from Triage to FAQ'), + `expected a handoff line in history, got:\n${history.join('\n')}`, + ); + assert.ok(history.includes('User: What is the baggage allowance?')); + assert.ok(history.includes('FAQ: You may bring one carry-on bag.')); + assert.ok(history.includes('User: Do you have wifi?')); + assert.ok(history.includes('FAQ: We offer free wifi; join Airline-Wifi.')); + }); + }); +}); diff --git a/openai-agents/customer-service/src/worker.ts b/openai-agents/customer-service/src/worker.ts new file mode 100644 index 00000000..ed28ae5e --- /dev/null +++ b/openai-agents/customer-service/src/worker.ts @@ -0,0 +1,42 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-customer-service', + workflowsPath: require.resolve('./workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/customer-service/src/workflows.ts b/openai-agents/customer-service/src/workflows.ts new file mode 100644 index 00000000..5ed5cdff --- /dev/null +++ b/openai-agents/customer-service/src/workflows.ts @@ -0,0 +1,65 @@ +import type { AgentInputItem } from '@openai/agents-core'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; +import { condition, continueAsNew, defineQuery, defineUpdate, setHandler, workflowInfo } from '@temporalio/workflow'; +import { initAgents, type AirlineAgentContext } from './agents'; + +export const processUserMessage = defineUpdate('processUserMessage'); +export const getHistory = defineQuery('getHistory'); + +export interface CustomerServiceState { + history: string[]; + currentAgentName: string; + context: AirlineAgentContext; + inputItems: AgentInputItem[]; +} + +export async function customerServiceWorkflow(state?: CustomerServiceState): Promise { + const runner = new TemporalOpenAIRunner(); + + const history: string[] = state?.history ?? []; + const context: AirlineAgentContext = state?.context ?? {}; + let inputItems: AgentInputItem[] = state?.inputItems ?? []; + let currentAgentName = state?.currentAgentName ?? initAgents().startingAgent.name; + + setHandler(getHistory, () => history); + + setHandler(processUserMessage, async (message: string): Promise => { + history.push(`User: ${message}`); + inputItems = [...inputItems, { role: 'user', content: message }]; + + const { agentsByName } = initAgents(); + const currentAgent = agentsByName.get(currentAgentName); + if (!currentAgent) { + throw new Error(`Unknown agent: ${currentAgentName}`); + } + + const result = await runner.run(currentAgent, inputItems, { context }); + + for (const item of result.newItems) { + if (item.type === 'message_output_item') { + const text = item.rawItem.content.map((c) => ('text' in c ? c.text : '')).join(''); + history.push(`${item.agent.name}: ${text}`); + } else if (item.type === 'handoff_output_item') { + history.push(`Handed off from ${item.sourceAgent.name} to ${item.targetAgent.name}`); + } else if (item.type === 'tool_call_item') { + history.push(`${item.agent.name}: calling a tool`); + } else if (item.type === 'tool_call_output_item') { + history.push(`${item.agent.name}: tool output: ${String(item.output)}`); + } + } + + inputItems = result.history; + currentAgentName = result.lastAgent?.name ?? currentAgentName; + + return result.finalOutput ?? ''; + }); + + await condition(() => workflowInfo().continueAsNewSuggested); + + await continueAsNew({ + history, + currentAgentName, + context, + inputItems, + }); +} diff --git a/openai-agents/customer-service/tsconfig.json b/openai-agents/customer-service/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/customer-service/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/handoffs/.eslintignore b/openai-agents/handoffs/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/handoffs/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/handoffs/.eslintrc.js b/openai-agents/handoffs/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/handoffs/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/handoffs/.gitignore b/openai-agents/handoffs/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/handoffs/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/handoffs/.npmrc b/openai-agents/handoffs/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/handoffs/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/handoffs/.nvmrc b/openai-agents/handoffs/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/handoffs/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/handoffs/.post-create b/openai-agents/handoffs/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/handoffs/.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/openai-agents/handoffs/.prettierignore b/openai-agents/handoffs/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/handoffs/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/handoffs/.prettierrc b/openai-agents/handoffs/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/handoffs/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/handoffs/README.md b/openai-agents/handoffs/README.md new file mode 100644 index 00000000..cb8abd7a --- /dev/null +++ b/openai-agents/handoffs/README.md @@ -0,0 +1,37 @@ +# OpenAI Agents: Handoffs + +Demonstrates agent handoffs with the Temporal OpenAI Agents integration. A triage agent routes +each request to one of two specialist agents (billing, support), running durably as a Temporal +Workflow. + +Scenarios (`src/workflows.ts`): + +- **agent-handoffs** — handoffs declared as a plain `Agent[]`. +- **handoff-function** — handoffs declared with the `handoff()` form. +- **handoff-with-filter** — a per-handoff `inputFilter` that strips the current turn's items + (the handoff `function_call_result`) before the specialist runs. + +## Run + +```bash +npm install +npm run build + +# In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): +OPENAI_API_KEY=sk-... npm start + +# In another terminal, start a scenario: +npm run workflow agent-handoffs +npm run workflow handoff-function +npm run workflow handoff-with-filter +``` + +## Test + +```bash +npm test +``` + +Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no +`OPENAI_API_KEY` is required. They assert that triage transfers to the correct specialist and that +the input filter removes the handoff tool result from the specialist's model input. diff --git a/openai-agents/handoffs/package.json b/openai-agents/handoffs/package.json new file mode 100644 index 00000000..750349ad --- /dev/null +++ b/openai-agents/handoffs/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-handoffs", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/handoffs/src/activities.ts b/openai-agents/handoffs/src/activities.ts new file mode 100644 index 00000000..7b8577b1 --- /dev/null +++ b/openai-agents/handoffs/src/activities.ts @@ -0,0 +1,3 @@ +export async function lookupAccount(input: { accountId: string }): Promise { + return JSON.stringify({ accountId: input.accountId, status: 'active', plan: 'pro' }); +} diff --git a/openai-agents/handoffs/src/client.ts b/openai-agents/handoffs/src/client.ts new file mode 100644 index 00000000..965db30c --- /dev/null +++ b/openai-agents/handoffs/src/client.ts @@ -0,0 +1,59 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { agentHandoffs, handoffFunction, handoffWithFilter } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'agent-handoffs'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-handoffs'; + const workflowId = 'openai-agents-' + nanoid(); + + let handle; + switch (scenario) { + case 'agent-handoffs': + handle = await client.workflow.start(agentHandoffs, { + taskQueue, + workflowId, + args: ['I have a billing question about my invoice.'], + }); + break; + case 'handoff-function': + handle = await client.workflow.start(handoffFunction, { + taskQueue, + workflowId, + args: ['I need help with a support issue.'], + }); + break; + case 'handoff-with-filter': + handle = await client.workflow.start(handoffWithFilter, { + taskQueue, + workflowId, + args: ['I have a billing question about my invoice.'], + }); + break; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/handoffs/src/mocha/fake-model.ts b/openai-agents/handoffs/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/handoffs/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/handoffs/src/mocha/workflows.test.ts b/openai-agents/handoffs/src/mocha/workflows.test.ts new file mode 100644 index 00000000..49b692d5 --- /dev/null +++ b/openai-agents/handoffs/src/mocha/workflows.test.ts @@ -0,0 +1,120 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import * as activities from '../activities'; +import assert from 'assert'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { agentHandoffs, handoffFunction, handoffWithFilter } from '../workflows'; + +describe('openai-agents/handoffs 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, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities, + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('agentHandoffs: triage routes to billing specialist via Agent[] handoffs', async () => { + const taskQueue = 'test-agent-handoffs'; + const { worker } = await makeWorker(taskQueue, [ + toolCallResponse('transfer_to_billing', {}), + textResponse('Your invoice has been processed.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(agentHandoffs, { + args: ['I have a billing question about my invoice.'], + workflowId: 'test-agent-handoffs-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Your invoice has been processed.'); + }); + + it('agentHandoffs: triage routes to support specialist via Agent[] handoffs', async () => { + const taskQueue = 'test-agent-handoffs-support'; + const { worker } = await makeWorker(taskQueue, [ + toolCallResponse('transfer_to_support', {}), + textResponse('Your support ticket has been filed.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(agentHandoffs, { + args: ['I need help with a support issue.'], + workflowId: 'test-agent-handoffs-support-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Your support ticket has been filed.'); + }); + + it('handoffFunction: triage routes to billing specialist via handoff() function', async () => { + const taskQueue = 'test-handoff-function'; + const { worker } = await makeWorker(taskQueue, [ + toolCallResponse('transfer_to_billing', {}), + textResponse('Billing handled via handoff function.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(handoffFunction, { + args: ['I have a billing question.'], + workflowId: 'test-handoff-function-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Billing handled via handoff function.'); + }); + + it('handoffWithFilter: filter drops newItems so specialist sees no handoff function_call_result', async () => { + const taskQueue = 'test-handoff-with-filter'; + const { worker, provider } = await makeWorker(taskQueue, [ + toolCallResponse('transfer_to_billing', {}), + textResponse('Billing answer after filtered handoff.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(handoffWithFilter, { + args: ['I have a billing question.'], + workflowId: 'test-handoff-with-filter-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Billing answer after filtered handoff.'); + + assert.strictEqual(provider.model.requests.length, 2, 'expected exactly two model requests'); + + const specialistRequest = provider.model.requests[1]!; + const inputItems = Array.isArray(specialistRequest.input) ? specialistRequest.input : []; + const handoffResultItems = inputItems.filter( + (item) => item.type === 'function_call_result' && (item as { name?: string }).name === 'transfer_to_billing', + ); + assert.strictEqual( + handoffResultItems.length, + 0, + 'filter should have removed the transfer_to_billing function_call_result from the specialist input', + ); + }); +}); diff --git a/openai-agents/handoffs/src/worker.ts b/openai-agents/handoffs/src/worker.ts new file mode 100644 index 00000000..dc934ae2 --- /dev/null +++ b/openai-agents/handoffs/src/worker.ts @@ -0,0 +1,44 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import * as activities from './activities'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-handoffs', + workflowsPath: require.resolve('./workflows'), + activities, + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/handoffs/src/workflows.ts b/openai-agents/handoffs/src/workflows.ts new file mode 100644 index 00000000..84763281 --- /dev/null +++ b/openai-agents/handoffs/src/workflows.ts @@ -0,0 +1,79 @@ +import { Agent, handoff } from '@openai/agents-core'; +import type { HandoffInputData } from '@openai/agents-core'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; + +export async function agentHandoffs(prompt: string): Promise { + const billingAgent = new Agent({ + name: 'billing', + instructions: 'You are a billing specialist. Help with billing questions.', + }); + + const supportAgent = new Agent({ + name: 'support', + instructions: 'You are a support specialist. Help with support questions.', + }); + + const triageAgent = new Agent({ + name: 'triage', + instructions: 'You are a triage agent. Route billing questions to billing and support questions to support.', + handoffs: [billingAgent, supportAgent], + }); + + const runner = new TemporalOpenAIRunner(); + const result = await runner.run(triageAgent, prompt); + return result.finalOutput ?? ''; +} + +export async function handoffFunction(prompt: string): Promise { + const billingAgent = new Agent({ + name: 'billing', + instructions: 'You are a billing specialist. Help with billing questions.', + }); + + const supportAgent = new Agent({ + name: 'support', + instructions: 'You are a support specialist. Help with support questions.', + }); + + const triageAgent = new Agent({ + name: 'triage', + instructions: 'You are a triage agent. Route billing questions to billing and support questions to support.', + handoffs: [handoff(billingAgent), handoff(supportAgent)], + }); + + const runner = new TemporalOpenAIRunner(); + const result = await runner.run(triageAgent, prompt); + return result.finalOutput ?? ''; +} + +function dropNewItems(data: HandoffInputData): HandoffInputData { + return { + ...data, + newItems: [], + }; +} + +export async function handoffWithFilter(prompt: string): Promise { + const billingAgent = new Agent({ + name: 'billing', + instructions: 'You are a billing specialist. Help with billing questions.', + }); + + const supportAgent = new Agent({ + name: 'support', + instructions: 'You are a support specialist. Help with support questions.', + }); + + const triageAgent = new Agent({ + name: 'triage', + instructions: 'You are a triage agent. Route billing questions to billing and support questions to support.', + handoffs: [ + handoff(billingAgent, { inputFilter: dropNewItems }), + handoff(supportAgent, { inputFilter: dropNewItems }), + ], + }); + + const runner = new TemporalOpenAIRunner(); + const result = await runner.run(triageAgent, prompt); + return result.finalOutput ?? ''; +} diff --git a/openai-agents/handoffs/tsconfig.json b/openai-agents/handoffs/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/handoffs/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/hosted-mcp/.eslintignore b/openai-agents/hosted-mcp/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/hosted-mcp/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.eslintrc.js b/openai-agents/hosted-mcp/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/hosted-mcp/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/hosted-mcp/.gitignore b/openai-agents/hosted-mcp/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/hosted-mcp/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.npmrc b/openai-agents/hosted-mcp/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/hosted-mcp/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.nvmrc b/openai-agents/hosted-mcp/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/hosted-mcp/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/hosted-mcp/.post-create b/openai-agents/hosted-mcp/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/hosted-mcp/.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/openai-agents/hosted-mcp/.prettierignore b/openai-agents/hosted-mcp/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/hosted-mcp/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.prettierrc b/openai-agents/hosted-mcp/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/hosted-mcp/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/hosted-mcp/README.md b/openai-agents/hosted-mcp/README.md new file mode 100644 index 00000000..559d87d5 --- /dev/null +++ b/openai-agents/hosted-mcp/README.md @@ -0,0 +1,56 @@ +# openai-agents/hosted-mcp + +Demonstrates using a `HostedMCPTool` from the OpenAI Agents SDK inside a Temporal Workflow with the +`@temporalio/openai-agents` plugin. A hosted MCP tool is executed server-side by the model: the +model itself calls the remote hosted MCP server during a model request, so there is no local MCP +server to run. + +Two workflows are included: + +- **simple** — an agent carrying a `hostedMcpTool` with `requireApproval: 'never'`. The model is + free to call the hosted MCP server without any approval round trip. +- **approval** — an agent carrying a `hostedMcpTool` with `requireApproval: 'always'`. Approval is + driven by a Temporal Signal (`approvalDecision`, carrying a boolean). The Workflow waits for the + Signal before running the agent, and the tool's `onApproval` callback resolves approve/reject from + the signaled decision. + +The runnable sample points at the public [GitMCP](https://gitmcp.io) hosted MCP server +(`serverLabel: 'gitmcp'`, `serverUrl: 'https://gitmcp.io/openai/codex'`). + +## Live-only requirement + +Running the worker and client makes real model calls against OpenAI, and the model in turn calls the +public GitMCP server. This sample therefore requires: + +- A real `OPENAI_API_KEY` environment variable. +- Outbound network access to OpenAI and to `https://gitmcp.io`. + +The included tests do not exercise any of this. They use a fake model provider, make no network +calls, and assert wiring/completion only (that the agent carries the hosted MCP tool and the +Workflow runs to completion). + +## Run + +1. Start a Temporal dev server: + + ```sh + temporal server start-dev + ``` + +2. In another terminal, start the worker: + + ```sh + export OPENAI_API_KEY=sk-... + npm run start.watch + ``` + +3. In a third terminal, run a workflow: + + ```sh + export OPENAI_API_KEY=sk-... + npm run workflow simple + npm run workflow approval + ``` + +The `approval` client starts the Workflow and then sends the `approvalDecision` Signal with `true` +so the demo completes. diff --git a/openai-agents/hosted-mcp/package.json b/openai-agents/hosted-mcp/package.json new file mode 100644 index 00000000..5fe4612c --- /dev/null +++ b/openai-agents/hosted-mcp/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-hosted-mcp", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/hosted-mcp/src/client.ts b/openai-agents/hosted-mcp/src/client.ts new file mode 100644 index 00000000..38dc2256 --- /dev/null +++ b/openai-agents/hosted-mcp/src/client.ts @@ -0,0 +1,53 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { simple, approval, approvalDecision } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'simple'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-hosted-mcp'; + const workflowId = 'openai-agents-' + nanoid(); + + let handle; + switch (scenario) { + case 'simple': + handle = await client.workflow.start(simple, { + taskQueue, + workflowId, + args: ['What does the openai/codex project do? Use the hosted MCP tools to find out.'], + }); + break; + case 'approval': + handle = await client.workflow.start(approval, { + taskQueue, + workflowId, + args: ['What does the openai/codex project do? Use the hosted MCP tools to find out.'], + }); + await handle.signal(approvalDecision, true); + break; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/hosted-mcp/src/mocha/fake-model.ts b/openai-agents/hosted-mcp/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/hosted-mcp/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/hosted-mcp/src/mocha/workflows.test.ts b/openai-agents/hosted-mcp/src/mocha/workflows.test.ts new file mode 100644 index 00000000..fdc2bdb9 --- /dev/null +++ b/openai-agents/hosted-mcp/src/mocha/workflows.test.ts @@ -0,0 +1,83 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import assert from 'assert'; +import { FakeModelProvider, textResponse } from './fake-model'; +import { simple, approval, approvalDecision } from '../workflows'; + +describe('openai-agents/hosted-mcp 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, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('simple: agent carrying the hosted MCP tool runs and returns model text', async () => { + const taskQueue = 'test-hosted-mcp-simple'; + const { worker, provider } = await makeWorker(taskQueue, [ + textResponse('The openai/codex project is a coding agent.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(simple, { + args: ['What does openai/codex do?'], + workflowId: 'test-hosted-mcp-simple-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'The openai/codex project is a coding agent.'); + + // The hosted MCP tool serializes as a hosted_tool named 'hosted_mcp'; asserting it + // is present in the first request's tool list proves the agent was wired with it. + const hostedTools = provider.model.requests[0]?.tools.filter((t) => t.name === 'hosted_mcp'); + assert.strictEqual(hostedTools?.length, 1, 'request should carry exactly one hosted_mcp tool'); + }); + + it('approval: workflow consumes the approval signal, runs, and returns model text', async () => { + const taskQueue = 'test-hosted-mcp-approval'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('Approved and answered.')]); + + const result = await worker.runUntil(async () => { + const handle = await testEnv.client.workflow.start(approval, { + args: ['What does openai/codex do?'], + workflowId: 'test-hosted-mcp-approval-' + Date.now(), + taskQueue, + }); + // The workflow gates the run on the approval signal; without this the run never + // starts, so a returned result proves the signal was received and consumed. + await handle.signal(approvalDecision, true); + return handle.result(); + }); + + assert.strictEqual(result, 'Approved and answered.'); + + const hostedTools = provider.model.requests[0]?.tools.filter((t) => t.name === 'hosted_mcp'); + assert.strictEqual(hostedTools?.length, 1, 'request should carry exactly one hosted_mcp tool'); + }); +}); diff --git a/openai-agents/hosted-mcp/src/worker.ts b/openai-agents/hosted-mcp/src/worker.ts new file mode 100644 index 00000000..d936b1b3 --- /dev/null +++ b/openai-agents/hosted-mcp/src/worker.ts @@ -0,0 +1,42 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-hosted-mcp', + workflowsPath: require.resolve('./workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/hosted-mcp/src/workflows.ts b/openai-agents/hosted-mcp/src/workflows.ts new file mode 100644 index 00000000..db491433 --- /dev/null +++ b/openai-agents/hosted-mcp/src/workflows.ts @@ -0,0 +1,53 @@ +import { Agent, RunContext, RunToolApprovalItem } from '@openai/agents-core'; +import { hostedMcpTool } from '@openai/agents-core'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; +import { condition, defineSignal, setHandler } from '@temporalio/workflow'; + +const HOSTED_MCP_SERVER_LABEL = 'gitmcp'; +const HOSTED_MCP_SERVER_URL = 'https://gitmcp.io/openai/codex'; + +export async function simple(prompt: string): Promise { + const agent = new Agent({ + name: 'HostedMcpSimpleAgent', + instructions: 'You are a helpful assistant. Use the hosted MCP tools when they are relevant.', + tools: [ + hostedMcpTool({ + serverLabel: HOSTED_MCP_SERVER_LABEL, + serverUrl: HOSTED_MCP_SERVER_URL, + requireApproval: 'never', + }), + ], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export const approvalDecision = defineSignal<[boolean]>('approvalDecision'); + +export async function approval(prompt: string): Promise { + let decision: boolean | undefined; + setHandler(approvalDecision, (approve: boolean) => { + decision = approve; + }); + + await condition(() => decision !== undefined); + + const agent = new Agent({ + name: 'HostedMcpApprovalAgent', + instructions: 'You are a helpful assistant. Use the hosted MCP tools when they are relevant.', + tools: [ + hostedMcpTool({ + serverLabel: HOSTED_MCP_SERVER_LABEL, + serverUrl: HOSTED_MCP_SERVER_URL, + requireApproval: 'always', + onApproval: async (_context: RunContext, _item: RunToolApprovalItem) => { + await condition(() => decision !== undefined); + return { approve: decision === true }; + }, + }), + ], + }); + + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} diff --git a/openai-agents/hosted-mcp/tsconfig.json b/openai-agents/hosted-mcp/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/hosted-mcp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/human-approval/.eslintignore b/openai-agents/human-approval/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/human-approval/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/human-approval/.eslintrc.js b/openai-agents/human-approval/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/human-approval/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/human-approval/.gitignore b/openai-agents/human-approval/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/human-approval/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/human-approval/.npmrc b/openai-agents/human-approval/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/human-approval/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/human-approval/.nvmrc b/openai-agents/human-approval/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/human-approval/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/human-approval/.post-create b/openai-agents/human-approval/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/human-approval/.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/openai-agents/human-approval/.prettierignore b/openai-agents/human-approval/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/human-approval/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/human-approval/.prettierrc b/openai-agents/human-approval/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/human-approval/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/human-approval/README.md b/openai-agents/human-approval/README.md new file mode 100644 index 00000000..67c4c09a --- /dev/null +++ b/openai-agents/human-approval/README.md @@ -0,0 +1,37 @@ +# OpenAI Agents: Human Approval + +Demonstrates a human-in-the-loop approval flow with the Temporal OpenAI Agents integration. A tool +marked `needsApproval` pauses the agent run with an interruption; the Workflow waits for an +`approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. + +Flow (`src/workflows.ts`): + +1. The agent requests a `dangerousAction` tool call; because the tool sets `needsApproval`, the run + returns with `result.interruptions`. +2. The Workflow waits for the `approve` Signal. +3. On approval it continues as new with `resumeFromRunState: result.state.toString()`. +4. The resumed run rehydrates with `RunState.fromString`, calls `state.approve` for each + interruption, and re-runs to completion. + +## Run + +```bash +npm install +npm run build + +# In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): +OPENAI_API_KEY=sk-... npm start + +# In another terminal, start the workflow (the client sends the approval Signal): +npm run workflow +``` + +## Test + +```bash +npm test +``` + +The test runs a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no +`OPENAI_API_KEY` is required. It scripts a `dangerousAction` tool call to produce an interruption, +drives the `approve` Signal, and asserts the resumed run completes. diff --git a/openai-agents/human-approval/package.json b/openai-agents/human-approval/package.json new file mode 100644 index 00000000..2f61848a --- /dev/null +++ b/openai-agents/human-approval/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-human-approval", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/human-approval/src/activities.ts b/openai-agents/human-approval/src/activities.ts new file mode 100644 index 00000000..5c9bbd88 --- /dev/null +++ b/openai-agents/human-approval/src/activities.ts @@ -0,0 +1 @@ +export async function placeholder(): Promise {} diff --git a/openai-agents/human-approval/src/client.ts b/openai-agents/human-approval/src/client.ts new file mode 100644 index 00000000..4b8a5102 --- /dev/null +++ b/openai-agents/human-approval/src/client.ts @@ -0,0 +1,40 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { approvalWorkflow } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'human-approval'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-human-approval'; + const workflowId = 'openai-agents-' + nanoid(); + + const handle = await client.workflow.start(approvalWorkflow, { + taskQueue, + workflowId, + args: [{}], + }); + + console.log(`Started workflow ${handle.workflowId}`); + console.log('Sending approval signal...'); + await handle.signal('approve'); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/human-approval/src/mocha/fake-model.ts b/openai-agents/human-approval/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/human-approval/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/human-approval/src/mocha/workflows.test.ts b/openai-agents/human-approval/src/mocha/workflows.test.ts new file mode 100644 index 00000000..4b92806b --- /dev/null +++ b/openai-agents/human-approval/src/mocha/workflows.test.ts @@ -0,0 +1,68 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import * as activities from '../activities'; +import assert from 'assert'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { approvalWorkflow } from '../workflows'; + +describe('openai-agents/human-approval workflow scenarios', function () { + this.timeout(60_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + async function makeWorker(taskQueue: string, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities, + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('approvalWorkflow: interruption produced, approval signal triggers continueAsNew, resumed run completes', async () => { + const taskQueue = 'test-human-approval'; + + // Two model turns total across both workflow runs: + // Turn 1 (original run): model requests dangerousAction → needsApproval interruption + // Turn 2 (resumed run after continueAsNew): tool executed, result returned to model → final text + const { worker } = await makeWorker(taskQueue, [ + toolCallResponse('dangerousAction', { reason: 'demo' }), + textResponse('Action completed successfully.'), + ]); + + const result = await worker.runUntil(async () => { + const handle = await testEnv.client.workflow.start(approvalWorkflow, { + args: [{}], + workflowId: 'test-human-approval-' + Date.now(), + taskQueue, + }); + + await handle.signal('approve'); + return handle.result(); + }); + + assert.strictEqual(result, 'Action completed successfully.'); + }); +}); diff --git a/openai-agents/human-approval/src/worker.ts b/openai-agents/human-approval/src/worker.ts new file mode 100644 index 00000000..21f5d690 --- /dev/null +++ b/openai-agents/human-approval/src/worker.ts @@ -0,0 +1,44 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import * as activities from './activities'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-human-approval', + workflowsPath: require.resolve('./workflows'), + activities, + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/human-approval/src/workflows.ts b/openai-agents/human-approval/src/workflows.ts new file mode 100644 index 00000000..1bee9b39 --- /dev/null +++ b/openai-agents/human-approval/src/workflows.ts @@ -0,0 +1,59 @@ +import { Agent, RunState, tool } from '@openai/agents-core'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; +import { condition, continueAsNew, defineSignal, setHandler } from '@temporalio/workflow'; + +export const approveSignal = defineSignal('approve'); + +export interface ApprovalInput { + resumeFromRunState?: string; +} + +export async function approvalWorkflow(input: ApprovalInput = {}): Promise { + const action = tool({ + name: 'dangerousAction', + description: 'Performs a dangerous action that requires human approval before execution.', + parameters: { + type: 'object', + properties: { + reason: { type: 'string', description: 'The reason for performing the dangerous action.' }, + }, + required: ['reason'], + additionalProperties: false, + } as const, + needsApproval: true, + execute: async (args) => `did: ${(args as { reason: string }).reason}`, + }); + + const agent = new Agent({ + name: 'Approver', + instructions: "You carry out the user's request using the dangerousAction tool.", + tools: [action], + modelSettings: { toolChoice: 'required' }, + }); + + const runner = new TemporalOpenAIRunner(); + + if (input.resumeFromRunState !== undefined) { + const state = await RunState.fromString(agent, input.resumeFromRunState); + for (const interruption of state.getInterruptions()) { + state.approve(interruption); + } + const resumed = await runner.run(agent, state); + return resumed.finalOutput ?? ''; + } + + let approved = false; + setHandler(approveSignal, () => { + approved = true; + }); + + const result = await runner.run(agent, 'Delete the old backup files.'); + + if (result.interruptions.length === 0) { + return result.finalOutput ?? ''; + } + + await condition(() => approved); + await continueAsNew({ resumeFromRunState: result.state.toString() }); + throw new Error('unreachable'); +} diff --git a/openai-agents/human-approval/tsconfig.json b/openai-agents/human-approval/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/human-approval/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/mcp/.eslintignore b/openai-agents/mcp/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/mcp/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/mcp/.eslintrc.js b/openai-agents/mcp/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/mcp/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/mcp/.gitignore b/openai-agents/mcp/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/mcp/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/mcp/.npmrc b/openai-agents/mcp/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/mcp/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/mcp/.nvmrc b/openai-agents/mcp/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/mcp/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/mcp/.post-create b/openai-agents/mcp/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/mcp/.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/openai-agents/mcp/.prettierignore b/openai-agents/mcp/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/mcp/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/mcp/.prettierrc b/openai-agents/mcp/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/mcp/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/mcp/README.md b/openai-agents/mcp/README.md new file mode 100644 index 00000000..00a7ef0c --- /dev/null +++ b/openai-agents/mcp/README.md @@ -0,0 +1,47 @@ +# openai-agents/mcp + +Demonstrates stateless and stateful MCP (Model Context Protocol) servers used by a Temporal-backed +OpenAI agent through the `@temporalio/openai-agents` plugin. Every MCP server in this sample is +bundled and runs locally — either in-process or as a localhost/subprocess server the sample starts +itself — so no external MCP service is required. + +Five scenarios are included: + +- `filesystem` — stateless MCP over stdio; a bundled stdio server (`src/servers/filesystem-server.ts`) + exposes `listFiles`/`readFile` over `src/servers/sample-files/`. +- `streamable-http` — stateless MCP over a localhost Streamable-HTTP server (`src/servers/tools-server.ts`) + with `add`/`getWeather`/`getSecret` tools. +- `sse` — stateless MCP over a localhost SSE server (`src/servers/sse-server.ts`) with the same tools. +- `prompt-server` — stateless MCP server (`src/servers/prompt-server.ts`) exposing a `summarize` + prompt; the workflow fetches the prompt and uses it as the agent's instructions. +- `stateful-memory` — a `StatefulMCPServerProvider` notes server (`src/servers/notes-server.ts`) with + `saveNote`/`listNotes`/`readNote`; the workflow calls `connect()`/`cleanup()` to keep server state + for the run. + +The worker requires `OPENAI_API_KEY`. The included tests use a fake model provider, make zero external +network calls, and drive the bundled MCP servers locally to assert that tool results flow back. + +## Run + +1. Start a Temporal dev server: + + ```sh + temporal server start-dev + ``` + +2. In another terminal, start the worker: + + ```sh + export OPENAI_API_KEY=sk-... + npm run start.watch + ``` + +3. In a third terminal, run a workflow: + + ```sh + npm run workflow filesystem + npm run workflow streamable-http + npm run workflow sse + npm run workflow prompt-server + npm run workflow stateful-memory + ``` diff --git a/openai-agents/mcp/package.json b/openai-agents/mcp/package.json new file mode 100644 index 00000000..b6a7fb1e --- /dev/null +++ b/openai-agents/mcp/package.json @@ -0,0 +1,55 @@ +{ + "name": "temporal-openai-agents-mcp", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "@modelcontextprotocol/sdk": "^1.29.0", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/mcp/src/activities.ts b/openai-agents/mcp/src/activities.ts new file mode 100644 index 00000000..5b9fdc39 --- /dev/null +++ b/openai-agents/mcp/src/activities.ts @@ -0,0 +1,24 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +export function makeActivities(promptServerUrl: string) { + return { + async fetchSummarizePrompt(): Promise { + const client = new Client({ name: 'prompt-activity-client', version: '1.0.0' }); + const transport = new StreamableHTTPClientTransport(new URL(promptServerUrl)); + await client.connect(transport); + try { + const result = await client.getPrompt({ name: 'summarize' }); + const textContent = result.messages.find((m) => m.content.type === 'text'); + if (!textContent || textContent.content.type !== 'text') { + throw new Error('summarize prompt returned no text content'); + } + return textContent.content.text; + } finally { + await client.close(); + } + }, + }; +} + +export type Activities = ReturnType; diff --git a/openai-agents/mcp/src/client.ts b/openai-agents/mcp/src/client.ts new file mode 100644 index 00000000..2a27298b --- /dev/null +++ b/openai-agents/mcp/src/client.ts @@ -0,0 +1,73 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { filesystem, streamableHttp, sse, promptServer, statefulMemory } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'filesystem'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-mcp'; + const workflowId = 'openai-agents-mcp-' + nanoid(); + + let handle; + switch (scenario) { + case 'filesystem': + handle = await client.workflow.start(filesystem, { + taskQueue, + workflowId, + args: ['List the files in the sample-files directory.'], + }); + break; + case 'streamable-http': + handle = await client.workflow.start(streamableHttp, { + taskQueue, + workflowId, + args: ['What is 17 plus 25?'], + }); + break; + case 'sse': + handle = await client.workflow.start(sse, { + taskQueue, + workflowId, + args: ['What is the weather in London?'], + }); + break; + case 'prompt-server': + handle = await client.workflow.start(promptServer, { + taskQueue, + workflowId, + args: ['Temporal is a durable execution platform for building reliable distributed systems.'], + }); + break; + case 'stateful-memory': + handle = await client.workflow.start(statefulMemory, { + taskQueue, + workflowId, + args: ['Save a note titled "greeting" with body "Hello, world!". Then list all notes.'], + }); + break; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/mcp/src/mocha/fake-model.ts b/openai-agents/mcp/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/mcp/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/mcp/src/mocha/workflows.test.ts b/openai-agents/mcp/src/mocha/workflows.test.ts new file mode 100644 index 00000000..aad518d7 --- /dev/null +++ b/openai-agents/mcp/src/mocha/workflows.test.ts @@ -0,0 +1,269 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin, StatelessMCPServerProvider, StatefulMCPServerProvider } from '@temporalio/openai-agents'; +import { MCPServerStdio, MCPServerStreamableHttp, MCPServerSSE } from '@openai/agents-core'; +import assert from 'assert'; +import * as path from 'path'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { filesystem, streamableHttp, sse, promptServer, statefulMemory } from '../workflows'; +import { makeActivities } from '../activities'; +import { startToolsHttpServer } from '../servers/tools-server'; +import { startToolsSseServer } from '../servers/sse-server'; +import { startPromptHttpServer, SUMMARIZE_PROMPT_TEXT } from '../servers/prompt-server'; +import { createNotesServer } from '../servers/notes-server'; + +describe('openai-agents/mcp workflow scenarios', function () { + this.timeout(60_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + const webpackConfigHook = (config: object & { resolve?: { conditionNames?: string[] } }) => ({ + ...config, + resolve: { + ...(config.resolve ?? {}), + conditionNames: ['require', 'browser', 'default'], + }, + }); + + it('filesystem: MCP stdio tool result flows back to the model', async () => { + const taskQueue = 'test-mcp-filesystem'; + const filesystemServerPath = path.resolve(__dirname, '..', 'servers', 'filesystem-server.ts'); + const provider = new FakeModelProvider([ + toolCallResponse('listFiles', {}), + textResponse('The files are: hello.txt, notes.txt'), + ]); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: provider, + mcpServerProviders: [ + new StatelessMCPServerProvider( + 'filesystem', + () => + new MCPServerStdio({ + command: 'npx', + args: ['ts-node', filesystemServerPath], + name: 'filesystem', + }), + ), + ], + }), + ], + bundlerOptions: { webpackConfigHook }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(filesystem, { + args: ['List the files.'], + workflowId: 'test-mcp-filesystem-' + Date.now(), + taskQueue, + }), + ); + + assert.strictEqual(result, 'The files are: hello.txt, notes.txt'); + + // The listFiles tool result must have been sent back to the model. + const toolResults = provider.model.requests + .flatMap((req) => (Array.isArray(req.input) ? req.input : [])) + .filter((item) => item.type === 'function_call_result' && (item as { name: string }).name === 'listFiles'); + assert.strictEqual(toolResults.length, 1, 'listFiles tool result should reach the model exactly once'); + }); + + it('streamableHttp: MCP HTTP tool result flows back to the model', async () => { + const taskQueue = 'test-mcp-streamable-http'; + const toolsHttp = await startToolsHttpServer(); + + try { + const provider = new FakeModelProvider([ + toolCallResponse('add', { a: 17, b: 25 }), + textResponse('The answer is 42.'), + ]); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: provider, + mcpServerProviders: [ + new StatelessMCPServerProvider( + 'streamableHttp', + () => new MCPServerStreamableHttp({ url: toolsHttp.url, name: 'streamableHttp' }), + ), + ], + }), + ], + bundlerOptions: { webpackConfigHook }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(streamableHttp, { + args: ['What is 17 plus 25?'], + workflowId: 'test-mcp-streamable-http-' + Date.now(), + taskQueue, + }), + ); + + assert.strictEqual(result, 'The answer is 42.'); + + // The add tool must have returned "42" to the model. + const toolResults = provider.model.requests + .flatMap((req) => (Array.isArray(req.input) ? req.input : [])) + .filter((item) => item.type === 'function_call_result' && (item as { name: string }).name === 'add'); + assert.strictEqual(toolResults.length, 1, 'add tool result should reach the model exactly once'); + assert.deepStrictEqual((toolResults[0] as { output: unknown }).output, [{ type: 'input_text', text: '42' }]); + } finally { + await toolsHttp.close(); + } + }); + + it('sse: MCP SSE tool result flows back to the model', async () => { + const taskQueue = 'test-mcp-sse'; + const toolsSse = await startToolsSseServer(); + + try { + const provider = new FakeModelProvider([ + toolCallResponse('getSecret', {}), + textResponse('The secret is swordfish.'), + ]); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: provider, + mcpServerProviders: [ + new StatelessMCPServerProvider('sse', () => new MCPServerSSE({ url: toolsSse.url, name: 'sse' })), + ], + }), + ], + bundlerOptions: { webpackConfigHook }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(sse, { + args: ['What is the secret?'], + workflowId: 'test-mcp-sse-' + Date.now(), + taskQueue, + }), + ); + + assert.strictEqual(result, 'The secret is swordfish.'); + + // The getSecret tool must have returned "swordfish" to the model. + const toolResults = provider.model.requests + .flatMap((req) => (Array.isArray(req.input) ? req.input : [])) + .filter((item) => item.type === 'function_call_result' && (item as { name: string }).name === 'getSecret'); + assert.strictEqual(toolResults.length, 1, 'getSecret tool result should reach the model exactly once'); + assert.deepStrictEqual((toolResults[0] as { output: unknown }).output, [ + { type: 'input_text', text: 'swordfish' }, + ]); + } finally { + await toolsSse.close(); + } + }); + + it('promptServer: fetched prompt text becomes the agent instructions', async () => { + const taskQueue = 'test-mcp-prompt-server'; + const promptHttp = await startPromptHttpServer(); + + try { + const provider = new FakeModelProvider([textResponse('Temporal is a durable execution platform.')]); + + const activities = makeActivities(promptHttp.url); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities, + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { webpackConfigHook }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(promptServer, { + args: ['Temporal is a durable execution platform for building reliable distributed systems.'], + workflowId: 'test-mcp-prompt-server-' + Date.now(), + taskQueue, + }), + ); + + assert.strictEqual(result, 'Temporal is a durable execution platform.'); + + // The summarize prompt text must have been set as the agent's system instructions. + const systemInstructions = provider.model.requests[0]?.systemInstructions; + assert.strictEqual( + systemInstructions, + SUMMARIZE_PROMPT_TEXT, + 'fetched prompt text should become agent instructions (systemInstructions)', + ); + } finally { + await promptHttp.close(); + } + }); + + it('statefulMemory: state persists across multiple tool calls within the run', async () => { + const taskQueue = 'test-mcp-stateful-memory'; + const provider = new FakeModelProvider([ + // First turn: save a note + toolCallResponse('saveNote', { title: 'greeting', body: 'Hello, world!' }), + // Second turn: read the note back + toolCallResponse('readNote', { title: 'greeting' }), + // Final turn: text response + textResponse('I saved and read back: Hello, world!'), + ]); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: provider, + mcpServerProviders: [ + new StatefulMCPServerProvider('memory', () => createNotesServer(), testEnv.nativeConnection), + ], + }), + ], + bundlerOptions: { webpackConfigHook }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(statefulMemory, { + args: ['Save a note titled "greeting" with body "Hello, world!" then read it back.'], + workflowId: 'test-mcp-stateful-memory-' + Date.now(), + taskQueue, + }), + ); + + assert.strictEqual(result, 'I saved and read back: Hello, world!'); + + // The readNote tool must have returned the saved note body to the model, + // proving that state persisted across the saveNote call within the same run. + const allInputItems = provider.model.requests.flatMap((req) => (Array.isArray(req.input) ? req.input : [])); + const readNoteResults = allInputItems.filter( + (item) => item.type === 'function_call_result' && (item as { name: string }).name === 'readNote', + ); + assert.strictEqual(readNoteResults.length, 1, 'readNote tool result should reach the model exactly once'); + assert.deepStrictEqual((readNoteResults[0] as { output: unknown }).output, [ + { type: 'input_text', text: 'Hello, world!' }, + ]); + }); +}); diff --git a/openai-agents/mcp/src/servers/filesystem-server.ts b/openai-agents/mcp/src/servers/filesystem-server.ts new file mode 100644 index 00000000..98b0ea47 --- /dev/null +++ b/openai-agents/mcp/src/servers/filesystem-server.ts @@ -0,0 +1,74 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; + +const SAMPLE_FILES_DIR = path.resolve(__dirname, 'sample-files'); + +function createFilesystemServer(): Server { + const server = new Server({ name: 'filesystem', version: '1.0.0' }, { capabilities: { tools: {} } }); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: 'listFiles', + description: 'List all files in the sample-files directory.', + inputSchema: { + type: 'object' as const, + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'readFile', + description: 'Read the contents of a file in the sample-files directory.', + inputSchema: { + type: 'object' as const, + properties: { + filename: { type: 'string', description: 'The filename to read (basename only).' }, + }, + required: ['filename'], + additionalProperties: false, + }, + }, + ], + })); + + server.setRequestHandler(CallToolRequestSchema, async (req) => { + const { name, arguments: args } = req.params; + if (name === 'listFiles') { + const files = fs + .readdirSync(SAMPLE_FILES_DIR) + .filter((f) => fs.statSync(path.join(SAMPLE_FILES_DIR, f)).isFile()); + return { content: [{ type: 'text', text: files.join('\n') }] }; + } + if (name === 'readFile') { + const filename = String((args as Record)?.filename ?? ''); + const resolved = path.resolve(SAMPLE_FILES_DIR, filename); + if (!resolved.startsWith(SAMPLE_FILES_DIR + path.sep) && resolved !== SAMPLE_FILES_DIR) { + return { content: [{ type: 'text', text: 'Error: path escape not allowed.' }], isError: true }; + } + if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) { + return { content: [{ type: 'text', text: `Error: file not found: ${filename}` }], isError: true }; + } + const text = fs.readFileSync(resolved, 'utf-8'); + return { content: [{ type: 'text', text }] }; + } + return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }; + }); + + return server; +} + +if (require.main === module) { + const server = createFilesystemServer(); + const transport = new StdioServerTransport(); + server.connect(transport).catch((err) => { + process.stderr.write(String(err) + '\n'); + process.exit(1); + }); +} + +export { createFilesystemServer }; diff --git a/openai-agents/mcp/src/servers/notes-server.ts b/openai-agents/mcp/src/servers/notes-server.ts new file mode 100644 index 00000000..77f6e94c --- /dev/null +++ b/openai-agents/mcp/src/servers/notes-server.ts @@ -0,0 +1,72 @@ +import type { StatefulTemporalMCPServer } from '@temporalio/openai-agents/workflow'; + +export function createNotesServer(): StatefulTemporalMCPServer { + const notes = new Map(); + + return { + cacheToolsList: false, + name: 'memory', + async connect() {}, + async close() {}, + async invalidateToolsCache() {}, + async cleanup() {}, + async listTools() { + return [ + { + name: 'saveNote', + description: 'Save a note with a title and body.', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string' }, + body: { type: 'string' }, + }, + required: ['title', 'body'], + additionalProperties: false, + }, + }, + { + name: 'listNotes', + description: 'List all note titles saved in this session.', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'readNote', + description: 'Read the body of a saved note by title.', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string' }, + }, + required: ['title'], + additionalProperties: false, + }, + }, + ]; + }, + async callTool(toolName: string, args: Record | null) { + const a = args ?? {}; + if (toolName === 'saveNote') { + const title = String(a.title); + const body = String(a.body); + notes.set(title, body); + return [{ type: 'text', text: `Saved note "${title}".` }]; + } + if (toolName === 'listNotes') { + const titles = Array.from(notes.keys()); + return [{ type: 'text', text: titles.length ? titles.join(', ') : '(no notes yet)' }]; + } + if (toolName === 'readNote') { + const title = String(a.title); + const body = notes.get(title); + return [{ type: 'text', text: body ?? `(no note titled "${title}")` }]; + } + return [{ type: 'text', text: `Unknown tool: ${toolName}` }]; + }, + }; +} diff --git a/openai-agents/mcp/src/servers/prompt-server.ts b/openai-agents/mcp/src/servers/prompt-server.ts new file mode 100644 index 00000000..378ce12f --- /dev/null +++ b/openai-agents/mcp/src/servers/prompt-server.ts @@ -0,0 +1,103 @@ +import * as http from 'http'; +import * as crypto from 'crypto'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js'; + +export const SUMMARIZE_PROMPT_TEXT = "You are a concise summarizer. Summarize the user's text in one sentence."; + +export async function startPromptHttpServer(port?: number): Promise<{ url: string; close(): Promise }> { + const transports = new Map(); + + function createSession(): { server: Server; transport: StreamableHTTPServerTransport } { + const server = new Server({ name: 'prompt-server', version: '1.0.0' }, { capabilities: { prompts: {} } }); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + }); + + transport.onclose = () => { + const sid = transport.sessionId; + if (sid) transports.delete(sid); + }; + + server.setRequestHandler(ListPromptsRequestSchema, async () => ({ + prompts: [ + { + name: 'summarize', + description: 'A prompt for concise text summarization.', + }, + ], + })); + + server.setRequestHandler(GetPromptRequestSchema, async (req) => { + if (req.params.name !== 'summarize') { + throw new Error(`Unknown prompt: ${req.params.name}`); + } + return { + messages: [ + { + role: 'user' as const, + content: { type: 'text' as const, text: SUMMARIZE_PROMPT_TEXT }, + }, + ], + }; + }); + + return { server, transport }; + } + + const httpServer = http.createServer(async (req, res) => { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(chunk as Buffer); + } + const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf-8')) : undefined; + + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (sessionId && transports.has(sessionId)) { + const transport = transports.get(sessionId)!; + await transport.handleRequest(req, res, body); + return; + } + + const isInit = body && typeof body === 'object' && !Array.isArray(body) && body.method === 'initialize'; + + if (isInit || !sessionId) { + const { server, transport } = createSession(); + await server.connect(transport); + await transport.handleRequest(req, res, body); + const sid = transport.sessionId; + if (sid) transports.set(sid, transport); + return; + } + + res.writeHead(404).end('Session not found'); + }); + + await new Promise((resolve) => { + httpServer.listen(port ?? 0, '127.0.0.1', resolve); + }); + + const addr = httpServer.address() as { port: number }; + const url = `http://127.0.0.1:${addr.port}/`; + + return { + url, + close: async () => { + await Promise.allSettled(Array.from(transports.values()).map((t) => t.close())); + await new Promise((resolve, reject) => httpServer.close((err) => (err ? reject(err) : resolve()))); + }, + }; +} + +if (require.main === module) { + startPromptHttpServer(3003) + .then(({ url }) => { + console.log(`Prompt HTTP MCP server listening at ${url}`); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/openai-agents/mcp/src/servers/sample-files/hello.txt b/openai-agents/mcp/src/servers/sample-files/hello.txt new file mode 100644 index 00000000..4fec89ed --- /dev/null +++ b/openai-agents/mcp/src/servers/sample-files/hello.txt @@ -0,0 +1,3 @@ +Hello from the MCP filesystem server! +This file is part of the sample-files directory. +It demonstrates the listFiles and readFile tools. diff --git a/openai-agents/mcp/src/servers/sample-files/notes.txt b/openai-agents/mcp/src/servers/sample-files/notes.txt new file mode 100644 index 00000000..1143080c --- /dev/null +++ b/openai-agents/mcp/src/servers/sample-files/notes.txt @@ -0,0 +1,3 @@ +Temporal is a durable execution platform. +Workflows are code that can survive process failures. +Activities are the building blocks for side effects. diff --git a/openai-agents/mcp/src/servers/sse-server.ts b/openai-agents/mcp/src/servers/sse-server.ts new file mode 100644 index 00000000..d7abb007 --- /dev/null +++ b/openai-agents/mcp/src/servers/sse-server.ts @@ -0,0 +1,125 @@ +import * as http from 'http'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; + +const SSE_PATH = '/sse'; +const MESSAGES_PATH = '/messages'; + +function buildToolHandlers(server: Server): void { + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: 'add', + description: 'Add two numbers together.', + inputSchema: { + type: 'object' as const, + properties: { + a: { type: 'number', description: 'First number.' }, + b: { type: 'number', description: 'Second number.' }, + }, + required: ['a', 'b'], + additionalProperties: false, + }, + }, + { + name: 'getWeather', + description: 'Get canned weather data for a city.', + inputSchema: { + type: 'object' as const, + properties: { + city: { type: 'string', description: 'City name.' }, + }, + required: ['city'], + additionalProperties: false, + }, + }, + { + name: 'getSecret', + description: 'Get the secret passphrase.', + inputSchema: { + type: 'object' as const, + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ], + })); + + server.setRequestHandler(CallToolRequestSchema, async (req) => { + const { name, arguments: args } = req.params; + const a = (args ?? {}) as Record; + if (name === 'add') { + return { content: [{ type: 'text', text: String(Number(a.a) + Number(a.b)) }] }; + } + if (name === 'getWeather') { + const city = String(a.city ?? 'unknown'); + return { content: [{ type: 'text', text: JSON.stringify({ city, temperature: '22C', conditions: 'Sunny' }) }] }; + } + if (name === 'getSecret') { + return { content: [{ type: 'text', text: 'swordfish' }] }; + } + return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }; + }); +} + +export async function startToolsSseServer(port?: number): Promise<{ url: string; close(): Promise }> { + const transports = new Map(); + + const httpServer = http.createServer(async (req, res) => { + if (req.method === 'GET' && req.url === SSE_PATH) { + const mcpServer = new Server({ name: 'tools-sse', version: '1.0.0' }, { capabilities: { tools: {} } }); + buildToolHandlers(mcpServer); + const transport = new SSEServerTransport(MESSAGES_PATH, res); + transports.set(transport.sessionId, transport); + transport.onclose = () => transports.delete(transport.sessionId); + await mcpServer.connect(transport); + return; + } + + if (req.method === 'POST' && req.url?.startsWith(MESSAGES_PATH)) { + const sessionId = new URL(req.url, 'http://localhost').searchParams.get('sessionId'); + const transport = sessionId ? transports.get(sessionId) : undefined; + if (!transport) { + res.writeHead(404).end('Session not found'); + return; + } + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(chunk as Buffer); + } + const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf-8')) : undefined; + await transport.handlePostMessage(req, res, body); + return; + } + + res.writeHead(404).end(); + }); + + await new Promise((resolve) => { + httpServer.listen(port ?? 0, '127.0.0.1', resolve); + }); + + const addr = httpServer.address() as { port: number }; + const url = `http://127.0.0.1:${addr.port}${SSE_PATH}`; + + return { + url, + close: async () => { + await Promise.allSettled(Array.from(transports.values()).map((t) => t.close())); + await new Promise((resolve, reject) => httpServer.close((err) => (err ? reject(err) : resolve()))); + }, + }; +} + +if (require.main === module) { + startToolsSseServer(3002) + .then(({ url }) => { + console.log(`Tools SSE MCP server listening at ${url}`); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/openai-agents/mcp/src/servers/tools-server.ts b/openai-agents/mcp/src/servers/tools-server.ts new file mode 100644 index 00000000..ce18dfa3 --- /dev/null +++ b/openai-agents/mcp/src/servers/tools-server.ts @@ -0,0 +1,104 @@ +import * as http from 'http'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; + +function registerToolHandlers(server: Server): void { + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: 'add', + description: 'Add two numbers together.', + inputSchema: { + type: 'object' as const, + properties: { + a: { type: 'number', description: 'First number.' }, + b: { type: 'number', description: 'Second number.' }, + }, + required: ['a', 'b'], + additionalProperties: false, + }, + }, + { + name: 'getWeather', + description: 'Get canned weather data for a city.', + inputSchema: { + type: 'object' as const, + properties: { + city: { type: 'string', description: 'City name.' }, + }, + required: ['city'], + additionalProperties: false, + }, + }, + { + name: 'getSecret', + description: 'Get the secret passphrase.', + inputSchema: { + type: 'object' as const, + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ], + })); + + server.setRequestHandler(CallToolRequestSchema, async (req) => { + const { name, arguments: args } = req.params; + const a = (args ?? {}) as Record; + if (name === 'add') { + const result = Number(a.a) + Number(a.b); + return { content: [{ type: 'text', text: String(result) }] }; + } + if (name === 'getWeather') { + const city = String(a.city ?? 'unknown'); + return { content: [{ type: 'text', text: JSON.stringify({ city, temperature: '22C', conditions: 'Sunny' }) }] }; + } + if (name === 'getSecret') { + return { content: [{ type: 'text', text: 'swordfish' }] }; + } + return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }; + }); +} + +export async function startToolsHttpServer(port?: number): Promise<{ url: string; close(): Promise }> { + const httpServer = http.createServer(async (req, res) => { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(chunk as Buffer); + } + const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf-8')) : undefined; + + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + const server = new Server({ name: 'tools', version: '1.0.0' }, { capabilities: { tools: {} } }); + registerToolHandlers(server); + await server.connect(transport); + await transport.handleRequest(req, res, body); + }); + + await new Promise((resolve) => { + httpServer.listen(port ?? 0, '127.0.0.1', resolve); + }); + + const addr = httpServer.address() as { port: number }; + const url = `http://127.0.0.1:${addr.port}/`; + + return { + url, + close: async () => { + await new Promise((resolve, reject) => httpServer.close((err) => (err ? reject(err) : resolve()))); + }, + }; +} + +if (require.main === module) { + startToolsHttpServer(3001) + .then(({ url }) => { + console.log(`Tools HTTP MCP server listening at ${url}`); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/openai-agents/mcp/src/worker.ts b/openai-agents/mcp/src/worker.ts new file mode 100644 index 00000000..5ede31a9 --- /dev/null +++ b/openai-agents/mcp/src/worker.ts @@ -0,0 +1,78 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin, StatelessMCPServerProvider, StatefulMCPServerProvider } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { MCPServerStdio, MCPServerStreamableHttp, MCPServerSSE } from '@openai/agents-core'; +import * as path from 'path'; +import { makeActivities } from './activities'; +import { startToolsHttpServer } from './servers/tools-server'; +import { startToolsSseServer } from './servers/sse-server'; +import { startPromptHttpServer } from './servers/prompt-server'; +import { createNotesServer } from './servers/notes-server'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const toolsHttp = await startToolsHttpServer(); + const toolsSse = await startToolsSseServer(); + const promptHttp = await startPromptHttpServer(); + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + + try { + const activities = makeActivities(promptHttp.url); + + const filesystemServerPath = path.resolve(__dirname, 'servers', 'filesystem-server.ts'); + + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-mcp', + workflowsPath: require.resolve('./workflows'), + activities, + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + mcpServerProviders: [ + new StatelessMCPServerProvider( + 'filesystem', + () => + new MCPServerStdio({ + command: 'npx', + args: ['ts-node', filesystemServerPath], + name: 'filesystem', + }), + ), + new StatelessMCPServerProvider( + 'streamableHttp', + () => new MCPServerStreamableHttp({ url: toolsHttp.url, name: 'streamableHttp' }), + ), + new StatelessMCPServerProvider('sse', () => new MCPServerSSE({ url: toolsSse.url, name: 'sse' })), + new StatefulMCPServerProvider('memory', () => createNotesServer(), connection), + ], + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + + await worker.run(); + } finally { + await Promise.allSettled([toolsHttp.close(), toolsSse.close(), promptHttp.close()]); + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/mcp/src/workflows.ts b/openai-agents/mcp/src/workflows.ts new file mode 100644 index 00000000..81afc803 --- /dev/null +++ b/openai-agents/mcp/src/workflows.ts @@ -0,0 +1,62 @@ +import { Agent } from '@openai/agents-core'; +import { TemporalOpenAIRunner, statelessMcpServer, statefulMcpServer } from '@temporalio/openai-agents/workflow'; +import { proxyActivities } from '@temporalio/workflow'; +import type { Activities } from './activities'; + +const activities = proxyActivities({ startToCloseTimeout: '1 minute' }); + +export async function filesystem(prompt: string): Promise { + const agent = new Agent({ + name: 'FilesystemAgent', + instructions: 'You are a helpful assistant with access to a filesystem.', + mcpServers: [statelessMcpServer('filesystem')], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function streamableHttp(prompt: string): Promise { + const agent = new Agent({ + name: 'StreamableHttpAgent', + instructions: 'You are a helpful assistant with access to tools via HTTP.', + mcpServers: [statelessMcpServer('streamableHttp')], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function sse(prompt: string): Promise { + const agent = new Agent({ + name: 'SseAgent', + instructions: 'You are a helpful assistant with access to tools via SSE.', + mcpServers: [statelessMcpServer('sse')], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function promptServer(prompt: string): Promise { + const instructions = await activities.fetchSummarizePrompt(); + const agent = new Agent({ + name: 'PromptAgent', + instructions, + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function statefulMemory(prompt: string): Promise { + const server = statefulMcpServer('memory'); + await server.connect(); + try { + const agent = new Agent({ + name: 'MemoryAgent', + instructions: 'You are a helpful assistant with access to a persistent notes store.', + mcpServers: [server], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; + } finally { + await server.cleanup(); + } +} diff --git a/openai-agents/mcp/tsconfig.json b/openai-agents/mcp/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/mcp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/model-providers/.eslintignore b/openai-agents/model-providers/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/model-providers/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/model-providers/.eslintrc.js b/openai-agents/model-providers/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/model-providers/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/model-providers/.gitignore b/openai-agents/model-providers/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/model-providers/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/model-providers/.npmrc b/openai-agents/model-providers/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/model-providers/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/model-providers/.nvmrc b/openai-agents/model-providers/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/model-providers/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/model-providers/.post-create b/openai-agents/model-providers/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/model-providers/.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/openai-agents/model-providers/.prettierignore b/openai-agents/model-providers/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/model-providers/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/model-providers/.prettierrc b/openai-agents/model-providers/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/model-providers/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/model-providers/README.md b/openai-agents/model-providers/README.md new file mode 100644 index 00000000..bd16f0e9 --- /dev/null +++ b/openai-agents/model-providers/README.md @@ -0,0 +1,55 @@ +# OpenAI Agents: Custom Model Providers + +Shows how to pass a **custom (non-default) `ModelProvider`** to `OpenAIAgentsPlugin`. Any OpenAI +Agents SDK `ModelProvider` can drive the model Activity, so you can point an agent at any +OpenAI-compatible endpoint (a local server such as Ollama, OpenRouter, or another gateway) by +constructing `OpenAIProvider` with a custom `baseURL`. + +The provider runs inside the model Activity, never in the Workflow sandbox. + +## Run + +Start a Temporal dev server: + +```bash +temporal server start-dev +``` + +Point the Worker at your OpenAI-compatible endpoint. For example, a local Ollama server: + +```bash +export OPENAI_BASE_URL=http://localhost:11434/v1 +export OPENAI_API_KEY=ollama # any non-empty value; most local servers ignore it +export OPENAI_MODEL=llama3.2 # a model your endpoint serves +``` + +Or an OpenRouter endpoint: + +```bash +export OPENAI_BASE_URL=https://openrouter.ai/api/v1 +export OPENAI_API_KEY=sk-or-... +export OPENAI_MODEL=meta-llama/llama-3.1-8b-instruct +``` + +Then start the Worker and run the Workflow: + +```bash +npm install +npm run start.watch +``` + +```bash +npm run workflow "Say hello in one sentence." +``` + +`OPENAI_MODEL` is forwarded to the run via `runConfig.model` and resolved by your custom provider. + +## Test + +The tests run fully offline. They inject a `FakeModelProvider` — itself a custom `ModelProvider` — +and assert the agent run is handled by the injected provider, including resolving the requested +model name. This is the same injection point the live custom-provider setup uses. + +```bash +npm test +``` diff --git a/openai-agents/model-providers/package.json b/openai-agents/model-providers/package.json new file mode 100644 index 00000000..d529ff56 --- /dev/null +++ b/openai-agents/model-providers/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-model-providers", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/model-providers/src/client.ts b/openai-agents/model-providers/src/client.ts new file mode 100644 index 00000000..36fdc24b --- /dev/null +++ b/openai-agents/model-providers/src/client.ts @@ -0,0 +1,38 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { customProvider } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + const baseURL = process.env.OPENAI_BASE_URL; + if (!baseURL) { + throw new Error('OPENAI_BASE_URL environment variable is required (the OpenAI-compatible endpoint)'); + } + const model = process.env.OPENAI_MODEL; + + const prompt = process.argv[2] ?? 'Say hello in one sentence.'; + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey, baseURL }) })], + }); + + const taskQueue = 'openai-agents-model-providers'; + const workflowId = 'openai-agents-' + nanoid(); + + const handle = await client.workflow.start(customProvider, { taskQueue, workflowId, args: [prompt, model] }); + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/model-providers/src/mocha/fake-model.ts b/openai-agents/model-providers/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/model-providers/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/model-providers/src/mocha/workflows.test.ts b/openai-agents/model-providers/src/mocha/workflows.test.ts new file mode 100644 index 00000000..3bc6779d --- /dev/null +++ b/openai-agents/model-providers/src/mocha/workflows.test.ts @@ -0,0 +1,68 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import assert from 'assert'; +import { FakeModelProvider, textResponse } from './fake-model'; +import { customProvider } from '../workflows'; + +describe('openai-agents/model-providers custom-provider injection', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + async function makeWorker(taskQueue: string, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('runs the agent through the injected custom provider', async () => { + const taskQueue = 'test-custom-provider'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('Hello from the custom provider.')]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(customProvider, { + args: ['Say hello.'], + workflowId: 'test-custom-provider-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Hello from the custom provider.'); + assert.strictEqual(provider.model.requests.length, 1, 'the injected provider should handle the model call'); + }); + + it('resolves the run model name through the injected provider', async () => { + const taskQueue = 'test-custom-provider-model'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('Done.')]); + await worker.runUntil( + testEnv.client.workflow.execute(customProvider, { + args: ['Say hello.', 'my-custom-model'], + workflowId: 'test-custom-provider-model-' + Date.now(), + taskQueue, + }), + ); + assert.deepStrictEqual(provider.requestedModelNames, ['my-custom-model']); + }); +}); diff --git a/openai-agents/model-providers/src/worker.ts b/openai-agents/model-providers/src/worker.ts new file mode 100644 index 00000000..a85a6ab6 --- /dev/null +++ b/openai-agents/model-providers/src/worker.ts @@ -0,0 +1,47 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + const baseURL = process.env.OPENAI_BASE_URL; + if (!baseURL) { + throw new Error('OPENAI_BASE_URL environment variable is required (the OpenAI-compatible endpoint)'); + } + const modelProvider = new OpenAIProvider({ apiKey, baseURL }); + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-model-providers', + workflowsPath: require.resolve('./workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider, + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/model-providers/src/workflows.ts b/openai-agents/model-providers/src/workflows.ts new file mode 100644 index 00000000..29976968 --- /dev/null +++ b/openai-agents/model-providers/src/workflows.ts @@ -0,0 +1,8 @@ +import { Agent } from '@openai/agents-core'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; + +export async function customProvider(prompt: string, model?: string): Promise { + const agent = new Agent({ name: 'Assistant', instructions: 'You are a helpful assistant.' }); + const result = await new TemporalOpenAIRunner().run(agent, prompt, model ? { runConfig: { model } } : undefined); + return result.finalOutput ?? ''; +} diff --git a/openai-agents/model-providers/tsconfig.json b/openai-agents/model-providers/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/model-providers/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/nexus-tools/.eslintignore b/openai-agents/nexus-tools/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/nexus-tools/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/nexus-tools/.eslintrc.js b/openai-agents/nexus-tools/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/nexus-tools/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/nexus-tools/.gitignore b/openai-agents/nexus-tools/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/nexus-tools/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/nexus-tools/.npmrc b/openai-agents/nexus-tools/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/nexus-tools/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/nexus-tools/.nvmrc b/openai-agents/nexus-tools/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/nexus-tools/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/nexus-tools/.post-create b/openai-agents/nexus-tools/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/nexus-tools/.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/openai-agents/nexus-tools/.prettierignore b/openai-agents/nexus-tools/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/nexus-tools/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/nexus-tools/.prettierrc b/openai-agents/nexus-tools/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/nexus-tools/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/nexus-tools/README.md b/openai-agents/nexus-tools/README.md new file mode 100644 index 00000000..7a6d5a8f --- /dev/null +++ b/openai-agents/nexus-tools/README.md @@ -0,0 +1,44 @@ +# OpenAI Agents: Nexus Tools + +Exposes a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with +`@temporalio/openai-agents`. The agent answers weather questions by calling a `weather.getWeather` +Nexus Operation through `nexusOperationAsTool`; the Workflow runs the Operation and feeds the result +back to the agent. + +The package defines: + +- a `nexus-rpc` weather service (`src/api.ts`), +- a synchronous service handler (`src/handler.ts`), registered on the Worker via `nexusServices`, +- a Workflow whose agent uses `nexusOperationAsTool` (`src/workflows.ts`). + +## Run + +Start the Temporal dev server: + +``` +temporal server start-dev +``` + +Set your OpenAI key and start the Worker: + +``` +export OPENAI_API_KEY=sk-... +npm run start +``` + +In another shell, run the Workflow. The client creates the Nexus endpoint if needed, then starts the +Workflow (optionally pass a prompt): + +``` +npm run workflow "What is the weather in Tokyo?" +``` + +## Test + +``` +npm test +``` + +The test uses `TestWorkflowEnvironment`, `env.createNexusEndpoint(...)`, a real Worker, and a +`FakeModelProvider`, so it runs without an API key. It asserts the Nexus operation result reaches the +model. diff --git a/openai-agents/nexus-tools/package.json b/openai-agents/nexus-tools/package.json new file mode 100644 index 00000000..6de9f6f8 --- /dev/null +++ b/openai-agents/nexus-tools/package.json @@ -0,0 +1,56 @@ +{ + "name": "temporal-openai-agents-nexus-tools", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/nexus": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nexus-rpc": "^0.0.2", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/nexus-tools/src/api.ts b/openai-agents/nexus-tools/src/api.ts new file mode 100644 index 00000000..87c17289 --- /dev/null +++ b/openai-agents/nexus-tools/src/api.ts @@ -0,0 +1,15 @@ +import * as nexus from 'nexus-rpc'; + +export interface GetWeatherInput { + city: string; +} + +export interface GetWeatherOutput { + city: string; + temperatureC: number; + conditions: string; +} + +export const weatherService = nexus.service('weather', { + getWeather: nexus.operation(), +}); diff --git a/openai-agents/nexus-tools/src/client.ts b/openai-agents/nexus-tools/src/client.ts new file mode 100644 index 00000000..12ba6202 --- /dev/null +++ b/openai-agents/nexus-tools/src/client.ts @@ -0,0 +1,52 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { nexusToolWorkflow, WEATHER_ENDPOINT } from './workflows'; +import { nanoid } from 'nanoid'; + +const TASK_QUEUE = 'openai-agents-nexus-tools'; + +async function ensureEndpoint(connection: Connection): Promise { + try { + await connection.operatorService.createNexusEndpoint({ + spec: { + name: WEATHER_ENDPOINT, + target: { worker: { namespace: 'default', taskQueue: TASK_QUEUE } }, + }, + }); + } catch (err) { + if (!String((err as { message?: string }).message).includes('already')) { + throw err; + } + } +} + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await Connection.connect(); + await ensureEndpoint(connection); + + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const prompt = process.argv[2] ?? 'What is the weather in Tokyo?'; + const handle = await client.workflow.start(nexusToolWorkflow, { + taskQueue: TASK_QUEUE, + workflowId: 'openai-agents-nexus-' + nanoid(), + args: [prompt], + }); + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/nexus-tools/src/handler.ts b/openai-agents/nexus-tools/src/handler.ts new file mode 100644 index 00000000..0659769a --- /dev/null +++ b/openai-agents/nexus-tools/src/handler.ts @@ -0,0 +1,15 @@ +import * as nexus from 'nexus-rpc'; +import { weatherService, GetWeatherInput, GetWeatherOutput } from './api'; + +const WEATHER: Record> = { + tokyo: { temperatureC: 22, conditions: 'Sunny' }, + london: { temperatureC: 14, conditions: 'Cloudy' }, + cairo: { temperatureC: 33, conditions: 'Clear' }, +}; + +export const weatherServiceHandler = nexus.serviceHandler(weatherService, { + getWeather: async (_ctx, input: GetWeatherInput): Promise => { + const found = WEATHER[input.city.toLowerCase()] ?? { temperatureC: 20, conditions: 'Unknown' }; + return { city: input.city, ...found }; + }, +}); diff --git a/openai-agents/nexus-tools/src/mocha/fake-model.ts b/openai-agents/nexus-tools/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/nexus-tools/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/nexus-tools/src/mocha/workflows.test.ts b/openai-agents/nexus-tools/src/mocha/workflows.test.ts new file mode 100644 index 00000000..e377ebf2 --- /dev/null +++ b/openai-agents/nexus-tools/src/mocha/workflows.test.ts @@ -0,0 +1,72 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import assert from 'assert'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { nexusToolWorkflow, WEATHER_ENDPOINT } from '../workflows'; +import { weatherServiceHandler } from '../handler'; + +describe('openai-agents/nexus-tools workflow', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('agent calls a Nexus operation as a tool and the result reaches the model', async () => { + const taskQueue = 'test-nexus-tools'; + await testEnv.createNexusEndpoint(WEATHER_ENDPOINT, taskQueue); + + const provider = new FakeModelProvider([ + toolCallResponse('getWeather', { city: 'Tokyo' }), + textResponse('It is 22C and sunny in Tokyo.'), + ]); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + nexusServices: [weatherServiceHandler], + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(nexusToolWorkflow, { + args: ['What is the weather in Tokyo?'], + workflowId: 'test-nexus-tools-' + Date.now(), + taskQueue, + }), + ); + + assert.strictEqual(result, 'It is 22C and sunny in Tokyo.'); + + // The Nexus operation result is sent back to the model on the second turn as a + // function_call_result item, proving the operation ran and its output reached the model. + const toolResults = provider.model.requests + .flatMap((req) => (Array.isArray(req.input) ? req.input : [])) + .filter((item) => item.type === 'function_call_result' && item.name === 'getWeather'); + assert.strictEqual(toolResults.length, 1, 'getWeather op result should reach the model exactly once'); + + const output = (toolResults[0] as { output: { type: string; text: string } }).output; + const opResult = JSON.parse(output.text); + assert.strictEqual(opResult.city, 'Tokyo'); + assert.strictEqual(opResult.temperatureC, 22); + assert.strictEqual(opResult.conditions, 'Sunny'); + }); +}); diff --git a/openai-agents/nexus-tools/src/worker.ts b/openai-agents/nexus-tools/src/worker.ts new file mode 100644 index 00000000..d641155f --- /dev/null +++ b/openai-agents/nexus-tools/src/worker.ts @@ -0,0 +1,44 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { weatherServiceHandler } from './handler'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-nexus-tools', + workflowsPath: require.resolve('./workflows'), + nexusServices: [weatherServiceHandler], + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/nexus-tools/src/workflows.ts b/openai-agents/nexus-tools/src/workflows.ts new file mode 100644 index 00000000..a0efa191 --- /dev/null +++ b/openai-agents/nexus-tools/src/workflows.ts @@ -0,0 +1,31 @@ +import { Agent } from '@openai/agents-core'; +import { nexusOperationAsTool, TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; +import { weatherService } from './api'; + +export const WEATHER_ENDPOINT = 'openai-agents-weather-endpoint'; + +export async function nexusToolWorkflow(prompt: string): Promise { + const weatherTool = nexusOperationAsTool( + weatherService.operations.getWeather, + { + name: 'getWeather', + description: 'Get the current weather for a city.', + parameters: { + type: 'object', + properties: { city: { type: 'string', description: 'The city name' } }, + required: ['city'], + additionalProperties: false, + }, + }, + { service: weatherService, endpoint: WEATHER_ENDPOINT, scheduleToCloseTimeout: '1 minute' }, + ); + + const agent = new Agent({ + name: 'WeatherAgent', + instructions: 'You are a weather assistant. Always use the getWeather tool to answer weather questions.', + tools: [weatherTool], + }); + + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} diff --git a/openai-agents/nexus-tools/tsconfig.json b/openai-agents/nexus-tools/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/nexus-tools/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/reasoning-content/.eslintignore b/openai-agents/reasoning-content/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/reasoning-content/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/reasoning-content/.eslintrc.js b/openai-agents/reasoning-content/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/reasoning-content/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/reasoning-content/.gitignore b/openai-agents/reasoning-content/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/reasoning-content/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/reasoning-content/.npmrc b/openai-agents/reasoning-content/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/reasoning-content/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/reasoning-content/.nvmrc b/openai-agents/reasoning-content/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/reasoning-content/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/reasoning-content/.post-create b/openai-agents/reasoning-content/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/reasoning-content/.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/openai-agents/reasoning-content/.prettierignore b/openai-agents/reasoning-content/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/reasoning-content/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/reasoning-content/.prettierrc b/openai-agents/reasoning-content/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/reasoning-content/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/reasoning-content/README.md b/openai-agents/reasoning-content/README.md new file mode 100644 index 00000000..804baa74 --- /dev/null +++ b/openai-agents/reasoning-content/README.md @@ -0,0 +1,48 @@ +# OpenAI Agents: Reasoning Content + +Extracts a reasoning model's **reasoning content** alongside its final answer. Unlike the other +samples, this one does **not** use `TemporalOpenAIRunner`. An Activity calls the `openai` SDK +directly with a reasoning-capable model and reads the non-standard `reasoning_content` field from +the response; the Workflow runs that Activity and returns both the reasoning and the answer. + +Mirrors the Python `openai_agents/reasoning_content` sample. + +## Run + +Start a Temporal dev server: + +```bash +temporal server start-dev +``` + +The default model is `deepseek-reasoner`, which returns `reasoning_content`. Point the `openai` +client at a compatible endpoint: + +```bash +export OPENAI_BASE_URL=https://api.deepseek.com +export OPENAI_API_KEY=sk-... +export OPENAI_MODEL=deepseek-reasoner +``` + +Then start the Worker and run the Workflow: + +```bash +npm install +npm run start.watch +``` + +```bash +npm run workflow "What is the square root of 841? Please explain your reasoning." +``` + +Not every model returns reasoning content; use one that does (such as `deepseek-reasoner`). + +## Test + +The test runs fully offline by overriding the Activity's `openai` client factory with a stub via +`setReasoningClientFactory`. It asserts the Workflow returns the reasoning and content the stub +provides, and that the prompt and model were forwarded to the client. + +```bash +npm test +``` diff --git a/openai-agents/reasoning-content/package.json b/openai-agents/reasoning-content/package.json new file mode 100644 index 00000000..92d3610e --- /dev/null +++ b/openai-agents/reasoning-content/package.json @@ -0,0 +1,53 @@ +{ + "name": "temporal-openai-agents-reasoning-content", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/reasoning-content/src/activities.ts b/openai-agents/reasoning-content/src/activities.ts new file mode 100644 index 00000000..582d1832 --- /dev/null +++ b/openai-agents/reasoning-content/src/activities.ts @@ -0,0 +1,39 @@ +import OpenAI from 'openai'; + +export interface ReasoningResponse { + reasoningContent: string | null; + content: string | null; +} + +export interface ReasoningClient { + chat: { + completions: { + create(body: { model: string; messages: { role: 'system' | 'user'; content: string }[] }): Promise<{ + choices: { message: { content: string | null; reasoning_content?: string | null } }[]; + }>; + }; + }; +} + +let clientFactory: () => ReasoningClient = () => new OpenAI() as unknown as ReasoningClient; + +export function setReasoningClientFactory(factory: () => ReasoningClient): void { + clientFactory = factory; +} + +export async function getReasoningResponse(prompt: string, model: string): Promise { + const client = clientFactory(); + const completion = await client.chat.completions.create({ + model, + messages: [ + { role: 'system', content: 'You are a helpful assistant that explains your reasoning step by step.' }, + { role: 'user', content: prompt }, + ], + }); + + const message = completion.choices[0]?.message; + return { + reasoningContent: message?.reasoning_content ?? null, + content: message?.content ?? null, + }; +} diff --git a/openai-agents/reasoning-content/src/client.ts b/openai-agents/reasoning-content/src/client.ts new file mode 100644 index 00000000..a7da7a98 --- /dev/null +++ b/openai-agents/reasoning-content/src/client.ts @@ -0,0 +1,32 @@ +import { Connection, Client } from '@temporalio/client'; +import { reasoningContent } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + if (!process.env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const prompt = process.argv[2] ?? 'What is the square root of 841? Please explain your reasoning.'; + const model = process.env.OPENAI_MODEL ?? 'deepseek-reasoner'; + + const connection = await Connection.connect(); + const client = new Client({ connection }); + + const handle = await client.workflow.start(reasoningContent, { + taskQueue: 'openai-agents-reasoning-content', + workflowId: 'openai-agents-' + nanoid(), + args: [prompt, model], + }); + + console.log(`Started workflow ${handle.workflowId}`); + const result = await handle.result(); + console.log(`\nPrompt: ${result.prompt}`); + console.log(`\nReasoning:\n${result.reasoningContent ?? 'No reasoning content provided'}`); + console.log(`\nAnswer:\n${result.content ?? 'No content provided'}`); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/reasoning-content/src/mocha/workflows.test.ts b/openai-agents/reasoning-content/src/mocha/workflows.test.ts new file mode 100644 index 00000000..0b2f6662 --- /dev/null +++ b/openai-agents/reasoning-content/src/mocha/workflows.test.ts @@ -0,0 +1,66 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import assert from 'assert'; +import * as activities from '../activities'; +import { reasoningContent } from '../workflows'; + +describe('openai-agents/reasoning-content', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('returns the reasoning and content from the model response', async () => { + const taskQueue = 'test-reasoning-content'; + const seenRequests: { model: string; prompt: string }[] = []; + activities.setReasoningClientFactory(() => ({ + chat: { + completions: { + create: async (body) => { + seenRequests.push({ model: body.model, prompt: body.messages[body.messages.length - 1]!.content }); + return { + choices: [ + { + message: { + reasoning_content: 'sqrt(841): 29 * 29 = 841, so the answer is 29.', + content: 'The square root of 841 is 29.', + }, + }, + ], + }; + }, + }, + }, + })); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(reasoningContent, { + args: ['What is the square root of 841?', 'fake-reasoning-model'], + workflowId: 'test-reasoning-content-' + Date.now(), + taskQueue, + }), + ); + + assert.strictEqual(result.prompt, 'What is the square root of 841?'); + assert.strictEqual(result.reasoningContent, 'sqrt(841): 29 * 29 = 841, so the answer is 29.'); + assert.strictEqual(result.content, 'The square root of 841 is 29.'); + assert.deepStrictEqual(seenRequests, [ + { model: 'fake-reasoning-model', prompt: 'What is the square root of 841?' }, + ]); + }); +}); diff --git a/openai-agents/reasoning-content/src/worker.ts b/openai-agents/reasoning-content/src/worker.ts new file mode 100644 index 00000000..33165c22 --- /dev/null +++ b/openai-agents/reasoning-content/src/worker.ts @@ -0,0 +1,26 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import * as activities from './activities'; + +async function run() { + if (!process.env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-reasoning-content', + workflowsPath: require.resolve('./workflows'), + activities, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/reasoning-content/src/workflows.ts b/openai-agents/reasoning-content/src/workflows.ts new file mode 100644 index 00000000..dd1555a0 --- /dev/null +++ b/openai-agents/reasoning-content/src/workflows.ts @@ -0,0 +1,16 @@ +import { proxyActivities } from '@temporalio/workflow'; +import type * as activities from './activities'; +import type { ReasoningResponse } from './activities'; + +const { getReasoningResponse } = proxyActivities({ startToCloseTimeout: '5 minutes' }); + +export interface ReasoningResult { + prompt: string; + reasoningContent: string | null; + content: string | null; +} + +export async function reasoningContent(prompt: string, model: string): Promise { + const response: ReasoningResponse = await getReasoningResponse(prompt, model); + return { prompt, reasoningContent: response.reasoningContent, content: response.content }; +} diff --git a/openai-agents/reasoning-content/tsconfig.json b/openai-agents/reasoning-content/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/reasoning-content/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/research-bot/.eslintignore b/openai-agents/research-bot/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/research-bot/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/research-bot/.eslintrc.js b/openai-agents/research-bot/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/research-bot/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/research-bot/.gitignore b/openai-agents/research-bot/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/research-bot/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/research-bot/.npmrc b/openai-agents/research-bot/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/research-bot/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/research-bot/.nvmrc b/openai-agents/research-bot/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/research-bot/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/research-bot/.post-create b/openai-agents/research-bot/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/research-bot/.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/openai-agents/research-bot/.prettierignore b/openai-agents/research-bot/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/research-bot/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/research-bot/.prettierrc b/openai-agents/research-bot/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/research-bot/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/research-bot/README.md b/openai-agents/research-bot/README.md new file mode 100644 index 00000000..8c5a3549 --- /dev/null +++ b/openai-agents/research-bot/README.md @@ -0,0 +1,39 @@ +# OpenAI Agents: Research Bot + +A multi-agent research Workflow built with `@temporalio/openai-agents`. It mirrors the OpenAI Agents +SDK research-bot sample: + +- A **planner** agent turns a query into a structured list of web searches (`outputType`). +- The planned searches run **concurrently** in the Workflow via `Promise.all`; each is its own durable + model call. +- A **writer** agent synthesizes the individual summaries into a final markdown report. + +## Run + +Start the Temporal dev server: + +``` +temporal server start-dev +``` + +Set your OpenAI key and start the Worker: + +``` +export OPENAI_API_KEY=sk-... +npm run start +``` + +In another shell, start the Workflow (optionally pass a query): + +``` +npm run workflow "Caribbean surfing in April" +``` + +## Test + +``` +npm test +``` + +The test uses `TestWorkflowEnvironment`, a real Worker, and a `FakeModelProvider`, so it runs without +an API key. diff --git a/openai-agents/research-bot/package.json b/openai-agents/research-bot/package.json new file mode 100644 index 00000000..4be19e9e --- /dev/null +++ b/openai-agents/research-bot/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-research-bot", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/research-bot/src/client.ts b/openai-agents/research-bot/src/client.ts new file mode 100644 index 00000000..5e980194 --- /dev/null +++ b/openai-agents/research-bot/src/client.ts @@ -0,0 +1,34 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { researchWorkflow } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const query = process.argv[2] ?? 'Caribbean vacation spots in April, optimizing for surfing.'; + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const handle = await client.workflow.start(researchWorkflow, { + taskQueue: 'openai-agents-research-bot', + workflowId: 'openai-agents-research-' + nanoid(), + args: [query], + }); + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/research-bot/src/mocha/fake-model.ts b/openai-agents/research-bot/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/research-bot/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/research-bot/src/mocha/workflows.test.ts b/openai-agents/research-bot/src/mocha/workflows.test.ts new file mode 100644 index 00000000..5b1fe820 --- /dev/null +++ b/openai-agents/research-bot/src/mocha/workflows.test.ts @@ -0,0 +1,77 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import assert from 'assert'; +import { FakeModelProvider, textResponse } from './fake-model'; +import { researchWorkflow } from '../workflows'; + +describe('openai-agents/research-bot workflow', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('researchWorkflow: plans searches, runs them concurrently, synthesizes a report', async () => { + const taskQueue = 'test-research-bot'; + + const plan = { + searches: [ + { reason: 'Surf conditions', query: 'best April surfing Caribbean' }, + { reason: 'Travel logistics', query: 'cheap flights Caribbean April' }, + ], + }; + const summaryA = 'SUMMARY_A: Barbados has consistent April swell.'; + const summaryB = 'SUMMARY_B: April flights to the Caribbean are affordable.'; + const report = { + shortSummary: 'A combined surf-and-travel overview.', + markdownReport: `# Report\n\n${summaryA}\n\n${summaryB}`, + followUpQuestions: ['Which island has the best board rental?'], + }; + + const provider = new FakeModelProvider([ + textResponse(JSON.stringify(plan)), + textResponse(summaryA), + textResponse(summaryB), + textResponse(JSON.stringify(report)), + ]); + + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(researchWorkflow, { + args: ['Caribbean surfing in April'], + workflowId: 'test-research-bot-' + Date.now(), + taskQueue, + }), + ); + + // The synthesized report incorporates both search summaries. + assert.ok(result.includes(summaryA), 'report should include first search summary'); + assert.ok(result.includes(summaryB), 'report should include second search summary'); + + // One planner call, two search calls (one per planned query), one writer call. + assert.strictEqual(provider.model.requests.length, 4, 'expected planner + 2 searches + writer model calls'); + }); +}); diff --git a/openai-agents/research-bot/src/worker.ts b/openai-agents/research-bot/src/worker.ts new file mode 100644 index 00000000..d9cea841 --- /dev/null +++ b/openai-agents/research-bot/src/worker.ts @@ -0,0 +1,42 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-research-bot', + workflowsPath: require.resolve('./workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/research-bot/src/workflows.ts b/openai-agents/research-bot/src/workflows.ts new file mode 100644 index 00000000..ad2ca5c2 --- /dev/null +++ b/openai-agents/research-bot/src/workflows.ts @@ -0,0 +1,76 @@ +import { Agent } from '@openai/agents-core'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; +import z from 'zod'; + +const WebSearchItem = z.object({ + reason: z.string().describe('Why this search is important to the query.'), + query: z.string().describe('The search term to use for the web search.'), +}); + +const WebSearchPlan = z.object({ + searches: z.array(WebSearchItem).describe('The web searches to perform to best answer the query.'), +}); + +const ReportData = z.object({ + shortSummary: z.string().describe('A short 2-3 sentence summary of the findings.'), + markdownReport: z.string().describe('The final report in markdown.'), + followUpQuestions: z.array(z.string()).describe('Suggested topics to research further.'), +}); + +function plannerAgent() { + return new Agent({ + name: 'PlannerAgent', + instructions: + 'You are a helpful research assistant. Given a query, come up with a set of web searches ' + + 'to perform to best answer the query. Output between 5 and 20 terms to query for.', + outputType: WebSearchPlan, + }); +} + +function searchAgent() { + return new Agent({ + name: 'SearchAgent', + instructions: + 'You are a research assistant. Given a search term, summarize the web results for that term ' + + 'in 2-3 short paragraphs under 300 words. Capture only the essence; ignore fluff.', + }); +} + +function writerAgent() { + return new Agent({ + name: 'WriterAgent', + instructions: + 'You are a senior researcher writing a cohesive report for a research query. You are given the ' + + 'original query and summaries from a research assistant. Produce a detailed markdown report that ' + + 'synthesizes the summaries.', + outputType: ReportData, + }); +} + +export async function researchWorkflow(query: string): Promise { + const runner = new TemporalOpenAIRunner(); + + const planResult = await runner.run(plannerAgent(), `Query: ${query}`); + const plan = planResult.finalOutput; + if (!plan) { + return ''; + } + + const summaries = await Promise.all( + plan.searches.map(async (item) => { + const result = await runner.run( + searchAgent(), + `Search term: ${item.query}\nReason for searching: ${item.reason}`, + ); + return result.finalOutput ?? ''; + }), + ); + + const writeResult = await runner.run( + writerAgent(), + `Original query: ${query}\nSummarized search results: ${JSON.stringify(summaries)}`, + ); + const report = writeResult.finalOutput; + + return report?.markdownReport ?? ''; +} diff --git a/openai-agents/research-bot/tsconfig.json b/openai-agents/research-bot/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/research-bot/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/sessions/.eslintignore b/openai-agents/sessions/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/sessions/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/sessions/.eslintrc.js b/openai-agents/sessions/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/sessions/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/sessions/.gitignore b/openai-agents/sessions/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/sessions/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/sessions/.npmrc b/openai-agents/sessions/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/sessions/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/sessions/.nvmrc b/openai-agents/sessions/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/sessions/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/sessions/.post-create b/openai-agents/sessions/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/sessions/.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/openai-agents/sessions/.prettierignore b/openai-agents/sessions/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/sessions/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/sessions/.prettierrc b/openai-agents/sessions/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/sessions/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/sessions/README.md b/openai-agents/sessions/README.md new file mode 100644 index 00000000..eaf30cff --- /dev/null +++ b/openai-agents/sessions/README.md @@ -0,0 +1,34 @@ +# OpenAI Agents: Sessions + +Demonstrates conversation sessions with the Temporal OpenAI Agents integration using +`WorkflowSafeMemorySession`, whose history lives on the Workflow heap and is rebuilt by replay. + +Scenarios (`src/workflows.ts`): + +- **multi-turn-chat** — runs several prompts over one shared session; later turns see earlier history. +- **carryover-chat** — carries session history across a `continueAsNew` boundary by capturing + `session.getItems()` and re-seeding the next run via `new WorkflowSafeMemorySession({ initialItems })`. + +## Run + +```bash +npm install +npm run build + +# In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): +OPENAI_API_KEY=sk-... npm start + +# In another terminal, start a scenario: +npm run workflow multi-turn-chat +npm run workflow carryover-chat +``` + +## Test + +```bash +npm test +``` + +Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no +`OPENAI_API_KEY` is required. They assert that a later turn's model input contains the prior +turn's history — including across the continue-as-new boundary. diff --git a/openai-agents/sessions/package.json b/openai-agents/sessions/package.json new file mode 100644 index 00000000..e072b904 --- /dev/null +++ b/openai-agents/sessions/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-sessions", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/sessions/src/activities.ts b/openai-agents/sessions/src/activities.ts new file mode 100644 index 00000000..d69b8fbb --- /dev/null +++ b/openai-agents/sessions/src/activities.ts @@ -0,0 +1,3 @@ +export async function ping(): Promise { + return 'pong'; +} diff --git a/openai-agents/sessions/src/client.ts b/openai-agents/sessions/src/client.ts new file mode 100644 index 00000000..0d4e6f65 --- /dev/null +++ b/openai-agents/sessions/src/client.ts @@ -0,0 +1,52 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { multiTurnChat, carryoverChat } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'multi-turn-chat'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-sessions'; + const workflowId = 'openai-agents-sessions-' + nanoid(); + + let handle; + switch (scenario) { + case 'multi-turn-chat': + handle = await client.workflow.start(multiTurnChat, { + taskQueue, + workflowId, + args: [['What is the capital of France?', 'And what is it known for?']], + }); + break; + case 'carryover-chat': + handle = await client.workflow.start(carryoverChat, { + taskQueue, + workflowId, + args: [{ prompts: ['What is the capital of France?', 'And what is it known for?', 'What is the population?'] }], + }); + break; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/sessions/src/mocha/fake-model.ts b/openai-agents/sessions/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/sessions/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/sessions/src/mocha/workflows.test.ts b/openai-agents/sessions/src/mocha/workflows.test.ts new file mode 100644 index 00000000..dfac9262 --- /dev/null +++ b/openai-agents/sessions/src/mocha/workflows.test.ts @@ -0,0 +1,153 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import * as activities from '../activities'; +import assert from 'assert'; +import { FakeModelProvider, textResponse } from './fake-model'; +import { multiTurnChat, carryoverChat } from '../workflows'; + +describe('openai-agents/sessions workflow scenarios', function () { + this.timeout(60_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + async function makeWorker(taskQueue: string, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities, + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('multiTurnChat: second turn input includes the first turn history', async () => { + const taskQueue = 'test-multi-turn-chat'; + const firstPrompt = 'What is the capital of France?'; + const firstReply = 'The capital of France is Paris.'; + const secondPrompt = 'What is it known for?'; + const secondReply = 'Paris is known for the Eiffel Tower.'; + + const { worker, provider } = await makeWorker(taskQueue, [textResponse(firstReply), textResponse(secondReply)]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(multiTurnChat, { + args: [[firstPrompt, secondPrompt]], + workflowId: 'test-multi-turn-chat-' + Date.now(), + taskQueue, + }), + ); + + assert.deepStrictEqual(result, [firstReply, secondReply]); + assert.strictEqual(provider.model.requests.length, 2, 'should have made one model call per prompt'); + + const turn1Input = provider.model.requests[0]!.input; + assert.ok( + turn1Input === firstPrompt || (Array.isArray(turn1Input) && JSON.stringify(turn1Input).includes(firstPrompt)), + 'turn-1 input should contain the first prompt', + ); + + const turn2Input = provider.model.requests[1]!.input; + assert.ok(Array.isArray(turn2Input), 'turn-2 input should be an array because the session prepends history items'); + + const turn2Text = JSON.stringify(turn2Input); + assert.ok( + turn2Text.includes(firstPrompt), + 'turn-2 input should include the turn-1 user prompt from session history', + ); + assert.ok( + turn2Text.includes(firstReply), + 'turn-2 input should include the turn-1 assistant reply from session history', + ); + + const userItems = (turn2Input as unknown[]).filter( + (item): item is { role: string; content: unknown } => + typeof item === 'object' && item !== null && 'role' in item && (item as { role: string }).role === 'user', + ); + const assistantItems = (turn2Input as unknown[]).filter( + (item): item is { role: string; content: unknown } => + typeof item === 'object' && item !== null && 'role' in item && (item as { role: string }).role === 'assistant', + ); + + assert.ok(userItems.length >= 2, 'turn-2 input should have at least two user items: history + new prompt'); + assert.ok(assistantItems.length >= 1, 'turn-2 input should have at least one assistant item from session history'); + + const firstUserContent = userItems[0]!.content; + const firstUserText = typeof firstUserContent === 'string' ? firstUserContent : JSON.stringify(firstUserContent); + assert.ok( + firstUserText.includes(firstPrompt), + 'first user item in turn-2 should be the turn-1 prompt (replayed from session)', + ); + + const assistantContent = assistantItems[0]!.content; + const assistantText = typeof assistantContent === 'string' ? assistantContent : JSON.stringify(assistantContent); + assert.ok( + assistantText.includes(firstReply), + 'assistant item in turn-2 should contain the turn-1 reply (replayed from session)', + ); + + const lastUserContent = userItems[userItems.length - 1]!.content; + const lastUserText = typeof lastUserContent === 'string' ? lastUserContent : JSON.stringify(lastUserContent); + assert.ok(lastUserText.includes(secondPrompt), 'last user item in turn-2 should be the new second prompt'); + }); + + it('carryoverChat: session history survives a continue-as-new boundary', async () => { + const taskQueue = 'test-carryover-chat'; + const firstPrompt = 'What is the capital of France?'; + const firstReply = 'The capital of France is Paris.'; + const secondPrompt = 'What is it known for?'; + const secondReply = 'Paris is known for the Eiffel Tower.'; + const thirdPrompt = 'What is the population?'; + const thirdReply = 'The population is about two million.'; + + const { worker, provider } = await makeWorker(taskQueue, [ + textResponse(firstReply), + textResponse(secondReply), + textResponse(thirdReply), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(carryoverChat, { + args: [{ prompts: [firstPrompt, secondPrompt, thirdPrompt] }], + workflowId: 'test-carryover-chat-' + Date.now(), + taskQueue, + }), + ); + + assert.deepStrictEqual(result, [firstReply, secondReply, thirdReply]); + assert.strictEqual( + provider.model.requests.length, + 3, + 'should have made one model call per prompt across continue-as-new runs', + ); + + const thirdTurnInput = provider.model.requests[2]!.input; + const thirdTurnText = Array.isArray(thirdTurnInput) ? JSON.stringify(thirdTurnInput) : String(thirdTurnInput); + assert.ok( + thirdTurnText.includes(firstPrompt), + 'third-run model input should include the first-run user prompt, proving history crossed the continue-as-new boundary', + ); + assert.ok( + thirdTurnText.includes(firstReply), + 'third-run model input should include the first-run assistant reply, proving history crossed the continue-as-new boundary', + ); + }); +}); diff --git a/openai-agents/sessions/src/worker.ts b/openai-agents/sessions/src/worker.ts new file mode 100644 index 00000000..1c1fa52b --- /dev/null +++ b/openai-agents/sessions/src/worker.ts @@ -0,0 +1,44 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import * as activities from './activities'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-sessions', + workflowsPath: require.resolve('./workflows'), + activities, + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/sessions/src/workflows.ts b/openai-agents/sessions/src/workflows.ts new file mode 100644 index 00000000..67bf86ff --- /dev/null +++ b/openai-agents/sessions/src/workflows.ts @@ -0,0 +1,48 @@ +import { Agent } from '@openai/agents-core'; +import type { AgentInputItem } from '@openai/agents-core'; +import { TemporalOpenAIRunner, WorkflowSafeMemorySession } from '@temporalio/openai-agents/workflow'; +import { continueAsNew } from '@temporalio/workflow'; + +export async function multiTurnChat(prompts: string[]): Promise { + const agent = new Agent({ name: 'ChatAgent', instructions: 'You are a helpful assistant.' }); + const session = new WorkflowSafeMemorySession(); + const runner = new TemporalOpenAIRunner(); + const replies: string[] = []; + for (const prompt of prompts) { + const result = await runner.run(agent, prompt, { session }); + replies.push(result.finalOutput ?? ''); + } + return replies; +} + +export interface CarryoverChatInput { + prompts: string[]; + initialItems?: AgentInputItem[]; + accumulated?: string[]; +} + +export async function carryoverChat(input: CarryoverChatInput): Promise { + const agent = new Agent({ name: 'ChatAgent', instructions: 'You are a helpful assistant.' }); + const session = new WorkflowSafeMemorySession({ initialItems: input.initialItems }); + const runner = new TemporalOpenAIRunner(); + const accumulated = input.accumulated ?? []; + + const [prompt, ...remaining] = input.prompts; + if (prompt === undefined) { + return accumulated; + } + + const result = await runner.run(agent, prompt, { session }); + accumulated.push(result.finalOutput ?? ''); + + if (remaining.length === 0) { + return accumulated; + } + + const items = await session.getItems(); + await continueAsNew({ + prompts: remaining, + initialItems: items, + accumulated, + }); +} diff --git a/openai-agents/sessions/tsconfig.json b/openai-agents/sessions/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/sessions/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/tools/.eslintignore b/openai-agents/tools/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/tools/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/tools/.eslintrc.js b/openai-agents/tools/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/tools/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/tools/.gitignore b/openai-agents/tools/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/tools/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/tools/.npmrc b/openai-agents/tools/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/tools/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/tools/.nvmrc b/openai-agents/tools/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/tools/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/tools/.post-create b/openai-agents/tools/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/tools/.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/openai-agents/tools/.prettierignore b/openai-agents/tools/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/tools/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/tools/.prettierrc b/openai-agents/tools/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/tools/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/tools/README.md b/openai-agents/tools/README.md new file mode 100644 index 00000000..1dcf9573 --- /dev/null +++ b/openai-agents/tools/README.md @@ -0,0 +1,48 @@ +# OpenAI Agents: Hosted Tools + +Runs OpenAI Agents SDK agents as Temporal Workflows using server-side **hosted tools** from +`@openai/agents-openai`. Each hosted tool executes inside the model provider during the model +Activity, so there is no Activity to back them in your own code. + +Scenarios (one Workflow each): + +- `web-search` — agent with `webSearchTool()` +- `image-generation` — agent with `imageGenerationTool()` +- `code-interpreter` — agent with `codeInterpreterTool()` + +## Run + +Start a Temporal dev server: + +```bash +temporal server start-dev +``` + +In one shell, start the Worker (a real `OPENAI_API_KEY` is required for hosted tools to fire): + +```bash +export OPENAI_API_KEY=sk-... +npm install +npm run start.watch +``` + +In another shell, run a scenario: + +```bash +npm run workflow web-search +npm run workflow image-generation +npm run workflow code-interpreter +``` + +Hosted tools only execute against the live OpenAI API. With a real key, the agent calls the tool +server-side and returns the model's answer. + +## Test + +The tests run fully offline with a `FakeModelProvider`. Because hosted tools run inside the real +model call, the fake provider cannot exercise them; instead each test asserts the Workflow **wires** +the hosted tool into the model request and completes with a scripted response. + +```bash +npm test +``` diff --git a/openai-agents/tools/package.json b/openai-agents/tools/package.json new file mode 100644 index 00000000..017878be --- /dev/null +++ b/openai-agents/tools/package.json @@ -0,0 +1,54 @@ +{ + "name": "temporal-openai-agents-tools", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21" + } +} diff --git a/openai-agents/tools/src/client.ts b/openai-agents/tools/src/client.ts new file mode 100644 index 00000000..0a700eea --- /dev/null +++ b/openai-agents/tools/src/client.ts @@ -0,0 +1,59 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { webSearch, imageGeneration, codeInterpreter } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const scenario = process.argv[2] ?? 'web-search'; + console.log(`Running scenario: ${scenario}`); + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const taskQueue = 'openai-agents-tools'; + const workflowId = 'openai-agents-' + nanoid(); + + let handle; + switch (scenario) { + case 'web-search': + handle = await client.workflow.start(webSearch, { + taskQueue, + workflowId, + args: ['What is a notable recent development in durable execution?'], + }); + break; + case 'image-generation': + handle = await client.workflow.start(imageGeneration, { + taskQueue, + workflowId, + args: ['Generate an image of a robot orchestrating workflows.'], + }); + break; + case 'code-interpreter': + handle = await client.workflow.start(codeInterpreter, { + taskQueue, + workflowId, + args: ['What is the 20th Fibonacci number? Compute it.'], + }); + break; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/tools/src/mocha/fake-model.ts b/openai-agents/tools/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/tools/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/tools/src/mocha/workflows.test.ts b/openai-agents/tools/src/mocha/workflows.test.ts new file mode 100644 index 00000000..78ac1c0c --- /dev/null +++ b/openai-agents/tools/src/mocha/workflows.test.ts @@ -0,0 +1,100 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import assert from 'assert'; +import { FakeModelProvider, textResponse } from './fake-model'; +import { webSearch, imageGeneration, codeInterpreter } from '../workflows'; + +describe('openai-agents/tools hosted-tool 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, responses: ReturnType[]) { + const provider = new FakeModelProvider(responses); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + return { worker, provider }; + } + + it('webSearch: agent with webSearchTool runs and returns model output', async () => { + const taskQueue = 'test-web-search'; + const { worker, provider } = await makeWorker(taskQueue, [ + textResponse('Durable execution keeps state on failure.'), + ]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(webSearch, { + args: ['What is durable execution?'], + workflowId: 'test-web-search-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Durable execution keeps state on failure.'); + + const tools = provider.model.requests[0]?.tools ?? []; + assert.ok( + tools.some((t) => t.type === 'hosted_tool' && t.name === 'web_search'), + 'web_search hosted tool should be sent to the model', + ); + }); + + it('imageGeneration: agent with imageGenerationTool runs and returns model output', async () => { + const taskQueue = 'test-image-generation'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('Here is your generated image.')]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(imageGeneration, { + args: ['Generate an image of a robot.'], + workflowId: 'test-image-generation-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'Here is your generated image.'); + + const tools = provider.model.requests[0]?.tools ?? []; + assert.ok( + tools.some((t) => t.type === 'hosted_tool' && t.name === 'image_generation'), + 'image_generation hosted tool should be sent to the model', + ); + }); + + it('codeInterpreter: agent with codeInterpreterTool runs and returns model output', async () => { + const taskQueue = 'test-code-interpreter'; + const { worker, provider } = await makeWorker(taskQueue, [textResponse('The answer is 6765.')]); + const result = await worker.runUntil( + testEnv.client.workflow.execute(codeInterpreter, { + args: ['What is the 20th Fibonacci number?'], + workflowId: 'test-code-interpreter-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, 'The answer is 6765.'); + + const tools = provider.model.requests[0]?.tools ?? []; + assert.ok( + tools.some((t) => t.type === 'hosted_tool' && t.name === 'code_interpreter'), + 'code_interpreter hosted tool should be sent to the model', + ); + }); +}); diff --git a/openai-agents/tools/src/worker.ts b/openai-agents/tools/src/worker.ts new file mode 100644 index 00000000..0725b814 --- /dev/null +++ b/openai-agents/tools/src/worker.ts @@ -0,0 +1,42 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-tools', + workflowsPath: require.resolve('./workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/tools/src/workflows.ts b/openai-agents/tools/src/workflows.ts new file mode 100644 index 00000000..7057ce09 --- /dev/null +++ b/openai-agents/tools/src/workflows.ts @@ -0,0 +1,34 @@ +import { Agent } from '@openai/agents-core'; +import { webSearchTool, imageGenerationTool, codeInterpreterTool } from '@openai/agents-openai'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; + +export async function webSearch(prompt: string): Promise { + const agent = new Agent({ + name: 'WebSearchAgent', + instructions: 'Use the web search tool to find current information, then answer concisely.', + tools: [webSearchTool()], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function imageGeneration(prompt: string): Promise { + const agent = new Agent({ + name: 'ImageAgent', + instructions: + 'Use the image generation tool to create an image for the user request, then reply with a one-sentence caption describing the image you created.', + tools: [imageGenerationTool({ outputFormat: 'webp', quality: 'low', outputCompression: 50 })], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} + +export async function codeInterpreter(prompt: string): Promise { + const agent = new Agent({ + name: 'CodeAgent', + instructions: 'Use the code interpreter tool to compute results, then explain the answer.', + tools: [codeInterpreterTool()], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} diff --git a/openai-agents/tools/tsconfig.json b/openai-agents/tools/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/tools/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/openai-agents/tracing/.eslintignore b/openai-agents/tracing/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/openai-agents/tracing/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/openai-agents/tracing/.eslintrc.js b/openai-agents/tracing/.eslintrc.js new file mode 100644 index 00000000..71141741 --- /dev/null +++ b/openai-agents/tracing/.eslintrc.js @@ -0,0 +1,32 @@ +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: { + '@typescript-eslint/no-floating-promises': 'error', + 'deprecation/deprecation': 'warn', + 'object-shorthand': ['error', 'always'], + '@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/openai-agents/tracing/.gitignore b/openai-agents/tracing/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/openai-agents/tracing/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/openai-agents/tracing/.npmrc b/openai-agents/tracing/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/openai-agents/tracing/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/openai-agents/tracing/.nvmrc b/openai-agents/tracing/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/openai-agents/tracing/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/openai-agents/tracing/.post-create b/openai-agents/tracing/.post-create new file mode 100644 index 00000000..055c11e9 --- /dev/null +++ b/openai-agents/tracing/.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/openai-agents/tracing/.prettierignore b/openai-agents/tracing/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/openai-agents/tracing/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/openai-agents/tracing/.prettierrc b/openai-agents/tracing/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/openai-agents/tracing/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/openai-agents/tracing/README.md b/openai-agents/tracing/README.md new file mode 100644 index 00000000..7490fc75 --- /dev/null +++ b/openai-agents/tracing/README.md @@ -0,0 +1,70 @@ +# OpenAI Agents: Tracing + +Shows the three tracing paths the Temporal OpenAI Agents integration supports. OpenAI Agents SDK +tracing works across Client, Workflow, Activity, Nexus, and MCP boundaries, and Workflow replay does +not duplicate spans. + +The Worker selects a tracing mode (default `custom`): + +```bash +npm run start.watch -- # mode: custom | openai | otel +# or set TRACING_MODE= +``` + +All modes also set `interceptorOptions: { addTemporalSpans: true }`, which emits `temporal:*` +orchestration spans (Workflow starts, Activities, Signals, and so on) to whichever sink is active. + +### 1. `custom` — a custom `TracingProcessor` + +Registers a `RecordingTracingProcessor` (see `src/recording-processor.ts`) via `addTraceProcessor`. +It receives every trace and span lifecycle callback, so you can record, filter, or forward spans +anywhere. This is the mode exercised by the test. + +### 2. `openai` — OpenAI hosted traces + +Registers the upstream hosted exporter before constructing the plugin, so traces appear on the +OpenAI dashboard: + +```ts +addTraceProcessor(new BatchTraceProcessor(new OpenAITracingExporter())); +``` + +Register this in the Worker process, not inside Workflow code. + +### 3. `otel` — OpenTelemetry + +Installs a replay-safe tracer provider from `@temporalio/openai-agents/otel` and enables +`useOtelInstrumentation`: + +```ts +trace.setGlobalTracerProvider(createTracerProvider()); +// plugin: interceptorOptions: { useOtelInstrumentation: true } +``` + +`createTracerProvider` uses `TemporalIdGenerator` for replay-safe span/trace IDs. This mode requires +the optional peer dependency `@opentelemetry/sdk-trace-base`. + +## Run + +```bash +temporal server start-dev +``` + +```bash +export OPENAI_API_KEY=sk-... +npm install +npm run start.watch -- custom +``` + +```bash +npm run workflow "What is 42 plus 58?" +``` + +## Test + +The test runs offline with a `FakeModelProvider`. It registers a custom `TracingProcessor`, runs an +agent that calls a function tool, and asserts that `agent` and `function` spans were emitted. + +```bash +npm test +``` diff --git a/openai-agents/tracing/package.json b/openai-agents/tracing/package.json new file mode 100644 index 00000000..d28de95e --- /dev/null +++ b/openai-agents/tracing/package.json @@ -0,0 +1,56 @@ +{ + "name": "temporal-openai-agents-tracing", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.18.0", + "@temporalio/client": "^1.18.0", + "@temporalio/openai-agents": "^1.18.0", + "@temporalio/worker": "^1.18.0", + "@temporalio/workflow": "^1.18.0", + "@openai/agents-core": "^0.11.6", + "@openai/agents-openai": "^0.11.6", + "openai": "^6.0.0", + "nanoid": "3.x", + "zod": "^4.0.0", + "@opentelemetry/api": "^1.9.0" + }, + "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", + "nodemon": "^3.1.7", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "source-map-support": "^0.5.21", + "@opentelemetry/sdk-trace-base": "^1.30.0" + } +} diff --git a/openai-agents/tracing/src/client.ts b/openai-agents/tracing/src/client.ts new file mode 100644 index 00000000..c8793869 --- /dev/null +++ b/openai-agents/tracing/src/client.ts @@ -0,0 +1,34 @@ +import { Connection, Client } from '@temporalio/client'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { tracedAgent } from './workflows'; +import { nanoid } from 'nanoid'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const prompt = process.argv[2] ?? 'What is 42 plus 58?'; + + const connection = await Connection.connect(); + const client = new Client({ + connection, + plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], + }); + + const handle = await client.workflow.start(tracedAgent, { + taskQueue: 'openai-agents-tracing', + workflowId: 'openai-agents-' + nanoid(), + args: [prompt], + }); + + console.log(`Started workflow ${handle.workflowId}`); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/tracing/src/mocha/fake-model.ts b/openai-agents/tracing/src/mocha/fake-model.ts new file mode 100644 index 00000000..f8e0ceae --- /dev/null +++ b/openai-agents/tracing/src/mocha/fake-model.ts @@ -0,0 +1,69 @@ +import { + Usage, + type AgentOutputItem, + type Model, + type ModelProvider, + type ModelRequest, + type ModelResponse, + type StreamEvent, +} from '@openai/agents-core'; + +export class FakeModel implements Model { + readonly requests: ModelRequest[] = []; + private getNext: () => ModelResponse; + constructor(source: ModelResponse[]) { + let index = 0; + this.getNext = () => { + if (index >= source.length) { + throw new Error( + `FakeModel: no more canned responses (called ${index + 1} times, only ${source.length} provided)`, + ); + } + return source[index++]!; + }; + } + async getResponse(request: ModelRequest): Promise { + this.requests.push(request); + return this.getNext(); + } + // eslint-disable-next-line require-yield + async *getStreamedResponse(_request: ModelRequest): AsyncIterable { + throw new Error('Streaming not supported in FakeModel'); + } +} + +export class FakeModelProvider implements ModelProvider { + readonly requestedModelNames: (string | undefined)[] = []; + readonly model: FakeModel; + constructor(source: ModelResponse[]) { + this.model = new FakeModel(source); + } + getModel(name?: string): Model { + this.requestedModelNames.push(name); + return this.model; + } +} + +function fakeUsage(outputTokens: number): Usage { + return new Usage({ requests: 1, inputTokens: 10, outputTokens, totalTokens: 10 + outputTokens }); +} + +export function textResponse(text: string): ModelResponse { + const output: AgentOutputItem[] = [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], status: 'completed' }, + ]; + return { output, usage: fakeUsage(text.length) }; +} + +export function toolCallResponse(toolName: string, args: Record): ModelResponse { + const output: AgentOutputItem[] = [ + { + type: 'function_call', + name: toolName, + arguments: JSON.stringify(args), + callId: `call_fake_${toolName}`, + status: 'completed', + }, + ]; + return { output, usage: fakeUsage(20) }; +} diff --git a/openai-agents/tracing/src/mocha/workflows.test.ts b/openai-agents/tracing/src/mocha/workflows.test.ts new file mode 100644 index 00000000..abb7dcc1 --- /dev/null +++ b/openai-agents/tracing/src/mocha/workflows.test.ts @@ -0,0 +1,62 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, describe, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { addTraceProcessor } from '@openai/agents-core'; +import assert from 'assert'; +import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; +import { RecordingTracingProcessor } from '../recording-processor'; +import { tracedAgent } from '../workflows'; + +describe('openai-agents/tracing custom TracingProcessor', function () { + this.timeout(30_000); + + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); + }); + + after(async () => { + await testEnv?.teardown(); + }); + + it('captures agent and function spans emitted during the run', async () => { + const taskQueue = 'test-tracing-custom'; + const recorder = new RecordingTracingProcessor(); + addTraceProcessor(recorder); + + const provider = new FakeModelProvider([ + toolCallResponse('add', { a: 42, b: 58 }), + textResponse('42 plus 58 is 100.'), + ]); + const worker = await Worker.create({ + connection: testEnv.nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + plugins: [new OpenAIAgentsPlugin({ modelProvider: provider, interceptorOptions: { addTemporalSpans: true } })], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + + const result = await worker.runUntil( + testEnv.client.workflow.execute(tracedAgent, { + args: ['What is 42 plus 58?'], + workflowId: 'test-tracing-custom-' + Date.now(), + taskQueue, + }), + ); + assert.strictEqual(result, '42 plus 58 is 100.'); + + assert.ok(recorder.traceIds.length >= 1, 'at least one trace should be recorded'); + assert.ok(recorder.spanTypes.includes('agent'), 'an agent span should be recorded'); + assert.ok(recorder.spanTypes.includes('function'), 'a function tool span should be recorded'); + }); +}); diff --git a/openai-agents/tracing/src/recording-processor.ts b/openai-agents/tracing/src/recording-processor.ts new file mode 100644 index 00000000..98bb4551 --- /dev/null +++ b/openai-agents/tracing/src/recording-processor.ts @@ -0,0 +1,22 @@ +import type { Span, SpanData, Trace, TracingProcessor } from '@openai/agents-core'; + +export class RecordingTracingProcessor implements TracingProcessor { + readonly spanTypes: string[] = []; + readonly traceIds: string[] = []; + + async onTraceStart(trace: Trace): Promise { + this.traceIds.push(trace.traceId); + } + + async onTraceEnd(_trace: Trace): Promise {} + + async onSpanStart(_span: Span): Promise {} + + async onSpanEnd(span: Span): Promise { + this.spanTypes.push(span.spanData.type); + } + + async shutdown(_timeout?: number): Promise {} + + async forceFlush(): Promise {} +} diff --git a/openai-agents/tracing/src/worker.ts b/openai-agents/tracing/src/worker.ts new file mode 100644 index 00000000..aa7896da --- /dev/null +++ b/openai-agents/tracing/src/worker.ts @@ -0,0 +1,69 @@ +import { NativeConnection, Worker } from '@temporalio/worker'; +import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; +import { OpenAIProvider } from '@openai/agents-openai'; +import { OpenAITracingExporter } from '@openai/agents-openai'; +import { addTraceProcessor, BatchTraceProcessor } from '@openai/agents-core'; +import { trace } from '@opentelemetry/api'; +import { createTracerProvider } from '@temporalio/openai-agents/otel'; +import { RecordingTracingProcessor } from './recording-processor'; + +type TracingMode = 'openai' | 'custom' | 'otel'; + +async function run() { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); + } + + const mode = (process.argv[2] ?? process.env.TRACING_MODE ?? 'custom') as TracingMode; + console.log(`Tracing mode: ${mode}`); + + let useOtelInstrumentation = false; + switch (mode) { + case 'openai': + addTraceProcessor(new BatchTraceProcessor(new OpenAITracingExporter())); + break; + case 'custom': + addTraceProcessor(new RecordingTracingProcessor()); + break; + case 'otel': + trace.setGlobalTracerProvider(createTracerProvider()); + useOtelInstrumentation = true; + break; + default: + throw new Error(`Unknown tracing mode: ${mode}`); + } + + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + taskQueue: 'openai-agents-tracing', + workflowsPath: require.resolve('./workflows'), + plugins: [ + new OpenAIAgentsPlugin({ + modelProvider: new OpenAIProvider({ apiKey }), + modelParams: { useLocalActivity: true }, + interceptorOptions: { useOtelInstrumentation, addTemporalSpans: true }, + }), + ], + bundlerOptions: { + webpackConfigHook: (config) => ({ + ...config, + resolve: { + ...config.resolve, + conditionNames: ['require', 'browser', 'default'], + }, + }), + }, + }); + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/openai-agents/tracing/src/workflows.ts b/openai-agents/tracing/src/workflows.ts new file mode 100644 index 00000000..074171f1 --- /dev/null +++ b/openai-agents/tracing/src/workflows.ts @@ -0,0 +1,20 @@ +import { Agent, tool } from '@openai/agents-core'; +import { TemporalOpenAIRunner } from '@temporalio/openai-agents/workflow'; +import z from 'zod'; + +export async function tracedAgent(prompt: string): Promise { + const addTool = tool({ + name: 'add', + description: 'Add two numbers together.', + parameters: z.object({ a: z.number().describe('First number'), b: z.number().describe('Second number') }), + execute: async ({ a, b }) => String(a + b), + }); + + const agent = new Agent({ + name: 'TracedAgent', + instructions: 'You are a math assistant. Use the add tool to compute sums.', + tools: [addTool], + }); + const result = await new TemporalOpenAIRunner().run(agent, prompt); + return result.finalOutput ?? ''; +} diff --git a/openai-agents/tracing/tsconfig.json b/openai-agents/tracing/tsconfig.json new file mode 100644 index 00000000..2b4d0034 --- /dev/null +++ b/openai-agents/tracing/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ead6e129..7d5a5564 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,7 +252,7 @@ importers: version: 3.0.2 '@modelcontextprotocol/sdk': specifier: ^1.10.2 - version: 1.25.1(hono@4.11.1)(zod@3.25.76) + version: 1.25.1(hono@4.12.25)(zod@3.25.76) '@temporalio/activity': specifier: ^1.18.1 version: 1.18.1 @@ -1224,7 +1224,7 @@ importers: version: 10.45.2(@trpc/server@10.45.2) '@trpc/next': specifier: ^10.0.0-rc.8 - version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@15.1.6(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@15.1.6(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@trpc/react-query': specifier: ^10.0.0-rc.8 version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1312,7 +1312,7 @@ importers: version: 10.45.2(@trpc/server@10.45.2) '@trpc/next': specifier: ^10.0.0-rc.8 - version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@15.1.6(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@15.1.6(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@trpc/react-query': specifier: ^10.0.0-rc.8 version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2722,6 +2722,1124 @@ importers: specifier: ^5.6.3 version: 5.7.3 + openai-agents/agent-patterns: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/basic: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/customer-service: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/handoffs: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/hosted-mcp: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/human-approval: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(zod@4.4.3) + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/model-providers: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/nexus-tools: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/nexus': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + nexus-rpc: + specifier: ^0.0.2 + version: 0.0.2 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/reasoning-content: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/research-bot: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/sessions: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/tools: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + + openai-agents/tracing: + dependencies: + '@openai/agents-core': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': + specifier: ^0.11.6 + version: 0.11.6(ws@8.18.0)(zod@4.4.3) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@temporalio/activity': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/client': + specifier: ^1.18.0 + version: 1.18.1 + '@temporalio/openai-agents': + specifier: ^1.18.0 + version: 1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3)) + '@temporalio/worker': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': + specifier: ^1.18.0 + version: 1.18.1 + nanoid: + specifier: 3.x + version: 3.3.8 + openai: + specifier: ^6.0.0 + version: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@opentelemetry/sdk-trace-base': + specifier: ^1.30.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@temporalio/testing': + specifier: ^1.18.0 + version: 1.18.1(@swc/helpers@0.5.15) + '@tsconfig/node22': + specifier: ^22.0.0 + version: 22.0.5 + '@types/mocha': + specifier: 8.x + version: 8.2.3 + '@types/node': + specifier: ^22.9.1 + version: 22.12.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.0 + version: 8.22.0(@typescript-eslint/parser@8.22.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.18.0 + version: 8.22.0(eslint@8.57.1)(typescript@5.7.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-deprecation: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.57.1)(typescript@5.7.3) + mocha: + specifier: 8.x + version: 8.4.0(ts-node@10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.10.11(@swc/helpers@0.5.15))(@types/node@22.12.0)(typescript@5.7.3) + typescript: + specifier: ^5.6.3 + version: 5.7.3 + patching-api: dependencies: '@temporalio/activity': @@ -5319,6 +6437,12 @@ packages: peerDependencies: react: '>= 16 || ^19.0.0-rc' + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hono/node-server@1.19.7': resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} engines: {node: '>=18.14.1'} @@ -5390,183 +6514,155 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -5887,6 +6983,16 @@ packages: '@cfworker/json-schema': optional: true + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@nestjs/axios@3.1.2': resolution: {integrity: sha512-pFlfi4ZQsZtTNNhvgssbxjCHUd1nMpV3sXy/xOOB2uEJhw3M8j8SFR08gjFNil2we2Har7VCsXLfCkwbMHECFQ==} peerDependencies: @@ -6005,56 +7111,48 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@16.2.6': resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.1.6': resolution: {integrity: sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.1.6': resolution: {integrity: sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.1.6': resolution: {integrity: sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.1.6': resolution: {integrity: sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==} @@ -6116,6 +7214,19 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true + '@openai/agents-core@0.11.6': + resolution: {integrity: sha512-jWeo4mF+zjp9R80OPg+9prAnwF53dIpok2ymp9OkC3DpK2qcqBO8CfoEgocNp+E5HXXFW4uvCaY0olNCCcmTFw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@openai/agents-openai@0.11.6': + resolution: {integrity: sha512-63zXy+EPmg3WErLO12jLEjCTqGBZ/f0ApsW/t5wpccqUYCtImIT6IYZDgGM+zSEafHNGJmNOwGtEqHjz/Pyl+A==} + peerDependencies: + zod: ^4.0.0 + '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} @@ -6796,61 +7907,51 @@ packages: resolution: {integrity: sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.32.1': resolution: {integrity: sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.32.1': resolution: {integrity: sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.32.1': resolution: {integrity: sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.32.1': resolution: {integrity: sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.32.1': resolution: {integrity: sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.32.1': resolution: {integrity: sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.32.1': resolution: {integrity: sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.32.1': resolution: {integrity: sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.32.1': resolution: {integrity: sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.32.1': resolution: {integrity: sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==} @@ -7001,28 +8102,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.10.11': resolution: {integrity: sha512-2mMscXe/ivq8c4tO3eQSbQDFBvagMJGlalXCspn0DgDImLYTEnt/8KHMUMGVfh0gMJTZ9q4FlGLo7mlnbx99MQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.10.11': resolution: {integrity: sha512-eu2apgDbC4xwsigpl6LS+iyw6a3mL6kB4I+6PZMbFF2nIb1Dh7RGnu70Ai6mMn1o80fTmRSKsCT3CKMfVdeNFg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.10.11': resolution: {integrity: sha512-0n+wPWpDigwqRay4IL2JIvAqSKCXv6nKxPig9M7+epAlEQlqX+8Oq/Ap3yHtuhjNPb7HmnqNJLCXT1Wx+BZo0w==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.10.11': resolution: {integrity: sha512-7+bMSIoqcbXKosIVd314YjckDRPneA4OpG1cb3/GrkQTEDXmWT3pFBBlJf82hzJfw7b6lfv6rDVEFBX7/PJoLA==} @@ -7173,6 +8270,18 @@ packages: '@temporalio/workflow': 1.18.1 webpack: ^5.106.2 + '@temporalio/openai-agents@1.18.1': + resolution: {integrity: sha512-/t8lWy1Ae/h9Pvb2THGmgAo87trQZ7uHtR5A2QfjRnF3GqtIDhZ3Cfy6DBBMxkrGX24l9AAt/yDoyYIHX/q2uw==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@openai/agents-core': ~0.11.6 + '@openai/agents-openai': ~0.11.6 + '@opentelemetry/sdk-trace-base': ^1.25.1 + openai: ^6.35.0 + peerDependenciesMeta: + '@opentelemetry/sdk-trace-base': + optional: true + '@temporalio/plugin@1.18.1': resolution: {integrity: sha512-sIAZtpSjVgkbfLqhTaJAu1rWxDQ/y5CNpu3v9atJagfGQAA7HF8974JI2bfTtL5OozhcclYbXOo9AciELFsuAA==} engines: {node: '>= 20.0.0'} @@ -7743,6 +8852,9 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@vanilla-extract/babel-plugin-debug-ids@1.2.0': resolution: {integrity: sha512-z5nx2QBnOhvmlmBKeRX5sPVLz437wV30u+GJL+Hzj1rGiJYVNvgIIlzUpRNjVQ0MgAgiQIqIUbqPnmMc6HmDlQ==} @@ -10003,6 +11115,12 @@ packages: peerDependencies: express: '>= 4.11' + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.20.0: resolution: {integrity: sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==} engines: {node: '>= 0.10.0'} @@ -10515,8 +11633,8 @@ packages: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} engines: {node: '>=8'} - hono@4.11.1: - resolution: {integrity: sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==} + hono@4.12.25: + resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} engines: {node: '>=16.9.0'} hoopy@0.1.4: @@ -10738,6 +11856,10 @@ packages: resolution: {integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==} engines: {node: '>=6'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -12050,10 +13172,6 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.53.0: - resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} @@ -12525,6 +13643,17 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@6.42.0: + resolution: {integrity: sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==} + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.8.3: resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} engines: {node: '>= 0.8.0'} @@ -15573,12 +16702,20 @@ packages: peerDependencies: zod: ^3.25 || ^4 + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} @@ -16958,9 +18095,13 @@ snapshots: dependencies: react: 18.3.1 - '@hono/node-server@1.19.7(hono@4.11.1)': + '@hono/node-server@1.19.14(hono@4.12.25)': dependencies: - hono: 4.11.1 + hono: 4.12.25 + + '@hono/node-server@1.19.7(hono@4.12.25)': + dependencies: + hono: 4.12.25 '@humanwhocodes/config-array@0.13.0': dependencies: @@ -17719,9 +18860,9 @@ snapshots: - encoding - supports-color - '@modelcontextprotocol/sdk@1.25.1(hono@4.11.1)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.25.1(hono@4.12.25)(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.7(hono@4.11.1) + '@hono/node-server': 1.19.7(hono@4.12.25) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -17741,6 +18882,28 @@ snapshots: - hono - supports-color + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.25) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.25 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + '@nestjs/axios@3.1.2(@nestjs/common@10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.7.9)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.15(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -17945,6 +19108,29 @@ snapshots: transitivePeerDependencies: - encoding + '@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3)': + dependencies: + debug: 4.4.3(supports-color@5.5.0) + openai: 6.42.0(ws@8.18.0)(zod@4.4.3) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3)': + dependencies: + '@openai/agents-core': 0.11.6(ws@8.18.0)(zod@4.4.3) + debug: 4.4.3(supports-color@5.5.0) + openai: 6.42.0(ws@8.18.0)(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -19189,6 +20375,37 @@ snapshots: - supports-color - typescript + '@temporalio/openai-agents@1.18.1(@openai/agents-core@0.11.6(ws@8.18.0)(zod@4.4.3))(@openai/agents-openai@0.11.6(ws@8.18.0)(zod@4.4.3))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.15)(openai@6.42.0(ws@8.18.0)(zod@4.4.3))': + dependencies: + '@openai/agents-core': 0.11.6(ws@8.18.0)(zod@4.4.3) + '@openai/agents-openai': 0.11.6(ws@8.18.0)(zod@4.4.3) + '@opentelemetry/api': 1.9.0 + '@temporalio/activity': 1.18.1 + '@temporalio/common': 1.18.1 + '@temporalio/plugin': 1.18.1 + '@temporalio/worker': 1.18.1(@swc/helpers@0.5.15) + '@temporalio/workflow': 1.18.1 + '@ungap/structured-clone': 1.3.1 + headers-polyfill: 4.0.3 + openai: 6.42.0(ws@8.18.0)(zod@4.4.3) + web-streams-polyfill: 4.2.0 + optionalDependencies: + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/helpers' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + - webpack-cli + '@temporalio/plugin@1.18.1': {} '@temporalio/proto@1.18.1': @@ -19359,7 +20576,7 @@ snapshots: '@textlint/markdown-to-ast@12.6.1': dependencies: '@textlint/ast-node-types': 12.6.1 - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) mdast-util-gfm-autolink-literal: 0.1.3 remark-footnotes: 3.0.0 remark-frontmatter: 3.0.0 @@ -19376,7 +20593,7 @@ snapshots: dependencies: '@trpc/server': 10.45.2 - '@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@15.1.6(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@15.1.6(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@trpc/client': 10.45.2(@trpc/server@10.45.2) @@ -19796,7 +21013,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -19838,7 +21055,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.7.3 @@ -19876,7 +21093,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 tsutils: 3.21.0(typescript@5.7.3) optionalDependencies: @@ -19888,7 +21105,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.22.0(typescript@5.7.3) '@typescript-eslint/utils': 8.22.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 2.0.0(typescript@5.7.3) typescript: 5.7.3 @@ -19905,7 +21122,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -19934,7 +21151,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.22.0 '@typescript-eslint/visitor-keys': 8.22.0 - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -20000,6 +21217,8 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@ungap/structured-clone@1.3.1': {} + '@vanilla-extract/babel-plugin-debug-ids@1.2.0': dependencies: '@babel/core': 7.26.7 @@ -20978,7 +22197,7 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3(supports-color@5.5.0) - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.7.1 on-finished: 2.4.1 qs: 6.14.0 @@ -21364,7 +22583,7 @@ snapshots: compressible@2.0.18: dependencies: - mime-db: 1.53.0 + mime-db: 1.54.0 compression@1.7.5: dependencies: @@ -22824,6 +24043,11 @@ snapshots: dependencies: express: 5.2.1 + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + express@4.20.0: dependencies: accepts: 1.3.8 @@ -22911,7 +24135,7 @@ snapshots: etag: 1.8.1 finalhandler: 2.1.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 mime-types: 3.0.2 on-finished: 2.4.1 @@ -22923,7 +24147,7 @@ snapshots: router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -23061,7 +24285,7 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -23545,7 +24769,7 @@ snapshots: hexoid@1.0.0: {} - hono@4.11.1: {} + hono@4.12.25: {} hoopy@0.1.4: {} @@ -23820,6 +25044,8 @@ snapshots: transitivePeerDependencies: - supports-color + ip-address@10.2.0: {} + ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -24096,7 +25322,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -25191,7 +26417,7 @@ snapshots: jsdom@16.7.0: dependencies: abab: 2.0.6 - acorn: 8.15.0 + acorn: 8.16.0 acorn-globals: 6.0.0 cssom: 0.4.4 cssstyle: 2.3.0 @@ -25994,8 +27220,6 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.53.0: {} - mime-db@1.54.0: {} mime-types@2.1.35: @@ -26511,6 +27735,11 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@6.42.0(ws@8.18.0)(zod@4.4.3): + optionalDependencies: + ws: 8.18.0 + zod: 4.4.3 + optionator@0.8.3: dependencies: deep-is: 0.1.4 @@ -28778,7 +30007,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0 + debug: 4.4.3(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.1 formidable: 2.1.2 @@ -30384,10 +31613,16 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod@3.24.1: {} zod@3.25.76: {} + zod@4.4.3: {} + zwitch@1.0.5: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3a2996bd..91640cae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,7 @@ packages: - 'monorepo-folders/packages/*' - 'food-delivery/apps/*' - 'food-delivery/packages/*' + - 'openai-agents/*' - 'polling/*' allowBuilds: '@nestjs/core': true From d10c33d0944d22c7cc117b606bb5ab50abe79d90 Mon Sep 17 00:00:00 2001 From: maplexu Date: Wed, 17 Jun 2026 15:38:12 -0400 Subject: [PATCH 2/5] refactor down --- .github/workflows/ci.yml | 15 +---- .scripts/copy-shared-files.mjs | 3 +- .scripts/list-of-samples.json | 15 +---- .../{agent-patterns => }/.eslintignore | 0 .../{agent-patterns => }/.eslintrc.js | 24 ++++++-- openai-agents/{agent-patterns => }/.gitignore | 0 openai-agents/{agent-patterns => }/.npmrc | 0 openai-agents/{agent-patterns => }/.nvmrc | 0 .../{agent-patterns => }/.post-create | 0 .../{agent-patterns => }/.prettierignore | 0 .../{agent-patterns => }/.prettierrc | 0 openai-agents/README.md | 38 ++++++------ openai-agents/agent-patterns/README.md | 21 ++++--- openai-agents/agent-patterns/package.json | 54 ----------------- openai-agents/agent-patterns/tsconfig.json | 12 ---- openai-agents/basic/.eslintignore | 3 - openai-agents/basic/.eslintrc.js | 32 ---------- openai-agents/basic/.gitignore | 2 - openai-agents/basic/.npmrc | 1 - openai-agents/basic/.nvmrc | 1 - openai-agents/basic/.post-create | 18 ------ openai-agents/basic/.prettierignore | 1 - openai-agents/basic/.prettierrc | 2 - openai-agents/basic/README.md | 15 +++-- openai-agents/basic/package.json | 54 ----------------- openai-agents/customer-service/.eslintignore | 3 - openai-agents/customer-service/.eslintrc.js | 32 ---------- openai-agents/customer-service/.gitignore | 2 - openai-agents/customer-service/.npmrc | 1 - openai-agents/customer-service/.nvmrc | 1 - openai-agents/customer-service/.post-create | 18 ------ .../customer-service/.prettierignore | 1 - openai-agents/customer-service/.prettierrc | 2 - openai-agents/customer-service/README.md | 8 +-- openai-agents/customer-service/package.json | 54 ----------------- openai-agents/customer-service/tsconfig.json | 12 ---- openai-agents/handoffs/.eslintignore | 3 - openai-agents/handoffs/.eslintrc.js | 32 ---------- openai-agents/handoffs/.gitignore | 2 - openai-agents/handoffs/.npmrc | 1 - openai-agents/handoffs/.nvmrc | 1 - openai-agents/handoffs/.post-create | 18 ------ openai-agents/handoffs/.prettierignore | 1 - openai-agents/handoffs/.prettierrc | 2 - openai-agents/handoffs/README.md | 15 +++-- openai-agents/handoffs/package.json | 54 ----------------- openai-agents/handoffs/tsconfig.json | 12 ---- openai-agents/hosted-mcp/.eslintignore | 3 - openai-agents/hosted-mcp/.eslintrc.js | 32 ---------- openai-agents/hosted-mcp/.gitignore | 2 - openai-agents/hosted-mcp/.npmrc | 1 - openai-agents/hosted-mcp/.nvmrc | 1 - openai-agents/hosted-mcp/.post-create | 18 ------ openai-agents/hosted-mcp/.prettierignore | 1 - openai-agents/hosted-mcp/.prettierrc | 2 - openai-agents/hosted-mcp/README.md | 8 +-- openai-agents/hosted-mcp/package.json | 54 ----------------- openai-agents/hosted-mcp/tsconfig.json | 12 ---- openai-agents/human-approval/.eslintignore | 3 - openai-agents/human-approval/.eslintrc.js | 32 ---------- openai-agents/human-approval/.gitignore | 2 - openai-agents/human-approval/.npmrc | 1 - openai-agents/human-approval/.nvmrc | 1 - openai-agents/human-approval/.post-create | 18 ------ openai-agents/human-approval/.prettierignore | 1 - openai-agents/human-approval/.prettierrc | 2 - openai-agents/human-approval/README.md | 11 ++-- openai-agents/human-approval/package.json | 54 ----------------- openai-agents/human-approval/tsconfig.json | 12 ---- openai-agents/mcp/.eslintignore | 3 - openai-agents/mcp/.eslintrc.js | 32 ---------- openai-agents/mcp/.gitignore | 2 - openai-agents/mcp/.npmrc | 1 - openai-agents/mcp/.nvmrc | 1 - openai-agents/mcp/.post-create | 18 ------ openai-agents/mcp/.prettierignore | 1 - openai-agents/mcp/.prettierrc | 2 - openai-agents/mcp/README.md | 14 ++--- openai-agents/mcp/tsconfig.json | 12 ---- openai-agents/model-providers/.eslintignore | 3 - openai-agents/model-providers/.eslintrc.js | 32 ---------- openai-agents/model-providers/.gitignore | 2 - openai-agents/model-providers/.npmrc | 1 - openai-agents/model-providers/.nvmrc | 1 - openai-agents/model-providers/.post-create | 18 ------ openai-agents/model-providers/.prettierignore | 1 - openai-agents/model-providers/.prettierrc | 2 - openai-agents/model-providers/README.md | 9 ++- openai-agents/model-providers/package.json | 54 ----------------- openai-agents/model-providers/tsconfig.json | 12 ---- openai-agents/nexus-tools/.eslintignore | 3 - openai-agents/nexus-tools/.eslintrc.js | 32 ---------- openai-agents/nexus-tools/.gitignore | 2 - openai-agents/nexus-tools/.npmrc | 1 - openai-agents/nexus-tools/.nvmrc | 1 - openai-agents/nexus-tools/.post-create | 18 ------ openai-agents/nexus-tools/.prettierignore | 1 - openai-agents/nexus-tools/.prettierrc | 2 - openai-agents/nexus-tools/README.md | 8 +-- openai-agents/nexus-tools/package.json | 56 ------------------ openai-agents/nexus-tools/tsconfig.json | 12 ---- openai-agents/{mcp => }/package.json | 21 ++----- openai-agents/reasoning-content/.eslintignore | 3 - openai-agents/reasoning-content/.eslintrc.js | 32 ---------- openai-agents/reasoning-content/.gitignore | 2 - openai-agents/reasoning-content/.npmrc | 1 - openai-agents/reasoning-content/.nvmrc | 1 - openai-agents/reasoning-content/.post-create | 18 ------ .../reasoning-content/.prettierignore | 1 - openai-agents/reasoning-content/.prettierrc | 2 - openai-agents/reasoning-content/README.md | 38 +++++++----- openai-agents/reasoning-content/package.json | 53 ----------------- .../reasoning-content/src/activities.ts | 59 ++++++++++++++----- openai-agents/reasoning-content/src/client.ts | 2 +- .../src/mocha/workflows.test.ts | 30 +++++----- openai-agents/reasoning-content/tsconfig.json | 12 ---- openai-agents/research-bot/.eslintignore | 3 - openai-agents/research-bot/.eslintrc.js | 32 ---------- openai-agents/research-bot/.gitignore | 2 - openai-agents/research-bot/.npmrc | 1 - openai-agents/research-bot/.nvmrc | 1 - openai-agents/research-bot/.post-create | 18 ------ openai-agents/research-bot/.prettierignore | 1 - openai-agents/research-bot/.prettierrc | 2 - openai-agents/research-bot/README.md | 8 +-- openai-agents/research-bot/package.json | 54 ----------------- openai-agents/research-bot/tsconfig.json | 12 ---- openai-agents/sessions/.eslintignore | 3 - openai-agents/sessions/.eslintrc.js | 32 ---------- openai-agents/sessions/.gitignore | 2 - openai-agents/sessions/.npmrc | 1 - openai-agents/sessions/.nvmrc | 1 - openai-agents/sessions/.post-create | 18 ------ openai-agents/sessions/.prettierignore | 1 - openai-agents/sessions/.prettierrc | 2 - openai-agents/sessions/README.md | 13 ++-- openai-agents/sessions/package.json | 54 ----------------- openai-agents/sessions/tsconfig.json | 12 ---- openai-agents/tools/.eslintignore | 3 - openai-agents/tools/.eslintrc.js | 32 ---------- openai-agents/tools/.gitignore | 2 - openai-agents/tools/.npmrc | 1 - openai-agents/tools/.nvmrc | 1 - openai-agents/tools/.post-create | 18 ------ openai-agents/tools/.prettierignore | 1 - openai-agents/tools/.prettierrc | 2 - openai-agents/tools/README.md | 13 ++-- openai-agents/tools/package.json | 54 ----------------- openai-agents/tools/tsconfig.json | 12 ---- openai-agents/tracing/.eslintignore | 3 - openai-agents/tracing/.eslintrc.js | 32 ---------- openai-agents/tracing/.gitignore | 2 - openai-agents/tracing/.npmrc | 1 - openai-agents/tracing/.nvmrc | 1 - openai-agents/tracing/.post-create | 18 ------ openai-agents/tracing/.prettierignore | 1 - openai-agents/tracing/.prettierrc | 2 - openai-agents/tracing/README.md | 11 ++-- openai-agents/tracing/package.json | 56 ------------------ openai-agents/tracing/tsconfig.json | 12 ---- openai-agents/{basic => }/tsconfig.json | 4 +- 161 files changed, 208 insertions(+), 1836 deletions(-) rename openai-agents/{agent-patterns => }/.eslintignore (100%) rename openai-agents/{agent-patterns => }/.eslintrc.js (58%) rename openai-agents/{agent-patterns => }/.gitignore (100%) rename openai-agents/{agent-patterns => }/.npmrc (100%) rename openai-agents/{agent-patterns => }/.nvmrc (100%) rename openai-agents/{agent-patterns => }/.post-create (100%) rename openai-agents/{agent-patterns => }/.prettierignore (100%) rename openai-agents/{agent-patterns => }/.prettierrc (100%) delete mode 100644 openai-agents/agent-patterns/package.json delete mode 100644 openai-agents/agent-patterns/tsconfig.json delete mode 100644 openai-agents/basic/.eslintignore delete mode 100644 openai-agents/basic/.eslintrc.js delete mode 100644 openai-agents/basic/.gitignore delete mode 100644 openai-agents/basic/.npmrc delete mode 100644 openai-agents/basic/.nvmrc delete mode 100644 openai-agents/basic/.post-create delete mode 100644 openai-agents/basic/.prettierignore delete mode 100644 openai-agents/basic/.prettierrc delete mode 100644 openai-agents/basic/package.json delete mode 100644 openai-agents/customer-service/.eslintignore delete mode 100644 openai-agents/customer-service/.eslintrc.js delete mode 100644 openai-agents/customer-service/.gitignore delete mode 100644 openai-agents/customer-service/.npmrc delete mode 100644 openai-agents/customer-service/.nvmrc delete mode 100644 openai-agents/customer-service/.post-create delete mode 100644 openai-agents/customer-service/.prettierignore delete mode 100644 openai-agents/customer-service/.prettierrc delete mode 100644 openai-agents/customer-service/package.json delete mode 100644 openai-agents/customer-service/tsconfig.json delete mode 100644 openai-agents/handoffs/.eslintignore delete mode 100644 openai-agents/handoffs/.eslintrc.js delete mode 100644 openai-agents/handoffs/.gitignore delete mode 100644 openai-agents/handoffs/.npmrc delete mode 100644 openai-agents/handoffs/.nvmrc delete mode 100644 openai-agents/handoffs/.post-create delete mode 100644 openai-agents/handoffs/.prettierignore delete mode 100644 openai-agents/handoffs/.prettierrc delete mode 100644 openai-agents/handoffs/package.json delete mode 100644 openai-agents/handoffs/tsconfig.json delete mode 100644 openai-agents/hosted-mcp/.eslintignore delete mode 100644 openai-agents/hosted-mcp/.eslintrc.js delete mode 100644 openai-agents/hosted-mcp/.gitignore delete mode 100644 openai-agents/hosted-mcp/.npmrc delete mode 100644 openai-agents/hosted-mcp/.nvmrc delete mode 100644 openai-agents/hosted-mcp/.post-create delete mode 100644 openai-agents/hosted-mcp/.prettierignore delete mode 100644 openai-agents/hosted-mcp/.prettierrc delete mode 100644 openai-agents/hosted-mcp/package.json delete mode 100644 openai-agents/hosted-mcp/tsconfig.json delete mode 100644 openai-agents/human-approval/.eslintignore delete mode 100644 openai-agents/human-approval/.eslintrc.js delete mode 100644 openai-agents/human-approval/.gitignore delete mode 100644 openai-agents/human-approval/.npmrc delete mode 100644 openai-agents/human-approval/.nvmrc delete mode 100644 openai-agents/human-approval/.post-create delete mode 100644 openai-agents/human-approval/.prettierignore delete mode 100644 openai-agents/human-approval/.prettierrc delete mode 100644 openai-agents/human-approval/package.json delete mode 100644 openai-agents/human-approval/tsconfig.json delete mode 100644 openai-agents/mcp/.eslintignore delete mode 100644 openai-agents/mcp/.eslintrc.js delete mode 100644 openai-agents/mcp/.gitignore delete mode 100644 openai-agents/mcp/.npmrc delete mode 100644 openai-agents/mcp/.nvmrc delete mode 100644 openai-agents/mcp/.post-create delete mode 100644 openai-agents/mcp/.prettierignore delete mode 100644 openai-agents/mcp/.prettierrc delete mode 100644 openai-agents/mcp/tsconfig.json delete mode 100644 openai-agents/model-providers/.eslintignore delete mode 100644 openai-agents/model-providers/.eslintrc.js delete mode 100644 openai-agents/model-providers/.gitignore delete mode 100644 openai-agents/model-providers/.npmrc delete mode 100644 openai-agents/model-providers/.nvmrc delete mode 100644 openai-agents/model-providers/.post-create delete mode 100644 openai-agents/model-providers/.prettierignore delete mode 100644 openai-agents/model-providers/.prettierrc delete mode 100644 openai-agents/model-providers/package.json delete mode 100644 openai-agents/model-providers/tsconfig.json delete mode 100644 openai-agents/nexus-tools/.eslintignore delete mode 100644 openai-agents/nexus-tools/.eslintrc.js delete mode 100644 openai-agents/nexus-tools/.gitignore delete mode 100644 openai-agents/nexus-tools/.npmrc delete mode 100644 openai-agents/nexus-tools/.nvmrc delete mode 100644 openai-agents/nexus-tools/.post-create delete mode 100644 openai-agents/nexus-tools/.prettierignore delete mode 100644 openai-agents/nexus-tools/.prettierrc delete mode 100644 openai-agents/nexus-tools/package.json delete mode 100644 openai-agents/nexus-tools/tsconfig.json rename openai-agents/{mcp => }/package.json (77%) delete mode 100644 openai-agents/reasoning-content/.eslintignore delete mode 100644 openai-agents/reasoning-content/.eslintrc.js delete mode 100644 openai-agents/reasoning-content/.gitignore delete mode 100644 openai-agents/reasoning-content/.npmrc delete mode 100644 openai-agents/reasoning-content/.nvmrc delete mode 100644 openai-agents/reasoning-content/.post-create delete mode 100644 openai-agents/reasoning-content/.prettierignore delete mode 100644 openai-agents/reasoning-content/.prettierrc delete mode 100644 openai-agents/reasoning-content/package.json delete mode 100644 openai-agents/reasoning-content/tsconfig.json delete mode 100644 openai-agents/research-bot/.eslintignore delete mode 100644 openai-agents/research-bot/.eslintrc.js delete mode 100644 openai-agents/research-bot/.gitignore delete mode 100644 openai-agents/research-bot/.npmrc delete mode 100644 openai-agents/research-bot/.nvmrc delete mode 100644 openai-agents/research-bot/.post-create delete mode 100644 openai-agents/research-bot/.prettierignore delete mode 100644 openai-agents/research-bot/.prettierrc delete mode 100644 openai-agents/research-bot/package.json delete mode 100644 openai-agents/research-bot/tsconfig.json delete mode 100644 openai-agents/sessions/.eslintignore delete mode 100644 openai-agents/sessions/.eslintrc.js delete mode 100644 openai-agents/sessions/.gitignore delete mode 100644 openai-agents/sessions/.npmrc delete mode 100644 openai-agents/sessions/.nvmrc delete mode 100644 openai-agents/sessions/.post-create delete mode 100644 openai-agents/sessions/.prettierignore delete mode 100644 openai-agents/sessions/.prettierrc delete mode 100644 openai-agents/sessions/package.json delete mode 100644 openai-agents/sessions/tsconfig.json delete mode 100644 openai-agents/tools/.eslintignore delete mode 100644 openai-agents/tools/.eslintrc.js delete mode 100644 openai-agents/tools/.gitignore delete mode 100644 openai-agents/tools/.npmrc delete mode 100644 openai-agents/tools/.nvmrc delete mode 100644 openai-agents/tools/.post-create delete mode 100644 openai-agents/tools/.prettierignore delete mode 100644 openai-agents/tools/.prettierrc delete mode 100644 openai-agents/tools/package.json delete mode 100644 openai-agents/tools/tsconfig.json delete mode 100644 openai-agents/tracing/.eslintignore delete mode 100644 openai-agents/tracing/.eslintrc.js delete mode 100644 openai-agents/tracing/.gitignore delete mode 100644 openai-agents/tracing/.npmrc delete mode 100644 openai-agents/tracing/.nvmrc delete mode 100644 openai-agents/tracing/.post-create delete mode 100644 openai-agents/tracing/.prettierignore delete mode 100644 openai-agents/tracing/.prettierrc delete mode 100644 openai-agents/tracing/package.json delete mode 100644 openai-agents/tracing/tsconfig.json rename openai-agents/{basic => }/tsconfig.json (79%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a152f39..a06c458f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,20 +68,7 @@ jobs: timer-examples message-passing/introduction message-passing/safe-message-handlers - openai-agents/agent-patterns - openai-agents/basic - openai-agents/customer-service - openai-agents/handoffs - openai-agents/hosted-mcp - openai-agents/human-approval - openai-agents/mcp - openai-agents/model-providers - openai-agents/nexus-tools - openai-agents/reasoning-content - openai-agents/research-bot - openai-agents/sessions - openai-agents/tools - openai-agents/tracing + openai-agents polling/infrequent ) for project in "${projects[@]}"; do diff --git a/.scripts/copy-shared-files.mjs b/.scripts/copy-shared-files.mjs index 8e10d2fa..8cb67e34 100644 --- a/.scripts/copy-shared-files.mjs +++ b/.scripts/copy-shared-files.mjs @@ -13,13 +13,13 @@ const ADDITIONAL_SAMPLES = []; // as samples. const HAS_CHILD_SAMPLES = [ 'message-passing', - 'openai-agents', 'polling', ]; // Some samples have different config files from those in .shared/ // that we don't want to overwrite const TSCONFIG_EXCLUDE = [ + 'openai-agents', 'nextjs-ecommerce-oneclick', 'monorepo-folders', 'fetch-esm', @@ -43,6 +43,7 @@ const GITIGNORE_EXCLUDE = [ 'nestjs-exchange-rates', ]; const ESLINTRC_EXCLUDE = [ + 'openai-agents', 'nextjs-ecommerce-oneclick', 'monorepo-folders', 'fetch-esm', diff --git a/.scripts/list-of-samples.json b/.scripts/list-of-samples.json index 1567f751..89143a4e 100644 --- a/.scripts/list-of-samples.json +++ b/.scripts/list-of-samples.json @@ -32,6 +32,7 @@ "nexus-cancellation", "nexus-hello", "nexus-messaging", + "openai-agents", "patching-api", "production", "protobufs", @@ -54,20 +55,6 @@ "message-passing/execute-update", "message-passing/introduction", "message-passing/safe-message-handlers", - "openai-agents/agent-patterns", - "openai-agents/basic", - "openai-agents/customer-service", - "openai-agents/handoffs", - "openai-agents/hosted-mcp", - "openai-agents/human-approval", - "openai-agents/mcp", - "openai-agents/model-providers", - "openai-agents/nexus-tools", - "openai-agents/reasoning-content", - "openai-agents/research-bot", - "openai-agents/sessions", - "openai-agents/tools", - "openai-agents/tracing", "polling/infrequent" ] } \ No newline at end of file diff --git a/openai-agents/agent-patterns/.eslintignore b/openai-agents/.eslintignore similarity index 100% rename from openai-agents/agent-patterns/.eslintignore rename to openai-agents/.eslintignore diff --git a/openai-agents/agent-patterns/.eslintrc.js b/openai-agents/.eslintrc.js similarity index 58% rename from openai-agents/agent-patterns/.eslintrc.js rename to openai-agents/.eslintrc.js index 71141741..84cbb0d7 100644 --- a/openai-agents/agent-patterns/.eslintrc.js +++ b/openai-agents/.eslintrc.js @@ -1,9 +1,14 @@ 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 }, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, plugins: ['@typescript-eslint', 'deprecation'], extends: [ 'eslint:recommended', @@ -12,15 +17,26 @@ module.exports = { 'prettier', ], rules: { - '@typescript-eslint/no-floating-promises': 'error', + // 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'], - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + + // 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'], + files: ['*/src/workflows.ts', '*/src/workflows-*.ts', '*/src/workflows/*.ts'], rules: { 'no-restricted-imports': [ 'error', diff --git a/openai-agents/agent-patterns/.gitignore b/openai-agents/.gitignore similarity index 100% rename from openai-agents/agent-patterns/.gitignore rename to openai-agents/.gitignore diff --git a/openai-agents/agent-patterns/.npmrc b/openai-agents/.npmrc similarity index 100% rename from openai-agents/agent-patterns/.npmrc rename to openai-agents/.npmrc diff --git a/openai-agents/agent-patterns/.nvmrc b/openai-agents/.nvmrc similarity index 100% rename from openai-agents/agent-patterns/.nvmrc rename to openai-agents/.nvmrc diff --git a/openai-agents/agent-patterns/.post-create b/openai-agents/.post-create similarity index 100% rename from openai-agents/agent-patterns/.post-create rename to openai-agents/.post-create diff --git a/openai-agents/agent-patterns/.prettierignore b/openai-agents/.prettierignore similarity index 100% rename from openai-agents/agent-patterns/.prettierignore rename to openai-agents/.prettierignore diff --git a/openai-agents/agent-patterns/.prettierrc b/openai-agents/.prettierrc similarity index 100% rename from openai-agents/agent-patterns/.prettierrc rename to openai-agents/.prettierrc diff --git a/openai-agents/README.md b/openai-agents/README.md index b0afc0f6..d447a4a0 100644 --- a/openai-agents/README.md +++ b/openai-agents/README.md @@ -2,7 +2,7 @@ These samples use the `@temporalio/openai-agents` integration to run [OpenAI Agents SDK](https://github.com/openai/openai-agents-js) agents as Temporal Workflows. Agent orchestration — the agent loop, handoffs, tool calls, and guardrails — runs inside the Workflow, while model calls run as durable Activities, so they retry on failure and are not repeated during Workflow replay. -Each subdirectory is a standalone sample with its own `package.json` and README. The integration package itself is documented in the [`@temporalio/openai-agents` README](https://github.com/temporalio/sdk-typescript/tree/main/packages/openai-agents). +This is a single project: one `package.json` and one set of configs at the `openai-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/openai-agents` README](https://github.com/temporalio/sdk-typescript/tree/main/packages/openai-agents). ## Prerequisites @@ -11,28 +11,28 @@ These apply to every sample in this directory: - A running Temporal dev server: `temporal server start-dev`. - Node 22 or later. - An OpenAI API key: `export OPENAI_API_KEY=...`. -- Dependencies installed in the sample directory: `npm install`. +- Dependencies installed once at the `openai-agents/` root: `npm install`. -Each sample's README describes how to start its Worker and run its scenarios. +Each scenario's README describes how to start its Worker and run its scenarios by path. ## Samples -| Sample | Demonstrates | -| :------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- | -| [`basic`](./basic) | A single agent plus the core building blocks: Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. | -| [`handoffs`](./handoffs) | A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. | -| [`agent-patterns`](./agent-patterns) | Multi-agent orchestration patterns: deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. | -| [`sessions`](./sessions) | Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. | -| [`human-approval`](./human-approval) | A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. | -| [`tools`](./tools) | Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider during the model Activity. | -| [`tracing`](./tracing) | The three supported tracing paths: a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry, plus `temporal:*` orchestration spans. | -| [`model-providers`](./model-providers) | Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. | -| [`reasoning-content`](./reasoning-content) | Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. | -| [`mcp`](./mcp) | Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. | -| [`hosted-mcp`](./hosted-mcp) | A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. | -| [`research-bot`](./research-bot) | A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. | -| [`customer-service`](./customer-service) | A long-running, multi-turn Workflow driven by Updates and Queries, with triage handoffs and `continueAsNew` to bound history. | -| [`nexus-tools`](./nexus-tools) | Expose a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with `nexusOperationAsTool`. | +| Sample | Demonstrates | +| :----------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`basic`](./basic) | A single agent plus the core building blocks: Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. | +| [`handoffs`](./handoffs) | A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. | +| [`agent-patterns`](./agent-patterns) | Multi-agent orchestration patterns: deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. | +| [`sessions`](./sessions) | Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. | +| [`human-approval`](./human-approval) | A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. | +| [`tools`](./tools) | Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider during the model Activity. | +| [`tracing`](./tracing) | The three supported tracing paths: a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry, plus `temporal:*` orchestration spans. | +| [`model-providers`](./model-providers) | Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. | +| [`reasoning-content`](./reasoning-content) | Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. | +| [`mcp`](./mcp) | Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. | +| [`hosted-mcp`](./hosted-mcp) | A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. | +| [`research-bot`](./research-bot) | A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. | +| [`customer-service`](./customer-service) | A long-running, multi-turn Workflow driven by Updates and Queries, with triage handoffs and `continueAsNew` to bound history. | +| [`nexus-tools`](./nexus-tools) | Expose a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with `nexusOperationAsTool`. | ## Feature support diff --git a/openai-agents/agent-patterns/README.md b/openai-agents/agent-patterns/README.md index bc05eea0..aa1deb5e 100644 --- a/openai-agents/agent-patterns/README.md +++ b/openai-agents/agent-patterns/README.md @@ -14,26 +14,25 @@ Scenarios: ## Run -```bash -npm install -npm run build +Run these from the `openai-agents/` root (run `npm install` there once first). +```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npm start +OPENAI_API_KEY=sk-... npx ts-node agent-patterns/src/worker.ts # In another terminal, start a scenario: -npm run workflow deterministic -npm run workflow parallelization -npm run workflow llm-as-judge -npm run workflow agents-as-tools -npm run workflow input-guardrails -npm run workflow output-guardrails +npx ts-node agent-patterns/src/client.ts deterministic +npx ts-node agent-patterns/src/client.ts parallelization +npx ts-node agent-patterns/src/client.ts llm-as-judge +npx ts-node agent-patterns/src/client.ts agents-as-tools +npx ts-node agent-patterns/src/client.ts input-guardrails +npx ts-node agent-patterns/src/client.ts output-guardrails ``` ## Test ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "agent-patterns/src/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/agent-patterns/package.json b/openai-agents/agent-patterns/package.json deleted file mode 100644 index b4c0d7e8..00000000 --- a/openai-agents/agent-patterns/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-agent-patterns", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/agent-patterns/tsconfig.json b/openai-agents/agent-patterns/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/agent-patterns/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/basic/.eslintignore b/openai-agents/basic/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/basic/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/basic/.eslintrc.js b/openai-agents/basic/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/basic/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/basic/.gitignore b/openai-agents/basic/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/basic/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/basic/.npmrc b/openai-agents/basic/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/basic/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/basic/.nvmrc b/openai-agents/basic/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/basic/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/basic/.post-create b/openai-agents/basic/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/basic/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/basic/.prettierignore b/openai-agents/basic/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/basic/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/basic/.prettierrc b/openai-agents/basic/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/basic/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/basic/README.md b/openai-agents/basic/README.md index cbfd4e6e..8e31624a 100644 --- a/openai-agents/basic/README.md +++ b/openai-agents/basic/README.md @@ -17,23 +17,22 @@ Scenarios (`src/workflows.ts`): ## Run -```bash -npm install -npm run build +Run these from the `openai-agents/` root (run `npm install` there once first). +```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npm start +OPENAI_API_KEY=sk-... npx ts-node basic/src/worker.ts # In another terminal, start a scenario: -npm run workflow hello-world -npm run workflow tools -npm run workflow structured-output +npx ts-node basic/src/client.ts hello-world +npx ts-node basic/src/client.ts tools +npx ts-node basic/src/client.ts structured-output ``` ## Test ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "basic/src/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/basic/package.json b/openai-agents/basic/package.json deleted file mode 100644 index 6edd5cb4..00000000 --- a/openai-agents/basic/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-basic", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/customer-service/.eslintignore b/openai-agents/customer-service/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/customer-service/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/customer-service/.eslintrc.js b/openai-agents/customer-service/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/customer-service/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/customer-service/.gitignore b/openai-agents/customer-service/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/customer-service/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/customer-service/.npmrc b/openai-agents/customer-service/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/customer-service/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/customer-service/.nvmrc b/openai-agents/customer-service/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/customer-service/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/customer-service/.post-create b/openai-agents/customer-service/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/customer-service/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/customer-service/.prettierignore b/openai-agents/customer-service/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/customer-service/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/customer-service/.prettierrc b/openai-agents/customer-service/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/customer-service/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/customer-service/README.md b/openai-agents/customer-service/README.md index 25629c09..9cf6929f 100644 --- a/openai-agents/customer-service/README.md +++ b/openai-agents/customer-service/README.md @@ -19,17 +19,17 @@ Start the Temporal dev server: temporal server start-dev ``` -Set your OpenAI key and start the Worker: +Set your OpenAI key and start the Worker (run from the `openai-agents/` root, after `npm install` there): ``` export OPENAI_API_KEY=sk-... -npm run start +npx ts-node customer-service/src/worker.ts ``` In another shell, start the interactive chat client: ``` -npm run workflow +npx ts-node customer-service/src/client.ts ``` Type messages to chat. Type `history` to print the transcript, or `exit` to quit. @@ -37,7 +37,7 @@ Type messages to chat. Type `history` to print the transcript, or `exit` to quit ## Test ``` -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "customer-service/src/mocha/*.test.ts" ``` The test uses `TestWorkflowEnvironment`, a real Worker, and a `FakeModelProvider`, so it runs without diff --git a/openai-agents/customer-service/package.json b/openai-agents/customer-service/package.json deleted file mode 100644 index a3f60286..00000000 --- a/openai-agents/customer-service/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-customer-service", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/customer-service/tsconfig.json b/openai-agents/customer-service/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/customer-service/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/handoffs/.eslintignore b/openai-agents/handoffs/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/handoffs/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/handoffs/.eslintrc.js b/openai-agents/handoffs/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/handoffs/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/handoffs/.gitignore b/openai-agents/handoffs/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/handoffs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/handoffs/.npmrc b/openai-agents/handoffs/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/handoffs/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/handoffs/.nvmrc b/openai-agents/handoffs/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/handoffs/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/handoffs/.post-create b/openai-agents/handoffs/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/handoffs/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/handoffs/.prettierignore b/openai-agents/handoffs/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/handoffs/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/handoffs/.prettierrc b/openai-agents/handoffs/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/handoffs/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/handoffs/README.md b/openai-agents/handoffs/README.md index cb8abd7a..66c41a13 100644 --- a/openai-agents/handoffs/README.md +++ b/openai-agents/handoffs/README.md @@ -13,23 +13,22 @@ Scenarios (`src/workflows.ts`): ## Run -```bash -npm install -npm run build +Run these from the `openai-agents/` root (run `npm install` there once first). +```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npm start +OPENAI_API_KEY=sk-... npx ts-node handoffs/src/worker.ts # In another terminal, start a scenario: -npm run workflow agent-handoffs -npm run workflow handoff-function -npm run workflow handoff-with-filter +npx ts-node handoffs/src/client.ts agent-handoffs +npx ts-node handoffs/src/client.ts handoff-function +npx ts-node handoffs/src/client.ts handoff-with-filter ``` ## Test ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "handoffs/src/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/handoffs/package.json b/openai-agents/handoffs/package.json deleted file mode 100644 index 750349ad..00000000 --- a/openai-agents/handoffs/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-handoffs", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/handoffs/tsconfig.json b/openai-agents/handoffs/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/handoffs/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/hosted-mcp/.eslintignore b/openai-agents/hosted-mcp/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/hosted-mcp/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.eslintrc.js b/openai-agents/hosted-mcp/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/hosted-mcp/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/hosted-mcp/.gitignore b/openai-agents/hosted-mcp/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/hosted-mcp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.npmrc b/openai-agents/hosted-mcp/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/hosted-mcp/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.nvmrc b/openai-agents/hosted-mcp/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/hosted-mcp/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/hosted-mcp/.post-create b/openai-agents/hosted-mcp/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/hosted-mcp/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/hosted-mcp/.prettierignore b/openai-agents/hosted-mcp/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/hosted-mcp/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/hosted-mcp/.prettierrc b/openai-agents/hosted-mcp/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/hosted-mcp/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/hosted-mcp/README.md b/openai-agents/hosted-mcp/README.md index 559d87d5..0f072f8d 100644 --- a/openai-agents/hosted-mcp/README.md +++ b/openai-agents/hosted-mcp/README.md @@ -37,19 +37,19 @@ Workflow runs to completion). temporal server start-dev ``` -2. In another terminal, start the worker: +2. In another terminal, start the worker (run from the `openai-agents/` root, after `npm install` there): ```sh export OPENAI_API_KEY=sk-... - npm run start.watch + npx ts-node hosted-mcp/src/worker.ts ``` 3. In a third terminal, run a workflow: ```sh export OPENAI_API_KEY=sk-... - npm run workflow simple - npm run workflow approval + npx ts-node hosted-mcp/src/client.ts simple + npx ts-node hosted-mcp/src/client.ts approval ``` The `approval` client starts the Workflow and then sends the `approvalDecision` Signal with `true` diff --git a/openai-agents/hosted-mcp/package.json b/openai-agents/hosted-mcp/package.json deleted file mode 100644 index 5fe4612c..00000000 --- a/openai-agents/hosted-mcp/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-hosted-mcp", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/hosted-mcp/tsconfig.json b/openai-agents/hosted-mcp/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/hosted-mcp/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/human-approval/.eslintignore b/openai-agents/human-approval/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/human-approval/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/human-approval/.eslintrc.js b/openai-agents/human-approval/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/human-approval/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/human-approval/.gitignore b/openai-agents/human-approval/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/human-approval/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/human-approval/.npmrc b/openai-agents/human-approval/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/human-approval/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/human-approval/.nvmrc b/openai-agents/human-approval/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/human-approval/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/human-approval/.post-create b/openai-agents/human-approval/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/human-approval/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/human-approval/.prettierignore b/openai-agents/human-approval/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/human-approval/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/human-approval/.prettierrc b/openai-agents/human-approval/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/human-approval/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/human-approval/README.md b/openai-agents/human-approval/README.md index 67c4c09a..3f193831 100644 --- a/openai-agents/human-approval/README.md +++ b/openai-agents/human-approval/README.md @@ -15,21 +15,20 @@ Flow (`src/workflows.ts`): ## Run -```bash -npm install -npm run build +Run these from the `openai-agents/` root (run `npm install` there once first). +```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npm start +OPENAI_API_KEY=sk-... npx ts-node human-approval/src/worker.ts # In another terminal, start the workflow (the client sends the approval Signal): -npm run workflow +npx ts-node human-approval/src/client.ts ``` ## Test ```bash -npm test +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` with a scripted fake model, so no diff --git a/openai-agents/human-approval/package.json b/openai-agents/human-approval/package.json deleted file mode 100644 index 2f61848a..00000000 --- a/openai-agents/human-approval/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-human-approval", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/human-approval/tsconfig.json b/openai-agents/human-approval/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/human-approval/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/mcp/.eslintignore b/openai-agents/mcp/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/mcp/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/mcp/.eslintrc.js b/openai-agents/mcp/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/mcp/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/mcp/.gitignore b/openai-agents/mcp/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/mcp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/mcp/.npmrc b/openai-agents/mcp/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/mcp/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/mcp/.nvmrc b/openai-agents/mcp/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/mcp/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/mcp/.post-create b/openai-agents/mcp/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/mcp/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/mcp/.prettierignore b/openai-agents/mcp/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/mcp/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/mcp/.prettierrc b/openai-agents/mcp/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/mcp/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/mcp/README.md b/openai-agents/mcp/README.md index 00a7ef0c..503fc009 100644 --- a/openai-agents/mcp/README.md +++ b/openai-agents/mcp/README.md @@ -29,19 +29,19 @@ network calls, and drive the bundled MCP servers locally to assert that tool res temporal server start-dev ``` -2. In another terminal, start the worker: +2. In another terminal, start the worker (run from the `openai-agents/` root, after `npm install` there): ```sh export OPENAI_API_KEY=sk-... - npm run start.watch + npx ts-node mcp/src/worker.ts ``` 3. In a third terminal, run a workflow: ```sh - npm run workflow filesystem - npm run workflow streamable-http - npm run workflow sse - npm run workflow prompt-server - npm run workflow stateful-memory + npx ts-node mcp/src/client.ts filesystem + npx ts-node mcp/src/client.ts streamable-http + npx ts-node mcp/src/client.ts sse + npx ts-node mcp/src/client.ts prompt-server + npx ts-node mcp/src/client.ts stateful-memory ``` diff --git a/openai-agents/mcp/tsconfig.json b/openai-agents/mcp/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/mcp/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/model-providers/.eslintignore b/openai-agents/model-providers/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/model-providers/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/model-providers/.eslintrc.js b/openai-agents/model-providers/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/model-providers/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/model-providers/.gitignore b/openai-agents/model-providers/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/model-providers/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/model-providers/.npmrc b/openai-agents/model-providers/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/model-providers/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/model-providers/.nvmrc b/openai-agents/model-providers/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/model-providers/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/model-providers/.post-create b/openai-agents/model-providers/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/model-providers/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/model-providers/.prettierignore b/openai-agents/model-providers/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/model-providers/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/model-providers/.prettierrc b/openai-agents/model-providers/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/model-providers/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/model-providers/README.md b/openai-agents/model-providers/README.md index bd16f0e9..b6a02549 100644 --- a/openai-agents/model-providers/README.md +++ b/openai-agents/model-providers/README.md @@ -31,15 +31,14 @@ export OPENAI_API_KEY=sk-or-... export OPENAI_MODEL=meta-llama/llama-3.1-8b-instruct ``` -Then start the Worker and run the Workflow: +Then start the Worker and run the Workflow (run from the `openai-agents/` root, after `npm install` there): ```bash -npm install -npm run start.watch +npx ts-node model-providers/src/worker.ts ``` ```bash -npm run workflow "Say hello in one sentence." +npx ts-node model-providers/src/client.ts "Say hello in one sentence." ``` `OPENAI_MODEL` is forwarded to the run via `runConfig.model` and resolved by your custom provider. @@ -51,5 +50,5 @@ and assert the agent run is handled by the injected provider, including resolvin model name. This is the same injection point the live custom-provider setup uses. ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "model-providers/src/mocha/*.test.ts" ``` diff --git a/openai-agents/model-providers/package.json b/openai-agents/model-providers/package.json deleted file mode 100644 index d529ff56..00000000 --- a/openai-agents/model-providers/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-model-providers", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/model-providers/tsconfig.json b/openai-agents/model-providers/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/model-providers/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/nexus-tools/.eslintignore b/openai-agents/nexus-tools/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/nexus-tools/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/nexus-tools/.eslintrc.js b/openai-agents/nexus-tools/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/nexus-tools/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/nexus-tools/.gitignore b/openai-agents/nexus-tools/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/nexus-tools/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/nexus-tools/.npmrc b/openai-agents/nexus-tools/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/nexus-tools/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/nexus-tools/.nvmrc b/openai-agents/nexus-tools/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/nexus-tools/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/nexus-tools/.post-create b/openai-agents/nexus-tools/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/nexus-tools/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/nexus-tools/.prettierignore b/openai-agents/nexus-tools/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/nexus-tools/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/nexus-tools/.prettierrc b/openai-agents/nexus-tools/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/nexus-tools/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/nexus-tools/README.md b/openai-agents/nexus-tools/README.md index 7a6d5a8f..21f31abc 100644 --- a/openai-agents/nexus-tools/README.md +++ b/openai-agents/nexus-tools/README.md @@ -19,24 +19,24 @@ Start the Temporal dev server: temporal server start-dev ``` -Set your OpenAI key and start the Worker: +Set your OpenAI key and start the Worker (run from the `openai-agents/` root, after `npm install` there): ``` export OPENAI_API_KEY=sk-... -npm run start +npx ts-node nexus-tools/src/worker.ts ``` In another shell, run the Workflow. The client creates the Nexus endpoint if needed, then starts the Workflow (optionally pass a prompt): ``` -npm run workflow "What is the weather in Tokyo?" +npx ts-node nexus-tools/src/client.ts "What is the weather in Tokyo?" ``` ## Test ``` -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "nexus-tools/src/mocha/*.test.ts" ``` The test uses `TestWorkflowEnvironment`, `env.createNexusEndpoint(...)`, a real Worker, and a diff --git a/openai-agents/nexus-tools/package.json b/openai-agents/nexus-tools/package.json deleted file mode 100644 index 6de9f6f8..00000000 --- a/openai-agents/nexus-tools/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "temporal-openai-agents-nexus-tools", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/nexus": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nexus-rpc": "^0.0.2", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/nexus-tools/tsconfig.json b/openai-agents/nexus-tools/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/nexus-tools/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/mcp/package.json b/openai-agents/package.json similarity index 77% rename from openai-agents/mcp/package.json rename to openai-agents/package.json index b6a7fb1e..dd1327d0 100644 --- a/openai-agents/mcp/package.json +++ b/openai-agents/package.json @@ -1,5 +1,5 @@ { - "name": "temporal-openai-agents-mcp", + "name": "temporal-openai-agents", "version": "0.1.0", "private": true, "scripts": { @@ -8,30 +8,21 @@ "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] + "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/nexus": "^1.18.0", "@temporalio/openai-agents": "^1.18.0", "@temporalio/worker": "^1.18.0", "@temporalio/workflow": "^1.18.0", "@openai/agents-core": "^0.11.6", "@openai/agents-openai": "^0.11.6", "@modelcontextprotocol/sdk": "^1.29.0", + "@opentelemetry/api": "^1.9.0", "openai": "^6.0.0", + "nexus-rpc": "^0.0.2", "nanoid": "3.x", "zod": "^4.0.0" }, @@ -42,11 +33,11 @@ "@types/node": "^22.9.1", "@typescript-eslint/eslint-plugin": "^8.18.0", "@typescript-eslint/parser": "^8.18.0", + "@opentelemetry/sdk-trace-base": "^1.30.0", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "mocha": "8.x", - "nodemon": "^3.1.7", "prettier": "^3.4.2", "ts-node": "^10.9.2", "typescript": "^5.6.3", diff --git a/openai-agents/reasoning-content/.eslintignore b/openai-agents/reasoning-content/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/reasoning-content/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/reasoning-content/.eslintrc.js b/openai-agents/reasoning-content/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/reasoning-content/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/reasoning-content/.gitignore b/openai-agents/reasoning-content/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/reasoning-content/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/reasoning-content/.npmrc b/openai-agents/reasoning-content/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/reasoning-content/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/reasoning-content/.nvmrc b/openai-agents/reasoning-content/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/reasoning-content/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/reasoning-content/.post-create b/openai-agents/reasoning-content/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/reasoning-content/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/reasoning-content/.prettierignore b/openai-agents/reasoning-content/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/reasoning-content/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/reasoning-content/.prettierrc b/openai-agents/reasoning-content/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/reasoning-content/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/reasoning-content/README.md b/openai-agents/reasoning-content/README.md index 804baa74..76fc7d2c 100644 --- a/openai-agents/reasoning-content/README.md +++ b/openai-agents/reasoning-content/README.md @@ -1,12 +1,23 @@ # OpenAI Agents: Reasoning Content -Extracts a reasoning model's **reasoning content** alongside its final answer. Unlike the other +Extracts a reasoning model's **reasoning summary** alongside its final answer. Unlike the other samples, this one does **not** use `TemporalOpenAIRunner`. An Activity calls the `openai` SDK -directly with a reasoning-capable model and reads the non-standard `reasoning_content` field from -the response; the Workflow runs that Activity and returns both the reasoning and the answer. +directly using the Responses API with `reasoning: { summary: 'auto' }`, reads the reasoning summary +from the `reasoning` item in the response `output`, and returns it together with the final answer; +the Workflow runs that Activity and returns both. + +Note that OpenAI returns a reasoning **summary**, not the model's raw chain-of-thought. The +returned field is named `reasoningContent` to match the sample name. Mirrors the Python `openai_agents/reasoning_content` sample. +## Prerequisites + +Reasoning summaries are only returned to **verified OpenAI organizations**. If your organization is +not verified, the Responses API returns an HTTP 400 with `Your organization must be verified to +generate reasoning summaries`. Verify your organization at + before running this sample. + ## Run Start a Temporal dev server: @@ -15,34 +26,29 @@ Start a Temporal dev server: temporal server start-dev ``` -The default model is `deepseek-reasoner`, which returns `reasoning_content`. Point the `openai` -client at a compatible endpoint: +The default model is `gpt-5.5`. Set your OpenAI credentials (override the model with `OPENAI_MODEL` +if you want a different reasoning-capable model): ```bash -export OPENAI_BASE_URL=https://api.deepseek.com export OPENAI_API_KEY=sk-... -export OPENAI_MODEL=deepseek-reasoner ``` -Then start the Worker and run the Workflow: +Then start the Worker and run the Workflow (run from the `openai-agents/` root, after `npm install` there): ```bash -npm install -npm run start.watch +npx ts-node reasoning-content/src/worker.ts ``` ```bash -npm run workflow "What is the square root of 841? Please explain your reasoning." +npx ts-node reasoning-content/src/client.ts "What is the square root of 841? Please explain your reasoning." ``` -Not every model returns reasoning content; use one that does (such as `deepseek-reasoner`). - ## Test The test runs fully offline by overriding the Activity's `openai` client factory with a stub via -`setReasoningClientFactory`. It asserts the Workflow returns the reasoning and content the stub -provides, and that the prompt and model were forwarded to the client. +`setReasoningClientFactory`. It asserts the Workflow returns the reasoning summary and content the +stub provides, and that the prompt and model were forwarded to the client. ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "reasoning-content/src/mocha/*.test.ts" ``` diff --git a/openai-agents/reasoning-content/package.json b/openai-agents/reasoning-content/package.json deleted file mode 100644 index 92d3610e..00000000 --- a/openai-agents/reasoning-content/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "temporal-openai-agents-reasoning-content", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/reasoning-content/src/activities.ts b/openai-agents/reasoning-content/src/activities.ts index 582d1832..9801d8d2 100644 --- a/openai-agents/reasoning-content/src/activities.ts +++ b/openai-agents/reasoning-content/src/activities.ts @@ -5,13 +5,30 @@ export interface ReasoningResponse { content: string | null; } +interface ReasoningOutputItem { + type: 'reasoning'; + summary: { text: string }[]; +} + +type MessageContentPart = + | { type: 'output_text'; text: string } + | { type: 'refusal'; refusal: string }; + +interface MessageOutputItem { + type: 'message'; + content: MessageContentPart[]; +} + +type ResponseOutputItem = ReasoningOutputItem | MessageOutputItem | { type: string }; + export interface ReasoningClient { - chat: { - completions: { - create(body: { model: string; messages: { role: 'system' | 'user'; content: string }[] }): Promise<{ - choices: { message: { content: string | null; reasoning_content?: string | null } }[]; - }>; - }; + responses: { + create(body: { + model: string; + instructions: string; + input: string; + reasoning: { summary: 'auto' | 'concise' | 'detailed' }; + }): Promise<{ output: ResponseOutputItem[]; output_text?: string }>; }; } @@ -23,17 +40,31 @@ export function setReasoningClientFactory(factory: () => ReasoningClient): void export async function getReasoningResponse(prompt: string, model: string): Promise { const client = clientFactory(); - const completion = await client.chat.completions.create({ + const response = await client.responses.create({ model, - messages: [ - { role: 'system', content: 'You are a helpful assistant that explains your reasoning step by step.' }, - { role: 'user', content: prompt }, - ], + instructions: 'You are a helpful assistant that explains your reasoning step by step.', + input: prompt, + reasoning: { summary: 'auto' }, }); - const message = completion.choices[0]?.message; + const reasoningSummaries: string[] = []; + let content: string | null = null; + for (const item of response.output) { + if (item.type === 'reasoning') { + for (const part of (item as ReasoningOutputItem).summary) { + reasoningSummaries.push(part.text); + } + } else if (item.type === 'message') { + for (const part of (item as MessageOutputItem).content) { + if (part.type === 'output_text' && typeof part.text === 'string') { + content = (content ?? '') + part.text; + } + } + } + } + return { - reasoningContent: message?.reasoning_content ?? null, - content: message?.content ?? null, + reasoningContent: reasoningSummaries.length > 0 ? reasoningSummaries.join('\n\n') : null, + content: content ?? response.output_text ?? null, }; } diff --git a/openai-agents/reasoning-content/src/client.ts b/openai-agents/reasoning-content/src/client.ts index a7da7a98..e912d233 100644 --- a/openai-agents/reasoning-content/src/client.ts +++ b/openai-agents/reasoning-content/src/client.ts @@ -8,7 +8,7 @@ async function run() { } const prompt = process.argv[2] ?? 'What is the square root of 841? Please explain your reasoning.'; - const model = process.env.OPENAI_MODEL ?? 'deepseek-reasoner'; + const model = process.env.OPENAI_MODEL ?? 'gpt-5.5'; const connection = await Connection.connect(); const client = new Client({ connection }); diff --git a/openai-agents/reasoning-content/src/mocha/workflows.test.ts b/openai-agents/reasoning-content/src/mocha/workflows.test.ts index 0b2f6662..03f7dfa6 100644 --- a/openai-agents/reasoning-content/src/mocha/workflows.test.ts +++ b/openai-agents/reasoning-content/src/mocha/workflows.test.ts @@ -22,21 +22,21 @@ describe('openai-agents/reasoning-content', function () { const taskQueue = 'test-reasoning-content'; const seenRequests: { model: string; prompt: string }[] = []; activities.setReasoningClientFactory(() => ({ - chat: { - completions: { - create: async (body) => { - seenRequests.push({ model: body.model, prompt: body.messages[body.messages.length - 1]!.content }); - return { - choices: [ - { - message: { - reasoning_content: 'sqrt(841): 29 * 29 = 841, so the answer is 29.', - content: 'The square root of 841 is 29.', - }, - }, - ], - }; - }, + responses: { + create: async (body) => { + seenRequests.push({ model: body.model, prompt: body.input }); + return { + output: [ + { + type: 'reasoning', + summary: [{ text: 'sqrt(841): 29 * 29 = 841, so the answer is 29.' }], + }, + { + type: 'message', + content: [{ type: 'output_text', text: 'The square root of 841 is 29.' }], + }, + ], + }; }, }, })); diff --git a/openai-agents/reasoning-content/tsconfig.json b/openai-agents/reasoning-content/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/reasoning-content/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/research-bot/.eslintignore b/openai-agents/research-bot/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/research-bot/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/research-bot/.eslintrc.js b/openai-agents/research-bot/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/research-bot/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/research-bot/.gitignore b/openai-agents/research-bot/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/research-bot/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/research-bot/.npmrc b/openai-agents/research-bot/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/research-bot/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/research-bot/.nvmrc b/openai-agents/research-bot/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/research-bot/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/research-bot/.post-create b/openai-agents/research-bot/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/research-bot/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/research-bot/.prettierignore b/openai-agents/research-bot/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/research-bot/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/research-bot/.prettierrc b/openai-agents/research-bot/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/research-bot/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/research-bot/README.md b/openai-agents/research-bot/README.md index 8c5a3549..8b923264 100644 --- a/openai-agents/research-bot/README.md +++ b/openai-agents/research-bot/README.md @@ -16,23 +16,23 @@ Start the Temporal dev server: temporal server start-dev ``` -Set your OpenAI key and start the Worker: +Set your OpenAI key and start the Worker (run from the `openai-agents/` root, after `npm install` there): ``` export OPENAI_API_KEY=sk-... -npm run start +npx ts-node research-bot/src/worker.ts ``` In another shell, start the Workflow (optionally pass a query): ``` -npm run workflow "Caribbean surfing in April" +npx ts-node research-bot/src/client.ts "Caribbean surfing in April" ``` ## Test ``` -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "research-bot/src/mocha/*.test.ts" ``` The test uses `TestWorkflowEnvironment`, a real Worker, and a `FakeModelProvider`, so it runs without diff --git a/openai-agents/research-bot/package.json b/openai-agents/research-bot/package.json deleted file mode 100644 index 4be19e9e..00000000 --- a/openai-agents/research-bot/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-research-bot", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/research-bot/tsconfig.json b/openai-agents/research-bot/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/research-bot/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/sessions/.eslintignore b/openai-agents/sessions/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/sessions/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/sessions/.eslintrc.js b/openai-agents/sessions/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/sessions/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/sessions/.gitignore b/openai-agents/sessions/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/sessions/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/sessions/.npmrc b/openai-agents/sessions/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/sessions/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/sessions/.nvmrc b/openai-agents/sessions/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/sessions/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/sessions/.post-create b/openai-agents/sessions/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/sessions/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/sessions/.prettierignore b/openai-agents/sessions/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/sessions/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/sessions/.prettierrc b/openai-agents/sessions/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/sessions/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/sessions/README.md b/openai-agents/sessions/README.md index eaf30cff..51da2613 100644 --- a/openai-agents/sessions/README.md +++ b/openai-agents/sessions/README.md @@ -11,22 +11,21 @@ Scenarios (`src/workflows.ts`): ## Run -```bash -npm install -npm run build +Run these from the `openai-agents/` root (run `npm install` there once first). +```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npm start +OPENAI_API_KEY=sk-... npx ts-node sessions/src/worker.ts # In another terminal, start a scenario: -npm run workflow multi-turn-chat -npm run workflow carryover-chat +npx ts-node sessions/src/client.ts multi-turn-chat +npx ts-node sessions/src/client.ts carryover-chat ``` ## Test ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "sessions/src/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/sessions/package.json b/openai-agents/sessions/package.json deleted file mode 100644 index e072b904..00000000 --- a/openai-agents/sessions/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-sessions", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/sessions/tsconfig.json b/openai-agents/sessions/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/sessions/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/tools/.eslintignore b/openai-agents/tools/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/tools/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/tools/.eslintrc.js b/openai-agents/tools/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/tools/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/tools/.gitignore b/openai-agents/tools/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/tools/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/tools/.npmrc b/openai-agents/tools/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/tools/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/tools/.nvmrc b/openai-agents/tools/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/tools/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/tools/.post-create b/openai-agents/tools/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/tools/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/tools/.prettierignore b/openai-agents/tools/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/tools/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/tools/.prettierrc b/openai-agents/tools/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/tools/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/tools/README.md b/openai-agents/tools/README.md index 1dcf9573..b53b64a9 100644 --- a/openai-agents/tools/README.md +++ b/openai-agents/tools/README.md @@ -18,20 +18,19 @@ Start a Temporal dev server: temporal server start-dev ``` -In one shell, start the Worker (a real `OPENAI_API_KEY` is required for hosted tools to fire): +In one shell, start the Worker (run from the `openai-agents/` root, after `npm install` there; a real `OPENAI_API_KEY` is required for hosted tools to fire): ```bash export OPENAI_API_KEY=sk-... -npm install -npm run start.watch +npx ts-node tools/src/worker.ts ``` In another shell, run a scenario: ```bash -npm run workflow web-search -npm run workflow image-generation -npm run workflow code-interpreter +npx ts-node tools/src/client.ts web-search +npx ts-node tools/src/client.ts image-generation +npx ts-node tools/src/client.ts code-interpreter ``` Hosted tools only execute against the live OpenAI API. With a real key, the agent calls the tool @@ -44,5 +43,5 @@ model call, the fake provider cannot exercise them; instead each test asserts th the hosted tool into the model request and completes with a scripted response. ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "tools/src/mocha/*.test.ts" ``` diff --git a/openai-agents/tools/package.json b/openai-agents/tools/package.json deleted file mode 100644 index 017878be..00000000 --- a/openai-agents/tools/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "temporal-openai-agents-tools", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21" - } -} diff --git a/openai-agents/tools/tsconfig.json b/openai-agents/tools/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/tools/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/tracing/.eslintignore b/openai-agents/tracing/.eslintignore deleted file mode 100644 index 7bd99a41..00000000 --- a/openai-agents/tracing/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.eslintrc.js \ No newline at end of file diff --git a/openai-agents/tracing/.eslintrc.js b/openai-agents/tracing/.eslintrc.js deleted file mode 100644 index 71141741..00000000 --- a/openai-agents/tracing/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -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: { - '@typescript-eslint/no-floating-promises': 'error', - 'deprecation/deprecation': 'warn', - 'object-shorthand': ['error', 'always'], - '@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/openai-agents/tracing/.gitignore b/openai-agents/tracing/.gitignore deleted file mode 100644 index a9f4ed54..00000000 --- a/openai-agents/tracing/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib -node_modules \ No newline at end of file diff --git a/openai-agents/tracing/.npmrc b/openai-agents/tracing/.npmrc deleted file mode 100644 index 9cf94950..00000000 --- a/openai-agents/tracing/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/openai-agents/tracing/.nvmrc b/openai-agents/tracing/.nvmrc deleted file mode 100644 index 2bd5a0a9..00000000 --- a/openai-agents/tracing/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/openai-agents/tracing/.post-create b/openai-agents/tracing/.post-create deleted file mode 100644 index 055c11e9..00000000 --- a/openai-agents/tracing/.post-create +++ /dev/null @@ -1,18 +0,0 @@ -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/openai-agents/tracing/.prettierignore b/openai-agents/tracing/.prettierignore deleted file mode 100644 index 7951405f..00000000 --- a/openai-agents/tracing/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/openai-agents/tracing/.prettierrc b/openai-agents/tracing/.prettierrc deleted file mode 100644 index 965d50bf..00000000 --- a/openai-agents/tracing/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -printWidth: 120 -singleQuote: true diff --git a/openai-agents/tracing/README.md b/openai-agents/tracing/README.md index 7490fc75..4c89b38d 100644 --- a/openai-agents/tracing/README.md +++ b/openai-agents/tracing/README.md @@ -7,7 +7,7 @@ not duplicate spans. The Worker selects a tracing mode (default `custom`): ```bash -npm run start.watch -- # mode: custom | openai | otel +npx ts-node tracing/src/worker.ts # mode: custom | openai | otel # or set TRACING_MODE= ``` @@ -50,14 +50,15 @@ the optional peer dependency `@opentelemetry/sdk-trace-base`. temporal server start-dev ``` +Run these from the `openai-agents/` root (run `npm install` there once first). + ```bash export OPENAI_API_KEY=sk-... -npm install -npm run start.watch -- custom +npx ts-node tracing/src/worker.ts custom ``` ```bash -npm run workflow "What is 42 plus 58?" +npx ts-node tracing/src/client.ts "What is 42 plus 58?" ``` ## Test @@ -66,5 +67,5 @@ The test runs offline with a `FakeModelProvider`. It registers a custom `Tracing agent that calls a function tool, and asserts that `agent` and `function` spans were emitted. ```bash -npm test +npx mocha --exit --require ts-node/register --require source-map-support/register "tracing/src/mocha/*.test.ts" ``` diff --git a/openai-agents/tracing/package.json b/openai-agents/tracing/package.json deleted file mode 100644 index d28de95e..00000000 --- a/openai-agents/tracing/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "temporal-openai-agents-tracing", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "tsc --build", - "build.watch": "tsc --build --watch", - "format": "prettier --write .", - "format:check": "prettier --check .", - "lint": "eslint .", - "start": "ts-node src/worker.ts", - "start.watch": "nodemon src/worker.ts", - "workflow": "ts-node src/client.ts", - "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.18.0", - "@temporalio/client": "^1.18.0", - "@temporalio/openai-agents": "^1.18.0", - "@temporalio/worker": "^1.18.0", - "@temporalio/workflow": "^1.18.0", - "@openai/agents-core": "^0.11.6", - "@openai/agents-openai": "^0.11.6", - "openai": "^6.0.0", - "nanoid": "3.x", - "zod": "^4.0.0", - "@opentelemetry/api": "^1.9.0" - }, - "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", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "source-map-support": "^0.5.21", - "@opentelemetry/sdk-trace-base": "^1.30.0" - } -} diff --git a/openai-agents/tracing/tsconfig.json b/openai-agents/tracing/tsconfig.json deleted file mode 100644 index 2b4d0034..00000000 --- a/openai-agents/tracing/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "lib": ["es2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": ["src/**/*.ts"] -} diff --git a/openai-agents/basic/tsconfig.json b/openai-agents/tsconfig.json similarity index 79% rename from openai-agents/basic/tsconfig.json rename to openai-agents/tsconfig.json index 2b4d0034..da8a0e08 100644 --- a/openai-agents/basic/tsconfig.json +++ b/openai-agents/tsconfig.json @@ -5,8 +5,8 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "rootDir": "./src", + "rootDir": ".", "outDir": "./lib" }, - "include": ["src/**/*.ts"] + "include": ["*/src/**/*.ts"] } From 348c9f66c49493bdb57c8ffa1a7259ca564e9b3e Mon Sep 17 00:00:00 2001 From: maplexu Date: Tue, 23 Jun 2026 13:16:44 -0400 Subject: [PATCH 3/5] AI-266: Restructure openai-agents samples to standard single-src layout Move each scenario from openai-agents//src/ to openai-agents/src//, letting the package use the shared tsconfig (removed from TSCONFIG_EXCLUDE). Rename two scenarios to feature-based names: research-bot -> multi-agent, customer-service -> stateful-conversation. Add an Ollama start snippet to the model-providers README. --- .scripts/copy-shared-files.mjs | 1 - openai-agents/.eslintrc.js | 2 +- openai-agents/README.md | 28 +++++++++---------- openai-agents/package.json | 2 +- .../{ => src}/agent-patterns/README.md | 18 ++++++------ .../src => src/agent-patterns}/client.ts | 0 .../agent-patterns}/mocha/fake-model.ts | 0 .../agent-patterns}/mocha/workflows.test.ts | 0 .../src => src/agent-patterns}/worker.ts | 0 .../src => src/agent-patterns}/workflows.ts | 0 openai-agents/{ => src}/basic/README.md | 12 ++++---- .../{basic/src => src/basic}/activities.ts | 0 .../{basic/src => src/basic}/client.ts | 0 .../src => src/basic}/mocha/fake-model.ts | 0 .../src => src/basic}/mocha/workflows.test.ts | 0 .../{basic/src => src/basic}/worker.ts | 0 .../{basic/src => src/basic}/workflows.ts | 0 openai-agents/{ => src}/handoffs/README.md | 12 ++++---- .../src => src/handoffs}/activities.ts | 0 .../{handoffs/src => src/handoffs}/client.ts | 0 .../src => src/handoffs}/mocha/fake-model.ts | 0 .../handoffs}/mocha/workflows.test.ts | 0 .../{handoffs/src => src/handoffs}/worker.ts | 0 .../src => src/handoffs}/workflows.ts | 0 openai-agents/{ => src}/hosted-mcp/README.md | 6 ++-- .../src => src/hosted-mcp}/client.ts | 0 .../hosted-mcp}/mocha/fake-model.ts | 0 .../hosted-mcp}/mocha/workflows.test.ts | 0 .../src => src/hosted-mcp}/worker.ts | 0 .../src => src/hosted-mcp}/workflows.ts | 0 .../{ => src}/human-approval/README.md | 8 +++--- .../src => src/human-approval}/activities.ts | 0 .../src => src/human-approval}/client.ts | 0 .../human-approval}/mocha/fake-model.ts | 0 .../human-approval}/mocha/workflows.test.ts | 0 .../src => src/human-approval}/worker.ts | 0 .../src => src/human-approval}/workflows.ts | 0 openai-agents/{ => src}/mcp/README.md | 24 ++++++++-------- .../{mcp/src => src/mcp}/activities.ts | 0 openai-agents/{mcp/src => src/mcp}/client.ts | 0 .../src => src/mcp}/mocha/fake-model.ts | 0 .../src => src/mcp}/mocha/workflows.test.ts | 0 .../mcp}/servers/filesystem-server.ts | 0 .../src => src/mcp}/servers/notes-server.ts | 0 .../src => src/mcp}/servers/prompt-server.ts | 0 .../mcp}/servers/sample-files/hello.txt | 0 .../mcp}/servers/sample-files/notes.txt | 0 .../src => src/mcp}/servers/sse-server.ts | 0 .../src => src/mcp}/servers/tools-server.ts | 0 openai-agents/{mcp/src => src/mcp}/worker.ts | 0 .../{mcp/src => src/mcp}/workflows.ts | 0 .../{ => src}/model-providers/README.md | 13 +++++++-- .../src => src/model-providers}/client.ts | 0 .../model-providers}/mocha/fake-model.ts | 0 .../model-providers}/mocha/workflows.test.ts | 0 .../src => src/model-providers}/worker.ts | 0 .../src => src/model-providers}/workflows.ts | 0 .../multi-agent}/README.md | 8 +++--- .../src => src/multi-agent}/client.ts | 4 +-- .../multi-agent}/mocha/fake-model.ts | 0 .../multi-agent}/mocha/workflows.test.ts | 6 ++-- .../src => src/multi-agent}/worker.ts | 2 +- .../src => src/multi-agent}/workflows.ts | 0 openai-agents/{ => src}/nexus-tools/README.md | 12 ++++---- .../src => src/nexus-tools}/api.ts | 0 .../src => src/nexus-tools}/client.ts | 0 .../src => src/nexus-tools}/handler.ts | 0 .../nexus-tools}/mocha/fake-model.ts | 0 .../nexus-tools}/mocha/workflows.test.ts | 0 .../src => src/nexus-tools}/worker.ts | 0 .../src => src/nexus-tools}/workflows.ts | 0 .../{ => src}/reasoning-content/README.md | 6 ++-- .../reasoning-content}/activities.ts | 0 .../src => src/reasoning-content}/client.ts | 0 .../mocha/workflows.test.ts | 0 .../src => src/reasoning-content}/worker.ts | 0 .../reasoning-content}/workflows.ts | 0 openai-agents/{ => src}/sessions/README.md | 10 +++---- .../src => src/sessions}/activities.ts | 0 .../{sessions/src => src/sessions}/client.ts | 0 .../src => src/sessions}/mocha/fake-model.ts | 0 .../sessions}/mocha/workflows.test.ts | 0 .../{sessions/src => src/sessions}/worker.ts | 0 .../src => src/sessions}/workflows.ts | 0 .../stateful-conversation}/README.md | 8 +++--- .../stateful-conversation}/agents.ts | 0 .../stateful-conversation}/client.ts | 8 +++--- .../mocha/fake-model.ts | 0 .../mocha/workflows.test.ts | 10 +++---- .../stateful-conversation}/worker.ts | 2 +- .../stateful-conversation}/workflows.ts | 6 ++-- openai-agents/{ => src}/tools/README.md | 10 +++---- .../{tools/src => src/tools}/client.ts | 0 .../src => src/tools}/mocha/fake-model.ts | 0 .../src => src/tools}/mocha/workflows.test.ts | 0 .../{tools/src => src/tools}/worker.ts | 0 .../{tools/src => src/tools}/workflows.ts | 0 openai-agents/{ => src}/tracing/README.md | 10 +++---- .../{tracing/src => src/tracing}/client.ts | 0 .../src => src/tracing}/mocha/fake-model.ts | 0 .../tracing}/mocha/workflows.test.ts | 0 .../tracing}/recording-processor.ts | 0 .../{tracing/src => src/tracing}/worker.ts | 0 .../{tracing/src => src/tracing}/workflows.ts | 0 openai-agents/tsconfig.json | 5 ++-- 105 files changed, 120 insertions(+), 113 deletions(-) rename openai-agents/{ => src}/agent-patterns/README.md (70%) rename openai-agents/{agent-patterns/src => src/agent-patterns}/client.ts (100%) rename openai-agents/{agent-patterns/src => src/agent-patterns}/mocha/fake-model.ts (100%) rename openai-agents/{agent-patterns/src => src/agent-patterns}/mocha/workflows.test.ts (100%) rename openai-agents/{agent-patterns/src => src/agent-patterns}/worker.ts (100%) rename openai-agents/{agent-patterns/src => src/agent-patterns}/workflows.ts (100%) rename openai-agents/{ => src}/basic/README.md (86%) rename openai-agents/{basic/src => src/basic}/activities.ts (100%) rename openai-agents/{basic/src => src/basic}/client.ts (100%) rename openai-agents/{basic/src => src/basic}/mocha/fake-model.ts (100%) rename openai-agents/{basic/src => src/basic}/mocha/workflows.test.ts (100%) rename openai-agents/{basic/src => src/basic}/worker.ts (100%) rename openai-agents/{basic/src => src/basic}/workflows.ts (100%) rename openai-agents/{ => src}/handoffs/README.md (78%) rename openai-agents/{handoffs/src => src/handoffs}/activities.ts (100%) rename openai-agents/{handoffs/src => src/handoffs}/client.ts (100%) rename openai-agents/{customer-service/src => src/handoffs}/mocha/fake-model.ts (100%) rename openai-agents/{handoffs/src => src/handoffs}/mocha/workflows.test.ts (100%) rename openai-agents/{handoffs/src => src/handoffs}/worker.ts (100%) rename openai-agents/{handoffs/src => src/handoffs}/workflows.ts (100%) rename openai-agents/{ => src}/hosted-mcp/README.md (93%) rename openai-agents/{hosted-mcp/src => src/hosted-mcp}/client.ts (100%) rename openai-agents/{handoffs/src => src/hosted-mcp}/mocha/fake-model.ts (100%) rename openai-agents/{hosted-mcp/src => src/hosted-mcp}/mocha/workflows.test.ts (100%) rename openai-agents/{hosted-mcp/src => src/hosted-mcp}/worker.ts (100%) rename openai-agents/{hosted-mcp/src => src/hosted-mcp}/workflows.ts (100%) rename openai-agents/{ => src}/human-approval/README.md (87%) rename openai-agents/{human-approval/src => src/human-approval}/activities.ts (100%) rename openai-agents/{human-approval/src => src/human-approval}/client.ts (100%) rename openai-agents/{hosted-mcp/src => src/human-approval}/mocha/fake-model.ts (100%) rename openai-agents/{human-approval/src => src/human-approval}/mocha/workflows.test.ts (100%) rename openai-agents/{human-approval/src => src/human-approval}/worker.ts (100%) rename openai-agents/{human-approval/src => src/human-approval}/workflows.ts (100%) rename openai-agents/{ => src}/mcp/README.md (66%) rename openai-agents/{mcp/src => src/mcp}/activities.ts (100%) rename openai-agents/{mcp/src => src/mcp}/client.ts (100%) rename openai-agents/{human-approval/src => src/mcp}/mocha/fake-model.ts (100%) rename openai-agents/{mcp/src => src/mcp}/mocha/workflows.test.ts (100%) rename openai-agents/{mcp/src => src/mcp}/servers/filesystem-server.ts (100%) rename openai-agents/{mcp/src => src/mcp}/servers/notes-server.ts (100%) rename openai-agents/{mcp/src => src/mcp}/servers/prompt-server.ts (100%) rename openai-agents/{mcp/src => src/mcp}/servers/sample-files/hello.txt (100%) rename openai-agents/{mcp/src => src/mcp}/servers/sample-files/notes.txt (100%) rename openai-agents/{mcp/src => src/mcp}/servers/sse-server.ts (100%) rename openai-agents/{mcp/src => src/mcp}/servers/tools-server.ts (100%) rename openai-agents/{mcp/src => src/mcp}/worker.ts (100%) rename openai-agents/{mcp/src => src/mcp}/workflows.ts (100%) rename openai-agents/{ => src}/model-providers/README.md (86%) rename openai-agents/{model-providers/src => src/model-providers}/client.ts (100%) rename openai-agents/{mcp/src => src/model-providers}/mocha/fake-model.ts (100%) rename openai-agents/{model-providers/src => src/model-providers}/mocha/workflows.test.ts (100%) rename openai-agents/{model-providers/src => src/model-providers}/worker.ts (100%) rename openai-agents/{model-providers/src => src/model-providers}/workflows.ts (100%) rename openai-agents/{research-bot => src/multi-agent}/README.md (82%) rename openai-agents/{research-bot/src => src/multi-agent}/client.ts (90%) rename openai-agents/{model-providers/src => src/multi-agent}/mocha/fake-model.ts (100%) rename openai-agents/{research-bot/src => src/multi-agent}/mocha/workflows.test.ts (94%) rename openai-agents/{research-bot/src => src/multi-agent}/worker.ts (95%) rename openai-agents/{research-bot/src => src/multi-agent}/workflows.ts (100%) rename openai-agents/{ => src}/nexus-tools/README.md (71%) rename openai-agents/{nexus-tools/src => src/nexus-tools}/api.ts (100%) rename openai-agents/{nexus-tools/src => src/nexus-tools}/client.ts (100%) rename openai-agents/{nexus-tools/src => src/nexus-tools}/handler.ts (100%) rename openai-agents/{nexus-tools/src => src/nexus-tools}/mocha/fake-model.ts (100%) rename openai-agents/{nexus-tools/src => src/nexus-tools}/mocha/workflows.test.ts (100%) rename openai-agents/{nexus-tools/src => src/nexus-tools}/worker.ts (100%) rename openai-agents/{nexus-tools/src => src/nexus-tools}/workflows.ts (100%) rename openai-agents/{ => src}/reasoning-content/README.md (91%) rename openai-agents/{reasoning-content/src => src/reasoning-content}/activities.ts (100%) rename openai-agents/{reasoning-content/src => src/reasoning-content}/client.ts (100%) rename openai-agents/{reasoning-content/src => src/reasoning-content}/mocha/workflows.test.ts (100%) rename openai-agents/{reasoning-content/src => src/reasoning-content}/worker.ts (100%) rename openai-agents/{reasoning-content/src => src/reasoning-content}/workflows.ts (100%) rename openai-agents/{ => src}/sessions/README.md (81%) rename openai-agents/{sessions/src => src/sessions}/activities.ts (100%) rename openai-agents/{sessions/src => src/sessions}/client.ts (100%) rename openai-agents/{research-bot/src => src/sessions}/mocha/fake-model.ts (100%) rename openai-agents/{sessions/src => src/sessions}/mocha/workflows.test.ts (100%) rename openai-agents/{sessions/src => src/sessions}/worker.ts (100%) rename openai-agents/{sessions/src => src/sessions}/workflows.ts (100%) rename openai-agents/{customer-service => src/stateful-conversation}/README.md (87%) rename openai-agents/{customer-service/src => src/stateful-conversation}/agents.ts (100%) rename openai-agents/{customer-service/src => src/stateful-conversation}/client.ts (83%) rename openai-agents/{sessions/src => src/stateful-conversation}/mocha/fake-model.ts (100%) rename openai-agents/{customer-service/src => src/stateful-conversation}/mocha/workflows.test.ts (86%) rename openai-agents/{customer-service/src => src/stateful-conversation}/worker.ts (95%) rename openai-agents/{customer-service/src => src/stateful-conversation}/workflows.ts (91%) rename openai-agents/{ => src}/tools/README.md (85%) rename openai-agents/{tools/src => src/tools}/client.ts (100%) rename openai-agents/{tools/src => src/tools}/mocha/fake-model.ts (100%) rename openai-agents/{tools/src => src/tools}/mocha/workflows.test.ts (100%) rename openai-agents/{tools/src => src/tools}/worker.ts (100%) rename openai-agents/{tools/src => src/tools}/workflows.ts (100%) rename openai-agents/{ => src}/tracing/README.md (86%) rename openai-agents/{tracing/src => src/tracing}/client.ts (100%) rename openai-agents/{tracing/src => src/tracing}/mocha/fake-model.ts (100%) rename openai-agents/{tracing/src => src/tracing}/mocha/workflows.test.ts (100%) rename openai-agents/{tracing/src => src/tracing}/recording-processor.ts (100%) rename openai-agents/{tracing/src => src/tracing}/worker.ts (100%) rename openai-agents/{tracing/src => src/tracing}/workflows.ts (100%) diff --git a/.scripts/copy-shared-files.mjs b/.scripts/copy-shared-files.mjs index 8cb67e34..464fbee7 100644 --- a/.scripts/copy-shared-files.mjs +++ b/.scripts/copy-shared-files.mjs @@ -19,7 +19,6 @@ const HAS_CHILD_SAMPLES = [ // Some samples have different config files from those in .shared/ // that we don't want to overwrite const TSCONFIG_EXCLUDE = [ - 'openai-agents', 'nextjs-ecommerce-oneclick', 'monorepo-folders', 'fetch-esm', diff --git a/openai-agents/.eslintrc.js b/openai-agents/.eslintrc.js index 84cbb0d7..53812fe4 100644 --- a/openai-agents/.eslintrc.js +++ b/openai-agents/.eslintrc.js @@ -36,7 +36,7 @@ module.exports = { }, overrides: [ { - files: ['*/src/workflows.ts', '*/src/workflows-*.ts', '*/src/workflows/*.ts'], + files: ['src/*/workflows.ts', 'src/*/workflows-*.ts', 'src/*/workflows/*.ts'], rules: { 'no-restricted-imports': [ 'error', diff --git a/openai-agents/README.md b/openai-agents/README.md index d447a4a0..3134e487 100644 --- a/openai-agents/README.md +++ b/openai-agents/README.md @@ -19,20 +19,20 @@ Each scenario's README describes how to start its Worker and run its scenarios b | Sample | Demonstrates | | :----------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`basic`](./basic) | A single agent plus the core building blocks: Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. | -| [`handoffs`](./handoffs) | A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. | -| [`agent-patterns`](./agent-patterns) | Multi-agent orchestration patterns: deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. | -| [`sessions`](./sessions) | Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. | -| [`human-approval`](./human-approval) | A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. | -| [`tools`](./tools) | Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider during the model Activity. | -| [`tracing`](./tracing) | The three supported tracing paths: a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry, plus `temporal:*` orchestration spans. | -| [`model-providers`](./model-providers) | Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. | -| [`reasoning-content`](./reasoning-content) | Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. | -| [`mcp`](./mcp) | Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. | -| [`hosted-mcp`](./hosted-mcp) | A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. | -| [`research-bot`](./research-bot) | A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. | -| [`customer-service`](./customer-service) | A long-running, multi-turn Workflow driven by Updates and Queries, with triage handoffs and `continueAsNew` to bound history. | -| [`nexus-tools`](./nexus-tools) | Expose a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with `nexusOperationAsTool`. | +| [`basic`](./src/basic) | A single agent plus the core building blocks: Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. | +| [`handoffs`](./src/handoffs) | A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. | +| [`agent-patterns`](./src/agent-patterns) | Multi-agent orchestration patterns: deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. | +| [`sessions`](./src/sessions) | Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. | +| [`human-approval`](./src/human-approval) | A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. | +| [`tools`](./src/tools) | Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider during the model Activity. | +| [`tracing`](./src/tracing) | The three supported tracing paths: a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry, plus `temporal:*` orchestration spans. | +| [`model-providers`](./src/model-providers) | Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. | +| [`reasoning-content`](./src/reasoning-content) | Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. | +| [`mcp`](./src/mcp) | Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. | +| [`hosted-mcp`](./src/hosted-mcp) | A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. | +| [`multi-agent`](./src/multi-agent) | A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. | +| [`stateful-conversation`](./src/stateful-conversation) | A long-running, multi-turn Workflow driven by Updates and Queries, with triage handoffs and `continueAsNew` to bound history. | +| [`nexus-tools`](./src/nexus-tools) | Expose a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with `nexusOperationAsTool`. | ## Feature support diff --git a/openai-agents/package.json b/openai-agents/package.json index dd1327d0..05117461 100644 --- a/openai-agents/package.json +++ b/openai-agents/package.json @@ -8,7 +8,7 @@ "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\"" + "test": "mocha --exit --require ts-node/register --require source-map-support/register \"src/*/mocha/*.test.ts\"" }, "dependencies": { "@temporalio/activity": "^1.18.0", diff --git a/openai-agents/agent-patterns/README.md b/openai-agents/src/agent-patterns/README.md similarity index 70% rename from openai-agents/agent-patterns/README.md rename to openai-agents/src/agent-patterns/README.md index aa1deb5e..583e6700 100644 --- a/openai-agents/agent-patterns/README.md +++ b/openai-agents/src/agent-patterns/README.md @@ -1,7 +1,7 @@ # OpenAI Agents: Agent Patterns Demonstrates common multi-agent orchestration patterns with the Temporal OpenAI Agents -integration. Each pattern is its own Workflow in `src/workflows.ts`. +integration. Each pattern is its own Workflow in `src/agent-patterns/workflows.ts`. Scenarios: @@ -18,21 +18,21 @@ Run these from the `openai-agents/` root (run `npm install` there once first). ```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npx ts-node agent-patterns/src/worker.ts +OPENAI_API_KEY=sk-... npx ts-node src/agent-patterns/worker.ts # In another terminal, start a scenario: -npx ts-node agent-patterns/src/client.ts deterministic -npx ts-node agent-patterns/src/client.ts parallelization -npx ts-node agent-patterns/src/client.ts llm-as-judge -npx ts-node agent-patterns/src/client.ts agents-as-tools -npx ts-node agent-patterns/src/client.ts input-guardrails -npx ts-node agent-patterns/src/client.ts output-guardrails +npx ts-node src/agent-patterns/client.ts deterministic +npx ts-node src/agent-patterns/client.ts parallelization +npx ts-node src/agent-patterns/client.ts llm-as-judge +npx ts-node src/agent-patterns/client.ts agents-as-tools +npx ts-node src/agent-patterns/client.ts input-guardrails +npx ts-node src/agent-patterns/client.ts output-guardrails ``` ## Test ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "agent-patterns/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/agent-patterns/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/agent-patterns/src/client.ts b/openai-agents/src/agent-patterns/client.ts similarity index 100% rename from openai-agents/agent-patterns/src/client.ts rename to openai-agents/src/agent-patterns/client.ts diff --git a/openai-agents/agent-patterns/src/mocha/fake-model.ts b/openai-agents/src/agent-patterns/mocha/fake-model.ts similarity index 100% rename from openai-agents/agent-patterns/src/mocha/fake-model.ts rename to openai-agents/src/agent-patterns/mocha/fake-model.ts diff --git a/openai-agents/agent-patterns/src/mocha/workflows.test.ts b/openai-agents/src/agent-patterns/mocha/workflows.test.ts similarity index 100% rename from openai-agents/agent-patterns/src/mocha/workflows.test.ts rename to openai-agents/src/agent-patterns/mocha/workflows.test.ts diff --git a/openai-agents/agent-patterns/src/worker.ts b/openai-agents/src/agent-patterns/worker.ts similarity index 100% rename from openai-agents/agent-patterns/src/worker.ts rename to openai-agents/src/agent-patterns/worker.ts diff --git a/openai-agents/agent-patterns/src/workflows.ts b/openai-agents/src/agent-patterns/workflows.ts similarity index 100% rename from openai-agents/agent-patterns/src/workflows.ts rename to openai-agents/src/agent-patterns/workflows.ts diff --git a/openai-agents/basic/README.md b/openai-agents/src/basic/README.md similarity index 86% rename from openai-agents/basic/README.md rename to openai-agents/src/basic/README.md index 8e31624a..f8e7e8ad 100644 --- a/openai-agents/basic/README.md +++ b/openai-agents/src/basic/README.md @@ -4,7 +4,7 @@ Demonstrates the building blocks of the Temporal OpenAI Agents integration: a si different ways to give it tools, run context, structured output, per-run model overrides, and dynamic instructions — each running durably as a Temporal Workflow. -Scenarios (`src/workflows.ts`): +Scenarios (`src/basic/workflows.ts`): - **hello-world** — a single agent that returns model text, with no tools. - **tools** — an Activity-backed tool wired in with `activityAsTool` (getWeather). @@ -21,18 +21,18 @@ Run these from the `openai-agents/` root (run `npm install` there once first). ```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npx ts-node basic/src/worker.ts +OPENAI_API_KEY=sk-... npx ts-node src/basic/worker.ts # In another terminal, start a scenario: -npx ts-node basic/src/client.ts hello-world -npx ts-node basic/src/client.ts tools -npx ts-node basic/src/client.ts structured-output +npx ts-node src/basic/client.ts hello-world +npx ts-node src/basic/client.ts tools +npx ts-node src/basic/client.ts structured-output ``` ## Test ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "basic/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/basic/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/basic/src/activities.ts b/openai-agents/src/basic/activities.ts similarity index 100% rename from openai-agents/basic/src/activities.ts rename to openai-agents/src/basic/activities.ts diff --git a/openai-agents/basic/src/client.ts b/openai-agents/src/basic/client.ts similarity index 100% rename from openai-agents/basic/src/client.ts rename to openai-agents/src/basic/client.ts diff --git a/openai-agents/basic/src/mocha/fake-model.ts b/openai-agents/src/basic/mocha/fake-model.ts similarity index 100% rename from openai-agents/basic/src/mocha/fake-model.ts rename to openai-agents/src/basic/mocha/fake-model.ts diff --git a/openai-agents/basic/src/mocha/workflows.test.ts b/openai-agents/src/basic/mocha/workflows.test.ts similarity index 100% rename from openai-agents/basic/src/mocha/workflows.test.ts rename to openai-agents/src/basic/mocha/workflows.test.ts diff --git a/openai-agents/basic/src/worker.ts b/openai-agents/src/basic/worker.ts similarity index 100% rename from openai-agents/basic/src/worker.ts rename to openai-agents/src/basic/worker.ts diff --git a/openai-agents/basic/src/workflows.ts b/openai-agents/src/basic/workflows.ts similarity index 100% rename from openai-agents/basic/src/workflows.ts rename to openai-agents/src/basic/workflows.ts diff --git a/openai-agents/handoffs/README.md b/openai-agents/src/handoffs/README.md similarity index 78% rename from openai-agents/handoffs/README.md rename to openai-agents/src/handoffs/README.md index 66c41a13..917ddadf 100644 --- a/openai-agents/handoffs/README.md +++ b/openai-agents/src/handoffs/README.md @@ -4,7 +4,7 @@ Demonstrates agent handoffs with the Temporal OpenAI Agents integration. A triag each request to one of two specialist agents (billing, support), running durably as a Temporal Workflow. -Scenarios (`src/workflows.ts`): +Scenarios (`src/handoffs/workflows.ts`): - **agent-handoffs** — handoffs declared as a plain `Agent[]`. - **handoff-function** — handoffs declared with the `handoff()` form. @@ -17,18 +17,18 @@ Run these from the `openai-agents/` root (run `npm install` there once first). ```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npx ts-node handoffs/src/worker.ts +OPENAI_API_KEY=sk-... npx ts-node src/handoffs/worker.ts # In another terminal, start a scenario: -npx ts-node handoffs/src/client.ts agent-handoffs -npx ts-node handoffs/src/client.ts handoff-function -npx ts-node handoffs/src/client.ts handoff-with-filter +npx ts-node src/handoffs/client.ts agent-handoffs +npx ts-node src/handoffs/client.ts handoff-function +npx ts-node src/handoffs/client.ts handoff-with-filter ``` ## Test ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "handoffs/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/handoffs/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/handoffs/src/activities.ts b/openai-agents/src/handoffs/activities.ts similarity index 100% rename from openai-agents/handoffs/src/activities.ts rename to openai-agents/src/handoffs/activities.ts diff --git a/openai-agents/handoffs/src/client.ts b/openai-agents/src/handoffs/client.ts similarity index 100% rename from openai-agents/handoffs/src/client.ts rename to openai-agents/src/handoffs/client.ts diff --git a/openai-agents/customer-service/src/mocha/fake-model.ts b/openai-agents/src/handoffs/mocha/fake-model.ts similarity index 100% rename from openai-agents/customer-service/src/mocha/fake-model.ts rename to openai-agents/src/handoffs/mocha/fake-model.ts diff --git a/openai-agents/handoffs/src/mocha/workflows.test.ts b/openai-agents/src/handoffs/mocha/workflows.test.ts similarity index 100% rename from openai-agents/handoffs/src/mocha/workflows.test.ts rename to openai-agents/src/handoffs/mocha/workflows.test.ts diff --git a/openai-agents/handoffs/src/worker.ts b/openai-agents/src/handoffs/worker.ts similarity index 100% rename from openai-agents/handoffs/src/worker.ts rename to openai-agents/src/handoffs/worker.ts diff --git a/openai-agents/handoffs/src/workflows.ts b/openai-agents/src/handoffs/workflows.ts similarity index 100% rename from openai-agents/handoffs/src/workflows.ts rename to openai-agents/src/handoffs/workflows.ts diff --git a/openai-agents/hosted-mcp/README.md b/openai-agents/src/hosted-mcp/README.md similarity index 93% rename from openai-agents/hosted-mcp/README.md rename to openai-agents/src/hosted-mcp/README.md index 0f072f8d..5c3288c4 100644 --- a/openai-agents/hosted-mcp/README.md +++ b/openai-agents/src/hosted-mcp/README.md @@ -41,15 +41,15 @@ Workflow runs to completion). ```sh export OPENAI_API_KEY=sk-... - npx ts-node hosted-mcp/src/worker.ts + npx ts-node src/hosted-mcp/worker.ts ``` 3. In a third terminal, run a workflow: ```sh export OPENAI_API_KEY=sk-... - npx ts-node hosted-mcp/src/client.ts simple - npx ts-node hosted-mcp/src/client.ts approval + npx ts-node src/hosted-mcp/client.ts simple + npx ts-node src/hosted-mcp/client.ts approval ``` The `approval` client starts the Workflow and then sends the `approvalDecision` Signal with `true` diff --git a/openai-agents/hosted-mcp/src/client.ts b/openai-agents/src/hosted-mcp/client.ts similarity index 100% rename from openai-agents/hosted-mcp/src/client.ts rename to openai-agents/src/hosted-mcp/client.ts diff --git a/openai-agents/handoffs/src/mocha/fake-model.ts b/openai-agents/src/hosted-mcp/mocha/fake-model.ts similarity index 100% rename from openai-agents/handoffs/src/mocha/fake-model.ts rename to openai-agents/src/hosted-mcp/mocha/fake-model.ts diff --git a/openai-agents/hosted-mcp/src/mocha/workflows.test.ts b/openai-agents/src/hosted-mcp/mocha/workflows.test.ts similarity index 100% rename from openai-agents/hosted-mcp/src/mocha/workflows.test.ts rename to openai-agents/src/hosted-mcp/mocha/workflows.test.ts diff --git a/openai-agents/hosted-mcp/src/worker.ts b/openai-agents/src/hosted-mcp/worker.ts similarity index 100% rename from openai-agents/hosted-mcp/src/worker.ts rename to openai-agents/src/hosted-mcp/worker.ts diff --git a/openai-agents/hosted-mcp/src/workflows.ts b/openai-agents/src/hosted-mcp/workflows.ts similarity index 100% rename from openai-agents/hosted-mcp/src/workflows.ts rename to openai-agents/src/hosted-mcp/workflows.ts diff --git a/openai-agents/human-approval/README.md b/openai-agents/src/human-approval/README.md similarity index 87% rename from openai-agents/human-approval/README.md rename to openai-agents/src/human-approval/README.md index 3f193831..249e331a 100644 --- a/openai-agents/human-approval/README.md +++ b/openai-agents/src/human-approval/README.md @@ -4,7 +4,7 @@ Demonstrates a human-in-the-loop approval flow with the Temporal OpenAI Agents i marked `needsApproval` pauses the agent run with an interruption; the Workflow waits for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. -Flow (`src/workflows.ts`): +Flow (`src/human-approval/workflows.ts`): 1. The agent requests a `dangerousAction` tool call; because the tool sets `needsApproval`, the run returns with `result.interruptions`. @@ -19,16 +19,16 @@ Run these from the `openai-agents/` root (run `npm install` there once first). ```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npx ts-node human-approval/src/worker.ts +OPENAI_API_KEY=sk-... npx ts-node src/human-approval/worker.ts # In another terminal, start the workflow (the client sends the approval Signal): -npx ts-node human-approval/src/client.ts +npx ts-node src/human-approval/client.ts ``` ## Test ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "human-approval/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/human-approval/mocha/*.test.ts" ``` The test runs a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/human-approval/src/activities.ts b/openai-agents/src/human-approval/activities.ts similarity index 100% rename from openai-agents/human-approval/src/activities.ts rename to openai-agents/src/human-approval/activities.ts diff --git a/openai-agents/human-approval/src/client.ts b/openai-agents/src/human-approval/client.ts similarity index 100% rename from openai-agents/human-approval/src/client.ts rename to openai-agents/src/human-approval/client.ts diff --git a/openai-agents/hosted-mcp/src/mocha/fake-model.ts b/openai-agents/src/human-approval/mocha/fake-model.ts similarity index 100% rename from openai-agents/hosted-mcp/src/mocha/fake-model.ts rename to openai-agents/src/human-approval/mocha/fake-model.ts diff --git a/openai-agents/human-approval/src/mocha/workflows.test.ts b/openai-agents/src/human-approval/mocha/workflows.test.ts similarity index 100% rename from openai-agents/human-approval/src/mocha/workflows.test.ts rename to openai-agents/src/human-approval/mocha/workflows.test.ts diff --git a/openai-agents/human-approval/src/worker.ts b/openai-agents/src/human-approval/worker.ts similarity index 100% rename from openai-agents/human-approval/src/worker.ts rename to openai-agents/src/human-approval/worker.ts diff --git a/openai-agents/human-approval/src/workflows.ts b/openai-agents/src/human-approval/workflows.ts similarity index 100% rename from openai-agents/human-approval/src/workflows.ts rename to openai-agents/src/human-approval/workflows.ts diff --git a/openai-agents/mcp/README.md b/openai-agents/src/mcp/README.md similarity index 66% rename from openai-agents/mcp/README.md rename to openai-agents/src/mcp/README.md index 503fc009..ba8b2eee 100644 --- a/openai-agents/mcp/README.md +++ b/openai-agents/src/mcp/README.md @@ -7,14 +7,14 @@ itself — so no external MCP service is required. Five scenarios are included: -- `filesystem` — stateless MCP over stdio; a bundled stdio server (`src/servers/filesystem-server.ts`) - exposes `listFiles`/`readFile` over `src/servers/sample-files/`. -- `streamable-http` — stateless MCP over a localhost Streamable-HTTP server (`src/servers/tools-server.ts`) +- `filesystem` — stateless MCP over stdio; a bundled stdio server (`src/mcp/servers/filesystem-server.ts`) + exposes `listFiles`/`readFile` over `src/mcp/servers/sample-files/`. +- `streamable-http` — stateless MCP over a localhost Streamable-HTTP server (`src/mcp/servers/tools-server.ts`) with `add`/`getWeather`/`getSecret` tools. -- `sse` — stateless MCP over a localhost SSE server (`src/servers/sse-server.ts`) with the same tools. -- `prompt-server` — stateless MCP server (`src/servers/prompt-server.ts`) exposing a `summarize` +- `sse` — stateless MCP over a localhost SSE server (`src/mcp/servers/sse-server.ts`) with the same tools. +- `prompt-server` — stateless MCP server (`src/mcp/servers/prompt-server.ts`) exposing a `summarize` prompt; the workflow fetches the prompt and uses it as the agent's instructions. -- `stateful-memory` — a `StatefulMCPServerProvider` notes server (`src/servers/notes-server.ts`) with +- `stateful-memory` — a `StatefulMCPServerProvider` notes server (`src/mcp/servers/notes-server.ts`) with `saveNote`/`listNotes`/`readNote`; the workflow calls `connect()`/`cleanup()` to keep server state for the run. @@ -33,15 +33,15 @@ network calls, and drive the bundled MCP servers locally to assert that tool res ```sh export OPENAI_API_KEY=sk-... - npx ts-node mcp/src/worker.ts + npx ts-node src/mcp/worker.ts ``` 3. In a third terminal, run a workflow: ```sh - npx ts-node mcp/src/client.ts filesystem - npx ts-node mcp/src/client.ts streamable-http - npx ts-node mcp/src/client.ts sse - npx ts-node mcp/src/client.ts prompt-server - npx ts-node mcp/src/client.ts stateful-memory + npx ts-node src/mcp/client.ts filesystem + npx ts-node src/mcp/client.ts streamable-http + npx ts-node src/mcp/client.ts sse + npx ts-node src/mcp/client.ts prompt-server + npx ts-node src/mcp/client.ts stateful-memory ``` diff --git a/openai-agents/mcp/src/activities.ts b/openai-agents/src/mcp/activities.ts similarity index 100% rename from openai-agents/mcp/src/activities.ts rename to openai-agents/src/mcp/activities.ts diff --git a/openai-agents/mcp/src/client.ts b/openai-agents/src/mcp/client.ts similarity index 100% rename from openai-agents/mcp/src/client.ts rename to openai-agents/src/mcp/client.ts diff --git a/openai-agents/human-approval/src/mocha/fake-model.ts b/openai-agents/src/mcp/mocha/fake-model.ts similarity index 100% rename from openai-agents/human-approval/src/mocha/fake-model.ts rename to openai-agents/src/mcp/mocha/fake-model.ts diff --git a/openai-agents/mcp/src/mocha/workflows.test.ts b/openai-agents/src/mcp/mocha/workflows.test.ts similarity index 100% rename from openai-agents/mcp/src/mocha/workflows.test.ts rename to openai-agents/src/mcp/mocha/workflows.test.ts diff --git a/openai-agents/mcp/src/servers/filesystem-server.ts b/openai-agents/src/mcp/servers/filesystem-server.ts similarity index 100% rename from openai-agents/mcp/src/servers/filesystem-server.ts rename to openai-agents/src/mcp/servers/filesystem-server.ts diff --git a/openai-agents/mcp/src/servers/notes-server.ts b/openai-agents/src/mcp/servers/notes-server.ts similarity index 100% rename from openai-agents/mcp/src/servers/notes-server.ts rename to openai-agents/src/mcp/servers/notes-server.ts diff --git a/openai-agents/mcp/src/servers/prompt-server.ts b/openai-agents/src/mcp/servers/prompt-server.ts similarity index 100% rename from openai-agents/mcp/src/servers/prompt-server.ts rename to openai-agents/src/mcp/servers/prompt-server.ts diff --git a/openai-agents/mcp/src/servers/sample-files/hello.txt b/openai-agents/src/mcp/servers/sample-files/hello.txt similarity index 100% rename from openai-agents/mcp/src/servers/sample-files/hello.txt rename to openai-agents/src/mcp/servers/sample-files/hello.txt diff --git a/openai-agents/mcp/src/servers/sample-files/notes.txt b/openai-agents/src/mcp/servers/sample-files/notes.txt similarity index 100% rename from openai-agents/mcp/src/servers/sample-files/notes.txt rename to openai-agents/src/mcp/servers/sample-files/notes.txt diff --git a/openai-agents/mcp/src/servers/sse-server.ts b/openai-agents/src/mcp/servers/sse-server.ts similarity index 100% rename from openai-agents/mcp/src/servers/sse-server.ts rename to openai-agents/src/mcp/servers/sse-server.ts diff --git a/openai-agents/mcp/src/servers/tools-server.ts b/openai-agents/src/mcp/servers/tools-server.ts similarity index 100% rename from openai-agents/mcp/src/servers/tools-server.ts rename to openai-agents/src/mcp/servers/tools-server.ts diff --git a/openai-agents/mcp/src/worker.ts b/openai-agents/src/mcp/worker.ts similarity index 100% rename from openai-agents/mcp/src/worker.ts rename to openai-agents/src/mcp/worker.ts diff --git a/openai-agents/mcp/src/workflows.ts b/openai-agents/src/mcp/workflows.ts similarity index 100% rename from openai-agents/mcp/src/workflows.ts rename to openai-agents/src/mcp/workflows.ts diff --git a/openai-agents/model-providers/README.md b/openai-agents/src/model-providers/README.md similarity index 86% rename from openai-agents/model-providers/README.md rename to openai-agents/src/model-providers/README.md index b6a02549..57d53222 100644 --- a/openai-agents/model-providers/README.md +++ b/openai-agents/src/model-providers/README.md @@ -23,6 +23,13 @@ export OPENAI_API_KEY=ollama # any non-empty value; most local servers ig export OPENAI_MODEL=llama3.2 # a model your endpoint serves ``` +Pull the model and start Ollama: + +```bash +ollama pull llama3.2 +ollama serve +``` + Or an OpenRouter endpoint: ```bash @@ -34,11 +41,11 @@ export OPENAI_MODEL=meta-llama/llama-3.1-8b-instruct Then start the Worker and run the Workflow (run from the `openai-agents/` root, after `npm install` there): ```bash -npx ts-node model-providers/src/worker.ts +npx ts-node src/model-providers/worker.ts ``` ```bash -npx ts-node model-providers/src/client.ts "Say hello in one sentence." +npx ts-node src/model-providers/client.ts "Say hello in one sentence." ``` `OPENAI_MODEL` is forwarded to the run via `runConfig.model` and resolved by your custom provider. @@ -50,5 +57,5 @@ and assert the agent run is handled by the injected provider, including resolvin model name. This is the same injection point the live custom-provider setup uses. ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "model-providers/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/model-providers/mocha/*.test.ts" ``` diff --git a/openai-agents/model-providers/src/client.ts b/openai-agents/src/model-providers/client.ts similarity index 100% rename from openai-agents/model-providers/src/client.ts rename to openai-agents/src/model-providers/client.ts diff --git a/openai-agents/mcp/src/mocha/fake-model.ts b/openai-agents/src/model-providers/mocha/fake-model.ts similarity index 100% rename from openai-agents/mcp/src/mocha/fake-model.ts rename to openai-agents/src/model-providers/mocha/fake-model.ts diff --git a/openai-agents/model-providers/src/mocha/workflows.test.ts b/openai-agents/src/model-providers/mocha/workflows.test.ts similarity index 100% rename from openai-agents/model-providers/src/mocha/workflows.test.ts rename to openai-agents/src/model-providers/mocha/workflows.test.ts diff --git a/openai-agents/model-providers/src/worker.ts b/openai-agents/src/model-providers/worker.ts similarity index 100% rename from openai-agents/model-providers/src/worker.ts rename to openai-agents/src/model-providers/worker.ts diff --git a/openai-agents/model-providers/src/workflows.ts b/openai-agents/src/model-providers/workflows.ts similarity index 100% rename from openai-agents/model-providers/src/workflows.ts rename to openai-agents/src/model-providers/workflows.ts diff --git a/openai-agents/research-bot/README.md b/openai-agents/src/multi-agent/README.md similarity index 82% rename from openai-agents/research-bot/README.md rename to openai-agents/src/multi-agent/README.md index 8b923264..5bb360c0 100644 --- a/openai-agents/research-bot/README.md +++ b/openai-agents/src/multi-agent/README.md @@ -1,4 +1,4 @@ -# OpenAI Agents: Research Bot +# OpenAI Agents: Multi-Agent A multi-agent research Workflow built with `@temporalio/openai-agents`. It mirrors the OpenAI Agents SDK research-bot sample: @@ -20,19 +20,19 @@ Set your OpenAI key and start the Worker (run from the `openai-agents/` root, af ``` export OPENAI_API_KEY=sk-... -npx ts-node research-bot/src/worker.ts +npx ts-node src/multi-agent/worker.ts ``` In another shell, start the Workflow (optionally pass a query): ``` -npx ts-node research-bot/src/client.ts "Caribbean surfing in April" +npx ts-node src/multi-agent/client.ts "Caribbean surfing in April" ``` ## Test ``` -npx mocha --exit --require ts-node/register --require source-map-support/register "research-bot/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/multi-agent/mocha/*.test.ts" ``` The test uses `TestWorkflowEnvironment`, a real Worker, and a `FakeModelProvider`, so it runs without diff --git a/openai-agents/research-bot/src/client.ts b/openai-agents/src/multi-agent/client.ts similarity index 90% rename from openai-agents/research-bot/src/client.ts rename to openai-agents/src/multi-agent/client.ts index 5e980194..e5bb8d25 100644 --- a/openai-agents/research-bot/src/client.ts +++ b/openai-agents/src/multi-agent/client.ts @@ -19,8 +19,8 @@ async function run() { }); const handle = await client.workflow.start(researchWorkflow, { - taskQueue: 'openai-agents-research-bot', - workflowId: 'openai-agents-research-' + nanoid(), + taskQueue: 'openai-agents-multi-agent', + workflowId: 'openai-agents-multi-agent-' + nanoid(), args: [query], }); diff --git a/openai-agents/model-providers/src/mocha/fake-model.ts b/openai-agents/src/multi-agent/mocha/fake-model.ts similarity index 100% rename from openai-agents/model-providers/src/mocha/fake-model.ts rename to openai-agents/src/multi-agent/mocha/fake-model.ts diff --git a/openai-agents/research-bot/src/mocha/workflows.test.ts b/openai-agents/src/multi-agent/mocha/workflows.test.ts similarity index 94% rename from openai-agents/research-bot/src/mocha/workflows.test.ts rename to openai-agents/src/multi-agent/mocha/workflows.test.ts index 5b1fe820..da05bd72 100644 --- a/openai-agents/research-bot/src/mocha/workflows.test.ts +++ b/openai-agents/src/multi-agent/mocha/workflows.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { FakeModelProvider, textResponse } from './fake-model'; import { researchWorkflow } from '../workflows'; -describe('openai-agents/research-bot workflow', function () { +describe('openai-agents/multi-agent workflow', function () { this.timeout(30_000); let testEnv: TestWorkflowEnvironment; @@ -20,7 +20,7 @@ describe('openai-agents/research-bot workflow', function () { }); it('researchWorkflow: plans searches, runs them concurrently, synthesizes a report', async () => { - const taskQueue = 'test-research-bot'; + const taskQueue = 'test-multi-agent'; const plan = { searches: [ @@ -62,7 +62,7 @@ describe('openai-agents/research-bot workflow', function () { const result = await worker.runUntil( testEnv.client.workflow.execute(researchWorkflow, { args: ['Caribbean surfing in April'], - workflowId: 'test-research-bot-' + Date.now(), + workflowId: 'test-multi-agent-' + Date.now(), taskQueue, }), ); diff --git a/openai-agents/research-bot/src/worker.ts b/openai-agents/src/multi-agent/worker.ts similarity index 95% rename from openai-agents/research-bot/src/worker.ts rename to openai-agents/src/multi-agent/worker.ts index d9cea841..b5dad450 100644 --- a/openai-agents/research-bot/src/worker.ts +++ b/openai-agents/src/multi-agent/worker.ts @@ -12,7 +12,7 @@ async function run() { try { const worker = await Worker.create({ connection, - taskQueue: 'openai-agents-research-bot', + taskQueue: 'openai-agents-multi-agent', workflowsPath: require.resolve('./workflows'), plugins: [ new OpenAIAgentsPlugin({ diff --git a/openai-agents/research-bot/src/workflows.ts b/openai-agents/src/multi-agent/workflows.ts similarity index 100% rename from openai-agents/research-bot/src/workflows.ts rename to openai-agents/src/multi-agent/workflows.ts diff --git a/openai-agents/nexus-tools/README.md b/openai-agents/src/nexus-tools/README.md similarity index 71% rename from openai-agents/nexus-tools/README.md rename to openai-agents/src/nexus-tools/README.md index 21f31abc..79a6eb64 100644 --- a/openai-agents/nexus-tools/README.md +++ b/openai-agents/src/nexus-tools/README.md @@ -7,9 +7,9 @@ back to the agent. The package defines: -- a `nexus-rpc` weather service (`src/api.ts`), -- a synchronous service handler (`src/handler.ts`), registered on the Worker via `nexusServices`, -- a Workflow whose agent uses `nexusOperationAsTool` (`src/workflows.ts`). +- a `nexus-rpc` weather service (`src/nexus-tools/api.ts`), +- a synchronous service handler (`src/nexus-tools/handler.ts`), registered on the Worker via `nexusServices`, +- a Workflow whose agent uses `nexusOperationAsTool` (`src/nexus-tools/workflows.ts`). ## Run @@ -23,20 +23,20 @@ Set your OpenAI key and start the Worker (run from the `openai-agents/` root, af ``` export OPENAI_API_KEY=sk-... -npx ts-node nexus-tools/src/worker.ts +npx ts-node src/nexus-tools/worker.ts ``` In another shell, run the Workflow. The client creates the Nexus endpoint if needed, then starts the Workflow (optionally pass a prompt): ``` -npx ts-node nexus-tools/src/client.ts "What is the weather in Tokyo?" +npx ts-node src/nexus-tools/client.ts "What is the weather in Tokyo?" ``` ## Test ``` -npx mocha --exit --require ts-node/register --require source-map-support/register "nexus-tools/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/nexus-tools/mocha/*.test.ts" ``` The test uses `TestWorkflowEnvironment`, `env.createNexusEndpoint(...)`, a real Worker, and a diff --git a/openai-agents/nexus-tools/src/api.ts b/openai-agents/src/nexus-tools/api.ts similarity index 100% rename from openai-agents/nexus-tools/src/api.ts rename to openai-agents/src/nexus-tools/api.ts diff --git a/openai-agents/nexus-tools/src/client.ts b/openai-agents/src/nexus-tools/client.ts similarity index 100% rename from openai-agents/nexus-tools/src/client.ts rename to openai-agents/src/nexus-tools/client.ts diff --git a/openai-agents/nexus-tools/src/handler.ts b/openai-agents/src/nexus-tools/handler.ts similarity index 100% rename from openai-agents/nexus-tools/src/handler.ts rename to openai-agents/src/nexus-tools/handler.ts diff --git a/openai-agents/nexus-tools/src/mocha/fake-model.ts b/openai-agents/src/nexus-tools/mocha/fake-model.ts similarity index 100% rename from openai-agents/nexus-tools/src/mocha/fake-model.ts rename to openai-agents/src/nexus-tools/mocha/fake-model.ts diff --git a/openai-agents/nexus-tools/src/mocha/workflows.test.ts b/openai-agents/src/nexus-tools/mocha/workflows.test.ts similarity index 100% rename from openai-agents/nexus-tools/src/mocha/workflows.test.ts rename to openai-agents/src/nexus-tools/mocha/workflows.test.ts diff --git a/openai-agents/nexus-tools/src/worker.ts b/openai-agents/src/nexus-tools/worker.ts similarity index 100% rename from openai-agents/nexus-tools/src/worker.ts rename to openai-agents/src/nexus-tools/worker.ts diff --git a/openai-agents/nexus-tools/src/workflows.ts b/openai-agents/src/nexus-tools/workflows.ts similarity index 100% rename from openai-agents/nexus-tools/src/workflows.ts rename to openai-agents/src/nexus-tools/workflows.ts diff --git a/openai-agents/reasoning-content/README.md b/openai-agents/src/reasoning-content/README.md similarity index 91% rename from openai-agents/reasoning-content/README.md rename to openai-agents/src/reasoning-content/README.md index 76fc7d2c..19b6978d 100644 --- a/openai-agents/reasoning-content/README.md +++ b/openai-agents/src/reasoning-content/README.md @@ -36,11 +36,11 @@ export OPENAI_API_KEY=sk-... Then start the Worker and run the Workflow (run from the `openai-agents/` root, after `npm install` there): ```bash -npx ts-node reasoning-content/src/worker.ts +npx ts-node src/reasoning-content/worker.ts ``` ```bash -npx ts-node reasoning-content/src/client.ts "What is the square root of 841? Please explain your reasoning." +npx ts-node src/reasoning-content/client.ts "What is the square root of 841? Please explain your reasoning." ``` ## Test @@ -50,5 +50,5 @@ The test runs fully offline by overriding the Activity's `openai` client factory stub provides, and that the prompt and model were forwarded to the client. ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "reasoning-content/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/reasoning-content/mocha/*.test.ts" ``` diff --git a/openai-agents/reasoning-content/src/activities.ts b/openai-agents/src/reasoning-content/activities.ts similarity index 100% rename from openai-agents/reasoning-content/src/activities.ts rename to openai-agents/src/reasoning-content/activities.ts diff --git a/openai-agents/reasoning-content/src/client.ts b/openai-agents/src/reasoning-content/client.ts similarity index 100% rename from openai-agents/reasoning-content/src/client.ts rename to openai-agents/src/reasoning-content/client.ts diff --git a/openai-agents/reasoning-content/src/mocha/workflows.test.ts b/openai-agents/src/reasoning-content/mocha/workflows.test.ts similarity index 100% rename from openai-agents/reasoning-content/src/mocha/workflows.test.ts rename to openai-agents/src/reasoning-content/mocha/workflows.test.ts diff --git a/openai-agents/reasoning-content/src/worker.ts b/openai-agents/src/reasoning-content/worker.ts similarity index 100% rename from openai-agents/reasoning-content/src/worker.ts rename to openai-agents/src/reasoning-content/worker.ts diff --git a/openai-agents/reasoning-content/src/workflows.ts b/openai-agents/src/reasoning-content/workflows.ts similarity index 100% rename from openai-agents/reasoning-content/src/workflows.ts rename to openai-agents/src/reasoning-content/workflows.ts diff --git a/openai-agents/sessions/README.md b/openai-agents/src/sessions/README.md similarity index 81% rename from openai-agents/sessions/README.md rename to openai-agents/src/sessions/README.md index 51da2613..00ce31fe 100644 --- a/openai-agents/sessions/README.md +++ b/openai-agents/src/sessions/README.md @@ -3,7 +3,7 @@ Demonstrates conversation sessions with the Temporal OpenAI Agents integration using `WorkflowSafeMemorySession`, whose history lives on the Workflow heap and is rebuilt by replay. -Scenarios (`src/workflows.ts`): +Scenarios (`src/sessions/workflows.ts`): - **multi-turn-chat** — runs several prompts over one shared session; later turns see earlier history. - **carryover-chat** — carries session history across a `continueAsNew` boundary by capturing @@ -15,17 +15,17 @@ Run these from the `openai-agents/` root (run `npm install` there once first). ```bash # In one terminal, start the Worker (requires a local Temporal server and OPENAI_API_KEY): -OPENAI_API_KEY=sk-... npx ts-node sessions/src/worker.ts +OPENAI_API_KEY=sk-... npx ts-node src/sessions/worker.ts # In another terminal, start a scenario: -npx ts-node sessions/src/client.ts multi-turn-chat -npx ts-node sessions/src/client.ts carryover-chat +npx ts-node src/sessions/client.ts multi-turn-chat +npx ts-node src/sessions/client.ts carryover-chat ``` ## Test ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "sessions/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/sessions/mocha/*.test.ts" ``` Tests run a real Worker against `TestWorkflowEnvironment` with a scripted fake model, so no diff --git a/openai-agents/sessions/src/activities.ts b/openai-agents/src/sessions/activities.ts similarity index 100% rename from openai-agents/sessions/src/activities.ts rename to openai-agents/src/sessions/activities.ts diff --git a/openai-agents/sessions/src/client.ts b/openai-agents/src/sessions/client.ts similarity index 100% rename from openai-agents/sessions/src/client.ts rename to openai-agents/src/sessions/client.ts diff --git a/openai-agents/research-bot/src/mocha/fake-model.ts b/openai-agents/src/sessions/mocha/fake-model.ts similarity index 100% rename from openai-agents/research-bot/src/mocha/fake-model.ts rename to openai-agents/src/sessions/mocha/fake-model.ts diff --git a/openai-agents/sessions/src/mocha/workflows.test.ts b/openai-agents/src/sessions/mocha/workflows.test.ts similarity index 100% rename from openai-agents/sessions/src/mocha/workflows.test.ts rename to openai-agents/src/sessions/mocha/workflows.test.ts diff --git a/openai-agents/sessions/src/worker.ts b/openai-agents/src/sessions/worker.ts similarity index 100% rename from openai-agents/sessions/src/worker.ts rename to openai-agents/src/sessions/worker.ts diff --git a/openai-agents/sessions/src/workflows.ts b/openai-agents/src/sessions/workflows.ts similarity index 100% rename from openai-agents/sessions/src/workflows.ts rename to openai-agents/src/sessions/workflows.ts diff --git a/openai-agents/customer-service/README.md b/openai-agents/src/stateful-conversation/README.md similarity index 87% rename from openai-agents/customer-service/README.md rename to openai-agents/src/stateful-conversation/README.md index 9cf6929f..c736e335 100644 --- a/openai-agents/customer-service/README.md +++ b/openai-agents/src/stateful-conversation/README.md @@ -1,4 +1,4 @@ -# OpenAI Agents: Customer Service +# OpenAI Agents: Stateful Conversation A stateful, multi-turn customer-service Workflow built with `@temporalio/openai-agents`. It mirrors the OpenAI Agents SDK airline customer-service sample: @@ -23,13 +23,13 @@ Set your OpenAI key and start the Worker (run from the `openai-agents/` root, af ``` export OPENAI_API_KEY=sk-... -npx ts-node customer-service/src/worker.ts +npx ts-node src/stateful-conversation/worker.ts ``` In another shell, start the interactive chat client: ``` -npx ts-node customer-service/src/client.ts +npx ts-node src/stateful-conversation/client.ts ``` Type messages to chat. Type `history` to print the transcript, or `exit` to quit. @@ -37,7 +37,7 @@ Type messages to chat. Type `history` to print the transcript, or `exit` to quit ## Test ``` -npx mocha --exit --require ts-node/register --require source-map-support/register "customer-service/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/stateful-conversation/mocha/*.test.ts" ``` The test uses `TestWorkflowEnvironment`, a real Worker, and a `FakeModelProvider`, so it runs without diff --git a/openai-agents/customer-service/src/agents.ts b/openai-agents/src/stateful-conversation/agents.ts similarity index 100% rename from openai-agents/customer-service/src/agents.ts rename to openai-agents/src/stateful-conversation/agents.ts diff --git a/openai-agents/customer-service/src/client.ts b/openai-agents/src/stateful-conversation/client.ts similarity index 83% rename from openai-agents/customer-service/src/client.ts rename to openai-agents/src/stateful-conversation/client.ts index dfd1fdc9..ba2987af 100644 --- a/openai-agents/customer-service/src/client.ts +++ b/openai-agents/src/stateful-conversation/client.ts @@ -2,7 +2,7 @@ import * as readline from 'node:readline'; import { Connection, Client } from '@temporalio/client'; import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; import { OpenAIProvider } from '@openai/agents-openai'; -import { customerServiceWorkflow, processUserMessage, getHistory } from './workflows'; +import { statefulConversationWorkflow, processUserMessage, getHistory } from './workflows'; import { nanoid } from 'nanoid'; async function run() { @@ -17,9 +17,9 @@ async function run() { plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })], }); - const workflowId = 'openai-agents-customer-service-' + nanoid(); - const handle = await client.workflow.start(customerServiceWorkflow, { - taskQueue: 'openai-agents-customer-service', + const workflowId = 'openai-agents-stateful-conversation-' + nanoid(); + const handle = await client.workflow.start(statefulConversationWorkflow, { + taskQueue: 'openai-agents-stateful-conversation', workflowId, }); console.log(`Started chat workflow ${workflowId}`); diff --git a/openai-agents/sessions/src/mocha/fake-model.ts b/openai-agents/src/stateful-conversation/mocha/fake-model.ts similarity index 100% rename from openai-agents/sessions/src/mocha/fake-model.ts rename to openai-agents/src/stateful-conversation/mocha/fake-model.ts diff --git a/openai-agents/customer-service/src/mocha/workflows.test.ts b/openai-agents/src/stateful-conversation/mocha/workflows.test.ts similarity index 86% rename from openai-agents/customer-service/src/mocha/workflows.test.ts rename to openai-agents/src/stateful-conversation/mocha/workflows.test.ts index 5c456e3d..0e3b0951 100644 --- a/openai-agents/customer-service/src/mocha/workflows.test.ts +++ b/openai-agents/src/stateful-conversation/mocha/workflows.test.ts @@ -4,9 +4,9 @@ import { Worker } from '@temporalio/worker'; import { OpenAIAgentsPlugin } from '@temporalio/openai-agents'; import assert from 'assert'; import { FakeModelProvider, textResponse, toolCallResponse } from './fake-model'; -import { customerServiceWorkflow, processUserMessage, getHistory } from '../workflows'; +import { statefulConversationWorkflow, processUserMessage, getHistory } from '../workflows'; -describe('openai-agents/customer-service workflow', function () { +describe('openai-agents/stateful-conversation workflow', function () { this.timeout(30_000); let testEnv: TestWorkflowEnvironment; @@ -20,7 +20,7 @@ describe('openai-agents/customer-service workflow', function () { }); it('drives two updates, hands off to a specialist, and exposes history via query', async () => { - const taskQueue = 'test-customer-service'; + const taskQueue = 'test-stateful-conversation'; // Turn 1: Triage hands off to the FAQ agent, which then answers. // Turn 2: the current agent is now FAQ, which answers directly. @@ -47,8 +47,8 @@ describe('openai-agents/customer-service workflow', function () { }); await worker.runUntil(async () => { - const handle = await testEnv.client.workflow.start(customerServiceWorkflow, { - workflowId: 'test-customer-service-' + Date.now(), + const handle = await testEnv.client.workflow.start(statefulConversationWorkflow, { + workflowId: 'test-stateful-conversation-' + Date.now(), taskQueue, }); diff --git a/openai-agents/customer-service/src/worker.ts b/openai-agents/src/stateful-conversation/worker.ts similarity index 95% rename from openai-agents/customer-service/src/worker.ts rename to openai-agents/src/stateful-conversation/worker.ts index ed28ae5e..ad8ca9c3 100644 --- a/openai-agents/customer-service/src/worker.ts +++ b/openai-agents/src/stateful-conversation/worker.ts @@ -12,7 +12,7 @@ async function run() { try { const worker = await Worker.create({ connection, - taskQueue: 'openai-agents-customer-service', + taskQueue: 'openai-agents-stateful-conversation', workflowsPath: require.resolve('./workflows'), plugins: [ new OpenAIAgentsPlugin({ diff --git a/openai-agents/customer-service/src/workflows.ts b/openai-agents/src/stateful-conversation/workflows.ts similarity index 91% rename from openai-agents/customer-service/src/workflows.ts rename to openai-agents/src/stateful-conversation/workflows.ts index 5ed5cdff..93797122 100644 --- a/openai-agents/customer-service/src/workflows.ts +++ b/openai-agents/src/stateful-conversation/workflows.ts @@ -6,14 +6,14 @@ import { initAgents, type AirlineAgentContext } from './agents'; export const processUserMessage = defineUpdate('processUserMessage'); export const getHistory = defineQuery('getHistory'); -export interface CustomerServiceState { +export interface StatefulConversationState { history: string[]; currentAgentName: string; context: AirlineAgentContext; inputItems: AgentInputItem[]; } -export async function customerServiceWorkflow(state?: CustomerServiceState): Promise { +export async function statefulConversationWorkflow(state?: StatefulConversationState): Promise { const runner = new TemporalOpenAIRunner(); const history: string[] = state?.history ?? []; @@ -56,7 +56,7 @@ export async function customerServiceWorkflow(state?: CustomerServiceState): Pro await condition(() => workflowInfo().continueAsNewSuggested); - await continueAsNew({ + await continueAsNew({ history, currentAgentName, context, diff --git a/openai-agents/tools/README.md b/openai-agents/src/tools/README.md similarity index 85% rename from openai-agents/tools/README.md rename to openai-agents/src/tools/README.md index b53b64a9..2b417664 100644 --- a/openai-agents/tools/README.md +++ b/openai-agents/src/tools/README.md @@ -22,15 +22,15 @@ In one shell, start the Worker (run from the `openai-agents/` root, after `npm i ```bash export OPENAI_API_KEY=sk-... -npx ts-node tools/src/worker.ts +npx ts-node src/tools/worker.ts ``` In another shell, run a scenario: ```bash -npx ts-node tools/src/client.ts web-search -npx ts-node tools/src/client.ts image-generation -npx ts-node tools/src/client.ts code-interpreter +npx ts-node src/tools/client.ts web-search +npx ts-node src/tools/client.ts image-generation +npx ts-node src/tools/client.ts code-interpreter ``` Hosted tools only execute against the live OpenAI API. With a real key, the agent calls the tool @@ -43,5 +43,5 @@ model call, the fake provider cannot exercise them; instead each test asserts th the hosted tool into the model request and completes with a scripted response. ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "tools/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/tools/mocha/*.test.ts" ``` diff --git a/openai-agents/tools/src/client.ts b/openai-agents/src/tools/client.ts similarity index 100% rename from openai-agents/tools/src/client.ts rename to openai-agents/src/tools/client.ts diff --git a/openai-agents/tools/src/mocha/fake-model.ts b/openai-agents/src/tools/mocha/fake-model.ts similarity index 100% rename from openai-agents/tools/src/mocha/fake-model.ts rename to openai-agents/src/tools/mocha/fake-model.ts diff --git a/openai-agents/tools/src/mocha/workflows.test.ts b/openai-agents/src/tools/mocha/workflows.test.ts similarity index 100% rename from openai-agents/tools/src/mocha/workflows.test.ts rename to openai-agents/src/tools/mocha/workflows.test.ts diff --git a/openai-agents/tools/src/worker.ts b/openai-agents/src/tools/worker.ts similarity index 100% rename from openai-agents/tools/src/worker.ts rename to openai-agents/src/tools/worker.ts diff --git a/openai-agents/tools/src/workflows.ts b/openai-agents/src/tools/workflows.ts similarity index 100% rename from openai-agents/tools/src/workflows.ts rename to openai-agents/src/tools/workflows.ts diff --git a/openai-agents/tracing/README.md b/openai-agents/src/tracing/README.md similarity index 86% rename from openai-agents/tracing/README.md rename to openai-agents/src/tracing/README.md index 4c89b38d..86001ce9 100644 --- a/openai-agents/tracing/README.md +++ b/openai-agents/src/tracing/README.md @@ -7,7 +7,7 @@ not duplicate spans. The Worker selects a tracing mode (default `custom`): ```bash -npx ts-node tracing/src/worker.ts # mode: custom | openai | otel +npx ts-node src/tracing/worker.ts # mode: custom | openai | otel # or set TRACING_MODE= ``` @@ -16,7 +16,7 @@ orchestration spans (Workflow starts, Activities, Signals, and so on) to whichev ### 1. `custom` — a custom `TracingProcessor` -Registers a `RecordingTracingProcessor` (see `src/recording-processor.ts`) via `addTraceProcessor`. +Registers a `RecordingTracingProcessor` (see `src/tracing/recording-processor.ts`) via `addTraceProcessor`. It receives every trace and span lifecycle callback, so you can record, filter, or forward spans anywhere. This is the mode exercised by the test. @@ -54,11 +54,11 @@ Run these from the `openai-agents/` root (run `npm install` there once first). ```bash export OPENAI_API_KEY=sk-... -npx ts-node tracing/src/worker.ts custom +npx ts-node src/tracing/worker.ts custom ``` ```bash -npx ts-node tracing/src/client.ts "What is 42 plus 58?" +npx ts-node src/tracing/client.ts "What is 42 plus 58?" ``` ## Test @@ -67,5 +67,5 @@ The test runs offline with a `FakeModelProvider`. It registers a custom `Tracing agent that calls a function tool, and asserts that `agent` and `function` spans were emitted. ```bash -npx mocha --exit --require ts-node/register --require source-map-support/register "tracing/src/mocha/*.test.ts" +npx mocha --exit --require ts-node/register --require source-map-support/register "src/tracing/mocha/*.test.ts" ``` diff --git a/openai-agents/tracing/src/client.ts b/openai-agents/src/tracing/client.ts similarity index 100% rename from openai-agents/tracing/src/client.ts rename to openai-agents/src/tracing/client.ts diff --git a/openai-agents/tracing/src/mocha/fake-model.ts b/openai-agents/src/tracing/mocha/fake-model.ts similarity index 100% rename from openai-agents/tracing/src/mocha/fake-model.ts rename to openai-agents/src/tracing/mocha/fake-model.ts diff --git a/openai-agents/tracing/src/mocha/workflows.test.ts b/openai-agents/src/tracing/mocha/workflows.test.ts similarity index 100% rename from openai-agents/tracing/src/mocha/workflows.test.ts rename to openai-agents/src/tracing/mocha/workflows.test.ts diff --git a/openai-agents/tracing/src/recording-processor.ts b/openai-agents/src/tracing/recording-processor.ts similarity index 100% rename from openai-agents/tracing/src/recording-processor.ts rename to openai-agents/src/tracing/recording-processor.ts diff --git a/openai-agents/tracing/src/worker.ts b/openai-agents/src/tracing/worker.ts similarity index 100% rename from openai-agents/tracing/src/worker.ts rename to openai-agents/src/tracing/worker.ts diff --git a/openai-agents/tracing/src/workflows.ts b/openai-agents/src/tracing/workflows.ts similarity index 100% rename from openai-agents/tracing/src/workflows.ts rename to openai-agents/src/tracing/workflows.ts diff --git a/openai-agents/tsconfig.json b/openai-agents/tsconfig.json index da8a0e08..488f2c62 100644 --- a/openai-agents/tsconfig.json +++ b/openai-agents/tsconfig.json @@ -1,12 +1,13 @@ { "extends": "@tsconfig/node22/tsconfig.json", + "version": "5.6.3", "compilerOptions": { "lib": ["es2021"], "declaration": true, "declarationMap": true, "sourceMap": true, - "rootDir": ".", + "rootDir": "./src", "outDir": "./lib" }, - "include": ["*/src/**/*.ts"] + "include": ["src/**/*.ts"] } From 7ca2174fcb4f847bf090ffab28d9063e6e479930 Mon Sep 17 00:00:00 2001 From: maplexu Date: Tue, 23 Jun 2026 14:14:04 -0400 Subject: [PATCH 4/5] AI-266: Reformat openai-agents README and reasoning-content to satisfy prettier --- openai-agents/README.md | 30 +++++++++---------- .../src/reasoning-content/activities.ts | 4 +-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/openai-agents/README.md b/openai-agents/README.md index 3134e487..0adea03a 100644 --- a/openai-agents/README.md +++ b/openai-agents/README.md @@ -17,22 +17,22 @@ Each scenario's README describes how to start its Worker and run its scenarios b ## Samples -| Sample | Demonstrates | -| :----------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`basic`](./src/basic) | A single agent plus the core building blocks: Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. | -| [`handoffs`](./src/handoffs) | A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. | -| [`agent-patterns`](./src/agent-patterns) | Multi-agent orchestration patterns: deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. | -| [`sessions`](./src/sessions) | Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. | -| [`human-approval`](./src/human-approval) | A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. | -| [`tools`](./src/tools) | Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider during the model Activity. | -| [`tracing`](./src/tracing) | The three supported tracing paths: a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry, plus `temporal:*` orchestration spans. | -| [`model-providers`](./src/model-providers) | Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. | -| [`reasoning-content`](./src/reasoning-content) | Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. | -| [`mcp`](./src/mcp) | Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. | -| [`hosted-mcp`](./src/hosted-mcp) | A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. | -| [`multi-agent`](./src/multi-agent) | A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. | +| Sample | Demonstrates | +| :----------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`basic`](./src/basic) | A single agent plus the core building blocks: Activity-backed and inline tools, local-Activity tools, agent context, structured output, per-run model override, and dynamic instructions. | +| [`handoffs`](./src/handoffs) | A triage agent routes each request to a specialist agent, using both the `Agent[]` and `handoff()` forms and a per-handoff input filter. | +| [`agent-patterns`](./src/agent-patterns) | Multi-agent orchestration patterns: deterministic chaining, parallelization, LLM-as-judge, agents-as-tools, and input/output guardrails. | +| [`sessions`](./src/sessions) | Conversation history with `WorkflowSafeMemorySession`, including carrying history across a `continueAsNew` boundary. | +| [`human-approval`](./src/human-approval) | A human-in-the-loop tool that pauses the run for an `approve` Signal, then resumes by serializing and rehydrating the run state across `continueAsNew`. | +| [`tools`](./src/tools) | Server-side hosted tools — web search, image generation, and code interpreter — executed by the model provider during the model Activity. | +| [`tracing`](./src/tracing) | The three supported tracing paths: a custom `TracingProcessor`, the OpenAI hosted exporter, and OpenTelemetry, plus `temporal:*` orchestration spans. | +| [`model-providers`](./src/model-providers) | Pass a custom `ModelProvider` to point an agent at any OpenAI-compatible endpoint. | +| [`reasoning-content`](./src/reasoning-content) | Read a reasoning model's `reasoning_content` field by calling the `openai` SDK directly from an Activity. | +| [`mcp`](./src/mcp) | Stateless and stateful Model Context Protocol servers (stdio, Streamable HTTP, SSE, and prompt servers) running locally. | +| [`hosted-mcp`](./src/hosted-mcp) | A `HostedMCPTool` the model calls server-side, with and without a Signal-driven approval round trip. | +| [`multi-agent`](./src/multi-agent) | A planner agent fans out concurrent web searches and a writer agent synthesizes a final report. | | [`stateful-conversation`](./src/stateful-conversation) | A long-running, multi-turn Workflow driven by Updates and Queries, with triage handoffs and `continueAsNew` to bound history. | -| [`nexus-tools`](./src/nexus-tools) | Expose a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with `nexusOperationAsTool`. | +| [`nexus-tools`](./src/nexus-tools) | Expose a [Nexus](https://docs.temporal.io/nexus) Operation as an agent tool with `nexusOperationAsTool`. | ## Feature support diff --git a/openai-agents/src/reasoning-content/activities.ts b/openai-agents/src/reasoning-content/activities.ts index 9801d8d2..5b9c5bac 100644 --- a/openai-agents/src/reasoning-content/activities.ts +++ b/openai-agents/src/reasoning-content/activities.ts @@ -10,9 +10,7 @@ interface ReasoningOutputItem { summary: { text: string }[]; } -type MessageContentPart = - | { type: 'output_text'; text: string } - | { type: 'refusal'; refusal: string }; +type MessageContentPart = { type: 'output_text'; text: string } | { type: 'refusal'; refusal: string }; interface MessageOutputItem { type: 'message'; From e403ea71cd05284b065d9cfd72a1b9f2ad1f9ba6 Mon Sep 17 00:00:00 2001 From: maplexu Date: Wed, 24 Jun 2026 12:49:04 -0400 Subject: [PATCH 5/5] AI-266: Give openai-agents a per-scenario post-create message The shared post-create pointed users at `npm run start.watch`/`npm run workflow`, which don't exist in openai-agents' per-scenario layout. Exclude it from the shared copy and ship instructions matching how the sample runs. --- .scripts/copy-shared-files.mjs | 1 + openai-agents/.post-create | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.scripts/copy-shared-files.mjs b/.scripts/copy-shared-files.mjs index 75f5fae7..b5658398 100644 --- a/.scripts/copy-shared-files.mjs +++ b/.scripts/copy-shared-files.mjs @@ -64,6 +64,7 @@ const ESLINTIGNORE_EXCLUDE = [ ]; const POST_CREATE_EXCLUDE = [ + 'openai-agents', 'schedules', 'timer-examples', 'query-subscriptions', diff --git a/openai-agents/.post-create b/openai-agents/.post-create index 055c11e9..a5b0cae2 100644 --- a/openai-agents/.post-create +++ b/openai-agents/.post-create @@ -12,7 +12,9 @@ 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: +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 npm run start.watch} -{cyan npm run workflow} +{cyan OPENAI_API_KEY= npx ts-node src/basic/worker.ts} +{cyan npx ts-node src/basic/client.ts hello-world} + +See README.md for the full list of scenarios.