@aws/nx-plugin 1.0.0-rc.69 → 1.0.0-rc.70
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/migrations.json +6 -1
- package/package.json +1 -1
- package/src/migrations/latest/py-agent-a2a-lifespan-construction/metadata.json +3 -0
- package/src/migrations/latest/py-agent-a2a-lifespan-construction/migration.d.ts +6 -0
- package/src/migrations/latest/py-agent-a2a-lifespan-construction/migration.js +234 -0
- package/src/migrations/latest/py-agent-a2a-lifespan-construction/migration.js.map +1 -0
- package/src/py/agent/files/langchain/a2a/main.py.template +8 -3
- package/src/py/agent/files/strands/a2a/main.py.template +33 -15
- package/src/utils/agent-connection/files/py-core-runtime-config/session_context.py.template +2 -2
- package/src/utils/agent-connection/files/py-core-strands/base/with_session_id_strands.py.template +2 -2
package/migrations.json
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"description": "Update the docs translate script to write whole translated files rather than editing them in place",
|
|
13
13
|
"implementation": "./src/migrations/latest/translate-script-whole-file-writes/migration"
|
|
14
14
|
},
|
|
15
|
+
"latest-py-agent-a2a-lifespan-construction": {
|
|
16
|
+
"version": "1.0.0-rc.70",
|
|
17
|
+
"description": "Move py#agent A2A agent construction (Strands and LangChain) out of module import time and into a FastAPI lifespan handler, stored on app.state; also updates with_session_id/session_id_context to use Generator instead of the deprecated Iterator return type",
|
|
18
|
+
"implementation": "./src/migrations/latest/py-agent-a2a-lifespan-construction/migration"
|
|
19
|
+
},
|
|
15
20
|
"v1.0.0-rc.50-0001-modernize-function-props-cast": {
|
|
16
21
|
"version": "1.0.0-rc.50",
|
|
17
22
|
"description": "Replace the legacy angle-bracket FunctionProps type assertion with the modern as syntax in generated API constructs",
|
|
@@ -108,7 +113,7 @@
|
|
|
108
113
|
"implementation": "./src/migrations/v1.0.0-rc.65/0002-rolldown-code-splitting/migration"
|
|
109
114
|
},
|
|
110
115
|
"sync-vended-versions": {
|
|
111
|
-
"version": "1.0.0-rc.
|
|
116
|
+
"version": "1.0.0-rc.70",
|
|
112
117
|
"description": "Sync vended dependency versions and the tracked plugin version to those vended by this release",
|
|
113
118
|
"implementation": "./src/utils/version-upgrade-migration/migration"
|
|
114
119
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Move py#agent A2A agent construction (Strands and LangChain) out of module import time and into a FastAPI lifespan handler, stored on app.state; also updates with_session_id/session_id_context to use Generator instead of the deprecated Iterator return type"
|
|
3
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type MigrationReturnObject, type Tree } from '@nx/devkit';
|
|
6
|
+
export default function migration(tree: Tree): Promise<MigrationReturnObject>;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { getProjects, joinPathFragments, visitNotIgnoredFiles } from "@nx/devkit";
|
|
5
|
+
import { PY_AGENT_GENERATOR_INFO } from "../../../py/agent/generator.js";
|
|
6
|
+
import { addPythonDestructuredImport, applyGritQL, captureGritQLVariable, matchGritQL } from "../../../utils/ast.js";
|
|
7
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
8
|
+
/**
|
|
9
|
+
* Move py#agent A2A agent construction (Strands and LangChain) out of module
|
|
10
|
+
* import time and into a FastAPI `lifespan` handler, stored on `app.state`.
|
|
11
|
+
*
|
|
12
|
+
* The `v1.0.0-rc.61/0001-py-agent-lifespan-construction` migration (HTTP/AG-UI)
|
|
13
|
+
* deliberately left A2A alone, reasoning that `with_session_id` only touches
|
|
14
|
+
* its agent factory lazily on first use, so the eager `_agent_ctx.__enter__()`
|
|
15
|
+
* was never actually eager in practice. That held for `with_session_id`'s own
|
|
16
|
+
* laziness, but missed a separate trigger: `strands.multiagent.a2a.executor
|
|
17
|
+
* .StrandsA2AExecutor.__init__`, in its deprecated single-`agent=` mode, does
|
|
18
|
+
* `getattr(agent, "_session_manager", None)` - which the session-routing
|
|
19
|
+
* proxy's `__getattr__` happily serves by building the real underlying Agent
|
|
20
|
+
* right then, under a bogus "default" session, since no request has bound a
|
|
21
|
+
* real session id yet. That call happens inside `A2AServer.__init__`, which
|
|
22
|
+
* the old template constructed at plain module import time - so the "lazy"
|
|
23
|
+
* proxy was forced eager anyway, just one layer further down than the
|
|
24
|
+
* previous migration checked.
|
|
25
|
+
*
|
|
26
|
+
* Strands: `agent_factory=` mode sidesteps this - `StrandsA2AExecutor` takes
|
|
27
|
+
* an entirely different branch when given a factory (no `_session_manager`
|
|
28
|
+
* probe at all), and the one placeholder call `A2AServer.__init__` makes
|
|
29
|
+
* before `lifespan` has run returns a bare `SimpleNamespace` with just
|
|
30
|
+
* `name`/`description` - no `__getattr__`, nothing to force construction.
|
|
31
|
+
*
|
|
32
|
+
* LangChain: the compiled graph from `get_agent()` has no such trap, but was
|
|
33
|
+
* still built eagerly at import time (`_graph = get_agent()`). Moved into
|
|
34
|
+
* `lifespan` for the same reason HTTP/AG-UI were: it can require env vars
|
|
35
|
+
* (model id, region) or do network setup that isn't guaranteed available at
|
|
36
|
+
* import - e.g. under test collection or OpenAPI spec generation tooling.
|
|
37
|
+
*
|
|
38
|
+
* Also folds in an unrelated but adjacent fix: Pylance (Pyright) deprecates
|
|
39
|
+
* annotating a `@contextmanager`-decorated function's return type as
|
|
40
|
+
* `Iterator[T]`, in favor of the single-argument `Generator[T]` form enabled
|
|
41
|
+
* by PEP 696 default type parameters (Python 3.13+ - generated projects
|
|
42
|
+
* target >=3.14). `with_session_id_strands.py` and `session_context.py` both
|
|
43
|
+
* use this pattern. Both files are shared, protocol-agnostic files copied
|
|
44
|
+
* once per workspace (not per py#agent component), so that part scans the
|
|
45
|
+
* whole tree by basename rather than going through component metadata.
|
|
46
|
+
*/ const pyMatch = (snippet)=>`language python\n\`${snippet}\``;
|
|
47
|
+
const pyDelete = (snippet)=>`${pyMatch(snippet)} => .`;
|
|
48
|
+
const pyRewrite = (from, to)=>{
|
|
49
|
+
const replacement = to.includes('\n') ? `raw\`${to}\`` : `\`${to}\``;
|
|
50
|
+
return `${pyMatch(from)} => ${replacement}`;
|
|
51
|
+
};
|
|
52
|
+
const AGENT_ENTER = '_agent = _agent_ctx.__enter__()';
|
|
53
|
+
const OLD_AGENT_CONTEXT = `_agent_ctx = with_session_id(
|
|
54
|
+
get_agent,
|
|
55
|
+
name="$name",
|
|
56
|
+
description="A Strands Agent exposed via the Agent-to-Agent (A2A) protocol.",
|
|
57
|
+
)`;
|
|
58
|
+
const NEW_CONSTANTS_AND_LIFESPAN = `AGENT_NAME = "$name"
|
|
59
|
+
AGENT_DESCRIPTION = "A Strands Agent exposed via the Agent-to-Agent (A2A) protocol."
|
|
60
|
+
|
|
61
|
+
# A2AServer.__init__ calls agent_factory once, synchronously, before the lifespan
|
|
62
|
+
# below has run - this is what it reads name/description from at that point.
|
|
63
|
+
# The cast is a lie about runtime shape, not behavior: only .name/.description
|
|
64
|
+
# are ever read off this value.
|
|
65
|
+
_card_placeholder = cast(
|
|
66
|
+
Agent, SimpleNamespace(name=AGENT_NAME, description=AGENT_DESCRIPTION)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@asynccontextmanager
|
|
71
|
+
async def lifespan(app: FastAPI):
|
|
72
|
+
with with_session_id(
|
|
73
|
+
get_agent,
|
|
74
|
+
name=AGENT_NAME,
|
|
75
|
+
description=AGENT_DESCRIPTION,
|
|
76
|
+
) as agent:
|
|
77
|
+
app.state.agent = agent
|
|
78
|
+
yield`;
|
|
79
|
+
const A2A_SERVER_STATEMENT = 'a2a_server = A2AServer($args)';
|
|
80
|
+
const OLD_MOUNT_STATEMENT = 'app.mount("/", a2a_server.to_fastapi_app())';
|
|
81
|
+
const OLD_FASTAPI_APP = 'app = FastAPI()';
|
|
82
|
+
const NEW_FASTAPI_APP = 'app = FastAPI(lifespan=lifespan)';
|
|
83
|
+
const SERVER_MATCH = 'language python\n`A2AServer($args)` where { $args <: contains `agent=_agent` }';
|
|
84
|
+
const allMatch = async (tree, filePath, patterns)=>{
|
|
85
|
+
for (const pattern of patterns){
|
|
86
|
+
if (!await matchGritQL(tree, filePath, pattern)) return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
const migrateStrandsA2AAgent = async (tree, mainPath, nextSteps)=>{
|
|
91
|
+
if (!tree.exists(mainPath)) return;
|
|
92
|
+
const contents = tree.read(mainPath, 'utf-8') ?? '';
|
|
93
|
+
if (!contents.includes('_agent_ctx = with_session_id(')) return;
|
|
94
|
+
const ready = await allMatch(tree, mainPath, [
|
|
95
|
+
pyMatch('import logging'),
|
|
96
|
+
pyMatch('from $mod import session_id_context, with_session_id'),
|
|
97
|
+
pyMatch(OLD_AGENT_CONTEXT),
|
|
98
|
+
pyMatch(AGENT_ENTER),
|
|
99
|
+
pyMatch(OLD_FASTAPI_APP),
|
|
100
|
+
pyMatch(OLD_MOUNT_STATEMENT),
|
|
101
|
+
SERVER_MATCH
|
|
102
|
+
]);
|
|
103
|
+
const diverged = ()=>{
|
|
104
|
+
nextSteps.push(`${mainPath}: diverged from the generated Strands A2A shape - left untouched. Manually rebuild the agent inside the FastAPI \`lifespan\` and have \`agent_factory\` read it off \`app.state\` (see the py#agent generator's strands/a2a template).`);
|
|
105
|
+
};
|
|
106
|
+
if (!ready) {
|
|
107
|
+
diverged();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await addPythonDestructuredImport(tree, mainPath, [
|
|
111
|
+
'asynccontextmanager'
|
|
112
|
+
], 'contextlib');
|
|
113
|
+
await addPythonDestructuredImport(tree, mainPath, [
|
|
114
|
+
'SimpleNamespace'
|
|
115
|
+
], 'types');
|
|
116
|
+
await addPythonDestructuredImport(tree, mainPath, [
|
|
117
|
+
'cast'
|
|
118
|
+
], 'typing');
|
|
119
|
+
await addPythonDestructuredImport(tree, mainPath, [
|
|
120
|
+
'Agent'
|
|
121
|
+
], 'strands');
|
|
122
|
+
await applyGritQL(tree, mainPath, pyRewrite(OLD_AGENT_CONTEXT, NEW_CONSTANTS_AND_LIFESPAN));
|
|
123
|
+
await applyGritQL(tree, mainPath, pyDelete(AGENT_ENTER));
|
|
124
|
+
// Swap the kwarg while the call is still in its original spot (any extra
|
|
125
|
+
// kwargs a user added are left alone), then capture the whole arg list so
|
|
126
|
+
// it can be reinserted at the call's new home below.
|
|
127
|
+
await applyGritQL(tree, mainPath, pyRewrite('agent=_agent', 'agent_factory=lambda _context_id: getattr(app.state, "agent", _card_placeholder)'));
|
|
128
|
+
const capturedArgs = await captureGritQLVariable(tree, mainPath, `language python\n\`${A2A_SERVER_STATEMENT}\``, 'args');
|
|
129
|
+
if (capturedArgs === undefined) {
|
|
130
|
+
diverged();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await applyGritQL(tree, mainPath, pyDelete(A2A_SERVER_STATEMENT));
|
|
134
|
+
await applyGritQL(tree, mainPath, pyRewrite(OLD_FASTAPI_APP, NEW_FASTAPI_APP));
|
|
135
|
+
await applyGritQL(tree, mainPath, pyRewrite(OLD_MOUNT_STATEMENT, `a2a_server = A2AServer(${capturedArgs})\n\n${OLD_MOUNT_STATEMENT}`));
|
|
136
|
+
};
|
|
137
|
+
// --- LangChain A2A -----------------------------------------------------------
|
|
138
|
+
const LANGCHAIN_GRAPH_OLD = '_graph = get_agent()';
|
|
139
|
+
const LANGCHAIN_LIFESPAN_NEW = `@asynccontextmanager
|
|
140
|
+
async def lifespan(app: FastAPI):
|
|
141
|
+
app.state.agent = get_agent()
|
|
142
|
+
yield`;
|
|
143
|
+
const LANGCHAIN_GRAPH_INVOKE_OLD = '_graph.ainvoke($args)';
|
|
144
|
+
const LANGCHAIN_GRAPH_INVOKE_NEW = 'app.state.agent.ainvoke($args)';
|
|
145
|
+
const LANGCHAIN_APP_OLD = 'app = FastAPI(title="$name")';
|
|
146
|
+
const LANGCHAIN_APP_NEW = 'app = FastAPI(title="$name", lifespan=lifespan)';
|
|
147
|
+
const migrateLangchainA2AAgent = async (tree, mainPath, nextSteps)=>{
|
|
148
|
+
if (!tree.exists(mainPath)) return;
|
|
149
|
+
const contents = tree.read(mainPath, 'utf-8') ?? '';
|
|
150
|
+
if (!contents.includes(LANGCHAIN_GRAPH_OLD)) return;
|
|
151
|
+
const ready = await allMatch(tree, mainPath, [
|
|
152
|
+
pyMatch(LANGCHAIN_GRAPH_OLD),
|
|
153
|
+
pyMatch('from .agent import get_agent'),
|
|
154
|
+
pyMatch(LANGCHAIN_GRAPH_INVOKE_OLD),
|
|
155
|
+
pyMatch(LANGCHAIN_APP_OLD)
|
|
156
|
+
]);
|
|
157
|
+
if (!ready) {
|
|
158
|
+
nextSteps.push(`${mainPath}: diverged from the generated LangChain A2A shape - left untouched. Manually wrap \`get_agent()\` in a \`lifespan\` handler storing the graph on \`app.state.agent\` (see the py#agent generator's langchain/a2a template).`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await addPythonDestructuredImport(tree, mainPath, [
|
|
162
|
+
'asynccontextmanager'
|
|
163
|
+
], 'contextlib');
|
|
164
|
+
await applyGritQL(tree, mainPath, pyRewrite(LANGCHAIN_GRAPH_OLD, LANGCHAIN_LIFESPAN_NEW));
|
|
165
|
+
await applyGritQL(tree, mainPath, pyRewrite(LANGCHAIN_GRAPH_INVOKE_OLD, LANGCHAIN_GRAPH_INVOKE_NEW));
|
|
166
|
+
await applyGritQL(tree, mainPath, pyRewrite(LANGCHAIN_APP_OLD, LANGCHAIN_APP_NEW));
|
|
167
|
+
};
|
|
168
|
+
// --- Iterator -> Generator (with_session_id_strands.py / session_context.py) ---
|
|
169
|
+
const WITH_SESSION_ID_IMPORT_OLD = 'from collections.abc import Callable, Iterator';
|
|
170
|
+
const WITH_SESSION_ID_IMPORT_NEW = 'from collections.abc import Callable, Generator';
|
|
171
|
+
const migrateWithSessionId = async (tree, filePath, nextSteps)=>{
|
|
172
|
+
const contents = tree.read(filePath, 'utf-8') ?? '';
|
|
173
|
+
if (!contents.includes('Iterator[Any]')) return;
|
|
174
|
+
const ready = await allMatch(tree, filePath, [
|
|
175
|
+
pyMatch(WITH_SESSION_ID_IMPORT_OLD),
|
|
176
|
+
pyMatch('Iterator[Any]')
|
|
177
|
+
]);
|
|
178
|
+
if (!ready) {
|
|
179
|
+
nextSteps.push(`${filePath}: diverged from the generated shape - left untouched. Manually change the \`Iterator\` import and \`Iterator[Any]\` return annotation on \`with_session_id\` to \`Generator\`/\`Generator[Any]\` (see the agent-connection generator's with_session_id_strands.py template).`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await applyGritQL(tree, filePath, pyRewrite(WITH_SESSION_ID_IMPORT_OLD, WITH_SESSION_ID_IMPORT_NEW));
|
|
183
|
+
await applyGritQL(tree, filePath, pyRewrite('Iterator[Any]', 'Generator[Any]'));
|
|
184
|
+
};
|
|
185
|
+
const SESSION_CONTEXT_IMPORT_OLD = 'from collections.abc import Iterator';
|
|
186
|
+
const SESSION_CONTEXT_IMPORT_NEW = 'from collections.abc import Generator';
|
|
187
|
+
const migrateSessionContext = async (tree, filePath, nextSteps)=>{
|
|
188
|
+
const contents = tree.read(filePath, 'utf-8') ?? '';
|
|
189
|
+
if (!contents.includes('Iterator[None]')) return;
|
|
190
|
+
const ready = await allMatch(tree, filePath, [
|
|
191
|
+
pyMatch(SESSION_CONTEXT_IMPORT_OLD),
|
|
192
|
+
pyMatch('Iterator[None]')
|
|
193
|
+
]);
|
|
194
|
+
if (!ready) {
|
|
195
|
+
nextSteps.push(`${filePath}: diverged from the generated shape - left untouched. Manually change the \`Iterator\` import and \`Iterator[None]\` return annotation on \`session_id_context\` to \`Generator\`/\`Generator[None]\` (see the agent-connection generator's session_context.py template).`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await applyGritQL(tree, filePath, pyRewrite(SESSION_CONTEXT_IMPORT_OLD, SESSION_CONTEXT_IMPORT_NEW));
|
|
199
|
+
await applyGritQL(tree, filePath, pyRewrite('Iterator[None]', 'Generator[None]'));
|
|
200
|
+
};
|
|
201
|
+
const findAgentComponents = (components)=>(components ?? []).filter((component)=>component.generator === PY_AGENT_GENERATOR_INFO.id);
|
|
202
|
+
export default async function migration(tree) {
|
|
203
|
+
const nextSteps = [];
|
|
204
|
+
for (const project of getProjects(tree).values()){
|
|
205
|
+
const components = findAgentComponents(project.metadata?.components);
|
|
206
|
+
for (const component of components){
|
|
207
|
+
if (!component.path || component.protocol !== 'a2a') {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const componentDir = component.path.endsWith('/agent.py') ? component.path.slice(0, -'/agent.py'.length) : component.path;
|
|
211
|
+
const mainPath = joinPathFragments(project.root, componentDir, 'main.py');
|
|
212
|
+
if ((component.framework ?? 'strands') === 'langchain') {
|
|
213
|
+
await migrateLangchainA2AAgent(tree, mainPath, nextSteps);
|
|
214
|
+
} else {
|
|
215
|
+
await migrateStrandsA2AAgent(tree, mainPath, nextSteps);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const filePaths = [];
|
|
220
|
+
visitNotIgnoredFiles(tree, '', (filePath)=>filePaths.push(filePath));
|
|
221
|
+
for (const filePath of filePaths){
|
|
222
|
+
if (filePath.endsWith('/with_session_id_strands.py')) {
|
|
223
|
+
await migrateWithSessionId(tree, filePath, nextSteps);
|
|
224
|
+
} else if (filePath.endsWith('/session_context.py')) {
|
|
225
|
+
await migrateSessionContext(tree, filePath, nextSteps);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
await formatFilesInSubtree(tree);
|
|
229
|
+
return {
|
|
230
|
+
nextSteps
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/py-agent-a2a-lifespan-construction/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type Tree,\n visitNotIgnoredFiles,\n} from '@nx/devkit';\nimport { PY_AGENT_GENERATOR_INFO } from '../../../py/agent/generator';\nimport {\n addPythonDestructuredImport,\n applyGritQL,\n captureGritQLVariable,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport type { ComponentMetadata } from '../../../utils/nx';\n\n/**\n * Move py#agent A2A agent construction (Strands and LangChain) out of module\n * import time and into a FastAPI `lifespan` handler, stored on `app.state`.\n *\n * The `v1.0.0-rc.61/0001-py-agent-lifespan-construction` migration (HTTP/AG-UI)\n * deliberately left A2A alone, reasoning that `with_session_id` only touches\n * its agent factory lazily on first use, so the eager `_agent_ctx.__enter__()`\n * was never actually eager in practice. That held for `with_session_id`'s own\n * laziness, but missed a separate trigger: `strands.multiagent.a2a.executor\n * .StrandsA2AExecutor.__init__`, in its deprecated single-`agent=` mode, does\n * `getattr(agent, \"_session_manager\", None)` - which the session-routing\n * proxy's `__getattr__` happily serves by building the real underlying Agent\n * right then, under a bogus \"default\" session, since no request has bound a\n * real session id yet. That call happens inside `A2AServer.__init__`, which\n * the old template constructed at plain module import time - so the \"lazy\"\n * proxy was forced eager anyway, just one layer further down than the\n * previous migration checked.\n *\n * Strands: `agent_factory=` mode sidesteps this - `StrandsA2AExecutor` takes\n * an entirely different branch when given a factory (no `_session_manager`\n * probe at all), and the one placeholder call `A2AServer.__init__` makes\n * before `lifespan` has run returns a bare `SimpleNamespace` with just\n * `name`/`description` - no `__getattr__`, nothing to force construction.\n *\n * LangChain: the compiled graph from `get_agent()` has no such trap, but was\n * still built eagerly at import time (`_graph = get_agent()`). Moved into\n * `lifespan` for the same reason HTTP/AG-UI were: it can require env vars\n * (model id, region) or do network setup that isn't guaranteed available at\n * import - e.g. under test collection or OpenAPI spec generation tooling.\n *\n * Also folds in an unrelated but adjacent fix: Pylance (Pyright) deprecates\n * annotating a `@contextmanager`-decorated function's return type as\n * `Iterator[T]`, in favor of the single-argument `Generator[T]` form enabled\n * by PEP 696 default type parameters (Python 3.13+ - generated projects\n * target >=3.14). `with_session_id_strands.py` and `session_context.py` both\n * use this pattern. Both files are shared, protocol-agnostic files copied\n * once per workspace (not per py#agent component), so that part scans the\n * whole tree by basename rather than going through component metadata.\n */\nconst pyMatch = (snippet: string) => `language python\\n\\`${snippet}\\``;\n\nconst pyDelete = (snippet: string) => `${pyMatch(snippet)} => .`;\n\nconst pyRewrite = (from: string, to: string): string => {\n const replacement = to.includes('\\n') ? `raw\\`${to}\\`` : `\\`${to}\\``;\n return `${pyMatch(from)} => ${replacement}`;\n};\n\nconst AGENT_ENTER = '_agent = _agent_ctx.__enter__()';\n\nconst OLD_AGENT_CONTEXT = `_agent_ctx = with_session_id(\n get_agent,\n name=\"$name\",\n description=\"A Strands Agent exposed via the Agent-to-Agent (A2A) protocol.\",\n)`;\n\nconst NEW_CONSTANTS_AND_LIFESPAN = `AGENT_NAME = \"$name\"\nAGENT_DESCRIPTION = \"A Strands Agent exposed via the Agent-to-Agent (A2A) protocol.\"\n\n# A2AServer.__init__ calls agent_factory once, synchronously, before the lifespan\n# below has run - this is what it reads name/description from at that point.\n# The cast is a lie about runtime shape, not behavior: only .name/.description\n# are ever read off this value.\n_card_placeholder = cast(\n Agent, SimpleNamespace(name=AGENT_NAME, description=AGENT_DESCRIPTION)\n)\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n with with_session_id(\n get_agent,\n name=AGENT_NAME,\n description=AGENT_DESCRIPTION,\n ) as agent:\n app.state.agent = agent\n yield`;\n\nconst A2A_SERVER_STATEMENT = 'a2a_server = A2AServer($args)';\n\nconst OLD_MOUNT_STATEMENT = 'app.mount(\"/\", a2a_server.to_fastapi_app())';\n\nconst OLD_FASTAPI_APP = 'app = FastAPI()';\n\nconst NEW_FASTAPI_APP = 'app = FastAPI(lifespan=lifespan)';\n\nconst SERVER_MATCH =\n 'language python\\n`A2AServer($args)` where { $args <: contains `agent=_agent` }';\n\nconst allMatch = async (\n tree: Tree,\n filePath: string,\n patterns: string[],\n): Promise<boolean> => {\n for (const pattern of patterns) {\n if (!(await matchGritQL(tree, filePath, pattern))) return false;\n }\n return true;\n};\n\nconst migrateStrandsA2AAgent = async (\n tree: Tree,\n mainPath: string,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(mainPath)) return;\n\n const contents = tree.read(mainPath, 'utf-8') ?? '';\n if (!contents.includes('_agent_ctx = with_session_id(')) return;\n\n const ready = await allMatch(tree, mainPath, [\n pyMatch('import logging'),\n pyMatch('from $mod import session_id_context, with_session_id'),\n pyMatch(OLD_AGENT_CONTEXT),\n pyMatch(AGENT_ENTER),\n pyMatch(OLD_FASTAPI_APP),\n pyMatch(OLD_MOUNT_STATEMENT),\n SERVER_MATCH,\n ]);\n\n const diverged = () => {\n nextSteps.push(\n `${mainPath}: diverged from the generated Strands A2A shape - left untouched. Manually rebuild the agent inside the FastAPI \\`lifespan\\` and have \\`agent_factory\\` read it off \\`app.state\\` (see the py#agent generator's strands/a2a template).`,\n );\n };\n\n if (!ready) {\n diverged();\n return;\n }\n\n await addPythonDestructuredImport(\n tree,\n mainPath,\n ['asynccontextmanager'],\n 'contextlib',\n );\n await addPythonDestructuredImport(\n tree,\n mainPath,\n ['SimpleNamespace'],\n 'types',\n );\n await addPythonDestructuredImport(tree, mainPath, ['cast'], 'typing');\n await addPythonDestructuredImport(tree, mainPath, ['Agent'], 'strands');\n await applyGritQL(\n tree,\n mainPath,\n pyRewrite(OLD_AGENT_CONTEXT, NEW_CONSTANTS_AND_LIFESPAN),\n );\n await applyGritQL(tree, mainPath, pyDelete(AGENT_ENTER));\n\n // Swap the kwarg while the call is still in its original spot (any extra\n // kwargs a user added are left alone), then capture the whole arg list so\n // it can be reinserted at the call's new home below.\n await applyGritQL(\n tree,\n mainPath,\n pyRewrite(\n 'agent=_agent',\n 'agent_factory=lambda _context_id: getattr(app.state, \"agent\", _card_placeholder)',\n ),\n );\n const capturedArgs = await captureGritQLVariable(\n tree,\n mainPath,\n `language python\\n\\`${A2A_SERVER_STATEMENT}\\``,\n 'args',\n );\n if (capturedArgs === undefined) {\n diverged();\n return;\n }\n\n await applyGritQL(tree, mainPath, pyDelete(A2A_SERVER_STATEMENT));\n await applyGritQL(\n tree,\n mainPath,\n pyRewrite(OLD_FASTAPI_APP, NEW_FASTAPI_APP),\n );\n await applyGritQL(\n tree,\n mainPath,\n pyRewrite(\n OLD_MOUNT_STATEMENT,\n `a2a_server = A2AServer(${capturedArgs})\\n\\n${OLD_MOUNT_STATEMENT}`,\n ),\n );\n};\n\n// --- LangChain A2A -----------------------------------------------------------\n\nconst LANGCHAIN_GRAPH_OLD = '_graph = get_agent()';\n\nconst LANGCHAIN_LIFESPAN_NEW = `@asynccontextmanager\nasync def lifespan(app: FastAPI):\n app.state.agent = get_agent()\n yield`;\n\nconst LANGCHAIN_GRAPH_INVOKE_OLD = '_graph.ainvoke($args)';\n\nconst LANGCHAIN_GRAPH_INVOKE_NEW = 'app.state.agent.ainvoke($args)';\n\nconst LANGCHAIN_APP_OLD = 'app = FastAPI(title=\"$name\")';\n\nconst LANGCHAIN_APP_NEW = 'app = FastAPI(title=\"$name\", lifespan=lifespan)';\n\nconst migrateLangchainA2AAgent = async (\n tree: Tree,\n mainPath: string,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(mainPath)) return;\n\n const contents = tree.read(mainPath, 'utf-8') ?? '';\n if (!contents.includes(LANGCHAIN_GRAPH_OLD)) return;\n\n const ready = await allMatch(tree, mainPath, [\n pyMatch(LANGCHAIN_GRAPH_OLD),\n pyMatch('from .agent import get_agent'),\n pyMatch(LANGCHAIN_GRAPH_INVOKE_OLD),\n pyMatch(LANGCHAIN_APP_OLD),\n ]);\n\n if (!ready) {\n nextSteps.push(\n `${mainPath}: diverged from the generated LangChain A2A shape - left untouched. Manually wrap \\`get_agent()\\` in a \\`lifespan\\` handler storing the graph on \\`app.state.agent\\` (see the py#agent generator's langchain/a2a template).`,\n );\n return;\n }\n\n await addPythonDestructuredImport(\n tree,\n mainPath,\n ['asynccontextmanager'],\n 'contextlib',\n );\n await applyGritQL(\n tree,\n mainPath,\n pyRewrite(LANGCHAIN_GRAPH_OLD, LANGCHAIN_LIFESPAN_NEW),\n );\n await applyGritQL(\n tree,\n mainPath,\n pyRewrite(LANGCHAIN_GRAPH_INVOKE_OLD, LANGCHAIN_GRAPH_INVOKE_NEW),\n );\n await applyGritQL(\n tree,\n mainPath,\n pyRewrite(LANGCHAIN_APP_OLD, LANGCHAIN_APP_NEW),\n );\n};\n\n// --- Iterator -> Generator (with_session_id_strands.py / session_context.py) ---\n\nconst WITH_SESSION_ID_IMPORT_OLD =\n 'from collections.abc import Callable, Iterator';\nconst WITH_SESSION_ID_IMPORT_NEW =\n 'from collections.abc import Callable, Generator';\n\nconst migrateWithSessionId = async (\n tree: Tree,\n filePath: string,\n nextSteps: string[],\n): Promise<void> => {\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (!contents.includes('Iterator[Any]')) return;\n\n const ready = await allMatch(tree, filePath, [\n pyMatch(WITH_SESSION_ID_IMPORT_OLD),\n pyMatch('Iterator[Any]'),\n ]);\n\n if (!ready) {\n nextSteps.push(\n `${filePath}: diverged from the generated shape - left untouched. Manually change the \\`Iterator\\` import and \\`Iterator[Any]\\` return annotation on \\`with_session_id\\` to \\`Generator\\`/\\`Generator[Any]\\` (see the agent-connection generator's with_session_id_strands.py template).`,\n );\n return;\n }\n\n await applyGritQL(\n tree,\n filePath,\n pyRewrite(WITH_SESSION_ID_IMPORT_OLD, WITH_SESSION_ID_IMPORT_NEW),\n );\n await applyGritQL(\n tree,\n filePath,\n pyRewrite('Iterator[Any]', 'Generator[Any]'),\n );\n};\n\nconst SESSION_CONTEXT_IMPORT_OLD = 'from collections.abc import Iterator';\nconst SESSION_CONTEXT_IMPORT_NEW = 'from collections.abc import Generator';\n\nconst migrateSessionContext = async (\n tree: Tree,\n filePath: string,\n nextSteps: string[],\n): Promise<void> => {\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (!contents.includes('Iterator[None]')) return;\n\n const ready = await allMatch(tree, filePath, [\n pyMatch(SESSION_CONTEXT_IMPORT_OLD),\n pyMatch('Iterator[None]'),\n ]);\n\n if (!ready) {\n nextSteps.push(\n `${filePath}: diverged from the generated shape - left untouched. Manually change the \\`Iterator\\` import and \\`Iterator[None]\\` return annotation on \\`session_id_context\\` to \\`Generator\\`/\\`Generator[None]\\` (see the agent-connection generator's session_context.py template).`,\n );\n return;\n }\n\n await applyGritQL(\n tree,\n filePath,\n pyRewrite(SESSION_CONTEXT_IMPORT_OLD, SESSION_CONTEXT_IMPORT_NEW),\n );\n await applyGritQL(\n tree,\n filePath,\n pyRewrite('Iterator[None]', 'Generator[None]'),\n );\n};\n\nconst findAgentComponents = (\n components: ComponentMetadata[] | undefined,\n): ComponentMetadata[] =>\n (components ?? []).filter(\n (component) => component.generator === PY_AGENT_GENERATOR_INFO.id,\n );\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const project of getProjects(tree).values()) {\n const components = findAgentComponents(\n (project.metadata as { components?: ComponentMetadata[] })?.components,\n );\n\n for (const component of components) {\n if (!component.path || component.protocol !== 'a2a') {\n continue;\n }\n\n const componentDir = component.path.endsWith('/agent.py')\n ? component.path.slice(0, -'/agent.py'.length)\n : component.path;\n const mainPath = joinPathFragments(project.root, componentDir, 'main.py');\n\n if ((component.framework ?? 'strands') === 'langchain') {\n await migrateLangchainA2AAgent(tree, mainPath, nextSteps);\n } else {\n await migrateStrandsA2AAgent(tree, mainPath, nextSteps);\n }\n }\n }\n\n const filePaths: string[] = [];\n visitNotIgnoredFiles(tree, '', (filePath) => filePaths.push(filePath));\n\n for (const filePath of filePaths) {\n if (filePath.endsWith('/with_session_id_strands.py')) {\n await migrateWithSessionId(tree, filePath, nextSteps);\n } else if (filePath.endsWith('/session_context.py')) {\n await migrateSessionContext(tree, filePath, nextSteps);\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","visitNotIgnoredFiles","PY_AGENT_GENERATOR_INFO","addPythonDestructuredImport","applyGritQL","captureGritQLVariable","matchGritQL","formatFilesInSubtree","pyMatch","snippet","pyDelete","pyRewrite","from","to","replacement","includes","AGENT_ENTER","OLD_AGENT_CONTEXT","NEW_CONSTANTS_AND_LIFESPAN","A2A_SERVER_STATEMENT","OLD_MOUNT_STATEMENT","OLD_FASTAPI_APP","NEW_FASTAPI_APP","SERVER_MATCH","allMatch","tree","filePath","patterns","pattern","migrateStrandsA2AAgent","mainPath","nextSteps","exists","contents","read","ready","diverged","push","capturedArgs","undefined","LANGCHAIN_GRAPH_OLD","LANGCHAIN_LIFESPAN_NEW","LANGCHAIN_GRAPH_INVOKE_OLD","LANGCHAIN_GRAPH_INVOKE_NEW","LANGCHAIN_APP_OLD","LANGCHAIN_APP_NEW","migrateLangchainA2AAgent","WITH_SESSION_ID_IMPORT_OLD","WITH_SESSION_ID_IMPORT_NEW","migrateWithSessionId","SESSION_CONTEXT_IMPORT_OLD","SESSION_CONTEXT_IMPORT_NEW","migrateSessionContext","findAgentComponents","components","filter","component","generator","id","migration","project","values","metadata","path","protocol","componentDir","endsWith","slice","length","root","framework","filePaths"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
import uuid
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
4
5
|
|
|
5
6
|
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
6
7
|
from a2a.server.apps import A2AStarletteApplication
|
|
@@ -23,7 +24,11 @@ RUNTIME_URL = os.environ.get("AGENTCORE_RUNTIME_URL", f"http://localhost:{PORT}/
|
|
|
23
24
|
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
|
|
24
25
|
DEFAULT_MODES = ["text/plain"]
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
@asynccontextmanager
|
|
29
|
+
async def lifespan(app: FastAPI):
|
|
30
|
+
app.state.agent = get_agent()
|
|
31
|
+
yield
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
class _GraphAgentExecutor(AgentExecutor):
|
|
@@ -37,7 +42,7 @@ class _GraphAgentExecutor(AgentExecutor):
|
|
|
37
42
|
# The session bound by the middleware drives the LangGraph thread, keeping
|
|
38
43
|
# checkpointed conversation state separate per caller session.
|
|
39
44
|
session_id = get_current_session_id() or task.context_id
|
|
40
|
-
result = await
|
|
45
|
+
result = await app.state.agent.ainvoke(
|
|
41
46
|
{"messages": [{"role": "user", "content": context.get_user_input()}]},
|
|
42
47
|
{"configurable": {"thread_id": session_id}},
|
|
43
48
|
)
|
|
@@ -77,7 +82,7 @@ class _SessionIdMiddleware(BaseHTTPMiddleware):
|
|
|
77
82
|
return await call_next(request)
|
|
78
83
|
|
|
79
84
|
|
|
80
|
-
app = FastAPI(title="<%= agentNameClassName %>")
|
|
85
|
+
app = FastAPI(title="<%= agentNameClassName %>", lifespan=lifespan)
|
|
81
86
|
app.add_middleware(_SessionIdMiddleware)
|
|
82
87
|
|
|
83
88
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
import uuid
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import cast
|
|
4
7
|
|
|
5
8
|
from fastapi import FastAPI, Request
|
|
6
9
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
10
|
+
from strands import Agent
|
|
7
11
|
from strands.multiagent.a2a import A2AServer
|
|
8
12
|
from <%- agentConnectionModuleName %> import session_id_context, with_session_id
|
|
9
13
|
|
|
@@ -14,23 +18,27 @@ logging.basicConfig(level=logging.INFO)
|
|
|
14
18
|
PORT = int(os.environ.get("PORT", "9000"))
|
|
15
19
|
RUNTIME_URL = os.environ.get("AGENTCORE_RUNTIME_URL", f"http://localhost:{PORT}/")
|
|
16
20
|
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
|
|
21
|
+
AGENT_NAME = "<%= agentNameClassName %>"
|
|
22
|
+
AGENT_DESCRIPTION = "A Strands Agent exposed via the Agent-to-Agent (A2A) protocol."
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
# A2AServer.__init__ calls agent_factory once, synchronously, before the lifespan
|
|
25
|
+
# below has run - this is what it reads name/description from at that point.
|
|
26
|
+
# The cast is a lie about runtime shape, not behavior: only .name/.description
|
|
27
|
+
# are ever read off this value.
|
|
28
|
+
_card_placeholder = cast(
|
|
29
|
+
Agent, SimpleNamespace(name=AGENT_NAME, description=AGENT_DESCRIPTION)
|
|
22
30
|
)
|
|
23
|
-
_agent = _agent_ctx.__enter__()
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
|
|
33
|
+
@asynccontextmanager
|
|
34
|
+
async def lifespan(app: FastAPI):
|
|
35
|
+
with with_session_id(
|
|
36
|
+
get_agent,
|
|
37
|
+
name=AGENT_NAME,
|
|
38
|
+
description=AGENT_DESCRIPTION,
|
|
39
|
+
) as agent:
|
|
40
|
+
app.state.agent = agent
|
|
41
|
+
yield
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
class _SessionIdMiddleware(BaseHTTPMiddleware):
|
|
@@ -42,7 +50,7 @@ class _SessionIdMiddleware(BaseHTTPMiddleware):
|
|
|
42
50
|
return await call_next(request)
|
|
43
51
|
|
|
44
52
|
|
|
45
|
-
app = FastAPI()
|
|
53
|
+
app = FastAPI(lifespan=lifespan)
|
|
46
54
|
app.add_middleware(_SessionIdMiddleware)
|
|
47
55
|
|
|
48
56
|
|
|
@@ -51,4 +59,14 @@ def ping() -> dict[str, str]:
|
|
|
51
59
|
return {"status": "Healthy"}
|
|
52
60
|
|
|
53
61
|
|
|
62
|
+
a2a_server = A2AServer(
|
|
63
|
+
agent_factory=lambda _context_id: getattr(app.state, "agent", _card_placeholder),
|
|
64
|
+
port=PORT,
|
|
65
|
+
http_url=RUNTIME_URL,
|
|
66
|
+
serve_at_root=True,
|
|
67
|
+
# Skip tool-registry introspection for the agent card — the per-session
|
|
68
|
+
# agents aren't constructed yet at startup.
|
|
69
|
+
skills=[],
|
|
70
|
+
)
|
|
71
|
+
|
|
54
72
|
app.mount("/", a2a_server.to_fastapi_app())
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import
|
|
1
|
+
from collections.abc import Generator
|
|
2
2
|
from contextlib import contextmanager
|
|
3
3
|
from contextvars import ContextVar
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ def get_current_session_id() -> str | None:
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@contextmanager
|
|
16
|
-
def session_id_context(session_id: str) ->
|
|
16
|
+
def session_id_context(session_id: str) -> Generator[None]:
|
|
17
17
|
"""Bind *session_id* as the current session for the scope of the block."""
|
|
18
18
|
token = _session_id_var.set(session_id)
|
|
19
19
|
try:
|
package/src/utils/agent-connection/files/py-core-strands/base/with_session_id_strands.py.template
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import Callable,
|
|
1
|
+
from collections.abc import Callable, Generator
|
|
2
2
|
from contextlib import AbstractContextManager, ExitStack, contextmanager
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ def with_session_id(
|
|
|
11
11
|
*,
|
|
12
12
|
name: str,
|
|
13
13
|
description: str,
|
|
14
|
-
) ->
|
|
14
|
+
) -> Generator[Any]:
|
|
15
15
|
"""Wrap an agent factory so each session gets its own cached Agent."""
|
|
16
16
|
stack = ExitStack()
|
|
17
17
|
agents: dict[str, Any] = {}
|