Skip to content

Commit cfe6cb0

Browse files
authored
fix(bb-agent): use Lambda execution region for S3Storage (#137)
1 parent e8f3854 commit cfe6cb0

3 files changed

Lines changed: 97 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@aws-blocks/bb-agent": patch
3+
---
4+
5+
fix(bb-agent): use the Lambda execution region for S3Storage (#120)
6+
7+
The deployed Agent constructed Strands' `S3Storage` without a `region`, so it defaulted to `us-east-1` and hard-pinned the snapshot S3 client there. Because the session bucket is created in the deploy region, any deployment outside `us-east-1` failed snapshot reads/writes with a cross-region 301 `PermanentRedirect`. `S3Storage` is now constructed with `region: process.env.AWS_REGION` — which the Lambda runtime always sets to the function's region — so snapshots resolve against the correct regional endpoint. `region` and `s3Client` are mutually exclusive in `S3StorageConfig`, so only `region` is passed.

packages/bb-agent/src/agent.aws.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,29 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import type { ScopeParent } from '@aws-blocks/core';
5-
import { AgentBase } from './agent.js';
5+
import type { FileBucket } from '@aws-blocks/bb-file-bucket';
6+
import type { SnapshotStorage } from '@strands-agents/sdk';
67
import { S3Storage } from '@strands-agents/sdk/session/s3-storage';
8+
import type { S3StorageConfig } from '@strands-agents/sdk/session/s3-storage';
9+
import { AgentBase } from './agent.js';
710
import type { AgentConfig, DefaultToolContext } from './types.js';
811
import { BedrockModels } from './models.js';
912

13+
/**
14+
* Builds the deployed Agent's snapshot storage, pinning S3Storage to the Lambda
15+
* execution region (`AWS_REGION`) so non-us-east-1 deploys use the correct regional
16+
* endpoint (#120). `S3StorageImpl` is injectable so tests can assert the resulting
17+
* config without depending on S3Storage/AWS SDK internals; production uses the real one.
18+
*/
19+
export function createDeployedSnapshotStorage(
20+
bucket: FileBucket,
21+
S3StorageImpl: new (config: S3StorageConfig) => SnapshotStorage = S3Storage,
22+
): SnapshotStorage {
23+
return new S3StorageImpl({ bucket: bucket.fullId, region: process.env.AWS_REGION });
24+
}
25+
1026
export class Agent<TContext = DefaultToolContext> extends AgentBase<TContext> {
1127
constructor(scope: ScopeParent, id: string, config: AgentConfig<TContext>) {
12-
super(scope, id, config, config.model?.deployed ?? BedrockModels.BALANCED, (bucket) => new S3Storage({ bucket: bucket.fullId }));
28+
super(scope, id, config, config.model?.deployed ?? BedrockModels.BALANCED, createDeployedSnapshotStorage);
1329
}
1430
}

packages/bb-agent/src/index.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,3 +1052,75 @@ describe('OllamaModels presets', () => {
10521052
}
10531053
});
10541054
});
1055+
1056+
// ── deployed Agent S3Storage region (multi-region) ──────────────────────────
1057+
// Regression for #120: the deployed (aws-runtime) Agent must build Strands' S3Storage
1058+
// with the Lambda execution region (AWS_REGION). Omitting `region` defaults S3Storage to
1059+
// us-east-1 and breaks deploys elsewhere — the session bucket lives in the deploy region,
1060+
// so snapshots hit a cross-region 301 PermanentRedirect. index.test.ts otherwise only
1061+
// drives the mock/local path, so this covers agent.aws.ts by spying the S3Storage ctor.
1062+
1063+
import { Agent as DeployedAgent } from './index.aws.js';
1064+
import { createDeployedSnapshotStorage } from './agent.aws.js';
1065+
import type { SnapshotStorage, SnapshotManifest } from '@strands-agents/sdk';
1066+
import type { S3StorageConfig } from '@strands-agents/sdk/session/s3-storage';
1067+
1068+
describe('deployed Agent S3Storage region (multi-region)', () => {
1069+
// Type-safe stand-in for S3Storage that records every config it's constructed with,
1070+
// so we assert exactly what agent.aws.ts passes in — no S3Storage/AWS SDK internals.
1071+
function s3StorageSpy() {
1072+
const configs: S3StorageConfig[] = [];
1073+
class Spy implements SnapshotStorage {
1074+
constructor(config: S3StorageConfig) {
1075+
configs.push(config);
1076+
}
1077+
async saveSnapshot(): Promise<void> {}
1078+
async loadSnapshot(): Promise<null> {
1079+
return null;
1080+
}
1081+
async listSnapshotIds(): Promise<string[]> {
1082+
return [];
1083+
}
1084+
async deleteSession(): Promise<void> {}
1085+
async loadManifest(): Promise<SnapshotManifest> {
1086+
return { schemaVersion: '1.0', updatedAt: new Date().toISOString() };
1087+
}
1088+
async saveManifest(): Promise<void> {}
1089+
}
1090+
return { configs, Spy };
1091+
}
1092+
1093+
// Capture the S3StorageConfig the deployed factory builds for a given AWS_REGION.
1094+
function configForRegion(region: string, scopeId: string): S3StorageConfig {
1095+
const prev = process.env.AWS_REGION;
1096+
process.env.AWS_REGION = region;
1097+
try {
1098+
const { configs, Spy } = s3StorageSpy();
1099+
const bucket = new FileBucket(new Scope(scopeId), 'sn');
1100+
createDeployedSnapshotStorage(bucket, Spy);
1101+
assert.strictEqual(configs.length, 1, 'S3Storage should be constructed exactly once');
1102+
assert.strictEqual(configs[0].bucket, bucket.fullId, 'session bucket id should be passed through');
1103+
return configs[0];
1104+
} finally {
1105+
if (prev === undefined) delete process.env.AWS_REGION;
1106+
else process.env.AWS_REGION = prev;
1107+
}
1108+
}
1109+
1110+
test('constructs S3Storage with the Lambda execution region (eu-west-1)', () => {
1111+
const config = configForRegion('eu-west-1', 'test-s3-region-euw1');
1112+
assert.strictEqual(config.region, 'eu-west-1');
1113+
assert.notStrictEqual(config.region, 'us-east-1');
1114+
});
1115+
1116+
test('does not hard-pin us-east-1 when deployed to another region (ap-southeast-2)', () => {
1117+
const config = configForRegion('ap-southeast-2', 'test-s3-region-apse2');
1118+
assert.strictEqual(config.region, 'ap-southeast-2');
1119+
assert.notStrictEqual(config.region, 'us-east-1');
1120+
});
1121+
1122+
test('deployed Agent constructs on the aws-runtime path', () => {
1123+
const agent = new DeployedAgent(new Scope('test-s3-agent'), 'r', { systemPrompt: 'test', model: { deployed: { provider: 'canned' } } });
1124+
assert.ok(agent);
1125+
});
1126+
});

0 commit comments

Comments
 (0)