thinkwork-cli 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/dist/cli.js +437 -83
- package/dist/terraform/examples/greenfield/main.tf +14 -9
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +7 -4
- package/dist/terraform/modules/app/agentcore-memory/README.md +77 -0
- package/dist/terraform/modules/app/agentcore-memory/main.tf +159 -0
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +212 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +29 -18
- package/dist/terraform/modules/app/hindsight-memory/README.md +50 -29
- package/dist/terraform/modules/app/hindsight-memory/main.tf +9 -7
- package/dist/terraform/modules/app/lambda-api/handlers.tf +43 -38
- package/dist/terraform/modules/app/lambda-api/main.tf +58 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +35 -5
- package/dist/terraform/modules/thinkwork/main.tf +44 -18
- package/dist/terraform/modules/thinkwork/outputs.tf +10 -5
- package/dist/terraform/modules/thinkwork/variables.tf +12 -6
- package/package.json +1 -1
|
@@ -72,10 +72,10 @@ variable "database_engine" {
|
|
|
72
72
|
default = "aurora-serverless"
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
variable "
|
|
76
|
-
description = "
|
|
77
|
-
type =
|
|
78
|
-
default =
|
|
75
|
+
variable "enable_hindsight" {
|
|
76
|
+
description = "Optional Hindsight add-on alongside the always-on managed memory (ECS+ALB for semantic + graph retrieval)"
|
|
77
|
+
type = bool
|
|
78
|
+
default = false
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
variable "google_oauth_client_id" {
|
|
@@ -119,7 +119,7 @@ module "thinkwork" {
|
|
|
119
119
|
|
|
120
120
|
db_password = var.db_password
|
|
121
121
|
database_engine = var.database_engine
|
|
122
|
-
|
|
122
|
+
enable_hindsight = var.enable_hindsight
|
|
123
123
|
google_oauth_client_id = var.google_oauth_client_id
|
|
124
124
|
google_oauth_client_secret = var.google_oauth_client_secret
|
|
125
125
|
pre_signup_lambda_zip = var.pre_signup_lambda_zip
|
|
@@ -199,16 +199,21 @@ output "database_name" {
|
|
|
199
199
|
value = module.thinkwork.database_name
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
output "
|
|
203
|
-
description = "
|
|
204
|
-
value = module.thinkwork.
|
|
202
|
+
output "hindsight_enabled" {
|
|
203
|
+
description = "Whether the Hindsight add-on is enabled"
|
|
204
|
+
value = module.thinkwork.hindsight_enabled
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
output "hindsight_endpoint" {
|
|
208
|
-
description = "Hindsight API endpoint (null when
|
|
208
|
+
description = "Hindsight API endpoint (null when enable_hindsight = false)"
|
|
209
209
|
value = module.thinkwork.hindsight_endpoint
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
output "agentcore_memory_id" {
|
|
213
|
+
description = "AgentCore Memory resource ID used for automatic retention"
|
|
214
|
+
value = module.thinkwork.agentcore_memory_id
|
|
215
|
+
}
|
|
216
|
+
|
|
212
217
|
output "admin_url" {
|
|
213
218
|
description = "Admin app URL"
|
|
214
219
|
value = "https://${module.thinkwork.admin_distribution_domain}"
|
|
@@ -12,10 +12,13 @@ account_id = "123456789012" # your AWS account ID
|
|
|
12
12
|
# "rds-postgres" — Standard RDS PostgreSQL (dev/test, cheaper, deletion protection off)
|
|
13
13
|
database_engine = "rds-postgres"
|
|
14
14
|
|
|
15
|
-
# Memory
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
15
|
+
# Memory:
|
|
16
|
+
# AgentCore managed memory is always on — every agent gets automatic
|
|
17
|
+
# per-turn retention with zero config. Uncomment the line below to
|
|
18
|
+
# enable Hindsight as an optional add-on (adds an ECS+ALB service for
|
|
19
|
+
# semantic + entity-graph + cross-encoder retrieval tools alongside the
|
|
20
|
+
# built-in remember/recall/forget tools).
|
|
21
|
+
# enable_hindsight = true
|
|
19
22
|
|
|
20
23
|
# Database master password — use a strong password
|
|
21
24
|
db_password = "CHANGE_ME_strong_password_here"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# AgentCore Memory — App Module
|
|
2
|
+
|
|
3
|
+
Provisions an AWS Bedrock AgentCore Memory resource with the four strategies
|
|
4
|
+
the thinkwork Strands agent container uses for automatic retention:
|
|
5
|
+
|
|
6
|
+
| Strategy | Namespace template | Purpose |
|
|
7
|
+
|-------------|------------------------------|------------------------------------------|
|
|
8
|
+
| semantic | `assistant_{actorId}` | Cross-thread facts about the user |
|
|
9
|
+
| preferences | `preferences_{actorId}` | User-stated preferences |
|
|
10
|
+
| summaries | `session_{sessionId}` | Per-thread rolling summaries |
|
|
11
|
+
| episodes | `episodes_{actorId}/{sessionId}` | Episodic memory of past interactions |
|
|
12
|
+
|
|
13
|
+
Automatic retention is wired in the agent container: every turn emits a
|
|
14
|
+
`CreateEvent` via `memory.store_turn_pair`, and AgentCore's background
|
|
15
|
+
strategies extract facts into the namespaces above. Agents can read them
|
|
16
|
+
back via the `recall()` tool (also always registered). There is no need for
|
|
17
|
+
the model to call `remember()` explicitly — it only exists for user-driven
|
|
18
|
+
"please remember X" requests.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```hcl
|
|
23
|
+
module "agentcore_memory" {
|
|
24
|
+
source = "../app/agentcore-memory"
|
|
25
|
+
|
|
26
|
+
stage = var.stage
|
|
27
|
+
region = var.region
|
|
28
|
+
# Optional: skip provisioning and reuse an existing memory resource
|
|
29
|
+
# existing_memory_id = "my-pre-existing-memory-id"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module "agentcore" {
|
|
33
|
+
source = "../app/agentcore-runtime"
|
|
34
|
+
# ...
|
|
35
|
+
agentcore_memory_id = module.agentcore_memory.memory_id
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why a shell script and not a first-class resource?
|
|
40
|
+
|
|
41
|
+
The AWS Terraform provider does not (yet) expose a
|
|
42
|
+
`aws_bedrockagentcore_memory` resource. Until it does, this module drives
|
|
43
|
+
the lifecycle through the `aws bedrock-agentcore-control` CLI:
|
|
44
|
+
|
|
45
|
+
- **Create/find**: `data "external"` runs `scripts/create_or_find_memory.sh`,
|
|
46
|
+
which is idempotent — it looks up an existing memory by name before
|
|
47
|
+
creating a new one. Safe to re-run.
|
|
48
|
+
- **Destroy**: a paired `terraform_data` resource has a destroy-time
|
|
49
|
+
`local-exec` that calls `delete-memory` on the ID captured during create.
|
|
50
|
+
|
|
51
|
+
When the AWS provider adds a native resource, migrate by importing the
|
|
52
|
+
existing memory ID into the new resource and removing this module's
|
|
53
|
+
external data source.
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- `aws` CLI v2 with `bedrock-agentcore-control` commands (recent versions)
|
|
58
|
+
- `jq` in PATH
|
|
59
|
+
- IAM permissions:
|
|
60
|
+
- `bedrock-agentcore-control:ListMemories`
|
|
61
|
+
- `bedrock-agentcore-control:CreateMemory`
|
|
62
|
+
- `bedrock-agentcore-control:DeleteMemory`
|
|
63
|
+
|
|
64
|
+
## Cost
|
|
65
|
+
|
|
66
|
+
AgentCore Memory charges per CreateEvent and per memory record extracted.
|
|
67
|
+
With automatic retention enabled, cost scales roughly linearly with chat
|
|
68
|
+
volume. Budget accordingly before enabling in production.
|
|
69
|
+
|
|
70
|
+
## Migration notes
|
|
71
|
+
|
|
72
|
+
- **Strategies are immutable after creation.** If you need to change a
|
|
73
|
+
namespace template, you must delete and recreate the memory (losing all
|
|
74
|
+
records). Version the `name_prefix` or `stage` if you need to keep the
|
|
75
|
+
old records around during migration.
|
|
76
|
+
- **BYO memory**: pass `existing_memory_id = "..."` to skip provisioning
|
|
77
|
+
entirely. Useful for shared memory across multiple stages.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# AgentCore Memory — App Module
|
|
3
|
+
#
|
|
4
|
+
# Provisions an AWS Bedrock AgentCore Memory resource with the four strategies
|
|
5
|
+
# the Strands agent container expects (semantic, preferences, summaries,
|
|
6
|
+
# episodes). The resource is always created — AgentCore managed memory is
|
|
7
|
+
# on by default so every agent gets automatic per-turn retention into
|
|
8
|
+
# semantic / preference / summary / episode strategies without any tool-
|
|
9
|
+
# calling by the model.
|
|
10
|
+
#
|
|
11
|
+
# **Why not a first-class resource?** The AWS provider does not (yet) expose a
|
|
12
|
+
# `aws_bedrockagentcore_memory` resource type. Until it does, we drive the
|
|
13
|
+
# create/find/destroy lifecycle through the `aws bedrock-agentcore-control`
|
|
14
|
+
# CLI via a small shell script, and read the resulting memory ID back into
|
|
15
|
+
# Terraform via `data "external"`. The script is idempotent — it lists
|
|
16
|
+
# existing memories with the same name and returns the existing ID if found,
|
|
17
|
+
# which keeps `terraform apply` safe to re-run.
|
|
18
|
+
#
|
|
19
|
+
# **BYO override:** If you already have an AgentCore Memory resource, set
|
|
20
|
+
# `var.existing_memory_id` to skip provisioning. The module output will echo
|
|
21
|
+
# that ID directly and no CLI calls are made.
|
|
22
|
+
################################################################################
|
|
23
|
+
|
|
24
|
+
terraform {
|
|
25
|
+
required_providers {
|
|
26
|
+
external = {
|
|
27
|
+
source = "hashicorp/external"
|
|
28
|
+
version = ">= 2.3.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
variable "stage" {
|
|
34
|
+
description = "Deployment stage (dev, prod, etc.) — used to name the memory resource"
|
|
35
|
+
type = string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
variable "name_prefix" {
|
|
39
|
+
description = "Prefix for the Bedrock AgentCore Memory resource name"
|
|
40
|
+
type = string
|
|
41
|
+
default = "thinkwork"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
variable "existing_memory_id" {
|
|
45
|
+
description = "Optional pre-existing AgentCore Memory ID. When set, the module skips provisioning and passes this ID through."
|
|
46
|
+
type = string
|
|
47
|
+
default = ""
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
variable "region" {
|
|
51
|
+
description = "AWS region"
|
|
52
|
+
type = string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
variable "account_id" {
|
|
56
|
+
description = "AWS account ID"
|
|
57
|
+
type = string
|
|
58
|
+
default = ""
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
locals {
|
|
62
|
+
memory_name = "${replace(var.name_prefix, "-", "_")}_${replace(var.stage, "-", "_")}"
|
|
63
|
+
bootstrap = var.existing_memory_id == ""
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
################################################################################
|
|
67
|
+
# IAM Role for custom memory strategies
|
|
68
|
+
################################################################################
|
|
69
|
+
|
|
70
|
+
resource "aws_iam_role" "memory_execution" {
|
|
71
|
+
count = local.bootstrap ? 1 : 0
|
|
72
|
+
name = "thinkwork-${var.stage}-memory-execution"
|
|
73
|
+
|
|
74
|
+
assume_role_policy = jsonencode({
|
|
75
|
+
Version = "2012-10-17"
|
|
76
|
+
Statement = [{
|
|
77
|
+
Effect = "Allow"
|
|
78
|
+
Principal = { Service = "bedrock-agentcore.amazonaws.com" }
|
|
79
|
+
Action = "sts:AssumeRole"
|
|
80
|
+
}]
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resource "aws_iam_role_policy" "memory_execution" {
|
|
85
|
+
count = local.bootstrap ? 1 : 0
|
|
86
|
+
name = "memory-execution"
|
|
87
|
+
role = aws_iam_role.memory_execution[0].id
|
|
88
|
+
|
|
89
|
+
policy = jsonencode({
|
|
90
|
+
Version = "2012-10-17"
|
|
91
|
+
Statement = [{
|
|
92
|
+
Effect = "Allow"
|
|
93
|
+
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
|
|
94
|
+
Resource = "arn:aws:bedrock:${var.region}::foundation-model/*"
|
|
95
|
+
}]
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
################################################################################
|
|
100
|
+
# Create-or-find via shell script (only when no existing_memory_id was given).
|
|
101
|
+
#
|
|
102
|
+
# The script produces JSON: `{"memory_id": "..."}`. Terraform re-runs it on
|
|
103
|
+
# every plan — if the memory already exists, the script returns the same ID
|
|
104
|
+
# without side effects. Inputs are passed as JSON on stdin; outputs MUST be
|
|
105
|
+
# a single JSON object on stdout for `data "external"` to parse.
|
|
106
|
+
################################################################################
|
|
107
|
+
|
|
108
|
+
data "external" "memory" {
|
|
109
|
+
count = local.bootstrap ? 1 : 0
|
|
110
|
+
program = ["bash", "${path.module}/scripts/create_or_find_memory.sh"]
|
|
111
|
+
|
|
112
|
+
query = {
|
|
113
|
+
name = local.memory_name
|
|
114
|
+
region = var.region
|
|
115
|
+
execution_role_arn = aws_iam_role.memory_execution[0].arn
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
################################################################################
|
|
120
|
+
# Destroy-time cleanup
|
|
121
|
+
#
|
|
122
|
+
# Terraform's `data "external"` has no destroy hook, so we use a paired
|
|
123
|
+
# `terraform_data` resource with a destroy-time local-exec that deletes the
|
|
124
|
+
# memory by ID. `triggers_replace` binds the resource to the memory ID so
|
|
125
|
+
# that replacing one memory correctly destroys the old one.
|
|
126
|
+
################################################################################
|
|
127
|
+
|
|
128
|
+
resource "terraform_data" "memory_lifecycle" {
|
|
129
|
+
count = local.bootstrap ? 1 : 0
|
|
130
|
+
|
|
131
|
+
input = {
|
|
132
|
+
memory_id = data.external.memory[0].result.memory_id
|
|
133
|
+
region = var.region
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
triggers_replace = [
|
|
137
|
+
local.memory_name,
|
|
138
|
+
var.region,
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
provisioner "local-exec" {
|
|
142
|
+
when = destroy
|
|
143
|
+
command = "aws bedrock-agentcore-control delete-memory --region ${self.output.region} --memory-id ${self.output.memory_id} || echo 'delete-memory failed (may already be gone)'"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
################################################################################
|
|
148
|
+
# Outputs
|
|
149
|
+
################################################################################
|
|
150
|
+
|
|
151
|
+
output "memory_id" {
|
|
152
|
+
description = "Bedrock AgentCore Memory resource ID — passed into the agent container as AGENTCORE_MEMORY_ID"
|
|
153
|
+
value = local.bootstrap ? data.external.memory[0].result.memory_id : var.existing_memory_id
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
output "memory_name" {
|
|
157
|
+
description = "Logical name used for the memory resource"
|
|
158
|
+
value = local.memory_name
|
|
159
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
################################################################################
|
|
3
|
+
# create_or_find_memory.sh
|
|
4
|
+
#
|
|
5
|
+
# Idempotent create-or-find for a Bedrock AgentCore Memory resource, plus
|
|
6
|
+
# drift-correction for the strategy list on already-existing resources.
|
|
7
|
+
#
|
|
8
|
+
# Input (stdin, JSON):
|
|
9
|
+
# {"name": "<logical name>", "region": "<aws-region>",
|
|
10
|
+
# "execution_role_arn": "<optional>"}
|
|
11
|
+
# Output (stdout, JSON): {"memory_id": "<resource-id>"}
|
|
12
|
+
#
|
|
13
|
+
# Behavior:
|
|
14
|
+
# 1. Lists existing memories via `aws bedrock-agentcore-control list-memories`
|
|
15
|
+
# and matches by exact `name` OR by ID starting with `name-` (the API
|
|
16
|
+
# uses `{name}-{randomSuffix}` for the resource ID, and `name` sometimes
|
|
17
|
+
# comes back null on existing resources).
|
|
18
|
+
# 2. If a match exists: get-memory, diff its current strategies against the
|
|
19
|
+
# desired set, and call update-memory with addMemoryStrategies for any
|
|
20
|
+
# that are missing. This lets us add new strategies to an existing
|
|
21
|
+
# memory without destructive recreation.
|
|
22
|
+
# 3. If no match: create-memory with the full desired strategy list.
|
|
23
|
+
#
|
|
24
|
+
# Strategy set must match memory.py:STRATEGY_NAMESPACES exactly so the
|
|
25
|
+
# agent container's recall() finds records written by the extractors:
|
|
26
|
+
# semantic -> assistant_{actorId}
|
|
27
|
+
# preferences -> preferences_{actorId}
|
|
28
|
+
# summaries -> session_{sessionId}
|
|
29
|
+
# episodes -> episodes_{actorId}/{sessionId} (built-in episodicMemoryStrategy)
|
|
30
|
+
#
|
|
31
|
+
# Called from terraform/modules/app/agentcore-memory/main.tf via
|
|
32
|
+
# `data "external"`. Keep stdout strictly JSON — any stray echo will break
|
|
33
|
+
# Terraform's JSON parser. All diagnostics go to stderr.
|
|
34
|
+
################################################################################
|
|
35
|
+
|
|
36
|
+
set -euo pipefail
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Input
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
input="$(cat)"
|
|
43
|
+
name="$(echo "$input" | jq -r '.name // empty')"
|
|
44
|
+
region="$(echo "$input" | jq -r '.region // empty')"
|
|
45
|
+
execution_role_arn="$(echo "$input" | jq -r '.execution_role_arn // empty')"
|
|
46
|
+
|
|
47
|
+
if [[ -z "$name" || -z "$region" ]]; then
|
|
48
|
+
echo '{"error": "name and region are required"}' >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Desired strategy set
|
|
54
|
+
#
|
|
55
|
+
# The full list passed to create-memory. Each entry is also a valid item
|
|
56
|
+
# for update-memory's addMemoryStrategies list, so we can reuse the same
|
|
57
|
+
# shape for drift correction. `episodes` uses the built-in
|
|
58
|
+
# `episodicMemoryStrategy` type (NOT customMemoryStrategy — that was the
|
|
59
|
+
# bug that silently dropped episodes on the first deploy).
|
|
60
|
+
#
|
|
61
|
+
# IMPORTANT: episodicMemoryStrategy REQUIRES a reflectionConfiguration whose
|
|
62
|
+
# namespace is a prefix of the episodic namespace. If omitted, the API
|
|
63
|
+
# synthesizes a default reflection namespace of
|
|
64
|
+
# `/strategies/{memoryStrategyId}/actors/{actorId}/` which is NOT a prefix
|
|
65
|
+
# of our flat `episodes_{actorId}/{sessionId}` template, and update-memory
|
|
66
|
+
# fails with ValidationException. We set it to `episodes_{actorId}/` which
|
|
67
|
+
# IS a prefix and gives cross-session reflection records a stable home.
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
strategies_json='[
|
|
71
|
+
{
|
|
72
|
+
"semanticMemoryStrategy": {
|
|
73
|
+
"name": "semantic",
|
|
74
|
+
"namespaces": ["assistant_{actorId}"]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"userPreferenceMemoryStrategy": {
|
|
79
|
+
"name": "preferences",
|
|
80
|
+
"namespaces": ["preferences_{actorId}"]
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"summaryMemoryStrategy": {
|
|
85
|
+
"name": "summaries",
|
|
86
|
+
"namespaces": ["session_{sessionId}"]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"episodicMemoryStrategy": {
|
|
91
|
+
"name": "episodes",
|
|
92
|
+
"namespaces": ["episodes_{actorId}/{sessionId}"],
|
|
93
|
+
"reflectionConfiguration": {
|
|
94
|
+
"namespaces": ["episodes_{actorId}/"]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]'
|
|
99
|
+
|
|
100
|
+
# Map logical strategy name -> the top-level key used in the create/update
|
|
101
|
+
# payload. Used to drift-correct existing memory resources by picking out
|
|
102
|
+
# the entries whose names don't yet exist.
|
|
103
|
+
desired_names=("semantic" "preferences" "summaries" "episodes")
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Step 1: look for an existing memory with this name
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
existing_id="$(
|
|
110
|
+
aws bedrock-agentcore-control list-memories \
|
|
111
|
+
--region "$region" \
|
|
112
|
+
--output json 2>/dev/null \
|
|
113
|
+
| jq -r --arg n "$name" '.memories[]? | select(.name == $n or (.id | startswith($n + "-"))) | .id' \
|
|
114
|
+
| head -n1 || true
|
|
115
|
+
)"
|
|
116
|
+
|
|
117
|
+
if [[ -n "$existing_id" && "$existing_id" != "null" ]]; then
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Step 2a: memory exists — drift-correct its strategy list
|
|
120
|
+
#
|
|
121
|
+
# Fetch current strategies, compute the set of desired strategy names that
|
|
122
|
+
# don't already exist, and call update-memory with addMemoryStrategies for
|
|
123
|
+
# the missing ones. This is idempotent — if everything matches, we call
|
|
124
|
+
# nothing and just return the existing ID.
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
current_names="$(
|
|
127
|
+
aws bedrock-agentcore-control get-memory \
|
|
128
|
+
--region "$region" \
|
|
129
|
+
--memory-id "$existing_id" \
|
|
130
|
+
--output json 2>/dev/null \
|
|
131
|
+
| jq -r '.memory.strategies[]? | .name'
|
|
132
|
+
)"
|
|
133
|
+
|
|
134
|
+
missing=()
|
|
135
|
+
for d in "${desired_names[@]}"; do
|
|
136
|
+
if ! grep -qxF "$d" <<<"$current_names"; then
|
|
137
|
+
missing+=("$d")
|
|
138
|
+
fi
|
|
139
|
+
done
|
|
140
|
+
|
|
141
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
142
|
+
echo "[create_or_find_memory] existing memory $existing_id is missing strategies: ${missing[*]}" >&2
|
|
143
|
+
|
|
144
|
+
# Build the addMemoryStrategies list from the entries in strategies_json
|
|
145
|
+
# whose .name field matches a missing strategy.
|
|
146
|
+
add_json="$(
|
|
147
|
+
echo "$strategies_json" \
|
|
148
|
+
| jq --argjson wanted "$(printf '%s\n' "${missing[@]}" | jq -R . | jq -s .)" '
|
|
149
|
+
map(
|
|
150
|
+
select(
|
|
151
|
+
(.semanticMemoryStrategy.name // .userPreferenceMemoryStrategy.name //
|
|
152
|
+
.summaryMemoryStrategy.name // .episodicMemoryStrategy.name //
|
|
153
|
+
.customMemoryStrategy.name) as $n
|
|
154
|
+
| $wanted | index($n)
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
'
|
|
158
|
+
)"
|
|
159
|
+
|
|
160
|
+
update_payload="$(jq -nc --argjson add "$add_json" '{addMemoryStrategies: $add}')"
|
|
161
|
+
|
|
162
|
+
# update-memory takes memory-strategies as a structured object with
|
|
163
|
+
# add/modify/delete lists.
|
|
164
|
+
if aws bedrock-agentcore-control update-memory \
|
|
165
|
+
--region "$region" \
|
|
166
|
+
--memory-id "$existing_id" \
|
|
167
|
+
--memory-strategies "$update_payload" \
|
|
168
|
+
--output json >/dev/null 2>&1; then
|
|
169
|
+
echo "[create_or_find_memory] added missing strategies to $existing_id" >&2
|
|
170
|
+
else
|
|
171
|
+
# Capture the error for diagnostics but don't fail the whole apply —
|
|
172
|
+
# retention on the existing strategies still works.
|
|
173
|
+
err="$(aws bedrock-agentcore-control update-memory \
|
|
174
|
+
--region "$region" \
|
|
175
|
+
--memory-id "$existing_id" \
|
|
176
|
+
--memory-strategies "$update_payload" 2>&1 || true)"
|
|
177
|
+
echo "[create_or_find_memory] WARNING: update-memory failed: $err" >&2
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
jq -nc --arg id "$existing_id" '{memory_id: $id}'
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Step 2b: no existing memory — create one with the full strategy set
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
role_arg=""
|
|
190
|
+
if [[ -n "$execution_role_arn" ]]; then
|
|
191
|
+
role_arg="--memory-execution-role-arn $execution_role_arn"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
create_output="$(
|
|
195
|
+
aws bedrock-agentcore-control create-memory \
|
|
196
|
+
--region "$region" \
|
|
197
|
+
--name "$name" \
|
|
198
|
+
--memory-strategies "$strategies_json" \
|
|
199
|
+
--event-expiry-duration 365 \
|
|
200
|
+
$role_arg \
|
|
201
|
+
--output json
|
|
202
|
+
)"
|
|
203
|
+
|
|
204
|
+
new_id="$(echo "$create_output" | jq -r '.memory.id // .id')"
|
|
205
|
+
|
|
206
|
+
if [[ -z "$new_id" || "$new_id" == "null" ]]; then
|
|
207
|
+
echo '{"error": "create-memory returned no id"}' >&2
|
|
208
|
+
echo "create-memory output was: $create_output" >&2
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
jq -nc --arg id "$new_id" '{memory_id: $id}'
|
|
@@ -26,20 +26,14 @@ variable "bucket_name" {
|
|
|
26
26
|
type = string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
variable "memory_engine" {
|
|
30
|
-
description = "Memory engine: 'managed' or 'hindsight'. Passed as MEMORY_ENGINE env var to the container."
|
|
31
|
-
type = string
|
|
32
|
-
default = "managed"
|
|
33
|
-
}
|
|
34
|
-
|
|
35
29
|
variable "hindsight_endpoint" {
|
|
36
|
-
description = "Hindsight API endpoint (
|
|
30
|
+
description = "Hindsight API endpoint. Empty string (default) disables Hindsight tools in the container; set to an endpoint URL to enable Hindsight as an add-on alongside the always-on managed memory."
|
|
37
31
|
type = string
|
|
38
32
|
default = ""
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
variable "agentcore_memory_id" {
|
|
42
|
-
description = "AgentCore Memory resource ID
|
|
36
|
+
description = "AgentCore Memory resource ID. Populated automatically by the agentcore-memory module; injected into the container as AGENTCORE_MEMORY_ID for auto-retention."
|
|
43
37
|
type = string
|
|
44
38
|
default = ""
|
|
45
39
|
}
|
|
@@ -120,6 +114,25 @@ resource "aws_iam_role_policy" "agentcore" {
|
|
|
120
114
|
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:InvokeAgent"]
|
|
121
115
|
Resource = "arn:aws:bedrock:${var.region}::foundation-model/*"
|
|
122
116
|
},
|
|
117
|
+
{
|
|
118
|
+
# Automatic memory retention — every agent turn calls CreateEvent
|
|
119
|
+
# to feed AgentCore's background strategies. Also needs read access
|
|
120
|
+
# so the recall() tool can fetch previously extracted records and
|
|
121
|
+
# so forget() can soft-archive old records.
|
|
122
|
+
Sid = "AgentCoreMemoryReadWrite"
|
|
123
|
+
Effect = "Allow"
|
|
124
|
+
Action = [
|
|
125
|
+
"bedrock-agentcore:CreateEvent",
|
|
126
|
+
"bedrock-agentcore:ListEvents",
|
|
127
|
+
"bedrock-agentcore:GetEvent",
|
|
128
|
+
"bedrock-agentcore:ListMemoryRecords",
|
|
129
|
+
"bedrock-agentcore:RetrieveMemoryRecords",
|
|
130
|
+
"bedrock-agentcore:GetMemoryRecord",
|
|
131
|
+
"bedrock-agentcore:BatchCreateMemoryRecords",
|
|
132
|
+
"bedrock-agentcore:BatchUpdateMemoryRecords",
|
|
133
|
+
]
|
|
134
|
+
Resource = "*"
|
|
135
|
+
},
|
|
123
136
|
{
|
|
124
137
|
Sid = "CloudWatchLogs"
|
|
125
138
|
Effect = "Allow"
|
|
@@ -177,7 +190,6 @@ resource "aws_lambda_function" "agentcore" {
|
|
|
177
190
|
variables = {
|
|
178
191
|
PORT = "8080"
|
|
179
192
|
AWS_LWA_PORT = "8080"
|
|
180
|
-
MEMORY_ENGINE = var.memory_engine
|
|
181
193
|
AGENTCORE_MEMORY_ID = var.agentcore_memory_id
|
|
182
194
|
AGENTCORE_FILES_BUCKET = var.bucket_name
|
|
183
195
|
}
|
|
@@ -193,10 +205,9 @@ resource "aws_lambda_function" "agentcore" {
|
|
|
193
205
|
}
|
|
194
206
|
}
|
|
195
207
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
208
|
+
# AgentCore is invoked directly via the Lambda SDK (InvokeCommand) from
|
|
209
|
+
# chat-agent-invoke — no Function URL is needed, and exposing one would be
|
|
210
|
+
# a public attack surface for prompt injection.
|
|
200
211
|
|
|
201
212
|
################################################################################
|
|
202
213
|
# Outputs
|
|
@@ -212,12 +223,12 @@ output "execution_role_arn" {
|
|
|
212
223
|
value = aws_iam_role.agentcore.arn
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
output "agentcore_invoke_url" {
|
|
216
|
-
description = "Lambda Function URL for the AgentCore container"
|
|
217
|
-
value = aws_lambda_function_url.agentcore.function_url
|
|
218
|
-
}
|
|
219
|
-
|
|
220
226
|
output "agentcore_function_name" {
|
|
221
227
|
description = "AgentCore Lambda function name (for direct SDK invoke)"
|
|
222
228
|
value = aws_lambda_function.agentcore.function_name
|
|
223
229
|
}
|
|
230
|
+
|
|
231
|
+
output "agentcore_function_arn" {
|
|
232
|
+
description = "AgentCore Lambda function ARN (for IAM policy on callers)"
|
|
233
|
+
value = aws_lambda_function.agentcore.arn
|
|
234
|
+
}
|