@aws-blocks/bb-agent 0.1.1 → 0.1.3

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 ADDED
@@ -0,0 +1,66 @@
1
+ # Agent — Design
2
+
3
+ Design document for the Agent Building Block. For usage, see [README.md](./README.md).
4
+
5
+ **Package:** `@aws-blocks/bb-agent`
6
+ **Type:** Composite (uses DistributedTable, Realtime, AsyncJob, FileBucket internally)
7
+ **AWS Services:** Bedrock, DynamoDB, S3, SQS, AppSync Events
8
+ **Agent Framework:** [Strands Agents SDK](https://strandsagents.com/)
9
+
10
+ ## Architecture
11
+
12
+ The Agent BB is a composite Building Block — it creates and manages 4 internal BBs:
13
+
14
+ | Internal BB | Purpose | Created when |
15
+ |-------------|---------|-------------|
16
+ | **FileBucket** | Session persistence (Strands SessionManager) | Always |
17
+ | **DistributedTable** | Frontend message history | `inferenceOnly: false` |
18
+ | **Realtime** | Streaming chunks to caller | Always |
19
+ | **AsyncJob** | Async agent execution (avoids 29s API Gateway timeout) | Always |
20
+
21
+ ```
22
+ stream() → AsyncJob.submit() → returns { channelId } immediately
23
+
24
+ AsyncJob consumer
25
+
26
+ runAgent() → Strands agent loop → publishes chunks to Realtime
27
+ → persists messages to DistributedTable
28
+ → SessionManager saves state to FileBucket
29
+ ```
30
+
31
+ ## Session Persistence
32
+
33
+ Two storage backends, same FileBucket BB:
34
+ - **AWS:** Strands' native `S3Storage` → FileBucket-provisioned S3 bucket
35
+ - **Local:** Custom `FileBucketSnapshotStorage` → FileBucket mock (mirrors S3Storage key layout exactly)
36
+
37
+ ## Infrastructure (CDK)
38
+
39
+ The CDK class mirrors the runtime's BB creation:
40
+ - **Bedrock IAM:** `InvokeModel` + `InvokeModelWithResponseStream` on all foundation models and inference profiles
41
+ - **FileBucket:** `${id}-sessions` — session snapshot storage
42
+ - **DistributedTable:** `${id}-messages` — conversation history (only when `inferenceOnly: false`)
43
+ - **Realtime:** `${id}-rt` — streaming namespace `chunks`
44
+ - **AsyncJob:** `${id}-job` — job payload: `{ message, conversationId?, channelId }`
45
+
46
+ > **Note:** Internal Building Blocks are created on the parent scope (not `this`) to ensure correct nested-scope resolution on AWS.
47
+
48
+ ## Model Providers
49
+
50
+ All providers are Strands model implementations, mapped from Blocks's `ModelConfig` via `model-factory.ts`:
51
+
52
+ | Provider | Strands Class | Use Case |
53
+ |----------|--------------|----------|
54
+ | `canned` | `CannedProvider` (custom) | Local dev — keyword-based responses with tool call support |
55
+ | `bedrock` | `BedrockModel` | AWS — Amazon Bedrock models |
56
+ | `openai-api` | `OpenAIModel` | Any OpenAI-compatible endpoint (OpenAI, Ollama, vLLM) |
57
+
58
+ ## CannedProvider
59
+
60
+ Custom Strands model provider for local development. No network, no API keys, no costs.
61
+
62
+ - Returns instant keyword-based responses (e.g., prompt contains "weather" → weather response, otherwise a default canned response)
63
+ - Streams word by word, matching the same `ModelStreamEvent` protocol as Bedrock/OpenAI
64
+ - Triggers tool calls when the prompt mentions a tool name — splits camelCase names into words (e.g., "weather" matches `getWeather`) and emits Strands `toolUse` events
65
+ - After Strands executes the tool and sends the result back, returns a fixed acknowledgment (`"I called the tool and got a result."`)
66
+ - Token usage reports zeros (no real model call)
package/README.md CHANGED
@@ -6,6 +6,8 @@ AI agent with streaming, tool calling, and conversation persistence. Powered by
6
6
 
7
7
  **Requires:** `zod` ^4.0.0 as a peer dependency. Tool parameters use Zod schemas for validation. If you see `ZodType missing properties` errors, check your zod version.
8
8
 
9
+ > Design & mock parity details: [DESIGN.md](./DESIGN.md)
10
+
9
11
  ## Quick Start
10
12
 
11
13
  ```typescript
@@ -96,6 +98,34 @@ Returned by `stream()`. Provides the Realtime channel and convenience methods:
96
98
  | `channel` | `Promise<RealtimeChannel>` | Realtime channel handle — `await` it, then call `.subscribe(handler)`. |
97
99
  | `complete()` | `Promise<AgentStreamChunk>` | Wait for the done chunk (full text + token usage). |
98
100
 
101
+ ### AgentStreamChunk
102
+
103
+ Each chunk published to the Realtime channel has a `type` and type-specific fields:
104
+
105
+ | Type | Fields | Description |
106
+ |------|--------|-------------|
107
+ | `text-delta` | `text: string` | Incremental text token (in `'token'` streaming mode) or full block (in `'block'` mode). |
108
+ | `tool-call` | `toolName: string`, `input: JSONValue` | Agent is calling a tool. |
109
+ | `tool-result` | `toolName: string`, `text: string` | Tool returned a result. |
110
+ | `done` | `text: string`, `usage: TokenUsage` | Agent finished. `text` contains the full response. `usage` has `{ inputTokens, outputTokens, totalTokens }`. |
111
+ | `error` | `error: string` | Agent encountered an error. |
112
+ | `interrupt` | `interrupts: Array<{ id, name, reason }>` | Agent paused for approval. See [Tool Approval](#tool-approval-human-in-the-loop). |
113
+
114
+ ### Message Roles
115
+
116
+ Messages stored in conversation history use these roles:
117
+
118
+ | Role | Description |
119
+ |------|-------------|
120
+ | `user` | User message. |
121
+ | `assistant` | Agent response text. |
122
+ | `tool-call` | Record of a tool invocation (stored for audit). |
123
+ | `tool-result` | Record of a tool's return value. |
124
+ | `approval` | User's approval/denial response to an interrupt. |
125
+ | `interrupt` | Agent paused — snapshot of pending interrupts. |
126
+
127
+ The `useChat` hook only surfaces `user`, `assistant`, and `approval` messages to the UI. Use `agent.getConversation()` directly to access the full history including tool-call/tool-result records.
128
+
99
129
  ### AgentConfig
100
130
 
101
131
  | Option | Type | Description |
@@ -630,7 +660,7 @@ const chat = useChat({
630
660
  resume: (chId, responses, convId) => api.resume(chId, responses, convId),
631
661
  },
632
662
  subscribe: async (channelId, handler) => {
633
- const { channel } = await api.getChannel(channelId);
663
+ const channel = await api.getChannel(channelId);
634
664
  return channel.subscribe(handler);
635
665
  },
636
666
  onMessagesChange: (msgs) => renderMessages(msgs),
@@ -716,13 +716,17 @@ describe('checkModelHealth', () => {
716
716
  assert.strictEqual(await checkModelHealth({ provider: 'canned' }, log), true);
717
717
  });
718
718
  test('bedrock foundation model found returns true', async () => {
719
- const mockClient = { send: async () => ({ modelDetails: { modelId: 'anthropic.claude-3-haiku' } }) };
719
+ let callCount = 0;
720
+ const mockClient = { send: async () => {
721
+ callCount++;
722
+ if (callCount === 1)
723
+ throw new Error('not an inference profile');
724
+ return { modelDetails: { modelId: 'anthropic.claude-3-haiku' } };
725
+ } };
720
726
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), true);
721
727
  });
722
- test('bedrock foundation model not found returns false', async () => {
723
- const err = new Error('not found');
724
- err.name = 'ValidationException';
725
- const mockClient = { send: async () => { throw err; } };
728
+ test('bedrock model not found returns false', async () => {
729
+ const mockClient = { send: async () => { throw new Error('not found'); } };
726
730
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'bad.model' }, log, mockClient), false);
727
731
  });
728
732
  test('bedrock credential error returns false', async () => {
@@ -731,15 +735,13 @@ describe('checkModelHealth', () => {
731
735
  const mockClient = { send: async () => { throw err; } };
732
736
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), false);
733
737
  });
734
- test('bedrock cross-region inference profile found returns true', async () => {
738
+ test('bedrock inference profile found returns true', async () => {
735
739
  const mockClient = { send: async () => ({ inferenceProfileName: 'US Claude Sonnet' }) };
736
740
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), true);
737
741
  });
738
- test('bedrock cross-region inference profile error returns false', async () => {
739
- const err = new Error('access denied');
740
- err.name = 'AccessDeniedException';
741
- const mockClient = { send: async () => { throw err; } };
742
- assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), false);
742
+ test('bedrock global inference profile found returns true', async () => {
743
+ const mockClient = { send: async () => ({ inferenceProfileName: 'Global Claude Opus' }) };
744
+ assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'global.anthropic.claude-opus-4-8-v1' }, log, mockClient), true);
743
745
  });
744
746
  test('openai-api with unreachable endpoint returns false', async () => {
745
747
  assert.strictEqual(await checkModelHealth({ provider: 'openai-api', modelId: 'gpt-4', endpoint: 'http://localhost:19999/v1' }, log), false);
@@ -1 +1 @@
1
- {"version":3,"file":"model-factory.d.ts","sourceRoot":"","sources":["../src/model-factory.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C;;;;;;;GAOG;AACH,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CACjC;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,CA6IjI;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAkDjH"}
1
+ {"version":3,"file":"model-factory.d.ts","sourceRoot":"","sources":["../src/model-factory.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C;;;;;;;GAOG;AACH,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CACjC;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,CAqHjI;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAkDjH"}
@@ -16,35 +16,24 @@ export async function checkModelHealth(config, log, _testClient) {
16
16
  }
17
17
  log.info(`Checking model health: ${config.provider}${config.modelId ? ` (${config.modelId})` : ''}`);
18
18
  if (config.provider === 'bedrock') {
19
- const isCrossRegionProfile = config.modelId && /^(us-gov|us|eu|apac)\./.test(config.modelId);
20
- const getClient = async () => {
21
- if (_testClient)
22
- return _testClient;
23
- const { BedrockClient } = await import('@aws-sdk/client-bedrock');
24
- return new BedrockClient({});
25
- };
26
- // Cross-region inference profiles use GetInferenceProfile instead of GetFoundationModel.
27
- if (isCrossRegionProfile) {
28
- try {
29
- const client = await getClient();
30
- const command = _testClient
31
- ? { inferenceProfileIdentifier: config.modelId }
32
- : new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
33
- const res = await client.send(command);
34
- if (res.inferenceProfileName) {
35
- log.info(`Inference profile '${config.modelId}' available`);
36
- return true;
37
- }
38
- return false;
39
- }
40
- catch (err) {
41
- const e = err;
42
- log.warn(`Inference profile health check failed for '${config.modelId}': ${e.name ?? e.message}`, { provider: config.provider, modelId: config.modelId });
43
- return false;
19
+ const client = _testClient ?? new (await import('@aws-sdk/client-bedrock')).BedrockClient({});
20
+ // Try GetInferenceProfile first (covers cross-region and global profiles).
21
+ try {
22
+ const command = _testClient
23
+ ? { inferenceProfileIdentifier: config.modelId }
24
+ : new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
25
+ const res = await client.send(command);
26
+ if (res.inferenceProfileName) {
27
+ log.info(`Inference profile '${config.modelId}' available`);
28
+ return true;
44
29
  }
45
30
  }
31
+ catch (err) {
32
+ const e = err;
33
+ log.debug?.(`Not an inference profile: ${e.name ?? e.message}`, { modelId: config.modelId });
34
+ }
35
+ // Try GetFoundationModel (covers base model IDs).
46
36
  try {
47
- const client = await getClient();
48
37
  const command = _testClient
49
38
  ? { modelIdentifier: config.modelId }
50
39
  : new (await import('@aws-sdk/client-bedrock')).GetFoundationModelCommand({ modelIdentifier: config.modelId });
@@ -53,29 +42,14 @@ export async function checkModelHealth(config, log, _testClient) {
53
42
  log.info(`Bedrock model '${config.modelId}' exists in catalog`);
54
43
  return true;
55
44
  }
56
- return false;
57
45
  }
58
46
  catch (err) {
59
47
  const e = err;
60
- // GetFoundationModel throws for unknown models (ValidationException / ResourceNotFoundException).
61
- if (e.name === 'ValidationException' || e.name === 'ResourceNotFoundException') {
62
- try {
63
- const client = await getClient();
64
- const listCommand = _testClient
65
- ? {}
66
- : new (await import('@aws-sdk/client-bedrock')).ListFoundationModelsCommand({});
67
- const list = await client.send(listCommand);
68
- const available = list.modelSummaries?.map((m) => m.modelId).filter(Boolean) ?? [];
69
- log.warn(`Bedrock model '${config.modelId}' not found. Available: ${available.slice(0, 10).join(', ')}${available.length > 10 ? ` (+${available.length - 10} more)` : ''}`);
70
- }
71
- catch {
72
- log.warn(`Bedrock model '${config.modelId}' not found. Could not list available models.`);
73
- }
74
- return false;
75
- }
76
- log.warn(`Bedrock health check failed: ${e.name ?? e.message}. Verify AWS credentials are configured.`, { provider: config.provider, modelId: config.modelId });
77
- return false;
48
+ log.debug?.(`Not a foundation model: ${e.name ?? e.message}`, { modelId: config.modelId });
78
49
  }
50
+ // Both failed — model not found.
51
+ log.warn(`Bedrock model '${config.modelId}' not found as inference profile or foundation model. Verify the model ID and AWS credentials.`, { provider: config.provider, modelId: config.modelId });
52
+ return false;
79
53
  }
80
54
  if (config.provider === 'openai-api') {
81
55
  const endpoint = config.endpoint ?? 'https://api.openai.com/v1';
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare const BB_NAME = "Agent";
2
- export declare const BB_VERSION = "0.1.1";
2
+ export declare const BB_VERSION = "0.1.3";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
2
  export const BB_NAME = 'Agent';
3
- export const BB_VERSION = '0.1.1';
3
+ export const BB_VERSION = '0.1.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws-blocks/bb-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "author": "Amazon Web Services",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -8,6 +8,7 @@
8
8
  "dist",
9
9
  "MODELS.md",
10
10
  "README.md",
11
+ "DESIGN.md",
11
12
  "src",
12
13
  "LICENSE"
13
14
  ],
@@ -33,11 +34,11 @@
33
34
  "test": "node --test dist/index.test.js"
34
35
  },
35
36
  "dependencies": {
36
- "@aws-blocks/bb-async-job": "^0.1.1",
37
- "@aws-blocks/bb-distributed-table": "^0.1.1",
38
- "@aws-blocks/bb-file-bucket": "^0.1.1",
39
- "@aws-blocks/bb-logger": "^0.1.1",
40
- "@aws-blocks/bb-realtime": "^0.1.1",
37
+ "@aws-blocks/bb-async-job": "^0.1.2",
38
+ "@aws-blocks/bb-distributed-table": "^0.1.3",
39
+ "@aws-blocks/bb-file-bucket": "^0.1.2",
40
+ "@aws-blocks/bb-logger": "^0.1.2",
41
+ "@aws-blocks/bb-realtime": "^0.1.2",
41
42
  "@aws-blocks/core": "^0.1.1",
42
43
  "@aws-sdk/client-bedrock": "^3.700.0",
43
44
  "@strands-agents/sdk": "~1.3.0",
package/src/index.test.ts CHANGED
@@ -844,13 +844,17 @@ describe('checkModelHealth', () => {
844
844
  });
845
845
 
846
846
  test('bedrock foundation model found returns true', async () => {
847
- const mockClient = { send: async () => ({ modelDetails: { modelId: 'anthropic.claude-3-haiku' } }) };
847
+ let callCount = 0;
848
+ const mockClient = { send: async () => {
849
+ callCount++;
850
+ if (callCount === 1) throw new Error('not an inference profile');
851
+ return { modelDetails: { modelId: 'anthropic.claude-3-haiku' } };
852
+ } };
848
853
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), true);
849
854
  });
850
855
 
851
- test('bedrock foundation model not found returns false', async () => {
852
- const err = new Error('not found'); err.name = 'ValidationException';
853
- const mockClient = { send: async () => { throw err; } };
856
+ test('bedrock model not found returns false', async () => {
857
+ const mockClient = { send: async () => { throw new Error('not found'); } };
854
858
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'bad.model' }, log, mockClient), false);
855
859
  });
856
860
 
@@ -860,15 +864,14 @@ describe('checkModelHealth', () => {
860
864
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), false);
861
865
  });
862
866
 
863
- test('bedrock cross-region inference profile found returns true', async () => {
867
+ test('bedrock inference profile found returns true', async () => {
864
868
  const mockClient = { send: async () => ({ inferenceProfileName: 'US Claude Sonnet' }) };
865
869
  assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), true);
866
870
  });
867
871
 
868
- test('bedrock cross-region inference profile error returns false', async () => {
869
- const err = new Error('access denied'); err.name = 'AccessDeniedException';
870
- const mockClient = { send: async () => { throw err; } };
871
- assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), false);
872
+ test('bedrock global inference profile found returns true', async () => {
873
+ const mockClient = { send: async () => ({ inferenceProfileName: 'Global Claude Opus' }) };
874
+ assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'global.anthropic.claude-opus-4-8-v1' }, log, mockClient), true);
872
875
  });
873
876
 
874
877
  test('openai-api with unreachable endpoint returns false', async () => {
@@ -36,36 +36,25 @@ export async function checkModelHealth(config: ModelConfig, log: ChildLogger, _t
36
36
  }
37
37
  log.info(`Checking model health: ${config.provider}${config.modelId ? ` (${config.modelId})` : ''}`);
38
38
  if (config.provider === 'bedrock') {
39
- const isCrossRegionProfile = config.modelId && /^(us-gov|us|eu|apac)\./.test(config.modelId);
40
-
41
- const getClient = async (): Promise<BedrockHealthClient> => {
42
- if (_testClient) return _testClient;
43
- const { BedrockClient } = await import('@aws-sdk/client-bedrock');
44
- return new BedrockClient({});
45
- };
46
-
47
- // Cross-region inference profiles use GetInferenceProfile instead of GetFoundationModel.
48
- if (isCrossRegionProfile) {
49
- try {
50
- const client = await getClient();
51
- const command = _testClient
52
- ? { inferenceProfileIdentifier: config.modelId }
53
- : new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
54
- const res = await client.send(command);
55
- if (res.inferenceProfileName) {
56
- log.info(`Inference profile '${config.modelId}' available`);
57
- return true;
58
- }
59
- return false;
60
- } catch (err: unknown) {
61
- const e = err as { name?: string; message?: string };
62
- log.warn(`Inference profile health check failed for '${config.modelId}': ${e.name ?? e.message}`, { provider: config.provider, modelId: config.modelId });
63
- return false;
39
+ const client: BedrockHealthClient = _testClient ?? new (await import('@aws-sdk/client-bedrock')).BedrockClient({});
40
+
41
+ // Try GetInferenceProfile first (covers cross-region and global profiles).
42
+ try {
43
+ const command = _testClient
44
+ ? { inferenceProfileIdentifier: config.modelId }
45
+ : new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
46
+ const res = await client.send(command);
47
+ if (res.inferenceProfileName) {
48
+ log.info(`Inference profile '${config.modelId}' available`);
49
+ return true;
64
50
  }
51
+ } catch (err: unknown) {
52
+ const e = err as { name?: string; message?: string };
53
+ log.debug?.(`Not an inference profile: ${e.name ?? e.message}`, { modelId: config.modelId });
65
54
  }
66
55
 
56
+ // Try GetFoundationModel (covers base model IDs).
67
57
  try {
68
- const client = await getClient();
69
58
  const command = _testClient
70
59
  ? { modelIdentifier: config.modelId }
71
60
  : new (await import('@aws-sdk/client-bedrock')).GetFoundationModelCommand({ modelIdentifier: config.modelId });
@@ -74,27 +63,14 @@ export async function checkModelHealth(config: ModelConfig, log: ChildLogger, _t
74
63
  log.info(`Bedrock model '${config.modelId}' exists in catalog`);
75
64
  return true;
76
65
  }
77
- return false;
78
66
  } catch (err: unknown) {
79
67
  const e = err as { name?: string; message?: string };
80
- // GetFoundationModel throws for unknown models (ValidationException / ResourceNotFoundException).
81
- if (e.name === 'ValidationException' || e.name === 'ResourceNotFoundException') {
82
- try {
83
- const client = await getClient();
84
- const listCommand = _testClient
85
- ? {}
86
- : new (await import('@aws-sdk/client-bedrock')).ListFoundationModelsCommand({});
87
- const list = await client.send(listCommand);
88
- const available = list.modelSummaries?.map((m: { modelId?: string }) => m.modelId).filter(Boolean) ?? [];
89
- log.warn(`Bedrock model '${config.modelId}' not found. Available: ${available.slice(0, 10).join(', ')}${available.length > 10 ? ` (+${available.length - 10} more)` : ''}`);
90
- } catch {
91
- log.warn(`Bedrock model '${config.modelId}' not found. Could not list available models.`);
92
- }
93
- return false;
94
- }
95
- log.warn(`Bedrock health check failed: ${e.name ?? e.message}. Verify AWS credentials are configured.`, { provider: config.provider, modelId: config.modelId });
96
- return false;
68
+ log.debug?.(`Not a foundation model: ${e.name ?? e.message}`, { modelId: config.modelId });
97
69
  }
70
+
71
+ // Both failed — model not found.
72
+ log.warn(`Bedrock model '${config.modelId}' not found as inference profile or foundation model. Verify the model ID and AWS credentials.`, { provider: config.provider, modelId: config.modelId });
73
+ return false;
98
74
  }
99
75
 
100
76
  if (config.provider === 'openai-api') {
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
2
  export const BB_NAME = 'Agent';
3
- export const BB_VERSION = '0.1.1';
3
+ export const BB_VERSION = '0.1.3';