@aws/nx-plugin 1.0.0-rc.70 → 1.0.0-rc.72

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 (87) hide show
  1. package/migrations.json +11 -1
  2. package/package.json +1 -1
  3. package/src/agentcore-gateway/__snapshots__/generator.spec.ts.snap +1 -1
  4. package/src/agentcore-harness/generator.d.ts +1 -1
  5. package/src/infra/app/generator.d.ts +1 -1
  6. package/src/init/generator.d.ts +1 -1
  7. package/src/license/dependency-check/evaluator.js +55 -10
  8. package/src/license/dependency-check/evaluator.js.map +1 -1
  9. package/src/migrations/latest/py-agent-a2a-httpx-client-per-call/metadata.json +3 -0
  10. package/src/migrations/latest/py-agent-a2a-httpx-client-per-call/migration.d.ts +6 -0
  11. package/src/migrations/latest/py-agent-a2a-httpx-client-per-call/migration.js +329 -0
  12. package/src/migrations/latest/py-agent-a2a-httpx-client-per-call/migration.js.map +1 -0
  13. package/src/migrations/latest/py-agent-session-management-support/metadata.json +3 -0
  14. package/src/migrations/latest/py-agent-session-management-support/migration.d.ts +2 -0
  15. package/src/migrations/latest/py-agent-session-management-support/migration.js +308 -0
  16. package/src/migrations/latest/py-agent-session-management-support/migration.js.map +1 -0
  17. package/src/preset/generator.d.ts +1 -1
  18. package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +615 -6
  19. package/src/py/agent/__snapshots__/generator.core.spec.ts.snap +186 -0
  20. package/src/py/agent/__snapshots__/generator.frameworks.spec.ts.snap +47 -3
  21. package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +668 -3
  22. package/src/py/agent/a2a-connection/__snapshots__/generator.spec.ts.snap +55 -14
  23. package/src/py/agent/a2a-connection/files/agent-connection/app/__targetAgentSnakeCase___client_strands.py.template +1 -3
  24. package/src/py/agent/files/langchain/common/__init__.py.template +0 -0
  25. package/src/py/agent/files/langchain/common/agent.py.template +3 -3
  26. package/src/py/agent/files/langchain/common/session.py.template +61 -0
  27. package/src/py/agent/files/strands/ag-ui/main.py.template +5 -1
  28. package/src/py/agent/files/strands/common/agent.py.template +9 -2
  29. package/src/py/agent/files/strands/common/session.py.template +36 -0
  30. package/src/py/agent/generator.d.ts +11 -1
  31. package/src/py/agent/generator.js +78 -28
  32. package/src/py/agent/generator.js.map +1 -1
  33. package/src/py/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +2 -0
  34. package/src/py/agent/react-connection/generator.d.ts +1 -1
  35. package/src/py/agent/schema.d.js.map +1 -1
  36. package/src/py/agent/schema.d.ts +1 -1
  37. package/src/py/agent/schema.json +3 -3
  38. package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
  39. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +12 -12
  40. package/src/py/fast-api/react/generator.d.ts +1 -1
  41. package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
  42. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +1 -1
  43. package/src/py/rdb/__snapshots__/generator.spec.ts.snap +1 -1
  44. package/src/smithy/project/generator.d.ts +1 -1
  45. package/src/smithy/react-connection/generator.d.ts +1 -1
  46. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +7 -7
  47. package/src/terraform/project/generator.d.ts +1 -1
  48. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +12 -12
  49. package/src/trpc/react/generator.d.ts +1 -1
  50. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +28 -7
  51. package/src/ts/agent/a2a-connection/generator.d.ts +1 -1
  52. package/src/ts/agent/gateway-connection/generator.d.ts +1 -1
  53. package/src/ts/agent/mcp-connection/generator.d.ts +1 -1
  54. package/src/ts/agent/react-connection/generator.d.ts +1 -1
  55. package/src/ts/astro-docs/generator.d.ts +1 -1
  56. package/src/ts/dcr-proxy/__snapshots__/generator.spec.ts.snap +1 -1
  57. package/src/ts/dcr-proxy/generator.d.ts +1 -1
  58. package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +2 -2
  59. package/src/ts/dynamodb/generator.d.ts +1 -1
  60. package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +2 -2
  61. package/src/ts/lambda-function/generator.d.ts +1 -1
  62. package/src/ts/lib/generator.d.ts +1 -1
  63. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +1 -1
  64. package/src/ts/nx-generator/generator.d.ts +1 -1
  65. package/src/ts/nx-migration/generator.d.ts +1 -1
  66. package/src/ts/nx-plugin/generator.d.ts +1 -1
  67. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +4 -4
  68. package/src/ts/rdb/generator.d.ts +1 -1
  69. package/src/ts/react-website/agui/generator.d.ts +1 -1
  70. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +2 -2
  71. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +1 -1
  72. package/src/utils/agent-connection/agent-connection.d.ts +10 -0
  73. package/src/utils/agent-connection/agent-connection.js +15 -0
  74. package/src/utils/agent-connection/agent-connection.js.map +1 -1
  75. package/src/utils/agent-connection/files/core-runtime-config/runtime-config.ts.template +5 -3
  76. package/src/utils/agent-connection/files/py-core-langchain/a2a/agentcore_a2a_client_langchain.py.template +26 -20
  77. package/src/utils/agent-connection/files/py-core-langchain/s3/s3_checkpoint_saver_langchain.py.template +360 -0
  78. package/src/utils/agent-connection/files/py-core-runtime-config/runtime_config.py.template +2 -4
  79. package/src/utils/agent-connection/files/py-core-strands/a2a/agentcore_a2a_client_strands.py.template +55 -10
  80. package/src/utils/agent-connection/files/py-core-strands/base/tool_errors_strands.py.template +24 -0
  81. package/src/utils/agent-core-constructs/agent-core-constructs.d.ts +1 -1
  82. package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
  83. package/src/utils/agent-core-constructs/files/cdk/app/agent-core/__nameKebabCase__/__nameKebabCase__.ts.template +113 -13
  84. package/src/utils/agent-core-constructs/files/terraform/app/agent-core/__nameKebabCase__/__nameKebabCase__.tf.template +76 -7
  85. package/src/utils/versions.d.ts +5 -2
  86. package/src/utils/versions.js +4 -1
  87. package/src/utils/versions.js.map +1 -1
@@ -42,21 +42,64 @@ class AgentCoreA2aClientConfig:
42
42
  `;
43
43
 
44
44
  exports[`py#agent#a2a-connection generator > should match snapshot for agent-connection core files > agentcore_a2a_client_strands.py 1`] = `
45
- "from collections.abc import Callable
45
+ "import asyncio
46
+ from collections.abc import Callable
47
+ from concurrent.futures import ThreadPoolExecutor
46
48
 
49
+ import httpx
50
+ from a2a.client import ClientConfig
47
51
  from strands.agent.a2a_agent import A2AAgent
52
+ from strands.agent.agent_result import AgentResult
48
53
 
49
54
  from .agentcore_a2a_client_config import AgentCoreA2aClientConfig
50
55
 
51
56
 
52
- def _build(config: tuple, *, name: str | None, description: str | None) -> A2AAgent:
57
+ def _run_sync(coro):
58
+ # The tool is invoked from sync agent code, which under uvicorn runs inside a
59
+ # live event loop where asyncio.run() would raise — fall back to a worker.
60
+ try:
61
+ asyncio.get_running_loop()
62
+ except RuntimeError:
63
+ return asyncio.run(coro)
64
+ with ThreadPoolExecutor(max_workers=1) as pool:
65
+ return pool.submit(asyncio.run, coro).result()
66
+
67
+
68
+ class _A2AClient:
69
+ def __init__(
70
+ self,
71
+ url: str,
72
+ client_config: ClientConfig,
73
+ *,
74
+ name: str | None,
75
+ description: str | None,
76
+ ):
77
+ shared_client = client_config.httpx_client
78
+ if shared_client is None:
79
+ raise RuntimeError("A2A client config is missing an httpx client")
80
+ self._url = url
81
+ self._auth = shared_client.auth
82
+ self._timeout = shared_client.timeout
83
+ self._name = name
84
+ self._description = description
85
+
86
+ def __call__(self, prompt: str) -> AgentResult:
87
+ return _run_sync(self._invoke(prompt))
88
+
89
+ async def _invoke(self, prompt: str) -> AgentResult:
90
+ async with httpx.AsyncClient(auth=self._auth, timeout=self._timeout) as httpx_client:
91
+ agent = A2AAgent(
92
+ endpoint=self._url,
93
+ name=self._name,
94
+ description=self._description,
95
+ client_config=ClientConfig(httpx_client=httpx_client, streaming=False),
96
+ )
97
+ return await agent.invoke_async(prompt)
98
+
99
+
100
+ def _build(config: tuple, *, name: str | None, description: str | None) -> _A2AClient:
53
101
  url, client_config = config
54
- kwargs: dict = {"endpoint": url, "client_config": client_config}
55
- if name:
56
- kwargs["name"] = name
57
- if description:
58
- kwargs["description"] = description
59
- return A2AAgent(**kwargs)
102
+ return _A2AClient(url, client_config, name=name, description=description)
60
103
 
61
104
 
62
105
  class AgentCoreA2aClientStrands:
@@ -68,7 +111,7 @@ class AgentCoreA2aClientStrands:
68
111
  *,
69
112
  name: str | None = None,
70
113
  description: str | None = None,
71
- ) -> A2AAgent:
114
+ ) -> _A2AClient:
72
115
  """SigV4-authenticated client for a Bedrock AgentCore runtime."""
73
116
  return _build(
74
117
  AgentCoreA2aClientConfig.with_iam_auth(agent_runtime_arn),
@@ -83,7 +126,7 @@ class AgentCoreA2aClientStrands:
83
126
  *,
84
127
  name: str | None = None,
85
128
  description: str | None = None,
86
- ) -> A2AAgent:
129
+ ) -> _A2AClient:
87
130
  """Bearer-authenticated client for a Bedrock AgentCore runtime."""
88
131
  return _build(
89
132
  AgentCoreA2aClientConfig.with_jwt_auth(agent_runtime_arn, access_token_provider),
@@ -97,7 +140,7 @@ class AgentCoreA2aClientStrands:
97
140
  *,
98
141
  name: str | None = None,
99
142
  description: str | None = None,
100
- ) -> A2AAgent:
143
+ ) -> _A2AClient:
101
144
  """Plain-HTTP client — for local dev."""
102
145
  return _build(
103
146
  AgentCoreA2aClientConfig.without_auth(url),
@@ -110,8 +153,6 @@ class AgentCoreA2aClientStrands:
110
153
  exports[`py#agent#a2a-connection generator > should match snapshot for agent-connection core files > remote_client_strands.py 1`] = `
111
154
  "import os
112
155
 
113
- from strands.agent.a2a_agent import A2AAgent
114
-
115
156
  from test_agent_connection.core.agentcore_a2a_client_strands import (
116
157
  AgentCoreA2aClientStrands,
117
158
  )
@@ -124,7 +165,7 @@ class RemoteClientStrands:
124
165
  """Strands client for the Remote A2A agent."""
125
166
 
126
167
  @staticmethod
127
- def create() -> A2AAgent:
168
+ def create():
128
169
  if os.environ.get("LOCAL_DEV") == "true":
129
170
  return AgentCoreA2aClientStrands.without_auth("http://localhost:9001/")
130
171
  config = get_agentcore_runtime_config()
@@ -1,7 +1,5 @@
1
1
  import os
2
2
 
3
- from strands.agent.a2a_agent import A2AAgent
4
-
5
3
  from <%- agentConnectionModuleName %>.core.agentcore_a2a_client_strands import (
6
4
  AgentCoreA2aClientStrands,
7
5
  )
@@ -14,7 +12,7 @@ class <%- targetAgentClassName %>ClientStrands:
14
12
  """Strands client for the <%- targetAgentClassName %> A2A agent."""
15
13
 
16
14
  @staticmethod
17
- def create() -> A2AAgent:
15
+ def create():
18
16
  if os.environ.get("LOCAL_DEV") == "true":
19
17
  return AgentCoreA2aClientStrands.without_auth(
20
18
  "http://localhost:<%- targetAgentPort %>/"
@@ -3,7 +3,8 @@ import os
3
3
  from langchain.agents import create_agent
4
4
  from langchain_aws import ChatBedrockConverse
5
5
  from langchain_core.tools import tool
6
- from langgraph.checkpoint.memory import InMemorySaver
6
+
7
+ from .session import get_checkpointer
7
8
 
8
9
  REGION = os.environ.get("AWS_REGION", "us-east-1")
9
10
  MODEL_ID = os.environ.get("MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
@@ -17,7 +18,6 @@ def subtract(a: int, b: int) -> int:
17
18
 
18
19
  def get_agent():
19
20
  model = ChatBedrockConverse(model=MODEL_ID, region_name=REGION)
20
- # Swap InMemorySaver for a durable checkpointer in a multi-replica deployment.
21
21
  return create_agent(
22
22
  model=model,
23
23
  tools=[subtract],
@@ -26,5 +26,5 @@ You are a mathematical wizard.
26
26
  Use your tools for mathematical tasks.
27
27
  Refer to tools as your 'spellbook'.
28
28
  """,
29
- checkpointer=InMemorySaver(),
29
+ checkpointer=get_checkpointer(),
30
30
  )
@@ -0,0 +1,61 @@
1
+ import os
2
+
3
+ <%_ if (session === 's3') { _%>
4
+ import boto3
5
+ <%_ } _%>
6
+ import aiosqlite
7
+ from langgraph.checkpoint.base import BaseCheckpointSaver
8
+ <%_ if (session === 'in-memory') { _%>
9
+ from langgraph.checkpoint.memory import InMemorySaver
10
+ <%_ } _%>
11
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
12
+ <%_ if (session === 's3') { _%>
13
+ from <%- agentConnectionModuleName %>.core.s3_checkpoint_saver_langchain import S3CheckpointSaver
14
+ <%_ } else if (session === 'dynamodb-s3') { _%>
15
+ from langgraph_checkpoint_aws import DynamoDBSaver
16
+ <%_ } _%>
17
+ <%_ if (session === 's3' || session === 'dynamodb-s3') { _%>
18
+ from <%- agentConnectionModuleName %>.core.runtime_config import get_agentcore_runtime_config
19
+ <%_ } _%>
20
+
21
+
22
+ def get_checkpointer() -> BaseCheckpointSaver:
23
+ """Returns a LangGraph checkpointer for persisting conversation state across
24
+ invocations. Local development always uses a local SQLite database for
25
+ convenience, regardless of the configured session option.
26
+ """
27
+ if os.environ.get("LOCAL_DEV") == "true":
28
+ os.makedirs("<%- localSessionsDir %>", exist_ok=True)
29
+ conn = aiosqlite.connect(os.path.join("<%- localSessionsDir %>", "checkpoints.sqlite"))
30
+ return AsyncSqliteSaver(conn)
31
+
32
+ <%_ if (session === 's3') { _%>
33
+ config = get_agentcore_runtime_config()
34
+ session_config = (config.get("agentRuntimes") or {}).get("<%- agentNameClassName %>", {}).get("session", {})
35
+ bucket_name = session_config.get("bucketName")
36
+ if not bucket_name:
37
+ raise RuntimeError(
38
+ "No S3 checkpoint bucket configured for this agent in runtime configuration."
39
+ )
40
+ return S3CheckpointSaver(
41
+ bucket_name=bucket_name,
42
+ prefix="checkpoints/",
43
+ s3_client=boto3.client("s3"),
44
+ )
45
+ <%_ } else if (session === 'dynamodb-s3') { _%>
46
+ config = get_agentcore_runtime_config()
47
+ session_config = (config.get("agentRuntimes") or {}).get("<%- agentNameClassName %>", {}).get("session", {})
48
+ table_name = session_config.get("tableName")
49
+ bucket_name = session_config.get("bucketName")
50
+ if not table_name or not bucket_name:
51
+ raise RuntimeError(
52
+ "No DynamoDB checkpoint table or S3 offload bucket configured for this agent in runtime configuration."
53
+ )
54
+ return DynamoDBSaver(
55
+ table_name=table_name,
56
+ region_name=os.environ.get("AWS_REGION"),
57
+ s3_offload_config={"bucket_name": bucket_name},
58
+ )
59
+ <%_ } else { _%>
60
+ return InMemorySaver()
61
+ <%_ } _%>
@@ -4,7 +4,7 @@ from contextlib import asynccontextmanager
4
4
 
5
5
  from ag_ui.core import EventType, RunAgentInput, RunErrorEvent
6
6
  from ag_ui.encoder import EventEncoder
7
- from ag_ui_strands import StrandsAgent
7
+ from ag_ui_strands import StrandsAgent, StrandsAgentConfig
8
8
  from fastapi import FastAPI, Request
9
9
  from fastapi.middleware.cors import CORSMiddleware
10
10
  from fastapi.responses import StreamingResponse
@@ -12,6 +12,7 @@ from <%- agentConnectionModuleName %> import get_current_session_id, session_id_
12
12
  from starlette.middleware.base import BaseHTTPMiddleware
13
13
 
14
14
  from .agent import get_agent
15
+ from .session import get_session_manager
15
16
 
16
17
  logging.basicConfig(level=logging.INFO)
17
18
 
@@ -25,6 +26,9 @@ async def lifespan(app: FastAPI):
25
26
  agent=agent,
26
27
  name="<%= agentNameClassName %>",
27
28
  description="A Strands Agent exposed via the AG-UI protocol.",
29
+ # A per-thread session manager, not the template Agent's own, since
30
+ # AG-UI caches one Strands agent per thread_id.
31
+ config=StrandsAgentConfig(session_manager_provider=lambda _input_data: get_session_manager()),
28
32
  )
29
33
  yield
30
34
 
@@ -2,7 +2,11 @@ from contextlib import contextmanager
2
2
 
3
3
  from strands import Agent, tool
4
4
  from strands_tools import current_time
5
- from <%- agentConnectionModuleName %> import log_model_errors
5
+ from <%- agentConnectionModuleName %> import log_model_errors, log_tool_errors
6
+ <%_ if (protocol !== 'ag-ui') { _%>
7
+
8
+ from .session import get_session_manager
9
+ <%_ } _%>
6
10
 
7
11
 
8
12
  @tool
@@ -21,5 +25,8 @@ Use your tools for mathematical tasks.
21
25
  Refer to tools as your 'spellbook'.
22
26
  """,
23
27
  tools=[subtract, current_time],
24
- hooks=[log_model_errors],
28
+ hooks=[log_model_errors, log_tool_errors],
29
+ <%_ if (protocol !== 'ag-ui') { _%>
30
+ session_manager=get_session_manager(),
31
+ <%_ } _%>
25
32
  )
@@ -0,0 +1,36 @@
1
+ import os
2
+
3
+ from strands.session import FileSessionManager, SessionManager<% if (session === 's3') { %>, S3SessionManager<% } %>
4
+
5
+ from <%- agentConnectionModuleName %> import get_current_session_id
6
+ <%_ if (session === 's3') { _%>
7
+ from <%- agentConnectionModuleName %>.core.runtime_config import get_agentcore_runtime_config
8
+ <%_ } _%>
9
+
10
+
11
+ def get_session_manager() -> SessionManager | None:
12
+ """Returns a SessionManager for persisting conversation state across
13
+ invocations. Local development always uses local file storage for
14
+ convenience, regardless of the configured session option. Without a
15
+ configured session option, conversation state is kept in memory only and
16
+ does not survive process restarts.
17
+ """
18
+ session_id = get_current_session_id()
19
+ if not session_id:
20
+ raise RuntimeError(
21
+ "No current session id — cannot resolve a SessionManager outside of a request scope."
22
+ )
23
+ if os.environ.get("LOCAL_DEV") == "true":
24
+ return FileSessionManager(session_id=session_id, storage_dir="<%- localSessionsDir %>")
25
+
26
+ <%_ if (session === 's3') { _%>
27
+ config = get_agentcore_runtime_config()
28
+ bucket_name = (
29
+ (config.get("agentRuntimes") or {}).get("<%- agentNameClassName %>", {}).get("session", {}).get("bucketName")
30
+ )
31
+ if not bucket_name:
32
+ raise RuntimeError("No S3 bucket configured for this agent's session in runtime configuration.")
33
+ return S3SessionManager(session_id=session_id, bucket=bucket_name)
34
+ <%_ } else { _%>
35
+ return None
36
+ <%_ } _%>
@@ -5,7 +5,7 @@
5
5
  import { type GeneratorCallback, type Tree } from '@nx/devkit';
6
6
  import { type NxGeneratorInfo } from '../../utils/nx';
7
7
  import type { IacMetadata } from '../../utils/shared-constructs-constants';
8
- import type { AgentProtocol, PyAgentFramework, PyAgentGeneratorSchema } from './schema';
8
+ import type { AgentProtocol, PyAgentFramework, PyAgentGeneratorSchema, PyAgentSession } from './schema';
9
9
  /** The metadata this generator records, which its predicates read. */
10
10
  export interface PyAgentMetadata extends IacMetadata {
11
11
  readonly port: number;
@@ -17,6 +17,7 @@ export interface PyAgentMetadata extends IacMetadata {
17
17
  * Strands vs LangChain Layer-2 client + agent.py transform.
18
18
  */
19
19
  readonly framework: PyAgentFramework;
20
+ readonly session: PyAgentSession;
20
21
  }
21
22
  export declare const DEPENDENCIES: import("../../utils/declared-dependencies").DependencyDeclaration<readonly [{
22
23
  readonly name: "agent-chat-cli";
@@ -120,6 +121,15 @@ export declare const DEPENDENCIES: import("../../utils/declared-dependencies").D
120
121
  }, {
121
122
  readonly name: "langgraph";
122
123
  readonly when: (m: PyAgentMetadata) => boolean;
124
+ }, {
125
+ readonly name: "langgraph-checkpoint-sqlite";
126
+ readonly when: (m: PyAgentMetadata) => boolean;
127
+ }, {
128
+ readonly name: "aiosqlite";
129
+ readonly when: (m: PyAgentMetadata) => boolean;
130
+ }, {
131
+ readonly name: "langgraph-checkpoint-aws";
132
+ readonly when: (m: PyAgentMetadata) => boolean;
123
133
  }, {
124
134
  readonly name: "ag-ui-protocol";
125
135
  readonly when: (m: PyAgentMetadata) => boolean;
@@ -6,7 +6,7 @@ import { ensureLicenseExceptions } from "../../license/config.js";
6
6
  import { AG_UI_LANGGRAPH_EXCEPTIONS } from "../../license/known-exceptions.js";
7
7
  import { addPyDependencies, addTsDependencies } from "../../utils/add-dependencies.js";
8
8
  import { addAgentChatScripts } from "../../utils/agent-chat/agent-chat.js";
9
- import { AGENT_CONNECTION_PY_DEPENDENCIES, addPythonFrameworkBase, ensurePythonAgentConnectionProject, getPythonAgentConnectionModuleName, getPythonAgentConnectionProject } from "../../utils/agent-connection/agent-connection.js";
9
+ import { AGENT_CONNECTION_PY_DEPENDENCIES, addPythonFrameworkBase, ensureLangchainS3CheckpointSaver, ensurePythonAgentConnectionProject, getPythonAgentConnectionModuleName, getPythonAgentConnectionProject } from "../../utils/agent-connection/agent-connection.js";
10
10
  import { addAgentInfra } from "../../utils/agent-core-constructs/agent-core-constructs.js";
11
11
  import { addPythonBundleTarget } from "../../utils/bundle/bundle.js";
12
12
  import { resolveContainers } from "../../utils/containers.js";
@@ -22,7 +22,7 @@ import { kebabCase, toClassName, toSnakeCase } from "../../utils/names.js";
22
22
  import { getNpmScope } from "../../utils/npm-scope.js";
23
23
  import { addComponentDevTarget, addComponentGeneratorMetadata, addDependencyToTargetIfNotPresent, getGeneratorInfo, normalizeTargetKeyOrder, readProjectConfigurationUnqualified } from "../../utils/nx.js";
24
24
  import { sortObjectKeys } from "../../utils/object.js";
25
- import { toProjectRelativePath } from "../../utils/paths.js";
25
+ import { getRelativePathToRootByDirectory, toProjectRelativePath } from "../../utils/paths.js";
26
26
  import { assignPort } from "../../utils/port.js";
27
27
  import { addWorkspaceDependencyToPyProject } from "../../utils/py.js";
28
28
  import { SHARED_CONSTRUCTS_DEPENDENCIES, sharedConstructsGenerator } from "../../utils/shared-constructs.js";
@@ -117,6 +117,22 @@ export const DEPENDENCIES = declareDependencies()({
117
117
  name: 'langgraph',
118
118
  when: (m)=>m.framework === 'langchain'
119
119
  },
120
+ // Local dev uses a SQLite-backed checkpointer for convenience (parity
121
+ // with the strands framework's local FileSessionManager).
122
+ {
123
+ name: 'langgraph-checkpoint-sqlite',
124
+ when: (m)=>m.framework === 'langchain'
125
+ },
126
+ // AsyncSqliteSaver's driver, required alongside langgraph-checkpoint-sqlite.
127
+ {
128
+ name: 'aiosqlite',
129
+ when: (m)=>m.framework === 'langchain'
130
+ },
131
+ // Provides DynamoDBSaver (with S3 offloading for large checkpoints).
132
+ {
133
+ name: 'langgraph-checkpoint-aws',
134
+ when: (m)=>m.framework === 'langchain' && m.session === 'dynamodb-s3'
135
+ },
120
136
  {
121
137
  name: 'ag-ui-protocol',
122
138
  when: (m)=>m.framework === 'langchain' && m.protocol === 'ag-ui'
@@ -131,25 +147,25 @@ export const DEPENDENCIES = declareDependencies()({
131
147
  },
132
148
  {
133
149
  name: 'strands-agents[a2a]',
134
- when: (m)=>m.framework !== 'langchain' && m.protocol === 'a2a'
150
+ when: (m)=>m.framework === 'strands' && m.protocol === 'a2a'
135
151
  },
136
152
  {
137
153
  name: 'strands-agents',
138
- when: (m)=>m.framework !== 'langchain' && m.protocol !== 'a2a'
154
+ when: (m)=>m.framework === 'strands' && m.protocol !== 'a2a'
139
155
  },
140
156
  {
141
157
  name: 'strands-agents-tools',
142
- when: (m)=>m.framework !== 'langchain'
158
+ when: (m)=>m.framework === 'strands'
143
159
  },
144
160
  // Declared again here so a Strands AG-UI agent lists it in the same order
145
161
  // the generated pyproject.toml had before.
146
162
  {
147
163
  name: 'ag-ui-protocol',
148
- when: (m)=>m.framework !== 'langchain' && m.protocol === 'ag-ui'
164
+ when: (m)=>m.framework === 'strands' && m.protocol === 'ag-ui'
149
165
  },
150
166
  {
151
167
  name: 'ag-ui-strands',
152
- when: (m)=>m.framework !== 'langchain' && m.protocol === 'ag-ui'
168
+ when: (m)=>m.framework === 'strands' && m.protocol === 'ag-ui'
153
169
  },
154
170
  {
155
171
  name: 'uvicorn'
@@ -193,9 +209,41 @@ export const pyAgentGenerator = async (tree, options)=>{
193
209
  console.warn('Warning: auth is ignored when no infrastructure is configured (no infrastructure is generated)');
194
210
  }
195
211
  const auth = options.auth ?? 'iam';
196
- // Only 'in-memory' is currently supported for Python agents (session manager
197
- // support for Python is not yet implemented).
198
- const session = options.session ?? 'in-memory';
212
+ // Session backends are framework-specific: Strands' SessionManager only has
213
+ // an S3-backed implementation here (via strands.session.S3SessionManager),
214
+ // while LangChain uses dedicated S3 and DynamoDB checkpointer libraries.
215
+ // 'in-memory' is valid for both.
216
+ const SESSIONS_BY_FRAMEWORK = {
217
+ strands: [
218
+ 's3',
219
+ 'in-memory'
220
+ ],
221
+ langchain: [
222
+ 's3',
223
+ 'dynamodb-s3',
224
+ 'in-memory'
225
+ ]
226
+ };
227
+ if (options.session && !SESSIONS_BY_FRAMEWORK[framework].includes(options.session)) {
228
+ throw new Error(`Unsupported combination: session '${options.session}' is not implemented for the ${framework} framework (supported: ${SESSIONS_BY_FRAMEWORK[framework].join(', ')}).`);
229
+ }
230
+ const session = options.session ?? 's3';
231
+ // With infra=none there's no CDK/Terraform construct to provision a bucket
232
+ // or table or set RUNTIME_CONFIG_APP_ID, so session.py's non-local-dev
233
+ // branch has nothing to read from AppConfig at runtime — the generated code
234
+ // still honors `session`, but only works outside local dev if the caller
235
+ // wires up matching infra/runtime config themselves.
236
+ if (infra === 'none' && session !== 'in-memory') {
237
+ console.warn(`Warning: session '${session}' requires infrastructure to configure it automatically (no infrastructure is generated for infra=none) — outside local dev this will fail unless you configure matching runtime config yourself`);
238
+ }
239
+ // Local-dev session storage lives at the workspace root
240
+ // (`tmp/agents/<framework>/<agent-name>`), not inside the project, so each
241
+ // agent gets its own storage directory — shared by both frameworks
242
+ // (Strands' FileSessionManager, LangChain's SqliteSaver). The
243
+ // `-dev`/`-serve` targets run with cwd={projectRoot}, so compute that
244
+ // directory relative to the project root here rather than resolving it at
245
+ // runtime.
246
+ const localSessionsDir = joinPathFragments(getRelativePathToRootByDirectory(project.root), `tmp/agents/${framework}/${name}`);
199
247
  // Ensure the shared agent-connection project exists so the server entry
200
248
  // point can import `session_id_context` and propagate the AgentCore
201
249
  // session ID to any downstream MCP / A2A clients a later connection
@@ -203,8 +251,8 @@ export const pyAgentGenerator = async (tree, options)=>{
203
251
  await ensurePythonAgentConnectionProject(tree, DEPENDENCIES);
204
252
  // The agent server imports the framework base helpers (session cache + model
205
253
  // error logging) regardless of whether a connection client is wired in. The
206
- // langchain framework has no base layer (its AG-UI foundation reuses only the
207
- // framework-agnostic session context), so this is a no-op for langchain.
254
+ // LangChain framework has no base layer (its AG-UI foundation reuses only the
255
+ // framework-agnostic session context), so this is a no-op for LangChain.
208
256
  await addPythonFrameworkBase(tree, DEPENDENCIES, framework);
209
257
  const agentConnectionModuleName = getPythonAgentConnectionModuleName(tree);
210
258
  addWorkspaceDependencyToPyProject(tree, project, getPythonAgentConnectionProject(tree));
@@ -214,27 +262,28 @@ export const pyAgentGenerator = async (tree, options)=>{
214
262
  agentNameClassName,
215
263
  moduleName,
216
264
  agentConnectionModuleName,
217
- framework
265
+ framework,
266
+ protocol,
267
+ session,
268
+ localSessionsDir
218
269
  };
219
- // Generate common files shared by both protocols. The agent module (agent.py)
220
- // is framework-specific (Strands yields a contextmanaged Agent; LangChain
221
- // returns a compiled create_agent graph), so it comes from a per-framework
222
- // dir. The package __init__.py is framework-agnostic (it stays in `common`).
270
+ // Files live under a per-framework dir (files/strands, files/langchain),
271
+ // mirrored one-for-one including files that don't vary by framework
272
+ // (e.g. the empty package __init__.py) so there's a single, uniform
273
+ // <framework>/<protocol> layout with no cross-framework fallbacks.
274
+ // `framework` is itself the directory name.
275
+ // Common files shared by both protocols: the agent module (Strands yields a
276
+ // contextmanaged Agent with a session.py sibling; LangChain returns a
277
+ // compiled create_agent graph) and the package __init__.py.
223
278
  generateFiles(tree, joinPathFragments(import.meta.dirname, 'files', framework, 'common'), targetSourceDir, templateContext, {
224
279
  overwriteStrategy: OverwriteStrategy.KeepExisting
225
280
  });
226
- if (framework === 'langchain') {
227
- // langchain/common only carries agent.py; the empty package __init__.py is
228
- // framework-agnostic, so emit it from the strands `common` dir.
229
- generateFiles(tree, joinPathFragments(import.meta.dirname, 'files', 'strands', 'common'), targetSourceDir, templateContext, {
230
- overwriteStrategy: OverwriteStrategy.KeepExisting
231
- });
281
+ if (framework === 'langchain' && session === 's3') {
282
+ ensureLangchainS3CheckpointSaver(tree);
232
283
  }
233
- // Generate protocol-specific files. Each protocol's server entry point (and,
234
- // for HTTP, its init.py building the FastAPI app + lifespan) is
235
- // framework-specific (Strands yields a contextmanaged Agent; LangChain drives
236
- // a compiled create_agent graph), so it comes from a per-framework,
237
- // per-protocol dir: `files/<framework>/<protocol>`.
284
+ // Protocol-specific files. Each protocol's server entry point is
285
+ // framework-specific (Strands yields a contextmanaged Agent; LangChain
286
+ // drives a compiled create_agent graph).
238
287
  const protocolLower = protocol.toLowerCase();
239
288
  generateFiles(tree, joinPathFragments(import.meta.dirname, 'files', framework, protocolLower), targetSourceDir, templateContext, {
240
289
  overwriteStrategy: OverwriteStrategy.KeepExisting
@@ -325,6 +374,7 @@ export const pyAgentGenerator = async (tree, options)=>{
325
374
  auth,
326
375
  protocol,
327
376
  framework,
377
+ session,
328
378
  ...iac ? {
329
379
  iac
330
380
  } : {}