@aws/agentcore 0.21.0 → 0.22.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 (77) hide show
  1. package/README.md +17 -17
  2. package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +531 -90
  3. package/dist/assets/__tests__/googleadk-session-eviction.test.ts +87 -0
  4. package/dist/assets/__tests__/summarization-namespace.test.ts +43 -0
  5. package/dist/assets/python/a2a/strands/capabilities/memory/session.py +1 -1
  6. package/dist/assets/python/agui/strands/capabilities/memory/session.py +1 -1
  7. package/dist/assets/python/http/autogen/base/main.py +33 -10
  8. package/dist/assets/python/http/googleadk/base/main.py +45 -8
  9. package/dist/assets/python/http/langchain_langgraph/base/main.py +46 -7
  10. package/dist/assets/python/http/openaiagents/base/main.py +19 -8
  11. package/dist/assets/python/http/strands/base/main.py +36 -29
  12. package/dist/assets/python/http/strands/base/mcp_client/client.py +4 -1
  13. package/dist/assets/python/http/strands/base/model/load.py +116 -0
  14. package/dist/assets/python/http/strands/base/model/mantle_compat.py +21 -0
  15. package/dist/assets/python/http/strands/base/pyproject.toml +3 -0
  16. package/dist/assets/python/http/strands/capabilities/memory/session.py +1 -1
  17. package/dist/assets/typescript/http/strands/base/main.ts +96 -18
  18. package/dist/assets/typescript/http/strands/base/package.json +3 -2
  19. package/dist/assets/typescript/http/strands/capabilities/memory/memory.ts +52 -0
  20. package/dist/assets/typescript/http/vercelai/base/main.ts +42 -2
  21. package/dist/cli/index.mjs +574 -608
  22. package/dist/lib/errors/types.d.ts +25 -0
  23. package/dist/lib/errors/types.d.ts.map +1 -1
  24. package/dist/lib/errors/types.js +40 -1
  25. package/dist/lib/errors/types.js.map +1 -1
  26. package/dist/lib/secrets/cipher.d.ts +12 -0
  27. package/dist/lib/secrets/cipher.d.ts.map +1 -0
  28. package/dist/lib/secrets/cipher.js +54 -0
  29. package/dist/lib/secrets/cipher.js.map +1 -0
  30. package/dist/lib/secrets/index.d.ts +4 -0
  31. package/dist/lib/secrets/index.d.ts.map +1 -0
  32. package/dist/lib/secrets/index.js +15 -0
  33. package/dist/lib/secrets/index.js.map +1 -0
  34. package/dist/lib/secrets/key-provider.d.ts +16 -0
  35. package/dist/lib/secrets/key-provider.d.ts.map +1 -0
  36. package/dist/lib/secrets/key-provider.js +191 -0
  37. package/dist/lib/secrets/key-provider.js.map +1 -0
  38. package/dist/lib/secrets/sensitive-keys.d.ts +21 -0
  39. package/dist/lib/secrets/sensitive-keys.d.ts.map +1 -0
  40. package/dist/lib/secrets/sensitive-keys.js +67 -0
  41. package/dist/lib/secrets/sensitive-keys.js.map +1 -0
  42. package/dist/lib/utils/env.d.ts +4 -2
  43. package/dist/lib/utils/env.d.ts.map +1 -1
  44. package/dist/lib/utils/env.js +57 -27
  45. package/dist/lib/utils/env.js.map +1 -1
  46. package/dist/schema/constants.d.ts +29 -2
  47. package/dist/schema/constants.d.ts.map +1 -1
  48. package/dist/schema/constants.js +41 -5
  49. package/dist/schema/constants.js.map +1 -1
  50. package/dist/schema/schemas/agent-env.d.ts +47 -2
  51. package/dist/schema/schemas/agent-env.d.ts.map +1 -1
  52. package/dist/schema/schemas/agent-env.js +34 -5
  53. package/dist/schema/schemas/agent-env.js.map +1 -1
  54. package/dist/schema/schemas/agentcore-project.d.ts +46 -1
  55. package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
  56. package/dist/schema/schemas/auth.d.ts +2 -3
  57. package/dist/schema/schemas/auth.d.ts.map +1 -1
  58. package/dist/schema/schemas/auth.js +8 -7
  59. package/dist/schema/schemas/auth.js.map +1 -1
  60. package/dist/schema/schemas/connections.d.ts +185 -0
  61. package/dist/schema/schemas/connections.d.ts.map +1 -0
  62. package/dist/schema/schemas/connections.js +176 -0
  63. package/dist/schema/schemas/connections.js.map +1 -0
  64. package/dist/schema/schemas/index.d.ts +1 -0
  65. package/dist/schema/schemas/index.d.ts.map +1 -1
  66. package/dist/schema/schemas/index.js +1 -0
  67. package/dist/schema/schemas/index.js.map +1 -1
  68. package/dist/schema/schemas/primitives/config-bundle.d.ts +1 -0
  69. package/dist/schema/schemas/primitives/config-bundle.d.ts.map +1 -1
  70. package/dist/schema/schemas/primitives/config-bundle.js +3 -0
  71. package/dist/schema/schemas/primitives/config-bundle.js.map +1 -1
  72. package/dist/schema/schemas/primitives/harness.d.ts +43 -0
  73. package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
  74. package/dist/schema/schemas/primitives/harness.js +20 -0
  75. package/dist/schema/schemas/primitives/harness.js.map +1 -1
  76. package/npm-shrinkwrap.json +223 -0
  77. package/package.json +4 -1
@@ -1,4 +1,65 @@
1
1
  {{#if (eq modelProvider "Bedrock")}}
2
+ {{#if bedrockMantle}}
3
+ import os
4
+
5
+ from aws_bedrock_token_generator import provide_token
6
+ {{#if (eq mantleApiFormat "chat_completions")}}
7
+ from strands.models.openai import OpenAIModel
8
+ {{else}}
9
+ {{#if mantleProprietary}}
10
+ from strands.models.openai_responses import OpenAIResponsesModel
11
+ {{else}}
12
+ from model.mantle_compat import MantleCompatResponsesModel
13
+ {{/if}}
14
+ {{/if}}
15
+
16
+ MODEL_ID = "{{modelId}}"
17
+
18
+
19
+ def load_model():
20
+ """
21
+ Get a Bedrock Mantle model client. These OpenAI-compatible models (e.g. openai.gpt-5.5,
22
+ openai.gpt-oss-120b) are served via the Bedrock Mantle endpoint, NOT the Converse API — so they
23
+ are invoked through an OpenAI-style client authenticated with a short-lived Bedrock bearer token.
24
+ Region is read from AWS_REGION (set by the AgentCore runtime).
25
+ """
26
+ region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1"))
27
+ token = provide_token(region=region)
28
+ {{#if mantleProprietary}}
29
+ # Proprietary OpenAI models only work on the /openai/v1 Mantle path.
30
+ base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1"
31
+ {{else}}
32
+ # Open-source OpenAI models (gpt-oss-*) only work on the /v1 Mantle path.
33
+ base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
34
+ {{/if}}
35
+ client_args = {"api_key": token, "base_url": base_url}
36
+
37
+ params = {}
38
+ {{#if modelMaxTokens}}
39
+ {{#if (eq mantleApiFormat "chat_completions")}}
40
+ params["max_completion_tokens"] = {{modelMaxTokens}}
41
+ {{else}}
42
+ params["max_output_tokens"] = {{modelMaxTokens}}
43
+ {{/if}}
44
+ {{/if}}
45
+ {{#if modelTemperature}}
46
+ params["temperature"] = {{modelTemperature}}
47
+ {{/if}}
48
+ {{#if modelTopP}}
49
+ params["top_p"] = {{modelTopP}}
50
+ {{/if}}
51
+ {{#if (eq mantleApiFormat "chat_completions")}}
52
+ return OpenAIModel(client_args=client_args, model_id=MODEL_ID, params=params)
53
+ {{else}}
54
+ # Responses API: Mantle does not persist responses, so disable server-side storage.
55
+ params["store"] = False
56
+ {{#if mantleProprietary}}
57
+ return OpenAIResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
58
+ {{else}}
59
+ return MantleCompatResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
60
+ {{/if}}
61
+ {{/if}}
62
+ {{else}}
2
63
  from strands.models.bedrock import BedrockModel
3
64
 
4
65
 
@@ -6,6 +67,7 @@ def load_model() -> BedrockModel:
6
67
  """Get Bedrock model client using IAM credentials."""
7
68
  return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}")
8
69
  {{/if}}
70
+ {{/if}}
9
71
  {{#if (eq modelProvider "Anthropic")}}
10
72
  import os
11
73
 
@@ -121,3 +183,57 @@ def load_model() -> GeminiModel:
121
183
  model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
122
184
  )
123
185
  {{/if}}
186
+ {{#if (eq modelProvider "LiteLLM")}}
187
+ import os
188
+ {{#if litellmAdditionalParams}}
189
+ import json
190
+ {{/if}}
191
+
192
+ from strands.models.litellm import LiteLLMModel
193
+ {{#if identityProviders.[0].name}}
194
+ from bedrock_agentcore.identity.auth import requires_api_key
195
+
196
+ IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}"
197
+ IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}"
198
+
199
+
200
+ @requires_api_key(provider_name=IDENTITY_PROVIDER_NAME)
201
+ def _agentcore_identity_api_key_provider(api_key: str) -> str:
202
+ """Fetch API key from AgentCore Identity."""
203
+ return api_key
204
+
205
+
206
+ def _get_api_key() -> str:
207
+ """
208
+ Uses AgentCore Identity for API key management in deployed environments.
209
+ For local development, run via 'agentcore dev' which loads agentcore/.env.
210
+ """
211
+ if os.getenv("LOCAL_DEV") == "1":
212
+ api_key = os.getenv(IDENTITY_ENV_VAR)
213
+ if not api_key:
214
+ raise RuntimeError(
215
+ f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local"
216
+ )
217
+ return api_key
218
+ return _agentcore_identity_api_key_provider()
219
+ {{/if}}
220
+
221
+
222
+
223
+
224
+ def load_model() -> LiteLLMModel:
225
+ """Get a LiteLLM model client (proxies to the provider encoded in model_id)."""
226
+ client_args = {}
227
+ {{#if identityProviders.[0].name}}
228
+ client_args["api_key"] = _get_api_key()
229
+ {{/if}}
230
+ {{#if litellmApiBase}}
231
+ client_args["api_base"] = {{safeJson litellmApiBase}}
232
+ {{/if}}
233
+ params = {{#if litellmAdditionalParams}}json.loads({{pyJsonStr litellmAdditionalParams}}){{else}}{}{{/if}}
234
+ return LiteLLMModel(
235
+ client_args=client_args,
236
+ model_id="{{#if modelId}}{{modelId}}{{else}}bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0{{/if}}",
237
+ params=params,
238
+ )
239
+ {{/if}}
@@ -0,0 +1,21 @@
1
+ from strands.models.openai_responses import OpenAIResponsesModel
2
+
3
+
4
+ class MantleCompatResponsesModel(OpenAIResponsesModel):
5
+ """Workaround for Bedrock Mantle rejecting output_text in EasyInputMessage content arrays.
6
+
7
+ Mantle's Pydantic validation only accepts content as a plain string for assistant messages, while
8
+ real OpenAI accepts both formats. Flatten assistant content arrays to strings so multi-turn works.
9
+ Used for open-source OpenAI models (gpt-oss-*) on the /v1 Mantle path; proprietary models use the
10
+ plain OpenAIResponsesModel on /openai/v1.
11
+ """
12
+
13
+ @classmethod
14
+ def _format_request_messages(cls, messages):
15
+ formatted = super()._format_request_messages(messages)
16
+ for msg in formatted:
17
+ if msg.get("role") == "assistant" and isinstance(msg.get("content"), list):
18
+ msg["content"] = "".join(
19
+ part.get("text", "") for part in msg["content"] if part.get("type") == "output_text"
20
+ )
21
+ return formatted
@@ -16,6 +16,9 @@ dependencies = [
16
16
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
17
17
  {{/if}}"mcp >= 1.19.0",
18
18
  {{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
19
+ {{/if}}{{#if (eq modelProvider "LiteLLM")}}"litellm >= 1.0.0",
20
+ {{/if}}{{#if bedrockMantle}}"openai >= 1.0.0",
21
+ "aws-bedrock-token-generator >= 1.0.0",
19
22
  {{/if}}"strands-agents >= 1.15.0",
20
23
  {{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
21
24
  {{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
@@ -28,7 +28,7 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
28
28
  f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
29
29
  {{/if}}
30
30
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
31
- f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
31
+ f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
32
32
  {{/if}}
33
33
  }
34
34
  {{/if}}
@@ -3,6 +3,9 @@ import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk';
3
3
  import { z } from 'zod';
4
4
  import { loadModel } from './model/load.js';
5
5
  import { getStreamableHttpMcpClient } from './mcp_client/client.js';
6
+ {{#if hasMemory}}
7
+ import { getActorId, getOrCreateMemoryManager } from './memory/memory.js';
8
+ {{/if}}
6
9
 
7
10
  // Define a collection of MCP clients (filter out anything that failed to initialize)
8
11
  const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter(
@@ -31,34 +34,109 @@ const SYSTEM_PROMPT = `
31
34
  You are a helpful assistant. Use tools when appropriate.
32
35
  `;
33
36
 
34
- let cachedAgent: Agent | null = null;
37
+ {{#if hasMemory}}
38
+ const agentCache = new Map<string, Agent>();
35
39
 
36
- async function getOrCreateAgent(): Promise<Agent> {
37
- if (!cachedAgent) {
38
- const model = await loadModel();
39
- cachedAgent = new Agent({
40
- model,
41
- systemPrompt: SYSTEM_PROMPT,
42
- tools,
43
- });
40
+ async function getOrCreateAgent(sessionId: string, actorId: string): Promise<Agent> {
41
+ const key = `${actorId}:${sessionId}`;
42
+ let agent = agentCache.get(key);
43
+ if (agent) return agent;
44
+
45
+ const model = await loadModel();
46
+ agent = new Agent({
47
+ model,
48
+ systemPrompt: SYSTEM_PROMPT,
49
+ tools,
50
+ memoryManager: getOrCreateMemoryManager(sessionId, actorId) ?? undefined,
51
+ });
52
+ agentCache.set(key, agent);
53
+ return agent;
54
+ }
55
+ {{else}}
56
+ const AGENT_CACHE_LIMIT = 128;
57
+
58
+ // Reuses one Agent per sessionId so each session keeps its own in-process
59
+ // conversation history (best-effort; resets on cold start). A Map preserves
60
+ // insertion order, so it doubles as an LRU bounded to 128 sessions — a local
61
+ // dev process serving many sessions cannot leak history between them or grow
62
+ // without bound. On AgentCore Runtime each microVM serves a single session, so
63
+ // this holds one entry. For durable history, attach memory.
64
+ const agentCache = new Map<string, Agent>();
65
+
66
+ async function getOrCreateAgent(sessionId: string): Promise<Agent> {
67
+ const existing = agentCache.get(sessionId);
68
+ if (existing) {
69
+ agentCache.delete(sessionId);
70
+ agentCache.set(sessionId, existing);
71
+ return existing;
44
72
  }
45
- return cachedAgent;
73
+ if (agentCache.size >= AGENT_CACHE_LIMIT) {
74
+ const oldest = agentCache.keys().next().value;
75
+ if (oldest !== undefined) agentCache.delete(oldest);
76
+ }
77
+ const model = await loadModel();
78
+ const agent = new Agent({
79
+ model,
80
+ systemPrompt: SYSTEM_PROMPT,
81
+ tools,
82
+ });
83
+ agentCache.set(sessionId, agent);
84
+ return agent;
46
85
  }
86
+ {{/if}}
47
87
 
48
88
  const app = new BedrockAgentCoreApp({
49
89
  invocationHandler: {
50
90
  async *process(payload: any, context: any) {
51
- const agent = await getOrCreateAgent();
91
+ {{#if hasMemory}}
92
+ const sessionId = context?.sessionId ?? 'default-session';
93
+ const actorId = getActorId(payload, context);
94
+ const agent = await getOrCreateAgent(sessionId, actorId);
95
+ {{else}}
96
+ const sessionId = context?.sessionId ?? 'default-session';
97
+ const agent = await getOrCreateAgent(sessionId);
98
+ {{/if}}
52
99
 
53
- for await (const event of agent.stream(payload.prompt ?? '')) {
54
- if (
55
- event.type === 'modelStreamUpdateEvent' &&
56
- event.event?.type === 'modelContentBlockDeltaEvent' &&
57
- event.event.delta?.type === 'textDelta'
58
- ) {
59
- yield { data: event.event.delta.text };
100
+ {{#if hasMemory}}
101
+ try {
102
+ for await (const event of agent.stream(payload.prompt ?? '')) {
103
+ if (
104
+ event.type === 'modelStreamUpdateEvent' &&
105
+ event.event?.type === 'modelContentBlockDeltaEvent' &&
106
+ event.event.delta?.type === 'textDelta'
107
+ ) {
108
+ yield { data: event.event.delta.text };
109
+ }
110
+ }
111
+ } finally {
112
+ // Drain in-flight createEvent calls before the runtime can reclaim
113
+ // the session microVM. flush() is the durability mechanism — without
114
+ // it, an idle reclamation can lose the tail of the conversation.
115
+ await agent.memoryManager?.flush();
116
+ }
117
+ {{else}}
118
+ // Snapshot history before streaming so a failed turn can be rolled back.
119
+ // Agent.stream() appends the user message before invoking the model; on a
120
+ // mid-stream error that user turn would otherwise linger in the cached
121
+ // agent, and the next turn for this session would send consecutive user
122
+ // messages (rejected by providers that require strict role alternation,
123
+ // e.g. Anthropic). Restoring on error keeps the session reusable.
124
+ const snapshot = agent.takeSnapshot({ include: ['messages'] });
125
+ try {
126
+ for await (const event of agent.stream(payload.prompt ?? '')) {
127
+ if (
128
+ event.type === 'modelStreamUpdateEvent' &&
129
+ event.event?.type === 'modelContentBlockDeltaEvent' &&
130
+ event.event.delta?.type === 'textDelta'
131
+ ) {
132
+ yield { data: event.event.delta.text };
133
+ }
60
134
  }
135
+ } catch (error) {
136
+ agent.loadSnapshot(snapshot);
137
+ throw error;
61
138
  }
139
+ {{/if}}
62
140
  },
63
141
  },
64
142
  });
@@ -20,8 +20,9 @@
20
20
  "@google/genai": "^1.40.0",
21
21
  {{/if}}
22
22
  "@modelcontextprotocol/sdk": "^1.25.2",
23
- "@strands-agents/sdk": "1.0.0-rc.4",
24
- "bedrock-agentcore": "^0.2.4",
23
+ "@opentelemetry/api": "^1.9.0",
24
+ "@strands-agents/sdk": "^1.5.0",
25
+ "bedrock-agentcore": "^0.3.0",
25
26
  "tsx": "^4.19.0",
26
27
  "zod": "^4.4.3"
27
28
  },
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { MemoryManager } from '@strands-agents/sdk';
3
+ import { createAgentCoreMemoryStores } from 'bedrock-agentcore/experimental/memory/strands';
4
+
5
+ const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}};
6
+
7
+ const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id';
8
+
9
+ export function getActorId(payload: any, context: any): string {
10
+ const raw =
11
+ context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ||
12
+ payload?.userId ||
13
+ context?.sessionId;
14
+ return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : randomUUID();
15
+ }
16
+
17
+ const memoryManagerCache = new Map<string, MemoryManager>();
18
+
19
+ export function getOrCreateMemoryManager(sessionId: string, actorId: string): MemoryManager | null {
20
+ if (!MEMORY_ID) return null;
21
+
22
+ const key = `${actorId}:${sessionId}`;
23
+ let manager = memoryManagerCache.get(key);
24
+ if (manager) return manager;
25
+
26
+ const stores = createAgentCoreMemoryStores({
27
+ memoryId: MEMORY_ID,
28
+ actorId,
29
+ sessionId,
30
+ namespaces: [
31
+ {{#if (includes memoryProviders.[0].strategies "SEMANTIC")}}
32
+ { namespace: '/users/{actorId}/facts' },
33
+ {{/if}}
34
+ {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
35
+ { namespace: '/users/{actorId}/preferences' },
36
+ {{/if}}
37
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
38
+ { namespace: '/episodes/{actorId}/{sessionId}' },
39
+ {{/if}}
40
+ {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
41
+ { namespace: '/summaries/{actorId}/{sessionId}' },
42
+ {{/if}}
43
+ ],
44
+ // readMode defaults to 'per-namespace' (one retrieve call per namespace).
45
+ // Switch to 'subtree' to consolidate to a single hierarchical recall call.
46
+ extraction: true,
47
+ });
48
+
49
+ manager = new MemoryManager({ stores });
50
+ memoryManagerCache.set(key, manager);
51
+ return manager;
52
+ }
@@ -1,22 +1,62 @@
1
1
  import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
2
- import { streamText } from 'ai';
2
+ import { streamText, type ModelMessage } from 'ai';
3
3
  import { loadModel } from './model/load.js';
4
4
 
5
5
  const SYSTEM_PROMPT = `You are a helpful assistant.`;
6
6
 
7
+ const HISTORY_LIMIT = 128;
8
+
9
+ // Keeps one message history per sessionId so each session remembers its own
10
+ // turns (best-effort; resets on cold start). A Map preserves insertion order,
11
+ // so it doubles as an LRU bounded to 128 sessions — a local dev process serving
12
+ // many sessions cannot leak history between them or grow without bound. On
13
+ // AgentCore Runtime each microVM serves a single session, so this holds one
14
+ // entry. For durable history, persist messages to an external store.
15
+ const histories = new Map<string, ModelMessage[]>();
16
+
17
+ function getHistory(sessionId: string): ModelMessage[] {
18
+ const existing = histories.get(sessionId);
19
+ if (existing) {
20
+ histories.delete(sessionId);
21
+ histories.set(sessionId, existing);
22
+ return existing;
23
+ }
24
+ if (histories.size >= HISTORY_LIMIT) {
25
+ const oldest = histories.keys().next().value;
26
+ if (oldest !== undefined) histories.delete(oldest);
27
+ }
28
+ const fresh: ModelMessage[] = [];
29
+ histories.set(sessionId, fresh);
30
+ return fresh;
31
+ }
32
+
7
33
  const app = new BedrockAgentCoreApp({
8
34
  invocationHandler: {
9
35
  async *process(payload: any, context: any) {
36
+ const sessionId = context?.sessionId ?? 'default-session';
37
+ const history = getHistory(sessionId);
38
+ const userMessage: ModelMessage = { role: 'user', content: payload.prompt ?? '' };
39
+
10
40
  const model = await loadModel();
11
41
  const result = streamText({
12
42
  model,
13
43
  system: SYSTEM_PROMPT,
14
- prompt: payload.prompt ?? '',
44
+ messages: [...history, userMessage],
15
45
  });
16
46
 
47
+ let assistant = '';
17
48
  for await (const chunk of result.textStream) {
49
+ assistant += chunk;
18
50
  yield { data: chunk };
19
51
  }
52
+
53
+ // Commit the exchange to history only after a non-empty reply. On a failed
54
+ // or empty stream the turn is dropped instead of leaving a dangling user
55
+ // (or empty assistant) message — consecutive same-role or empty-content
56
+ // messages would otherwise be rejected on the next turn for this session.
57
+ if (assistant.length > 0) {
58
+ history.push(userMessage, { role: 'assistant', content: assistant });
59
+ }
20
60
  },
21
61
  },
22
62
  });