@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.
- package/DESIGN.md +65 -17
- package/README.md +73 -6
- package/dist/agent.aws.d.ts +15 -1
- package/dist/agent.aws.d.ts.map +1 -1
- package/dist/agent.aws.js +49 -0
- package/dist/agent.d.ts +82 -14
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +238 -57
- package/dist/agentcore-bundle.d.ts +12 -0
- package/dist/agentcore-bundle.d.ts.map +1 -0
- package/dist/agentcore-bundle.js +150 -0
- package/dist/agentcore-bundle.test.d.ts +2 -0
- package/dist/agentcore-bundle.test.d.ts.map +1 -0
- package/dist/agentcore-bundle.test.js +46 -0
- package/dist/agentcore-entry.d.ts +21 -0
- package/dist/agentcore-entry.d.ts.map +1 -0
- package/dist/agentcore-entry.js +120 -0
- package/dist/agentcore-runtime.cdk.d.ts +27 -0
- package/dist/agentcore-runtime.cdk.d.ts.map +1 -0
- package/dist/agentcore-runtime.cdk.js +168 -0
- package/dist/index.aws.d.ts +1 -0
- package/dist/index.aws.d.ts.map +1 -1
- package/dist/index.cdk.d.ts +10 -4
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +27 -28
- package/dist/index.cdk.test.js +128 -51
- package/dist/index.mock.d.ts +1 -0
- package/dist/index.mock.d.ts.map +1 -1
- package/dist/index.test.js +404 -1
- package/dist/model-factory.d.ts +2 -2
- package/dist/model-factory.d.ts.map +1 -1
- package/dist/model-factory.js +2 -2
- package/dist/providers/canned.d.ts +8 -1
- package/dist/providers/canned.d.ts.map +1 -1
- package/dist/providers/canned.js +127 -42
- package/dist/types.d.ts +63 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +16 -9
- package/src/agent.aws.ts +58 -1
- package/src/agent.ts +269 -56
- package/src/agentcore-bundle.test.ts +52 -0
- package/src/agentcore-bundle.ts +162 -0
- package/src/agentcore-entry.ts +134 -0
- package/src/agentcore-runtime.cdk.ts +203 -0
- package/src/index.aws.ts +3 -0
- package/src/index.cdk.test.ts +145 -53
- package/src/index.cdk.ts +29 -31
- package/src/index.mock.ts +3 -0
- package/src/index.test.ts +449 -1
- package/src/model-factory.ts +3 -3
- package/src/providers/canned.ts +131 -36
- package/src/types.ts +64 -1
- package/src/version.ts +1 -1
- package/dist/job-event-source.d.ts +0 -19
- package/dist/job-event-source.d.ts.map +0 -1
- package/dist/job-event-source.js +0 -20
- 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';
|