@aws-blocks/bb-agent 0.1.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 (75) hide show
  1. package/LICENSE +174 -0
  2. package/README.md +801 -0
  3. package/dist/agent.aws.d.ts +7 -0
  4. package/dist/agent.aws.d.ts.map +1 -0
  5. package/dist/agent.aws.js +9 -0
  6. package/dist/agent.d.ts +121 -0
  7. package/dist/agent.d.ts.map +1 -0
  8. package/dist/agent.js +588 -0
  9. package/dist/agent.mock.d.ts +7 -0
  10. package/dist/agent.mock.d.ts.map +1 -0
  11. package/dist/agent.mock.js +12 -0
  12. package/dist/errors.d.ts +39 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +40 -0
  15. package/dist/file-bucket-snapshot-storage.d.ts +49 -0
  16. package/dist/file-bucket-snapshot-storage.d.ts.map +1 -0
  17. package/dist/file-bucket-snapshot-storage.js +84 -0
  18. package/dist/index.aws.d.ts +5 -0
  19. package/dist/index.aws.d.ts.map +1 -0
  20. package/dist/index.aws.js +5 -0
  21. package/dist/index.browser.d.ts +4 -0
  22. package/dist/index.browser.d.ts.map +1 -0
  23. package/dist/index.browser.js +8 -0
  24. package/dist/index.cdk.d.ts +15 -0
  25. package/dist/index.cdk.d.ts.map +1 -0
  26. package/dist/index.cdk.js +60 -0
  27. package/dist/index.hooks.d.ts +122 -0
  28. package/dist/index.hooks.d.ts.map +1 -0
  29. package/dist/index.hooks.js +179 -0
  30. package/dist/index.mock.d.ts +5 -0
  31. package/dist/index.mock.d.ts.map +1 -0
  32. package/dist/index.mock.js +5 -0
  33. package/dist/index.test.d.ts +2 -0
  34. package/dist/index.test.d.ts.map +1 -0
  35. package/dist/index.test.js +864 -0
  36. package/dist/model-factory.d.ts +26 -0
  37. package/dist/model-factory.d.ts.map +1 -0
  38. package/dist/model-factory.js +197 -0
  39. package/dist/models.d.ts +83 -0
  40. package/dist/models.d.ts.map +1 -0
  41. package/dist/models.js +84 -0
  42. package/dist/providers/canned.d.ts +32 -0
  43. package/dist/providers/canned.d.ts.map +1 -0
  44. package/dist/providers/canned.js +187 -0
  45. package/dist/providers/throwing.d.ts +10 -0
  46. package/dist/providers/throwing.d.ts.map +1 -0
  47. package/dist/providers/throwing.js +16 -0
  48. package/dist/schemas.d.ts +59 -0
  49. package/dist/schemas.d.ts.map +1 -0
  50. package/dist/schemas.js +36 -0
  51. package/dist/types.d.ts +295 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +3 -0
  54. package/dist/version.d.ts +3 -0
  55. package/dist/version.d.ts.map +1 -0
  56. package/dist/version.js +3 -0
  57. package/package.json +59 -0
  58. package/src/agent.aws.ts +13 -0
  59. package/src/agent.mock.ts +16 -0
  60. package/src/agent.ts +604 -0
  61. package/src/errors.ts +44 -0
  62. package/src/file-bucket-snapshot-storage.ts +85 -0
  63. package/src/index.aws.ts +7 -0
  64. package/src/index.browser.ts +10 -0
  65. package/src/index.cdk.ts +70 -0
  66. package/src/index.hooks.ts +256 -0
  67. package/src/index.mock.ts +7 -0
  68. package/src/index.test.ts +1010 -0
  69. package/src/model-factory.ts +228 -0
  70. package/src/models.ts +88 -0
  71. package/src/providers/canned.ts +205 -0
  72. package/src/providers/throwing.ts +19 -0
  73. package/src/schemas.ts +40 -0
  74. package/src/types.ts +311 -0
  75. package/src/version.ts +3 -0
@@ -0,0 +1,26 @@
1
+ import { type Model } from '@strands-agents/sdk';
2
+ import type { BaseModelConfig } from '@strands-agents/sdk';
3
+ import type { ChildLogger } from '@aws-blocks/bb-logger';
4
+ import type { ModelConfig } from './types.js';
5
+ /**
6
+ * Checks if a model endpoint is available and the specified model exists.
7
+ * Verifies endpoint/model availability only. Does not guarantee EULA acceptance or feature support (e.g. tool calling).
8
+ * Even calls to verified models can fail at invocation time (e.g. legacy models, quota limits) — always check error logs.
9
+ * For openai-api: pings GET /v1/models and checks if modelId is in the list.
10
+ * For bedrock: verifies model availability via @aws-sdk/client-bedrock (free, no inference cost).
11
+ * For canned: always returns true.
12
+ */
13
+ /** @internal Injectable client interface for testing. */
14
+ export interface BedrockHealthClient {
15
+ send(command: any): Promise<any>;
16
+ }
17
+ export declare function checkModelHealth(config: ModelConfig, log: ChildLogger, _testClient?: BedrockHealthClient): Promise<boolean>;
18
+ /**
19
+ * Maps Blocks' ModelConfig to the corresponding Strands model provider.
20
+ * The developer configures one unified ModelConfig shape — this factory
21
+ * translates it to BedrockModel, OpenAIModel, or CannedProvider internally.
22
+ *
23
+ * @see https://strandsagents.com/docs/user-guide/concepts/model-providers/
24
+ */
25
+ export declare function createStrandsModel(config?: ModelConfig, log?: ChildLogger): Promise<Model<BaseModelConfig>>;
26
+ //# sourceMappingURL=model-factory.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,197 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { BedrockModel } from '@strands-agents/sdk';
4
+ import { OpenAIModel } from '@strands-agents/sdk/models/openai';
5
+ import { CannedProvider } from './providers/canned.js';
6
+ import { ThrowingProvider } from './providers/throwing.js';
7
+ import { AgentErrors, blocksAgentError } from './errors.js';
8
+ export async function checkModelHealth(config, log, _testClient) {
9
+ if (!config || config.provider === 'canned') {
10
+ log.info('Using canned provider (local mock, no real model)');
11
+ return true;
12
+ }
13
+ if (config.provider === 'throwing') {
14
+ log.info('Using throwing provider (test-only)');
15
+ return true;
16
+ }
17
+ log.info(`Checking model health: ${config.provider}${config.modelId ? ` (${config.modelId})` : ''}`);
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;
44
+ }
45
+ }
46
+ try {
47
+ const client = await getClient();
48
+ const command = _testClient
49
+ ? { modelIdentifier: config.modelId }
50
+ : new (await import('@aws-sdk/client-bedrock')).GetFoundationModelCommand({ modelIdentifier: config.modelId });
51
+ const res = await client.send(command);
52
+ if (res.modelDetails) {
53
+ log.info(`Bedrock model '${config.modelId}' exists in catalog`);
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+ catch (err) {
59
+ 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;
78
+ }
79
+ }
80
+ if (config.provider === 'openai-api') {
81
+ const endpoint = config.endpoint ?? 'https://api.openai.com/v1';
82
+ const baseUrl = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint;
83
+ const url = `${baseUrl}/models`;
84
+ const resolvedKey = typeof config.apiKey === 'function'
85
+ ? await config.apiKey()
86
+ : config.apiKey ?? process.env.OPENAI_API_KEY;
87
+ // 1. Check if endpoint is reachable
88
+ let res;
89
+ try {
90
+ res = await fetch(url, {
91
+ method: 'GET',
92
+ headers: resolvedKey ? { Authorization: `Bearer ${resolvedKey}` } : {},
93
+ signal: AbortSignal.timeout(3000),
94
+ });
95
+ }
96
+ catch (err) {
97
+ log.warn(`Endpoint unreachable: ${baseUrl}`, { provider: config.provider, error: err.message });
98
+ return false;
99
+ }
100
+ if (!res.ok) {
101
+ log.warn(`Endpoint returned HTTP ${res.status}: ${baseUrl}`, { provider: config.provider, status: res.status });
102
+ return false;
103
+ }
104
+ // 2. Endpoint is up — check if specified model exists
105
+ if (!config.modelId)
106
+ return true;
107
+ // Parse the model list defensively. A 200 response is not a guarantee of a
108
+ // JSON body: misconfigured proxies, captive portals, or a non-OpenAI server
109
+ // sharing the URL can return HTML or plain text. JSON.parse throws on such
110
+ // bodies — if that escaped, it would abort the model fallback loop in
111
+ // createStrandsAgent() and prevent the implicit canned fallback from ever
112
+ // running. Treat an unparseable response as "unhealthy" (return false) so the
113
+ // next candidate is tried, matching the fetch-failure handling above. Read the
114
+ // raw text first so we can log a short snippet of the offending body, which
115
+ // makes a misconfigured proxy / captive portal obvious during debugging.
116
+ let body;
117
+ let text = '';
118
+ try {
119
+ text = await res.text();
120
+ }
121
+ catch (err) {
122
+ log.warn(`Failed to read model-list response body: ${baseUrl}`, { provider: config.provider, error: err.message });
123
+ return false;
124
+ }
125
+ try {
126
+ body = JSON.parse(text);
127
+ }
128
+ catch (err) {
129
+ log.warn(`Endpoint returned a non-JSON body, make sure this is a valid OpenAI-compatible server: ${baseUrl}`, { provider: config.provider, error: err.message, bodySnippet: text.slice(0, 100) });
130
+ return false;
131
+ }
132
+ const availableModels = body.data?.map(m => m.id) ?? [];
133
+ if (availableModels.includes(config.modelId)) {
134
+ log.info(`Model '${config.modelId}' available at ${baseUrl}`);
135
+ return true;
136
+ }
137
+ // 3. Model not found — log what IS available
138
+ log.warn(`Model '${config.modelId}' not found at ${baseUrl}. Available: ${availableModels.join(', ') || 'none'}`, { provider: config.provider, modelId: config.modelId, availableModels });
139
+ return false;
140
+ }
141
+ return false;
142
+ }
143
+ /**
144
+ * Maps Blocks' ModelConfig to the corresponding Strands model provider.
145
+ * The developer configures one unified ModelConfig shape — this factory
146
+ * translates it to BedrockModel, OpenAIModel, or CannedProvider internally.
147
+ *
148
+ * @see https://strandsagents.com/docs/user-guide/concepts/model-providers/
149
+ */
150
+ export async function createStrandsModel(config, log) {
151
+ if (!config || config.provider === 'canned')
152
+ return new CannedProvider();
153
+ // Test-only provider — throws mid-stream to verify error handling
154
+ if (config.provider === 'throwing') {
155
+ log?.warn('ThrowingProvider is only for internal test purposes');
156
+ return new ThrowingProvider();
157
+ }
158
+ if (config.provider === 'bedrock') {
159
+ if (!config.modelId) {
160
+ throw blocksAgentError(AgentErrors.InvalidModelConfig, "Model provider 'bedrock' requires modelId.");
161
+ }
162
+ return new BedrockModel({
163
+ modelId: config.modelId,
164
+ ...(config.inferenceConfig && {
165
+ temperature: config.inferenceConfig.temperature,
166
+ topP: config.inferenceConfig.topP,
167
+ maxTokens: config.inferenceConfig.maxTokens,
168
+ stopSequences: config.inferenceConfig.stopSequences,
169
+ }),
170
+ });
171
+ }
172
+ if (config.provider === 'openai-api') {
173
+ if (!config.modelId) {
174
+ throw blocksAgentError(AgentErrors.InvalidModelConfig, "Model provider 'openai-api' requires modelId.");
175
+ }
176
+ // Resolve apiKey: string, async function, or env var fallback
177
+ const apiKey = typeof config.apiKey === 'function' ? await config.apiKey() : config.apiKey;
178
+ if (!apiKey && !process.env.OPENAI_API_KEY) {
179
+ throw blocksAgentError(AgentErrors.InvalidModelConfig, "provider 'openai-api' requires apiKey or OPENAI_API_KEY environment variable.");
180
+ }
181
+ return new OpenAIModel({
182
+ api: 'chat',
183
+ apiKey: apiKey ?? '',
184
+ ...(config.endpoint && { clientConfig: { baseURL: config.endpoint } }),
185
+ modelId: config.modelId,
186
+ ...(config.inferenceConfig && {
187
+ temperature: config.inferenceConfig.temperature,
188
+ topP: config.inferenceConfig.topP,
189
+ maxTokens: config.inferenceConfig.maxTokens,
190
+ ...(config.inferenceConfig.stopSequences && {
191
+ params: { stop: config.inferenceConfig.stopSequences },
192
+ }),
193
+ }),
194
+ });
195
+ }
196
+ throw blocksAgentError(AgentErrors.InvalidModelConfig, `Unknown provider: '${config.provider}'.`);
197
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Pre-configured Bedrock model presets using cross-region inference profiles.
3
+ * Names are capability-based so the underlying model can be upgraded without breaking user code.
4
+ */
5
+ export declare const BedrockModels: {
6
+ /** Highest capability and best performance. Recommended default. Currently: Claude Opus 4.8. */
7
+ readonly DEFAULT: {
8
+ readonly provider: "bedrock";
9
+ readonly modelId: "us.anthropic.claude-opus-4-8-20250610-v1:0";
10
+ };
11
+ /** Strong quality/cost balance. Currently: Claude Sonnet 4. */
12
+ readonly BALANCED: {
13
+ readonly provider: "bedrock";
14
+ readonly modelId: "us.anthropic.claude-sonnet-4-20250514-v1:0";
15
+ };
16
+ /** Fastest and lowest latency. Currently: Claude Haiku 4.5. */
17
+ readonly FAST: {
18
+ readonly provider: "bedrock";
19
+ readonly modelId: "us.anthropic.claude-haiku-4-5-20251001-v1:0";
20
+ };
21
+ /** Low cost per token with acceptable quality. Currently: Amazon Nova Pro. */
22
+ readonly BUDGET: {
23
+ readonly provider: "bedrock";
24
+ readonly modelId: "us.amazon.nova-pro-v1:0";
25
+ };
26
+ /** Ultra-cheap for simple tasks. Currently: Amazon Nova Lite. */
27
+ readonly MICRO: {
28
+ readonly provider: "bedrock";
29
+ readonly modelId: "us.amazon.nova-lite-v1:0";
30
+ };
31
+ };
32
+ /**
33
+ * Pre-configured Ollama model presets for local development.
34
+ * These are convenience shortcuts that use the `openai-api` provider under the hood.
35
+ *
36
+ * **Requirements:**
37
+ * - Ollama must be installed and running (`ollama serve`)
38
+ * - The model must be pulled first (`ollama pull <modelId>`)
39
+ * - Assumes the default Ollama endpoint: `http://localhost:11434/v1`
40
+ *
41
+ * If your Ollama runs on a different port or host, use the `openai-api` provider directly:
42
+ * ```ts
43
+ * { provider: 'openai-api', modelId: 'llama3.1:8b', endpoint: 'http://custom-host:11434/v1', apiKey: 'ollama' }
44
+ * ```
45
+ */
46
+ export declare const OllamaModels: {
47
+ /** Fast and lightweight for quick iteration. Currently: Llama 3.2 3B (~2 GB, needs 4 GB VRAM). */
48
+ readonly XSMALL: {
49
+ readonly provider: "openai-api";
50
+ readonly modelId: "llama3.2:3b";
51
+ readonly endpoint: "http://localhost:11434/v1";
52
+ readonly apiKey: "ollama";
53
+ };
54
+ /** Good balance of speed and capability. Currently: Llama 3.1 8B (~4.7 GB, needs 8 GB VRAM). */
55
+ readonly SMALL: {
56
+ readonly provider: "openai-api";
57
+ readonly modelId: "llama3.1:8b";
58
+ readonly endpoint: "http://localhost:11434/v1";
59
+ readonly apiKey: "ollama";
60
+ };
61
+ /** Strong reasoning at moderate size. Currently: DeepSeek R1 14B (~9 GB, needs 16 GB VRAM). */
62
+ readonly MEDIUM: {
63
+ readonly provider: "openai-api";
64
+ readonly modelId: "deepseek-r1:14b";
65
+ readonly endpoint: "http://localhost:11434/v1";
66
+ readonly apiKey: "ollama";
67
+ };
68
+ /** High quality for complex tasks. Currently: Llama 3.3 70B (~43 GB, needs 48 GB+ VRAM). */
69
+ readonly LARGE: {
70
+ readonly provider: "openai-api";
71
+ readonly modelId: "llama3.3:70b";
72
+ readonly endpoint: "http://localhost:11434/v1";
73
+ readonly apiKey: "ollama";
74
+ };
75
+ /** Largest local model. Currently: Llama 4 Scout (~67 GB, needs 80 GB+ VRAM). */
76
+ readonly XLARGE: {
77
+ readonly provider: "openai-api";
78
+ readonly modelId: "llama4:16x17b";
79
+ readonly endpoint: "http://localhost:11434/v1";
80
+ readonly apiKey: "ollama";
81
+ };
82
+ };
83
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,eAAO,MAAM,aAAa;IACzB,gGAAgG;;;;;IAKhG,+DAA+D;;;;;IAK/D,+DAA+D;;;;;IAK/D,8EAA8E;;;;;IAK9E,iEAAiE;;;;;CAKlB,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY;IACxB,kGAAkG;;;;;;;IAOlG,gGAAgG;;;;;;;IAOhG,+FAA+F;;;;;;;IAO/F,4FAA4F;;;;;;;IAO5F,iFAAiF;;;;;;;CAOlC,CAAC"}
package/dist/models.js ADDED
@@ -0,0 +1,84 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Pre-configured Bedrock model presets using cross-region inference profiles.
5
+ * Names are capability-based so the underlying model can be upgraded without breaking user code.
6
+ */
7
+ export const BedrockModels = {
8
+ /** Highest capability and best performance. Recommended default. Currently: Claude Opus 4.8. */
9
+ DEFAULT: {
10
+ provider: 'bedrock',
11
+ modelId: 'us.anthropic.claude-opus-4-8-20250610-v1:0',
12
+ },
13
+ /** Strong quality/cost balance. Currently: Claude Sonnet 4. */
14
+ BALANCED: {
15
+ provider: 'bedrock',
16
+ modelId: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
17
+ },
18
+ /** Fastest and lowest latency. Currently: Claude Haiku 4.5. */
19
+ FAST: {
20
+ provider: 'bedrock',
21
+ modelId: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
22
+ },
23
+ /** Low cost per token with acceptable quality. Currently: Amazon Nova Pro. */
24
+ BUDGET: {
25
+ provider: 'bedrock',
26
+ modelId: 'us.amazon.nova-pro-v1:0',
27
+ },
28
+ /** Ultra-cheap for simple tasks. Currently: Amazon Nova Lite. */
29
+ MICRO: {
30
+ provider: 'bedrock',
31
+ modelId: 'us.amazon.nova-lite-v1:0',
32
+ },
33
+ };
34
+ /**
35
+ * Pre-configured Ollama model presets for local development.
36
+ * These are convenience shortcuts that use the `openai-api` provider under the hood.
37
+ *
38
+ * **Requirements:**
39
+ * - Ollama must be installed and running (`ollama serve`)
40
+ * - The model must be pulled first (`ollama pull <modelId>`)
41
+ * - Assumes the default Ollama endpoint: `http://localhost:11434/v1`
42
+ *
43
+ * If your Ollama runs on a different port or host, use the `openai-api` provider directly:
44
+ * ```ts
45
+ * { provider: 'openai-api', modelId: 'llama3.1:8b', endpoint: 'http://custom-host:11434/v1', apiKey: 'ollama' }
46
+ * ```
47
+ */
48
+ export const OllamaModels = {
49
+ /** Fast and lightweight for quick iteration. Currently: Llama 3.2 3B (~2 GB, needs 4 GB VRAM). */
50
+ XSMALL: {
51
+ provider: 'openai-api',
52
+ modelId: 'llama3.2:3b',
53
+ endpoint: 'http://localhost:11434/v1',
54
+ apiKey: 'ollama',
55
+ },
56
+ /** Good balance of speed and capability. Currently: Llama 3.1 8B (~4.7 GB, needs 8 GB VRAM). */
57
+ SMALL: {
58
+ provider: 'openai-api',
59
+ modelId: 'llama3.1:8b',
60
+ endpoint: 'http://localhost:11434/v1',
61
+ apiKey: 'ollama',
62
+ },
63
+ /** Strong reasoning at moderate size. Currently: DeepSeek R1 14B (~9 GB, needs 16 GB VRAM). */
64
+ MEDIUM: {
65
+ provider: 'openai-api',
66
+ modelId: 'deepseek-r1:14b',
67
+ endpoint: 'http://localhost:11434/v1',
68
+ apiKey: 'ollama',
69
+ },
70
+ /** High quality for complex tasks. Currently: Llama 3.3 70B (~43 GB, needs 48 GB+ VRAM). */
71
+ LARGE: {
72
+ provider: 'openai-api',
73
+ modelId: 'llama3.3:70b',
74
+ endpoint: 'http://localhost:11434/v1',
75
+ apiKey: 'ollama',
76
+ },
77
+ /** Largest local model. Currently: Llama 4 Scout (~67 GB, needs 80 GB+ VRAM). */
78
+ XLARGE: {
79
+ provider: 'openai-api',
80
+ modelId: 'llama4:16x17b',
81
+ endpoint: 'http://localhost:11434/v1',
82
+ apiKey: 'ollama',
83
+ },
84
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * CannedProvider — a fake Strands model provider for local dev.
3
+ * Returns keyword-based responses without calling any real model.
4
+ * Speaks the same ModelStreamEvent protocol as Bedrock/OpenAI,
5
+ * so Strands processes it identically to a real provider.
6
+ *
7
+ * Tool call support: if the prompt mentions a tool name from the available toolSpecs,
8
+ * emits toolUse events so Strands executes the tool. On the follow-up call (with tool
9
+ * result in messages), emits a simple text summary.
10
+ *
11
+ * @see https://strandsagents.com/docs/user-guide/concepts/model-providers/custom_model_provider/
12
+ */
13
+ import { Model } from '@strands-agents/sdk';
14
+ import type { Message, ModelStreamEvent, StreamOptions } from '@strands-agents/sdk';
15
+ interface CannedConfig {
16
+ modelId: string;
17
+ }
18
+ export declare class CannedProvider extends Model<CannedConfig> {
19
+ private config;
20
+ constructor(config?: Partial<CannedConfig>);
21
+ updateConfig(config: Partial<CannedConfig>): void;
22
+ getConfig(): CannedConfig;
23
+ stream(messages: Message[], options?: StreamOptions): AsyncIterable<ModelStreamEvent>;
24
+ /** Emit a text response as ModelStreamEvents. */
25
+ private emitText;
26
+ /** Emit multiple tool calls in one message (parallel execution). */
27
+ private emitParallelToolCalls;
28
+ /** Emit a tool call as ModelStreamEvents. Strands executes the tool and calls stream() again with the result. */
29
+ private emitToolCall;
30
+ }
31
+ export {};
32
+ //# sourceMappingURL=canned.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canned.d.ts","sourceRoot":"","sources":["../../src/providers/canned.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpF,UAAU,YAAY;IACrB,OAAO,EAAE,MAAM,CAAC;CAChB;AAoGD,qBAAa,cAAe,SAAQ,KAAK,CAAC,YAAY,CAAC;IACtD,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC;IAK1C,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,IAAI;IAIjD,SAAS,IAAI,YAAY;IAIlB,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,aAAa,CAAC,gBAAgB,CAAC;IA+B5F,iDAAiD;YAClC,QAAQ;IAWvB,oEAAoE;YACrD,qBAAqB;IAYpC,iHAAiH;YAClG,YAAY;CAS3B"}
@@ -0,0 +1,187 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * CannedProvider — a fake Strands model provider for local dev.
5
+ * Returns keyword-based responses without calling any real model.
6
+ * Speaks the same ModelStreamEvent protocol as Bedrock/OpenAI,
7
+ * so Strands processes it identically to a real provider.
8
+ *
9
+ * Tool call support: if the prompt mentions a tool name from the available toolSpecs,
10
+ * emits toolUse events so Strands executes the tool. On the follow-up call (with tool
11
+ * result in messages), emits a simple text summary.
12
+ *
13
+ * @see https://strandsagents.com/docs/user-guide/concepts/model-providers/custom_model_provider/
14
+ */
15
+ import { Model } from '@strands-agents/sdk';
16
+ const CANNED_RESPONSES = {
17
+ weather: 'The weather is 22°C and sunny. [canned response]',
18
+ order: 'Order #12345 has been shipped and is on its way. [canned response]',
19
+ help: 'I can help you with weather, orders, and general questions. [canned response]',
20
+ };
21
+ const DEFAULT_RESPONSE = 'This is a canned mock response. No real model was called. [canned]';
22
+ function matchResponse(prompt) {
23
+ const lower = prompt.toLowerCase();
24
+ for (const [keyword, response] of Object.entries(CANNED_RESPONSES)) {
25
+ if (lower.includes(keyword))
26
+ return response;
27
+ }
28
+ return DEFAULT_RESPONSE;
29
+ }
30
+ /**
31
+ * Match a single word against the prompt on word boundaries (case-insensitive).
32
+ * Uses `\b...\b` rather than substring `includes()` so a tool word like "cat"
33
+ * (from `getCat`) is NOT triggered by an unrelated word like "category", and
34
+ * "pass" (from `getPass`) is not triggered by "password". The word is regex-
35
+ * escaped so punctuation in tool names can't break the pattern.
36
+ */
37
+ function promptMentionsWord(lowerPrompt, word) {
38
+ const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ return new RegExp(`\\b${escaped}\\b`).test(lowerPrompt);
40
+ }
41
+ /** Find ALL tools mentioned in the prompt (for parallel tool calls). */
42
+ function findAllToolMatches(prompt, toolSpecs) {
43
+ if (!toolSpecs?.length)
44
+ return [];
45
+ const lower = prompt.toLowerCase();
46
+ return toolSpecs.filter(t => {
47
+ const name = t.name.toLowerCase();
48
+ if (promptMentionsWord(lower, name))
49
+ return true;
50
+ // Split camelCase into words (getWeather -> "get weather") and match each
51
+ // on word boundaries. Skip short words (<=2 chars) to avoid noise.
52
+ const words = t.name.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().split(' ');
53
+ return words.some(w => w.length > 2 && promptMentionsWord(lower, w));
54
+ }).map(t => t.name);
55
+ }
56
+ /** Check if the last message contains a tool result — means we're in the follow-up after a tool call. */
57
+ function hasToolResult(messages) {
58
+ const last = messages[messages.length - 1];
59
+ return last?.content?.some((block) => 'toolResult' in block || block.type === 'toolResultBlock' || ('toolUseId' in block && 'status' in block)) ?? false;
60
+ }
61
+ /** Extract tool result text from the last message. */
62
+ function getToolResultText(messages) {
63
+ const last = messages[messages.length - 1];
64
+ const results = [];
65
+ for (const block of last?.content ?? []) {
66
+ const b = block;
67
+ if ('toolResult' in b || b.type === 'toolResultBlock' || ('toolUseId' in b && 'status' in b)) {
68
+ const content = b.toolResult?.content ?? b.content ?? [];
69
+ if (!Array.isArray(content)) {
70
+ results.push(String(content));
71
+ continue;
72
+ }
73
+ results.push(content.map((c) => c.text ?? JSON.stringify(c)).join(' '));
74
+ }
75
+ }
76
+ return results.join(' | ');
77
+ }
78
+ /** Generate placeholder input from a JSON Schema. Produces values that pass validation. */
79
+ function generatePlaceholderInput(schema) {
80
+ if (!schema || typeof schema !== 'object')
81
+ return {};
82
+ if (schema.type === 'object' && schema.properties) {
83
+ const result = {};
84
+ for (const [key, prop] of Object.entries(schema.properties)) {
85
+ if (prop.type === 'string') {
86
+ if (prop.enum?.length)
87
+ result[key] = prop.enum[0];
88
+ else
89
+ result[key] = 'sample';
90
+ }
91
+ else if (prop.type === 'number' || prop.type === 'integer') {
92
+ result[key] = 1;
93
+ }
94
+ else if (prop.type === 'boolean') {
95
+ result[key] = true;
96
+ }
97
+ else if (prop.type === 'array') {
98
+ result[key] = [];
99
+ }
100
+ else if (prop.type === 'object') {
101
+ result[key] = generatePlaceholderInput(prop);
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+ return {};
107
+ }
108
+ /** Look up a tool's inputSchema from toolSpecs and generate placeholder input. */
109
+ function getToolInput(toolName, toolSpecs) {
110
+ const spec = toolSpecs?.find(t => t.name === toolName);
111
+ if (!spec?.inputSchema)
112
+ return '{}';
113
+ return JSON.stringify(generatePlaceholderInput(spec.inputSchema));
114
+ }
115
+ let toolCallCounter = 0;
116
+ export class CannedProvider extends Model {
117
+ config;
118
+ constructor(config) {
119
+ super();
120
+ this.config = { modelId: config?.modelId ?? 'canned-mock' };
121
+ }
122
+ updateConfig(config) {
123
+ Object.assign(this.config, config);
124
+ }
125
+ getConfig() {
126
+ return { ...this.config };
127
+ }
128
+ async *stream(messages, options) {
129
+ const lastMessage = messages[messages.length - 1];
130
+ const prompt = lastMessage?.content
131
+ ?.map((block) => ('text' in block ? block.text : ''))
132
+ .join('') ?? '';
133
+ // Follow-up after tool execution — Strands sends the tool result back to the model
134
+ if (hasToolResult(messages)) {
135
+ const resultText = getToolResultText(messages);
136
+ yield* this.emitText(`I called the tool. Output: ${resultText} [canned tool response]`);
137
+ return;
138
+ }
139
+ // Check if prompt mentions tool names — trigger tool call(s)
140
+ const toolMatches = findAllToolMatches(prompt, options?.toolSpecs);
141
+ if (toolMatches.length > 1) {
142
+ yield* this.emitParallelToolCalls(toolMatches, options?.toolSpecs);
143
+ return;
144
+ }
145
+ const toolName = toolMatches[0];
146
+ if (toolName) {
147
+ yield* this.emitToolCall(toolName, options?.toolSpecs);
148
+ return;
149
+ }
150
+ // Default: keyword-based text response
151
+ // Default: keyword-based text response
152
+ yield* this.emitText(matchResponse(prompt));
153
+ }
154
+ /** Emit a text response as ModelStreamEvents. */
155
+ async *emitText(response) {
156
+ yield { type: 'modelMessageStartEvent', role: 'assistant' };
157
+ yield { type: 'modelContentBlockStartEvent' };
158
+ for (const word of response.split(' ')) {
159
+ yield { type: 'modelContentBlockDeltaEvent', delta: { type: 'textDelta', text: word + ' ' } };
160
+ }
161
+ yield { type: 'modelContentBlockStopEvent' };
162
+ yield { type: 'modelMessageStopEvent', stopReason: 'endTurn' };
163
+ yield { type: 'modelMetadataEvent', usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, metrics: { latencyMs: 0 } };
164
+ }
165
+ /** Emit multiple tool calls in one message (parallel execution). */
166
+ async *emitParallelToolCalls(toolNames, toolSpecs) {
167
+ yield { type: 'modelMessageStartEvent', role: 'assistant' };
168
+ for (const toolName of toolNames) {
169
+ const toolUseId = `canned-tool-${++toolCallCounter}`;
170
+ yield { type: 'modelContentBlockStartEvent', start: { type: 'toolUseStart', name: toolName, toolUseId } };
171
+ yield { type: 'modelContentBlockDeltaEvent', delta: { type: 'toolUseInputDelta', input: getToolInput(toolName, toolSpecs) } };
172
+ yield { type: 'modelContentBlockStopEvent' };
173
+ }
174
+ yield { type: 'modelMessageStopEvent', stopReason: 'toolUse' };
175
+ yield { type: 'modelMetadataEvent', usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, metrics: { latencyMs: 0 } };
176
+ }
177
+ /** Emit a tool call as ModelStreamEvents. Strands executes the tool and calls stream() again with the result. */
178
+ async *emitToolCall(toolName, toolSpecs) {
179
+ const toolUseId = `canned-tool-${++toolCallCounter}`;
180
+ yield { type: 'modelMessageStartEvent', role: 'assistant' };
181
+ yield { type: 'modelContentBlockStartEvent', start: { type: 'toolUseStart', name: toolName, toolUseId } };
182
+ yield { type: 'modelContentBlockDeltaEvent', delta: { type: 'toolUseInputDelta', input: getToolInput(toolName, toolSpecs) } };
183
+ yield { type: 'modelContentBlockStopEvent' };
184
+ yield { type: 'modelMessageStopEvent', stopReason: 'toolUse' };
185
+ yield { type: 'modelMetadataEvent', usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, metrics: { latencyMs: 0 } };
186
+ }
187
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ThrowingProvider — extends CannedProvider but throws mid-stream.
3
+ * Used in unit tests to verify block buffer flush on error.
4
+ */
5
+ import { CannedProvider } from './canned.js';
6
+ import type { Message, ModelStreamEvent, StreamOptions } from '@strands-agents/sdk';
7
+ export declare class ThrowingProvider extends CannedProvider {
8
+ stream(_messages: Message[], _options?: StreamOptions): AsyncIterable<ModelStreamEvent>;
9
+ }
10
+ //# sourceMappingURL=throwing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throwing.d.ts","sourceRoot":"","sources":["../../src/providers/throwing.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpF,qBAAa,gBAAiB,SAAQ,cAAc;IAC5C,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,aAAa,GAAG,aAAa,CAAC,gBAAgB,CAAC;CAO9F"}
@@ -0,0 +1,16 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * ThrowingProvider — extends CannedProvider but throws mid-stream.
5
+ * Used in unit tests to verify block buffer flush on error.
6
+ */
7
+ import { CannedProvider } from './canned.js';
8
+ export class ThrowingProvider extends CannedProvider {
9
+ async *stream(_messages, _options) {
10
+ yield { type: 'modelMessageStartEvent', role: 'assistant' };
11
+ yield { type: 'modelContentBlockStartEvent' };
12
+ yield { type: 'modelContentBlockDeltaEvent', delta: { type: 'textDelta', text: 'partial ' } };
13
+ yield { type: 'modelContentBlockDeltaEvent', delta: { type: 'textDelta', text: 'text' } };
14
+ throw new Error('simulated mid-stream failure');
15
+ }
16
+ }