thinkwork-cli 0.5.2 → 0.5.4

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.
@@ -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 (only used when memory_engine = 'hindsight')"
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 (only used when memory_engine = 'managed')"
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
- resource "aws_lambda_function_url" "agentcore" {
197
- function_name = aws_lambda_function.agentcore.function_name
198
- authorization_type = "NONE"
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
+ }
@@ -1,66 +1,87 @@
1
- # Memory Engine Module
1
+ # Hindsight Memory Module (optional add-on)
2
2
 
3
- Thinkwork supports pluggable long-term memory for agents. Choose the engine that fits your stage and requirements.
3
+ Thinkwork has two long-term memory systems. **AgentCore managed memory is
4
+ always on** — every agent gets automatic per-turn retention out of the box
5
+ with zero configuration. **Hindsight is an optional add-on** you can layer
6
+ on top for advanced semantic + entity-graph retrieval.
4
7
 
5
- ## Engines
8
+ ## Memory layers
6
9
 
7
- | Engine | `memory_engine` value | Best for | Extra infra | Cost |
8
- |--------|----------------------|----------|-------------|------|
9
- | **AgentCore Managed** | `managed` (default) | All stages. Production-ready, zero-config. | None | $0 additional |
10
- | **Hindsight** | `hindsight` | Advanced recall/reflect with entity graph, cross-encoder reranking. | ECS Fargate + ALB | ~$75/mo (ARM64) |
10
+ | Layer | Backend | Always on | What it stores |
11
+ |-------|---------|-----------|----------------|
12
+ | 1. Workspace files | S3 per-agent | Yes | Scratchpad, working files |
13
+ | 2. Thread history | Aurora `messages` table | Yes | Last 30 turns per thread |
14
+ | 3a. Managed long-term | AgentCore Memory | **Yes** | Semantic facts, preferences, summaries, episodes — extracted automatically from every turn |
15
+ | 3b. Hindsight long-term | Hindsight ECS service | **Optional** | Same purpose as 3a, plus entity graph + BM25 + cross-encoder reranking |
11
16
 
12
- Both engines provide agents with memory tools. The Strands runtime reads `MEMORY_ENGINE` and loads the right tool set:
17
+ ## How retention works
13
18
 
14
- - **Managed**: `remember()`, `recall()`, `forget()` backed by AgentCore Memory API with 4 strategies (semantic facts, user preferences, session summaries, episodic).
15
- - **Hindsight**: `hindsight_retain()`, `hindsight_recall()`, `hindsight_reflect()` backed by the Hindsight service with semantic + BM25 + entity graph + temporal retrieval and cross-encoder reranking.
19
+ The agent container automatically emits a `CreateEvent` into AgentCore
20
+ Memory after every turn (user message + assistant response), via
21
+ `memory.store_turn_pair` in `packages/agentcore/agent-container/memory.py`.
22
+ AgentCore's background strategies extract facts into four namespaces:
16
23
 
17
- ## What's pluggable
24
+ - `assistant_{actorId}` — semantic facts
25
+ - `preferences_{actorId}` — user preferences
26
+ - `session_{sessionId}` — session summaries
27
+ - `episodes_{actorId}/{sessionId}` — episodic memory
18
28
 
19
- Only **Layer 3 (long-term cross-thread memory)** is pluggable. The other memory layers are core infrastructure:
29
+ The agent reads them back via the `recall()` tool. There is no need for
30
+ the model to call `remember()` for routine facts — it only exists for
31
+ user-driven "please remember X" requests.
20
32
 
21
- - **Layer 1** Workspace files on S3 (per-agent scratchpad). Always available.
22
- - **Layer 2** Thread history in Aurora (conversation memory). Always available.
23
- - **Layer 3** Long-term cross-thread recall. **This is what `memory_engine` controls.**
33
+ When Hindsight is enabled, the container ALSO registers
34
+ `hindsight_retain`, `hindsight_recall`, and `hindsight_reflect` tools that
35
+ route to the Hindsight service. `remember()` dual-writes to both backends
36
+ so explicit memories land in both systems.
24
37
 
25
38
  ## Usage
26
39
 
27
- ### Managed (default no extra config needed)
40
+ ### Defaultmanaged memory only (zero config)
28
41
 
29
42
  ```hcl
30
43
  module "thinkwork" {
31
44
  source = "thinkwork-ai/thinkwork/aws"
32
45
 
33
- stage = "prod"
34
- # memory_engine defaults to "managed" — nothing to set
46
+ stage = "prod"
47
+ # enable_hindsight defaults to false — nothing else to set
35
48
  }
36
49
  ```
37
50
 
38
- ### Hindsight (opt-in)
51
+ The `terraform/modules/app/agentcore-memory` module is always instantiated
52
+ and provisions the AgentCore Memory resource with the four strategies.
53
+
54
+ ### With the Hindsight add-on
39
55
 
40
56
  ```hcl
41
57
  module "thinkwork" {
42
58
  source = "thinkwork-ai/thinkwork/aws"
43
59
 
44
- stage = "prod"
45
- memory_engine = "hindsight"
60
+ stage = "prod"
61
+ enable_hindsight = true
46
62
 
47
63
  # Optional: pin the Hindsight image version
48
- # hindsight_image_tag = "0.4.22"
64
+ # hindsight_image_tag = "0.5.0"
49
65
  }
50
66
  ```
51
67
 
52
- When `memory_engine = "hindsight"`, Terraform creates:
68
+ When `enable_hindsight = true`, Terraform creates:
53
69
  - ECS Fargate cluster + service (ARM64, 2 vCPU, 4 GB)
54
70
  - Application Load Balancer
55
71
  - Security groups (ALB → Hindsight → Aurora ingress)
56
72
  - CloudWatch log group
57
73
 
58
- When `memory_engine = "managed"`, none of the above is created.
74
+ Cost: ~$75/mo (ARM64 Fargate + ALB hours).
75
+
76
+ ## Turning the add-on on/off
59
77
 
60
- ## Switching engines
78
+ Toggling `enable_hindsight` and re-running `thinkwork deploy`:
61
79
 
62
- Changing `memory_engine` and running `thinkwork deploy` will:
63
- - **managed hindsight**: Create the ECS/ALB infrastructure. Agents start using Hindsight tools on next invoke.
64
- - **hindsight → managed**: Destroy the ECS/ALB infrastructure. Agents fall back to AgentCore memory tools.
80
+ - **false true**: creates the ECS + ALB infra. Agents gain the three
81
+ `hindsight_*` tools on their next invoke. Managed retention continues
82
+ running unchanged.
83
+ - **true → false**: destroys the ECS + ALB infra. Agents lose the
84
+ `hindsight_*` tools. Managed memory keeps working as before.
65
85
 
66
- Memory data is not migrated between engines. Each engine has its own storage backend.
86
+ Memory data is not migrated between backends. Hindsight records and
87
+ AgentCore records live in separate stores.