@aws-blocks/bb-agent 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/DESIGN.md +65 -17
  2. package/README.md +73 -6
  3. package/dist/agent.aws.d.ts +15 -1
  4. package/dist/agent.aws.d.ts.map +1 -1
  5. package/dist/agent.aws.js +49 -0
  6. package/dist/agent.d.ts +82 -14
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +238 -57
  9. package/dist/agentcore-bundle.d.ts +12 -0
  10. package/dist/agentcore-bundle.d.ts.map +1 -0
  11. package/dist/agentcore-bundle.js +150 -0
  12. package/dist/agentcore-bundle.test.d.ts +2 -0
  13. package/dist/agentcore-bundle.test.d.ts.map +1 -0
  14. package/dist/agentcore-bundle.test.js +46 -0
  15. package/dist/agentcore-entry.d.ts +21 -0
  16. package/dist/agentcore-entry.d.ts.map +1 -0
  17. package/dist/agentcore-entry.js +120 -0
  18. package/dist/agentcore-runtime.cdk.d.ts +27 -0
  19. package/dist/agentcore-runtime.cdk.d.ts.map +1 -0
  20. package/dist/agentcore-runtime.cdk.js +168 -0
  21. package/dist/index.aws.d.ts +1 -0
  22. package/dist/index.aws.d.ts.map +1 -1
  23. package/dist/index.cdk.d.ts +10 -4
  24. package/dist/index.cdk.d.ts.map +1 -1
  25. package/dist/index.cdk.js +27 -28
  26. package/dist/index.cdk.test.js +128 -51
  27. package/dist/index.mock.d.ts +1 -0
  28. package/dist/index.mock.d.ts.map +1 -1
  29. package/dist/index.test.js +404 -1
  30. package/dist/model-factory.d.ts +2 -2
  31. package/dist/model-factory.d.ts.map +1 -1
  32. package/dist/model-factory.js +2 -2
  33. package/dist/providers/canned.d.ts +8 -1
  34. package/dist/providers/canned.d.ts.map +1 -1
  35. package/dist/providers/canned.js +127 -42
  36. package/dist/types.d.ts +63 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/package.json +16 -9
  41. package/src/agent.aws.ts +58 -1
  42. package/src/agent.ts +269 -56
  43. package/src/agentcore-bundle.test.ts +52 -0
  44. package/src/agentcore-bundle.ts +162 -0
  45. package/src/agentcore-entry.ts +134 -0
  46. package/src/agentcore-runtime.cdk.ts +203 -0
  47. package/src/index.aws.ts +3 -0
  48. package/src/index.cdk.test.ts +145 -53
  49. package/src/index.cdk.ts +29 -31
  50. package/src/index.mock.ts +3 -0
  51. package/src/index.test.ts +449 -1
  52. package/src/model-factory.ts +3 -3
  53. package/src/providers/canned.ts +131 -36
  54. package/src/types.ts +64 -1
  55. package/src/version.ts +1 -1
  56. package/dist/job-event-source.d.ts +0 -19
  57. package/dist/job-event-source.d.ts.map +0 -1
  58. package/dist/job-event-source.js +0 -20
  59. package/src/job-event-source.ts +0 -21
@@ -1,66 +1,143 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  /**
4
- * CDK-side regression tests for the Agent's internal AsyncJob event source.
4
+ * CDK-side tests for the Agent BB.
5
5
  *
6
- * `stream()` submits one job per interactive turn (and a second on HITL resume),
7
- * so the caller is blocked on that job starting. AsyncJob's defaults
8
- * (batchSize 10 / maxBatchingWindowSeconds 5) would add up to 5s of SQS
9
- * batching delay to that human-blocking path, so the Agent opts out at both
10
- * construction sites. These tests pin the opt-out to the synthesized template:
11
- * if the defaults are ever inherited again, the latency regression fails here
12
- * instead of surfacing as a slow agent in production.
13
- *
14
- * Must run under `--conditions=cdk`; otherwise the internal BBs resolve to
15
- * their mock implementations and no CloudFormation resources are produced.
6
+ * Pin that the Agent provisions an AgentCore Runtime for the streaming loop that runs AS the shared
7
+ * Blocks execution role (the same principal as the handler) not a bespoke per-runtime role. That
8
+ * shared role already carries everything the loop touches (Realtime publish via the handler wiring,
9
+ * Bedrock, the conversation/message tables, the session bucket); the Agent adds only the
10
+ * AgentCore-specific bits the scoped `bedrock-agentcore` assume-role trust, Bedrock model access,
11
+ * and the shared handler's permission to INVOKE the runtime and injects the config location so the
12
+ * container loads the same app config as the handler.
16
13
  */
17
- import { test, afterEach } from 'node:test';
14
+ import { test, before, after } from 'node:test';
18
15
  import assert from 'node:assert';
16
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
19
  import * as cdk from 'aws-cdk-lib';
20
20
  import { Template } from 'aws-cdk-lib/assertions';
21
- import { Scope, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
21
+ import { BlocksStack, BlocksPresets } from '@aws-blocks/core/cdk';
22
+ import { LambdaCompute } from '@aws-blocks/bb-lambda-compute/cdk';
22
23
  import { Agent } from './index.cdk.js';
23
- class StubBlocksStack extends cdk.Stack {
24
- handler;
25
- executionRole;
26
- id;
27
- constructor(scope, id) {
28
- super(scope, id);
29
- this.id = id;
30
- globalThis.CURRENT_BLOCKS_STACK = this;
31
- this.executionRole = new cdk.aws_iam.Role(this, 'BlocksRole', {
32
- assumedBy: new cdk.aws_iam.ServicePrincipal('lambda.amazonaws.com'),
33
- });
34
- this.handler = new cdk.aws_lambda.Function(this, 'StubHandler', {
35
- runtime: DEFAULT_NODE_RUNTIME,
36
- handler: 'index.handler',
37
- code: cdk.aws_lambda.Code.fromInline('exports.handler = async () => {};'),
38
- role: this.executionRole,
39
- });
40
- }
41
- }
42
- // synthAgent() installs its own stack as the ambient CURRENT_BLOCKS_STACK; clear it
43
- // afterwards so no test observes a stack left behind by the previous one (node
44
- // --test isolates files by default, so this is hygiene against future sharing).
45
- afterEach(() => {
46
- delete globalThis.CURRENT_BLOCKS_STACK;
24
+ /**
25
+ * Inject LambdaCompute as the stack's default compute, the same way
26
+ * `@aws-blocks/blocks` does for real apps. `main` (PR #459) made the Agent
27
+ * resolve the stack's `defaultCompute` at construction (the Realtime/AgentCore
28
+ * wiring reads it), so a bare handler-only stub stack (no default compute) can
29
+ * no longer synthesize it — `BlocksStack.create` initializes the default
30
+ * compute the getter resolves to. (`root as never` is core's existing plumbing
31
+ * pattern for the factory signature.)
32
+ */
33
+ const lambdaFactory = (root) => new LambdaCompute(root, 'DefaultCompute');
34
+ /** A real directory to hand fromCodeAsset (any existing dir works for a synth-only test). */
35
+ const ASSET_DIR = dirname(fileURLToPath(import.meta.url));
36
+ let tmpDir;
37
+ let handlerPath;
38
+ let backendPath;
39
+ before(() => {
40
+ // Building Blocks resolve to their mock entry points unless `--conditions=cdk`
41
+ // is active; keep it set so `BlocksStack.create` produces real CloudFormation.
42
+ process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --conditions=cdk`;
43
+ // `BlocksStack.create` needs a real backend handler + backend module on disk
44
+ // (it imports the backend module and points the handler compute at the file).
45
+ tmpDir = mkdtempSync(join(ASSET_DIR, 'tmp-agent-cdk-'));
46
+ handlerPath = join(tmpDir, 'handler.mjs');
47
+ writeFileSync(handlerPath, "export const handler = async () => ({ statusCode: 200, body: '{}' });\n");
48
+ backendPath = join(tmpDir, 'backend.mjs');
49
+ writeFileSync(backendPath, 'export default () => {};\n');
47
50
  });
48
- function synthAgent() {
51
+ after(() => {
52
+ rmSync(tmpDir, { recursive: true, force: true });
53
+ });
54
+ async function synth() {
49
55
  const app = new cdk.App();
50
- const stack = new StubBlocksStack(app, 'teststack');
51
- const parent = new Scope('app');
52
- new Agent(parent, 'agent', { inferenceOnly: true });
53
- const mappings = Template.fromStack(stack).findResources('AWS::Lambda::EventSourceMapping');
54
- const keys = Object.keys(mappings);
55
- assert.strictEqual(keys.length, 1, 'exactly one event source mapping expected for the agent job');
56
- return mappings[keys[0]].Properties;
56
+ // Real BlocksStack (with the shared executionRole/BlocksRole + a LambdaCompute default compute)
57
+ // so the Agent can resolve its default compute. `id` === 'teststack' so BLOCKS_STACK_NAME (derived
58
+ // from the owning root id) resolves to 'teststack', which the injection assertion below matches.
59
+ const stack = await BlocksStack.create(app, 'teststack', {
60
+ backendHandlerPath: handlerPath,
61
+ backendCDKPath: backendPath,
62
+ defaults: BlocksPresets.production,
63
+ defaultComputeFactory: lambdaFactory,
64
+ });
65
+ // agentcoreAssetPath bypasses the synth-time co-bundle (which needs a BlocksStack backend path).
66
+ new Agent(stack, 'agent', { systemPrompt: 'You are a test agent.', agentcoreAssetPath: ASSET_DIR });
67
+ return Template.fromStack(stack);
57
68
  }
58
- test('CDK: the Agent job takes one message per invocation (no batching on the interactive path)', () => {
59
- assert.strictEqual(synthAgent().BatchSize, 1);
69
+ test('CDK: Agent provisions an AgentCore Runtime for the loop', async () => {
70
+ const template = await synth();
71
+ assert.ok(Object.keys(template.findResources('AWS::BedrockAgentCore::Runtime')).length >= 1, 'expected an AWS::BedrockAgentCore::Runtime resource');
72
+ });
73
+ test('CDK: the loop runs as the shared execution role, which carries everything it touches', async () => {
74
+ const template = await synth();
75
+ const json = JSON.stringify(template.toJSON());
76
+ // Realtime publish (both halves) — load-bearing for streaming from the container. Granted to the
77
+ // shared role by the Realtime BB's handler wiring (not by the Agent), and inherited because the
78
+ // loop runs AS that role.
79
+ assert.ok(json.includes('execute-api:ManageConnections'), 'Realtime postToConnection');
80
+ assert.ok(json.includes('dynamodb:Query'), 'connections-table + history query');
81
+ // Model + storage the loop uses (Bedrock granted here; S3/DynamoDB via the child BBs).
82
+ assert.ok(json.includes('bedrock:InvokeModel'), 'Bedrock invoke');
83
+ assert.ok(json.includes('s3:GetObject'), 'session-bucket access (via FileBucket)');
84
+ // The RPC handler (also the shared role) can start the loop.
85
+ assert.ok(json.includes('bedrock-agentcore:InvokeAgentRuntime'), 'InvokeAgentRuntime');
86
+ // The runtime runs AS the shared BlocksRole — not a bespoke per-runtime role. Its RoleArn
87
+ // should reference BlocksRole, and there should be no `RuntimeRole` construct anywhere.
88
+ const runtime = Object.values(template.findResources('AWS::BedrockAgentCore::Runtime'))[0];
89
+ assert.ok(JSON.stringify(runtime.Properties ?? {}).includes('BlocksRole'), 'runtime executionRole should be the shared BlocksRole');
90
+ assert.ok(!json.includes('RuntimeRole'), 'no bespoke per-runtime role should be created');
91
+ // The container is given the config location so loadConfigToProcessEnv() loads the same full app
92
+ // config as the handler (delivers BLOCKS_RT_CALLBACK_URL + any config-backed BB values a tool needs).
93
+ const runtimeJson = JSON.stringify(runtime.Properties ?? {});
94
+ assert.ok(runtimeJson.includes('BLOCKS_CONFIG_BUCKET'), 'runtime must be injected BLOCKS_CONFIG_BUCKET');
95
+ assert.ok(runtimeJson.includes('BLOCKS_CONFIG_KEY'), 'runtime must be injected BLOCKS_CONFIG_KEY');
96
+ // BLOCKS_STACK_NAME must be the owning root id (`backendStackName`) — the SAME value the handler
97
+ // uses to derive resource names — so the container's namespace matches what CDK provisioned. Here
98
+ // the owning BlocksStack's id is 'teststack', so backendStackName resolves to 'teststack'.
99
+ assert.ok(runtimeJson.includes('BLOCKS_STACK_NAME'), 'runtime must be injected BLOCKS_STACK_NAME');
100
+ assert.ok(runtimeJson.includes('teststack'), 'BLOCKS_STACK_NAME resolves to the owning root id');
60
101
  });
61
- test('CDK: the Agent job has no SQS batching window (no added latency for the caller)', () => {
62
- assert.strictEqual(synthAgent().MaximumBatchingWindowInSeconds, 0);
102
+ test('CDK: the Agent adds the bedrock-agentcore assume-role trust to the shared role (scoped)', async () => {
103
+ // Core is BB-agnostic — it does not trust bedrock-agentcore. The Agent adds that trust here, so
104
+ // the AgentCore Runtime can assume the shared role it runs AS. It must be scoped by
105
+ // aws:SourceAccount (AWS's recommended AgentCore trust policy), and lambda trust must remain.
106
+ const template = await synth();
107
+ const roles = template.findResources('AWS::IAM::Role');
108
+ const blocksRoleId = Object.keys(roles).find(k => k.includes('BlocksRole'));
109
+ assert.ok(blocksRoleId, 'expected the shared BlocksRole');
110
+ const statements = roles[blocksRoleId].Properties.AssumeRolePolicyDocument.Statement;
111
+ const servicesOf = (s) => {
112
+ const svc = s.Principal?.Service;
113
+ return Array.isArray(svc) ? svc : svc ? [svc] : [];
114
+ };
115
+ const services = statements.flatMap(servicesOf);
116
+ assert.ok(services.includes('lambda.amazonaws.com'), 'shared role must stay Lambda-assumable');
117
+ assert.ok(services.includes('bedrock-agentcore.amazonaws.com'), 'shared role must be assumable by the AgentCore Runtime');
118
+ const agentCoreStmt = statements.find(s => servicesOf(s).includes('bedrock-agentcore.amazonaws.com'));
119
+ assert.ok(agentCoreStmt?.Condition, 'AgentCore trust statement must carry a scoping Condition');
120
+ assert.ok(JSON.stringify(agentCoreStmt.Condition).includes('aws:SourceAccount'), 'AgentCore trust must be scoped by aws:SourceAccount');
63
121
  });
64
- test('CDK: the Agent job still reports partial batch failures', () => {
65
- assert.deepStrictEqual(synthAgent().FunctionResponseTypes, ['ReportBatchItemFailures']);
122
+ test('CDK: multiple agents add the identical shared-role grants ONCE, not per-agent', async () => {
123
+ // Every agent would otherwise add the SAME bedrock-agentcore trust / Bedrock / InvokeAgentRuntime
124
+ // statements to the one shared role. They're identical (stack-scoped), so they must be added
125
+ // exactly once regardless of agent count — keeping the shared role's policy from bloating with
126
+ // duplicates. (Overflow into managed policies is a normal CDK mechanism and not asserted against.)
127
+ const app = new cdk.App();
128
+ const stack = await BlocksStack.create(app, 'teststack', {
129
+ backendHandlerPath: handlerPath,
130
+ backendCDKPath: backendPath,
131
+ defaults: BlocksPresets.production,
132
+ defaultComputeFactory: lambdaFactory,
133
+ });
134
+ for (const id of ['agent1', 'agent2', 'agent3']) {
135
+ new Agent(stack, id, { systemPrompt: 'You are a test agent.', agentcoreAssetPath: ASSET_DIR });
136
+ }
137
+ const template = Template.fromStack(stack);
138
+ const json = JSON.stringify(template.toJSON());
139
+ // Three runtimes (per-agent), but the shared-role grant statements appear once (deduped).
140
+ assert.strictEqual(Object.keys(template.findResources('AWS::BedrockAgentCore::Runtime')).length, 3, 'three runtimes');
141
+ const invokeGrants = json.split('bedrock-agentcore:InvokeAgentRuntime').length - 1;
142
+ assert.strictEqual(invokeGrants, 1, `InvokeAgentRuntime should be granted once, found ${invokeGrants}`);
66
143
  });
@@ -1,4 +1,5 @@
1
1
  export { Agent } from './agent.mock.js';
2
+ export type { AgentTurnPayload } from './agent.js';
2
3
  export { AgentErrors, InterruptError } from './errors.js';
3
4
  export { BedrockModels, OllamaModels } from './models.js';
4
5
  export type { AgentConfig, AgentResult, AgentStreamChunk, AgentStreamResult, ToolDefinition, AgentTool, ToolFactory, ToolsConfig, ToolHandlerArgs, DefaultToolContext, InterruptResponse, ToolCallRecord, ModelConfig, StreamOptions, Message, Conversation, JSONValue, TokenUsage } from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAGxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}