@aws/agentcore 1.0.0-preview.17 → 1.0.0-preview.19
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/README.md +17 -17
- package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +531 -90
- package/dist/assets/__tests__/googleadk-session-eviction.test.ts +87 -0
- package/dist/assets/__tests__/summarization-namespace.test.ts +43 -0
- package/dist/assets/python/a2a/strands/capabilities/memory/session.py +1 -1
- package/dist/assets/python/agui/strands/capabilities/memory/session.py +1 -1
- package/dist/assets/python/http/autogen/base/main.py +33 -10
- package/dist/assets/python/http/googleadk/base/main.py +45 -8
- package/dist/assets/python/http/langchain_langgraph/base/main.py +46 -7
- package/dist/assets/python/http/openaiagents/base/main.py +19 -8
- package/dist/assets/python/http/strands/base/main.py +36 -29
- package/dist/assets/python/http/strands/base/mcp_client/client.py +4 -1
- package/dist/assets/python/http/strands/base/model/load.py +116 -0
- package/dist/assets/python/http/strands/base/model/mantle_compat.py +21 -0
- package/dist/assets/python/http/strands/base/pyproject.toml +3 -0
- package/dist/assets/python/http/strands/capabilities/memory/session.py +1 -1
- package/dist/assets/typescript/http/strands/base/main.ts +96 -18
- package/dist/assets/typescript/http/strands/base/package.json +3 -2
- package/dist/assets/typescript/http/strands/capabilities/memory/memory.ts +52 -0
- package/dist/assets/typescript/http/vercelai/base/main.ts +42 -2
- package/dist/cli/index.mjs +574 -608
- package/dist/lib/errors/types.d.ts +25 -0
- package/dist/lib/errors/types.d.ts.map +1 -1
- package/dist/lib/errors/types.js +40 -1
- package/dist/lib/errors/types.js.map +1 -1
- package/dist/lib/secrets/cipher.d.ts +12 -0
- package/dist/lib/secrets/cipher.d.ts.map +1 -0
- package/dist/lib/secrets/cipher.js +54 -0
- package/dist/lib/secrets/cipher.js.map +1 -0
- package/dist/lib/secrets/index.d.ts +4 -0
- package/dist/lib/secrets/index.d.ts.map +1 -0
- package/dist/lib/secrets/index.js +15 -0
- package/dist/lib/secrets/index.js.map +1 -0
- package/dist/lib/secrets/key-provider.d.ts +16 -0
- package/dist/lib/secrets/key-provider.d.ts.map +1 -0
- package/dist/lib/secrets/key-provider.js +191 -0
- package/dist/lib/secrets/key-provider.js.map +1 -0
- package/dist/lib/secrets/sensitive-keys.d.ts +21 -0
- package/dist/lib/secrets/sensitive-keys.d.ts.map +1 -0
- package/dist/lib/secrets/sensitive-keys.js +67 -0
- package/dist/lib/secrets/sensitive-keys.js.map +1 -0
- package/dist/lib/utils/env.d.ts +4 -2
- package/dist/lib/utils/env.d.ts.map +1 -1
- package/dist/lib/utils/env.js +57 -27
- package/dist/lib/utils/env.js.map +1 -1
- package/dist/schema/constants.d.ts +29 -2
- package/dist/schema/constants.d.ts.map +1 -1
- package/dist/schema/constants.js +41 -5
- package/dist/schema/constants.js.map +1 -1
- package/dist/schema/schemas/agent-env.d.ts +47 -2
- package/dist/schema/schemas/agent-env.d.ts.map +1 -1
- package/dist/schema/schemas/agent-env.js +34 -5
- package/dist/schema/schemas/agent-env.js.map +1 -1
- package/dist/schema/schemas/agentcore-project.d.ts +46 -1
- package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
- package/dist/schema/schemas/auth.d.ts +2 -3
- package/dist/schema/schemas/auth.d.ts.map +1 -1
- package/dist/schema/schemas/auth.js +8 -7
- package/dist/schema/schemas/auth.js.map +1 -1
- package/dist/schema/schemas/connections.d.ts +185 -0
- package/dist/schema/schemas/connections.d.ts.map +1 -0
- package/dist/schema/schemas/connections.js +176 -0
- package/dist/schema/schemas/connections.js.map +1 -0
- package/dist/schema/schemas/index.d.ts +1 -0
- package/dist/schema/schemas/index.d.ts.map +1 -1
- package/dist/schema/schemas/index.js +1 -0
- package/dist/schema/schemas/index.js.map +1 -1
- package/dist/schema/schemas/primitives/config-bundle.d.ts +1 -0
- package/dist/schema/schemas/primitives/config-bundle.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/config-bundle.js +3 -0
- package/dist/schema/schemas/primitives/config-bundle.js.map +1 -1
- package/dist/schema/schemas/primitives/harness.d.ts +43 -0
- package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/harness.js +20 -0
- package/dist/schema/schemas/primitives/harness.js.map +1 -1
- package/npm-shrinkwrap.json +223 -0
- 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}
|
|
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
|
-
|
|
37
|
+
{{#if hasMemory}}
|
|
38
|
+
const agentCache = new Map<string, Agent>();
|
|
35
39
|
|
|
36
|
-
async function getOrCreateAgent(): Promise<Agent> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
"@
|
|
24
|
-
"
|
|
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
|
-
|
|
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
|
});
|