diff --git a/docs/memory.md b/docs/memory.md index cd429ce57..0f38513d2 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -210,6 +210,43 @@ Each strategy can have optional configuration: | `namespaces` | No | **Deprecated alias for `namespaceTemplates`.** Accepted for backward compatibility. | | `reflectionNamespaces` | EPISODIC only | **Deprecated alias for `reflectionNamespaceTemplates`.** Accepted for backward compatibility. | +## Indexed Metadata Keys + +Indexed keys declare metadata fields on a memory that can be used to filter long-term memory records on retrieval. Up to +10 keys per memory. + +```bash +agentcore add memory \ + --name SupportMemory \ + --strategies SEMANTIC \ + --indexed-key priority:NUMBER \ + --indexed-key agent_type:STRING \ + --indexed-key tags:STRINGLIST +``` + +In `agentcore.json`: + +```json +{ + "name": "SupportMemory", + "strategies": [{ "type": "SEMANTIC" }], + "indexedKeys": [ + { "key": "priority", "type": "NUMBER" }, + { "key": "agent_type", "type": "STRING" }, + { "key": "tags", "type": "STRINGLIST" } + ] +} +``` + +| Type | Description | +| ------------ | --------------------- | +| `STRING` | Single string value | +| `STRINGLIST` | List of string values | +| `NUMBER` | Numeric value | + +Indexed keys require at least one long-term memory strategy. They can only be added to an existing memory — once +declared, an indexed key cannot be removed. + ## Event Expiry Memory events expire after a configurable duration (7-365 days, default 30): diff --git a/package-lock.json b/package-lock.json index caacc4419..dc07a0016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@aws-sdk/client-bedrock": "^3.1012.0", "@aws-sdk/client-bedrock-agent": "^3.1012.0", "@aws-sdk/client-bedrock-agentcore": "^3.1020.0", - "@aws-sdk/client-bedrock-agentcore-control": "^3.1039.0", + "@aws-sdk/client-bedrock-agentcore-control": "^3.1048.0", "@aws-sdk/client-bedrock-runtime": "^3.893.0", "@aws-sdk/client-cloudformation": "^3.893.0", "@aws-sdk/client-cloudwatch-logs": "^3.893.0", @@ -944,50 +944,20 @@ } }, "node_modules/@aws-sdk/client-bedrock-agentcore-control": { - "version": "3.1039.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore-control/-/client-bedrock-agentcore-control-3.1039.0.tgz", - "integrity": "sha512-YoJ1rYHikWbsBuTyjADxtYzD+PXmyR2X6NVWkX4AqUtZT1YJC1HW+BsFABlXfItsEOjss3kpsjQHnlMbiCBtig==", + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore-control/-/client-bedrock-agentcore-control-3.1048.0.tgz", + "integrity": "sha512-l8ghHdi/re43kWU+Zsz/I0i2vJIoybTfkMverrViIe2f7y7nYIh+ULdMTCaJeRUFf6WjqM4CC8iDJ2Weml7EqQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.7", - "@aws-sdk/credential-provider-node": "^3.972.38", - "@aws-sdk/middleware-host-header": "^3.972.10", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.37", - "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.23", - "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.17", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/hash-node": "^4.2.14", - "@smithy/invalid-dependency": "^4.2.14", - "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.7", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.49", - "@smithy/util-defaults-mode-node": "^4.2.54", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -2176,24 +2146,18 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.7.tgz", - "integrity": "sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw==", + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.22", - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-utf8": "^4.2.2", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { @@ -2264,14 +2228,14 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.33.tgz", - "integrity": "sha512-bJV7eViSJV6GSuuN+VIdNVPdwPsNSf75BiC2v5alPrjR/OCcqgKwSZInKbDFz9mNeizldsyf67jt6YSIiv53Cw==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", + "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2280,20 +2244,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.35.tgz", - "integrity": "sha512-x/BQGEIdq0oI+4WxLjKmnQvT7CnF9r8ezdGt7wXwxb7ckHXQz0Zmgxt8v3Ne0JaT3R5YefmuybHX6E8EnsDXyA==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", + "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -2301,23 +2262,22 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.37.tgz", - "integrity": "sha512-eUTpmWfd/BKsq9medhCRcu+GRAhFP2Zrn7/2jKDHHOOjCkhrMoTp/t4cEthqFoG7gE0VGp5wUxrXTdvBCmSmJg==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", - "@aws-sdk/credential-provider-env": "^3.972.33", - "@aws-sdk/credential-provider-http": "^3.972.35", - "@aws-sdk/credential-provider-login": "^3.972.37", - "@aws-sdk/credential-provider-process": "^3.972.33", - "@aws-sdk/credential-provider-sso": "^3.972.37", - "@aws-sdk/credential-provider-web-identity": "^3.972.37", - "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2326,17 +2286,15 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.37.tgz", - "integrity": "sha512-Ty68y8ISSC+g5Q3D0K8uAaoINwvfaOslnNpsF/LgVUxyosYXHawcK2yV4HLXDVugiTTYLQfJfcw0ce5meAGkKw==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", - "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2345,21 +2303,20 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.38.tgz", - "integrity": "sha512-BQ9XYnBDVxR2HuV5huXYQYF/PZMTsY+EnwfGnCU2cA8Zw63XpkOtPY8WqiMIZMQCrKPQQEiFURS/o9CIolRLqg==", + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.33", - "@aws-sdk/credential-provider-http": "^3.972.35", - "@aws-sdk/credential-provider-ini": "^3.972.37", - "@aws-sdk/credential-provider-process": "^3.972.33", - "@aws-sdk/credential-provider-sso": "^3.972.37", - "@aws-sdk/credential-provider-web-identity": "^3.972.37", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2368,15 +2325,14 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.33.tgz", - "integrity": "sha512-yfjGksI9WQbdMObb0VeLXqzTLI+a0qXLJT9gCDiv0+X/xjPpI3mTz6a5FibrhpuEKIe0gSgvs3MaoFZy5cx4WA==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", + "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2385,17 +2341,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.37.tgz", - "integrity": "sha512-fpwE+20ntpp3i9Xb9vUuQfXLDKYHH+5I2V+ZG96SX1nBzrruhy10RXDgmN7t1etOz3c55stlA3TeQASUA451NQ==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", - "@aws-sdk/nested-clients": "^3.997.5", - "@aws-sdk/token-providers": "3.1039.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2404,16 +2359,15 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.37.tgz", - "integrity": "sha512-aryawqyebf+3WhAFNHfF62rekFpYtVcVN7dQ89qnAWsa4n5hJst8qBG6gXC24WHtW7Nnhkf9ScYnjwo0Brn3bw==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", - "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2754,49 +2708,20 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.5.tgz", - "integrity": "sha512-jGFr6DxtcMTmzOkG/a0jCZYv4BBDmeNYVeO+/memSoDkYCJu4Y58xviYmzwJfYyIVSts+X/BVjJm1uGBnwHEMg==", + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.7", - "@aws-sdk/middleware-host-header": "^3.972.10", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.37", - "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.24", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.23", - "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.17", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/hash-node": "^4.2.14", - "@smithy/invalid-dependency": "^4.2.14", - "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.7", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.49", - "@smithy/util-defaults-mode-node": "^4.2.54", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -2820,15 +2745,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.24.tgz", - "integrity": "sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw==", + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.36", "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -2837,16 +2761,15 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1039.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1039.0.tgz", - "integrity": "sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw==", + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.7", - "@aws-sdk/nested-clients": "^3.997.5", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -5218,20 +5141,25 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.17", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", - "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.25", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -5239,15 +5167,25 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", - "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -5325,15 +5263,25 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", - "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -5521,14 +5469,25 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", - "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5614,18 +5573,25 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", - "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { diff --git a/package.json b/package.json index c323cd4a0..e1f1564ea 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@aws-sdk/client-bedrock": "^3.1012.0", "@aws-sdk/client-bedrock-agent": "^3.1012.0", "@aws-sdk/client-bedrock-agentcore": "^3.1020.0", - "@aws-sdk/client-bedrock-agentcore-control": "^3.1039.0", + "@aws-sdk/client-bedrock-agentcore-control": "^3.1048.0", "@aws-sdk/client-bedrock-runtime": "^3.893.0", "@aws-sdk/client-cloudformation": "^3.893.0", "@aws-sdk/client-cloudwatch-logs": "^3.893.0", diff --git a/src/cli/aws/agentcore-control.ts b/src/cli/aws/agentcore-control.ts index 159939b17..54e89eb3e 100644 --- a/src/cli/aws/agentcore-control.ts +++ b/src/cli/aws/agentcore-control.ts @@ -371,6 +371,7 @@ export interface MemoryDetail { namespaceTemplates?: string[]; reflectionNamespaceTemplates?: string[]; }[]; + indexedKeys?: { key: string; type: string }[]; tags?: Record; encryptionKeyArn?: string; executionRoleArn?: string; @@ -408,6 +409,14 @@ export async function getMemoryDetail(options: GetMemoryOptions): Promise { + if (!k.key || !k.type) { + console.warn(`Warning: Skipping malformed indexed key from API response: ${JSON.stringify(k)}`); + return []; + } + return [{ key: k.key, type: k.type }]; + }); + return { memoryId: memory.id, memoryArn: memory.arn, @@ -418,6 +427,7 @@ export async function getMemoryDetail(options: GetMemoryOptions): Promise 0 && { indexedKeys }), strategies: (memory.strategies ?? []).map(s => { if (!s.type) { throw new Error(`Memory ${options.memoryId} has a strategy with missing required field: type`); diff --git a/src/cli/aws/policy-generation.ts b/src/cli/aws/policy-generation.ts index da58edf81..fa75548c6 100644 --- a/src/cli/aws/policy-generation.ts +++ b/src/cli/aws/policy-generation.ts @@ -6,7 +6,7 @@ import { StartPolicyGenerationCommand, waitUntilPolicyGenerationCompleted, } from '@aws-sdk/client-bedrock-agentcore-control'; -import { WaiterState } from '@smithy/util-waiter'; +import { WaiterState } from '@smithy/core/client'; export interface StartPolicyGenerationOptions { policyEngineId: string; diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index 2be895494..b22b5455c 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -1126,6 +1126,118 @@ describe('validate', () => { expect(result.valid).toBe(false); expect(result.error).toContain('does not match the expected schema'); }); + + // Indexed keys: requires LTM strategy + it('rejects --indexed-key without any LTM strategy', () => { + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: undefined, + indexedKey: ['priority:NUMBER'], + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('requires at least one long-term memory strategy'); + }); + + it('accepts --indexed-key with an LTM strategy', () => { + expect( + validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: ['priority:NUMBER'], + }) + ).toEqual({ valid: true }); + }); + + it('rejects more than 10 indexed keys', () => { + const eleven = Array.from({ length: 11 }, (_, i) => `k${i}:STRING`); + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: eleven, + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Maximum 10 indexed keys'); + }); + + it('accepts exactly 10 indexed keys (boundary)', () => { + const ten = Array.from({ length: 10 }, (_, i) => `k${i}:STRING`); + expect(validateAddMemoryOptions({ ...validMemoryOptions, strategies: 'SEMANTIC', indexedKey: ten })).toEqual({ + valid: true, + }); + }); + + it('rejects an empty key (":STRING")', () => { + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: [':STRING'], + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Key name cannot be empty'); + }); + + it('rejects a key longer than 128 characters', () => { + const longKey = 'a'.repeat(129); + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: [`${longKey}:STRING`], + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('exceeds maximum length'); + }); + + it('rejects an invalid type token', () => { + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: ['priority:INTEGER'], + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid type'); + }); + + it('rejects duplicate keys', () => { + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: ['priority:NUMBER', 'priority:STRING'], + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Duplicate indexed key'); + }); + + it('rejects whitespace-only key', () => { + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: [' :STRING'], + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('whitespace'); + }); + + it('rejects malformed entry without colon', () => { + const result = validateAddMemoryOptions({ + ...validMemoryOptions, + strategies: 'SEMANTIC', + indexedKey: ['priority'], + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Expected key:TYPE'); + }); + + it.each([ + ['user.email:STRING'], + ['tag/v2:STRINGLIST'], + ['kebab-case:STRING'], + ['x-custom:STRING'], + ['has:colons:in:key:NUMBER'], + ])('accepts punctuation-rich key %s', raw => { + expect(validateAddMemoryOptions({ ...validMemoryOptions, strategies: 'SEMANTIC', indexedKey: [raw] })).toEqual({ + valid: true, + }); + }); }); describe('validateAddCredentialOptions', () => { diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index c1dd6641f..a6555e970 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -96,6 +96,7 @@ export interface AddMemoryOptions { dataStreamArn?: string; contentLevel?: string; streamDeliveryResources?: string; + indexedKey?: string[]; json?: boolean; } diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 4a6275010..ed4c9a320 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -19,6 +19,7 @@ import { } from '../../../schema'; import { ARN_VALIDATION_MESSAGE, isValidArn } from '../shared/arn-utils'; import { validateHeaderAllowlist } from '../shared/header-utils'; +import { MAX_INDEXED_KEYS, parseIndexedKeyArg } from '../shared/indexed-key-parser'; import { parseAndValidateLifecycleOptions } from '../shared/lifecycle-utils'; import { validateVpcOptions } from '../shared/vpc-utils'; import { validateJwtAuthorizerOptions } from './auth-options'; @@ -721,6 +722,37 @@ export function validateAddMemoryOptions(options: AddMemoryOptions): ValidationR } } + if (options.indexedKey && options.indexedKey.length > 0) { + const ltmStrategies = (options.strategies ?? '') + .split(',') + .map(s => s.trim().toUpperCase()) + .filter(Boolean); + if (ltmStrategies.length === 0) { + return { + valid: false, + error: + '--indexed-key requires at least one long-term memory strategy (--strategies). Indexed keys filter long-term memory records on retrieval.', + }; + } + + if (options.indexedKey.length > MAX_INDEXED_KEYS) { + return { valid: false, error: `Maximum ${MAX_INDEXED_KEYS} indexed keys allowed` }; + } + + const seenKeys = new Set(); + for (const raw of options.indexedKey) { + const result = parseIndexedKeyArg(raw); + if (!result.ok) { + return { valid: false, error: result.error }; + } + const { key } = result.value; + if (seenKeys.has(key)) { + return { valid: false, error: `Duplicate indexed key: "${key}"` }; + } + seenKeys.add(key); + } + } + if (options.streamDeliveryResources && (options.dataStreamArn || options.contentLevel || options.deliveryType)) { return { valid: false, diff --git a/src/cli/commands/import/import-memory.ts b/src/cli/commands/import/import-memory.ts index 2558d941a..370f3f726 100644 --- a/src/cli/commands/import/import-memory.ts +++ b/src/cli/commands/import/import-memory.ts @@ -1,4 +1,5 @@ import type { Memory } from '../../../schema'; +import { IndexedKeyTypeSchema } from '../../../schema'; import type { MemoryDetail, MemorySummary } from '../../aws/agentcore-control'; import { getMemoryDetail, listAllMemories } from '../../aws/agentcore-control'; import { ANSI } from './constants'; @@ -55,10 +56,26 @@ function toMemorySpec(memory: MemoryDetail, localName: string): Memory { }; }); + // Validate each indexed key's type against our enum. Drop + // entries whose type is not one we recognize with a warning + const indexedKeys: Memory['indexedKeys'] = memory.indexedKeys + ?.flatMap(k => { + const parsedType = IndexedKeyTypeSchema.safeParse(k.type); + if (!parsedType.success) { + console.warn( + `${ANSI.yellow}[warn]${ANSI.reset} Skipping indexed key "${k.key}" with unrecognised type "${k.type}".` + ); + return []; + } + return [{ key: k.key, type: parsedType.data }]; + }) + .filter(Boolean); + return { name: localName, eventExpiryDuration: Math.max(3, Math.min(365, memory.eventExpiryDuration)), strategies, + ...(indexedKeys && indexedKeys.length > 0 && { indexedKeys }), ...(memory.tags && Object.keys(memory.tags).length > 0 && { tags: memory.tags }), ...(memory.encryptionKeyArn && { encryptionKeyArn: memory.encryptionKeyArn }), ...(memory.executionRoleArn && { executionRoleArn: memory.executionRoleArn }), diff --git a/src/cli/commands/shared/indexed-key-parser.ts b/src/cli/commands/shared/indexed-key-parser.ts new file mode 100644 index 000000000..4ddf19184 --- /dev/null +++ b/src/cli/commands/shared/indexed-key-parser.ts @@ -0,0 +1,86 @@ +import type { IndexedKey, IndexedKeyType } from '../../../schema'; +import { + INDEXED_KEY_NAME_PATTERN, + INDEXED_KEY_NAME_PATTERN_MESSAGE, + IndexedKeyTypeSchema, + MAX_INDEXED_KEYS, + MAX_INDEXED_KEY_NAME_LENGTH, +} from '../../../schema'; + +export { INDEXED_KEY_NAME_PATTERN, MAX_INDEXED_KEYS }; +export const MAX_KEY_NAME_LENGTH = MAX_INDEXED_KEY_NAME_LENGTH; +export const VALID_INDEXED_KEY_TYPES: readonly IndexedKeyType[] = ['STRING', 'STRINGLIST', 'NUMBER']; + +/** + * Validate an indexed key name. Returns `true` when valid, or an error message. + * Shared between the schema-side regex (via constants) and TUI inline validation. + */ +export function validateIndexedKeyName(value: string, existingNames: readonly string[] = []): true | string { + if (!INDEXED_KEY_NAME_PATTERN.test(value)) { + return INDEXED_KEY_NAME_PATTERN_MESSAGE; + } + if (value.trim().length === 0) { + return 'Key cannot be only whitespace'; + } + if (value.length > MAX_INDEXED_KEY_NAME_LENGTH) { + return `Maximum ${MAX_INDEXED_KEY_NAME_LENGTH} characters`; + } + if (existingNames.includes(value)) { + return 'Key already defined'; + } + return true; +} + +export interface IndexedKeyParseError { + ok: false; + error: string; +} + +export interface IndexedKeyParseSuccess { + ok: true; + value: IndexedKey; +} + +export type IndexedKeyParseResult = IndexedKeyParseError | IndexedKeyParseSuccess; + +/** + * Parse a single `key:TYPE` argument into a validated IndexedKey. + * + * Splits on the *last* `:` so that key names may contain `:` (the AgentCore + * service accepts `:` in indexed key names; type tokens never do). + */ +export function parseIndexedKeyArg(raw: string): IndexedKeyParseResult { + const colonIdx = raw.lastIndexOf(':'); + if (colonIdx === -1) { + return { ok: false, error: `Invalid indexed key format: "${raw}". Expected key:TYPE (e.g. priority:NUMBER)` }; + } + const key = raw.slice(0, colonIdx); + const typeToken = raw.slice(colonIdx + 1).toUpperCase(); + + if (!key) { + return { ok: false, error: `Invalid indexed key format: "${raw}". Key name cannot be empty` }; + } + if (key.length > MAX_KEY_NAME_LENGTH) { + return { + ok: false, + error: `Indexed key name "${key}" exceeds maximum length of ${MAX_KEY_NAME_LENGTH} characters`, + }; + } + if (!INDEXED_KEY_NAME_PATTERN.test(key)) { + return { + ok: false, + error: `Invalid indexed key name "${key}". Must contain only alphanumeric characters, whitespace, or the symbols . _ : / = + @ -`, + }; + } + if (key.trim().length === 0) { + return { ok: false, error: `Invalid indexed key name "${key}". Key cannot be only whitespace` }; + } + const parsedType = IndexedKeyTypeSchema.safeParse(typeToken); + if (!parsedType.success) { + return { + ok: false, + error: `Invalid type "${typeToken}" for indexed key "${key}". Must be one of: ${VALID_INDEXED_KEY_TYPES.join(', ')}`, + }; + } + return { ok: true, value: { key, type: parsedType.data } }; +} diff --git a/src/cli/primitives/MemoryPrimitive.tsx b/src/cli/primitives/MemoryPrimitive.tsx index 6f02941bc..2fa877e23 100644 --- a/src/cli/primitives/MemoryPrimitive.tsx +++ b/src/cli/primitives/MemoryPrimitive.tsx @@ -1,6 +1,7 @@ import { ResourceNotFoundError, findConfigRoot, serializeResult, toError } from '../../lib'; import type { Result } from '../../lib/result'; import type { + IndexedKey, Memory, MemoryStrategy, MemoryStrategyType, @@ -16,6 +17,7 @@ import { StreamDeliveryResourcesSchema, } from '../../schema'; import { DEFAULT_DELIVERY_TYPE, validateAddMemoryOptions } from '../commands/add/validate'; +import { parseIndexedKeyArg } from '../commands/shared/indexed-key-parser'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, SchemaChange } from '../operations/remove/types'; import { runCliCommand } from '../telemetry/cli-command-run.js'; @@ -39,6 +41,8 @@ export interface AddMemoryOptions { contentLevel?: string; // Raw JSON for advanced/multi-target configurations. Takes precedence over flat flags. streamDeliveryResources?: string; + // Repeatable flag values as "key:TYPE" strings + indexedKey?: string[]; } /** @@ -75,11 +79,14 @@ export class MemoryPrimitive extends BasePrimitive', 'Stream delivery config as JSON string (advanced, overrides flat flags) [non-interactive]' ) + .option( + '--indexed-key ', + 'Indexed metadata key as key:TYPE (repeatable, max 10). TYPE is STRING, STRINGLIST, or NUMBER [non-interactive]', + (val: string, acc: string[]) => [...acc, val], + [] as string[] + ) .option('--json', 'Output as JSON [non-interactive]') .action( async (cliOptions: { @@ -182,6 +195,7 @@ export class MemoryPrimitive extends BasePrimitive { if (!findConfigRoot()) { @@ -193,6 +207,9 @@ export class MemoryPrimitive extends BasePrimitive { const expiry = cliOptions.expiry ? parseInt(cliOptions.expiry, 10) : undefined; + const indexedKey = + cliOptions.indexedKey && cliOptions.indexedKey.length > 0 ? cliOptions.indexedKey : undefined; + const validation = validateAddMemoryOptions({ name: cliOptions.name, strategies: cliOptions.strategies, @@ -201,6 +218,7 @@ export class MemoryPrimitive extends BasePrimitive s.trim().toUpperCase()) .filter(Boolean); + const indexedKeyCount = cliOptions.indexedKey?.length ?? 0; return { strategy_count: strategyList.length, strategy_semantic: strategyList.includes('SEMANTIC'), strategy_summarization: strategyList.includes('SUMMARIZATION'), strategy_user_preference: strategyList.includes('USER_PREFERENCE'), strategy_episodic: strategyList.includes('EPISODIC'), + indexed_key_count: indexedKeyCount, + has_indexed_keys: indexedKeyCount > 0, }; }); } else { @@ -282,6 +304,7 @@ export class MemoryPrimitive extends BasePrimitive { const project = await this.readProjectSpec(); @@ -301,6 +324,7 @@ export class MemoryPrimitive extends BasePrimitive 0 && { indexedKeys: config.indexedKeys }), ...(config.streamDeliveryResources && { streamDeliveryResources: config.streamDeliveryResources }), }; @@ -341,4 +365,14 @@ export class MemoryPrimitive extends BasePrimitive { + const result = parseIndexedKeyArg(raw); + if (!result.ok) { + throw new Error(result.error); + } + return result.value; + }); + } } diff --git a/src/cli/telemetry/schemas/command-run.ts b/src/cli/telemetry/schemas/command-run.ts index fae8f8394..0d66627f9 100644 --- a/src/cli/telemetry/schemas/command-run.ts +++ b/src/cli/telemetry/schemas/command-run.ts @@ -65,6 +65,8 @@ const AddMemoryAttrs = safeSchema({ strategy_summarization: z.boolean(), strategy_user_preference: z.boolean(), strategy_episodic: z.boolean(), + indexed_key_count: Count, + has_indexed_keys: z.boolean(), }); const AddCredentialAttrs = safeSchema({ credential_type: CredentialType }); diff --git a/src/cli/tui/hooks/useCreateMemory.ts b/src/cli/tui/hooks/useCreateMemory.ts index cf891c4f9..e452c27ed 100644 --- a/src/cli/tui/hooks/useCreateMemory.ts +++ b/src/cli/tui/hooks/useCreateMemory.ts @@ -9,6 +9,7 @@ interface CreateMemoryConfig { name: string; eventExpiryDuration: number; strategies: { type: string }[]; + indexedKeys?: { key: string; type: string }[]; streaming?: { dataStreamArn: string; contentLevel: string }; } @@ -26,6 +27,9 @@ export function useCreateMemory() { try { const strategiesStr = config.strategies.map(s => s.type).join(','); const strategyList = strategiesStr ? strategiesStr.split(',').map(s => s.trim().toUpperCase()) : []; + const indexedKey = config.indexedKeys?.map(k => `${k.key}:${k.type}`); + const indexedKeyCount = config.indexedKeys?.length ?? 0; + const addResult = await withCommandRunTelemetry( 'add.memory', { @@ -34,6 +38,8 @@ export function useCreateMemory() { strategy_summarization: strategyList.includes('SUMMARIZATION'), strategy_user_preference: strategyList.includes('USER_PREFERENCE'), strategy_episodic: strategyList.includes('EPISODIC'), + indexed_key_count: indexedKeyCount, + has_indexed_keys: indexedKeyCount > 0, }, () => memoryPrimitive.add({ @@ -42,6 +48,7 @@ export function useCreateMemory() { strategies: strategiesStr || undefined, dataStreamArn: config.streaming?.dataStreamArn, contentLevel: config.streaming?.contentLevel, + indexedKey, }) ); if (!addResult.success) { diff --git a/src/cli/tui/screens/memory/AddMemoryScreen.tsx b/src/cli/tui/screens/memory/AddMemoryScreen.tsx index da6cb3bdb..1006d07cd 100644 --- a/src/cli/tui/screens/memory/AddMemoryScreen.tsx +++ b/src/cli/tui/screens/memory/AddMemoryScreen.tsx @@ -1,6 +1,7 @@ -import type { MemoryStrategyType } from '../../../../schema'; +import type { IndexedKeyType, MemoryStrategyType } from '../../../../schema'; import { AgentNameSchema, StreamContentLevelSchema } from '../../../../schema'; import { ARN_VALIDATION_MESSAGE, isValidArn } from '../../../commands/shared/arn-utils'; +import { validateIndexedKeyName } from '../../../commands/shared/indexed-key-parser'; import { ConfirmReview, Panel, @@ -14,10 +15,17 @@ import type { SelectableItem } from '../../components'; import { HELP_TEXT } from '../../constants'; import { useListNavigation, useMultiSelectNavigation } from '../../hooks'; import { generateUniqueName } from '../../utils'; -import type { AddMemoryConfig } from './types'; -import { CONTENT_LEVEL_OPTIONS, EVENT_EXPIRY_OPTIONS, MEMORY_STEP_LABELS, MEMORY_STRATEGY_OPTIONS } from './types'; +import type { AddMemoryConfig, AddMemoryIndexedKeyConfig } from './types'; +import { + CONTENT_LEVEL_OPTIONS, + EVENT_EXPIRY_OPTIONS, + INDEXED_KEY_TYPE_OPTIONS, + MEMORY_STEP_LABELS, + MEMORY_STRATEGY_OPTIONS, +} from './types'; import { useAddMemoryWizard } from './useAddMemoryWizard'; -import React, { useMemo } from 'react'; +import { Box, Text } from 'ink'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; interface AddMemoryScreenProps { onComplete: (config: AddMemoryConfig) => void; @@ -30,9 +38,16 @@ const STREAMING_OPTIONS: SelectableItem[] = [ { id: 'yes', title: 'Yes', description: 'Stream memory record events to a delivery target (e.g. Kinesis)' }, ]; +type IndexedKeysSubStep = 'prompt' | 'keyName' | 'keyType' | 'addAnother'; + export function AddMemoryScreen({ onComplete, onExit, existingMemoryNames }: AddMemoryScreenProps) { const wizard = useAddMemoryWizard(); + // Indexed keys sub-flow state + const [indexedKeysSubStep, setIndexedKeysSubStep] = useState('prompt'); + const [pendingKeyName, setPendingKeyName] = useState(''); + const [collectedKeys, setCollectedKeys] = useState([]); + const strategyItems: SelectableItem[] = useMemo( () => MEMORY_STRATEGY_OPTIONS.map(opt => ({ id: opt.id, title: opt.title, description: opt.description })), [] @@ -48,9 +63,31 @@ export function AddMemoryScreen({ onComplete, onExit, existingMemoryNames }: Add [] ); + const indexedKeyTypeItems: SelectableItem[] = useMemo( + () => INDEXED_KEY_TYPE_OPTIONS.map(opt => ({ id: opt.id, title: opt.title, description: opt.description })), + [] + ); + const isNameStep = wizard.step === 'name'; const isExpiryStep = wizard.step === 'expiry'; const isStrategiesStep = wizard.step === 'strategies'; + const isIndexedKeysStep = wizard.step === 'indexedKeys'; + + /* eslint-disable react-hooks/set-state-in-effect */ + useEffect(() => { + if ( + isIndexedKeysStep && + indexedKeysSubStep === 'prompt' && + collectedKeys.length === 0 && + wizard.config.indexedKeys && + wizard.config.indexedKeys.length > 0 + ) { + setCollectedKeys(wizard.config.indexedKeys); + setIndexedKeysSubStep('addAnother'); + } + }, [isIndexedKeysStep, indexedKeysSubStep, collectedKeys.length, wizard.config.indexedKeys]); + /* eslint-enable react-hooks/set-state-in-effect */ + const isStreamingStep = wizard.step === 'streaming'; const isStreamArnStep = wizard.step === 'streamArn'; const isContentLevelStep = wizard.step === 'contentLevel'; @@ -72,6 +109,89 @@ export function AddMemoryScreen({ onComplete, onExit, existingMemoryNames }: Add requireSelection: false, }); + // Indexed keys sub-flow: initial prompt (yes/no) + const INDEXED_KEYS_PROMPT_OPTIONS: SelectableItem[] = useMemo( + () => [ + { id: 'no', title: 'No', description: 'Skip indexed keys' }, + { id: 'yes', title: 'Yes', description: 'Define keys for metadata filtering on retrieval' }, + ], + [] + ); + + const ADD_ANOTHER_OPTIONS: SelectableItem[] = useMemo( + () => [ + { id: 'no', title: 'No', description: 'Done adding keys' }, + { id: 'yes', title: 'Yes', description: `Add another key (${collectedKeys.length}/10 defined)` }, + { id: 'clear', title: 'Clear keys', description: 'Discard all keys and start over' }, + ], + [collectedKeys.length] + ); + + const indexedKeysPromptNav = useListNavigation({ + items: INDEXED_KEYS_PROMPT_OPTIONS, + onSelect: item => { + if (item.id === 'yes') { + setIndexedKeysSubStep('keyName'); + } else { + wizard.setIndexedKeys([]); + } + }, + onExit: () => wizard.goBack(), + isActive: isIndexedKeysStep && indexedKeysSubStep === 'prompt', + }); + + const handleKeyNameSubmit = useCallback((name: string) => { + setPendingKeyName(name); + setIndexedKeysSubStep('keyType'); + }, []); + + const indexedKeyTypeNav = useListNavigation({ + items: indexedKeyTypeItems, + onSelect: item => { + const newKey: AddMemoryIndexedKeyConfig = { key: pendingKeyName, type: item.id as IndexedKeyType }; + const updated = [...collectedKeys, newKey]; + setCollectedKeys(updated); + setPendingKeyName(''); + if (updated.length >= 10) { + wizard.setIndexedKeys(updated); + setCollectedKeys([]); + setIndexedKeysSubStep('prompt'); + } else { + setIndexedKeysSubStep('addAnother'); + } + }, + onExit: () => setIndexedKeysSubStep('keyName'), + isActive: isIndexedKeysStep && indexedKeysSubStep === 'keyType', + }); + + const addAnotherNav = useListNavigation({ + items: ADD_ANOTHER_OPTIONS, + onSelect: item => { + if (item.id === 'yes') { + setIndexedKeysSubStep('keyName'); + } else if (item.id === 'clear') { + // Discard everything and return to the initial Yes/No prompt as if + // entering the step for the first time. + setCollectedKeys([]); + wizard.clearIndexedKeys(); + setIndexedKeysSubStep('prompt'); + } else { + wizard.setIndexedKeys(collectedKeys); + setCollectedKeys([]); + setIndexedKeysSubStep('prompt'); + } + }, + onExit: () => { + const lastKey = collectedKeys[collectedKeys.length - 1]; + if (lastKey) { + setPendingKeyName(lastKey.key); + setCollectedKeys(collectedKeys.slice(0, -1)); + setIndexedKeysSubStep('keyType'); + } + }, + isActive: isIndexedKeysStep && indexedKeysSubStep === 'addAnother', + }); + const streamingNav = useListNavigation({ items: STREAMING_OPTIONS, onSelect: item => wizard.setStreamingEnabled(item.id === 'yes'), @@ -97,9 +217,13 @@ export function AddMemoryScreen({ onComplete, onExit, existingMemoryNames }: Add ? 'Space toggle · Enter confirm · Esc back' : isExpiryStep || isStreamingStep || isContentLevelStep ? HELP_TEXT.NAVIGATE_SELECT - : isConfirmStep - ? HELP_TEXT.CONFIRM_CANCEL - : HELP_TEXT.TEXT_INPUT; + : isIndexedKeysStep && (indexedKeysSubStep === 'prompt' || indexedKeysSubStep === 'addAnother') + ? HELP_TEXT.NAVIGATE_SELECT + : isIndexedKeysStep && indexedKeysSubStep === 'keyType' + ? HELP_TEXT.NAVIGATE_SELECT + : isConfirmStep + ? HELP_TEXT.CONFIRM_CANCEL + : HELP_TEXT.TEXT_INPUT; const headerContent = ; @@ -108,6 +232,9 @@ export function AddMemoryScreen({ onComplete, onExit, existingMemoryNames }: Add { label: 'Name', value: wizard.config.name }, { label: 'Event Expiry', value: `${wizard.config.eventExpiryDuration} days` }, { label: 'Strategies', value: wizard.config.strategies.map(s => s.type).join(', ') || 'None' }, + ...(wizard.config.indexedKeys && wizard.config.indexedKeys.length > 0 + ? [{ label: 'Indexed Keys', value: wizard.config.indexedKeys.map(k => `${k.key} (${k.type})`).join(', ') }] + : []), ...(wizard.config.streaming ? [ { label: 'Stream ARN', value: wizard.config.streaming.dataStreamArn }, @@ -118,6 +245,8 @@ export function AddMemoryScreen({ onComplete, onExit, existingMemoryNames }: Add [wizard.config] ); + const existingKeyNames = useMemo(() => collectedKeys.map(k => k.key), [collectedKeys]); + return ( )} + {isIndexedKeysStep && indexedKeysSubStep === 'prompt' && ( + + )} + + {isIndexedKeysStep && indexedKeysSubStep === 'keyName' && ( + + {collectedKeys.length > 0 && ( + + {collectedKeys.map(k => ( + + {` ✓ ${k.key} (${k.type})`} + + ))} + + )} + { + setPendingKeyName(''); + if (collectedKeys.length > 0) { + setIndexedKeysSubStep('addAnother'); + } else { + wizard.clearIndexedKeys(); + setIndexedKeysSubStep('prompt'); + } + }} + customValidation={value => validateIndexedKeyName(value, existingKeyNames)} + /> + + )} + + {isIndexedKeysStep && indexedKeysSubStep === 'keyType' && ( + + {collectedKeys.length > 0 && ( + + {collectedKeys.map(k => ( + + {` ✓ ${k.key} (${k.type})`} + + ))} + + )} + + + )} + + {isIndexedKeysStep && indexedKeysSubStep === 'addAnother' && ( + + + {collectedKeys.map(k => ( + + {` ✓ ${k.key} (${k.type})`} + + ))} + + + + )} + {isStreamingStep && ( = { name: 'Name', expiry: 'Expiry', strategies: 'Strategies', + indexedKeys: 'Indexed Keys', streaming: 'Streaming', streamArn: 'Stream ARN', contentLevel: 'Content Level', @@ -71,4 +86,16 @@ export const CONTENT_LEVEL_OPTIONS = [ // Defaults // ───────────────────────────────────────────────────────────────────────────── +const INDEXED_KEY_TYPE_DESCRIPTIONS: Record = { + STRING: 'Single string value', + STRINGLIST: 'List of string values', + NUMBER: 'Numeric value', +}; + +export const INDEXED_KEY_TYPE_OPTIONS = IndexedKeyTypeSchema.options.map(type => ({ + id: type, + title: type, + description: INDEXED_KEY_TYPE_DESCRIPTIONS[type], +})); + export const DEFAULT_EVENT_EXPIRY = 30; diff --git a/src/cli/tui/screens/memory/useAddMemoryWizard.ts b/src/cli/tui/screens/memory/useAddMemoryWizard.ts index aca78e069..9bf0d410e 100644 --- a/src/cli/tui/screens/memory/useAddMemoryWizard.ts +++ b/src/cli/tui/screens/memory/useAddMemoryWizard.ts @@ -1,13 +1,21 @@ import type { MemoryStrategyType, StreamContentLevel } from '../../../../schema'; -import type { AddMemoryConfig, AddMemoryStep, AddMemoryStrategyConfig } from './types'; +import type { AddMemoryConfig, AddMemoryIndexedKeyConfig, AddMemoryStep, AddMemoryStrategyConfig } from './types'; import { DEFAULT_EVENT_EXPIRY } from './types'; import { useCallback, useMemo, useState } from 'react'; -const BASE_STEPS = ['name', 'expiry', 'strategies', 'streaming'] as const; -const STREAMING_STEPS = ['streamArn', 'contentLevel'] as const; -const FIRST_STREAMING_STEP = STREAMING_STEPS[0]; +const BASE_STEPS = ['name', 'expiry', 'strategies'] as const; +const INDEXED_KEYS_STEP = 'indexedKeys' as const; +const STREAMING_STEP = 'streaming' as const; +const STREAMING_DETAIL_STEPS = ['streamArn', 'contentLevel'] as const; +const FIRST_STREAMING_DETAIL_STEP = STREAMING_DETAIL_STEPS[0]; const CONFIRM_STEP = 'confirm' as const; +const LTM_STRATEGY_TYPES: MemoryStrategyType[] = ['SEMANTIC', 'USER_PREFERENCE', 'SUMMARIZATION', 'EPISODIC']; + +function hasLtmStrategy(strategies: AddMemoryStrategyConfig[]): boolean { + return strategies.some(s => LTM_STRATEGY_TYPES.includes(s.type)); +} + function getDefaultConfig(): AddMemoryConfig { return { name: '', @@ -21,10 +29,19 @@ export function useAddMemoryWizard() { const [step, setStep] = useState('name'); const [enableStreaming, setEnableStreaming] = useState(false); - const allSteps = useMemo( - () => (enableStreaming ? [...BASE_STEPS, ...STREAMING_STEPS, CONFIRM_STEP] : [...BASE_STEPS, CONFIRM_STEP]), - [enableStreaming] - ); + const allSteps = useMemo(() => { + const steps: AddMemoryStep[] = [...BASE_STEPS]; + if (hasLtmStrategy(config.strategies)) { + steps.push(INDEXED_KEYS_STEP); + } + steps.push(STREAMING_STEP); + if (enableStreaming) { + steps.push(...STREAMING_DETAIL_STEPS); + } + steps.push(CONFIRM_STEP); + return steps; + }, [enableStreaming, config.strategies]); + const currentIndex = allSteps.indexOf(step); const goBack = useCallback(() => { @@ -59,22 +76,28 @@ export function useAddMemoryWizard() { [nextStep] ); - const setStrategyTypes = useCallback( - (types: MemoryStrategyType[]) => { - const strategies: AddMemoryStrategyConfig[] = types.map(type => ({ type })); - setConfig(c => ({ ...c, strategies })); - const next = nextStep('strategies'); - if (next) setStep(next); - }, - [nextStep] - ); + const setStrategyTypes = useCallback((types: MemoryStrategyType[]) => { + const strategies: AddMemoryStrategyConfig[] = types.map(type => ({ type })); + const hasLtm = types.some(t => LTM_STRATEGY_TYPES.includes(t)); + // After setting strategies, we need to determine the next step. + // If LTM strategies were selected, next is indexedKeys; otherwise streaming. + setConfig(c => ({ ...c, strategies, ...(hasLtm ? {} : { indexedKeys: undefined }) })); + setStep(hasLtm ? INDEXED_KEYS_STEP : STREAMING_STEP); + }, []); + + const setIndexedKeys = useCallback((indexedKeys: AddMemoryIndexedKeyConfig[]) => { + setConfig(c => ({ ...c, indexedKeys: indexedKeys.length > 0 ? indexedKeys : undefined })); + setStep(STREAMING_STEP); + }, []); + + const clearIndexedKeys = useCallback(() => { + setConfig(c => ({ ...c, indexedKeys: undefined })); + }, []); const setStreamingEnabled = useCallback((enabled: boolean) => { setEnableStreaming(enabled); if (enabled) { - // Can't use nextStep() here — allSteps hasn't updated yet since - // setEnableStreaming is queued. Hardcode the known next step. - setStep(FIRST_STREAMING_STEP); + setStep(FIRST_STREAMING_DETAIL_STEP); } else { setConfig(c => ({ ...c, streaming: undefined })); setStep(CONFIRM_STEP); @@ -87,7 +110,7 @@ export function useAddMemoryWizard() { ...c, streaming: { dataStreamArn, contentLevel: c.streaming?.contentLevel ?? 'FULL_CONTENT' }, })); - const next = nextStep(FIRST_STREAMING_STEP); + const next = nextStep(FIRST_STREAMING_DETAIL_STEP); if (next) setStep(next); }, [nextStep] @@ -125,6 +148,8 @@ export function useAddMemoryWizard() { setName, setExpiry, setStrategyTypes, + setIndexedKeys, + clearIndexedKeys, setStreamingEnabled, setStreamArn, setContentLevel, diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index b3f4d3d6f..8cf22df2a 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -149,25 +149,77 @@ export const StreamDeliveryResourcesSchema = z.object({ export type StreamDeliveryResources = z.infer; -export const MemorySchema = z.object({ - name: MemoryNameSchema, - eventExpiryDuration: z.number().int().min(3).max(365), - // Strategies array can be empty for short-term memory (just base memory with expiration) - // Long-term memory includes strategies like SEMANTIC, SUMMARIZATION, USER_PREFERENCE - strategies: z - .array(MemoryStrategySchema) - .default([]) - .superRefine( - uniqueBy( - strategy => strategy.type, - type => `Duplicate memory strategy type: ${type}` - ) - ), - tags: TagsSchema.optional(), - encryptionKeyArn: z.string().optional(), - executionRoleArn: z.string().optional(), - streamDeliveryResources: StreamDeliveryResourcesSchema.optional(), +export const IndexedKeyTypeSchema = z.enum(['STRING', 'STRINGLIST', 'NUMBER']); +export type IndexedKeyType = z.infer; + +/** + * Indexed metadata key declaration on a Memory. + * Indexed keys enable filtering memory records on retrieval by attached metadata. + * + * Key pattern matches the AgentCore Control API: alphanumeric characters, whitespace, + * and the symbols `. _ : / = + @ -` (max 128 chars). + * + * Note: indexed keys are append-only on the AWS service side — once added to a Memory, + * a key cannot be removed. Reducing the array on update will fail at deploy time. + */ +export const INDEXED_KEY_NAME_PATTERN = /^[a-zA-Z0-9\s._:/=+@-]+$/; +export const INDEXED_KEY_NAME_PATTERN_MESSAGE = + 'Must contain only alphanumeric characters, whitespace, or the symbols . _ : / = + @ -'; +export const MAX_INDEXED_KEY_NAME_LENGTH = 128; +export const MAX_INDEXED_KEYS = 10; + +export const IndexedKeySchema = z.object({ + key: z + .string() + .min(1) + .max(MAX_INDEXED_KEY_NAME_LENGTH) + .regex(INDEXED_KEY_NAME_PATTERN, INDEXED_KEY_NAME_PATTERN_MESSAGE) + .refine(s => s.trim().length > 0, 'Key cannot be only whitespace'), + type: IndexedKeyTypeSchema, }); +export type IndexedKey = z.infer; + +export const MemorySchema = z + .object({ + name: MemoryNameSchema, + eventExpiryDuration: z.number().int().min(3).max(365), + // Strategies array can be empty for short-term memory (just base memory with expiration) + // Long-term memory includes strategies like SEMANTIC, SUMMARIZATION, USER_PREFERENCE + strategies: z + .array(MemoryStrategySchema) + .default([]) + .superRefine( + uniqueBy( + strategy => strategy.type, + type => `Duplicate memory strategy type: ${type}` + ) + ), + indexedKeys: z + .array(IndexedKeySchema) + .max(MAX_INDEXED_KEYS) + .superRefine( + uniqueBy( + entry => entry.key, + key => `Duplicate indexed key: ${key}` + ) + ) + .optional(), + tags: TagsSchema.optional(), + encryptionKeyArn: z.string().optional(), + executionRoleArn: z.string().optional(), + streamDeliveryResources: StreamDeliveryResourcesSchema.optional(), + }) + .superRefine((memory, ctx) => { + // Indexed keys filter long-term memory records on retrieval; they have no + // meaning on a short-term-only memory (no strategies => no LTM records). + if (memory.indexedKeys && memory.indexedKeys.length > 0 && memory.strategies.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['indexedKeys'], + message: 'indexedKeys requires at least one memory strategy (long-term memory)', + }); + } + }); export type Memory = z.infer;