@aws/nx-plugin 1.0.0-rc.78 → 1.0.0-rc.79

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 CHANGED
@@ -37,6 +37,11 @@
37
37
  "description": "Surface enableWaf, encryption, encryptionKey and enableKeyRotation on the vended StaticWebsite construct/module",
38
38
  "implementation": "./src/migrations/latest/static-website-configurable-waf-encryption/migration"
39
39
  },
40
+ "latest-py-agent-ag-ui-hooks": {
41
+ "version": "1.0.0-rc.79",
42
+ "description": "Forward the py#agent Strands hooks to the AG-UI adapter so model and tool errors are reported",
43
+ "implementation": "./src/migrations/latest/py-agent-ag-ui-hooks/migration"
44
+ },
40
45
  "v1.0.0-rc.50-0001-modernize-function-props-cast": {
41
46
  "version": "1.0.0-rc.50",
42
47
  "description": "Replace the legacy angle-bracket FunctionProps type assertion with the modern as syntax in generated API constructs",
@@ -158,7 +163,7 @@
158
163
  "implementation": "./src/migrations/v1.0.0-rc.72/0001-py-agent-session-management-support/migration"
159
164
  },
160
165
  "sync-vended-versions": {
161
- "version": "1.0.0-rc.78",
166
+ "version": "1.0.0-rc.79",
162
167
  "description": "Sync vended dependency versions and the tracked plugin version to those vended by this release",
163
168
  "implementation": "./src/utils/version-upgrade-migration/migration"
164
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin",
3
- "version": "1.0.0-rc.78",
3
+ "version": "1.0.0-rc.79",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",
@@ -0,0 +1,3 @@
1
+ {
2
+ "description": "Forward the py#agent Strands hooks to the AG-UI adapter so model and tool errors are reported"
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,113 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { getProjects, joinPathFragments } from "@nx/devkit";
5
+ import { PY_AGENT_GENERATOR_INFO } from "../../../py/agent/generator.js";
6
+ import { addPythonDestructuredImport, applyGritQL, captureGritQLVariable, GRIT_INSERT_PLACEHOLDER, insertViaGritQL, matchGritQL } from "../../../utils/ast.js";
7
+ import { formatFilesInSubtree } from "../../../utils/format.js";
8
+ /**
9
+ * Forward a Strands py#agent's hooks to the AG-UI adapter.
10
+ *
11
+ * `StrandsAgent` builds a fresh `Agent` per `thread_id` by copying the template
12
+ * Agent's kwargs, but deliberately skips `hooks` - Strands keeps only the built
13
+ * `HookRegistry`, so the providers can't be read back off it. An AG-UI agent's
14
+ * `log_model_errors` / `log_tool_errors` therefore never fired for served
15
+ * requests, and model/tool failures were reported as successful runs.
16
+ *
17
+ * The fix hoists the hooks list out of the `Agent(...)` call into an
18
+ * `AGENT_HOOKS` module constant in `agent.py` - whatever the user has put in it,
19
+ * so custom hooks come along - and passes that to `StrandsAgent(...)` in
20
+ * `main.py` as well. `agent.py` is hoisted for every protocol so the constant
21
+ * matches what the generator vends today; only AG-UI needs the `main.py` half.
22
+ */ // `Agent`'s `hooks` list is invariant, so the hoisted constant has to declare
23
+ // the parameter's own type - an inline literal was inferred from it instead.
24
+ const HOOKS_TYPE = 'list[HookProvider | HookCallback]';
25
+ const HOOKS_MODULE = 'strands.hooks';
26
+ // Anchored on the `get_agent` context manager - the one shape shared by every
27
+ // generated protocol and connection combination - so the constant lands
28
+ // directly above its only use, where the generator vends it.
29
+ const HOIST_HOOKS_PATTERN = `language python
30
+ \`@contextmanager
31
+ def get_agent($params):
32
+ $body\` => raw\`AGENT_HOOKS: ${HOOKS_TYPE} = ${GRIT_INSERT_PLACEHOLDER}
33
+
34
+
35
+ @contextmanager
36
+ def get_agent($params):
37
+ $body\` where { $program <: not contains \`AGENT_HOOKS\` }`;
38
+ // Scoped to the Agent constructor so a hooks list the user passes elsewhere in
39
+ // agent.py is left alone.
40
+ const HOOKS_LIST_CAPTURE_PATTERN = 'language python\n`hooks=[$hooks]` where { $hooks <: within `Agent($_)` }';
41
+ const HOOKS_LIST_REWRITE_PATTERN = 'language python\n`hooks=[$_]` as $kwarg where { $kwarg <: within `Agent($_)`, $kwarg => `hooks=AGENT_HOOKS` }';
42
+ const AGUI_CONSTRUCTOR_MATCH_PATTERN = 'language python\n`StrandsAgent($args)` where { $args <: contains `agent=$_` }';
43
+ const AGUI_HOOKS_WIRED_PATTERN = 'language python\n`StrandsAgent($args)` where { $args <: contains `agent=$_`, $args <: contains `hooks=$_` }';
44
+ // Appends after the last argument rather than re-emitting the argument list,
45
+ // both to land `hooks=` where the template puts it and to leave the other
46
+ // arguments' formatting and comments untouched.
47
+ const AGUI_HOOKS_APPEND_PATTERN = 'language python\n`StrandsAgent($args)` where { $args <: contains `agent=$_`, $args <: not contains `hooks=$_`, $args <: [$..., $last], $last += `, hooks=AGENT_HOOKS` }';
48
+ const findAgentComponents = (components)=>(components ?? []).filter((component)=>component.generator === PY_AGENT_GENERATOR_INFO.id);
49
+ /**
50
+ * Hoists `agent.py`'s `hooks=[...]` list into an `AGENT_HOOKS` module constant,
51
+ * returning whether the file ends up exporting one.
52
+ */ const hoistAgentHooks = async (tree, agentPath)=>{
53
+ if ((tree.read(agentPath, 'utf-8') ?? '').includes('AGENT_HOOKS')) {
54
+ return true;
55
+ }
56
+ // Nothing to hoist unless the Agent is constructed with an inline hooks list.
57
+ const hooks = await captureGritQLVariable(tree, agentPath, HOOKS_LIST_CAPTURE_PATTERN, 'hooks');
58
+ if (!hooks) return false;
59
+ if (!await insertViaGritQL(tree, agentPath, HOIST_HOOKS_PATTERN, `[${hooks}]`)) {
60
+ return false;
61
+ }
62
+ await applyGritQL(tree, agentPath, HOOKS_LIST_REWRITE_PATTERN);
63
+ await addPythonDestructuredImport(tree, agentPath, [
64
+ 'HookCallback',
65
+ 'HookProvider'
66
+ ], HOOKS_MODULE);
67
+ return true;
68
+ };
69
+ const migrateAgent = async (tree, agentDir, component, nextSteps)=>{
70
+ // LangChain agents pass no hooks - their AG-UI adapter is handed the graph
71
+ // itself rather than rebuilding one, so nothing is dropped.
72
+ if (component.framework === 'langchain') return;
73
+ const agentPath = joinPathFragments(agentDir, 'agent.py');
74
+ const mainPath = joinPathFragments(agentDir, 'main.py');
75
+ const isAgUi = component.protocol === 'ag-ui';
76
+ if (!tree.exists(agentPath)) return;
77
+ if (!await hoistAgentHooks(tree, agentPath)) {
78
+ if (isAgUi) {
79
+ nextSteps.push(`${agentPath}: the hooks passed to \`Agent(...)\` have diverged from the generated shape - left untouched. Hoist them into an \`AGENT_HOOKS\` module constant and pass it to \`StrandsAgent(...)\` in ${mainPath} too, or AG-UI will not register them (see the py#agent generator's template).`);
80
+ }
81
+ return;
82
+ }
83
+ // Only AG-UI rebuilds the Agent, so only its main.py needs the hooks.
84
+ if (!isAgUi || !tree.exists(mainPath)) return;
85
+ // Already passing hooks, whether from a previous run or the user's own wiring.
86
+ if (await matchGritQL(tree, mainPath, AGUI_HOOKS_WIRED_PATTERN)) return;
87
+ if (!await matchGritQL(tree, mainPath, AGUI_CONSTRUCTOR_MATCH_PATTERN)) {
88
+ nextSteps.push(`${mainPath}: the \`StrandsAgent(...)\` constructor has diverged from the generated shape - left untouched. Pass \`hooks=AGENT_HOOKS\` to it, importing \`AGENT_HOOKS\` from \`.agent\`, or AG-UI will not register the agent's hooks (see the py#agent generator's template).`);
89
+ return;
90
+ }
91
+ await applyGritQL(tree, mainPath, AGUI_HOOKS_APPEND_PATTERN);
92
+ await addPythonDestructuredImport(tree, mainPath, [
93
+ 'AGENT_HOOKS'
94
+ ], '.agent');
95
+ };
96
+ export default async function migration(tree) {
97
+ const nextSteps = [];
98
+ for (const project of getProjects(tree).values()){
99
+ const components = findAgentComponents(project.metadata?.components);
100
+ for (const component of components){
101
+ if (!component.path) continue;
102
+ // Legacy component paths point at agent.py rather than its directory.
103
+ const componentDir = component.path.endsWith('/agent.py') ? component.path.slice(0, -'/agent.py'.length) : component.path;
104
+ await migrateAgent(tree, joinPathFragments(project.root, componentDir), component, nextSteps);
105
+ }
106
+ }
107
+ await formatFilesInSubtree(tree);
108
+ return {
109
+ nextSteps
110
+ };
111
+ }
112
+
113
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/py-agent-ag-ui-hooks/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} from '@nx/devkit';\nimport { PY_AGENT_GENERATOR_INFO } from '../../../py/agent/generator';\nimport {\n addPythonDestructuredImport,\n applyGritQL,\n captureGritQLVariable,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport type { ComponentMetadata } from '../../../utils/nx';\n\n/**\n * Forward a Strands py#agent's hooks to the AG-UI adapter.\n *\n * `StrandsAgent` builds a fresh `Agent` per `thread_id` by copying the template\n * Agent's kwargs, but deliberately skips `hooks` - Strands keeps only the built\n * `HookRegistry`, so the providers can't be read back off it. An AG-UI agent's\n * `log_model_errors` / `log_tool_errors` therefore never fired for served\n * requests, and model/tool failures were reported as successful runs.\n *\n * The fix hoists the hooks list out of the `Agent(...)` call into an\n * `AGENT_HOOKS` module constant in `agent.py` - whatever the user has put in it,\n * so custom hooks come along - and passes that to `StrandsAgent(...)` in\n * `main.py` as well. `agent.py` is hoisted for every protocol so the constant\n * matches what the generator vends today; only AG-UI needs the `main.py` half.\n */\n\n// `Agent`'s `hooks` list is invariant, so the hoisted constant has to declare\n// the parameter's own type - an inline literal was inferred from it instead.\nconst HOOKS_TYPE = 'list[HookProvider | HookCallback]';\nconst HOOKS_MODULE = 'strands.hooks';\n\n// Anchored on the `get_agent` context manager - the one shape shared by every\n// generated protocol and connection combination - so the constant lands\n// directly above its only use, where the generator vends it.\nconst HOIST_HOOKS_PATTERN = `language python\n\\`@contextmanager\ndef get_agent($params):\n $body\\` => raw\\`AGENT_HOOKS: ${HOOKS_TYPE} = ${GRIT_INSERT_PLACEHOLDER}\n\n\n@contextmanager\ndef get_agent($params):\n $body\\` where { $program <: not contains \\`AGENT_HOOKS\\` }`;\n\n// Scoped to the Agent constructor so a hooks list the user passes elsewhere in\n// agent.py is left alone.\nconst HOOKS_LIST_CAPTURE_PATTERN =\n 'language python\\n`hooks=[$hooks]` where { $hooks <: within `Agent($_)` }';\nconst HOOKS_LIST_REWRITE_PATTERN =\n 'language python\\n`hooks=[$_]` as $kwarg where { $kwarg <: within `Agent($_)`, $kwarg => `hooks=AGENT_HOOKS` }';\n\nconst AGUI_CONSTRUCTOR_MATCH_PATTERN =\n 'language python\\n`StrandsAgent($args)` where { $args <: contains `agent=$_` }';\nconst AGUI_HOOKS_WIRED_PATTERN =\n 'language python\\n`StrandsAgent($args)` where { $args <: contains `agent=$_`, $args <: contains `hooks=$_` }';\n// Appends after the last argument rather than re-emitting the argument list,\n// both to land `hooks=` where the template puts it and to leave the other\n// arguments' formatting and comments untouched.\nconst AGUI_HOOKS_APPEND_PATTERN =\n 'language python\\n`StrandsAgent($args)` where { $args <: contains `agent=$_`, $args <: not contains `hooks=$_`, $args <: [$..., $last], $last += `, hooks=AGENT_HOOKS` }';\n\nconst findAgentComponents = (\n components: ComponentMetadata[] | undefined,\n): ComponentMetadata[] =>\n (components ?? []).filter(\n (component) => component.generator === PY_AGENT_GENERATOR_INFO.id,\n );\n\n/**\n * Hoists `agent.py`'s `hooks=[...]` list into an `AGENT_HOOKS` module constant,\n * returning whether the file ends up exporting one.\n */\nconst hoistAgentHooks = async (\n tree: Tree,\n agentPath: string,\n): Promise<boolean> => {\n if ((tree.read(agentPath, 'utf-8') ?? '').includes('AGENT_HOOKS')) {\n return true;\n }\n\n // Nothing to hoist unless the Agent is constructed with an inline hooks list.\n const hooks = await captureGritQLVariable(\n tree,\n agentPath,\n HOOKS_LIST_CAPTURE_PATTERN,\n 'hooks',\n );\n if (!hooks) return false;\n\n if (\n !(await insertViaGritQL(tree, agentPath, HOIST_HOOKS_PATTERN, `[${hooks}]`))\n ) {\n return false;\n }\n await applyGritQL(tree, agentPath, HOOKS_LIST_REWRITE_PATTERN);\n await addPythonDestructuredImport(\n tree,\n agentPath,\n ['HookCallback', 'HookProvider'],\n HOOKS_MODULE,\n );\n return true;\n};\n\nconst migrateAgent = async (\n tree: Tree,\n agentDir: string,\n component: ComponentMetadata,\n nextSteps: string[],\n): Promise<void> => {\n // LangChain agents pass no hooks - their AG-UI adapter is handed the graph\n // itself rather than rebuilding one, so nothing is dropped.\n if (component.framework === 'langchain') return;\n\n const agentPath = joinPathFragments(agentDir, 'agent.py');\n const mainPath = joinPathFragments(agentDir, 'main.py');\n const isAgUi = component.protocol === 'ag-ui';\n\n if (!tree.exists(agentPath)) return;\n\n if (!(await hoistAgentHooks(tree, agentPath))) {\n if (isAgUi) {\n nextSteps.push(\n `${agentPath}: the hooks passed to \\`Agent(...)\\` have diverged from the generated shape - left untouched. Hoist them into an \\`AGENT_HOOKS\\` module constant and pass it to \\`StrandsAgent(...)\\` in ${mainPath} too, or AG-UI will not register them (see the py#agent generator's template).`,\n );\n }\n return;\n }\n\n // Only AG-UI rebuilds the Agent, so only its main.py needs the hooks.\n if (!isAgUi || !tree.exists(mainPath)) return;\n\n // Already passing hooks, whether from a previous run or the user's own wiring.\n if (await matchGritQL(tree, mainPath, AGUI_HOOKS_WIRED_PATTERN)) return;\n\n if (!(await matchGritQL(tree, mainPath, AGUI_CONSTRUCTOR_MATCH_PATTERN))) {\n nextSteps.push(\n `${mainPath}: the \\`StrandsAgent(...)\\` constructor has diverged from the generated shape - left untouched. Pass \\`hooks=AGENT_HOOKS\\` to it, importing \\`AGENT_HOOKS\\` from \\`.agent\\`, or AG-UI will not register the agent's hooks (see the py#agent generator's template).`,\n );\n return;\n }\n\n await applyGritQL(tree, mainPath, AGUI_HOOKS_APPEND_PATTERN);\n await addPythonDestructuredImport(tree, mainPath, ['AGENT_HOOKS'], '.agent');\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) continue;\n\n // Legacy component paths point at agent.py rather than its directory.\n const componentDir = component.path.endsWith('/agent.py')\n ? component.path.slice(0, -'/agent.py'.length)\n : component.path;\n\n await migrateAgent(\n tree,\n joinPathFragments(project.root, componentDir),\n component,\n nextSteps,\n );\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","PY_AGENT_GENERATOR_INFO","addPythonDestructuredImport","applyGritQL","captureGritQLVariable","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","HOOKS_TYPE","HOOKS_MODULE","HOIST_HOOKS_PATTERN","HOOKS_LIST_CAPTURE_PATTERN","HOOKS_LIST_REWRITE_PATTERN","AGUI_CONSTRUCTOR_MATCH_PATTERN","AGUI_HOOKS_WIRED_PATTERN","AGUI_HOOKS_APPEND_PATTERN","findAgentComponents","components","filter","component","generator","id","hoistAgentHooks","tree","agentPath","read","includes","hooks","migrateAgent","agentDir","nextSteps","framework","mainPath","isAgUi","protocol","exists","push","migration","project","values","metadata","path","componentDir","endsWith","slice","length","root"],"mappings":"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"}
@@ -665,6 +665,7 @@ exports[`py#agent generator > should match snapshot for generated files > agent-
665
665
 
666
666
  from proj_agent_connection import log_model_errors, log_tool_errors
667
667
  from strands import Agent, tool
668
+ from strands.hooks import HookCallback, HookProvider
668
669
  from strands_tools import current_time
669
670
 
670
671
  from .session import get_session_manager
@@ -675,6 +676,9 @@ def subtract(a: int, b: int) -> int:
675
676
  return a - b
676
677
 
677
678
 
679
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
680
+
681
+
678
682
  @contextmanager
679
683
  def get_agent():
680
684
  yield Agent(
@@ -686,7 +690,7 @@ Use your tools for mathematical tasks.
686
690
  Refer to tools as your 'spellbook'.
687
691
  """,
688
692
  tools=[subtract, current_time],
689
- hooks=[log_model_errors, log_tool_errors],
693
+ hooks=AGENT_HOOKS,
690
694
  session_manager=get_session_manager(),
691
695
  )
692
696
  "
@@ -10,7 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
10
10
  from fastapi.responses import StreamingResponse
11
11
  from <%- agentConnectionModuleName %> import get_current_session_id, session_id_context
12
12
 
13
- from .agent import get_agent
13
+ from .agent import AGENT_HOOKS, get_agent
14
14
  from .middleware.session_id_middleware import SESSION_ID_HEADER, SessionIdMiddleware
15
15
  from .session import get_session_manager
16
16
 
@@ -27,6 +27,10 @@ async def lifespan(app: FastAPI):
27
27
  # A per-thread session manager, not the template Agent's own, since
28
28
  # AG-UI caches one Strands agent per thread_id.
29
29
  config=StrandsAgentConfig(session_manager_provider=lambda _input_data: get_session_manager()),
30
+ # Required as well as on the template Agent: AG-UI keeps only the
31
+ # built HookRegistry, so hooks it can't read back are never
32
+ # registered and model/tool failures go unreported.
33
+ hooks=AGENT_HOOKS,
30
34
  )
31
35
  yield
32
36
 
@@ -1,6 +1,7 @@
1
1
  from contextlib import contextmanager
2
2
 
3
3
  from strands import Agent, tool
4
+ from strands.hooks import HookCallback, HookProvider
4
5
  from strands_tools import current_time
5
6
  from <%- agentConnectionModuleName %> import log_model_errors, log_tool_errors
6
7
  <%_ if (protocol !== 'ag-ui') { _%>
@@ -14,6 +15,9 @@ def subtract(a: int, b: int) -> int:
14
15
  return a - b
15
16
 
16
17
 
18
+ AGENT_HOOKS: list[HookProvider | HookCallback] = [log_model_errors, log_tool_errors]
19
+
20
+
17
21
  @contextmanager
18
22
  def get_agent():
19
23
  yield Agent(
@@ -25,7 +29,7 @@ Use your tools for mathematical tasks.
25
29
  Refer to tools as your 'spellbook'.
26
30
  """,
27
31
  tools=[subtract, current_time],
28
- hooks=[log_model_errors, log_tool_errors],
32
+ hooks=AGENT_HOOKS,
29
33
  <%_ if (protocol !== 'ag-ui') { _%>
30
34
  session_manager=get_session_manager(),
31
35
  <%_ } _%>