diff --git a/examples/agent-protocol-poc/.env.example b/examples/agent-protocol-poc/.env.example new file mode 100644 index 000000000..3101be15a --- /dev/null +++ b/examples/agent-protocol-poc/.env.example @@ -0,0 +1,6 @@ +ANTHROPIC_API_KEY="" +TAVILY_API_KEY="" + +# When running against the ECS deployment, set this to the ALB DNS name. +# Leave blank to use the local langgraph dev server (http://localhost:2024). +RESEARCHER_URL="" diff --git a/examples/agent-protocol-poc/.gitignore b/examples/agent-protocol-poc/.gitignore new file mode 100644 index 000000000..5a23824c2 --- /dev/null +++ b/examples/agent-protocol-poc/.gitignore @@ -0,0 +1,3 @@ + +# LangGraph API +.langgraph_api diff --git a/examples/agent-protocol-poc/infra/bin/app.ts b/examples/agent-protocol-poc/infra/bin/app.ts new file mode 100644 index 000000000..5360b9376 --- /dev/null +++ b/examples/agent-protocol-poc/infra/bin/app.ts @@ -0,0 +1,11 @@ +import * as cdk from "aws-cdk-lib"; +import { ResearcherStack } from "../lib/researcher-stack.js"; + +const app = new cdk.App(); + +new ResearcherStack(app, "ResearcherStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION ?? "us-east-1", + }, +}); diff --git a/examples/agent-protocol-poc/infra/lib/researcher-stack.ts b/examples/agent-protocol-poc/infra/lib/researcher-stack.ts new file mode 100644 index 000000000..fb1462c55 --- /dev/null +++ b/examples/agent-protocol-poc/infra/lib/researcher-stack.ts @@ -0,0 +1,204 @@ +import * as cdk from "aws-cdk-lib"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as ecr from "aws-cdk-lib/aws-ecr"; +import * as ecs from "aws-cdk-lib/aws-ecs"; +import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; +import * as rds from "aws-cdk-lib/aws-rds"; +import * as elasticache from "aws-cdk-lib/aws-elasticache"; +import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; +import { Construct } from "constructs"; + +/** + * ResearcherStack + * + * Provisions the infrastructure needed to self-host a LangGraph Agent Protocol + * server (the researcher subagent) on ECS Fargate with no LangSmith dependency. + * + * Architecture: + * Supervisor (local) → ALB → ECS Fargate (langgraph-api) → RDS Postgres + ElastiCache Redis + * + * Deploy steps: + * 1. cdk deploy — provisions ECR repo and all infra; note the EcrRepositoryUri output + * 2. langgraph build -t researcher -c examples/agent-protocol-poc/langgraph.json + * 3. docker tag researcher :latest && docker push :latest + * 4. aws ecs update-service --cluster --service --force-new-deployment + * 5. Set RESEARCHER_URL in .env to the ResearcherUrl output + */ +export class ResearcherStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // ── VPC ────────────────────────────────────────────────────────────────── + const vpc = new ec2.Vpc(this, "Vpc", { + maxAzs: 2, + natGateways: 1, + }); + + // ── ECR repository ─────────────────────────────────────────────────────── + const repository = new ecr.Repository(this, "ResearcherRepo", { + repositoryName: "researcher", + removalPolicy: cdk.RemovalPolicy.DESTROY, + emptyOnDelete: true, + }); + + // ── Security groups ────────────────────────────────────────────────────── + const albSg = new ec2.SecurityGroup(this, "AlbSg", { + vpc, + description: "ALB: allow inbound 8123 from anywhere", + }); + albSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(8123)); + + const taskSg = new ec2.SecurityGroup(this, "TaskSg", { + vpc, + description: "ECS task: allow inbound from ALB", + }); + taskSg.addIngressRule(albSg, ec2.Port.tcp(8123)); + + const rdsSg = new ec2.SecurityGroup(this, "RdsSg", { + vpc, + description: "RDS: allow inbound from ECS task", + }); + rdsSg.addIngressRule(taskSg, ec2.Port.tcp(5432)); + + const redisSg = new ec2.SecurityGroup(this, "RedisSg", { + vpc, + description: "Redis: allow inbound from ECS task", + }); + redisSg.addIngressRule(taskSg, ec2.Port.tcp(6379)); + + // ── RDS PostgreSQL ─────────────────────────────────────────────────────── + const dbSecret = new secretsmanager.Secret(this, "DbSecret", { + generateSecretString: { + secretStringTemplate: JSON.stringify({ username: "langgraph" }), + generateStringKey: "password", + excludeCharacters: "/@\"' ", + }, + }); + + const dbSubnetGroup = new rds.SubnetGroup(this, "DbSubnetGroup", { + vpc, + description: "RDS subnet group", + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + const db = new rds.DatabaseInstance(this, "Db", { + engine: rds.DatabaseInstanceEngine.postgres({ + version: rds.PostgresEngineVersion.VER_16, + }), + instanceType: ec2.InstanceType.of( + ec2.InstanceClass.T4G, + ec2.InstanceSize.MICRO, + ), + vpc, + subnetGroup: dbSubnetGroup, + securityGroups: [rdsSg], + credentials: rds.Credentials.fromSecret(dbSecret), + databaseName: "langgraph", + removalPolicy: cdk.RemovalPolicy.DESTROY, + deletionProtection: false, + }); + + // ── ElastiCache Redis ──────────────────────────────────────────────────── + const redisSubnetGroup = new elasticache.CfnSubnetGroup( + this, + "RedisSubnetGroup", + { + description: "Redis subnet group", + subnetIds: vpc.privateSubnets.map((s) => s.subnetId), + }, + ); + + const redis = new elasticache.CfnCacheCluster(this, "Redis", { + cacheNodeType: "cache.t4g.micro", + engine: "redis", + numCacheNodes: 1, + cacheSubnetGroupName: redisSubnetGroup.ref, + vpcSecurityGroupIds: [redisSg.securityGroupId], + }); + + // ── ECS cluster + task ─────────────────────────────────────────────────── + const cluster = new ecs.Cluster(this, "Cluster", { vpc }); + + const taskDef = new ecs.FargateTaskDefinition(this, "TaskDef", { + cpu: 512, + memoryLimitMiB: 1024, + }); + + repository.grantPull(taskDef.obtainExecutionRole()); + + const redisUri = `redis://${redis.attrRedisEndpointAddress}:${redis.attrRedisEndpointPort}`; + + // DATABASE_URI must be stored as a pre-built secret because the RDS-generated + // password contains special characters that break URL parsing if inlined at synth time. + // After deploying, run the post-deploy script to populate this secret with the + // URL-encoded URI, then force a new ECS deployment. + const postgresUriSecret = new secretsmanager.Secret(this, "PostgresUriSecret", { + secretName: "agent-protocol-poc/DATABASE_URI", + description: "postgresql+psycopg://... URI for langgraph-api (populate after RDS is created)", + }); + + taskDef.addContainer("Researcher", { + image: ecs.ContainerImage.fromEcrRepository(repository, "latest"), + portMappings: [{ containerPort: 8123 }], + environment: { + REDIS_URI: redisUri, + PORT: "8123", + }, + secrets: { + ANTHROPIC_API_KEY: ecs.Secret.fromSecretsManager( + secretsmanager.Secret.fromSecretNameV2( + this, + "AnthropicKey", + "agent-protocol-poc/ANTHROPIC_API_KEY", + ), + ), + DATABASE_URI: ecs.Secret.fromSecretsManager(postgresUriSecret), + }, + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "researcher" }), + }); + + // ── Fargate service ────────────────────────────────────────────────────── + const service = new ecs.FargateService(this, "Service", { + cluster, + taskDefinition: taskDef, + desiredCount: 1, + securityGroups: [taskSg], + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // ── Application Load Balancer ──────────────────────────────────────────── + const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", { + vpc, + internetFacing: true, + securityGroup: albSg, + }); + + const listener = alb.addListener("Listener", { + port: 8123, + protocol: elbv2.ApplicationProtocol.HTTP, + open: false, + }); + + listener.addTargets("ResearcherTarget", { + port: 8123, + protocol: elbv2.ApplicationProtocol.HTTP, + targets: [service], + healthCheck: { + path: "/ok", + interval: cdk.Duration.seconds(30), + healthyHttpCodes: "200", + }, + }); + + // ── Outputs ────────────────────────────────────────────────────────────── + new cdk.CfnOutput(this, "ResearcherUrl", { + value: `http://${alb.loadBalancerDnsName}`, + description: "Set this as RESEARCHER_URL in your .env", + }); + + new cdk.CfnOutput(this, "EcrRepositoryUri", { + value: repository.repositoryUri, + description: "Push your researcher image here", + }); + } +} diff --git a/examples/agent-protocol-poc/infra/package-lock.json b/examples/agent-protocol-poc/infra/package-lock.json new file mode 100644 index 000000000..3f33873c6 --- /dev/null +++ b/examples/agent-protocol-poc/infra/package-lock.json @@ -0,0 +1,698 @@ +{ + "name": "agent-protocol-poc-infra", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-protocol-poc-infra", + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "aws-cdk": "^2.180.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib": { + "version": "2.245.0", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.263", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.0", + "@aws-cdk/cloud-assembly-schema": "^53.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" + }, + "devDependencies": { + "@aws-cdk/aws-custom-resource-sdk-adapter": "2.245.0-alpha.0", + "@aws-cdk/aws-service-spec": "^0.1.165", + "@aws-cdk/cdk-build-tools": "2.245.0-alpha.0", + "@aws-cdk/custom-resource-handlers": "2.245.0-alpha.0", + "@aws-cdk/lambda-layer-kubectl-v31": "^2.1.0", + "@aws-cdk/lambda-layer-kubectl-v33": "^2.0.1", + "@aws-cdk/lazify": "2.245.0-alpha.0", + "@aws-cdk/pkglint": "2.245.0-alpha.0", + "@aws-cdk/spec2cdk": "2.245.0-alpha.0", + "@aws-sdk/client-account": "3.632.0", + "@aws-sdk/client-acm": "3.632.0", + "@aws-sdk/client-cloudwatch-logs": "3.632.0", + "@aws-sdk/client-codepipeline": "3.632.0", + "@aws-sdk/client-dynamodb": "3.632.0", + "@aws-sdk/client-ec2": "3.632.0", + "@aws-sdk/client-ecr": "3.632.0", + "@aws-sdk/client-eks": "3.632.0", + "@aws-sdk/client-iam": "3.632.0", + "@aws-sdk/client-lambda": "3.632.0", + "@aws-sdk/client-route-53": "3.632.0", + "@aws-sdk/client-s3": "3.632.0", + "@aws-sdk/client-sfn": "3.632.0", + "@aws-sdk/client-ssm": "3.632.0", + "@aws-sdk/client-sts": "3.632.0", + "@aws-sdk/credential-providers": "3.632.0", + "@smithy/node-http-handler": "3.3.3", + "@smithy/types": "3.7.2", + "@smithy/util-stream": "3.3.4", + "@types/aws-lambda": "^8.10.160", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.24", + "@types/mime-types": "^2.1.4", + "@types/punycode": "^2.1.4", + "aws-sdk-client-mock": "4.1.0", + "aws-sdk-client-mock-jest": "4.1.0", + "cdk8s": "2.70.48", + "constructs": "^10.5.0", + "delay": "5.0.0", + "esbuild": "^0.27.3", + "fast-check": "^3.23.2", + "jest": "^29.7.0", + "jest-each": "^29.7.0", + "jest-environment-node": "^29.7.0", + "lambda-tester": "^4.0.1", + "lodash": "^4.17.23", + "nock": "^13.5.6", + "sinon": "^9.2.4", + "ts-mock-imports": "^1.3.19", + "ts-node": "^10.9.2", + "typescript": "~5.5.4", + "typescript-json-schema": "^0.67.1" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.0", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=53.0.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.18.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.4", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "../../../node_modules/.pnpm/aws-cdk@2.1114.1/node_modules/aws-cdk": { + "version": "2.1114.1", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "devDependencies": { + "@aws-cdk/cdk-assets-lib": "^1.4.2", + "@aws-cdk/cli-plugin-contract": "2.182.1", + "@aws-cdk/cloud-assembly-api": "2.2.1", + "@aws-cdk/cloud-assembly-schema": ">=53.9.0", + "@aws-cdk/cloudformation-diff": "2.186.0", + "@aws-cdk/cx-api": "^2", + "@aws-cdk/toolkit-lib": "^1.19.3", + "@aws-cdk/user-input-gen": "0.0.0", + "@aws-sdk/client-appsync": "^3", + "@aws-sdk/client-bedrock-agentcore-control": "^3", + "@aws-sdk/client-cloudcontrol": "^3", + "@aws-sdk/client-cloudformation": "^3", + "@aws-sdk/client-cloudwatch-logs": "^3", + "@aws-sdk/client-codebuild": "^3", + "@aws-sdk/client-ec2": "^3", + "@aws-sdk/client-ecr": "^3", + "@aws-sdk/client-ecs": "^3", + "@aws-sdk/client-elastic-load-balancing-v2": "^3", + "@aws-sdk/client-iam": "^3", + "@aws-sdk/client-kms": "^3", + "@aws-sdk/client-lambda": "^3", + "@aws-sdk/client-route-53": "^3", + "@aws-sdk/client-s3": "^3", + "@aws-sdk/client-secrets-manager": "^3", + "@aws-sdk/client-sfn": "^3", + "@aws-sdk/client-ssm": "^3", + "@aws-sdk/client-sts": "^3", + "@aws-sdk/credential-providers": "^3", + "@aws-sdk/ec2-metadata-service": "^3", + "@aws-sdk/lib-storage": "^3", + "@cdklabs/eslint-plugin": "^1.5.9", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-waiter": "^4.2.13", + "@stylistic/eslint-plugin": "^3", + "@types/archiver": "^6.0.4", + "@types/fs-extra": "^9", + "@types/jest": "^29.5.14", + "@types/mockery": "^1.4.33", + "@types/node": "^16", + "@types/picomatch": "^4.0.2", + "@types/promptly": "^3.0.5", + "@types/semver": "^7.7.1", + "@types/sinon": "^17.0.4", + "@types/yargs": "^15", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "archiver": "^7.0.1", + "aws-cdk-lib": "2.244.0", + "aws-sdk-client-mock": "^4.1.0", + "aws-sdk-client-mock-jest": "^4.1.0", + "axios": "^1.13.6", + "camelcase": "^6", + "cdk-from-cfn": "^0.291.0", + "chalk": "^4", + "chokidar": "^4", + "commit-and-tag-version": "^12", + "constructs": "^10.0.0", + "decamelize": "^5", + "enquirer": "^2.4.1", + "eslint": "^9", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^29.15.1", + "eslint-plugin-jsdoc": "^62.8.1", + "eslint-plugin-prettier": "^5.5.5", + "fast-check": "^3.23.2", + "fast-glob": "^3.3.3", + "fs-extra": "^9", + "jest": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-junit": "^16", + "jest-mock": "^29.7.0", + "license-checker": "^25.0.1", + "madge": "^8.0.0", + "nock": "13", + "node-backpack": "^1.1.26", + "p-limit": "^3", + "p-queue": "^6", + "picomatch": "^4.0.4", + "prettier": "^2.8", + "promptly": "^3.2.0", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "sinon": "^19.0.5", + "strip-ansi": "^6", + "ts-jest": "^29.4.6", + "ts-mock-imports": "^1.3.19", + "typescript": "5.9", + "uuid": "^11.1.0", + "wrap-ansi": "^7", + "xml-js": "^1.6.11", + "yaml": "^1", + "yargs": "^15" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "../../../node_modules/.pnpm/constructs@10.6.0/node_modules/constructs": { + "version": "10.6.0", + "license": "Apache-2.0", + "devDependencies": { + "@stylistic/eslint-plugin": "^2", + "@types/jest": "^29", + "@types/node": "^18", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "cdklabs-projen-project-types": "^0.3.7", + "commit-and-tag-version": "^12", + "eslint": "^9", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import": "^2.32.0", + "jest": "^29", + "jest-junit": "^16", + "jsii": "5.9.x", + "jsii-diff": "^1.127.0", + "jsii-docgen": "^10.5.0", + "jsii-pacmak": "^1.127.0", + "jsii-rosetta": "5.9.x", + "projen": "^0.98.4", + "ts-jest": "^29", + "ts-node": "^10.9.2", + "typescript": "5.9.x" + } + }, + "../../../node_modules/.pnpm/tsx@4.21.0/node_modules/tsx": { + "version": "4.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "devDependencies": { + "@dprint/formatter": "^0.4.1", + "@dprint/typescript": "0.93.4", + "@esfx/canceltoken": "^1.0.0", + "@eslint/js": "^9.20.0", + "@octokit/rest": "^21.1.1", + "@types/chai": "^4.3.20", + "@types/diff": "^7.0.1", + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", + "@types/ms": "^0.7.34", + "@types/node": "latest", + "@types/source-map-support": "^0.5.10", + "@types/which": "^3.0.4", + "@typescript-eslint/rule-tester": "^8.24.1", + "@typescript-eslint/type-utils": "^8.24.1", + "@typescript-eslint/utils": "^8.24.1", + "azure-devops-node-api": "^14.1.0", + "c8": "^10.1.3", + "chai": "^4.5.0", + "chokidar": "^4.0.3", + "diff": "^7.0.0", + "dprint": "^0.49.0", + "esbuild": "^0.25.0", + "eslint": "^9.20.1", + "eslint-formatter-autolinkable-stylish": "^1.4.0", + "eslint-plugin-regexp": "^2.7.0", + "fast-xml-parser": "^4.5.2", + "glob": "^10.4.5", + "globals": "^15.15.0", + "hereby": "^1.10.0", + "jsonc-parser": "^3.3.1", + "knip": "^5.44.4", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "mocha-fivemat-progress-reporter": "^0.1.0", + "monocart-coverage-reports": "^2.12.1", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "playwright": "^1.50.1", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1", + "typescript": "^5.7.3", + "typescript-eslint": "^8.24.1", + "which": "^3.0.1" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/aws-cdk": { + "resolved": "../../../node_modules/.pnpm/aws-cdk@2.1114.1/node_modules/aws-cdk", + "link": true + }, + "node_modules/aws-cdk-lib": { + "resolved": "../../../node_modules/.pnpm/aws-cdk-lib@2.245.0_constructs@10.6.0/node_modules/aws-cdk-lib", + "link": true + }, + "node_modules/constructs": { + "resolved": "../../../node_modules/.pnpm/constructs@10.6.0/node_modules/constructs", + "link": true + }, + "node_modules/tsx": { + "resolved": "../../../node_modules/.pnpm/tsx@4.21.0/node_modules/tsx", + "link": true + }, + "node_modules/typescript": { + "resolved": "../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript", + "link": true + } + } +} diff --git a/examples/agent-protocol-poc/infra/package.json b/examples/agent-protocol-poc/infra/package.json new file mode 100644 index 000000000..aeca2ab21 --- /dev/null +++ b/examples/agent-protocol-poc/infra/package.json @@ -0,0 +1,20 @@ +{ + "name": "agent-protocol-poc-infra", + "private": true, + "type": "module", + "scripts": { + "deploy": "cdk deploy", + "destroy": "cdk destroy", + "synth": "cdk synth", + "diff": "cdk diff" + }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "aws-cdk": "^2.180.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } +} diff --git a/examples/agent-protocol-poc/infra/tsconfig.json b/examples/agent-protocol-poc/infra/tsconfig.json new file mode 100644 index 000000000..54d41cadf --- /dev/null +++ b/examples/agent-protocol-poc/infra/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["bin/**/*.ts", "lib/**/*.ts"] +} diff --git a/examples/agent-protocol-poc/langgraph.json b/examples/agent-protocol-poc/langgraph.json new file mode 100644 index 000000000..a1d5f3566 --- /dev/null +++ b/examples/agent-protocol-poc/langgraph.json @@ -0,0 +1,8 @@ +{ + "dependencies": ["."], + "graphs": { + "supervisor": "./supervisor.ts:graph", + "researcher": "./researcher.ts:graph" + }, + "env": ".env" +} diff --git a/examples/agent-protocol-poc/package.json b/examples/agent-protocol-poc/package.json new file mode 100644 index 000000000..20b85686e --- /dev/null +++ b/examples/agent-protocol-poc/package.json @@ -0,0 +1,14 @@ +{ + "name": "agent-protocol-poc", + "private": true, + "type": "module", + "dependencies": { + "deepagents": "^1.8.6", + "@langchain/langgraph": "^1.1.4", + "@langchain/core": "^1.1.33", + "@langchain/anthropic": "^1.3.18", + "langchain": "^1.2.34", + "dotenv": "^17.2.4", + "zod": "^4.3.6" + } +} diff --git a/examples/agent-protocol-poc/researcher.ts b/examples/agent-protocol-poc/researcher.ts new file mode 100644 index 000000000..346e255a4 --- /dev/null +++ b/examples/agent-protocol-poc/researcher.ts @@ -0,0 +1,84 @@ +/** + * Researcher subagent — Agent Protocol PoC + * + * A LangGraph ReAct agent with web search. Deployed as part of the PoC + * to run on an Agent Protocol server (local dev or ECS). + * + * Deploy with: + * npx @langchain/langgraph-cli dev -c examples/agent-protocol-poc/langgraph.json + * + * Or build a Docker image for ECS: + * langgraph build -t researcher -c examples/agent-protocol-poc/langgraph.json + */ +import "dotenv/config"; +import { tool } from "langchain"; +import { z } from "zod"; +import { createDeepAgent } from "deepagents"; + +// ─── Web search tool (Tavily with stub fallback) ────────────────────────────── + +let searchImpl: (query: string) => Promise; + +if (process.env.TAVILY_API_KEY) { + searchImpl = async (query: string) => { + const res = await fetch("https://api.tavily.com/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + api_key: process.env.TAVILY_API_KEY, + query, + max_results: 5, + }), + }); + const data = (await res.json()) as { + results?: { title: string; content: string; url: string }[]; + }; + if (!data.results?.length) return `No results found for "${query}"`; + return data.results + .map( + (r, i) => + `${i + 1}. **${r.title}**\n ${r.content}\n Source: ${r.url}`, + ) + .join("\n\n"); + }; +} else { + searchImpl = async (query: string) => { + return [ + `Search results for "${query}":`, + `1. Key finding: Recent developments show significant progress in ${query}`, + `2. Expert analysis: Industry leaders are investing heavily in ${query}`, + `3. Market data: The ${query} sector is projected to grow 25% annually`, + `4. Trend report: Consumer adoption of ${query} has accelerated`, + `5. Research paper: New breakthroughs in ${query} published this quarter`, + ].join("\n"); + }; +} + +const webSearch = tool( + async (input: { query: string }) => searchImpl(input.query), + { + name: "web_search", + description: + "Search the web for information. Use this to find current data, news, and analysis.", + schema: z.object({ + query: z.string().describe("The search query"), + }), + }, +); + +// ─── Researcher agent ───────────────────────────────────────────────────────── + +export const graph = createDeepAgent({ + systemPrompt: + "You are a thorough research agent. Your job is to investigate a topic using web search, " + + "analyze the findings, and produce a well-structured research summary.\n\n" + + "Guidelines:\n" + + "- Make multiple searches to cover the topic comprehensively\n" + + "- Synthesize findings into a clear, structured report\n" + + "- Include key facts, data points, and notable developments\n" + + "- Cite your sources where possible\n" + + "- Keep your final report concise but thorough (300-500 words)\n\n" + + "If you receive a new instruction mid-conversation, immediately follow it without asking for " + + "clarification. Discard any prior work and start fresh on the new task.", + tools: [webSearch], +}); diff --git a/examples/agent-protocol-poc/supervisor.ts b/examples/agent-protocol-poc/supervisor.ts new file mode 100644 index 000000000..301094279 --- /dev/null +++ b/examples/agent-protocol-poc/supervisor.ts @@ -0,0 +1,57 @@ +/** + * Supervisor agent — Agent Protocol PoC + * + * Demonstrates async subagents running against a self-hosted Agent Protocol + * server with no LangSmith dependency. The supervisor coordinates a researcher + * subagent deployed to an Agent Protocol server (local dev or ECS). + * + * Deploy with: + * npx @langchain/langgraph-cli dev -c examples/agent-protocol-poc/langgraph.json + * + * The RESEARCHER_URL env var controls which server the supervisor talks to: + * - unset / blank → http://localhost:2024 (local langgraph dev server) + * - set to ALB DNS → ECS-hosted researcher (production proof) + * + * No LANGSMITH_API_KEY required. This is the point of the PoC. + */ +import "dotenv/config"; +import { createDeepAgent, type AsyncSubAgent } from "deepagents"; + +const RESEARCHER_URL = process.env.RESEARCHER_URL || "http://localhost:2024"; + +const asyncSubAgents: AsyncSubAgent[] = [ + { + name: "researcher", + description: "A research agent that investigates any topic using web search.", + graphId: "researcher", + url: RESEARCHER_URL, + }, +]; + +export const graph = createDeepAgent({ + systemPrompt: + "You are a research supervisor coordinating background research agents.\n\n" + + "For general questions, answer directly — do NOT launch researchers.\n\n" + + 'Only launch researchers when the user uses words like: "research", "investigate", "look into", "find out".\n\n' + + "=== PoC stress test — cover all five operations ===\n\n" + + "START: When the user asks to research something:\n" + + '1. Launch a researcher with start_async_task (agentName: "researcher").\n' + + "2. Report the taskId and stop. Do NOT immediately check status.\n\n" + + "CHECK: When the user asks for status or results:\n" + + "1. Call check_async_task with the exact taskId.\n" + + "2. Report what the tool returns. If still running, say so and stop.\n\n" + + "UPDATE: When the user asks to change what a researcher is working on:\n" + + "1. Call update_async_task with the taskId and the new instructions.\n" + + "2. Confirm the update to the user.\n\n" + + "CANCEL: When the user asks to cancel a researcher:\n" + + "1. Call cancel_async_task with the exact taskId.\n" + + "2. Confirm the cancellation.\n\n" + + "LIST: When the user asks 'what tasks are running' or 'show all tasks':\n" + + "1. Call list_async_tasks.\n" + + "2. Present the live statuses.\n\n" + + "Critical rules:\n" + + "- Never report stale status from memory. Always call a tool.\n" + + "- Never poll in a loop. One tool call per user request.\n" + + "- Always show the full taskId — never truncate it.", + subagents: asyncSubAgents, +}); diff --git a/examples/agent-protocol-server/Dockerfile b/examples/agent-protocol-server/Dockerfile new file mode 100644 index 000000000..ed1a67d99 --- /dev/null +++ b/examples/agent-protocol-server/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20-slim + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy workspace root manifests (needed to resolve workspace deps) +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY libs/ ./libs/ +COPY examples/agent-protocol-server/ ./examples/agent-protocol-server/ + +# Install dependencies +RUN pnpm install --no-frozen-lockfile + +WORKDIR /app/examples/agent-protocol-server + +EXPOSE 2024 + +CMD ["npx", "tsx", "server.ts"] diff --git a/examples/agent-protocol-server/infra/bin/app.ts b/examples/agent-protocol-server/infra/bin/app.ts new file mode 100644 index 000000000..51ebba4d2 --- /dev/null +++ b/examples/agent-protocol-server/infra/bin/app.ts @@ -0,0 +1,11 @@ +import * as cdk from "aws-cdk-lib"; +import { AgentProtocolServerStack } from "../lib/stack.js"; + +const app = new cdk.App(); + +new AgentProtocolServerStack(app, "AgentProtocolServerStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION ?? "us-east-1", + }, +}); diff --git a/examples/agent-protocol-server/infra/cdk.context.json b/examples/agent-protocol-server/infra/cdk.context.json new file mode 100644 index 000000000..373f29f01 --- /dev/null +++ b/examples/agent-protocol-server/infra/cdk.context.json @@ -0,0 +1,10 @@ +{ + "availability-zones:account=796326355178:region=us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ] +} diff --git a/examples/agent-protocol-server/infra/cdk.json b/examples/agent-protocol-server/infra/cdk.json new file mode 100644 index 000000000..05cb324a2 --- /dev/null +++ b/examples/agent-protocol-server/infra/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "npx tsx bin/app.ts", + "context": { + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, + "@aws-cdk/core:stackRelativeExports": true + } +} diff --git a/examples/agent-protocol-server/infra/lib/stack.ts b/examples/agent-protocol-server/infra/lib/stack.ts new file mode 100644 index 000000000..62d7e75a1 --- /dev/null +++ b/examples/agent-protocol-server/infra/lib/stack.ts @@ -0,0 +1,124 @@ +import * as cdk from "aws-cdk-lib"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as ecr from "aws-cdk-lib/aws-ecr"; +import * as ecs from "aws-cdk-lib/aws-ecs"; +import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; +import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; +import { Construct } from "constructs"; + +/** + * AgentProtocolServerStack + * + * Deploys the minimal independent Agent Protocol server to ECS Fargate. + * No Postgres. No Redis. No LangSmith license key. + * + * Deploy steps: + * 1. cdk deploy + * 2. docker build --platform linux/amd64 -f ../Dockerfile -t agent-protocol-server ../../../ + * 3. docker tag agent-protocol-server :latest + * 4. docker push :latest + * 5. aws ecs update-service --cluster ... --service ... --force-new-deployment + * 6. Set RESEARCHER_URL in .env to the ServerUrl output + */ +export class AgentProtocolServerStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // ── VPC ────────────────────────────────────────────────────────────────── + const vpc = new ec2.Vpc(this, "Vpc", { + maxAzs: 2, + natGateways: 1, + }); + + // ── ECR ────────────────────────────────────────────────────────────────── + const repository = new ecr.Repository(this, "Repo", { + repositoryName: "agent-protocol-server", + removalPolicy: cdk.RemovalPolicy.DESTROY, + emptyOnDelete: true, + }); + + // ── Security groups ────────────────────────────────────────────────────── + const albSg = new ec2.SecurityGroup(this, "AlbSg", { + vpc, + description: "ALB: allow inbound 2024 from anywhere", + }); + albSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(2024)); + + const taskSg = new ec2.SecurityGroup(this, "TaskSg", { + vpc, + description: "ECS task: allow inbound from ALB", + }); + taskSg.addIngressRule(albSg, ec2.Port.tcp(2024)); + + // ── ECS ────────────────────────────────────────────────────────────────── + const cluster = new ecs.Cluster(this, "Cluster", { vpc }); + + const taskDef = new ecs.FargateTaskDefinition(this, "TaskDef", { + cpu: 512, + memoryLimitMiB: 1024, + }); + + repository.grantPull(taskDef.obtainExecutionRole()); + + taskDef.addContainer("Server", { + image: ecs.ContainerImage.fromEcrRepository(repository, "latest"), + portMappings: [{ containerPort: 2024 }], + environment: { + PORT: "2024", + }, + secrets: { + ANTHROPIC_API_KEY: ecs.Secret.fromSecretsManager( + secretsmanager.Secret.fromSecretNameV2( + this, + "AnthropicKey", + "agent-protocol-server/ANTHROPIC_API_KEY", + ), + ), + }, + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "agent-protocol-server" }), + }); + + const service = new ecs.FargateService(this, "Service", { + cluster, + taskDefinition: taskDef, + desiredCount: 1, + securityGroups: [taskSg], + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // ── ALB ────────────────────────────────────────────────────────────────── + const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", { + vpc, + internetFacing: true, + securityGroup: albSg, + }); + + const listener = alb.addListener("Listener", { + port: 2024, + protocol: elbv2.ApplicationProtocol.HTTP, + open: false, + }); + + listener.addTargets("Target", { + port: 2024, + protocol: elbv2.ApplicationProtocol.HTTP, + targets: [service], + healthCheck: { + path: "/ok", + interval: cdk.Duration.seconds(30), + healthyHttpCodes: "200", + }, + }); + + // ── Outputs ────────────────────────────────────────────────────────────── + new cdk.CfnOutput(this, "ServerUrl", { + value: `http://${alb.loadBalancerDnsName}:2024`, + description: "Set this as RESEARCHER_URL in your .env", + }); + + new cdk.CfnOutput(this, "EcrRepositoryUri", { + value: repository.repositoryUri, + description: "Push your server image here", + }); + } +} diff --git a/examples/agent-protocol-server/infra/package-lock.json b/examples/agent-protocol-server/infra/package-lock.json new file mode 100644 index 000000000..ea8954d94 --- /dev/null +++ b/examples/agent-protocol-server/infra/package-lock.json @@ -0,0 +1,1035 @@ +{ + "name": "agent-protocol-server-infra", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-protocol-server-infra", + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "aws-cdk": "^2.180.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.263", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.263.tgz", + "integrity": "sha512-X9JvcJhYcb7PHs8R7m4zMablO5C9PGb/hYfLnxds9h/rKJu6l7MiXE/SabCibuehxPnuO/vk+sVVJiUWrccarQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "53.9.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.9.0.tgz", + "integrity": "sha512-Ss7Af943iyyTABqeJS30LylmELpdpGgHzQP87KxO+HGPFIFDsoZymSuU1H5eQAcuuOvcfIPSKA62/lf274UB2A==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/aws-cdk": { + "version": "2.1114.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1114.1.tgz", + "integrity": "sha512-jMaKPWQQs1G6AbhfCQG2zGrgAhTxzP0jn4T2CuwONuvcV374dMPhfC/LBAv48ruSOgpCK9x6V1xeO/aH3EchAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.245.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.245.0.tgz", + "integrity": "sha512-Yfeb+wKC6s+Ttm/N93C6vY6ksyCh68WaG/j3N6dalJWTW/V4o6hUolHm+v2c2IofJEUS45c5AF/EEj24e9hfMA==", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.263", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.0", + "@aws-cdk/cloud-assembly-schema": "^53.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.0", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=53.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.18.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.4", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/constructs": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", + "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", + "license": "Apache-2.0" + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/examples/agent-protocol-server/infra/package.json b/examples/agent-protocol-server/infra/package.json new file mode 100644 index 000000000..5a3705c06 --- /dev/null +++ b/examples/agent-protocol-server/infra/package.json @@ -0,0 +1,20 @@ +{ + "name": "agent-protocol-server-infra", + "private": true, + "type": "module", + "scripts": { + "deploy": "cdk deploy", + "destroy": "cdk destroy", + "synth": "cdk synth", + "diff": "cdk diff" + }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "aws-cdk": "^2.180.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } +} diff --git a/examples/agent-protocol-server/infra/tsconfig.json b/examples/agent-protocol-server/infra/tsconfig.json new file mode 100644 index 000000000..54d41cadf --- /dev/null +++ b/examples/agent-protocol-server/infra/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["bin/**/*.ts", "lib/**/*.ts"] +} diff --git a/examples/agent-protocol-server/package.json b/examples/agent-protocol-server/package.json new file mode 100644 index 000000000..3987f4f21 --- /dev/null +++ b/examples/agent-protocol-server/package.json @@ -0,0 +1,24 @@ +{ + "name": "agent-protocol-server", + "private": true, + "type": "module", + "scripts": { + "start": "tsx server.ts" + }, + "dependencies": { + "express": "^4.18.0", + "deepagents": "workspace:*", + "@langchain/anthropic": "^1.3.18", + "@langchain/core": "^1.1.33", + "langchain": "^1.2.34", + "zod": "^4.3.6", + "dotenv": "^17.2.4", + "uuid": "^9.0.0" + }, + "devDependencies": { + "tsx": "^4.0.0", + "typescript": "^5.0.0", + "@types/express": "^4.17.0", + "@types/uuid": "^9.0.0" + } +} diff --git a/examples/agent-protocol-server/server.ts b/examples/agent-protocol-server/server.ts new file mode 100644 index 000000000..b8c5053bd --- /dev/null +++ b/examples/agent-protocol-server/server.ts @@ -0,0 +1,236 @@ +/** + * Minimal Agent Protocol Server + * + * An independent implementation of the Agent Protocol REST API backed by any + * LangGraph agent. No LangSmith account, no Postgres, no Redis — just an + * in-memory task store and your agent. + * + * Implements the endpoints the DeepAgents async subagent middleware calls: + * POST /threads → create thread + * POST /threads/:threadId/runs → create run (start or update/interrupt) + * GET /threads/:threadId/runs/:runId → get run status + * GET /threads/:threadId/state → get thread state (output) + * POST /threads/:threadId/runs/:runId/cancel → cancel run + * GET /ok → health check + * + * Run: + * ANTHROPIC_API_KEY=... tsx examples/agent-protocol-server/server.ts + * + * Then point your supervisor at: + * RESEARCHER_URL=http://localhost:2024 + */ +import "dotenv/config"; +import express from "express"; +import { v4 as uuidv4 } from "uuid"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import { createDeepAgent } from "deepagents"; +import { HumanMessage } from "@langchain/core/messages"; + +const app = express(); +app.use(express.json()); +app.use((req, _res, next) => { + console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); + next(); +}); + +// ── In-memory store ────────────────────────────────────────────────────────── + +interface Thread { + thread_id: string; + created_at: string; + messages: { role: string; content: string }[]; + output: string | null; +} + +interface Run { + run_id: string; + thread_id: string; + assistant_id: string; + status: "pending" | "running" | "success" | "error" | "cancelled"; + created_at: string; + error?: string; +} + +const threads = new Map(); +const runs = new Map(); + +// ── Agent ──────────────────────────────────────────────────────────────────── + +// Web search stub — replace with Tavily if TAVILY_API_KEY is set +const webSearch = tool( + async ({ query }: { query: string }) => { + if (process.env.TAVILY_API_KEY) { + const res = await fetch("https://api.tavily.com/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + api_key: process.env.TAVILY_API_KEY, + query, + max_results: 5, + }), + }); + const data = (await res.json()) as { + results?: { title: string; content: string; url: string }[]; + }; + if (!data.results?.length) return `No results for "${query}"`; + return data.results + .map((r, i) => `${i + 1}. **${r.title}**\n ${r.content}\n ${r.url}`) + .join("\n\n"); + } + return [ + `Search results for "${query}":`, + `1. Key finding: Recent developments show significant progress in ${query}`, + `2. Expert analysis: Industry leaders are investing heavily in ${query}`, + `3. Market data: The ${query} sector is projected to grow 25% annually`, + ].join("\n"); + }, + { + name: "web_search", + description: "Search the web for information.", + schema: z.object({ query: z.string() }), + }, +); + +const researcher = createDeepAgent({ + systemPrompt: + "You are a thorough research agent. Investigate topics using web search and produce " + + "a well-structured research summary (300-500 words). If you receive new instructions " + + "mid-conversation, immediately follow them without asking for clarification.", + tools: [webSearch], +}); + +// ── Run executor ───────────────────────────────────────────────────────────── + +async function executeRun(run: Run, thread: Thread, input: string): Promise { + run.status = "running"; + try { + const result = await researcher.invoke({ + messages: [new HumanMessage(input)], + }); + + const lastMessage = result.messages[result.messages.length - 1]; + thread.output = typeof lastMessage.content === "string" + ? lastMessage.content + : JSON.stringify(lastMessage.content); + + thread.messages.push({ role: "assistant", content: thread.output }); + run.status = "success"; + } catch (e) { + run.status = "error"; + run.error = String(e); + } +} + +// ── Routes ─────────────────────────────────────────────────────────────────── + +// Health check +app.get("/ok", (_req, res) => { + res.json({ ok: true }); +}); + +// Create thread +app.post("/threads", (_req, res) => { + const thread: Thread = { + thread_id: uuidv4(), + created_at: new Date().toISOString(), + messages: [], + output: null, + }; + threads.set(thread.thread_id, thread); + res.json(thread); +}); + +// Create run (start or interrupt/update) +app.post("/threads/:threadId/runs", (req, res) => { + const thread = threads.get(req.params.threadId); + if (!thread) { + res.status(404).json({ error: "Thread not found" }); + return; + } + + const { input, assistant_id, multitask_strategy } = req.body as { + input?: { messages?: { role: string; content: string }[] }; + assistant_id?: string; + multitask_strategy?: string; + }; + + // If interrupt — cancel any running runs on this thread + if (multitask_strategy === "interrupt") { + for (const run of runs.values()) { + if (run.thread_id === req.params.threadId && run.status === "running") { + run.status = "cancelled"; + } + } + // Reset thread output for the new task + thread.output = null; + } + + const userMessage = input?.messages?.find((m) => m.role === "user")?.content ?? ""; + thread.messages.push({ role: "user", content: userMessage }); + + const run: Run = { + run_id: uuidv4(), + thread_id: req.params.threadId, + assistant_id: assistant_id ?? "researcher", + status: "pending", + created_at: new Date().toISOString(), + }; + runs.set(run.run_id, run); + + // Execute in background — don't await + executeRun(run, thread, userMessage); + + res.json(run); +}); + +// Get run status +app.get("/threads/:threadId/runs/:runId", (req, res) => { + const run = runs.get(req.params.runId); + if (!run || run.thread_id !== req.params.threadId) { + res.status(404).json({ error: "Run not found" }); + return; + } + res.json(run); +}); + +// Get thread state (output) +app.get("/threads/:threadId/state", (req, res) => { + const thread = threads.get(req.params.threadId); + if (!thread) { + res.status(404).json({ error: "Thread not found" }); + return; + } + res.json({ + values: { + messages: thread.messages, + output: thread.output, + }, + next: [], + metadata: {}, + }); +}); + +// Cancel run +app.post("/threads/:threadId/runs/:runId/cancel", (req, res) => { + const run = runs.get(req.params.runId); + if (!run || run.thread_id !== req.params.threadId) { + res.status(404).json({ error: "Run not found" }); + return; + } + run.status = "cancelled"; + res.json(run); +}); + +// List runs (used by list_async_tasks) +app.get("/runs", (_req, res) => { + res.json(Array.from(runs.values())); +}); + +// ── Start ───────────────────────────────────────────────────────────────────── + +const PORT = process.env.PORT ?? 2024; +app.listen(PORT, () => { + console.log(`Agent Protocol server listening on http://localhost:${PORT}`); + console.log(`Graphs: researcher`); +}); diff --git a/libs/deepagents/src/middleware/async_subagents.test.ts b/libs/deepagents/src/middleware/async_subagents.test.ts index 10fc1c052..11363a527 100644 --- a/libs/deepagents/src/middleware/async_subagents.test.ts +++ b/libs/deepagents/src/middleware/async_subagents.test.ts @@ -59,7 +59,7 @@ function makeAgent(overrides: Partial = {}): AsyncSubAgent { name: "researcher", description: "Research agent", graphId: "research_graph", - url: "https://example.langsmith.dev", + url: "https://researcher.example.com", ...overrides, }; } @@ -226,10 +226,10 @@ describe("ClientCache", () => { it("should create separate Clients for agents with different urls", () => { const agents = { - researcher: makeAgent({ url: "https://server-a.langsmith.dev" }), + researcher: makeAgent({ url: "https://server-a.example.com" }), analyst: makeAgent({ name: "analyst", - url: "https://server-b.langsmith.dev", + url: "https://server-b.example.com", }), }; const cache = new ClientCache(agents); @@ -252,9 +252,10 @@ describe("ClientCache", () => { expect(client1).not.toBe(client2); }); - it("should add x-auth-scheme: langsmith header by default", () => { - // Verified indirectly: agents with and without x-auth-scheme explicitly - // set should produce the same resolved headers → same cache key → same Client. + it("should not inject any default auth headers", () => { + // No x-auth-scheme header is added by default. + // An agent with the header explicitly set and one without should produce + // different cache keys and therefore different Client instances. const agents = { withHeader: makeAgent({ name: "withHeader", @@ -268,25 +269,25 @@ describe("ClientCache", () => { const cache = new ClientCache(agents); const client1 = cache.getClient("withHeader"); const client2 = cache.getClient("withoutHeader"); - // Same resolved headers → same cache key → same Client - expect(client1).toBe(client2); + // Different headers → different cache key → different Clients + expect(client1).not.toBe(client2); }); - it("should not overwrite a custom x-auth-scheme header", () => { + it("should pass user-supplied headers through unchanged", () => { const agents = { custom: makeAgent({ name: "custom", headers: { "x-auth-scheme": "custom-auth" }, }), - default: makeAgent({ - name: "default", - headers: {}, + other: makeAgent({ + name: "other", + headers: { "x-auth-scheme": "other-auth" }, }), }; const cache = new ClientCache(agents); const client1 = cache.getClient("custom"); - const client2 = cache.getClient("default"); - // Different x-auth-scheme → different cache key → different Clients + const client2 = cache.getClient("other"); + // Different custom headers → different cache key → different Clients expect(client1).not.toBe(client2); }); diff --git a/libs/deepagents/src/middleware/async_subagents.ts b/libs/deepagents/src/middleware/async_subagents.ts index 4bd0b9bf5..57b89bad9 100644 --- a/libs/deepagents/src/middleware/async_subagents.ts +++ b/libs/deepagents/src/middleware/async_subagents.ts @@ -11,14 +11,20 @@ import { z } from "zod/v4"; import type { AnySubAgent } from "../types.js"; /** - * Specification for an async subagent running on a remote LangGraph server. + * Specification for an async subagent running on an Agent Protocol server. * - * Async subagents connect to LangGraph deployments via the LangGraph SDK. - * They run as background tasks that the main agent can monitor and update. + * Async subagents connect to any Agent Protocol-compliant server via the + * LangGraph SDK. They run as background tasks that the main agent can + * monitor and update. + * + * Options include LangGraph Platform (managed), or self-hosted via + * `langgraph build` deployed to your own infrastructure (ECS, GKE, etc.) + * with Postgres and Redis. * * Authentication is handled via environment variables (`LANGGRAPH_API_KEY`, * `LANGSMITH_API_KEY`, or `LANGCHAIN_API_KEY`), which the LangGraph SDK - * reads automatically. + * reads automatically. For self-hosted servers that require custom auth, + * pass the relevant headers via the `headers` field. */ export interface AsyncSubAgent { /** Unique identifier for the async subagent. */ @@ -27,13 +33,13 @@ export interface AsyncSubAgent { /** What this subagent does. The main agent uses this to decide when to delegate. */ description: string; - /** The graph name or assistant ID on the remote server. */ + /** The graph name or assistant ID on the Agent Protocol server. */ graphId: string; - /** URL of the LangGraph server. Omit for local ASGI transport. */ + /** URL of the Agent Protocol server. Omit for local ASGI transport. */ url?: string; - /** Additional headers to include in requests to the remote server. */ + /** Additional headers to include in requests to the server. */ headers?: Record; } @@ -183,7 +189,7 @@ export function asyncTasksReducer( * The `{available_agents}` placeholder is replaced at middleware creation * time with a formatted list of configured async subagent names and descriptions. */ -const ASYNC_TASK_TOOL_DESCRIPTION = `Launch an async subagent on a remote LangGraph server. The subagent runs in the background and returns a task ID immediately. +const ASYNC_TASK_TOOL_DESCRIPTION = `Launch an async subagent on a remote Agent Protocol server. The subagent runs in the background and returns a task ID immediately. Available async agent types: {available_agents} @@ -204,9 +210,9 @@ Available async agent types: * critical rules about polling behavior, and guidance on when to use async * subagents vs. synchronous delegation. */ -export const ASYNC_TASK_SYSTEM_PROMPT = `## Async subagents (remote LangGraph servers) +export const ASYNC_TASK_SYSTEM_PROMPT = `## Async subagents (remote Agent Protocol servers) -You have access to async subagent tools that launch background tasks on remote LangGraph servers. +You have access to async subagent tools that launch background tasks on remote Agent Protocol servers. ### Tools: - \`start_async_task\`: Start a new background task. Returns a task ID immediately. @@ -368,7 +374,7 @@ function formatTaskEntry(task: AsyncTask, status: AsyncTaskStatus): string { } /** - * Lazily-created, cached LangGraph SDK clients keyed by (url, headers). + * Lazily-created, cached Agent Protocol SDK clients keyed by (url, headers). * * Agents that share the same URL and headers will reuse a single `Client` * instance, avoiding unnecessary connections. @@ -382,15 +388,11 @@ export class ClientCache { } /** - * Build headers for a remote LangGraph server, adding the default - * `x-auth-scheme: langsmith` header if not already present. + * Build headers for an Agent Protocol server from the agent spec. + * User-supplied headers are passed through as-is with no defaults injected. */ private resolveHeaders(spec: AsyncSubAgent): Record { - const headers = { ...(spec.headers || {}) }; - if (!("x-auth-scheme" in headers)) { - headers["x-auth-scheme"] = "langsmith"; - } - return headers; + return { ...(spec.headers || {}) }; } /** @@ -820,10 +822,13 @@ export interface AsyncSubAgentMiddlewareOptions { * Create middleware that adds async subagent tools to an agent. * * Provides five tools for launching, checking, updating, cancelling, and - * listing background tasks on remote LangGraph deployments. Task state is + * listing background tasks on remote Agent Protocol servers. Task state is * persisted in the `asyncTasks` state channel so it survives * context compaction. * + * Any Agent Protocol-compliant server works — LangGraph Platform (managed), + * or self-hosted via `langgraph build` on your own infrastructure. + * * @throws {Error} If no async subagents are provided or names are duplicated. * * @example @@ -832,7 +837,7 @@ export interface AsyncSubAgentMiddlewareOptions { * asyncSubAgents: [{ * name: "researcher", * description: "Research agent for deep analysis", - * url: "https://my-deployment.langsmith.dev", + * url: "https://my-researcher.example.com", * graphId: "research_agent", * }], * }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4410db81c..9f1f81b02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,49 @@ importers: specifier: ^5.9.3 version: 5.9.3 + examples/agent-protocol-poc: + dependencies: + '@langchain/anthropic': + specifier: ^1.3.18 + version: 1.3.23(@langchain/core@1.1.33(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.29.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0)) + '@langchain/core': + specifier: ^1.1.33 + version: 1.1.33(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.29.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0) + '@langchain/langgraph': + specifier: ^1.1.4 + version: 1.2.3(@langchain/core@1.1.33(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.29.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + deepagents: + specifier: workspace:* + version: link:../../libs/deepagents + dotenv: + specifier: ^17.2.4 + version: 17.3.1 + langchain: + specifier: ^1.2.34 + version: 1.2.36(@langchain/core@1.1.33(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.29.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.29.0(ws@8.19.0)(zod@4.3.6))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0) + zod: + specifier: ^4.3.6 + version: 4.3.6 + + examples/agent-protocol-poc/infra: + dependencies: + aws-cdk-lib: + specifier: ^2.180.0 + version: 2.245.0(constructs@10.6.0) + constructs: + specifier: ^10.0.0 + version: 10.6.0 + devDependencies: + aws-cdk: + specifier: ^2.180.0 + version: 2.1114.1 + tsx: + specifier: ^4.0.0 + version: 4.21.0 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + examples/async-subagents/parallel-research: dependencies: '@langchain/anthropic': @@ -766,6 +809,19 @@ packages: resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} + '@aws-cdk/asset-awscli-v1@2.2.263': + resolution: {integrity: sha512-X9JvcJhYcb7PHs8R7m4zMablO5C9PGb/hYfLnxds9h/rKJu6l7MiXE/SabCibuehxPnuO/vk+sVVJiUWrccarQ==} + + '@aws-cdk/asset-node-proxy-agent-v6@2.1.1': + resolution: {integrity: sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==} + + '@aws-cdk/cloud-assembly-schema@53.9.0': + resolution: {integrity: sha512-Ss7Af943iyyTABqeJS30LylmELpdpGgHzQP87KxO+HGPFIFDsoZymSuU1H5eQAcuuOvcfIPSKA62/lf274UB2A==} + engines: {node: '>= 18.0.0'} + bundledDependencies: + - jsonschema + - semver + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -2793,6 +2849,30 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-cdk-lib@2.245.0: + resolution: {integrity: sha512-Yfeb+wKC6s+Ttm/N93C6vY6ksyCh68WaG/j3N6dalJWTW/V4o6hUolHm+v2c2IofJEUS45c5AF/EEj24e9hfMA==} + engines: {node: '>= 20.0.0'} + peerDependencies: + constructs: ^10.5.0 + bundledDependencies: + - '@balena/dockerignore' + - '@aws-cdk/cloud-assembly-api' + - case + - fs-extra + - ignore + - jsonschema + - minimatch + - punycode + - semver + - table + - yaml + - mime-types + + aws-cdk@2.1114.1: + resolution: {integrity: sha512-jMaKPWQQs1G6AbhfCQG2zGrgAhTxzP0jn4T2CuwONuvcV374dMPhfC/LBAv48ruSOgpCK9x6V1xeO/aH3EchAA==} + engines: {node: '>= 18.0.0'} + hasBin: true + axios@1.13.6: resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} @@ -2921,6 +3001,9 @@ packages: console-table-printer@2.15.0: resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} + constructs@10.6.0: + resolution: {integrity: sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4796,6 +4879,12 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 + '@aws-cdk/asset-awscli-v1@2.2.263': {} + + '@aws-cdk/asset-node-proxy-agent-v6@2.1.1': {} + + '@aws-cdk/cloud-assembly-schema@53.9.0': {} + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -5999,7 +6088,6 @@ snapshots: uuid: 10.0.0 zod: 4.3.6 transitivePeerDependencies: - - '@angular/core' - react - react-dom - svelte @@ -7265,6 +7353,15 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-cdk-lib@2.245.0(constructs@10.6.0): + dependencies: + '@aws-cdk/asset-awscli-v1': 2.2.263 + '@aws-cdk/asset-node-proxy-agent-v6': 2.1.1 + '@aws-cdk/cloud-assembly-schema': 53.9.0 + constructs: 10.6.0 + + aws-cdk@2.1114.1: {} + axios@1.13.6: dependencies: follow-redirects: 1.15.11 @@ -7394,6 +7491,8 @@ snapshots: dependencies: simple-wcswidth: 1.1.2 + constructs@10.6.0: {} + convert-source-map@2.0.0: {} cross-spawn@7.0.6: @@ -8233,7 +8332,6 @@ snapshots: uuid: 11.1.0 zod: 4.3.6 transitivePeerDependencies: - - '@angular/core' - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' - '@opentelemetry/sdk-trace-base' @@ -8254,7 +8352,6 @@ snapshots: uuid: 11.1.0 zod: 4.3.6 transitivePeerDependencies: - - '@angular/core' - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' - '@opentelemetry/sdk-trace-base'