@aws-blocks/bb-agent 0.3.5 → 0.4.1

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 (62) hide show
  1. package/DESIGN.md +65 -17
  2. package/README.md +156 -8
  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 +12 -6
  24. package/dist/index.cdk.d.ts.map +1 -1
  25. package/dist/index.cdk.js +31 -31
  26. package/dist/index.cdk.test.js +128 -51
  27. package/dist/index.hooks.d.ts +2 -2
  28. package/dist/index.hooks.d.ts.map +1 -1
  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 +426 -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 +24 -9
  43. package/src/agent.aws.ts +58 -1
  44. package/src/agent.ts +269 -56
  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 +145 -53
  51. package/src/index.cdk.ts +33 -34
  52. package/src/index.hooks.ts +2 -2
  53. package/src/index.mock.ts +3 -0
  54. package/src/index.test.ts +473 -1
  55. package/src/model-factory.ts +3 -3
  56. package/src/providers/canned.ts +131 -36
  57. package/src/types.ts +64 -1
  58. package/src/version.ts +1 -1
  59. package/dist/job-event-source.d.ts +0 -19
  60. package/dist/job-event-source.d.ts.map +0 -1
  61. package/dist/job-event-source.js +0 -20
  62. package/src/job-event-source.ts +0 -21
@@ -0,0 +1,162 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Synth-time co-bundle for the AgentCore Runtime code asset.
6
+ *
7
+ * The AgentCore process must run BOTH the developer's backend (which constructs the real
8
+ * Agent and its tool closures) AND bb-agent's `serve()` — from a SINGLE module graph, so
9
+ * the Agent instance registry (a module singleton in agent.ts) is shared. If the backend
10
+ * and the entrypoint were bundled separately they'd each get their own bb-agent copy, the
11
+ * Agent would register in one registry and the lookup would read the other, and `serve()`
12
+ * would fail with "No Agent registered".
13
+ *
14
+ * We esbuild-bundle a tiny generated entry that imports both from the same graph. We call
15
+ * `esbuild.buildSync()` DIRECTLY (not CDK's `NodejsFunction`) — that avoids the
16
+ * `PathNotUnderRoot` failure `NodejsFunction` hits for npm-installed packages, since direct
17
+ * esbuild has no projectRoot/lockfile requirement. `buildSync` because CDK synth is sync.
18
+ *
19
+ * Packaging mirrors the official `@aws/agentcore` CLI's Node CodeZip packager
20
+ * (lib/packaging/node.js), because the AgentCore direct-deploy base image provides ONLY the
21
+ * Node runtime — every dependency (including the AWS SDK) must be in the asset. Two wrinkles
22
+ * the CLI solves and we copy verbatim:
23
+ *
24
+ * 1. The `bedrock-agentcore` harness does `createRequire(import.meta.url); require('@fastify/sse')`
25
+ * at module load. esbuild can't statically bundle those dynamic requires. So we emit CJS
26
+ * (`format: 'cjs'`), shim `import.meta.url` to a real value, and prepend a banner that
27
+ * patches `Module._resolveFilename` to fall back to a sibling `_deps/` dir. The dynamic
28
+ * packages are copied into `_deps/` so the fallback finds them at runtime.
29
+ * 2. The `@aws-sdk/*` packages are pure JS and NOT in the base image, so we bundle them
30
+ * (no `external`).
31
+ */
32
+ import { buildSync } from 'esbuild';
33
+ import { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
34
+ import { createRequire } from 'node:module';
35
+ import { dirname, join } from 'node:path';
36
+ import { fileURLToPath } from 'node:url';
37
+
38
+ const __dirname = dirname(fileURLToPath(import.meta.url));
39
+ /** bb-agent package root (dist/.. == package dir), used as esbuild's module-resolution base. */
40
+ const PKG_ROOT = join(__dirname, '..');
41
+
42
+ /** Sibling dir (next to the bundle) holding packages that are loaded via dynamic require(). */
43
+ const DEPS_DIR = '_deps';
44
+
45
+ /**
46
+ * Packages the `bedrock-agentcore` harness (and its Fastify plugins) load via dynamic
47
+ * `require()` at runtime, which esbuild leaves unbundled. Copied into `_deps/` and resolved
48
+ * by the `_resolveFilename` banner below. This list is taken verbatim from the official
49
+ * `@aws/agentcore` CLI so it stays in sync with the harness's runtime require graph.
50
+ */
51
+ const DYNAMIC_REQUIRE_PACKAGES = [
52
+ '@fastify/sse',
53
+ '@fastify/websocket',
54
+ 'duplexify',
55
+ 'end-of-stream',
56
+ 'fastify-plugin',
57
+ 'inherits',
58
+ 'once',
59
+ 'readable-stream',
60
+ 'safe-buffer',
61
+ 'stream-shift',
62
+ 'string_decoder',
63
+ 'util-deprecate',
64
+ 'wrappy',
65
+ 'ws',
66
+ ];
67
+
68
+ /**
69
+ * Banner prepended to the CJS bundle. First line gives ESM-style `import.meta.url` a real
70
+ * value (the harness calls `createRequire(import.meta.url)`); the IIFE patches Node's module
71
+ * resolver so a failed `require('X')` retries against `__dirname/_deps/X` (reading that
72
+ * package's `main` from its package.json). Byte-for-byte the CLI's banner.
73
+ */
74
+ const CJS_BANNER =
75
+ 'const importMetaUrl = require("url").pathToFileURL(__filename).href;' +
76
+ '(function(){var M=require("module"),p=require("path"),f=require("fs"),d=p.join(__dirname,"_deps"),o=M._resolveFilename;' +
77
+ 'M._resolveFilename=function(r,P,i,O){try{return o.call(this,r,P,i,O)}catch(e){' +
78
+ 'var dp=p.join(d,r);if(f.existsSync(dp)){var pk=p.join(dp,"package.json");' +
79
+ 'if(f.existsSync(pk)){var m=JSON.parse(f.readFileSync(pk,"utf8")).main||"index.js";return p.resolve(dp,m)}' +
80
+ 'return p.resolve(dp,"index.js")}throw e}};})();';
81
+
82
+ /** Copy each dynamic-require package into `<outDir>/_deps/<pkg>`, resolving via bb-agent's
83
+ * module graph so hoisted (monorepo) and nested installs both work. */
84
+ function copyDynamicDeps(outDir: string): void {
85
+ const require = createRequire(join(PKG_ROOT, 'noop.js'));
86
+ for (const pkg of DYNAMIC_REQUIRE_PACKAGES) {
87
+ let pkgDir: string;
88
+ try {
89
+ // Resolve the package's own package.json, then take its directory.
90
+ pkgDir = dirname(require.resolve(`${pkg}/package.json`));
91
+ } catch {
92
+ // Not installed / not resolvable from here — skip; the runtime fallback only
93
+ // needs the packages actually reached by the harness's require graph.
94
+ continue;
95
+ }
96
+ if (existsSync(pkgDir)) {
97
+ cpSync(pkgDir, join(outDir, DEPS_DIR, pkg), { recursive: true });
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Co-bundle the app backend + `serve()` into a self-contained AgentCore asset directory.
104
+ *
105
+ * @param backendModulePath - absolute path to the app's backend module (BlocksStack `backendCDKPath`)
106
+ * @param outDir - directory to write the bundle into (a stable, synth-scoped path under cdk.out)
107
+ * @returns `outDir` (contains `main.js`, `_deps/`, `package.json`), ready for `fromCodeAsset`.
108
+ * The bundle is named `main.js` (not `agentcore-entry.js`) because AgentCore's entrypoint
109
+ * validator rejects names it considers to have "multiple dots" / disallowed chars; `main.js`
110
+ * matches the official @aws/agentcore CLI's convention and passes.
111
+ */
112
+ export function bundleAgentCoreAsset(backendModulePath: string, outDir: string): string {
113
+ // Generated entry: load config, import the backend (constructs + registers the Agent),
114
+ // then serve. `resolveDir` = bb-agent package root so `@aws-blocks/*` and this package's
115
+ // own `serve` resolve via node_modules; the backend is imported by absolute PATH (esbuild
116
+ // resolves paths, not file:// URLs) so it's bundled into the SAME graph and its
117
+ // `@aws-blocks/*` deps dedupe to one instance.
118
+ // Wrapped in an async IIFE (not top-level await) because the bundle is emitted as CJS,
119
+ // which does not support top-level await.
120
+ const entrySource = [
121
+ "import { loadConfigToProcessEnv } from '@aws-blocks/core';",
122
+ "import { serve } from '@aws-blocks/bb-agent/agentcore';",
123
+ '(async () => {',
124
+ ' await loadConfigToProcessEnv();',
125
+ ` await import(${JSON.stringify(backendModulePath)});`,
126
+ ' serve();',
127
+ '})();',
128
+ ].join('\n');
129
+
130
+ mkdirSync(outDir, { recursive: true });
131
+
132
+ buildSync({
133
+ stdin: {
134
+ contents: entrySource,
135
+ resolveDir: PKG_ROOT,
136
+ sourcefile: '__agentcore_entry.mjs',
137
+ loader: 'js',
138
+ },
139
+ outfile: join(outDir, 'main.js'),
140
+ bundle: true,
141
+ platform: 'node',
142
+ target: 'node22',
143
+ // CJS (not ESM) so the harness's createRequire + our _resolveFilename patch work.
144
+ format: 'cjs',
145
+ minify: true,
146
+ // Resolve @aws-blocks/* (and the backend's BB constructions) to their AWS-runtime
147
+ // variants, exactly as core bundles the Lambda handler.
148
+ conditions: ['aws-runtime', 'node'],
149
+ banner: { js: CJS_BANNER },
150
+ // Give ESM `import.meta.url` (used by the harness) a real value under CJS output.
151
+ define: { 'import.meta.url': 'importMetaUrl' },
152
+ });
153
+
154
+ // `{"type":"commonjs"}` so Node treats the .js bundle as CJS regardless of any ambient
155
+ // "type":"module" in a parent package.json.
156
+ writeFileSync(join(outDir, 'package.json'), '{"type":"commonjs"}');
157
+
158
+ // Ship the harness's dynamic-require closure alongside the bundle.
159
+ copyDynamicDeps(outDir);
160
+
161
+ return outDir;
162
+ }
@@ -0,0 +1,134 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * AgentCore Runtime entrypoint for the Agent BB.
6
+ *
7
+ * Hosts the developer's real Agent — the same instance the Lambda handler would build — on
8
+ * the `BedrockAgentCoreApp` harness (implements the `/invocations` + `/ping` contract on port
9
+ * 8080), and runs the agent loop as a **background async task** that streams chunks to the
10
+ * browser over the Realtime BB (exactly as the loop does on Lambda today).
11
+ *
12
+ * Why background + Realtime (not the harness's SSE response): the browser never holds a
13
+ * connection to AgentCore — it subscribes to a Realtime channel by `channelId`. So the
14
+ * invocation must return immediately while the loop keeps running server-side. The harness
15
+ * keeps the microVM alive (up to the 8h session lifetime) while an async task is in flight —
16
+ * `/ping` reports `HealthyBusy` — via `addAsyncTask()`/`completeAsyncTask()`. `runAgent()`
17
+ * publishes every chunk to Realtime under the runtime's execution role.
18
+ *
19
+ * How the developer's agent definition reaches this process:
20
+ * The `tools` callback in AgentConfig is a JS closure and can't be serialized across a
21
+ * process boundary. So instead of shipping data, we ship code: this entrypoint imports
22
+ * the SAME developer backend module the Lambda handler imports (co-bundled with
23
+ * `--conditions=aws-runtime`, so `new Agent()` resolves to the AWS runtime class). That
24
+ * construction registers the live Agent in the instance registry (see agent.ts); we look
25
+ * it up by the `BB_AGENT_ID` the CDK Runtime construct set, and drive its loop.
26
+ *
27
+ * Launched by the CodeZip artifact as: ['main.js'] (the co-bundle from agentcore-bundle.ts).
28
+ */
29
+
30
+ import { loadConfigToProcessEnv } from '@aws-blocks/core';
31
+ import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
32
+ import { z } from 'zod';
33
+ import { getAgentInstance } from './agent.js';
34
+
35
+ /** Request contract — mirrors the Lambda jobPayloadSchema, minus transport-only fields. */
36
+ const requestSchema = z.object({
37
+ /** User prompt. Empty on resume (interruptResponses drive the turn instead). */
38
+ prompt: z.string().default(''),
39
+ /** Realtime channel the client subscribes to for this turn's chunks. */
40
+ channelId: z.string(),
41
+ /** Conversation to persist to / restore the session from. Falls back to the AgentCore session id. */
42
+ conversationId: z.string().optional(),
43
+ /** Owner of the conversation. Required when persistence is enabled (not inferenceOnly). */
44
+ userId: z.string().optional(),
45
+ /** HITL resume: approval responses to apply instead of a new prompt. */
46
+ interruptResponses: z.array(z.object({ interruptId: z.string(), response: z.string() })).optional(),
47
+ /** Per-call tool context, threaded through to tool handlers. Must be JSON-serializable. */
48
+ context: z.unknown().optional(),
49
+ });
50
+
51
+ /**
52
+ * Serve a registered Agent on the AgentCore harness.
53
+ *
54
+ * The developer's backend must already have been imported in THIS process (so the Agent
55
+ * registered itself in the shared registry) — either by `main()` below (standalone launch
56
+ * via `BB_AGENT_BACKEND_MODULE`) or by the co-bundle (agentcore-bundle.ts) that imports the
57
+ * backend and this `serve` from the same bb-agent module instance. Co-bundling is required
58
+ * because the registry is a module singleton — a split would put the Agent in one map and
59
+ * the lookup in another.
60
+ *
61
+ * @param agentId - fullId of the target Agent (defaults to process.env.BB_AGENT_ID)
62
+ */
63
+ export function serve(agentId = process.env.BB_AGENT_ID): void {
64
+ if (!agentId) throw new Error('BB_AGENT_ID is required (fullId of the target Agent).');
65
+ const agent = getAgentInstance(agentId);
66
+ if (!agent) {
67
+ throw new Error(
68
+ `No Agent registered with id '${agentId}'. Ensure the backend module constructs it at import time.`,
69
+ );
70
+ }
71
+
72
+ const app = new BedrockAgentCoreApp({
73
+ invocationHandler: {
74
+ requestSchema,
75
+ process: (request, context) => {
76
+ // AgentCore routes every invocation for a session to the same warm microVM.
77
+ // runtimeSessionId maps to the Agent BB's conversationId (session state key).
78
+ const conversationId = request.conversationId ?? context.sessionId;
79
+
80
+ // Run the turn as a background async task so this invocation returns immediately.
81
+ // The harness reports `/ping` = HealthyBusy while the task is in flight, keeping
82
+ // the microVM alive (up to the 8h session lifetime) until the loop completes.
83
+ // Chunks are delivered out-of-band via Realtime — this HTTP response is just an ack.
84
+ const taskId = app.addAsyncTask('agent-turn');
85
+ void agent
86
+ .invokeTurn({
87
+ message: request.prompt,
88
+ conversationId,
89
+ channelId: request.channelId,
90
+ userId: request.userId ?? 'anonymous',
91
+ interruptResponses: request.interruptResponses,
92
+ context: request.context,
93
+ })
94
+ .finally(() => app.completeAsyncTask(taskId));
95
+
96
+ // The client already has channelId (from the RPC that invoked us) and subscribes
97
+ // to Realtime; it does not consume this response body.
98
+ return { channelId: request.channelId, status: 'accepted' };
99
+ },
100
+ },
101
+ });
102
+
103
+ app.run();
104
+ }
105
+
106
+ /**
107
+ * Standalone launch: load config, import the developer backend by path, then serve.
108
+ * Used when the artifact runs this file directly with BB_AGENT_BACKEND_MODULE pointing at the
109
+ * backend. When an app co-bundles the backend with `serve` (agentcore-bundle.ts), it calls
110
+ * `serve()` directly instead and this `main()` is not the entry.
111
+ */
112
+ export async function main(): Promise<void> {
113
+ // Same cold-start contract as the Lambda handler: pull BB resource identifiers
114
+ // (table names, bucket names, Realtime callback URL) into process.env before importing
115
+ // the backend, so BB constructors can resolve them.
116
+ await loadConfigToProcessEnv();
117
+
118
+ const backendModule = process.env.BB_AGENT_BACKEND_MODULE;
119
+ if (!backendModule)
120
+ throw new Error('BB_AGENT_BACKEND_MODULE env var is required (path to the developer backend module).');
121
+
122
+ // Import the developer backend — constructing the real Agent, which registers itself.
123
+ await import(backendModule);
124
+
125
+ serve();
126
+ }
127
+
128
+ // Auto-run the standalone launch path ONLY when a backend module path is provided.
129
+ // The co-bundle path (agentcore-bundle.ts) imports `serve` and invokes it directly after
130
+ // importing the backend inline, and does NOT set BB_AGENT_BACKEND_MODULE — so `main()` must
131
+ // not fire there (it would double-serve and throw on the missing env var).
132
+ if (process.env.BB_AGENT_BACKEND_MODULE) {
133
+ void main();
134
+ }
@@ -0,0 +1,203 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Self-contained CDK provisioning for the Agent BB's AgentCore Runtime.
6
+ *
7
+ * This class owns EVERYTHING AgentCore-specific: the synth-time co-bundle of the app backend,
8
+ * the `Runtime` construct (via `fromCodeAsset` — Node 22 CodeZip, no Docker), the shared role's
9
+ * AgentCore trust + grants, the container env injection, and the grant that lets the app's RPC
10
+ * handler invoke the runtime. It is deliberately kept in one place, with no AgentCore details
11
+ * leaking into the `Agent` CDK constructor or into core, so it can later fold into a per-BB
12
+ * compute abstraction (should one land) without touching call sites — the Agent just constructs
13
+ * it and hands over references to the BBs the loop uses.
14
+ *
15
+ * The loop runs INSIDE this runtime AS the shared Blocks execution role (the same role the Lambda
16
+ * handler runs as), so it inherits every Building Block's grants — including the Realtime publish
17
+ * permissions already granted to the handler, so it streams chunks to the browser via the Realtime
18
+ * BB with no extra grant. The container loads the full app config (via the injected config-bucket
19
+ * location) exactly as the handler does, so it discovers the Realtime callback URL and every other
20
+ * registerConfig() value a tool's BB may need. This class adds to that shared role only what's
21
+ * AgentCore-specific: the `bedrock-agentcore` assume-role trust, Bedrock model access, and the
22
+ * handler's `InvokeAgentRuntime` permission. Inbound auth is IAM (SigV4): the RPC handler
23
+ * invokes with its own credentials; the browser never talks to the runtime directly.
24
+ */
25
+
26
+ import { mkdirSync } from 'node:fs';
27
+ import { join } from 'node:path';
28
+ import * as cdk from 'aws-cdk-lib';
29
+ import { AgentCoreRuntime as AgentCoreRuntimeVersion, AgentRuntimeArtifact, Runtime } from 'aws-cdk-lib/aws-bedrockagentcore';
30
+ import { Effect, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
31
+ import { getConfigLocation, registerConfig, Scope } from '@aws-blocks/core/cdk';
32
+ import type { ScopeParent } from '@aws-blocks/core';
33
+ import { bundleAgentCoreAsset } from './agentcore-bundle.js';
34
+
35
+ /** References the agent loop needs, handed in by the Agent CDK constructor. */
36
+ export interface AgentCoreRuntimeProps {
37
+ /** fullId of the owning Agent — the container looks the Agent up by this (`BB_AGENT_ID`). */
38
+ agentFullId: string;
39
+ /**
40
+ * Pre-built asset dir to use instead of co-bundling at synth. Set by unit tests and apps
41
+ * that pre-bundle; when omitted, the backend module is co-bundled from the BlocksStack.
42
+ */
43
+ agentcoreAssetPath?: string;
44
+ }
45
+
46
+ export class AgentCoreRuntime extends Scope {
47
+ /** The provisioned runtime, or undefined when no backend asset could be resolved (isolated tests). */
48
+ readonly runtime?: Runtime;
49
+ /** The runtime ARN (empty string when not provisioned). */
50
+ readonly runtimeArn: string;
51
+
52
+ constructor(scope: ScopeParent, id: string, props: AgentCoreRuntimeProps) {
53
+ super(id, { parent: scope });
54
+
55
+ const assetPath = props.agentcoreAssetPath ?? this.buildAsset();
56
+ if (!assetPath) {
57
+ // No backend module discoverable (e.g. an isolated unit test that constructs the
58
+ // Agent without a BlocksStack) — skip provisioning rather than failing synth.
59
+ this.runtimeArn = '';
60
+ return;
61
+ }
62
+
63
+ const stack = cdk.Stack.of(this);
64
+
65
+ // Run the loop AS the shared Blocks execution role (`role: this.executionRole`) — the same
66
+ // role the Lambda handler runs as. Because every Building Block grants its runtime permissions
67
+ // to this role (a tool that uses KVStore/tables/etc. grants there too), the AgentCore
68
+ // container inherits them all automatically — no bespoke, under-granted role, and no need to
69
+ // mirror each BB's grants. One shared role that any compute can assume keeps this a drop-in
70
+ // for a future per-BB compute abstraction.
71
+ const role = this.executionRole;
72
+
73
+ // Add the agent's shared-role trust + grants ONCE per stack. Every agent instance would
74
+ // otherwise add the SAME trust / Bedrock / InvokeAgentRuntime statements to the ONE shared
75
+ // role; with several agents in an app that piles up duplicates and overflows the IAM inline-
76
+ // policy size limit (CDK then spills into `OverflowPolicy` managed policies and the role
77
+ // misbehaves). These are identical for every agent — Bedrock models and the runtime-ARN
78
+ // wildcard are stack-scoped — so doing it once covers every agent's container. (Realtime
79
+ // publish, the session bucket, and the conversation/message tables are already granted to the
80
+ // shared role by the Realtime BB's handler wiring and the Agent's FileBucket/DistributedTable
81
+ // children, so they're not repeated here — the loop inherits them by running as the role.)
82
+ const SHARED_GRANTS_KEY = Symbol.for('BLOCKS_AGENT_RUNTIME_SHARED_ROLE_GRANTS');
83
+ const stackAny = stack as unknown as Record<symbol, boolean | undefined>;
84
+ if (!stackAny[SHARED_GRANTS_KEY]) {
85
+ // The container publishes to Realtime AS this shared role, which already holds the publish
86
+ // grants (the Realtime BB grants postToConnection + the connections table to the handler on
87
+ // the same role) — so no Realtime IAM grant is needed here; it's inherited by running as the role.
88
+
89
+ // Trust: let the AgentCore Runtime assume this shared role (it runs AS the role). Added here
90
+ // rather than in core, so the role only trusts `bedrock-agentcore` when an Agent exists.
91
+ // Scope it to this account/region with aws:SourceAccount + aws:SourceArn — AWS's recommended
92
+ // AgentCore Runtime trust policy — so only AgentCore runtimes in THIS account can assume the
93
+ // role (tightens the confused-deputy surface) without breaking assumption. `assumeRolePolicy`
94
+ // exists only on the concrete `Role`; core always creates BlocksRole concretely, so narrow
95
+ // and fail loud if that ever changes.
96
+ if (!(role instanceof Role)) {
97
+ throw new Error(
98
+ 'AgentCore Runtime requires the shared Blocks execution role to be a concrete iam.Role to add its assume-role trust',
99
+ );
100
+ }
101
+ role.assumeRolePolicy?.addStatements(
102
+ new PolicyStatement({
103
+ effect: Effect.ALLOW,
104
+ principals: [new ServicePrincipal('bedrock-agentcore.amazonaws.com')],
105
+ actions: ['sts:AssumeRole'],
106
+ conditions: {
107
+ StringEquals: { 'aws:SourceAccount': stack.account },
108
+ ArnLike: { 'aws:SourceArn': `arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:*` },
109
+ },
110
+ }),
111
+ );
112
+ // Bedrock model access for the loop (no Building Block grants this, and the loop no longer
113
+ // runs on the handler).
114
+ role.addToPrincipalPolicy(
115
+ new PolicyStatement({
116
+ actions: [
117
+ 'bedrock:InvokeModel',
118
+ 'bedrock:InvokeModelWithResponseStream',
119
+ 'bedrock:GetFoundationModel',
120
+ 'bedrock:ListFoundationModels',
121
+ 'bedrock:GetInferenceProfile',
122
+ ],
123
+ resources: [`arn:${stack.partition}:bedrock:*::foundation-model/*`, `arn:${stack.partition}:bedrock:*:*:inference-profile/*`],
124
+ }),
125
+ );
126
+ // Let the app's RPC handler (also the shared role) invoke the runtimes — stream()/resume()
127
+ // call InvokeAgentRuntime. Scope to a wildcard runtime ARN rather than a specific runtime's
128
+ // ARN: the runtime uses this same shared role as its executionRole, so referencing its ARN
129
+ // here would create a Role→Runtime→Role dependency cycle. (Same wildcard style as Bedrock.)
130
+ role.addToPrincipalPolicy(
131
+ new PolicyStatement({
132
+ actions: ['bedrock-agentcore:InvokeAgentRuntime'],
133
+ resources: [
134
+ `arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:runtime/*`,
135
+ `arn:${stack.partition}:bedrock-agentcore:${stack.region}:${stack.account}:runtime/*/*`,
136
+ ],
137
+ }),
138
+ );
139
+ stackAny[SHARED_GRANTS_KEY] = true;
140
+ }
141
+
142
+ // Inject the config location so the container loads the FULL app config via
143
+ // loadConfigToProcessEnv() — the same config the Lambda handler loads. That's how it gets
144
+ // BLOCKS_RT_CALLBACK_URL (registered by the Realtime BB) plus any other config-backed BB value
145
+ // an agent's tools touch; without it the container runs with empty config and those BBs fail.
146
+ // Idempotent (one config bucket per stack); IAM to read it is inherited via the shared role.
147
+ const { bucketName: configBucketName, key: configKey } = getConfigLocation(this);
148
+
149
+ const runtime = new Runtime(this, 'AgentRuntime', {
150
+ agentRuntimeArtifact: AgentRuntimeArtifact.fromCodeAsset({
151
+ path: assetPath,
152
+ runtime: AgentCoreRuntimeVersion.NODE_22,
153
+ // Launch command, NOT a Lambda file.export handler. Single element = the .js file;
154
+ // the NODE_22 runtime invokes `node` itself. (A leading 'node' element is rejected.)
155
+ entrypoint: ['main.js'],
156
+ }),
157
+ executionRole: role,
158
+ // Inbound auth defaults to IAM (SigV4): the RPC handler invokes with its own creds.
159
+ // The browser never invokes the runtime directly (it subscribes to Realtime).
160
+ environmentVariables: {
161
+ // The Agent's fullId so the container's getAgentInstance(BB_AGENT_ID) matches the
162
+ // Agent the co-bundled backend registers at import.
163
+ BB_AGENT_ID: props.agentFullId,
164
+ // The namespace the container rebuilds fullId (and every derived resource name) from.
165
+ // MUST be the owning stack/backend's canonical root id — the SAME value the Lambda
166
+ // handler and the Lambda compute use (`backendStackName`) — not the raw CFN stack name.
167
+ // They coincide for a top-level BlocksStack, but for a BlocksBackend embedded in a
168
+ // customer stack the handler uses the backend fullId while cdk.Stack.of(this).stackName
169
+ // is the customer stack name; using the latter would make the container derive names from
170
+ // the wrong namespace and miss its own tables/bucket/config.
171
+ BLOCKS_STACK_NAME: this.backendStackName,
172
+ // Config location so the container's loadConfigToProcessEnv() loads the full app config
173
+ // (same as the handler) — this delivers BLOCKS_RT_CALLBACK_URL and every other
174
+ // registerConfig() value a tool's BB may read. IAM to read it is inherited (shared role).
175
+ BLOCKS_CONFIG_BUCKET: configBucketName,
176
+ BLOCKS_CONFIG_KEY: configKey,
177
+ },
178
+ });
179
+ this.runtime = runtime;
180
+ this.runtimeArn = runtime.agentRuntimeArn;
181
+
182
+ // Expose THIS agent's runtime ARN to the Lambda runtime path so stream() can resolve it at
183
+ // call time. Per-agent (each agent has its own runtime), so it stays outside the shared guard.
184
+ registerConfig(this, `BB_AGENT_${props.agentFullId}_RUNTIME_ARN`, runtime.agentRuntimeArn);
185
+ }
186
+
187
+ /**
188
+ * Co-bundle the app backend + `serve()` into an AgentCore code-asset dir. Returns undefined
189
+ * when the backend module path can't be discovered (isolated unit tests) — the caller then
190
+ * skips provisioning rather than failing synth.
191
+ */
192
+ private buildAsset(): string | undefined {
193
+ const stack = (globalThis as any).CURRENT_BLOCKS_STACK as { backendModulePath?: string } | undefined;
194
+ const backendModulePath = stack?.backendModulePath;
195
+ if (!backendModulePath) return undefined;
196
+ const outDir = join(
197
+ cdk.App.of(this)?.outdir ?? cdk.Stack.of(this).node.tryGetContext('cdk.out') ?? '.cdk-agentcore',
198
+ `agentcore-${this.fullId}`,
199
+ );
200
+ mkdirSync(outDir, { recursive: true });
201
+ return bundleAgentCoreAsset(backendModulePath, outDir);
202
+ }
203
+ }
package/src/index.aws.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  export { Agent } from './agent.aws.js';
5
+ // Exported so api-extractor can resolve the (protected, @internal) dispatchTurn signature; the type
6
+ // itself is @internal — not part of the public API (customers use stream()/resume()).
7
+ export type { AgentTurnPayload } from './agent.js';
5
8
  export { AgentErrors, InterruptError } from './errors.js';
6
9
  export { BedrockModels, OllamaModels } from './models.js';
7
10
  export type { AgentConfig, AgentResult, AgentStreamChunk, AgentStreamResult, ToolDefinition, AgentTool, ToolFactory, ToolsConfig, ToolHandlerArgs, DefaultToolContext, InterruptResponse, ToolCallRecord, ModelConfig, StreamOptions, Message, Conversation, JSONValue, TokenUsage } from './types.js';