thinkwork-cli 0.5.2 → 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/dist/cli.js +385 -51
- 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
|
@@ -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
|
+
}
|
|
@@ -1,66 +1,87 @@
|
|
|
1
|
-
# Memory
|
|
1
|
+
# Hindsight Memory Module (optional add-on)
|
|
2
2
|
|
|
3
|
-
Thinkwork
|
|
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
|
-
##
|
|
8
|
+
## Memory layers
|
|
6
9
|
|
|
7
|
-
|
|
|
8
|
-
|
|
9
|
-
|
|
|
10
|
-
|
|
|
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
|
-
|
|
17
|
+
## How retention works
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
24
|
+
- `assistant_{actorId}` — semantic facts
|
|
25
|
+
- `preferences_{actorId}` — user preferences
|
|
26
|
+
- `session_{sessionId}` — session summaries
|
|
27
|
+
- `episodes_{actorId}/{sessionId}` — episodic memory
|
|
18
28
|
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
###
|
|
40
|
+
### Default — managed memory only (zero config)
|
|
28
41
|
|
|
29
42
|
```hcl
|
|
30
43
|
module "thinkwork" {
|
|
31
44
|
source = "thinkwork-ai/thinkwork/aws"
|
|
32
45
|
|
|
33
|
-
stage
|
|
34
|
-
#
|
|
46
|
+
stage = "prod"
|
|
47
|
+
# enable_hindsight defaults to false — nothing else to set
|
|
35
48
|
}
|
|
36
49
|
```
|
|
37
50
|
|
|
38
|
-
|
|
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
|
|
45
|
-
|
|
60
|
+
stage = "prod"
|
|
61
|
+
enable_hindsight = true
|
|
46
62
|
|
|
47
63
|
# Optional: pin the Hindsight image version
|
|
48
|
-
# hindsight_image_tag = "0.
|
|
64
|
+
# hindsight_image_tag = "0.5.0"
|
|
49
65
|
}
|
|
50
66
|
```
|
|
51
67
|
|
|
52
|
-
When `
|
|
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
|
-
|
|
74
|
+
Cost: ~$75/mo (ARM64 Fargate + ALB hours).
|
|
75
|
+
|
|
76
|
+
## Turning the add-on on/off
|
|
59
77
|
|
|
60
|
-
|
|
78
|
+
Toggling `enable_hindsight` and re-running `thinkwork deploy`:
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
86
|
+
Memory data is not migrated between backends. Hindsight records and
|
|
87
|
+
AgentCore records live in separate stores.
|