@aws-blocks/bb-agent 0.3.4 → 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 (57) hide show
  1. package/DESIGN.md +65 -16
  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 -55
  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 -26
  26. package/dist/index.cdk.test.d.ts +2 -0
  27. package/dist/index.cdk.test.d.ts.map +1 -0
  28. package/dist/index.cdk.test.js +143 -0
  29. package/dist/index.mock.d.ts +1 -0
  30. package/dist/index.mock.d.ts.map +1 -1
  31. package/dist/index.test.js +404 -1
  32. package/dist/model-factory.d.ts +2 -2
  33. package/dist/model-factory.d.ts.map +1 -1
  34. package/dist/model-factory.js +2 -2
  35. package/dist/providers/canned.d.ts +8 -1
  36. package/dist/providers/canned.d.ts.map +1 -1
  37. package/dist/providers/canned.js +127 -42
  38. package/dist/types.d.ts +63 -1
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +16 -9
  43. package/src/agent.aws.ts +58 -1
  44. package/src/agent.ts +269 -54
  45. package/src/agentcore-bundle.test.ts +52 -0
  46. package/src/agentcore-bundle.ts +162 -0
  47. package/src/agentcore-entry.ts +134 -0
  48. package/src/agentcore-runtime.cdk.ts +203 -0
  49. package/src/index.aws.ts +3 -0
  50. package/src/index.cdk.test.ts +167 -0
  51. package/src/index.cdk.ts +29 -29
  52. package/src/index.mock.ts +3 -0
  53. package/src/index.test.ts +449 -1
  54. package/src/model-factory.ts +3 -3
  55. package/src/providers/canned.ts +131 -36
  56. package/src/types.ts +64 -1
  57. package/src/version.ts +1 -1
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Serve a registered Agent on the AgentCore harness.
3
+ *
4
+ * The developer's backend must already have been imported in THIS process (so the Agent
5
+ * registered itself in the shared registry) — either by `main()` below (standalone launch
6
+ * via `BB_AGENT_BACKEND_MODULE`) or by the co-bundle (agentcore-bundle.ts) that imports the
7
+ * backend and this `serve` from the same bb-agent module instance. Co-bundling is required
8
+ * because the registry is a module singleton — a split would put the Agent in one map and
9
+ * the lookup in another.
10
+ *
11
+ * @param agentId - fullId of the target Agent (defaults to process.env.BB_AGENT_ID)
12
+ */
13
+ export declare function serve(agentId?: string | undefined): void;
14
+ /**
15
+ * Standalone launch: load config, import the developer backend by path, then serve.
16
+ * Used when the artifact runs this file directly with BB_AGENT_BACKEND_MODULE pointing at the
17
+ * backend. When an app co-bundles the backend with `serve` (agentcore-bundle.ts), it calls
18
+ * `serve()` directly instead and this `main()` is not the entry.
19
+ */
20
+ export declare function main(): Promise<void>;
21
+ //# sourceMappingURL=agentcore-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentcore-entry.d.ts","sourceRoot":"","sources":["../src/agentcore-entry.ts"],"names":[],"mappings":"AAkDA;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,OAAO,qBAA0B,GAAG,IAAI,CAyC7D;AAED;;;;;GAKG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAc1C"}
@@ -0,0 +1,120 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * AgentCore Runtime entrypoint for the Agent BB.
5
+ *
6
+ * Hosts the developer's real Agent — the same instance the Lambda handler would build — on
7
+ * the `BedrockAgentCoreApp` harness (implements the `/invocations` + `/ping` contract on port
8
+ * 8080), and runs the agent loop as a **background async task** that streams chunks to the
9
+ * browser over the Realtime BB (exactly as the loop does on Lambda today).
10
+ *
11
+ * Why background + Realtime (not the harness's SSE response): the browser never holds a
12
+ * connection to AgentCore — it subscribes to a Realtime channel by `channelId`. So the
13
+ * invocation must return immediately while the loop keeps running server-side. The harness
14
+ * keeps the microVM alive (up to the 8h session lifetime) while an async task is in flight —
15
+ * `/ping` reports `HealthyBusy` — via `addAsyncTask()`/`completeAsyncTask()`. `runAgent()`
16
+ * publishes every chunk to Realtime under the runtime's execution role.
17
+ *
18
+ * How the developer's agent definition reaches this process:
19
+ * The `tools` callback in AgentConfig is a JS closure and can't be serialized across a
20
+ * process boundary. So instead of shipping data, we ship code: this entrypoint imports
21
+ * the SAME developer backend module the Lambda handler imports (co-bundled with
22
+ * `--conditions=aws-runtime`, so `new Agent()` resolves to the AWS runtime class). That
23
+ * construction registers the live Agent in the instance registry (see agent.ts); we look
24
+ * it up by the `BB_AGENT_ID` the CDK Runtime construct set, and drive its loop.
25
+ *
26
+ * Launched by the CodeZip artifact as: ['main.js'] (the co-bundle from agentcore-bundle.ts).
27
+ */
28
+ import { loadConfigToProcessEnv } from '@aws-blocks/core';
29
+ import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
30
+ import { z } from 'zod';
31
+ import { getAgentInstance } from './agent.js';
32
+ /** Request contract — mirrors the Lambda jobPayloadSchema, minus transport-only fields. */
33
+ const requestSchema = z.object({
34
+ /** User prompt. Empty on resume (interruptResponses drive the turn instead). */
35
+ prompt: z.string().default(''),
36
+ /** Realtime channel the client subscribes to for this turn's chunks. */
37
+ channelId: z.string(),
38
+ /** Conversation to persist to / restore the session from. Falls back to the AgentCore session id. */
39
+ conversationId: z.string().optional(),
40
+ /** Owner of the conversation. Required when persistence is enabled (not inferenceOnly). */
41
+ userId: z.string().optional(),
42
+ /** HITL resume: approval responses to apply instead of a new prompt. */
43
+ interruptResponses: z.array(z.object({ interruptId: z.string(), response: z.string() })).optional(),
44
+ /** Per-call tool context, threaded through to tool handlers. Must be JSON-serializable. */
45
+ context: z.unknown().optional(),
46
+ });
47
+ /**
48
+ * Serve a registered Agent on the AgentCore harness.
49
+ *
50
+ * The developer's backend must already have been imported in THIS process (so the Agent
51
+ * registered itself in the shared registry) — either by `main()` below (standalone launch
52
+ * via `BB_AGENT_BACKEND_MODULE`) or by the co-bundle (agentcore-bundle.ts) that imports the
53
+ * backend and this `serve` from the same bb-agent module instance. Co-bundling is required
54
+ * because the registry is a module singleton — a split would put the Agent in one map and
55
+ * the lookup in another.
56
+ *
57
+ * @param agentId - fullId of the target Agent (defaults to process.env.BB_AGENT_ID)
58
+ */
59
+ export function serve(agentId = process.env.BB_AGENT_ID) {
60
+ if (!agentId)
61
+ throw new Error('BB_AGENT_ID is required (fullId of the target Agent).');
62
+ const agent = getAgentInstance(agentId);
63
+ if (!agent) {
64
+ throw new Error(`No Agent registered with id '${agentId}'. Ensure the backend module constructs it at import time.`);
65
+ }
66
+ const app = new BedrockAgentCoreApp({
67
+ invocationHandler: {
68
+ requestSchema,
69
+ process: (request, context) => {
70
+ // AgentCore routes every invocation for a session to the same warm microVM.
71
+ // runtimeSessionId maps to the Agent BB's conversationId (session state key).
72
+ const conversationId = request.conversationId ?? context.sessionId;
73
+ // Run the turn as a background async task so this invocation returns immediately.
74
+ // The harness reports `/ping` = HealthyBusy while the task is in flight, keeping
75
+ // the microVM alive (up to the 8h session lifetime) until the loop completes.
76
+ // Chunks are delivered out-of-band via Realtime — this HTTP response is just an ack.
77
+ const taskId = app.addAsyncTask('agent-turn');
78
+ void agent
79
+ .invokeTurn({
80
+ message: request.prompt,
81
+ conversationId,
82
+ channelId: request.channelId,
83
+ userId: request.userId ?? 'anonymous',
84
+ interruptResponses: request.interruptResponses,
85
+ context: request.context,
86
+ })
87
+ .finally(() => app.completeAsyncTask(taskId));
88
+ // The client already has channelId (from the RPC that invoked us) and subscribes
89
+ // to Realtime; it does not consume this response body.
90
+ return { channelId: request.channelId, status: 'accepted' };
91
+ },
92
+ },
93
+ });
94
+ app.run();
95
+ }
96
+ /**
97
+ * Standalone launch: load config, import the developer backend by path, then serve.
98
+ * Used when the artifact runs this file directly with BB_AGENT_BACKEND_MODULE pointing at the
99
+ * backend. When an app co-bundles the backend with `serve` (agentcore-bundle.ts), it calls
100
+ * `serve()` directly instead and this `main()` is not the entry.
101
+ */
102
+ export async function main() {
103
+ // Same cold-start contract as the Lambda handler: pull BB resource identifiers
104
+ // (table names, bucket names, Realtime callback URL) into process.env before importing
105
+ // the backend, so BB constructors can resolve them.
106
+ await loadConfigToProcessEnv();
107
+ const backendModule = process.env.BB_AGENT_BACKEND_MODULE;
108
+ if (!backendModule)
109
+ throw new Error('BB_AGENT_BACKEND_MODULE env var is required (path to the developer backend module).');
110
+ // Import the developer backend — constructing the real Agent, which registers itself.
111
+ await import(backendModule);
112
+ serve();
113
+ }
114
+ // Auto-run the standalone launch path ONLY when a backend module path is provided.
115
+ // The co-bundle path (agentcore-bundle.ts) imports `serve` and invokes it directly after
116
+ // importing the backend inline, and does NOT set BB_AGENT_BACKEND_MODULE — so `main()` must
117
+ // not fire there (it would double-serve and throw on the missing env var).
118
+ if (process.env.BB_AGENT_BACKEND_MODULE) {
119
+ void main();
120
+ }
@@ -0,0 +1,27 @@
1
+ import { Runtime } from 'aws-cdk-lib/aws-bedrockagentcore';
2
+ import { Scope } from '@aws-blocks/core/cdk';
3
+ import type { ScopeParent } from '@aws-blocks/core';
4
+ /** References the agent loop needs, handed in by the Agent CDK constructor. */
5
+ export interface AgentCoreRuntimeProps {
6
+ /** fullId of the owning Agent — the container looks the Agent up by this (`BB_AGENT_ID`). */
7
+ agentFullId: string;
8
+ /**
9
+ * Pre-built asset dir to use instead of co-bundling at synth. Set by unit tests and apps
10
+ * that pre-bundle; when omitted, the backend module is co-bundled from the BlocksStack.
11
+ */
12
+ agentcoreAssetPath?: string;
13
+ }
14
+ export declare class AgentCoreRuntime extends Scope {
15
+ /** The provisioned runtime, or undefined when no backend asset could be resolved (isolated tests). */
16
+ readonly runtime?: Runtime;
17
+ /** The runtime ARN (empty string when not provisioned). */
18
+ readonly runtimeArn: string;
19
+ constructor(scope: ScopeParent, id: string, props: AgentCoreRuntimeProps);
20
+ /**
21
+ * Co-bundle the app backend + `serve()` into an AgentCore code-asset dir. Returns undefined
22
+ * when the backend module path can't be discovered (isolated unit tests) — the caller then
23
+ * skips provisioning rather than failing synth.
24
+ */
25
+ private buildAsset;
26
+ }
27
+ //# sourceMappingURL=agentcore-runtime.cdk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentcore-runtime.cdk.d.ts","sourceRoot":"","sources":["../src/agentcore-runtime.cdk.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAqE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAE9H,OAAO,EAAqC,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACrC,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IAC1C,sGAAsG;IACtG,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB;IAuIxE;;;;OAIG;IACH,OAAO,CAAC,UAAU;CAWlB"}
@@ -0,0 +1,168 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Self-contained CDK provisioning for the Agent BB's AgentCore Runtime.
5
+ *
6
+ * This class owns EVERYTHING AgentCore-specific: the synth-time co-bundle of the app backend,
7
+ * the `Runtime` construct (via `fromCodeAsset` — Node 22 CodeZip, no Docker), the shared role's
8
+ * AgentCore trust + grants, the container env injection, and the grant that lets the app's RPC
9
+ * handler invoke the runtime. It is deliberately kept in one place, with no AgentCore details
10
+ * leaking into the `Agent` CDK constructor or into core, so it can later fold into a per-BB
11
+ * compute abstraction (should one land) without touching call sites — the Agent just constructs
12
+ * it and hands over references to the BBs the loop uses.
13
+ *
14
+ * The loop runs INSIDE this runtime AS the shared Blocks execution role (the same role the Lambda
15
+ * handler runs as), so it inherits every Building Block's grants — including the Realtime publish
16
+ * permissions already granted to the handler, so it streams chunks to the browser via the Realtime
17
+ * BB with no extra grant. The container loads the full app config (via the injected config-bucket
18
+ * location) exactly as the handler does, so it discovers the Realtime callback URL and every other
19
+ * registerConfig() value a tool's BB may need. This class adds to that shared role only what's
20
+ * AgentCore-specific: the `bedrock-agentcore` assume-role trust, Bedrock model access, and the
21
+ * handler's `InvokeAgentRuntime` permission. Inbound auth is IAM (SigV4): the RPC handler
22
+ * invokes with its own credentials; the browser never talks to the runtime directly.
23
+ */
24
+ import { mkdirSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import * as cdk from 'aws-cdk-lib';
27
+ import { AgentCoreRuntime as AgentCoreRuntimeVersion, AgentRuntimeArtifact, Runtime } from 'aws-cdk-lib/aws-bedrockagentcore';
28
+ import { Effect, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
29
+ import { getConfigLocation, registerConfig, Scope } from '@aws-blocks/core/cdk';
30
+ import { bundleAgentCoreAsset } from './agentcore-bundle.js';
31
+ export class AgentCoreRuntime extends Scope {
32
+ /** The provisioned runtime, or undefined when no backend asset could be resolved (isolated tests). */
33
+ runtime;
34
+ /** The runtime ARN (empty string when not provisioned). */
35
+ runtimeArn;
36
+ constructor(scope, id, props) {
37
+ super(id, { parent: scope });
38
+ const assetPath = props.agentcoreAssetPath ?? this.buildAsset();
39
+ if (!assetPath) {
40
+ // No backend module discoverable (e.g. an isolated unit test that constructs the
41
+ // Agent without a BlocksStack) — skip provisioning rather than failing synth.
42
+ this.runtimeArn = '';
43
+ return;
44
+ }
45
+ const stack = cdk.Stack.of(this);
46
+ // Run the loop AS the shared Blocks execution role (`role: this.executionRole`) — the same
47
+ // role the Lambda handler runs as. Because every Building Block grants its runtime permissions
48
+ // to this role (a tool that uses KVStore/tables/etc. grants there too), the AgentCore
49
+ // container inherits them all automatically — no bespoke, under-granted role, and no need to
50
+ // mirror each BB's grants. One shared role that any compute can assume keeps this a drop-in
51
+ // for a future per-BB compute abstraction.
52
+ const role = this.executionRole;
53
+ // Add the agent's shared-role trust + grants ONCE per stack. Every agent instance would
54
+ // otherwise add the SAME trust / Bedrock / InvokeAgentRuntime statements to the ONE shared
55
+ // role; with several agents in an app that piles up duplicates and overflows the IAM inline-
56
+ // policy size limit (CDK then spills into `OverflowPolicy` managed policies and the role
57
+ // misbehaves). These are identical for every agent — Bedrock models and the runtime-ARN
58
+ // wildcard are stack-scoped — so doing it once covers every agent's container. (Realtime
59
+ // publish, the session bucket, and the conversation/message tables are already granted to the
60
+ // shared role by the Realtime BB's handler wiring and the Agent's FileBucket/DistributedTable
61
+ // children, so they're not repeated here — the loop inherits them by running as the role.)
62
+ const SHARED_GRANTS_KEY = Symbol.for('BLOCKS_AGENT_RUNTIME_SHARED_ROLE_GRANTS');
63
+ const stackAny = stack;
64
+ if (!stackAny[SHARED_GRANTS_KEY]) {
65
+ // The container publishes to Realtime AS this shared role, which already holds the publish
66
+ // grants (the Realtime BB grants postToConnection + the connections table to the handler on
67
+ // the same role) — so no Realtime IAM grant is needed here; it's inherited by running as the role.
68
+ // Trust: let the AgentCore Runtime assume this shared role (it runs AS the role). Added here
69
+ // rather than in core, so the role only trusts `bedrock-agentcore` when an Agent exists.
70
+ // Scope it to this account/region with aws:SourceAccount + aws:SourceArn — AWS's recommended
71
+ // AgentCore Runtime trust policy — so only AgentCore runtimes in THIS account can assume the
72
+ // role (tightens the confused-deputy surface) without breaking assumption. `assumeRolePolicy`
73
+ // exists only on the concrete `Role`; core always creates BlocksRole concretely, so narrow
74
+ // and fail loud if that ever changes.
75
+ if (!(role instanceof Role)) {
76
+ throw new Error('AgentCore Runtime requires the shared Blocks execution role to be a concrete iam.Role to add its assume-role trust');
77
+ }
78
+ role.assumeRolePolicy?.addStatements(new PolicyStatement({
79
+ effect: Effect.ALLOW,
80
+ principals: [new ServicePrincipal('bedrock-agentcore.amazonaws.com')],
81
+ actions: ['sts:AssumeRole'],
82
+ conditions: {
83
+ StringEquals: { 'aws:SourceAccount': stack.account },
84
+ ArnLike: { 'aws:SourceArn': `arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:*` },
85
+ },
86
+ }));
87
+ // Bedrock model access for the loop (no Building Block grants this, and the loop no longer
88
+ // runs on the handler).
89
+ role.addToPrincipalPolicy(new PolicyStatement({
90
+ actions: [
91
+ 'bedrock:InvokeModel',
92
+ 'bedrock:InvokeModelWithResponseStream',
93
+ 'bedrock:GetFoundationModel',
94
+ 'bedrock:ListFoundationModels',
95
+ 'bedrock:GetInferenceProfile',
96
+ ],
97
+ resources: [`arn:${stack.partition}:bedrock:*::foundation-model/*`, `arn:${stack.partition}:bedrock:*:*:inference-profile/*`],
98
+ }));
99
+ // Let the app's RPC handler (also the shared role) invoke the runtimes — stream()/resume()
100
+ // call InvokeAgentRuntime. Scope to a wildcard runtime ARN rather than a specific runtime's
101
+ // ARN: the runtime uses this same shared role as its executionRole, so referencing its ARN
102
+ // here would create a Role→Runtime→Role dependency cycle. (Same wildcard style as Bedrock.)
103
+ role.addToPrincipalPolicy(new PolicyStatement({
104
+ actions: ['bedrock-agentcore:InvokeAgentRuntime'],
105
+ resources: [
106
+ `arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:runtime/*`,
107
+ `arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:runtime/*/*`,
108
+ ],
109
+ }));
110
+ stackAny[SHARED_GRANTS_KEY] = true;
111
+ }
112
+ // Inject the config location so the container loads the FULL app config via
113
+ // loadConfigToProcessEnv() — the same config the Lambda handler loads. That's how it gets
114
+ // BLOCKS_RT_CALLBACK_URL (registered by the Realtime BB) plus any other config-backed BB value
115
+ // an agent's tools touch; without it the container runs with empty config and those BBs fail.
116
+ // Idempotent (one config bucket per stack); IAM to read it is inherited via the shared role.
117
+ const { bucketName: configBucketName, key: configKey } = getConfigLocation(this);
118
+ const runtime = new Runtime(this, 'AgentRuntime', {
119
+ agentRuntimeArtifact: AgentRuntimeArtifact.fromCodeAsset({
120
+ path: assetPath,
121
+ runtime: AgentCoreRuntimeVersion.NODE_22,
122
+ // Launch command, NOT a Lambda file.export handler. Single element = the .js file;
123
+ // the NODE_22 runtime invokes `node` itself. (A leading 'node' element is rejected.)
124
+ entrypoint: ['main.js'],
125
+ }),
126
+ executionRole: role,
127
+ // Inbound auth defaults to IAM (SigV4): the RPC handler invokes with its own creds.
128
+ // The browser never invokes the runtime directly (it subscribes to Realtime).
129
+ environmentVariables: {
130
+ // The Agent's fullId so the container's getAgentInstance(BB_AGENT_ID) matches the
131
+ // Agent the co-bundled backend registers at import.
132
+ BB_AGENT_ID: props.agentFullId,
133
+ // The namespace the container rebuilds fullId (and every derived resource name) from.
134
+ // MUST be the owning stack/backend's canonical root id — the SAME value the Lambda
135
+ // handler and the Lambda compute use (`backendStackName`) — not the raw CFN stack name.
136
+ // They coincide for a top-level BlocksStack, but for a BlocksBackend embedded in a
137
+ // customer stack the handler uses the backend fullId while cdk.Stack.of(this).stackName
138
+ // is the customer stack name; using the latter would make the container derive names from
139
+ // the wrong namespace and miss its own tables/bucket/config.
140
+ BLOCKS_STACK_NAME: this.backendStackName,
141
+ // Config location so the container's loadConfigToProcessEnv() loads the full app config
142
+ // (same as the handler) — this delivers BLOCKS_RT_CALLBACK_URL and every other
143
+ // registerConfig() value a tool's BB may read. IAM to read it is inherited (shared role).
144
+ BLOCKS_CONFIG_BUCKET: configBucketName,
145
+ BLOCKS_CONFIG_KEY: configKey,
146
+ },
147
+ });
148
+ this.runtime = runtime;
149
+ this.runtimeArn = runtime.agentRuntimeArn;
150
+ // Expose THIS agent's runtime ARN to the Lambda runtime path so stream() can resolve it at
151
+ // call time. Per-agent (each agent has its own runtime), so it stays outside the shared guard.
152
+ registerConfig(this, `BB_AGENT_${props.agentFullId}_RUNTIME_ARN`, runtime.agentRuntimeArn);
153
+ }
154
+ /**
155
+ * Co-bundle the app backend + `serve()` into an AgentCore code-asset dir. Returns undefined
156
+ * when the backend module path can't be discovered (isolated unit tests) — the caller then
157
+ * skips provisioning rather than failing synth.
158
+ */
159
+ buildAsset() {
160
+ const stack = globalThis.CURRENT_BLOCKS_STACK;
161
+ const backendModulePath = stack?.backendModulePath;
162
+ if (!backendModulePath)
163
+ return undefined;
164
+ const outDir = join(cdk.App.of(this)?.outdir ?? cdk.Stack.of(this).node.tryGetContext('cdk.out') ?? '.cdk-agentcore', `agentcore-${this.fullId}`);
165
+ mkdirSync(outDir, { recursive: true });
166
+ return bundleAgentCoreAsset(backendModulePath, outDir);
167
+ }
168
+ }
@@ -1,4 +1,5 @@
1
1
  export { Agent } from './agent.aws.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.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,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.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,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"}
@@ -1,15 +1,21 @@
1
1
  import { Scope } from '@aws-blocks/core/cdk';
2
2
  import type { ScopeParent } from '@aws-blocks/core';
3
+ import type { AgentConfig } from './types.js';
3
4
  export { AgentErrors } from './errors.js';
4
5
  export { BedrockModels, OllamaModels } from './models.js';
5
6
  export declare class Agent extends Scope {
6
7
  /**
7
8
  * CDK layer for the Agent BB.
8
- * Mirrors the runtime's BB creation so CDK discovers and provisions all resources.
9
9
  *
10
- * TODO: scope Bedrock IAM grant to specific modelId from config
11
- * TODO: guardrails CDK provisioning
10
+ * Provisions the session FileBucket, the conversation + message DistributedTables, the
11
+ * Realtime BB used to stream chunks to the browser, and the AgentCore Runtime that hosts
12
+ * the streaming agent loop. All AgentCore-specific provisioning (co-bundle, runtime role
13
+ * and its grants, container env, and the handler's invoke permission) lives in the
14
+ * self-contained {@link AgentCoreRuntime} so it can later fold into a per-BB compute abstraction.
15
+ *
16
+ * The loop runs inside the AgentCore Runtime (not the shared handler Lambda), so the shared
17
+ * handler no longer needs Bedrock access — the runtime's own role gets it (see AgentCoreRuntime).
12
18
  */
13
- constructor(scope: ScopeParent, id: string, config?: any);
19
+ constructor(scope: ScopeParent, id: string, config?: AgentConfig);
14
20
  }
15
21
  //# sourceMappingURL=index.cdk.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAQpD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAO1D,qBAAa,KAAM,SAAQ,KAAK;IAC/B;;;;;;OAMG;gBACS,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG;CAwCxD"}
1
+ {"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE1D,qBAAa,KAAM,SAAQ,KAAK;IAC/B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW;CAyChE"}
package/dist/index.cdk.js CHANGED
@@ -1,42 +1,37 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
- import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
4
3
  import { Scope } from '@aws-blocks/core/cdk';
5
4
  import { DistributedTable } from '@aws-blocks/bb-distributed-table';
6
5
  import { Realtime } from '@aws-blocks/bb-realtime';
7
- import { AsyncJob } from '@aws-blocks/bb-async-job';
8
6
  import { FileBucket } from '@aws-blocks/bb-file-bucket';
7
+ import { AgentCoreRuntime } from './agentcore-runtime.cdk.js';
9
8
  import { messageSchema, conversationSchema, agentStreamChunkSchema } from './schemas.js';
10
- import { z } from 'zod';
11
9
  export { AgentErrors } from './errors.js';
12
10
  export { BedrockModels, OllamaModels } from './models.js';
13
- const jobPayloadSchema = z.object({
14
- message: z.string(),
15
- conversationId: z.string().optional(),
16
- });
17
11
  export class Agent extends Scope {
18
12
  /**
19
13
  * CDK layer for the Agent BB.
20
- * Mirrors the runtime's BB creation so CDK discovers and provisions all resources.
21
14
  *
22
- * TODO: scope Bedrock IAM grant to specific modelId from config
23
- * TODO: guardrails CDK provisioning
15
+ * Provisions the session FileBucket, the conversation + message DistributedTables, the
16
+ * Realtime BB used to stream chunks to the browser, and the AgentCore Runtime that hosts
17
+ * the streaming agent loop. All AgentCore-specific provisioning (co-bundle, runtime role
18
+ * and its grants, container env, and the handler's invoke permission) lives in the
19
+ * self-contained {@link AgentCoreRuntime} so it can later fold into a per-BB compute abstraction.
20
+ *
21
+ * The loop runs inside the AgentCore Runtime (not the shared handler Lambda), so the shared
22
+ * handler no longer needs Bedrock access — the runtime's own role gets it (see AgentCoreRuntime).
24
23
  */
25
24
  constructor(scope, id, config) {
26
25
  super(id, { parent: scope });
27
- this.handler.addToRolePolicy(new PolicyStatement({
28
- actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream', 'bedrock:GetFoundationModel', 'bedrock:ListFoundationModels', 'bedrock:GetInferenceProfile'],
29
- resources: [
30
- 'arn:aws:bedrock:*::foundation-model/*',
31
- 'arn:aws:bedrock:*:*:inference-profile/*',
32
- ],
33
- }));
34
- // Propagate `removalPolicy` to the sessions bucket so customers can
35
- // opt sandbox stacks into clean teardown. Without it, CDK's RETAIN
36
- // default applies (production-safe) and `cdk destroy` will fail on
37
- // a non-empty bucket — same pattern as FileBucket / KnowledgeBase.
38
- // ID shortened to keep S3 bucket names within the 63-char limit
26
+ // Session-snapshot bucket. Provisioned here (and granted to the shared execution role that the
27
+ // AgentCore Runtime runs as); the deployed loop re-derives its name from this bucket's `fullId`
28
+ // in-process — the same `'sn'` id → same fullId → same physical bucket — so no name needs to be
29
+ // injected into the container. Propagate `removalPolicy` so customers can opt sandbox stacks into
30
+ // clean teardown (without it, CDK's RETAIN default applies). ID shortened to keep the S3 bucket
31
+ // name within the 63-char limit.
39
32
  new FileBucket(this, 'sn', { removalPolicy: config?.removalPolicy });
33
+ // Conversation metadata + message history. These grant read/write to the shared execution
34
+ // role, which the AgentCore Runtime then runs as — so the loop can persist history.
40
35
  if (!config?.inferenceOnly) {
41
36
  new DistributedTable(this, 'convos', {
42
37
  schema: conversationSchema,
@@ -49,12 +44,18 @@ export class Agent extends Scope {
49
44
  }
50
45
  new Realtime(this, 'rt', {
51
46
  namespaces: {
52
- chunks: { schema: agentStreamChunkSchema },
47
+ chunks: Realtime.namespace(agentStreamChunkSchema),
53
48
  },
54
49
  });
55
- new AsyncJob(this, 'job', {
56
- schema: jobPayloadSchema,
57
- handler: async () => { },
50
+ // The agent loop runs on the AgentCore Runtime (as the shared Blocks execution role) and
51
+ // streams to the browser over Realtime. AgentCoreRuntime co-bundles the app backend,
52
+ // provisions the runtime, adds the bedrock-agentcore trust + Bedrock to the shared role, and
53
+ // injects the config location so the container loads the same app config as the handler
54
+ // (the session bucket/tables and Realtime publish are already granted to that role). Kept
55
+ // self-contained so it can later fold into a per-BB compute abstraction.
56
+ new AgentCoreRuntime(this, 'runtime', {
57
+ agentFullId: this.fullId,
58
+ agentcoreAssetPath: config?.agentcoreAssetPath,
58
59
  });
59
60
  }
60
61
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.cdk.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cdk.test.d.ts","sourceRoot":"","sources":["../src/index.cdk.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,143 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * CDK-side tests for the Agent BB.
5
+ *
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.
13
+ */
14
+ import { test, before, after } from 'node:test';
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
+ import * as cdk from 'aws-cdk-lib';
20
+ import { Template } from 'aws-cdk-lib/assertions';
21
+ import { BlocksStack, BlocksPresets } from '@aws-blocks/core/cdk';
22
+ import { LambdaCompute } from '@aws-blocks/bb-lambda-compute/cdk';
23
+ import { Agent } from './index.cdk.js';
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');
50
+ });
51
+ after(() => {
52
+ rmSync(tmpDir, { recursive: true, force: true });
53
+ });
54
+ async function synth() {
55
+ const app = new cdk.App();
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);
68
+ }
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');
101
+ });
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');
121
+ });
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}`);
143
+ });